diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index 07126c38f4..af64583090 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -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 diff --git a/python/packages/a2a/pyproject.toml b/python/packages/a2a/pyproject.toml index 07f0c05895..d4de542199 100644 --- a/python/packages/a2a/pyproject.toml +++ b/python/packages/a2a/pyproject.toml @@ -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" diff --git a/python/packages/ag-ui/pyproject.toml b/python/packages/ag-ui/pyproject.toml index bf6643d565..627a71279c 100644 --- a/python/packages/ag-ui/pyproject.toml +++ b/python/packages/ag-ui/pyproject.toml @@ -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"] diff --git a/python/packages/anthropic/pyproject.toml b/python/packages/anthropic/pyproject.toml index 0ce9371792..8935476ed5 100644 --- a/python/packages/anthropic/pyproject.toml +++ b/python/packages/anthropic/pyproject.toml @@ -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" diff --git a/python/packages/azure-ai-search/pyproject.toml b/python/packages/azure-ai-search/pyproject.toml index df7d4b07b9..fb4763dfd8 100644 --- a/python/packages/azure-ai-search/pyproject.toml +++ b/python/packages/azure-ai-search/pyproject.toml @@ -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" diff --git a/python/packages/azure-ai/pyproject.toml b/python/packages/azure-ai/pyproject.toml index 8c762a1857..bf8e969519 100644 --- a/python/packages/azure-ai/pyproject.toml +++ b/python/packages/azure-ai/pyproject.toml @@ -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" diff --git a/python/packages/azurefunctions/pyproject.toml b/python/packages/azurefunctions/pyproject.toml index 6000408247..be650a7516 100644 --- a/python/packages/azurefunctions/pyproject.toml +++ b/python/packages/azurefunctions/pyproject.toml @@ -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" diff --git a/python/packages/bedrock/pyproject.toml b/python/packages/bedrock/pyproject.toml index 2da38343ba..aa864223a9 100644 --- a/python/packages/bedrock/pyproject.toml +++ b/python/packages/bedrock/pyproject.toml @@ -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" diff --git a/python/packages/chatkit/pyproject.toml b/python/packages/chatkit/pyproject.toml index c1c5ca4661..632bf5aa61 100644 --- a/python/packages/chatkit/pyproject.toml +++ b/python/packages/chatkit/pyproject.toml @@ -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" diff --git a/python/packages/claude/LICENSE b/python/packages/claude/LICENSE new file mode 100644 index 0000000000..9e841e7a26 --- /dev/null +++ b/python/packages/claude/LICENSE @@ -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 diff --git a/python/packages/claude/README.md b/python/packages/claude/README.md new file mode 100644 index 0000000000..a89f99920a --- /dev/null +++ b/python/packages/claude/README.md @@ -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. diff --git a/python/packages/claude/agent_framework_claude/__init__.py b/python/packages/claude/agent_framework_claude/__init__.py new file mode 100644 index 0000000000..18f30bf25e --- /dev/null +++ b/python/packages/claude/agent_framework_claude/__init__.py @@ -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__", +] diff --git a/python/packages/claude/agent_framework_claude/_agent.py b/python/packages/claude/agent_framework_claude/_agent.py new file mode 100644 index 0000000000..8335d2f149 --- /dev/null +++ b/python/packages/claude/agent_framework_claude/_agent.py @@ -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 diff --git a/python/packages/claude/agent_framework_claude/_settings.py b/python/packages/claude/agent_framework_claude/_settings.py new file mode 100644 index 0000000000..b01e189cc8 --- /dev/null +++ b/python/packages/claude/agent_framework_claude/_settings.py @@ -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 diff --git a/python/packages/claude/pyproject.toml b/python/packages/claude/pyproject.toml new file mode 100644 index 0000000000..1fd9d04f54 --- /dev/null +++ b/python/packages/claude/pyproject.toml @@ -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" diff --git a/python/packages/claude/tests/__init__.py b/python/packages/claude/tests/__init__.py new file mode 100644 index 0000000000..2a50eae894 --- /dev/null +++ b/python/packages/claude/tests/__init__.py @@ -0,0 +1 @@ +# Copyright (c) Microsoft. All rights reserved. diff --git a/python/packages/claude/tests/test_claude_agent.py b/python/packages/claude/tests/test_claude_agent.py new file mode 100644 index 0000000000..4402b611e4 --- /dev/null +++ b/python/packages/claude/tests/test_claude_agent.py @@ -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() diff --git a/python/packages/copilotstudio/pyproject.toml b/python/packages/copilotstudio/pyproject.toml index f2dcb2f8ff..fef08eeaa4 100644 --- a/python/packages/copilotstudio/pyproject.toml +++ b/python/packages/copilotstudio/pyproject.toml @@ -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" diff --git a/python/packages/core/pyproject.toml b/python/packages/core/pyproject.toml index 58f285f0ef..b68e8038dd 100644 --- a/python/packages/core/pyproject.toml +++ b/python/packages/core/pyproject.toml @@ -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" diff --git a/python/packages/declarative/pyproject.toml b/python/packages/declarative/pyproject.toml index 5ae6e325a3..ab43d50104 100644 --- a/python/packages/declarative/pyproject.toml +++ b/python/packages/declarative/pyproject.toml @@ -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" diff --git a/python/packages/devui/pyproject.toml b/python/packages/devui/pyproject.toml index 7c07ab3f15..6ea79e48e0 100644 --- a/python/packages/devui/pyproject.toml +++ b/python/packages/devui/pyproject.toml @@ -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" diff --git a/python/packages/durabletask/pyproject.toml b/python/packages/durabletask/pyproject.toml index 3c854f7243..e8b66c59ab 100644 --- a/python/packages/durabletask/pyproject.toml +++ b/python/packages/durabletask/pyproject.toml @@ -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" diff --git a/python/packages/foundry_local/pyproject.toml b/python/packages/foundry_local/pyproject.toml index 41c638ec20..1a338ede43 100644 --- a/python/packages/foundry_local/pyproject.toml +++ b/python/packages/foundry_local/pyproject.toml @@ -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" diff --git a/python/packages/github_copilot/pyproject.toml b/python/packages/github_copilot/pyproject.toml index 9adb4caa92..a80197d2d7 100644 --- a/python/packages/github_copilot/pyproject.toml +++ b/python/packages/github_copilot/pyproject.toml @@ -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" diff --git a/python/packages/lab/pyproject.toml b/python/packages/lab/pyproject.toml index be5d8a9868..86cee50527 100644 --- a/python/packages/lab/pyproject.toml +++ b/python/packages/lab/pyproject.toml @@ -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" diff --git a/python/packages/mem0/pyproject.toml b/python/packages/mem0/pyproject.toml index 619b3ccd4d..26e8343aa9 100644 --- a/python/packages/mem0/pyproject.toml +++ b/python/packages/mem0/pyproject.toml @@ -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" diff --git a/python/packages/ollama/pyproject.toml b/python/packages/ollama/pyproject.toml index 84bfc15e1b..e050978fca 100644 --- a/python/packages/ollama/pyproject.toml +++ b/python/packages/ollama/pyproject.toml @@ -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" diff --git a/python/packages/purview/pyproject.toml b/python/packages/purview/pyproject.toml index d3e66f0834..df243154fc 100644 --- a/python/packages/purview/pyproject.toml +++ b/python/packages/purview/pyproject.toml @@ -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" diff --git a/python/packages/redis/pyproject.toml b/python/packages/redis/pyproject.toml index b53385097e..14c75ba37c 100644 --- a/python/packages/redis/pyproject.toml +++ b/python/packages/redis/pyproject.toml @@ -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" diff --git a/python/pyproject.toml b/python/pyproject.toml index 8afc0d36bc..a14354cbe4 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -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 diff --git a/python/samples/getting_started/agents/anthropic/README.md b/python/samples/getting_started/agents/anthropic/README.md index 2fee2f5c07..84a3b855d7 100644 --- a/python/samples/getting_started/agents/anthropic/README.md +++ b/python/samples/getting_started/agents/anthropic/README.md @@ -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 diff --git a/python/samples/getting_started/agents/anthropic/anthropic_claude_basic.py b/python/samples/getting_started/agents/anthropic/anthropic_claude_basic.py new file mode 100644 index 0000000000..f62cc60664 --- /dev/null +++ b/python/samples/getting_started/agents/anthropic/anthropic_claude_basic.py @@ -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()) diff --git a/python/samples/getting_started/agents/anthropic/anthropic_claude_with_mcp.py b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_mcp.py new file mode 100644 index 0000000000..f47dbd1648 --- /dev/null +++ b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_mcp.py @@ -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()) diff --git a/python/samples/getting_started/agents/anthropic/anthropic_claude_with_multiple_permissions.py b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_multiple_permissions.py new file mode 100644 index 0000000000..e4e2d10605 --- /dev/null +++ b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_multiple_permissions.py @@ -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()) diff --git a/python/samples/getting_started/agents/anthropic/anthropic_claude_with_session.py b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_session.py new file mode 100644 index 0000000000..2549457800 --- /dev/null +++ b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_session.py @@ -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()) diff --git a/python/samples/getting_started/agents/anthropic/anthropic_claude_with_shell.py b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_shell.py new file mode 100644 index 0000000000..849a96c593 --- /dev/null +++ b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_shell.py @@ -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()) diff --git a/python/samples/getting_started/agents/anthropic/anthropic_claude_with_tools.py b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_tools.py new file mode 100644 index 0000000000..15b9cbc5dc --- /dev/null +++ b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_tools.py @@ -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()) diff --git a/python/samples/getting_started/agents/anthropic/anthropic_claude_with_url.py b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_url.py new file mode 100644 index 0000000000..102785ca94 --- /dev/null +++ b/python/samples/getting_started/agents/anthropic/anthropic_claude_with_url.py @@ -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()) diff --git a/python/uv.lock b/python/uv.lock index 0d7436c449..63e2cd11b8 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -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 = [