mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Add BaseAgent implementation for Claude Agent SDK (#3509)
* Added ClaudeAgent implementation * Updated streaming logic * Small updates * Small update * Fixes * Small fix * Naming improvements * Updated imports * Addressed comments * Updated package versions
This commit is contained in:
committed by
GitHub
Unverified
parent
0fcf075ea7
commit
8b475afe17
@@ -4,7 +4,7 @@ description = "A2A integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "agent-framework-ag-ui"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
description = "AG-UI protocol integration for Agent Framework"
|
||||
readme = "README.md"
|
||||
license-files = ["LICENSE"]
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Anthropic integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Azure AI Search integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Azure AI Foundry integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Azure Functions integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Amazon Bedrock integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "OpenAI ChatKit integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
||||
@@ -0,0 +1,11 @@
|
||||
# Get Started with Microsoft Agent Framework Claude
|
||||
|
||||
Please install this package via pip:
|
||||
|
||||
```bash
|
||||
pip install agent-framework-claude --pre
|
||||
```
|
||||
|
||||
## Claude Agent
|
||||
|
||||
The Claude agent enables integration with Claude Agent SDK, allowing you to interact with Claude's agentic capabilities through the Agent Framework.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
from ._agent import ClaudeAgent, ClaudeAgentOptions
|
||||
from ._settings import ClaudeAgentSettings
|
||||
|
||||
try:
|
||||
__version__ = importlib.metadata.version(__name__)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
__version__ = "0.0.0" # Fallback for development mode
|
||||
|
||||
__all__ = [
|
||||
"ClaudeAgent",
|
||||
"ClaudeAgentOptions",
|
||||
"ClaudeAgentSettings",
|
||||
"__version__",
|
||||
]
|
||||
@@ -0,0 +1,645 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import contextlib
|
||||
import sys
|
||||
from collections.abc import AsyncIterable, Callable, MutableMapping, Sequence
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Generic
|
||||
|
||||
from agent_framework import (
|
||||
AgentMiddlewareTypes,
|
||||
AgentResponse,
|
||||
AgentResponseUpdate,
|
||||
AgentThread,
|
||||
BaseAgent,
|
||||
ChatMessage,
|
||||
Content,
|
||||
ContextProvider,
|
||||
FunctionTool,
|
||||
Role,
|
||||
ToolProtocol,
|
||||
get_logger,
|
||||
normalize_messages,
|
||||
)
|
||||
from agent_framework._types import normalize_tools
|
||||
from agent_framework.exceptions import ServiceException, ServiceInitializationError
|
||||
from claude_agent_sdk import (
|
||||
ClaudeAgentOptions as SDKOptions,
|
||||
)
|
||||
from claude_agent_sdk import (
|
||||
ClaudeSDKClient,
|
||||
ResultMessage,
|
||||
SdkMcpTool,
|
||||
create_sdk_mcp_server,
|
||||
)
|
||||
from claude_agent_sdk.types import StreamEvent
|
||||
from pydantic import ValidationError
|
||||
|
||||
from ._settings import ClaudeAgentSettings
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from typing import TypeVar # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import TypeVar # type: ignore # pragma: no cover
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import TypedDict # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import TypedDict # pragma: no cover
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from claude_agent_sdk import (
|
||||
AgentDefinition,
|
||||
CanUseTool,
|
||||
HookMatcher,
|
||||
McpServerConfig,
|
||||
PermissionMode,
|
||||
SandboxSettings,
|
||||
SdkBeta,
|
||||
)
|
||||
|
||||
__all__ = ["ClaudeAgent", "ClaudeAgentOptions"]
|
||||
|
||||
logger = get_logger("agent_framework.claude")
|
||||
|
||||
# Name of the in-process MCP server that hosts Agent Framework tools.
|
||||
# FunctionTool instances are converted to SDK MCP tools and served
|
||||
# through this server, as Claude Code CLI only supports tools via MCP.
|
||||
TOOLS_MCP_SERVER_NAME = "_agent_framework_tools"
|
||||
|
||||
|
||||
class ClaudeAgentOptions(TypedDict, total=False):
|
||||
"""Claude Agent-specific options."""
|
||||
|
||||
system_prompt: str
|
||||
"""System prompt for the agent."""
|
||||
|
||||
cli_path: str | Path
|
||||
"""Path to Claude CLI executable. Default: auto-detected."""
|
||||
|
||||
cwd: str | Path
|
||||
"""Working directory for Claude CLI. Default: current working directory."""
|
||||
|
||||
env: dict[str, str]
|
||||
"""Environment variables to pass to CLI."""
|
||||
|
||||
settings: str
|
||||
"""Path to Claude settings file."""
|
||||
|
||||
model: str
|
||||
"""Model to use ("sonnet", "opus", "haiku"). Default: "sonnet"."""
|
||||
|
||||
fallback_model: str
|
||||
"""Fallback model if primary fails."""
|
||||
|
||||
max_thinking_tokens: int
|
||||
"""Maximum tokens for thinking blocks."""
|
||||
|
||||
allowed_tools: list[str]
|
||||
"""Allowlist of tools. If set, Claude can ONLY use tools in this list."""
|
||||
|
||||
disallowed_tools: list[str]
|
||||
"""Blocklist of tools. Claude cannot use these tools."""
|
||||
|
||||
mcp_servers: dict[str, "McpServerConfig"]
|
||||
"""MCP server configurations for external tools."""
|
||||
|
||||
permission_mode: "PermissionMode"
|
||||
"""Permission handling mode ("default", "acceptEdits", "plan", "bypassPermissions")."""
|
||||
|
||||
can_use_tool: "CanUseTool"
|
||||
"""Permission callback for tool use."""
|
||||
|
||||
max_turns: int
|
||||
"""Maximum conversation turns."""
|
||||
|
||||
max_budget_usd: float
|
||||
"""Budget limit in USD."""
|
||||
|
||||
hooks: dict[str, list["HookMatcher"]]
|
||||
"""Pre/post tool hooks."""
|
||||
|
||||
add_dirs: list[str | Path]
|
||||
"""Additional directories to add to context."""
|
||||
|
||||
sandbox: "SandboxSettings"
|
||||
"""Sandbox configuration for bash isolation."""
|
||||
|
||||
agents: dict[str, "AgentDefinition"]
|
||||
"""Custom agent definitions."""
|
||||
|
||||
output_format: dict[str, Any]
|
||||
"""Structured output format (JSON schema)."""
|
||||
|
||||
enable_file_checkpointing: bool
|
||||
"""Enable file checkpointing for rewind."""
|
||||
|
||||
betas: list["SdkBeta"]
|
||||
"""Beta features to enable."""
|
||||
|
||||
|
||||
TOptions = TypeVar(
|
||||
"TOptions",
|
||||
bound=TypedDict, # type: ignore[valid-type]
|
||||
default="ClaudeAgentOptions",
|
||||
covariant=True,
|
||||
)
|
||||
|
||||
|
||||
class ClaudeAgent(BaseAgent, Generic[TOptions]):
|
||||
"""Claude Agent using Claude Code CLI.
|
||||
|
||||
Wraps the Claude Agent SDK to provide agentic capabilities including
|
||||
tool use, session management, and streaming responses.
|
||||
|
||||
This agent communicates with Claude through the Claude Code CLI,
|
||||
enabling access to Claude's full agentic capabilities like file
|
||||
editing, code execution, and tool use.
|
||||
|
||||
The agent can be used as an async context manager to ensure proper cleanup:
|
||||
|
||||
Examples:
|
||||
Basic usage with context manager:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework_claude import ClaudeAgent
|
||||
|
||||
async with ClaudeAgent(
|
||||
instructions="You are a helpful assistant.",
|
||||
) as agent:
|
||||
response = await agent.run("Hello!")
|
||||
print(response.text)
|
||||
|
||||
With streaming:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async with ClaudeAgent() as agent:
|
||||
async for update in agent.run_stream("Write a poem"):
|
||||
print(update.text, end="", flush=True)
|
||||
|
||||
With session management:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async with ClaudeAgent() as agent:
|
||||
thread = agent.get_new_thread()
|
||||
await agent.run("Remember my name is Alice", thread=thread)
|
||||
response = await agent.run("What's my name?", thread=thread)
|
||||
# Claude will remember "Alice" from the same session
|
||||
|
||||
With Agent Framework tools:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework import tool
|
||||
|
||||
@tool
|
||||
def greet(name: str) -> str:
|
||||
\"\"\"Greet someone by name.\"\"\"
|
||||
return f"Hello, {name}!"
|
||||
|
||||
async with ClaudeAgent(tools=[greet]) as agent:
|
||||
response = await agent.run("Greet Alice")
|
||||
"""
|
||||
|
||||
AGENT_PROVIDER_NAME: ClassVar[str] = "anthropic.claude"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
instructions: str | None = None,
|
||||
*,
|
||||
client: ClaudeSDKClient | None = None,
|
||||
id: str | None = None,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
context_provider: ContextProvider | None = None,
|
||||
middleware: Sequence[AgentMiddlewareTypes] | None = None,
|
||||
tools: ToolProtocol
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| str
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] | str]
|
||||
| None = None,
|
||||
default_options: TOptions | MutableMapping[str, Any] | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize a ClaudeAgent instance.
|
||||
|
||||
Args:
|
||||
instructions: System prompt for the agent.
|
||||
|
||||
Keyword Args:
|
||||
client: Optional pre-configured ClaudeSDKClient instance. If not provided,
|
||||
a new client will be created using the other parameters.
|
||||
id: Unique identifier for the agent.
|
||||
name: Name of the agent.
|
||||
description: Description of the agent.
|
||||
context_provider: Context provider for the agent.
|
||||
middleware: List of middleware.
|
||||
tools: Tools for the agent. Can be:
|
||||
- Strings for built-in tools (e.g., "Read", "Write", "Bash", "Glob")
|
||||
- Functions or ToolProtocol instances for custom tools
|
||||
default_options: Default ClaudeAgentOptions including system_prompt, model, etc.
|
||||
env_file_path: Path to .env file.
|
||||
env_file_encoding: Encoding of .env file.
|
||||
"""
|
||||
super().__init__(
|
||||
id=id,
|
||||
name=name,
|
||||
description=description,
|
||||
context_provider=context_provider,
|
||||
middleware=middleware,
|
||||
)
|
||||
|
||||
self._client = client
|
||||
self._owns_client = client is None
|
||||
|
||||
# Parse options
|
||||
opts: dict[str, Any] = dict(default_options) if default_options else {}
|
||||
|
||||
# Handle instructions parameter - set as system_prompt in options
|
||||
if instructions is not None:
|
||||
opts["system_prompt"] = instructions
|
||||
|
||||
cli_path = opts.pop("cli_path", None)
|
||||
model = opts.pop("model", None)
|
||||
cwd = opts.pop("cwd", None)
|
||||
permission_mode = opts.pop("permission_mode", None)
|
||||
max_turns = opts.pop("max_turns", None)
|
||||
max_budget_usd = opts.pop("max_budget_usd", None)
|
||||
self._mcp_servers: dict[str, Any] = opts.pop("mcp_servers", None) or {}
|
||||
|
||||
# Load settings from environment and options
|
||||
try:
|
||||
self._settings = ClaudeAgentSettings(
|
||||
cli_path=cli_path,
|
||||
model=model,
|
||||
cwd=cwd,
|
||||
permission_mode=permission_mode,
|
||||
max_turns=max_turns,
|
||||
max_budget_usd=max_budget_usd,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
except ValidationError as ex:
|
||||
raise ServiceInitializationError("Failed to create Claude Agent settings.", ex) from ex
|
||||
|
||||
# Separate built-in tools (strings) from custom tools (callables/ToolProtocol)
|
||||
self._builtin_tools: list[str] = []
|
||||
self._custom_tools: list[ToolProtocol | MutableMapping[str, Any]] = []
|
||||
self._normalize_tools(tools)
|
||||
|
||||
self._default_options = opts
|
||||
self._started = False
|
||||
self._current_session_id: str | None = None
|
||||
|
||||
def _normalize_tools(
|
||||
self,
|
||||
tools: ToolProtocol
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| str
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] | str]
|
||||
| None,
|
||||
) -> None:
|
||||
"""Separate built-in tools (strings) from custom tools.
|
||||
|
||||
Args:
|
||||
tools: Mixed list of tool names and custom tools.
|
||||
"""
|
||||
if tools is None:
|
||||
return
|
||||
|
||||
# Normalize to sequence
|
||||
if isinstance(tools, str):
|
||||
tools_list: Sequence[Any] = [tools]
|
||||
elif isinstance(tools, (ToolProtocol, MutableMapping)) or callable(tools):
|
||||
tools_list = [tools]
|
||||
else:
|
||||
tools_list = list(tools)
|
||||
|
||||
for tool in tools_list:
|
||||
if isinstance(tool, str):
|
||||
self._builtin_tools.append(tool)
|
||||
else:
|
||||
# Use normalize_tools for custom tools
|
||||
normalized = normalize_tools(tool)
|
||||
self._custom_tools.extend(normalized)
|
||||
|
||||
async def __aenter__(self) -> "ClaudeAgent[TOptions]":
|
||||
"""Start the agent when entering async context."""
|
||||
await self.start()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
"""Stop the agent when exiting async context."""
|
||||
await self.stop()
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the Claude SDK client.
|
||||
|
||||
This method initializes the Claude SDK client and establishes a connection
|
||||
to the Claude Code CLI. It is called automatically when using the agent
|
||||
as an async context manager.
|
||||
|
||||
Raises:
|
||||
ServiceException: If the client fails to start.
|
||||
"""
|
||||
await self._ensure_session()
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the Claude SDK client and clean up resources.
|
||||
|
||||
Stops the client if owned by this agent. Called automatically when
|
||||
using the agent as an async context manager.
|
||||
"""
|
||||
if self._client and self._owns_client:
|
||||
with contextlib.suppress(Exception):
|
||||
await self._client.disconnect()
|
||||
|
||||
self._started = False
|
||||
self._current_session_id = None
|
||||
|
||||
async def _ensure_session(self, session_id: str | None = None) -> None:
|
||||
"""Ensure the client is connected for the specified session.
|
||||
|
||||
If the requested session differs from the current one, recreates the client.
|
||||
|
||||
Args:
|
||||
session_id: The session ID to use, or None for a new session.
|
||||
"""
|
||||
needs_new_client = (
|
||||
not self._started or self._client is None or (session_id and session_id != self._current_session_id)
|
||||
)
|
||||
|
||||
if needs_new_client:
|
||||
# Stop existing client if any
|
||||
if self._client and self._owns_client:
|
||||
with contextlib.suppress(Exception):
|
||||
await self._client.disconnect()
|
||||
self._started = False
|
||||
|
||||
# Create new client with resume option if needed
|
||||
opts = self._prepare_client_options(resume_session_id=session_id)
|
||||
self._client = ClaudeSDKClient(options=opts)
|
||||
self._owns_client = True
|
||||
|
||||
try:
|
||||
await self._client.connect()
|
||||
self._started = True
|
||||
self._current_session_id = session_id
|
||||
except Exception as ex:
|
||||
self._client = None
|
||||
raise ServiceException(f"Failed to start Claude SDK client: {ex}") from ex
|
||||
|
||||
def _prepare_client_options(self, resume_session_id: str | None = None) -> SDKOptions:
|
||||
"""Prepare SDK options for client initialization.
|
||||
|
||||
Args:
|
||||
resume_session_id: Optional session ID to resume.
|
||||
|
||||
Returns:
|
||||
SDKOptions instance configured for the client.
|
||||
"""
|
||||
opts: dict[str, Any] = {}
|
||||
|
||||
# Set resume option if provided
|
||||
if resume_session_id:
|
||||
opts["resume"] = resume_session_id
|
||||
|
||||
# Apply settings from environment
|
||||
if self._settings.cli_path:
|
||||
opts["cli_path"] = self._settings.cli_path
|
||||
if self._settings.model:
|
||||
opts["model"] = self._settings.model
|
||||
if self._settings.cwd:
|
||||
opts["cwd"] = self._settings.cwd
|
||||
if self._settings.permission_mode:
|
||||
opts["permission_mode"] = self._settings.permission_mode
|
||||
if self._settings.max_turns:
|
||||
opts["max_turns"] = self._settings.max_turns
|
||||
if self._settings.max_budget_usd:
|
||||
opts["max_budget_usd"] = self._settings.max_budget_usd
|
||||
|
||||
# Apply default options
|
||||
for key, value in self._default_options.items():
|
||||
if value is not None:
|
||||
opts[key] = value
|
||||
|
||||
# Add built-in tools (strings like "Read", "Write", "Bash")
|
||||
if self._builtin_tools:
|
||||
opts["tools"] = self._builtin_tools
|
||||
|
||||
# Prepare custom tools (FunctionTool instances)
|
||||
custom_tools_server, custom_tool_names = (
|
||||
self._prepare_tools(self._custom_tools) if self._custom_tools else (None, [])
|
||||
)
|
||||
|
||||
# MCP servers - merge user-provided servers with custom tools server
|
||||
mcp_servers = dict(self._mcp_servers) if self._mcp_servers else {}
|
||||
if custom_tools_server:
|
||||
mcp_servers[TOOLS_MCP_SERVER_NAME] = custom_tools_server
|
||||
if mcp_servers:
|
||||
opts["mcp_servers"] = mcp_servers
|
||||
|
||||
# Add custom tools to allowed_tools so they can be executed
|
||||
if custom_tool_names:
|
||||
existing_allowed = opts.get("allowed_tools", [])
|
||||
opts["allowed_tools"] = list(existing_allowed) + custom_tool_names
|
||||
|
||||
# Always enable partial messages for streaming support
|
||||
opts["include_partial_messages"] = True
|
||||
|
||||
return SDKOptions(**opts)
|
||||
|
||||
def _prepare_tools(
|
||||
self,
|
||||
tools: list[ToolProtocol | MutableMapping[str, Any]],
|
||||
) -> tuple[Any, list[str]]:
|
||||
"""Convert Agent Framework tools to SDK MCP server.
|
||||
|
||||
Args:
|
||||
tools: List of Agent Framework tools.
|
||||
|
||||
Returns:
|
||||
Tuple of (MCP server config, list of allowed tool names).
|
||||
"""
|
||||
sdk_tools: list[SdkMcpTool[Any]] = []
|
||||
tool_names: list[str] = []
|
||||
|
||||
for tool in tools:
|
||||
if isinstance(tool, FunctionTool):
|
||||
sdk_tools.append(self._function_tool_to_sdk_mcp_tool(tool))
|
||||
# Claude Agent SDK convention: MCP tools use format "mcp__{server}__{tool}"
|
||||
tool_names.append(f"mcp__{TOOLS_MCP_SERVER_NAME}__{tool.name}")
|
||||
elif isinstance(tool, ToolProtocol):
|
||||
logger.debug(f"Unsupported tool type: {type(tool)}")
|
||||
|
||||
if not sdk_tools:
|
||||
return None, []
|
||||
|
||||
return create_sdk_mcp_server(name=TOOLS_MCP_SERVER_NAME, tools=sdk_tools), tool_names
|
||||
|
||||
def _function_tool_to_sdk_mcp_tool(self, func_tool: FunctionTool[Any, Any]) -> SdkMcpTool[Any]:
|
||||
"""Convert a FunctionTool to an SDK MCP tool.
|
||||
|
||||
Args:
|
||||
func_tool: The FunctionTool to convert.
|
||||
|
||||
Returns:
|
||||
An SdkMcpTool instance.
|
||||
"""
|
||||
|
||||
async def handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Handler that invokes the FunctionTool."""
|
||||
try:
|
||||
if func_tool.input_model:
|
||||
args_instance = func_tool.input_model(**args)
|
||||
result = await func_tool.invoke(arguments=args_instance)
|
||||
else:
|
||||
result = await func_tool.invoke(arguments=args)
|
||||
return {"content": [{"type": "text", "text": str(result)}]}
|
||||
except Exception as e:
|
||||
return {"content": [{"type": "text", "text": f"Error: {e}"}]}
|
||||
|
||||
# Get JSON schema from pydantic model
|
||||
schema: dict[str, Any] = func_tool.input_model.model_json_schema() if func_tool.input_model else {}
|
||||
input_schema: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": schema.get("properties", {}),
|
||||
"required": schema.get("required", []),
|
||||
}
|
||||
|
||||
return SdkMcpTool(
|
||||
name=func_tool.name,
|
||||
description=func_tool.description,
|
||||
input_schema=input_schema,
|
||||
handler=handler,
|
||||
)
|
||||
|
||||
async def _apply_runtime_options(self, options: dict[str, Any] | None) -> None:
|
||||
"""Apply runtime options that can be changed dynamically.
|
||||
|
||||
The Claude SDK supports changing model and permission_mode after connection.
|
||||
|
||||
Args:
|
||||
options: Runtime options to apply.
|
||||
"""
|
||||
if not options or not self._client:
|
||||
return
|
||||
|
||||
if "model" in options:
|
||||
await self._client.set_model(options["model"])
|
||||
|
||||
if "permission_mode" in options:
|
||||
await self._client.set_permission_mode(options["permission_mode"])
|
||||
|
||||
def _format_prompt(self, messages: list[ChatMessage] | None) -> str:
|
||||
"""Format messages into a prompt string.
|
||||
|
||||
Args:
|
||||
messages: List of chat messages.
|
||||
|
||||
Returns:
|
||||
Formatted prompt string.
|
||||
"""
|
||||
if not messages:
|
||||
return ""
|
||||
return "\n".join([msg.text or "" for msg in messages])
|
||||
|
||||
async def run(
|
||||
self,
|
||||
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
|
||||
*,
|
||||
thread: AgentThread | None = None,
|
||||
options: TOptions | MutableMapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AgentResponse[Any]:
|
||||
"""Run the agent with the given messages.
|
||||
|
||||
Args:
|
||||
messages: The messages to process.
|
||||
|
||||
Keyword Args:
|
||||
thread: The conversation thread. If thread has service_thread_id set,
|
||||
the agent will resume that session.
|
||||
options: Runtime options (model, permission_mode can be changed per-request).
|
||||
kwargs: Additional keyword arguments.
|
||||
|
||||
Returns:
|
||||
AgentResponse with the agent's response.
|
||||
"""
|
||||
thread = thread or self.get_new_thread()
|
||||
return await AgentResponse.from_agent_response_generator(
|
||||
self.run_stream(messages, thread=thread, options=options, **kwargs)
|
||||
)
|
||||
|
||||
async def run_stream(
|
||||
self,
|
||||
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
|
||||
*,
|
||||
thread: AgentThread | None = None,
|
||||
options: TOptions | MutableMapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[AgentResponseUpdate]:
|
||||
"""Stream the agent's response.
|
||||
|
||||
Args:
|
||||
messages: The messages to process.
|
||||
|
||||
Keyword Args:
|
||||
thread: The conversation thread. If thread has service_thread_id set,
|
||||
the agent will resume that session.
|
||||
options: Runtime options (model, permission_mode can be changed per-request).
|
||||
kwargs: Additional keyword arguments.
|
||||
|
||||
Yields:
|
||||
AgentResponseUpdate objects containing chunks of the response.
|
||||
"""
|
||||
thread = thread or self.get_new_thread()
|
||||
|
||||
# Ensure we're connected to the right session
|
||||
await self._ensure_session(thread.service_thread_id)
|
||||
|
||||
if not self._client:
|
||||
raise ServiceException("Claude SDK client not initialized.")
|
||||
|
||||
prompt = self._format_prompt(normalize_messages(messages))
|
||||
|
||||
# Apply runtime options (model, permission_mode)
|
||||
await self._apply_runtime_options(dict(options) if options else None)
|
||||
|
||||
session_id: str | None = None
|
||||
|
||||
await self._client.query(prompt)
|
||||
async for message in self._client.receive_response():
|
||||
if isinstance(message, StreamEvent):
|
||||
# Handle streaming events - extract text/thinking deltas
|
||||
event = message.event
|
||||
if event.get("type") == "content_block_delta":
|
||||
delta = event.get("delta", {})
|
||||
delta_type = delta.get("type")
|
||||
if delta_type == "text_delta":
|
||||
text = delta.get("text", "")
|
||||
if text:
|
||||
yield AgentResponseUpdate(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[Content.from_text(text=text, raw_representation=message)],
|
||||
raw_representation=message,
|
||||
)
|
||||
elif delta_type == "thinking_delta":
|
||||
thinking = delta.get("thinking", "")
|
||||
if thinking:
|
||||
yield AgentResponseUpdate(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[Content.from_text_reasoning(text=thinking, raw_representation=message)],
|
||||
raw_representation=message,
|
||||
)
|
||||
elif isinstance(message, ResultMessage):
|
||||
session_id = message.session_id
|
||||
|
||||
# Update thread with session ID
|
||||
if session_id:
|
||||
thread.service_thread_id = session_id
|
||||
@@ -0,0 +1,51 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import ClassVar
|
||||
|
||||
from agent_framework._pydantic import AFBaseSettings
|
||||
|
||||
__all__ = ["ClaudeAgentSettings"]
|
||||
|
||||
|
||||
class ClaudeAgentSettings(AFBaseSettings):
|
||||
"""Claude Agent settings.
|
||||
|
||||
The settings are first loaded from environment variables with the prefix 'CLAUDE_AGENT_'.
|
||||
If the environment variables are not found, the settings can be loaded from a .env file
|
||||
with the encoding 'utf-8'. If the settings are not found in the .env file, the settings
|
||||
are ignored; however, validation will fail alerting that the settings are missing.
|
||||
|
||||
Keyword Args:
|
||||
cli_path: The path to Claude CLI executable.
|
||||
model: The model to use (sonnet, opus, haiku).
|
||||
cwd: The working directory for Claude CLI.
|
||||
permission_mode: Permission mode (default, acceptEdits, plan, bypassPermissions).
|
||||
max_turns: Maximum number of conversation turns.
|
||||
max_budget_usd: Maximum budget in USD.
|
||||
env_file_path: If provided, the .env settings are read from this file path location.
|
||||
env_file_encoding: The encoding of the .env file, defaults to 'utf-8'.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.anthropic import ClaudeAgentSettings
|
||||
|
||||
# Using environment variables
|
||||
# Set CLAUDE_AGENT_MODEL=sonnet
|
||||
# CLAUDE_AGENT_PERMISSION_MODE=default
|
||||
|
||||
# Or passing parameters directly
|
||||
settings = ClaudeAgentSettings(model="sonnet")
|
||||
|
||||
# Or loading from a .env file
|
||||
settings = ClaudeAgentSettings(env_file_path="path/to/.env")
|
||||
"""
|
||||
|
||||
env_prefix: ClassVar[str] = "CLAUDE_AGENT_"
|
||||
|
||||
cli_path: str | None = None
|
||||
model: str | None = None
|
||||
cwd: str | None = None
|
||||
permission_mode: str | None = None
|
||||
max_turns: int | None = None
|
||||
max_budget_usd: float | None = None
|
||||
@@ -0,0 +1,89 @@
|
||||
[project]
|
||||
name = "agent-framework-claude"
|
||||
description = "Claude Agent SDK integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true"
|
||||
urls.issues = "https://github.com/microsoft/agent-framework/issues"
|
||||
classifiers = [
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core",
|
||||
"claude-agent-sdk>=0.1.25",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
prerelease = "if-necessary-or-explicit"
|
||||
environments = [
|
||||
"sys_platform == 'darwin'",
|
||||
"sys_platform == 'linux'",
|
||||
"sys_platform == 'win32'"
|
||||
]
|
||||
|
||||
[tool.uv-dynamic-versioning]
|
||||
fallback-version = "0.0.0"
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = 'tests'
|
||||
addopts = "-ra -q -r fEX"
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
filterwarnings = [
|
||||
"ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*"
|
||||
]
|
||||
timeout = 120
|
||||
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
|
||||
[tool.coverage.run]
|
||||
omit = [
|
||||
"**/__init__.py"
|
||||
]
|
||||
|
||||
[tool.pyright]
|
||||
extends = "../../pyproject.toml"
|
||||
exclude = ['tests']
|
||||
|
||||
[tool.mypy]
|
||||
plugins = ['pydantic.mypy']
|
||||
strict = true
|
||||
python_version = "3.10"
|
||||
ignore_missing_imports = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
check_untyped_defs = true
|
||||
warn_return_any = true
|
||||
show_error_codes = true
|
||||
warn_unused_ignores = false
|
||||
disallow_incomplete_defs = true
|
||||
disallow_untyped_decorators = true
|
||||
|
||||
[tool.bandit]
|
||||
targets = ["agent_framework_claude"]
|
||||
exclude_dirs = ["tests"]
|
||||
|
||||
[tool.poe]
|
||||
executor.type = "uv"
|
||||
include = "../../shared_tasks.toml"
|
||||
[tool.poe.tasks]
|
||||
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_claude"
|
||||
test = "pytest --cov=agent_framework_claude --cov-report=term-missing:skip-covered tests"
|
||||
|
||||
[build-system]
|
||||
requires = ["flit-core >= 3.11,<4.0"]
|
||||
build-backend = "flit_core.buildapi"
|
||||
@@ -0,0 +1 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
@@ -0,0 +1,714 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from agent_framework import AgentResponseUpdate, AgentThread, ChatMessage, Content, Role, tool
|
||||
|
||||
from agent_framework_claude import ClaudeAgent, ClaudeAgentOptions, ClaudeAgentSettings
|
||||
from agent_framework_claude._agent import TOOLS_MCP_SERVER_NAME
|
||||
|
||||
# region Test ClaudeAgentSettings
|
||||
|
||||
|
||||
class TestClaudeAgentSettings:
|
||||
"""Tests for ClaudeAgentSettings."""
|
||||
|
||||
def test_env_prefix(self) -> None:
|
||||
"""Test that env_prefix is correctly set."""
|
||||
assert ClaudeAgentSettings.env_prefix == "CLAUDE_AGENT_"
|
||||
|
||||
def test_default_values(self) -> None:
|
||||
"""Test default values are None."""
|
||||
settings = ClaudeAgentSettings()
|
||||
assert settings.cli_path is None
|
||||
assert settings.model is None
|
||||
assert settings.cwd is None
|
||||
assert settings.permission_mode is None
|
||||
assert settings.max_turns is None
|
||||
assert settings.max_budget_usd is None
|
||||
|
||||
def test_explicit_values(self) -> None:
|
||||
"""Test explicit values override defaults."""
|
||||
settings = ClaudeAgentSettings(
|
||||
cli_path="/usr/local/bin/claude",
|
||||
model="sonnet",
|
||||
cwd="/home/user/project",
|
||||
permission_mode="default",
|
||||
max_turns=10,
|
||||
max_budget_usd=5.0,
|
||||
)
|
||||
assert settings.cli_path == "/usr/local/bin/claude"
|
||||
assert settings.model == "sonnet"
|
||||
assert settings.cwd == "/home/user/project"
|
||||
assert settings.permission_mode == "default"
|
||||
assert settings.max_turns == 10
|
||||
assert settings.max_budget_usd == 5.0
|
||||
|
||||
def test_env_variable_loading(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Test loading from environment variables."""
|
||||
monkeypatch.setenv("CLAUDE_AGENT_MODEL", "opus")
|
||||
monkeypatch.setenv("CLAUDE_AGENT_MAX_TURNS", "20")
|
||||
settings = ClaudeAgentSettings()
|
||||
assert settings.model == "opus"
|
||||
assert settings.max_turns == 20
|
||||
|
||||
|
||||
# region Test ClaudeAgent Initialization
|
||||
|
||||
|
||||
class TestClaudeAgentInit:
|
||||
"""Tests for ClaudeAgent initialization."""
|
||||
|
||||
def test_default_initialization(self) -> None:
|
||||
"""Test agent initializes with defaults."""
|
||||
agent = ClaudeAgent()
|
||||
assert agent.id is not None
|
||||
assert agent.name is None
|
||||
assert agent.description is None
|
||||
|
||||
def test_with_name_and_description(self) -> None:
|
||||
"""Test agent with name and description."""
|
||||
agent = ClaudeAgent(name="test-agent", description="A test agent")
|
||||
assert agent.name == "test-agent"
|
||||
assert agent.description == "A test agent"
|
||||
|
||||
def test_with_instructions_parameter(self) -> None:
|
||||
"""Test agent with instructions parameter."""
|
||||
agent = ClaudeAgent(instructions="You are a helpful assistant.")
|
||||
assert agent._default_options.get("system_prompt") == "You are a helpful assistant." # type: ignore[reportPrivateUsage]
|
||||
|
||||
def test_with_system_prompt_in_options(self) -> None:
|
||||
"""Test agent with system_prompt in options."""
|
||||
options: ClaudeAgentOptions = {
|
||||
"system_prompt": "You are a helpful assistant.",
|
||||
}
|
||||
agent = ClaudeAgent(default_options=options)
|
||||
assert agent._default_options.get("system_prompt") == "You are a helpful assistant." # type: ignore[reportPrivateUsage]
|
||||
|
||||
def test_with_default_options(self) -> None:
|
||||
"""Test agent with default options."""
|
||||
options: ClaudeAgentOptions = {
|
||||
"model": "sonnet",
|
||||
"permission_mode": "default",
|
||||
"max_turns": 10,
|
||||
}
|
||||
agent = ClaudeAgent(default_options=options)
|
||||
assert agent._settings.model == "sonnet" # type: ignore[reportPrivateUsage]
|
||||
assert agent._settings.permission_mode == "default" # type: ignore[reportPrivateUsage]
|
||||
assert agent._settings.max_turns == 10 # type: ignore[reportPrivateUsage]
|
||||
|
||||
def test_with_function_tool(self) -> None:
|
||||
"""Test agent with function tool."""
|
||||
|
||||
@tool
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone."""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
agent = ClaudeAgent(tools=[greet])
|
||||
assert len(agent._custom_tools) == 1 # type: ignore[reportPrivateUsage]
|
||||
|
||||
def test_with_single_tool(self) -> None:
|
||||
"""Test agent with single tool (not in list)."""
|
||||
|
||||
@tool
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone."""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
agent = ClaudeAgent(tools=greet)
|
||||
assert len(agent._custom_tools) == 1 # type: ignore[reportPrivateUsage]
|
||||
|
||||
def test_with_builtin_tools(self) -> None:
|
||||
"""Test agent with built-in tool names."""
|
||||
agent = ClaudeAgent(tools=["Read", "Write", "Bash"])
|
||||
assert agent._builtin_tools == ["Read", "Write", "Bash"] # type: ignore[reportPrivateUsage]
|
||||
assert agent._custom_tools == [] # type: ignore[reportPrivateUsage]
|
||||
|
||||
def test_with_mixed_tools(self) -> None:
|
||||
"""Test agent with both built-in and custom tools."""
|
||||
|
||||
@tool
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone."""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
agent = ClaudeAgent(tools=["Read", greet, "Bash"])
|
||||
assert agent._builtin_tools == ["Read", "Bash"] # type: ignore[reportPrivateUsage]
|
||||
assert len(agent._custom_tools) == 1 # type: ignore[reportPrivateUsage]
|
||||
|
||||
|
||||
# region Test ClaudeAgent Lifecycle
|
||||
|
||||
|
||||
class TestClaudeAgentLifecycle:
|
||||
"""Tests for ClaudeAgent tool initialization."""
|
||||
|
||||
def test_custom_tools_stored_from_constructor(self) -> None:
|
||||
"""Test that custom tools from constructor are stored."""
|
||||
|
||||
@tool
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone."""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
agent = ClaudeAgent(tools=[greet])
|
||||
assert len(agent._custom_tools) == 1 # type: ignore[reportPrivateUsage]
|
||||
|
||||
def test_multiple_custom_tools(self) -> None:
|
||||
"""Test agent with multiple custom tools."""
|
||||
|
||||
@tool
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone."""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
@tool
|
||||
def farewell(name: str) -> str:
|
||||
"""Say goodbye."""
|
||||
return f"Goodbye, {name}!"
|
||||
|
||||
agent = ClaudeAgent(tools=[greet, farewell])
|
||||
assert len(agent._custom_tools) == 2 # type: ignore[reportPrivateUsage]
|
||||
|
||||
def test_no_tools(self) -> None:
|
||||
"""Test agent without tools."""
|
||||
agent = ClaudeAgent()
|
||||
assert agent._custom_tools == [] # type: ignore[reportPrivateUsage]
|
||||
assert agent._builtin_tools == [] # type: ignore[reportPrivateUsage]
|
||||
|
||||
|
||||
# region Test ClaudeAgent Run
|
||||
|
||||
|
||||
class TestClaudeAgentRun:
|
||||
"""Tests for ClaudeAgent run method."""
|
||||
|
||||
@staticmethod
|
||||
async def _create_async_generator(items: list[Any]) -> Any:
|
||||
"""Helper to create async generator from list."""
|
||||
for item in items:
|
||||
yield item
|
||||
|
||||
def _create_mock_client(self, messages: list[Any]) -> MagicMock:
|
||||
"""Create a mock ClaudeSDKClient that yields given messages."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client.query = AsyncMock()
|
||||
mock_client.set_model = AsyncMock()
|
||||
mock_client.set_permission_mode = AsyncMock()
|
||||
mock_client.receive_response = MagicMock(return_value=self._create_async_generator(messages))
|
||||
return mock_client
|
||||
|
||||
async def test_run_with_string_message(self) -> None:
|
||||
"""Test run with string message."""
|
||||
from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock
|
||||
from claude_agent_sdk.types import StreamEvent
|
||||
|
||||
messages = [
|
||||
StreamEvent(
|
||||
event={
|
||||
"type": "content_block_delta",
|
||||
"delta": {"type": "text_delta", "text": "Hello!"},
|
||||
},
|
||||
uuid="event-1",
|
||||
session_id="session-123",
|
||||
),
|
||||
AssistantMessage(
|
||||
content=[TextBlock(text="Hello!")],
|
||||
model="claude-sonnet",
|
||||
),
|
||||
ResultMessage(
|
||||
subtype="success",
|
||||
duration_ms=100,
|
||||
duration_api_ms=50,
|
||||
is_error=False,
|
||||
num_turns=1,
|
||||
session_id="session-123",
|
||||
),
|
||||
]
|
||||
mock_client = self._create_mock_client(messages)
|
||||
|
||||
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
|
||||
agent = ClaudeAgent()
|
||||
response = await agent.run("Hello")
|
||||
assert response.text == "Hello!"
|
||||
|
||||
async def test_run_captures_session_id(self) -> None:
|
||||
"""Test that session ID is captured from ResultMessage."""
|
||||
from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock
|
||||
from claude_agent_sdk.types import StreamEvent
|
||||
|
||||
messages = [
|
||||
StreamEvent(
|
||||
event={
|
||||
"type": "content_block_delta",
|
||||
"delta": {"type": "text_delta", "text": "Response"},
|
||||
},
|
||||
uuid="event-1",
|
||||
session_id="test-session-id",
|
||||
),
|
||||
AssistantMessage(
|
||||
content=[TextBlock(text="Response")],
|
||||
model="claude-sonnet",
|
||||
),
|
||||
ResultMessage(
|
||||
subtype="success",
|
||||
duration_ms=100,
|
||||
duration_api_ms=50,
|
||||
is_error=False,
|
||||
num_turns=1,
|
||||
session_id="test-session-id",
|
||||
),
|
||||
]
|
||||
mock_client = self._create_mock_client(messages)
|
||||
|
||||
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
|
||||
agent = ClaudeAgent()
|
||||
thread = agent.get_new_thread()
|
||||
await agent.run("Hello", thread=thread)
|
||||
assert thread.service_thread_id == "test-session-id"
|
||||
|
||||
async def test_run_with_thread(self) -> None:
|
||||
"""Test run with existing thread."""
|
||||
from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock
|
||||
from claude_agent_sdk.types import StreamEvent
|
||||
|
||||
messages = [
|
||||
StreamEvent(
|
||||
event={
|
||||
"type": "content_block_delta",
|
||||
"delta": {"type": "text_delta", "text": "Response"},
|
||||
},
|
||||
uuid="event-1",
|
||||
session_id="session-123",
|
||||
),
|
||||
AssistantMessage(
|
||||
content=[TextBlock(text="Response")],
|
||||
model="claude-sonnet",
|
||||
),
|
||||
ResultMessage(
|
||||
subtype="success",
|
||||
duration_ms=100,
|
||||
duration_api_ms=50,
|
||||
is_error=False,
|
||||
num_turns=1,
|
||||
session_id="session-123",
|
||||
),
|
||||
]
|
||||
mock_client = self._create_mock_client(messages)
|
||||
|
||||
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
|
||||
agent = ClaudeAgent()
|
||||
thread = agent.get_new_thread()
|
||||
thread.service_thread_id = "existing-session"
|
||||
await agent.run("Hello", thread=thread)
|
||||
|
||||
|
||||
# region Test ClaudeAgent Run Stream
|
||||
|
||||
|
||||
class TestClaudeAgentRunStream:
|
||||
"""Tests for ClaudeAgent run_stream method."""
|
||||
|
||||
@staticmethod
|
||||
async def _create_async_generator(items: list[Any]) -> Any:
|
||||
"""Helper to create async generator from list."""
|
||||
for item in items:
|
||||
yield item
|
||||
|
||||
def _create_mock_client(self, messages: list[Any]) -> MagicMock:
|
||||
"""Create a mock ClaudeSDKClient that yields given messages."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client.query = AsyncMock()
|
||||
mock_client.set_model = AsyncMock()
|
||||
mock_client.set_permission_mode = AsyncMock()
|
||||
mock_client.receive_response = MagicMock(return_value=self._create_async_generator(messages))
|
||||
return mock_client
|
||||
|
||||
async def test_run_stream_yields_updates(self) -> None:
|
||||
"""Test run_stream yields AgentResponseUpdate objects."""
|
||||
from claude_agent_sdk import AssistantMessage, ResultMessage, TextBlock
|
||||
from claude_agent_sdk.types import StreamEvent
|
||||
|
||||
messages = [
|
||||
StreamEvent(
|
||||
event={
|
||||
"type": "content_block_delta",
|
||||
"delta": {"type": "text_delta", "text": "Streaming "},
|
||||
},
|
||||
uuid="event-1",
|
||||
session_id="stream-session",
|
||||
),
|
||||
StreamEvent(
|
||||
event={
|
||||
"type": "content_block_delta",
|
||||
"delta": {"type": "text_delta", "text": "response"},
|
||||
},
|
||||
uuid="event-2",
|
||||
session_id="stream-session",
|
||||
),
|
||||
AssistantMessage(
|
||||
content=[TextBlock(text="Streaming response")],
|
||||
model="claude-sonnet",
|
||||
),
|
||||
ResultMessage(
|
||||
subtype="success",
|
||||
duration_ms=100,
|
||||
duration_api_ms=50,
|
||||
is_error=False,
|
||||
num_turns=1,
|
||||
session_id="stream-session",
|
||||
),
|
||||
]
|
||||
mock_client = self._create_mock_client(messages)
|
||||
|
||||
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
|
||||
agent = ClaudeAgent()
|
||||
updates: list[AgentResponseUpdate] = []
|
||||
async for update in agent.run_stream("Hello"):
|
||||
updates.append(update)
|
||||
# StreamEvent yields text deltas
|
||||
assert len(updates) == 2
|
||||
assert updates[0].role == Role.ASSISTANT
|
||||
assert updates[0].text == "Streaming "
|
||||
assert updates[1].text == "response"
|
||||
|
||||
|
||||
# region Test ClaudeAgent Session Management
|
||||
|
||||
|
||||
class TestClaudeAgentSessionManagement:
|
||||
"""Tests for ClaudeAgent session management."""
|
||||
|
||||
def test_get_new_thread(self) -> None:
|
||||
"""Test get_new_thread creates a new thread."""
|
||||
agent = ClaudeAgent()
|
||||
thread = agent.get_new_thread()
|
||||
assert isinstance(thread, AgentThread)
|
||||
assert thread.service_thread_id is None
|
||||
|
||||
def test_get_new_thread_with_service_thread_id(self) -> None:
|
||||
"""Test get_new_thread with existing service_thread_id."""
|
||||
agent = ClaudeAgent()
|
||||
thread = agent.get_new_thread(service_thread_id="existing-session-123")
|
||||
assert isinstance(thread, AgentThread)
|
||||
assert thread.service_thread_id == "existing-session-123"
|
||||
|
||||
def test_thread_inherits_context_provider(self) -> None:
|
||||
"""Test that thread inherits context provider."""
|
||||
mock_provider = MagicMock()
|
||||
agent = ClaudeAgent(context_provider=mock_provider)
|
||||
thread = agent.get_new_thread()
|
||||
assert thread.context_provider == mock_provider
|
||||
|
||||
async def test_ensure_session_creates_client(self) -> None:
|
||||
"""Test _ensure_session creates client when not started."""
|
||||
with patch("agent_framework_claude._agent.ClaudeSDKClient") as mock_client_class:
|
||||
mock_client = MagicMock()
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
agent = ClaudeAgent()
|
||||
await agent._ensure_session(None) # type: ignore[reportPrivateUsage]
|
||||
|
||||
assert agent._started # type: ignore[reportPrivateUsage]
|
||||
mock_client.connect.assert_called_once()
|
||||
|
||||
async def test_ensure_session_recreates_for_different_session(self) -> None:
|
||||
"""Test _ensure_session recreates client for different session ID."""
|
||||
with patch("agent_framework_claude._agent.ClaudeSDKClient") as mock_client_class:
|
||||
mock_client1 = MagicMock()
|
||||
mock_client1.connect = AsyncMock()
|
||||
mock_client1.disconnect = AsyncMock()
|
||||
|
||||
mock_client2 = MagicMock()
|
||||
mock_client2.connect = AsyncMock()
|
||||
|
||||
mock_client_class.side_effect = [mock_client1, mock_client2]
|
||||
|
||||
agent = ClaudeAgent()
|
||||
|
||||
# First session
|
||||
await agent._ensure_session(None) # type: ignore[reportPrivateUsage]
|
||||
assert agent._started # type: ignore[reportPrivateUsage]
|
||||
|
||||
# Different session should recreate client
|
||||
await agent._ensure_session("new-session-id") # type: ignore[reportPrivateUsage]
|
||||
assert agent._current_session_id == "new-session-id" # type: ignore[reportPrivateUsage]
|
||||
mock_client1.disconnect.assert_called_once()
|
||||
|
||||
async def test_ensure_session_reuses_for_same_session(self) -> None:
|
||||
"""Test _ensure_session reuses client for same session ID."""
|
||||
with patch("agent_framework_claude._agent.ClaudeSDKClient") as mock_client_class:
|
||||
mock_client = MagicMock()
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
agent = ClaudeAgent()
|
||||
|
||||
# First call
|
||||
await agent._ensure_session("session-123") # type: ignore[reportPrivateUsage]
|
||||
|
||||
# Same session should not recreate
|
||||
await agent._ensure_session("session-123") # type: ignore[reportPrivateUsage]
|
||||
|
||||
# Only called once
|
||||
assert mock_client_class.call_count == 1
|
||||
|
||||
|
||||
# region Test ClaudeAgent Tool Conversion
|
||||
|
||||
|
||||
class TestClaudeAgentToolConversion:
|
||||
"""Tests for ClaudeAgent tool conversion."""
|
||||
|
||||
def test_prepare_tools_creates_mcp_server(self) -> None:
|
||||
"""Test _prepare_tools creates MCP server for AF tools."""
|
||||
|
||||
@tool
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers."""
|
||||
return a + b
|
||||
|
||||
agent = ClaudeAgent(tools=[add])
|
||||
server, tool_names = agent._prepare_tools(agent._custom_tools) # type: ignore[reportPrivateUsage]
|
||||
|
||||
assert server is not None
|
||||
assert len(tool_names) == 1
|
||||
assert tool_names[0] == f"mcp__{TOOLS_MCP_SERVER_NAME}__add"
|
||||
|
||||
def test_function_tool_to_sdk_mcp_tool(self) -> None:
|
||||
"""Test converting FunctionTool to SDK MCP tool."""
|
||||
|
||||
@tool
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone."""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
agent = ClaudeAgent()
|
||||
sdk_tool = agent._function_tool_to_sdk_mcp_tool(greet) # type: ignore[reportPrivateUsage]
|
||||
|
||||
assert sdk_tool.name == "greet"
|
||||
assert sdk_tool.description == "Greet someone."
|
||||
assert sdk_tool.input_schema is not None
|
||||
assert "properties" in sdk_tool.input_schema # type: ignore[operator]
|
||||
|
||||
async def test_tool_handler_success(self) -> None:
|
||||
"""Test tool handler executes successfully."""
|
||||
|
||||
@tool
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone."""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
agent = ClaudeAgent()
|
||||
sdk_tool = agent._function_tool_to_sdk_mcp_tool(greet) # type: ignore[reportPrivateUsage]
|
||||
|
||||
result = await sdk_tool.handler({"name": "World"})
|
||||
assert result["content"][0]["text"] == "Hello, World!"
|
||||
|
||||
async def test_tool_handler_error(self) -> None:
|
||||
"""Test tool handler handles errors."""
|
||||
|
||||
@tool
|
||||
def failing_tool() -> str:
|
||||
"""A tool that fails."""
|
||||
raise ValueError("Something went wrong")
|
||||
|
||||
agent = ClaudeAgent()
|
||||
sdk_tool = agent._function_tool_to_sdk_mcp_tool(failing_tool) # type: ignore[reportPrivateUsage]
|
||||
|
||||
result = await sdk_tool.handler({})
|
||||
assert "Error:" in result["content"][0]["text"]
|
||||
assert "Something went wrong" in result["content"][0]["text"]
|
||||
|
||||
|
||||
# region Test ClaudeAgent Permissions
|
||||
|
||||
|
||||
class TestClaudeAgentPermissions:
|
||||
"""Tests for ClaudeAgent permission handling."""
|
||||
|
||||
def test_default_permission_mode(self) -> None:
|
||||
"""Test default permission mode."""
|
||||
agent = ClaudeAgent()
|
||||
assert agent._settings.permission_mode is None # type: ignore[reportPrivateUsage]
|
||||
|
||||
def test_permission_mode_from_settings(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Test permission mode from environment settings."""
|
||||
monkeypatch.setenv("CLAUDE_AGENT_PERMISSION_MODE", "acceptEdits")
|
||||
settings = ClaudeAgentSettings()
|
||||
assert settings.permission_mode == "acceptEdits"
|
||||
|
||||
def test_permission_mode_in_options(self) -> None:
|
||||
"""Test permission mode in options."""
|
||||
options: ClaudeAgentOptions = {
|
||||
"permission_mode": "bypassPermissions",
|
||||
}
|
||||
agent = ClaudeAgent(default_options=options)
|
||||
assert agent._settings.permission_mode == "bypassPermissions" # type: ignore[reportPrivateUsage]
|
||||
|
||||
|
||||
# region Test ClaudeAgent Error Handling
|
||||
|
||||
|
||||
class TestClaudeAgentErrorHandling:
|
||||
"""Tests for ClaudeAgent error handling."""
|
||||
|
||||
@staticmethod
|
||||
async def _empty_gen() -> Any:
|
||||
"""Empty async generator."""
|
||||
if False:
|
||||
yield
|
||||
|
||||
async def test_handles_empty_response(self) -> None:
|
||||
"""Test handling of empty response."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client.query = AsyncMock()
|
||||
mock_client.set_model = AsyncMock()
|
||||
mock_client.set_permission_mode = AsyncMock()
|
||||
mock_client.receive_response = MagicMock(return_value=self._empty_gen())
|
||||
|
||||
with patch("agent_framework_claude._agent.ClaudeSDKClient", return_value=mock_client):
|
||||
agent = ClaudeAgent()
|
||||
response = await agent.run("Hello")
|
||||
assert response.messages == []
|
||||
|
||||
|
||||
# region Test Format Prompt
|
||||
|
||||
|
||||
class TestFormatPrompt:
|
||||
"""Tests for _format_prompt method."""
|
||||
|
||||
def test_format_empty_messages(self) -> None:
|
||||
"""Test formatting empty messages."""
|
||||
agent = ClaudeAgent()
|
||||
result = agent._format_prompt([]) # type: ignore[reportPrivateUsage]
|
||||
assert result == ""
|
||||
|
||||
def test_format_none_messages(self) -> None:
|
||||
"""Test formatting None messages."""
|
||||
agent = ClaudeAgent()
|
||||
result = agent._format_prompt(None) # type: ignore[reportPrivateUsage]
|
||||
assert result == ""
|
||||
|
||||
def test_format_user_message(self) -> None:
|
||||
"""Test formatting user message."""
|
||||
agent = ClaudeAgent()
|
||||
msg = ChatMessage(
|
||||
role=Role.USER,
|
||||
contents=[Content.from_text(text="Hello")],
|
||||
)
|
||||
result = agent._format_prompt([msg]) # type: ignore[reportPrivateUsage]
|
||||
assert "Hello" in result
|
||||
|
||||
def test_format_multiple_messages(self) -> None:
|
||||
"""Test formatting multiple messages."""
|
||||
agent = ClaudeAgent()
|
||||
messages = [
|
||||
ChatMessage(role=Role.USER, contents=[Content.from_text(text="Hi")]),
|
||||
ChatMessage(role=Role.ASSISTANT, contents=[Content.from_text(text="Hello!")]),
|
||||
ChatMessage(role=Role.USER, contents=[Content.from_text(text="How are you?")]),
|
||||
]
|
||||
result = agent._format_prompt(messages) # type: ignore[reportPrivateUsage]
|
||||
assert "Hi" in result
|
||||
assert "Hello!" in result
|
||||
assert "How are you?" in result
|
||||
|
||||
|
||||
# region Test Build Options
|
||||
|
||||
|
||||
class TestPrepareClientOptions:
|
||||
"""Tests for _prepare_client_options method."""
|
||||
|
||||
def test_prepare_client_options_with_settings(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Test preparing options with settings."""
|
||||
monkeypatch.setenv("CLAUDE_AGENT_MODEL", "opus")
|
||||
monkeypatch.setenv("CLAUDE_AGENT_MAX_TURNS", "15")
|
||||
|
||||
agent = ClaudeAgent()
|
||||
|
||||
with patch("agent_framework_claude._agent.SDKOptions") as mock_opts:
|
||||
mock_opts.return_value = MagicMock()
|
||||
agent._prepare_client_options() # type: ignore[reportPrivateUsage]
|
||||
call_kwargs = mock_opts.call_args[1]
|
||||
assert call_kwargs.get("model") == "opus"
|
||||
assert call_kwargs.get("max_turns") == 15
|
||||
|
||||
def test_prepare_client_options_with_instructions(self) -> None:
|
||||
"""Test building options with instructions parameter."""
|
||||
agent = ClaudeAgent(instructions="Be helpful")
|
||||
|
||||
with patch("agent_framework_claude._agent.SDKOptions") as mock_opts:
|
||||
mock_opts.return_value = MagicMock()
|
||||
agent._prepare_client_options() # type: ignore[reportPrivateUsage]
|
||||
call_kwargs = mock_opts.call_args[1]
|
||||
assert call_kwargs.get("system_prompt") == "Be helpful"
|
||||
|
||||
def test_prepare_client_options_includes_custom_tools(self) -> None:
|
||||
"""Test that _prepare_client_options includes custom tools MCP server."""
|
||||
|
||||
@tool
|
||||
def greet(name: str) -> str:
|
||||
"""Greet someone."""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
agent = ClaudeAgent(tools=[greet])
|
||||
|
||||
with patch("agent_framework_claude._agent.SDKOptions") as mock_opts:
|
||||
mock_opts.return_value = MagicMock()
|
||||
agent._prepare_client_options() # type: ignore[reportPrivateUsage]
|
||||
call_kwargs = mock_opts.call_args[1]
|
||||
assert "mcp_servers" in call_kwargs
|
||||
assert TOOLS_MCP_SERVER_NAME in call_kwargs["mcp_servers"]
|
||||
|
||||
|
||||
class TestApplyRuntimeOptions:
|
||||
"""Tests for _apply_runtime_options method."""
|
||||
|
||||
async def test_apply_runtime_model(self) -> None:
|
||||
"""Test applying runtime model option."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.set_model = AsyncMock()
|
||||
mock_client.set_permission_mode = AsyncMock()
|
||||
|
||||
agent = ClaudeAgent()
|
||||
agent._client = mock_client # type: ignore[reportPrivateUsage]
|
||||
|
||||
await agent._apply_runtime_options({"model": "opus"}) # type: ignore[reportPrivateUsage]
|
||||
mock_client.set_model.assert_called_once_with("opus")
|
||||
|
||||
async def test_apply_runtime_permission_mode(self) -> None:
|
||||
"""Test applying runtime permission_mode option."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.set_model = AsyncMock()
|
||||
mock_client.set_permission_mode = AsyncMock()
|
||||
|
||||
agent = ClaudeAgent()
|
||||
agent._client = mock_client # type: ignore[reportPrivateUsage]
|
||||
|
||||
await agent._apply_runtime_options({"permission_mode": "acceptEdits"}) # type: ignore[reportPrivateUsage]
|
||||
mock_client.set_permission_mode.assert_called_once_with("acceptEdits")
|
||||
|
||||
async def test_apply_runtime_options_none(self) -> None:
|
||||
"""Test applying None options does nothing."""
|
||||
mock_client = MagicMock()
|
||||
mock_client.set_model = AsyncMock()
|
||||
mock_client.set_permission_mode = AsyncMock()
|
||||
|
||||
agent = ClaudeAgent()
|
||||
agent._client = mock_client # type: ignore[reportPrivateUsage]
|
||||
|
||||
await agent._apply_runtime_options(None) # type: ignore[reportPrivateUsage]
|
||||
mock_client.set_model.assert_not_called()
|
||||
mock_client.set_permission_mode.assert_not_called()
|
||||
@@ -4,7 +4,7 @@ description = "Copilot Studio integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Declarative specification support for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Debug UI for Microsoft Agent Framework with OpenAI-compatible API
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://github.com/microsoft/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Durable Task integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Foundry Local integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "GitHub Copilot integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Experimental modules for Microsoft Agent Framework"
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Mem0 integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Ollama integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://learn.microsoft.com/en-us/agent-framework/"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Microsoft Purview (Graph dataSecurityAndGovernance) integration f
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://github.com/microsoft/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Redis integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260128"
|
||||
version = "1.0.0b260130"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
Reference in New Issue
Block a user