mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
* Python: Add OpenTelemetry instrumentation to ClaudeAgent (#4278) Add inline telemetry to ClaudeAgent.run() so that enable_instrumentation() emits invoke_agent spans and metrics. Covers both streaming and non-streaming paths using the same observability helpers as AgentTelemetryLayer. Adds 5 unit tests for telemetry behavior. Co-Authored-By: amitmukh <amimukherjee@microsoft.com> * Address PR review feedback for ClaudeAgent telemetry - Add justification comment for private observability API imports - Pass system_instructions to capture_messages for system prompt capture - Use monkeypatch instead of try/finally for test global state isolation Co-Authored-By: amitmukh <amitmukh@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com> * Adopt AgentTelemetryLayer instead of inline telemetry Restructure ClaudeAgent to inherit from AgentTelemetryLayer via a _ClaudeAgentRunImpl mixin, eliminating duplicated telemetry code and private API imports. MRO: ClaudeAgent → AgentTelemetryLayer → _ClaudeAgentRunImpl → BaseAgent - Remove inline _run_with_telemetry / _run_with_telemetry_stream methods - Remove private observability helper imports (_capture_messages, etc.) - Add default_options property mapping system_prompt → instructions - Net -105 lines by reusing core telemetry layer Co-Authored-By: amitmukh <amitmukh@users.noreply.github.com> Co-Authored-By: Claude <noreply@anthropic.com> * Fix mypy: align _ClaudeAgentRunImpl.run() signature with AgentTelemetryLayer.run() Remove explicit `options` parameter from mixin's run() signature and extract it from **kwargs to match AgentTelemetryLayer's signature. Also align overload return types (ResponseStream, Awaitable) to match. Co-Authored-By: Claude <noreply@anthropic.com> * Introduce RawClaudeAgent following framework's RawAgent/Agent pattern Replace private _ClaudeAgentRunImpl mixin with public RawClaudeAgent class that contains all core logic (init, run, lifecycle, tools). ClaudeAgent becomes a thin wrapper that adds AgentTelemetryLayer. - RawClaudeAgent(BaseAgent): full implementation without telemetry - ClaudeAgent(AgentTelemetryLayer, RawClaudeAgent): adds OTel tracing - Export RawClaudeAgent from package __init__.py Users who want to skip telemetry or provide their own can use RawClaudeAgent directly. Co-Authored-By: Claude <noreply@anthropic.com> * Address review nits: trim RawClaudeAgent docstring, fix import paths - Simplify RawClaudeAgent docstring to a single basic example (not the primary entry point for most users) - Use agent_framework.anthropic import path in docstrings instead of direct agent_framework_claude path - Add RawClaudeAgent to agent_framework.anthropic lazy re-exports Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Amit Mukherjee <amimukherjee@microsoft.com> Co-authored-by: amitmukh <amitmukh@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude
Amit Mukherjee
amitmukh
Dmytro Struk
parent
c5ed8209df
commit
fae36b36f2
@@ -2,7 +2,7 @@
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
from ._agent import ClaudeAgent, ClaudeAgentOptions, ClaudeAgentSettings
|
||||
from ._agent import ClaudeAgent, ClaudeAgentOptions, ClaudeAgentSettings, RawClaudeAgent
|
||||
|
||||
try:
|
||||
__version__ = importlib.metadata.version(__name__)
|
||||
@@ -13,5 +13,6 @@ __all__ = [
|
||||
"ClaudeAgent",
|
||||
"ClaudeAgentOptions",
|
||||
"ClaudeAgentSettings",
|
||||
"RawClaudeAgent",
|
||||
"__version__",
|
||||
]
|
||||
|
||||
@@ -27,6 +27,7 @@ from agent_framework import (
|
||||
normalize_tools,
|
||||
)
|
||||
from agent_framework.exceptions import AgentException
|
||||
from agent_framework.observability import AgentTelemetryLayer
|
||||
from claude_agent_sdk import (
|
||||
AssistantMessage,
|
||||
ClaudeSDKClient,
|
||||
@@ -171,8 +172,11 @@ OptionsT = TypeVar(
|
||||
)
|
||||
|
||||
|
||||
class ClaudeAgent(BaseAgent, Generic[OptionsT]):
|
||||
"""Claude Agent using Claude Code CLI.
|
||||
class RawClaudeAgent(BaseAgent, Generic[OptionsT]):
|
||||
"""Claude Agent using Claude Code CLI without telemetry layers.
|
||||
|
||||
This is the core Claude agent implementation without OpenTelemetry instrumentation.
|
||||
For most use cases, prefer :class:`ClaudeAgent` which includes telemetry support.
|
||||
|
||||
Wraps the Claude Agent SDK to provide agentic capabilities including
|
||||
tool use, session management, and streaming responses.
|
||||
@@ -188,45 +192,13 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework_claude import ClaudeAgent
|
||||
from agent_framework.anthropic import RawClaudeAgent
|
||||
|
||||
async with ClaudeAgent(
|
||||
async with RawClaudeAgent(
|
||||
instructions="You are a helpful assistant.",
|
||||
) as agent:
|
||||
response = await agent.run("Hello!")
|
||||
print(response.text)
|
||||
|
||||
With streaming:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async with ClaudeAgent() as agent:
|
||||
async for update in agent.run("Write a poem"):
|
||||
print(update.text, end="", flush=True)
|
||||
|
||||
With session management:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async with ClaudeAgent() as agent:
|
||||
session = agent.create_session()
|
||||
await agent.run("Remember my name is Alice", session=session)
|
||||
response = await agent.run("What's my name?", session=session)
|
||||
# Claude will remember "Alice" from the same session
|
||||
|
||||
With Agent Framework tools:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import tool
|
||||
|
||||
@tool
|
||||
def greet(name: str) -> str:
|
||||
\"\"\"Greet someone by name.\"\"\"
|
||||
return f"Hello, {name}!"
|
||||
|
||||
async with ClaudeAgent(tools=[greet]) as agent:
|
||||
response = await agent.run("Greet Alice")
|
||||
"""
|
||||
|
||||
AGENT_PROVIDER_NAME: ClassVar[str] = "anthropic.claude"
|
||||
@@ -246,7 +218,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize a ClaudeAgent instance.
|
||||
"""Initialize a RawClaudeAgent instance.
|
||||
|
||||
Args:
|
||||
instructions: System prompt for the agent.
|
||||
@@ -343,7 +315,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
|
||||
normalized = normalize_tools(tool)
|
||||
self._custom_tools.extend(normalized)
|
||||
|
||||
async def __aenter__(self) -> ClaudeAgent[OptionsT]:
|
||||
async def __aenter__(self) -> RawClaudeAgent[OptionsT]:
|
||||
"""Start the agent when entering async context."""
|
||||
await self.start()
|
||||
return self
|
||||
@@ -568,61 +540,19 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
|
||||
return ""
|
||||
return "\n".join([msg.text or "" for msg in messages])
|
||||
|
||||
@overload
|
||||
def run(
|
||||
self,
|
||||
messages: AgentRunInputs | None = None,
|
||||
*,
|
||||
stream: Literal[True],
|
||||
session: AgentSession | None = None,
|
||||
options: OptionsT | MutableMapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[AgentResponseUpdate]: ...
|
||||
@property
|
||||
def default_options(self) -> dict[str, Any]:
|
||||
"""Expose options with ``instructions`` key.
|
||||
|
||||
@overload
|
||||
async def run(
|
||||
self,
|
||||
messages: AgentRunInputs | None = None,
|
||||
*,
|
||||
stream: Literal[False] = ...,
|
||||
session: AgentSession | None = None,
|
||||
options: OptionsT | MutableMapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AgentResponse[Any]: ...
|
||||
|
||||
def run(
|
||||
self,
|
||||
messages: AgentRunInputs | None = None,
|
||||
*,
|
||||
stream: bool = False,
|
||||
session: AgentSession | None = None,
|
||||
options: OptionsT | MutableMapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[AgentResponseUpdate] | Awaitable[AgentResponse[Any]]:
|
||||
"""Run the agent with the given messages.
|
||||
|
||||
Args:
|
||||
messages: The messages to process.
|
||||
|
||||
Keyword Args:
|
||||
stream: If True, returns an async iterable of updates. If False (default),
|
||||
returns an awaitable AgentResponse.
|
||||
session: The conversation session. If session has service_session_id set,
|
||||
the agent will resume that session.
|
||||
options: Runtime options (model, permission_mode can be changed per-request).
|
||||
kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
When stream=True: An ResponseStream for streaming updates.
|
||||
When stream=False: An Awaitable[AgentResponse] with the complete response.
|
||||
Maps ``system_prompt`` to ``instructions`` for compatibility with
|
||||
:class:`AgentTelemetryLayer`, which reads the system prompt from
|
||||
the ``instructions`` key.
|
||||
"""
|
||||
response = ResponseStream(
|
||||
self._get_stream(messages, session=session, options=options, **kwargs),
|
||||
finalizer=self._finalize_response,
|
||||
)
|
||||
if stream:
|
||||
return response
|
||||
return response.get_final_response()
|
||||
opts = dict(self._default_options)
|
||||
system_prompt = opts.pop("system_prompt", None)
|
||||
if system_prompt is not None:
|
||||
opts["instructions"] = system_prompt
|
||||
return opts
|
||||
|
||||
def _finalize_response(self, updates: Sequence[AgentResponseUpdate]) -> AgentResponse[Any]:
|
||||
"""Build AgentResponse and propagate structured_output as value.
|
||||
@@ -636,6 +566,61 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
|
||||
structured_output = getattr(self, "_structured_output", None)
|
||||
return AgentResponse.from_updates(updates, value=structured_output)
|
||||
|
||||
@overload
|
||||
def run(
|
||||
self,
|
||||
messages: AgentRunInputs | None = None,
|
||||
*,
|
||||
stream: Literal[False] = ...,
|
||||
session: AgentSession | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[Any]]: ...
|
||||
|
||||
@overload
|
||||
def run(
|
||||
self,
|
||||
messages: AgentRunInputs | None = None,
|
||||
*,
|
||||
stream: Literal[True],
|
||||
session: AgentSession | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
|
||||
|
||||
def run(
|
||||
self,
|
||||
messages: AgentRunInputs | None = None,
|
||||
*,
|
||||
stream: bool = False,
|
||||
session: AgentSession | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
|
||||
"""Run the agent with the given messages.
|
||||
|
||||
Args:
|
||||
messages: The messages to process.
|
||||
|
||||
Keyword Args:
|
||||
stream: If True, returns an async iterable of updates. If False (default),
|
||||
returns an awaitable AgentResponse.
|
||||
session: The conversation session. If session has service_session_id set,
|
||||
the agent will resume that session.
|
||||
kwargs: Additional keyword arguments including 'options' for runtime options
|
||||
(model, permission_mode can be changed per-request).
|
||||
|
||||
Returns:
|
||||
When stream=True: An ResponseStream for streaming updates.
|
||||
When stream=False: An Awaitable[AgentResponse] with the complete response.
|
||||
"""
|
||||
options = kwargs.pop("options", None)
|
||||
response = ResponseStream(
|
||||
self._get_stream(messages, session=session, options=options, **kwargs),
|
||||
finalizer=self._finalize_response,
|
||||
)
|
||||
|
||||
if stream:
|
||||
return response
|
||||
return response.get_final_response()
|
||||
|
||||
async def _get_stream(
|
||||
self,
|
||||
messages: AgentRunInputs | None = None,
|
||||
@@ -721,3 +706,25 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
|
||||
|
||||
# Store structured output for the finalizer
|
||||
self._structured_output = structured_output
|
||||
|
||||
|
||||
class ClaudeAgent(AgentTelemetryLayer, RawClaudeAgent[OptionsT], Generic[OptionsT]):
|
||||
"""Claude Agent with OpenTelemetry instrumentation.
|
||||
|
||||
This is the recommended agent class for most use cases. It includes
|
||||
OpenTelemetry-based telemetry for observability. For a minimal
|
||||
implementation without telemetry, use :class:`RawClaudeAgent`.
|
||||
|
||||
Examples:
|
||||
Basic usage with context manager:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.anthropic import ClaudeAgent
|
||||
|
||||
async with ClaudeAgent(
|
||||
instructions="You are a helpful assistant.",
|
||||
) as agent:
|
||||
response = await agent.run("Hello!")
|
||||
print(response.text)
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user