mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Fixed Anthropic and GitHub Copilot samples (#4025)
* Fixed Anthropic advanced example * Small improvement * Simplified skills sample * Fixed custom agent sample * Added service_session_id parameter * Added tests * Resolved comments
This commit is contained in:
committed by
GitHub
Unverified
parent
28e3fc308b
commit
f900febb6f
@@ -324,8 +324,9 @@ class AnthropicClient(
|
||||
self.anthropic_client = anthropic_client
|
||||
self.additional_beta_flags = additional_beta_flags or []
|
||||
self.model_id = anthropic_settings["chat_model_id"]
|
||||
# streaming requires tracking the last function call ID and name
|
||||
# streaming requires tracking the last function call ID, name, and content type
|
||||
self._last_call_id_name: tuple[str, str] | None = None
|
||||
self._last_call_content_type: str | None = None
|
||||
|
||||
# region Static factory methods for hosted tools
|
||||
|
||||
@@ -333,11 +334,13 @@ class AnthropicClient(
|
||||
def get_code_interpreter_tool(
|
||||
*,
|
||||
type_name: str | None = None,
|
||||
name: str = "code_execution",
|
||||
) -> dict[str, Any]:
|
||||
"""Create a code interpreter tool configuration for Anthropic.
|
||||
|
||||
Keyword Args:
|
||||
type_name: Override the tool type name. Defaults to "code_execution_20250825".
|
||||
name: The name for this tool. Defaults to "code_execution".
|
||||
|
||||
Returns:
|
||||
A dict-based tool configuration ready to pass to ChatAgent.
|
||||
@@ -350,17 +353,19 @@ class AnthropicClient(
|
||||
tool = AnthropicClient.get_code_interpreter_tool()
|
||||
agent = AnthropicClient().as_agent(tools=[tool])
|
||||
"""
|
||||
return {"type": type_name or "code_execution_20250825"}
|
||||
return {"type": type_name or "code_execution_20250825", "name": name}
|
||||
|
||||
@staticmethod
|
||||
def get_web_search_tool(
|
||||
*,
|
||||
type_name: str | None = None,
|
||||
name: str = "web_search",
|
||||
) -> dict[str, Any]:
|
||||
"""Create a web search tool configuration for Anthropic.
|
||||
|
||||
Keyword Args:
|
||||
type_name: Override the tool type name. Defaults to "web_search_20250305".
|
||||
name: The name for this tool. Defaults to "web_search".
|
||||
|
||||
Returns:
|
||||
A dict-based tool configuration ready to pass to ChatAgent.
|
||||
@@ -373,7 +378,7 @@ class AnthropicClient(
|
||||
tool = AnthropicClient.get_web_search_tool()
|
||||
agent = AnthropicClient().as_agent(tools=[tool])
|
||||
"""
|
||||
return {"type": type_name or "web_search_20250305"}
|
||||
return {"type": type_name or "web_search_20250305", "name": name}
|
||||
|
||||
@staticmethod
|
||||
def get_mcp_tool(
|
||||
@@ -661,8 +666,27 @@ class AnthropicClient(
|
||||
"content": content.result if content.result is not None else "",
|
||||
"is_error": content.exception is not None,
|
||||
})
|
||||
case "mcp_server_tool_call":
|
||||
mcp_call: dict[str, Any] = {
|
||||
"type": "mcp_tool_use",
|
||||
"id": content.call_id,
|
||||
"name": content.tool_name,
|
||||
"server_name": content.server_name or "",
|
||||
"input": content.parse_arguments() or {},
|
||||
}
|
||||
a_content.append(mcp_call)
|
||||
case "mcp_server_tool_result":
|
||||
mcp_result: dict[str, Any] = {
|
||||
"type": "mcp_tool_result",
|
||||
"tool_use_id": content.call_id,
|
||||
"content": content.output if content.output is not None else "",
|
||||
}
|
||||
a_content.append(mcp_result)
|
||||
case "text_reasoning":
|
||||
a_content.append({"type": "thinking", "thinking": content.text})
|
||||
thinking_block: dict[str, Any] = {"type": "thinking", "thinking": content.text}
|
||||
if content.protected_data:
|
||||
thinking_block["signature"] = content.protected_data
|
||||
a_content.append(thinking_block)
|
||||
case _:
|
||||
logger.debug(f"Ignoring unsupported content type: {content.type} for now")
|
||||
|
||||
@@ -866,12 +890,13 @@ class AnthropicClient(
|
||||
)
|
||||
case "tool_use" | "mcp_tool_use" | "server_tool_use":
|
||||
self._last_call_id_name = (content_block.id, content_block.name)
|
||||
self._last_call_content_type = content_block.type
|
||||
if content_block.type == "mcp_tool_use":
|
||||
contents.append(
|
||||
Content.from_mcp_server_tool_call(
|
||||
call_id=content_block.id,
|
||||
tool_name=content_block.name,
|
||||
server_name=None,
|
||||
server_name=getattr(content_block, "server_name", None),
|
||||
arguments=content_block.input,
|
||||
raw_representation=content_block,
|
||||
)
|
||||
@@ -1129,24 +1154,32 @@ class AnthropicClient(
|
||||
)
|
||||
)
|
||||
case "input_json_delta":
|
||||
# For streaming argument deltas, only pass call_id and arguments.
|
||||
# Pass empty string for name - it causes ag-ui to emit duplicate ToolCallStartEvents
|
||||
# since it triggers on `if content.name:`. The initial tool_use event already
|
||||
# provides the name, so deltas should only carry incremental arguments.
|
||||
# This matches OpenAI's behavior where streaming chunks have name="".
|
||||
call_id, _name = self._last_call_id_name if self._last_call_id_name else ("", "")
|
||||
contents.append(
|
||||
Content.from_function_call(
|
||||
call_id=call_id,
|
||||
name="",
|
||||
arguments=content_block.partial_json,
|
||||
raw_representation=content_block,
|
||||
# Skip argument deltas for MCP tools — execution is handled server-side.
|
||||
if self._last_call_content_type == "mcp_tool_use":
|
||||
pass
|
||||
else:
|
||||
call_id = self._last_call_id_name[0] if self._last_call_id_name else ""
|
||||
contents.append(
|
||||
Content.from_function_call(
|
||||
call_id=call_id,
|
||||
name="",
|
||||
arguments=content_block.partial_json,
|
||||
raw_representation=content_block,
|
||||
)
|
||||
)
|
||||
)
|
||||
case "thinking" | "thinking_delta":
|
||||
contents.append(
|
||||
Content.from_text_reasoning(
|
||||
text=content_block.thinking,
|
||||
protected_data=getattr(content_block, "signature", None),
|
||||
raw_representation=content_block,
|
||||
)
|
||||
)
|
||||
case "signature_delta":
|
||||
contents.append(
|
||||
Content.from_text_reasoning(
|
||||
text=None,
|
||||
protected_data=content_block.signature,
|
||||
raw_representation=content_block,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -220,6 +220,119 @@ def test_prepare_message_for_anthropic_text_reasoning(mock_anthropic_client: Mag
|
||||
assert len(result["content"]) == 1
|
||||
assert result["content"][0]["type"] == "thinking"
|
||||
assert result["content"][0]["thinking"] == "Let me think about this..."
|
||||
assert "signature" not in result["content"][0]
|
||||
|
||||
|
||||
def test_prepare_message_for_anthropic_text_reasoning_with_signature(mock_anthropic_client: MagicMock) -> None:
|
||||
"""Test converting text reasoning message with signature to Anthropic format."""
|
||||
client = create_test_anthropic_client(mock_anthropic_client)
|
||||
message = Message(
|
||||
role="assistant",
|
||||
contents=[Content.from_text_reasoning(text="Let me think about this...", protected_data="sig_abc123")],
|
||||
)
|
||||
|
||||
result = client._prepare_message_for_anthropic(message)
|
||||
|
||||
assert result["role"] == "assistant"
|
||||
assert len(result["content"]) == 1
|
||||
assert result["content"][0]["type"] == "thinking"
|
||||
assert result["content"][0]["thinking"] == "Let me think about this..."
|
||||
assert result["content"][0]["signature"] == "sig_abc123"
|
||||
|
||||
|
||||
def test_prepare_message_for_anthropic_mcp_server_tool_call(mock_anthropic_client: MagicMock) -> None:
|
||||
"""Test converting MCP server tool call message to Anthropic format."""
|
||||
client = create_test_anthropic_client(mock_anthropic_client)
|
||||
message = Message(
|
||||
role="assistant",
|
||||
contents=[
|
||||
Content.from_mcp_server_tool_call(
|
||||
call_id="mcp_call_123",
|
||||
tool_name="search_docs",
|
||||
server_name="microsoft-learn",
|
||||
arguments={"query": "Azure Functions"},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = client._prepare_message_for_anthropic(message)
|
||||
|
||||
assert result["role"] == "assistant"
|
||||
assert len(result["content"]) == 1
|
||||
assert result["content"][0]["type"] == "mcp_tool_use"
|
||||
assert result["content"][0]["id"] == "mcp_call_123"
|
||||
assert result["content"][0]["name"] == "search_docs"
|
||||
assert result["content"][0]["server_name"] == "microsoft-learn"
|
||||
assert result["content"][0]["input"] == {"query": "Azure Functions"}
|
||||
|
||||
|
||||
def test_prepare_message_for_anthropic_mcp_server_tool_call_no_server_name(mock_anthropic_client: MagicMock) -> None:
|
||||
"""Test converting MCP server tool call with no server name defaults to empty string."""
|
||||
client = create_test_anthropic_client(mock_anthropic_client)
|
||||
message = Message(
|
||||
role="assistant",
|
||||
contents=[
|
||||
Content.from_mcp_server_tool_call(
|
||||
call_id="mcp_call_456",
|
||||
tool_name="list_files",
|
||||
arguments=None,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = client._prepare_message_for_anthropic(message)
|
||||
|
||||
assert result["role"] == "assistant"
|
||||
assert len(result["content"]) == 1
|
||||
assert result["content"][0]["type"] == "mcp_tool_use"
|
||||
assert result["content"][0]["id"] == "mcp_call_456"
|
||||
assert result["content"][0]["name"] == "list_files"
|
||||
assert result["content"][0]["server_name"] == ""
|
||||
assert result["content"][0]["input"] == {}
|
||||
|
||||
|
||||
def test_prepare_message_for_anthropic_mcp_server_tool_result(mock_anthropic_client: MagicMock) -> None:
|
||||
"""Test converting MCP server tool result message to Anthropic format."""
|
||||
client = create_test_anthropic_client(mock_anthropic_client)
|
||||
message = Message(
|
||||
role="tool",
|
||||
contents=[
|
||||
Content.from_mcp_server_tool_result(
|
||||
call_id="mcp_call_123",
|
||||
output="Found 3 results for Azure Functions.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = client._prepare_message_for_anthropic(message)
|
||||
|
||||
assert result["role"] == "user"
|
||||
assert len(result["content"]) == 1
|
||||
assert result["content"][0]["type"] == "mcp_tool_result"
|
||||
assert result["content"][0]["tool_use_id"] == "mcp_call_123"
|
||||
assert result["content"][0]["content"] == "Found 3 results for Azure Functions."
|
||||
|
||||
|
||||
def test_prepare_message_for_anthropic_mcp_server_tool_result_none_output(mock_anthropic_client: MagicMock) -> None:
|
||||
"""Test converting MCP server tool result with None output defaults to empty string."""
|
||||
client = create_test_anthropic_client(mock_anthropic_client)
|
||||
message = Message(
|
||||
role="tool",
|
||||
contents=[
|
||||
Content.from_mcp_server_tool_result(
|
||||
call_id="mcp_call_789",
|
||||
output=None,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = client._prepare_message_for_anthropic(message)
|
||||
|
||||
assert result["role"] == "user"
|
||||
assert len(result["content"]) == 1
|
||||
assert result["content"][0]["type"] == "mcp_tool_result"
|
||||
assert result["content"][0]["tool_use_id"] == "mcp_call_789"
|
||||
assert result["content"][0]["content"] == ""
|
||||
|
||||
|
||||
def test_prepare_messages_for_anthropic_with_system(mock_anthropic_client: MagicMock) -> None:
|
||||
@@ -287,6 +400,7 @@ def test_prepare_tools_for_anthropic_web_search(mock_anthropic_client: MagicMock
|
||||
assert "tools" in result
|
||||
assert len(result["tools"]) == 1
|
||||
assert result["tools"][0]["type"] == "web_search_20250305"
|
||||
assert result["tools"][0]["name"] == "web_search"
|
||||
|
||||
|
||||
def test_prepare_tools_for_anthropic_code_interpreter(mock_anthropic_client: MagicMock) -> None:
|
||||
@@ -300,6 +414,7 @@ def test_prepare_tools_for_anthropic_code_interpreter(mock_anthropic_client: Mag
|
||||
assert "tools" in result
|
||||
assert len(result["tools"]) == 1
|
||||
assert result["tools"][0]["type"] == "code_execution_20250825"
|
||||
assert result["tools"][0]["name"] == "code_execution"
|
||||
|
||||
|
||||
def test_prepare_tools_for_anthropic_mcp_tool(mock_anthropic_client: MagicMock) -> None:
|
||||
@@ -1764,11 +1879,13 @@ def test_parse_thinking_block(mock_anthropic_client: MagicMock) -> None:
|
||||
mock_block = MagicMock()
|
||||
mock_block.type = "thinking"
|
||||
mock_block.thinking = "Let me think about this..."
|
||||
mock_block.signature = "sig_abc123"
|
||||
|
||||
result = client._parse_contents_from_anthropic([mock_block])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].type == "text_reasoning"
|
||||
assert result[0].protected_data == "sig_abc123"
|
||||
|
||||
|
||||
def test_parse_thinking_delta_block(mock_anthropic_client: MagicMock) -> None:
|
||||
@@ -1786,6 +1903,23 @@ def test_parse_thinking_delta_block(mock_anthropic_client: MagicMock) -> None:
|
||||
assert result[0].type == "text_reasoning"
|
||||
|
||||
|
||||
def test_parse_signature_delta_block(mock_anthropic_client: MagicMock) -> None:
|
||||
"""Test parsing signature delta content block."""
|
||||
client = create_test_anthropic_client(mock_anthropic_client)
|
||||
|
||||
# Create mock signature delta block
|
||||
mock_block = MagicMock()
|
||||
mock_block.type = "signature_delta"
|
||||
mock_block.signature = "sig_xyz789"
|
||||
|
||||
result = client._parse_contents_from_anthropic([mock_block])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].type == "text_reasoning"
|
||||
assert result[0].text is None
|
||||
assert result[0].protected_data == "sig_xyz789"
|
||||
|
||||
|
||||
# Citation Tests
|
||||
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ async def main() -> None:
|
||||
print("Agent: ", end="", flush=True)
|
||||
async for chunk in agent.run(query, stream=True):
|
||||
for content in chunk.contents:
|
||||
if content.type == "text_reasoning":
|
||||
if content.type == "text_reasoning" and content.text:
|
||||
print(f"\033[32m{content.text}\033[0m", end="", flush=True)
|
||||
if content.type == "usage":
|
||||
print(f"\n\033[34m[Usage so far: {content.usage_details}]\033[0m\n", end="", flush=True)
|
||||
|
||||
@@ -123,8 +123,8 @@ async def example_with_existing_session_id() -> None:
|
||||
)
|
||||
|
||||
async with agent2:
|
||||
# Create session with existing session ID
|
||||
session = agent2.create_session(service_session_id=existing_session_id)
|
||||
# Get session with existing session ID
|
||||
session = agent2.get_session(service_session_id=existing_session_id)
|
||||
|
||||
query2 = "What was the last city I asked about?"
|
||||
print(f"User: {query2}")
|
||||
|
||||
@@ -47,7 +47,7 @@ async def main() -> None:
|
||||
)
|
||||
|
||||
async with agent:
|
||||
query = "List the first 3 Python files in the current directory"
|
||||
query = "List the first 3 markdown (.md) files in the current directory"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
print(f"Agent: {result.text}\n")
|
||||
|
||||
@@ -36,8 +36,8 @@ async def main() -> None:
|
||||
instructions="You are a helpful agent for creating powerpoint presentations.",
|
||||
tools=client.get_code_interpreter_tool(),
|
||||
default_options={
|
||||
"max_tokens": 20000,
|
||||
"thinking": {"type": "enabled", "budget_tokens": 10000},
|
||||
"max_tokens": 4096,
|
||||
"thinking": {"type": "enabled", "budget_tokens": 2000},
|
||||
"container": {"skills": [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}]},
|
||||
},
|
||||
)
|
||||
@@ -49,7 +49,7 @@ async def main() -> None:
|
||||
"\033[32mAgent Reasoning: (green)\033[0m\n"
|
||||
"\033[34mUsage: (blue)\033[0m\n"
|
||||
)
|
||||
query = "Create a presentation about renewable energy with 5 slides"
|
||||
query = "Create a simple presentation with 2 slides about Python programming"
|
||||
print(f"User: {query}")
|
||||
print("Agent: ", end="", flush=True)
|
||||
files: list[Content] = []
|
||||
@@ -79,9 +79,9 @@ async def main() -> None:
|
||||
file_content = await client.anthropic_client.beta.files.download(
|
||||
file_id=file.file_id, betas=["files-api-2025-04-14"]
|
||||
)
|
||||
with open(Path(__file__).parent / f"renewable_energy-{idx}.pptx", "wb") as f:
|
||||
with open(Path(__file__).parent / f"python_programming-{idx}.pptx", "wb") as f:
|
||||
await file_content.write_to_file(f.name)
|
||||
print(f"File {idx}: renewable_energy-{idx}.pptx saved to disk.")
|
||||
print(f"File {idx}: python_programming-{idx}.pptx saved to disk.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -12,7 +12,6 @@ from agent_framework import (
|
||||
Content,
|
||||
InMemoryHistoryProvider,
|
||||
Message,
|
||||
Role,
|
||||
normalize_messages,
|
||||
)
|
||||
|
||||
@@ -93,7 +92,7 @@ class EchoAgent(BaseAgent):
|
||||
|
||||
if not normalized_messages:
|
||||
response_message = Message(
|
||||
role=Role.ASSISTANT,
|
||||
role="assistant",
|
||||
contents=[
|
||||
Content.from_text(text="Hello! I'm a custom echo agent. Send me a message and I'll echo it back.")
|
||||
],
|
||||
@@ -106,11 +105,11 @@ class EchoAgent(BaseAgent):
|
||||
else:
|
||||
echo_text = f"{self.echo_prefix}[Non-text message received]"
|
||||
|
||||
response_message = Message(role=Role.ASSISTANT, contents=[Content.from_text(text=echo_text)])
|
||||
response_message = Message(role="assistant", contents=[Content.from_text(text=echo_text)])
|
||||
|
||||
# Store messages in session state if provided
|
||||
if session is not None:
|
||||
stored = session.state.setdefault("memory", {}).setdefault("messages", [])
|
||||
stored = session.state.setdefault(InMemoryHistoryProvider.DEFAULT_SOURCE_ID, {}).setdefault("messages", [])
|
||||
stored.extend(normalized_messages)
|
||||
stored.append(response_message)
|
||||
|
||||
@@ -145,7 +144,7 @@ class EchoAgent(BaseAgent):
|
||||
|
||||
yield AgentResponseUpdate(
|
||||
contents=[Content.from_text(text=chunk_text)],
|
||||
role=Role.ASSISTANT,
|
||||
role="assistant",
|
||||
)
|
||||
|
||||
# Small delay to simulate streaming
|
||||
@@ -153,8 +152,8 @@ class EchoAgent(BaseAgent):
|
||||
|
||||
# Store messages in session state if provided
|
||||
if session is not None:
|
||||
complete_response = Message(role=Role.ASSISTANT, contents=[Content.from_text(text=response_text)])
|
||||
stored = session.state.setdefault("memory", {}).setdefault("messages", [])
|
||||
complete_response = Message(role="assistant", contents=[Content.from_text(text=response_text)])
|
||||
stored = session.state.setdefault(InMemoryHistoryProvider.DEFAULT_SOURCE_ID, {}).setdefault("messages", [])
|
||||
stored.extend(normalized_messages)
|
||||
stored.append(complete_response)
|
||||
|
||||
|
||||
@@ -118,8 +118,8 @@ async def example_with_existing_session_id() -> None:
|
||||
)
|
||||
|
||||
async with agent2:
|
||||
# Create session with existing session ID
|
||||
session = agent2.create_session(service_session_id=existing_session_id)
|
||||
# Get session with existing session ID
|
||||
session = agent2.get_session(service_session_id=existing_session_id)
|
||||
|
||||
query2 = "What was the last city I asked about?"
|
||||
print(f"User: {query2}")
|
||||
|
||||
@@ -32,8 +32,8 @@ from typing import Any
|
||||
from agent_framework import (
|
||||
AgentExecutorRequest,
|
||||
AgentExecutorResponse,
|
||||
Message,
|
||||
Executor,
|
||||
Message,
|
||||
Workflow,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
|
||||
Reference in New Issue
Block a user