mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
0fcf075ea7
commit
8b475afe17
+17
-1
@@ -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
|
||||
|
||||
@@ -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,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"]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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())
|
||||
+69
@@ -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())
|
||||
Generated
+59
-26
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user