Python: Add BaseAgent implementation for Claude Agent SDK (#3509)

* Added ClaudeAgent implementation

* Updated streaming logic

* Small updates

* Small update

* Fixes

* Small fix

* Naming improvements

* Updated imports

* Addressed comments

* Updated package versions
This commit is contained in:
Dmytro Struk
2026-01-30 10:11:41 -08:00
committed by GitHub
Unverified
parent 0fcf075ea7
commit 8b475afe17
39 changed files with 2180 additions and 52 deletions
+17 -1
View File
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.0.0b260130] - 2026-01-30
### Added
- **agent-framework-claude**: Add BaseAgent implementation for Claude Agent SDK ([#3509](https://github.com/microsoft/agent-framework/pull/3509))
- **agent-framework-core**: Add core types and agents unit tests ([#3470](https://github.com/microsoft/agent-framework/pull/3470))
- **agent-framework-core**: Add core utilities unit tests ([#3487](https://github.com/microsoft/agent-framework/pull/3487))
- **agent-framework-core**: Add observability unit tests to improve coverage ([#3469](https://github.com/microsoft/agent-framework/pull/3469))
- **agent-framework-azure-ai**: Improved AzureAI package test coverage ([#3452](https://github.com/microsoft/agent-framework/pull/3452))
### Changed
- **agent-framework-core**: Added generic types to `ChatOptions` and `ChatResponse`/`AgentResponse` for Response Format ([#3305](https://github.com/microsoft/agent-framework/pull/3305))
- **agent-framework-durabletask**: Update durabletask package ([#3492](https://github.com/microsoft/agent-framework/pull/3492))
## [1.0.0b260128] - 2026-01-28
### Changed
@@ -556,7 +571,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
For more information, see the [announcement blog post](https://devblogs.microsoft.com/foundry/introducing-microsoft-agent-framework-the-open-source-engine-for-agentic-ai-apps/).
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260128...HEAD
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260130...HEAD
[1.0.0b260130]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260128...python-1.0.0b260130
[1.0.0b260128]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260127...python-1.0.0b260128
[1.0.0b260127]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260123...python-1.0.0b260127
[1.0.0b260123]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260116...python-1.0.0b260123
+1 -1
View File
@@ -4,7 +4,7 @@ description = "A2A integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "agent-framework-ag-ui"
version = "1.0.0b260128"
version = "1.0.0b260130"
description = "AG-UI protocol integration for Agent Framework"
readme = "README.md"
license-files = ["LICENSE"]
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Anthropic integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -4,7 +4,7 @@ description = "Azure AI Search integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Azure AI Foundry integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -4,7 +4,7 @@ description = "Azure Functions integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Amazon Bedrock integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "OpenAI ChatKit integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
+11
View File
@@ -0,0 +1,11 @@
# Get Started with Microsoft Agent Framework Claude
Please install this package via pip:
```bash
pip install agent-framework-claude --pre
```
## Claude Agent
The Claude agent enables integration with Claude Agent SDK, allowing you to interact with Claude's agentic capabilities through the Agent Framework.
@@ -0,0 +1,18 @@
# Copyright (c) Microsoft. All rights reserved.
import importlib.metadata
from ._agent import ClaudeAgent, ClaudeAgentOptions
from ._settings import ClaudeAgentSettings
try:
__version__ = importlib.metadata.version(__name__)
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0" # Fallback for development mode
__all__ = [
"ClaudeAgent",
"ClaudeAgentOptions",
"ClaudeAgentSettings",
"__version__",
]
@@ -0,0 +1,645 @@
# Copyright (c) Microsoft. All rights reserved.
import contextlib
import sys
from collections.abc import AsyncIterable, Callable, MutableMapping, Sequence
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, Generic
from agent_framework import (
AgentMiddlewareTypes,
AgentResponse,
AgentResponseUpdate,
AgentThread,
BaseAgent,
ChatMessage,
Content,
ContextProvider,
FunctionTool,
Role,
ToolProtocol,
get_logger,
normalize_messages,
)
from agent_framework._types import normalize_tools
from agent_framework.exceptions import ServiceException, ServiceInitializationError
from claude_agent_sdk import (
ClaudeAgentOptions as SDKOptions,
)
from claude_agent_sdk import (
ClaudeSDKClient,
ResultMessage,
SdkMcpTool,
create_sdk_mcp_server,
)
from claude_agent_sdk.types import StreamEvent
from pydantic import ValidationError
from ._settings import ClaudeAgentSettings
if sys.version_info >= (3, 13):
from typing import TypeVar # type: ignore # pragma: no cover
else:
from typing_extensions import TypeVar # type: ignore # pragma: no cover
if sys.version_info >= (3, 11):
from typing import TypedDict # pragma: no cover
else:
from typing_extensions import TypedDict # pragma: no cover
if TYPE_CHECKING:
from claude_agent_sdk import (
AgentDefinition,
CanUseTool,
HookMatcher,
McpServerConfig,
PermissionMode,
SandboxSettings,
SdkBeta,
)
__all__ = ["ClaudeAgent", "ClaudeAgentOptions"]
logger = get_logger("agent_framework.claude")
# Name of the in-process MCP server that hosts Agent Framework tools.
# FunctionTool instances are converted to SDK MCP tools and served
# through this server, as Claude Code CLI only supports tools via MCP.
TOOLS_MCP_SERVER_NAME = "_agent_framework_tools"
class ClaudeAgentOptions(TypedDict, total=False):
"""Claude Agent-specific options."""
system_prompt: str
"""System prompt for the agent."""
cli_path: str | Path
"""Path to Claude CLI executable. Default: auto-detected."""
cwd: str | Path
"""Working directory for Claude CLI. Default: current working directory."""
env: dict[str, str]
"""Environment variables to pass to CLI."""
settings: str
"""Path to Claude settings file."""
model: str
"""Model to use ("sonnet", "opus", "haiku"). Default: "sonnet"."""
fallback_model: str
"""Fallback model if primary fails."""
max_thinking_tokens: int
"""Maximum tokens for thinking blocks."""
allowed_tools: list[str]
"""Allowlist of tools. If set, Claude can ONLY use tools in this list."""
disallowed_tools: list[str]
"""Blocklist of tools. Claude cannot use these tools."""
mcp_servers: dict[str, "McpServerConfig"]
"""MCP server configurations for external tools."""
permission_mode: "PermissionMode"
"""Permission handling mode ("default", "acceptEdits", "plan", "bypassPermissions")."""
can_use_tool: "CanUseTool"
"""Permission callback for tool use."""
max_turns: int
"""Maximum conversation turns."""
max_budget_usd: float
"""Budget limit in USD."""
hooks: dict[str, list["HookMatcher"]]
"""Pre/post tool hooks."""
add_dirs: list[str | Path]
"""Additional directories to add to context."""
sandbox: "SandboxSettings"
"""Sandbox configuration for bash isolation."""
agents: dict[str, "AgentDefinition"]
"""Custom agent definitions."""
output_format: dict[str, Any]
"""Structured output format (JSON schema)."""
enable_file_checkpointing: bool
"""Enable file checkpointing for rewind."""
betas: list["SdkBeta"]
"""Beta features to enable."""
TOptions = TypeVar(
"TOptions",
bound=TypedDict, # type: ignore[valid-type]
default="ClaudeAgentOptions",
covariant=True,
)
class ClaudeAgent(BaseAgent, Generic[TOptions]):
"""Claude Agent using Claude Code CLI.
Wraps the Claude Agent SDK to provide agentic capabilities including
tool use, session management, and streaming responses.
This agent communicates with Claude through the Claude Code CLI,
enabling access to Claude's full agentic capabilities like file
editing, code execution, and tool use.
The agent can be used as an async context manager to ensure proper cleanup:
Examples:
Basic usage with context manager:
.. code-block:: python
from agent_framework_claude import ClaudeAgent
async with ClaudeAgent(
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_stream("Write a poem"):
print(update.text, end="", flush=True)
With session management:
.. code-block:: python
async with ClaudeAgent() as agent:
thread = agent.get_new_thread()
await agent.run("Remember my name is Alice", thread=thread)
response = await agent.run("What's my name?", thread=thread)
# 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"
def __init__(
self,
instructions: str | None = None,
*,
client: ClaudeSDKClient | None = None,
id: str | None = None,
name: str | None = None,
description: str | None = None,
context_provider: ContextProvider | None = None,
middleware: Sequence[AgentMiddlewareTypes] | None = None,
tools: ToolProtocol
| Callable[..., Any]
| MutableMapping[str, Any]
| str
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] | str]
| None = None,
default_options: TOptions | MutableMapping[str, Any] | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize a ClaudeAgent instance.
Args:
instructions: System prompt for the agent.
Keyword Args:
client: Optional pre-configured ClaudeSDKClient instance. If not provided,
a new client will be created using the other parameters.
id: Unique identifier for the agent.
name: Name of the agent.
description: Description of the agent.
context_provider: Context provider for the agent.
middleware: List of middleware.
tools: Tools for the agent. Can be:
- Strings for built-in tools (e.g., "Read", "Write", "Bash", "Glob")
- Functions or ToolProtocol instances for custom tools
default_options: Default ClaudeAgentOptions including system_prompt, model, etc.
env_file_path: Path to .env file.
env_file_encoding: Encoding of .env file.
"""
super().__init__(
id=id,
name=name,
description=description,
context_provider=context_provider,
middleware=middleware,
)
self._client = client
self._owns_client = client is None
# Parse options
opts: dict[str, Any] = dict(default_options) if default_options else {}
# Handle instructions parameter - set as system_prompt in options
if instructions is not None:
opts["system_prompt"] = instructions
cli_path = opts.pop("cli_path", None)
model = opts.pop("model", None)
cwd = opts.pop("cwd", None)
permission_mode = opts.pop("permission_mode", None)
max_turns = opts.pop("max_turns", None)
max_budget_usd = opts.pop("max_budget_usd", None)
self._mcp_servers: dict[str, Any] = opts.pop("mcp_servers", None) or {}
# Load settings from environment and options
try:
self._settings = ClaudeAgentSettings(
cli_path=cli_path,
model=model,
cwd=cwd,
permission_mode=permission_mode,
max_turns=max_turns,
max_budget_usd=max_budget_usd,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
except ValidationError as ex:
raise ServiceInitializationError("Failed to create Claude Agent settings.", ex) from ex
# Separate built-in tools (strings) from custom tools (callables/ToolProtocol)
self._builtin_tools: list[str] = []
self._custom_tools: list[ToolProtocol | MutableMapping[str, Any]] = []
self._normalize_tools(tools)
self._default_options = opts
self._started = False
self._current_session_id: str | None = None
def _normalize_tools(
self,
tools: ToolProtocol
| Callable[..., Any]
| MutableMapping[str, Any]
| str
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] | str]
| None,
) -> None:
"""Separate built-in tools (strings) from custom tools.
Args:
tools: Mixed list of tool names and custom tools.
"""
if tools is None:
return
# Normalize to sequence
if isinstance(tools, str):
tools_list: Sequence[Any] = [tools]
elif isinstance(tools, (ToolProtocol, MutableMapping)) or callable(tools):
tools_list = [tools]
else:
tools_list = list(tools)
for tool in tools_list:
if isinstance(tool, str):
self._builtin_tools.append(tool)
else:
# Use normalize_tools for custom tools
normalized = normalize_tools(tool)
self._custom_tools.extend(normalized)
async def __aenter__(self) -> "ClaudeAgent[TOptions]":
"""Start the agent when entering async context."""
await self.start()
return self
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
"""Stop the agent when exiting async context."""
await self.stop()
async def start(self) -> None:
"""Start the Claude SDK client.
This method initializes the Claude SDK client and establishes a connection
to the Claude Code CLI. It is called automatically when using the agent
as an async context manager.
Raises:
ServiceException: If the client fails to start.
"""
await self._ensure_session()
async def stop(self) -> None:
"""Stop the Claude SDK client and clean up resources.
Stops the client if owned by this agent. Called automatically when
using the agent as an async context manager.
"""
if self._client and self._owns_client:
with contextlib.suppress(Exception):
await self._client.disconnect()
self._started = False
self._current_session_id = None
async def _ensure_session(self, session_id: str | None = None) -> None:
"""Ensure the client is connected for the specified session.
If the requested session differs from the current one, recreates the client.
Args:
session_id: The session ID to use, or None for a new session.
"""
needs_new_client = (
not self._started or self._client is None or (session_id and session_id != self._current_session_id)
)
if needs_new_client:
# Stop existing client if any
if self._client and self._owns_client:
with contextlib.suppress(Exception):
await self._client.disconnect()
self._started = False
# Create new client with resume option if needed
opts = self._prepare_client_options(resume_session_id=session_id)
self._client = ClaudeSDKClient(options=opts)
self._owns_client = True
try:
await self._client.connect()
self._started = True
self._current_session_id = session_id
except Exception as ex:
self._client = None
raise ServiceException(f"Failed to start Claude SDK client: {ex}") from ex
def _prepare_client_options(self, resume_session_id: str | None = None) -> SDKOptions:
"""Prepare SDK options for client initialization.
Args:
resume_session_id: Optional session ID to resume.
Returns:
SDKOptions instance configured for the client.
"""
opts: dict[str, Any] = {}
# Set resume option if provided
if resume_session_id:
opts["resume"] = resume_session_id
# Apply settings from environment
if self._settings.cli_path:
opts["cli_path"] = self._settings.cli_path
if self._settings.model:
opts["model"] = self._settings.model
if self._settings.cwd:
opts["cwd"] = self._settings.cwd
if self._settings.permission_mode:
opts["permission_mode"] = self._settings.permission_mode
if self._settings.max_turns:
opts["max_turns"] = self._settings.max_turns
if self._settings.max_budget_usd:
opts["max_budget_usd"] = self._settings.max_budget_usd
# Apply default options
for key, value in self._default_options.items():
if value is not None:
opts[key] = value
# Add built-in tools (strings like "Read", "Write", "Bash")
if self._builtin_tools:
opts["tools"] = self._builtin_tools
# Prepare custom tools (FunctionTool instances)
custom_tools_server, custom_tool_names = (
self._prepare_tools(self._custom_tools) if self._custom_tools else (None, [])
)
# MCP servers - merge user-provided servers with custom tools server
mcp_servers = dict(self._mcp_servers) if self._mcp_servers else {}
if custom_tools_server:
mcp_servers[TOOLS_MCP_SERVER_NAME] = custom_tools_server
if mcp_servers:
opts["mcp_servers"] = mcp_servers
# Add custom tools to allowed_tools so they can be executed
if custom_tool_names:
existing_allowed = opts.get("allowed_tools", [])
opts["allowed_tools"] = list(existing_allowed) + custom_tool_names
# Always enable partial messages for streaming support
opts["include_partial_messages"] = True
return SDKOptions(**opts)
def _prepare_tools(
self,
tools: list[ToolProtocol | MutableMapping[str, Any]],
) -> tuple[Any, list[str]]:
"""Convert Agent Framework tools to SDK MCP server.
Args:
tools: List of Agent Framework tools.
Returns:
Tuple of (MCP server config, list of allowed tool names).
"""
sdk_tools: list[SdkMcpTool[Any]] = []
tool_names: list[str] = []
for tool in tools:
if isinstance(tool, FunctionTool):
sdk_tools.append(self._function_tool_to_sdk_mcp_tool(tool))
# Claude Agent SDK convention: MCP tools use format "mcp__{server}__{tool}"
tool_names.append(f"mcp__{TOOLS_MCP_SERVER_NAME}__{tool.name}")
elif isinstance(tool, ToolProtocol):
logger.debug(f"Unsupported tool type: {type(tool)}")
if not sdk_tools:
return None, []
return create_sdk_mcp_server(name=TOOLS_MCP_SERVER_NAME, tools=sdk_tools), tool_names
def _function_tool_to_sdk_mcp_tool(self, func_tool: FunctionTool[Any, Any]) -> SdkMcpTool[Any]:
"""Convert a FunctionTool to an SDK MCP tool.
Args:
func_tool: The FunctionTool to convert.
Returns:
An SdkMcpTool instance.
"""
async def handler(args: dict[str, Any]) -> dict[str, Any]:
"""Handler that invokes the FunctionTool."""
try:
if func_tool.input_model:
args_instance = func_tool.input_model(**args)
result = await func_tool.invoke(arguments=args_instance)
else:
result = await func_tool.invoke(arguments=args)
return {"content": [{"type": "text", "text": str(result)}]}
except Exception as e:
return {"content": [{"type": "text", "text": f"Error: {e}"}]}
# Get JSON schema from pydantic model
schema: dict[str, Any] = func_tool.input_model.model_json_schema() if func_tool.input_model else {}
input_schema: dict[str, Any] = {
"type": "object",
"properties": schema.get("properties", {}),
"required": schema.get("required", []),
}
return SdkMcpTool(
name=func_tool.name,
description=func_tool.description,
input_schema=input_schema,
handler=handler,
)
async def _apply_runtime_options(self, options: dict[str, Any] | None) -> None:
"""Apply runtime options that can be changed dynamically.
The Claude SDK supports changing model and permission_mode after connection.
Args:
options: Runtime options to apply.
"""
if not options or not self._client:
return
if "model" in options:
await self._client.set_model(options["model"])
if "permission_mode" in options:
await self._client.set_permission_mode(options["permission_mode"])
def _format_prompt(self, messages: list[ChatMessage] | None) -> str:
"""Format messages into a prompt string.
Args:
messages: List of chat messages.
Returns:
Formatted prompt string.
"""
if not messages:
return ""
return "\n".join([msg.text or "" for msg in messages])
async def run(
self,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
options: TOptions | MutableMapping[str, Any] | None = None,
**kwargs: Any,
) -> AgentResponse[Any]:
"""Run the agent with the given messages.
Args:
messages: The messages to process.
Keyword Args:
thread: The conversation thread. If thread has service_thread_id set,
the agent will resume that session.
options: Runtime options (model, permission_mode can be changed per-request).
kwargs: Additional keyword arguments.
Returns:
AgentResponse with the agent's response.
"""
thread = thread or self.get_new_thread()
return await AgentResponse.from_agent_response_generator(
self.run_stream(messages, thread=thread, options=options, **kwargs)
)
async def run_stream(
self,
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
*,
thread: AgentThread | None = None,
options: TOptions | MutableMapping[str, Any] | None = None,
**kwargs: Any,
) -> AsyncIterable[AgentResponseUpdate]:
"""Stream the agent's response.
Args:
messages: The messages to process.
Keyword Args:
thread: The conversation thread. If thread has service_thread_id set,
the agent will resume that session.
options: Runtime options (model, permission_mode can be changed per-request).
kwargs: Additional keyword arguments.
Yields:
AgentResponseUpdate objects containing chunks of the response.
"""
thread = thread or self.get_new_thread()
# Ensure we're connected to the right session
await self._ensure_session(thread.service_thread_id)
if not self._client:
raise ServiceException("Claude SDK client not initialized.")
prompt = self._format_prompt(normalize_messages(messages))
# Apply runtime options (model, permission_mode)
await self._apply_runtime_options(dict(options) if options else None)
session_id: str | None = None
await self._client.query(prompt)
async for message in self._client.receive_response():
if isinstance(message, StreamEvent):
# Handle streaming events - extract text/thinking deltas
event = message.event
if event.get("type") == "content_block_delta":
delta = event.get("delta", {})
delta_type = delta.get("type")
if delta_type == "text_delta":
text = delta.get("text", "")
if text:
yield AgentResponseUpdate(
role=Role.ASSISTANT,
contents=[Content.from_text(text=text, raw_representation=message)],
raw_representation=message,
)
elif delta_type == "thinking_delta":
thinking = delta.get("thinking", "")
if thinking:
yield AgentResponseUpdate(
role=Role.ASSISTANT,
contents=[Content.from_text_reasoning(text=thinking, raw_representation=message)],
raw_representation=message,
)
elif isinstance(message, ResultMessage):
session_id = message.session_id
# Update thread with session ID
if session_id:
thread.service_thread_id = session_id
@@ -0,0 +1,51 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import ClassVar
from agent_framework._pydantic import AFBaseSettings
__all__ = ["ClaudeAgentSettings"]
class ClaudeAgentSettings(AFBaseSettings):
"""Claude Agent settings.
The settings are first loaded from environment variables with the prefix 'CLAUDE_AGENT_'.
If the environment variables are not found, the settings can be loaded from a .env file
with the encoding 'utf-8'. If the settings are not found in the .env file, the settings
are ignored; however, validation will fail alerting that the settings are missing.
Keyword Args:
cli_path: The path to Claude CLI executable.
model: The model to use (sonnet, opus, haiku).
cwd: The working directory for Claude CLI.
permission_mode: Permission mode (default, acceptEdits, plan, bypassPermissions).
max_turns: Maximum number of conversation turns.
max_budget_usd: Maximum budget in USD.
env_file_path: If provided, the .env settings are read from this file path location.
env_file_encoding: The encoding of the .env file, defaults to 'utf-8'.
Examples:
.. code-block:: python
from agent_framework.anthropic import ClaudeAgentSettings
# Using environment variables
# Set CLAUDE_AGENT_MODEL=sonnet
# CLAUDE_AGENT_PERMISSION_MODE=default
# Or passing parameters directly
settings = ClaudeAgentSettings(model="sonnet")
# Or loading from a .env file
settings = ClaudeAgentSettings(env_file_path="path/to/.env")
"""
env_prefix: ClassVar[str] = "CLAUDE_AGENT_"
cli_path: str | None = None
model: str | None = None
cwd: str | None = None
permission_mode: str | None = None
max_turns: int | None = None
max_budget_usd: float | None = None
+89
View File
@@ -0,0 +1,89 @@
[project]
name = "agent-framework-claude"
description = "Claude Agent SDK integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true"
urls.issues = "https://github.com/microsoft/agent-framework/issues"
classifiers = [
"License :: OSI Approved :: MIT License",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
dependencies = [
"agent-framework-core",
"claude-agent-sdk>=0.1.25",
]
[tool.uv]
prerelease = "if-necessary-or-explicit"
environments = [
"sys_platform == 'darwin'",
"sys_platform == 'linux'",
"sys_platform == 'win32'"
]
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
filterwarnings = [
"ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*"
]
timeout = 120
[tool.ruff]
extend = "../../pyproject.toml"
[tool.coverage.run]
omit = [
"**/__init__.py"
]
[tool.pyright]
extends = "../../pyproject.toml"
exclude = ['tests']
[tool.mypy]
plugins = ['pydantic.mypy']
strict = true
python_version = "3.10"
ignore_missing_imports = true
disallow_untyped_defs = true
no_implicit_optional = true
check_untyped_defs = true
warn_return_any = true
show_error_codes = true
warn_unused_ignores = false
disallow_incomplete_defs = true
disallow_untyped_decorators = true
[tool.bandit]
targets = ["agent_framework_claude"]
exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_claude"
test = "pytest --cov=agent_framework_claude --cov-report=term-missing:skip-covered tests"
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
build-backend = "flit_core.buildapi"
+1
View File
@@ -0,0 +1 @@
# Copyright (c) Microsoft. All rights reserved.
@@ -0,0 +1,714 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from agent_framework import AgentResponseUpdate, AgentThread, ChatMessage, Content, Role, tool
from agent_framework_claude import ClaudeAgent, ClaudeAgentOptions, ClaudeAgentSettings
from agent_framework_claude._agent import TOOLS_MCP_SERVER_NAME
# region Test ClaudeAgentSettings
class TestClaudeAgentSettings:
"""Tests for ClaudeAgentSettings."""
def test_env_prefix(self) -> None:
"""Test that env_prefix is correctly set."""
assert ClaudeAgentSettings.env_prefix == "CLAUDE_AGENT_"
def test_default_values(self) -> None:
"""Test default values are None."""
settings = ClaudeAgentSettings()
assert settings.cli_path is None
assert settings.model is None
assert settings.cwd is None
assert settings.permission_mode is None
assert settings.max_turns is None
assert settings.max_budget_usd is None
def test_explicit_values(self) -> None:
"""Test explicit values override defaults."""
settings = ClaudeAgentSettings(
cli_path="/usr/local/bin/claude",
model="sonnet",
cwd="/home/user/project",
permission_mode="default",
max_turns=10,
max_budget_usd=5.0,
)
assert settings.cli_path == "/usr/local/bin/claude"
assert settings.model == "sonnet"
assert settings.cwd == "/home/user/project"
assert settings.permission_mode == "default"
assert settings.max_turns == 10
assert settings.max_budget_usd == 5.0
def test_env_variable_loading(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test loading from environment variables."""
monkeypatch.setenv("CLAUDE_AGENT_MODEL", "opus")
monkeypatch.setenv("CLAUDE_AGENT_MAX_TURNS", "20")
settings = ClaudeAgentSettings()
assert settings.model == "opus"
assert settings.max_turns == 20
# region Test ClaudeAgent Initialization
class TestClaudeAgentInit:
"""Tests for ClaudeAgent initialization."""
def test_default_initialization(self) -> None:
"""Test agent initializes with defaults."""
agent = ClaudeAgent()
assert agent.id is not None
assert agent.name is None
assert agent.description is None
def test_with_name_and_description(self) -> None:
"""Test agent with name and description."""
agent = ClaudeAgent(name="test-agent", description="A test agent")
assert agent.name == "test-agent"
assert agent.description == "A test agent"
def test_with_instructions_parameter(self) -> None:
"""Test agent with instructions parameter."""
agent = ClaudeAgent(instructions="You are a helpful assistant.")
assert agent._default_options.get("system_prompt") == "You are a helpful assistant." # type: ignore[reportPrivateUsage]
def test_with_system_prompt_in_options(self) -> None:
"""Test agent with system_prompt in options."""
options: ClaudeAgentOptions = {
"system_prompt": "You are a helpful assistant.",
}
agent = ClaudeAgent(default_options=options)
assert agent._default_options.get("system_prompt") == "You are a helpful assistant." # type: ignore[reportPrivateUsage]
def test_with_default_options(self) -> None:
"""Test agent with default options."""
options: ClaudeAgentOptions = {
"model": "sonnet",
"permission_mode": "default",
"max_turns": 10,
}
agent = ClaudeAgent(default_options=options)
assert agent._settings.model == "sonnet" # type: ignore[reportPrivateUsage]
assert agent._settings.permission_mode == "default" # type: ignore[reportPrivateUsage]
assert agent._settings.max_turns == 10 # type: ignore[reportPrivateUsage]
def test_with_function_tool(self) -> None:
"""Test agent with function tool."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
agent = ClaudeAgent(tools=[greet])
assert len(agent._custom_tools) == 1 # type: ignore[reportPrivateUsage]
def test_with_single_tool(self) -> None:
"""Test agent with single tool (not in list)."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
agent = ClaudeAgent(tools=greet)
assert len(agent._custom_tools) == 1 # type: ignore[reportPrivateUsage]
def test_with_builtin_tools(self) -> None:
"""Test agent with built-in tool names."""
agent = ClaudeAgent(tools=["Read", "Write", "Bash"])
assert agent._builtin_tools == ["Read", "Write", "Bash"] # type: ignore[reportPrivateUsage]
assert agent._custom_tools == [] # type: ignore[reportPrivateUsage]
def test_with_mixed_tools(self) -> None:
"""Test agent with both built-in and custom tools."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
agent = ClaudeAgent(tools=["Read", greet, "Bash"])
assert agent._builtin_tools == ["Read", "Bash"] # type: ignore[reportPrivateUsage]
assert len(agent._custom_tools) == 1 # type: ignore[reportPrivateUsage]
# region Test ClaudeAgent Lifecycle
class TestClaudeAgentLifecycle:
"""Tests for ClaudeAgent tool initialization."""
def test_custom_tools_stored_from_constructor(self) -> None:
"""Test that custom tools from constructor are stored."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
agent = ClaudeAgent(tools=[greet])
assert len(agent._custom_tools) == 1 # type: ignore[reportPrivateUsage]
def test_multiple_custom_tools(self) -> None:
"""Test agent with multiple custom tools."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
@tool
def farewell(name: str) -> str:
"""Say goodbye."""
return f"Goodbye, {name}!"
agent = ClaudeAgent(tools=[greet, farewell])
assert len(agent._custom_tools) == 2 # type: ignore[reportPrivateUsage]
def test_no_tools(self) -> None:
"""Test agent without tools."""
agent = ClaudeAgent()
assert agent._custom_tools == [] # type: ignore[reportPrivateUsage]
assert agent._builtin_tools == [] # type: ignore[reportPrivateUsage]
# region Test ClaudeAgent Run
class TestClaudeAgentRun:
"""Tests for ClaudeAgent run method."""
@staticmethod
async def _create_async_generator(items: list[Any]) -> Any:
"""Helper to create async generator from list."""
for item in items:
yield item
def _create_mock_client(self, messages: list[Any]) -> MagicMock:
"""Create a mock ClaudeSDKClient that yields given messages."""
mock_client = MagicMock()
mock_client.connect = AsyncMock()
mock_client.disconnect = AsyncMock()
mock_client.query = AsyncMock()
mock_client.set_model = AsyncMock()
mock_client.set_permission_mode = AsyncMock()
mock_client.receive_response = MagicMock(return_value=self._create_async_generator(messages))
return mock_client
async def test_run_with_string_message(self) -> None:
"""Test run with string message."""
from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock
from claude_agent_sdk.types import StreamEvent
messages = [
StreamEvent(
event={
"type": "content_block_delta",
"delta": {"type": "text_delta", "text": "Hello!"},
},
uuid="event-1",
session_id="session-123",
),
AssistantMessage(
content=[TextBlock(text="Hello!")],
model="claude-sonnet",
),
ResultMessage(
subtype="success",
duration_ms=100,
duration_api_ms=50,
is_error=False,
num_turns=1,
session_id="session-123",
),
]
mock_client = self._create_mock_client(messages)
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
agent = ClaudeAgent()
response = await agent.run("Hello")
assert response.text == "Hello!"
async def test_run_captures_session_id(self) -> None:
"""Test that session ID is captured from ResultMessage."""
from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock
from claude_agent_sdk.types import StreamEvent
messages = [
StreamEvent(
event={
"type": "content_block_delta",
"delta": {"type": "text_delta", "text": "Response"},
},
uuid="event-1",
session_id="test-session-id",
),
AssistantMessage(
content=[TextBlock(text="Response")],
model="claude-sonnet",
),
ResultMessage(
subtype="success",
duration_ms=100,
duration_api_ms=50,
is_error=False,
num_turns=1,
session_id="test-session-id",
),
]
mock_client = self._create_mock_client(messages)
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
agent = ClaudeAgent()
thread = agent.get_new_thread()
await agent.run("Hello", thread=thread)
assert thread.service_thread_id == "test-session-id"
async def test_run_with_thread(self) -> None:
"""Test run with existing thread."""
from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock
from claude_agent_sdk.types import StreamEvent
messages = [
StreamEvent(
event={
"type": "content_block_delta",
"delta": {"type": "text_delta", "text": "Response"},
},
uuid="event-1",
session_id="session-123",
),
AssistantMessage(
content=[TextBlock(text="Response")],
model="claude-sonnet",
),
ResultMessage(
subtype="success",
duration_ms=100,
duration_api_ms=50,
is_error=False,
num_turns=1,
session_id="session-123",
),
]
mock_client = self._create_mock_client(messages)
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
agent = ClaudeAgent()
thread = agent.get_new_thread()
thread.service_thread_id = "existing-session"
await agent.run("Hello", thread=thread)
# region Test ClaudeAgent Run Stream
class TestClaudeAgentRunStream:
"""Tests for ClaudeAgent run_stream method."""
@staticmethod
async def _create_async_generator(items: list[Any]) -> Any:
"""Helper to create async generator from list."""
for item in items:
yield item
def _create_mock_client(self, messages: list[Any]) -> MagicMock:
"""Create a mock ClaudeSDKClient that yields given messages."""
mock_client = MagicMock()
mock_client.connect = AsyncMock()
mock_client.disconnect = AsyncMock()
mock_client.query = AsyncMock()
mock_client.set_model = AsyncMock()
mock_client.set_permission_mode = AsyncMock()
mock_client.receive_response = MagicMock(return_value=self._create_async_generator(messages))
return mock_client
async def test_run_stream_yields_updates(self) -> None:
"""Test run_stream yields AgentResponseUpdate objects."""
from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock
from claude_agent_sdk.types import StreamEvent
messages = [
StreamEvent(
event={
"type": "content_block_delta",
"delta": {"type": "text_delta", "text": "Streaming "},
},
uuid="event-1",
session_id="stream-session",
),
StreamEvent(
event={
"type": "content_block_delta",
"delta": {"type": "text_delta", "text": "response"},
},
uuid="event-2",
session_id="stream-session",
),
AssistantMessage(
content=[TextBlock(text="Streaming response")],
model="claude-sonnet",
),
ResultMessage(
subtype="success",
duration_ms=100,
duration_api_ms=50,
is_error=False,
num_turns=1,
session_id="stream-session",
),
]
mock_client = self._create_mock_client(messages)
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
agent = ClaudeAgent()
updates: list[AgentResponseUpdate] = []
async for update in agent.run_stream("Hello"):
updates.append(update)
# StreamEvent yields text deltas
assert len(updates) == 2
assert updates[0].role == Role.ASSISTANT
assert updates[0].text == "Streaming "
assert updates[1].text == "response"
# region Test ClaudeAgent Session Management
class TestClaudeAgentSessionManagement:
"""Tests for ClaudeAgent session management."""
def test_get_new_thread(self) -> None:
"""Test get_new_thread creates a new thread."""
agent = ClaudeAgent()
thread = agent.get_new_thread()
assert isinstance(thread, AgentThread)
assert thread.service_thread_id is None
def test_get_new_thread_with_service_thread_id(self) -> None:
"""Test get_new_thread with existing service_thread_id."""
agent = ClaudeAgent()
thread = agent.get_new_thread(service_thread_id="existing-session-123")
assert isinstance(thread, AgentThread)
assert thread.service_thread_id == "existing-session-123"
def test_thread_inherits_context_provider(self) -> None:
"""Test that thread inherits context provider."""
mock_provider = MagicMock()
agent = ClaudeAgent(context_provider=mock_provider)
thread = agent.get_new_thread()
assert thread.context_provider == mock_provider
async def test_ensure_session_creates_client(self) -> None:
"""Test _ensure_session creates client when not started."""
with patch("agent_framework_claude._agent.ClaudeSDKClient") as mock_client_class:
mock_client = MagicMock()
mock_client.connect = AsyncMock()
mock_client_class.return_value = mock_client
agent = ClaudeAgent()
await agent._ensure_session(None) # type: ignore[reportPrivateUsage]
assert agent._started # type: ignore[reportPrivateUsage]
mock_client.connect.assert_called_once()
async def test_ensure_session_recreates_for_different_session(self) -> None:
"""Test _ensure_session recreates client for different session ID."""
with patch("agent_framework_claude._agent.ClaudeSDKClient") as mock_client_class:
mock_client1 = MagicMock()
mock_client1.connect = AsyncMock()
mock_client1.disconnect = AsyncMock()
mock_client2 = MagicMock()
mock_client2.connect = AsyncMock()
mock_client_class.side_effect = [mock_client1, mock_client2]
agent = ClaudeAgent()
# First session
await agent._ensure_session(None) # type: ignore[reportPrivateUsage]
assert agent._started # type: ignore[reportPrivateUsage]
# Different session should recreate client
await agent._ensure_session("new-session-id") # type: ignore[reportPrivateUsage]
assert agent._current_session_id == "new-session-id" # type: ignore[reportPrivateUsage]
mock_client1.disconnect.assert_called_once()
async def test_ensure_session_reuses_for_same_session(self) -> None:
"""Test _ensure_session reuses client for same session ID."""
with patch("agent_framework_claude._agent.ClaudeSDKClient") as mock_client_class:
mock_client = MagicMock()
mock_client.connect = AsyncMock()
mock_client_class.return_value = mock_client
agent = ClaudeAgent()
# First call
await agent._ensure_session("session-123") # type: ignore[reportPrivateUsage]
# Same session should not recreate
await agent._ensure_session("session-123") # type: ignore[reportPrivateUsage]
# Only called once
assert mock_client_class.call_count == 1
# region Test ClaudeAgent Tool Conversion
class TestClaudeAgentToolConversion:
"""Tests for ClaudeAgent tool conversion."""
def test_prepare_tools_creates_mcp_server(self) -> None:
"""Test _prepare_tools creates MCP server for AF tools."""
@tool
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
agent = ClaudeAgent(tools=[add])
server, tool_names = agent._prepare_tools(agent._custom_tools) # type: ignore[reportPrivateUsage]
assert server is not None
assert len(tool_names) == 1
assert tool_names[0] == f"mcp__{TOOLS_MCP_SERVER_NAME}__add"
def test_function_tool_to_sdk_mcp_tool(self) -> None:
"""Test converting FunctionTool to SDK MCP tool."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
agent = ClaudeAgent()
sdk_tool = agent._function_tool_to_sdk_mcp_tool(greet) # type: ignore[reportPrivateUsage]
assert sdk_tool.name == "greet"
assert sdk_tool.description == "Greet someone."
assert sdk_tool.input_schema is not None
assert "properties" in sdk_tool.input_schema # type: ignore[operator]
async def test_tool_handler_success(self) -> None:
"""Test tool handler executes successfully."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
agent = ClaudeAgent()
sdk_tool = agent._function_tool_to_sdk_mcp_tool(greet) # type: ignore[reportPrivateUsage]
result = await sdk_tool.handler({"name": "World"})
assert result["content"][0]["text"] == "Hello, World!"
async def test_tool_handler_error(self) -> None:
"""Test tool handler handles errors."""
@tool
def failing_tool() -> str:
"""A tool that fails."""
raise ValueError("Something went wrong")
agent = ClaudeAgent()
sdk_tool = agent._function_tool_to_sdk_mcp_tool(failing_tool) # type: ignore[reportPrivateUsage]
result = await sdk_tool.handler({})
assert "Error:" in result["content"][0]["text"]
assert "Something went wrong" in result["content"][0]["text"]
# region Test ClaudeAgent Permissions
class TestClaudeAgentPermissions:
"""Tests for ClaudeAgent permission handling."""
def test_default_permission_mode(self) -> None:
"""Test default permission mode."""
agent = ClaudeAgent()
assert agent._settings.permission_mode is None # type: ignore[reportPrivateUsage]
def test_permission_mode_from_settings(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test permission mode from environment settings."""
monkeypatch.setenv("CLAUDE_AGENT_PERMISSION_MODE", "acceptEdits")
settings = ClaudeAgentSettings()
assert settings.permission_mode == "acceptEdits"
def test_permission_mode_in_options(self) -> None:
"""Test permission mode in options."""
options: ClaudeAgentOptions = {
"permission_mode": "bypassPermissions",
}
agent = ClaudeAgent(default_options=options)
assert agent._settings.permission_mode == "bypassPermissions" # type: ignore[reportPrivateUsage]
# region Test ClaudeAgent Error Handling
class TestClaudeAgentErrorHandling:
"""Tests for ClaudeAgent error handling."""
@staticmethod
async def _empty_gen() -> Any:
"""Empty async generator."""
if False:
yield
async def test_handles_empty_response(self) -> None:
"""Test handling of empty response."""
mock_client = MagicMock()
mock_client.connect = AsyncMock()
mock_client.disconnect = AsyncMock()
mock_client.query = AsyncMock()
mock_client.set_model = AsyncMock()
mock_client.set_permission_mode = AsyncMock()
mock_client.receive_response = MagicMock(return_value=self._empty_gen())
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
agent = ClaudeAgent()
response = await agent.run("Hello")
assert response.messages == []
# region Test Format Prompt
class TestFormatPrompt:
"""Tests for _format_prompt method."""
def test_format_empty_messages(self) -> None:
"""Test formatting empty messages."""
agent = ClaudeAgent()
result = agent._format_prompt([]) # type: ignore[reportPrivateUsage]
assert result == ""
def test_format_none_messages(self) -> None:
"""Test formatting None messages."""
agent = ClaudeAgent()
result = agent._format_prompt(None) # type: ignore[reportPrivateUsage]
assert result == ""
def test_format_user_message(self) -> None:
"""Test formatting user message."""
agent = ClaudeAgent()
msg = ChatMessage(
role=Role.USER,
contents=[Content.from_text(text="Hello")],
)
result = agent._format_prompt([msg]) # type: ignore[reportPrivateUsage]
assert "Hello" in result
def test_format_multiple_messages(self) -> None:
"""Test formatting multiple messages."""
agent = ClaudeAgent()
messages = [
ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hi")]),
ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Hello!")]),
ChatMessage(role=Role.USER, contents=[Content.from_text(text="How are you?")]),
]
result = agent._format_prompt(messages) # type: ignore[reportPrivateUsage]
assert "Hi" in result
assert "Hello!" in result
assert "How are you?" in result
# region Test Build Options
class TestPrepareClientOptions:
"""Tests for _prepare_client_options method."""
def test_prepare_client_options_with_settings(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test preparing options with settings."""
monkeypatch.setenv("CLAUDE_AGENT_MODEL", "opus")
monkeypatch.setenv("CLAUDE_AGENT_MAX_TURNS", "15")
agent = ClaudeAgent()
with patch("agent_framework_claude._agent.SDKOptions") as mock_opts:
mock_opts.return_value = MagicMock()
agent._prepare_client_options() # type: ignore[reportPrivateUsage]
call_kwargs = mock_opts.call_args[1]
assert call_kwargs.get("model") == "opus"
assert call_kwargs.get("max_turns") == 15
def test_prepare_client_options_with_instructions(self) -> None:
"""Test building options with instructions parameter."""
agent = ClaudeAgent(instructions="Be helpful")
with patch("agent_framework_claude._agent.SDKOptions") as mock_opts:
mock_opts.return_value = MagicMock()
agent._prepare_client_options() # type: ignore[reportPrivateUsage]
call_kwargs = mock_opts.call_args[1]
assert call_kwargs.get("system_prompt") == "Be helpful"
def test_prepare_client_options_includes_custom_tools(self) -> None:
"""Test that _prepare_client_options includes custom tools MCP server."""
@tool
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
agent = ClaudeAgent(tools=[greet])
with patch("agent_framework_claude._agent.SDKOptions") as mock_opts:
mock_opts.return_value = MagicMock()
agent._prepare_client_options() # type: ignore[reportPrivateUsage]
call_kwargs = mock_opts.call_args[1]
assert "mcp_servers" in call_kwargs
assert TOOLS_MCP_SERVER_NAME in call_kwargs["mcp_servers"]
class TestApplyRuntimeOptions:
"""Tests for _apply_runtime_options method."""
async def test_apply_runtime_model(self) -> None:
"""Test applying runtime model option."""
mock_client = MagicMock()
mock_client.set_model = AsyncMock()
mock_client.set_permission_mode = AsyncMock()
agent = ClaudeAgent()
agent._client = mock_client # type: ignore[reportPrivateUsage]
await agent._apply_runtime_options({"model": "opus"}) # type: ignore[reportPrivateUsage]
mock_client.set_model.assert_called_once_with("opus")
async def test_apply_runtime_permission_mode(self) -> None:
"""Test applying runtime permission_mode option."""
mock_client = MagicMock()
mock_client.set_model = AsyncMock()
mock_client.set_permission_mode = AsyncMock()
agent = ClaudeAgent()
agent._client = mock_client # type: ignore[reportPrivateUsage]
await agent._apply_runtime_options({"permission_mode": "acceptEdits"}) # type: ignore[reportPrivateUsage]
mock_client.set_permission_mode.assert_called_once_with("acceptEdits")
async def test_apply_runtime_options_none(self) -> None:
"""Test applying None options does nothing."""
mock_client = MagicMock()
mock_client.set_model = AsyncMock()
mock_client.set_permission_mode = AsyncMock()
agent = ClaudeAgent()
agent._client = mock_client # type: ignore[reportPrivateUsage]
await agent._apply_runtime_options(None) # type: ignore[reportPrivateUsage]
mock_client.set_model.assert_not_called()
mock_client.set_permission_mode.assert_not_called()
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Copilot Studio integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Declarative specification support for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Debug UI for Microsoft Agent Framework with OpenAI-compatible API
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://github.com/microsoft/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Durable Task integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Foundry Local integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -4,7 +4,7 @@ description = "GitHub Copilot integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Experimental modules for Microsoft Agent Framework"
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Mem0 integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Ollama integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://learn.microsoft.com/en-us/agent-framework/"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Microsoft Purview (Graph dataSecurityAndGovernance) integration f
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://github.com/microsoft/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Redis integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+3 -2
View File
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260128"
version = "1.0.0b260130"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core[all]==1.0.0b260128",
"agent-framework-core[all]==1.0.0b260130",
]
[dependency-groups]
@@ -103,6 +103,7 @@ agent-framework-ollama = { workspace = true }
agent-framework-purview = { workspace = true }
agent-framework-redis = { workspace = true }
agent-framework-github-copilot = { workspace = true }
agent-framework-claude = { workspace = true }
[tool.ruff]
line-length = 120
@@ -2,7 +2,7 @@
This folder contains examples demonstrating how to use Anthropic's Claude models with the Agent Framework.
## Examples
## Anthropic Client Examples
| File | Description |
|------|-------------|
@@ -11,14 +11,36 @@ This folder contains examples demonstrating how to use Anthropic's Claude models
| [`anthropic_skills.py`](anthropic_skills.py) | Illustrates how to use Anthropic-managed Skills with an agent, including the Code Interpreter tool and file generation and saving. |
| [`anthropic_foundry.py`](anthropic_foundry.py) | Example of using Foundry's Anthropic integration with the Agent Framework. |
## Claude Agent Examples
| File | Description |
|------|-------------|
| [`anthropic_claude_basic.py`](anthropic_claude_basic.py) | Basic usage of ClaudeAgent with streaming, non-streaming, and custom tools. |
| [`anthropic_claude_with_tools.py`](anthropic_claude_with_tools.py) | Using built-in tools (Read, Glob, Grep, etc.). |
| [`anthropic_claude_with_shell.py`](anthropic_claude_with_shell.py) | Shell command execution with interactive permission handling. |
| [`anthropic_claude_with_multiple_permissions.py`](anthropic_claude_with_multiple_permissions.py) | Combining multiple tools (Bash, Read, Write) with permission prompts. |
| [`anthropic_claude_with_url.py`](anthropic_claude_with_url.py) | Fetching and processing web content with WebFetch. |
| [`anthropic_claude_with_mcp.py`](anthropic_claude_with_mcp.py) | Local (stdio) and remote (HTTP) MCP server configuration. |
| [`anthropic_claude_with_session.py`](anthropic_claude_with_session.py) | Session management, persistence, and resumption. |
## Environment Variables
Set the following environment variables before running the examples:
### Anthropic Client
- `ANTHROPIC_API_KEY`: Your Anthropic API key (get one from [Anthropic Console](https://console.anthropic.com/))
- `ANTHROPIC_CHAT_MODEL_ID`: The Claude model to use (e.g., `claude-haiku-4-5`, `claude-sonnet-4-5-20250929`)
Or, for Foundry:
### Foundry
- `ANTHROPIC_FOUNDRY_API_KEY`: Your Foundry Anthropic API key
- `ANTHROPIC_FOUNDRY_ENDPOINT`: The endpoint URL for your Foundry Anthropic resource
- `ANTHROPIC_CHAT_MODEL_ID`: The Claude model to use in Foundry (e.g., `claude-haiku-4-5`)
### Claude Agent
- `CLAUDE_AGENT_CLI_PATH`: Path to the Claude Code CLI executable
- `CLAUDE_AGENT_MODEL`: Model to use (sonnet, opus, haiku)
- `CLAUDE_AGENT_CWD`: Working directory for Claude CLI
- `CLAUDE_AGENT_PERMISSION_MODE`: Permission mode (default, acceptEdits, plan, bypassPermissions)
- `CLAUDE_AGENT_MAX_TURNS`: Maximum number of conversation turns
- `CLAUDE_AGENT_MAX_BUDGET_USD`: Maximum budget in USD
@@ -0,0 +1,76 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Claude Agent Basic Example
This sample demonstrates using ClaudeAgent for basic interactions
with Claude Agent SDK.
Prerequisites:
- Claude Code CLI must be installed and configured
- pip install agent-framework-claude
Environment variables:
- CLAUDE_AGENT_MODEL: Model to use (sonnet, opus, haiku)
- CLAUDE_AGENT_PERMISSION_MODE: Permission mode (default, acceptEdits, bypassPermissions)
"""
import asyncio
from typing import Annotated
from agent_framework import tool
from agent_framework_claude import ClaudeAgent
@tool
def get_weather(location: Annotated[str, "The city name"]) -> str:
"""Get the current weather for a location."""
return f"The weather in {location} is sunny with a high of 25C."
async def non_streaming_example() -> None:
"""Example of non-streaming response."""
print("=== Non-streaming Example ===")
agent = ClaudeAgent(
name="BasicAgent",
instructions="You are a helpful assistant. Keep responses concise.",
tools=[get_weather],
)
async with agent:
query = "What's the weather in Seattle?"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}\n")
async def streaming_example() -> None:
"""Example of streaming response."""
print("=== Streaming Example ===")
agent = ClaudeAgent(
name="StreamingAgent",
instructions="You are a helpful assistant.",
tools=[get_weather],
)
async with agent:
query = "What's the weather in Paris?"
print(f"User: {query}")
print("Agent: ", end="", flush=True)
async for chunk in agent.run_stream(query):
if chunk.text:
print(chunk.text, end="", flush=True)
print("\n")
async def main() -> None:
print("=== Claude Agent Basic Example ===\n")
await non_streaming_example()
await streaming_example()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,81 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Claude Agent with MCP Servers
This sample demonstrates how to configure MCP (Model Context Protocol) servers
with ClaudeAgent. It shows both local (stdio) and remote (HTTP) server
configurations, giving the agent access to external tools and data sources.
Supported MCP server types:
- "stdio": Local process-based server
- "http": Remote HTTP server
- "sse": Remote SSE (Server-Sent Events) server
SECURITY NOTE: MCP servers can expose powerful capabilities. Only configure
servers you trust. Use permission handlers to control what actions are allowed.
"""
import asyncio
from typing import Any
from agent_framework_claude import ClaudeAgent
from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny
async def prompt_permission(
tool_name: str,
tool_input: dict[str, Any],
context: object,
) -> PermissionResultAllow | PermissionResultDeny:
"""Permission handler that prompts the user for approval."""
print(f"\n[Permission Request: {tool_name}]")
response = input("Approve? (y/n): ").strip().lower()
if response in ("y", "yes"):
return PermissionResultAllow()
return PermissionResultDeny(message="Denied by user")
async def main() -> None:
print("=== Claude Agent with MCP Servers ===\n")
# Configure both local and remote MCP servers
mcp_servers: dict[str, Any] = {
# Local stdio server: provides filesystem access tools
"filesystem": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
},
# Remote HTTP server: Microsoft Learn documentation
"microsoft-learn": {
"type": "http",
"url": "https://learn.microsoft.com/api/mcp",
},
}
agent = ClaudeAgent(
instructions="You are a helpful assistant with access to the local filesystem and Microsoft Learn.",
default_options={
"can_use_tool": prompt_permission,
"mcp_servers": mcp_servers,
},
)
async with agent:
# Query that exercises the local filesystem MCP server
query1 = "List the first three files in the current directory"
print(f"User: {query1}")
result1 = await agent.run(query1)
print(f"Agent: {result1.text}\n")
# Query that exercises the remote Microsoft Learn MCP server
query2 = "Search Microsoft Learn for 'Azure Functions Python' and summarize the top result"
print(f"User: {query2}")
result2 = await agent.run(query2)
print(f"Agent: {result2.text}\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,69 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Claude Agent with Multiple Permissions
This sample demonstrates how to enable multiple permission types with ClaudeAgent.
By combining different tools and using a permission handler, the agent can perform
complex tasks that require multiple capabilities.
Available built-in tools:
- "Bash": Execute shell commands
- "Read": Read files from the filesystem
- "Write": Write files to the filesystem
- "Edit": Edit existing files
- "Glob": Search for files by pattern
- "Grep": Search file contents
SECURITY NOTE: Only enable permissions that are necessary for your use case.
More permissions mean more potential for unintended actions.
"""
import asyncio
from typing import Any
from agent_framework_claude import ClaudeAgent
from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny
async def prompt_permission(
tool_name: str,
tool_input: dict[str, Any],
context: object,
) -> PermissionResultAllow | PermissionResultDeny:
"""Permission handler that prompts the user for approval."""
print(f"\n[Permission Request: {tool_name}]")
if "command" in tool_input:
print(f" Command: {tool_input.get('command')}")
if "file_path" in tool_input:
print(f" Path: {tool_input.get('file_path')}")
if "pattern" in tool_input:
print(f" Pattern: {tool_input.get('pattern')}")
response = input("Approve? (y/n): ").strip().lower()
if response in ("y", "yes"):
return PermissionResultAllow()
return PermissionResultDeny(message="Denied by user")
async def main() -> None:
print("=== Claude Agent with Multiple Permissions ===\n")
agent = ClaudeAgent(
instructions="You are a helpful development assistant that can read, write files and run commands.",
tools=["Bash", "Read", "Write", "Glob"],
default_options={
"can_use_tool": prompt_permission,
},
)
async with agent:
query = "List the first 3 Python files, then read the first one and create a summary in summary.txt"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,145 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Claude Agent with Session Management
This sample demonstrates session management with ClaudeAgent, showing
persistent conversation capabilities. Sessions are automatically persisted
by the Claude Code CLI.
"""
import asyncio
from random import randint
from typing import Annotated
from agent_framework import tool
from agent_framework_claude import ClaudeAgent
from pydantic import Field
@tool
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
async def example_with_automatic_session_creation() -> None:
"""Each agent instance creates a new session."""
print("=== Automatic Session Creation Example ===")
# First agent - first session
agent1 = ClaudeAgent(
instructions="You are a helpful weather agent.",
tools=[get_weather],
)
async with agent1:
query1 = "What's the weather like in Seattle?"
print(f"User: {query1}")
result1 = await agent1.run(query1)
print(f"Agent: {result1.text}")
# Second agent - new session, no memory of previous conversation
agent2 = ClaudeAgent(
instructions="You are a helpful weather agent.",
tools=[get_weather],
)
async with agent2:
query2 = "What was the last city I asked about?"
print(f"\nUser: {query2}")
result2 = await agent2.run(query2)
print(f"Agent: {result2.text}")
print("Note: Each agent instance creates a separate session, so the agent doesn't remember previous context.\n")
async def example_with_session_persistence() -> None:
"""Reuse session via thread object for multi-turn conversations."""
print("=== Session Persistence Example ===")
agent = ClaudeAgent(
instructions="You are a helpful weather agent.",
tools=[get_weather],
)
async with agent:
# Create a thread to maintain conversation context
thread = agent.get_new_thread()
# First query
query1 = "What's the weather like in Tokyo?"
print(f"User: {query1}")
result1 = await agent.run(query1, thread=thread)
print(f"Agent: {result1.text}")
# Second query - using same thread maintains context
query2 = "How about London?"
print(f"\nUser: {query2}")
result2 = await agent.run(query2, thread=thread)
print(f"Agent: {result2.text}")
# Third query - agent should remember both previous cities
query3 = "Which of the cities I asked about has better weather?"
print(f"\nUser: {query3}")
result3 = await agent.run(query3, thread=thread)
print(f"Agent: {result3.text}")
print("Note: The agent remembers context from previous messages in the same session.\n")
async def example_with_existing_session_id() -> None:
"""Resume session in new agent instance using service_thread_id."""
print("=== Existing Session ID Example ===")
existing_session_id = None
# First agent instance - start a conversation
agent1 = ClaudeAgent(
instructions="You are a helpful weather agent.",
tools=[get_weather],
)
async with agent1:
thread = agent1.get_new_thread()
query1 = "What's the weather in Paris?"
print(f"User: {query1}")
result1 = await agent1.run(query1, thread=thread)
print(f"Agent: {result1.text}")
# Capture the session ID for later use
existing_session_id = thread.service_thread_id
print(f"Session ID: {existing_session_id}")
if existing_session_id:
print("\n--- Continuing with the same session ID in a new agent instance ---")
# Second agent instance - resume the conversation
agent2 = ClaudeAgent(
instructions="You are a helpful weather agent.",
tools=[get_weather],
)
async with agent2:
# Create thread with existing session ID
thread = agent2.get_new_thread(service_thread_id=existing_session_id)
query2 = "What was the last city I asked about?"
print(f"User: {query2}")
result2 = await agent2.run(query2, thread=thread)
print(f"Agent: {result2.text}")
print("Note: The agent continues the conversation using the session ID.\n")
async def main() -> None:
print("=== Claude Agent Session Management Examples ===\n")
await example_with_automatic_session_creation()
await example_with_session_persistence()
await example_with_existing_session_id()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,57 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Claude Agent with Shell Permissions
This sample demonstrates how to enable shell command execution with ClaudeAgent.
By providing a permission handler via `can_use_tool`, the agent can execute
shell commands to perform tasks like listing files, running scripts, or executing system commands.
SECURITY NOTE: Only enable shell permissions when you trust the agent's actions.
Shell commands have full access to your system within the permissions of the running process.
"""
import asyncio
from typing import Any
from agent_framework_claude import ClaudeAgent
from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny
async def prompt_permission(
tool_name: str,
tool_input: dict[str, Any],
context: object,
) -> PermissionResultAllow | PermissionResultDeny:
"""Permission handler that prompts the user for approval."""
print(f"\n[Permission Request: {tool_name}]")
if "command" in tool_input:
print(f" Command: {tool_input.get('command')}")
response = input("Approve? (y/n): ").strip().lower()
if response in ("y", "yes"):
return PermissionResultAllow()
return PermissionResultDeny(message="Denied by user")
async def main() -> None:
print("=== Claude Agent with Shell Permissions ===\n")
agent = ClaudeAgent(
instructions="You are a helpful assistant that can execute shell commands.",
tools=["Bash"],
default_options={
"can_use_tool": prompt_permission,
},
)
async with agent:
query = "List the first 3 Python files in the current directory"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,40 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Claude Agent with Built-in Tools
This sample demonstrates using ClaudeAgent with built-in tools for file operations.
Built-in tools are specified as strings in the tools parameter.
Available built-in tools:
- "Bash": Execute shell commands
- "Read": Read files from the filesystem
- "Write": Write files to the filesystem
- "Edit": Edit existing files
- "Glob": Search for files by pattern
- "Grep": Search file contents
"""
import asyncio
from agent_framework_claude import ClaudeAgent
async def main() -> None:
print("=== Claude Agent with Built-in Tools ===\n")
# Built-in tools can be specified as strings in the tools parameter
agent = ClaudeAgent(
instructions="You are a helpful assistant that can read files.",
tools=["Read", "Glob"],
)
async with agent:
query = "List the first 3 Python files in the current directory"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Claude Agent with URL Fetching
This sample demonstrates how to enable URL fetching with ClaudeAgent.
By enabling the WebFetch tool, the agent can fetch and process content from web URLs.
Available web tools:
- "WebFetch": Fetch content from URLs
- "WebSearch": Search the web
SECURITY NOTE: Only enable URL permissions when you trust the agent's actions.
URL fetching allows the agent to access any URL accessible from your network.
"""
import asyncio
from agent_framework_claude import ClaudeAgent
async def main() -> None:
print("=== Claude Agent with URL Fetching ===\n")
agent = ClaudeAgent(
instructions="You are a helpful assistant that can fetch and summarize web content.",
tools=["WebFetch"],
)
async with agent:
query = "Fetch https://learn.microsoft.com/agent-framework/tutorials/quick-start and summarize its contents"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}\n")
if __name__ == "__main__":
asyncio.run(main())
+59 -26
View File
@@ -35,6 +35,7 @@ members = [
"agent-framework-azurefunctions",
"agent-framework-bedrock",
"agent-framework-chatkit",
"agent-framework-claude",
"agent-framework-copilotstudio",
"agent-framework-core",
"agent-framework-declarative",
@@ -94,7 +95,7 @@ wheels = [
[[package]]
name = "agent-framework"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { virtual = "." }
dependencies = [
{ name = "agent-framework-core", extra = ["all"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -161,7 +162,7 @@ docs = [
[[package]]
name = "agent-framework-a2a"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/a2a" }
dependencies = [
{ name = "a2a-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -176,7 +177,7 @@ requires-dist = [
[[package]]
name = "agent-framework-ag-ui"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/ag-ui" }
dependencies = [
{ name = "ag-ui-protocol", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -206,7 +207,7 @@ provides-extras = ["dev"]
[[package]]
name = "agent-framework-anthropic"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/anthropic" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -221,7 +222,7 @@ requires-dist = [
[[package]]
name = "agent-framework-azure-ai"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/azure-ai" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -240,7 +241,7 @@ requires-dist = [
[[package]]
name = "agent-framework-azure-ai-search"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/azure-ai-search" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -255,7 +256,7 @@ requires-dist = [
[[package]]
name = "agent-framework-azurefunctions"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/azurefunctions" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -277,7 +278,7 @@ dev = []
[[package]]
name = "agent-framework-bedrock"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/bedrock" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -294,7 +295,7 @@ requires-dist = [
[[package]]
name = "agent-framework-chatkit"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/chatkit" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -307,9 +308,24 @@ requires-dist = [
{ name = "openai-chatkit", specifier = ">=1.4.0,<2.0.0" },
]
[[package]]
name = "agent-framework-claude"
version = "1.0.0b260130"
source = { editable = "packages/claude" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "claude-agent-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
[package.metadata]
requires-dist = [
{ name = "agent-framework-core", editable = "packages/core" },
{ name = "claude-agent-sdk", specifier = ">=0.1.25" },
]
[[package]]
name = "agent-framework-copilotstudio"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/copilotstudio" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -324,7 +340,7 @@ requires-dist = [
[[package]]
name = "agent-framework-core"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/core" }
dependencies = [
{ name = "azure-identity", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -394,7 +410,7 @@ provides-extras = ["all"]
[[package]]
name = "agent-framework-declarative"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/declarative" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -419,7 +435,7 @@ dev = [{ name = "types-pyyaml" }]
[[package]]
name = "agent-framework-devui"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/devui" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -453,7 +469,7 @@ provides-extras = ["dev", "all"]
[[package]]
name = "agent-framework-durabletask"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/durabletask" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -480,7 +496,7 @@ dev = [{ name = "types-python-dateutil", specifier = ">=2.9.0" }]
[[package]]
name = "agent-framework-foundry-local"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/foundry_local" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -495,7 +511,7 @@ requires-dist = [
[[package]]
name = "agent-framework-github-copilot"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/github_copilot" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -510,7 +526,7 @@ requires-dist = [
[[package]]
name = "agent-framework-lab"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/lab" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -601,7 +617,7 @@ dev = [
[[package]]
name = "agent-framework-mem0"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/mem0" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -616,7 +632,7 @@ requires-dist = [
[[package]]
name = "agent-framework-ollama"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/ollama" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -631,7 +647,7 @@ requires-dist = [
[[package]]
name = "agent-framework-purview"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/purview" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -648,7 +664,7 @@ requires-dist = [
[[package]]
name = "agent-framework-redis"
version = "1.0.0b260128"
version = "1.0.0b260130"
source = { editable = "packages/redis" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -1406,6 +1422,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
[[package]]
name = "claude-agent-sdk"
version = "0.1.25"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "mcp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c5/ce/d8dd6eb56e981d1b981bf6766e1849878c54fbd160b6862e7c8e11b282d3/claude_agent_sdk-0.1.25.tar.gz", hash = "sha256:e2284fa2ece778d04b225f0f34118ea2623ae1f9fe315bc3bf921792658b6645", size = 57113, upload-time = "2026-01-29T01:20:17.353Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/23/09/e25dad92af3305ded5490d4493f782b1cb8c530145a7107bceea26ec811e/claude_agent_sdk-0.1.25-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6adeffacbb75fe5c91529512331587a7af0e5e6dcbce4bd6b3a6ef8a51bdabeb", size = 54672313, upload-time = "2026-01-29T01:20:03.651Z" },
{ url = "https://files.pythonhosted.org/packages/28/0f/7b39ce9dd7d8f995e2c9d2049e1ce79f9010144a6793e8dd6ea9df23f53e/claude_agent_sdk-0.1.25-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:f210a05b2b471568c7f4019875b0ab451c783397f21edc32d7bd9a7144d9aad1", size = 68848229, upload-time = "2026-01-29T01:20:07.311Z" },
{ url = "https://files.pythonhosted.org/packages/40/6f/0b22cd9a68c39c0a8f5bd024072c15ca89bfa2dbfad3a94a35f6a1a90ecd/claude_agent_sdk-0.1.25-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:3399c3c748eb42deac308c6230cb0bb6b975c51b0495b42fe06896fa741d336f", size = 70562885, upload-time = "2026-01-29T01:20:11.033Z" },
{ url = "https://files.pythonhosted.org/packages/5c/b6/2aaf28eeaa994e5491ad9589a9b006d5112b167aab8ced0823a6ffd86e4f/claude_agent_sdk-0.1.25-py3-none-win_amd64.whl", hash = "sha256:c5e8fe666b88049080ae4ac2a02dbd2d5c00ab1c495683d3c2f7dfab8ff1fec9", size = 72746667, upload-time = "2026-01-29T01:20:14.271Z" },
]
[[package]]
name = "click"
version = "8.3.1"
@@ -1423,7 +1456,7 @@ name = "clr-loader"
version = "0.2.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" }
wheels = [
@@ -1926,7 +1959,7 @@ name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" },
{ name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
@@ -4687,8 +4720,8 @@ name = "powerfx"
version = "0.0.34"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pythonnet", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
{ name = "pythonnet", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555, upload-time = "2025-12-22T15:50:59.682Z" }
wheels = [
@@ -5355,7 +5388,7 @@ name = "pythonnet"
version = "3.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "clr-loader", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "clr-loader", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" }
wheels = [