diff --git a/python/packages/claude/agent_framework_claude/_agent.py b/python/packages/claude/agent_framework_claude/_agent.py index e34f507c07f..14f7d2a871b 100644 --- a/python/packages/claude/agent_framework_claude/_agent.py +++ b/python/packages/claude/agent_framework_claude/_agent.py @@ -741,7 +741,13 @@ def _format_prompt(self, messages: list[Message] | None) -> str: """ if not messages: return "" - return "\n".join([msg.text or "" for msg in messages]) + if len(messages) == 1 and messages[0].role == "user": + return messages[0].text or "" + prefix = "The following is conversation history supplied to this agent.\n" + prefix += "Each label identifies the original speaker's role.\n" + prefix += "Use this history as context for your assigned task.\n" + + return prefix + "\n".join(f"[{m.role}]: {m.text or ''}" for m in messages) @property def default_options(self) -> dict[str, Any]: @@ -1026,8 +1032,8 @@ def run( tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None, compaction_strategy: Any = None, tokenizer: Any = None, - function_invocation_kwargs: dict[str, Any] | None = None, - client_kwargs: dict[str, Any] | None = None, + function_invocation_kwargs: Mapping[str, Any] | None = None, + client_kwargs: Mapping[str, Any] | None = None, **kwargs: Any, ) -> Awaitable[AgentResponse[Any]]: ... @@ -1043,8 +1049,8 @@ def run( tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None, compaction_strategy: Any = None, tokenizer: Any = None, - function_invocation_kwargs: dict[str, Any] | None = None, - client_kwargs: dict[str, Any] | None = None, + function_invocation_kwargs: Mapping[str, Any] | None = None, + client_kwargs: Mapping[str, Any] | None = None, **kwargs: Any, ) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ... @@ -1059,8 +1065,8 @@ def run( # pyright: ignore[reportIncompatibleMethodOverride] tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None, compaction_strategy: Any = None, tokenizer: Any = None, - function_invocation_kwargs: dict[str, Any] | None = None, - client_kwargs: dict[str, Any] | None = None, + function_invocation_kwargs: Mapping[str, Any] | None = None, + client_kwargs: Mapping[str, Any] | None = None, **kwargs: Any, ) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: """Run the Claude agent with telemetry enabled.""" diff --git a/python/packages/claude/tests/test_claude_agent.py b/python/packages/claude/tests/test_claude_agent.py index 38359f9d152..1caa157d82d 100644 --- a/python/packages/claude/tests/test_claude_agent.py +++ b/python/packages/claude/tests/test_claude_agent.py @@ -1011,7 +1011,7 @@ def test_format_user_message(self) -> None: contents=[Content.from_text(text="Hello")], ) result = agent._format_prompt([msg]) # type: ignore[reportPrivateUsage] - assert "Hello" in result + assert result == "Hello" def test_format_multiple_messages(self) -> None: """Test formatting multiple messages.""" @@ -1022,9 +1022,49 @@ def test_format_multiple_messages(self) -> None: Message(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 + assert result == ( + "The following is conversation history supplied to this agent.\n" + "Each label identifies the original speaker's role.\n" + "Use this history as context for your assigned task.\n" + "[user]: Hi\n" + "[assistant]: Hello!\n" + "[user]: How are you?" + ) + + def test_format_messages_from_other_agent(self) -> None: + """Test that author names do not replace roles in handed-over history.""" + agent = ClaudeAgent() + messages = [ + Message( + role="assistant", + author_name="previous_agent", + contents=[Content.from_text(text="Hello from previous agent")], + ), + Message(role="assistant", contents=[Content.from_text(text="Hello from a nameless author")]), + ] + result = agent._format_prompt(messages) # type: ignore[reportPrivateUsage] + assert result == ( + "The following is conversation history supplied to this agent.\n" + "Each label identifies the original speaker's role.\n" + "Use this history as context for your assigned task.\n" + "[assistant]: Hello from previous agent\n" + "[assistant]: Hello from a nameless author" + ) + + def test_format_single_assistant_message(self) -> None: + """Test formatting a single assistant message.""" + agent = ClaudeAgent() + msg = Message( + role="assistant", + contents=[Content.from_text(text="Hello from assistant")], + ) + result = agent._format_prompt([msg]) # type: ignore[reportPrivateUsage] + assert result == ( + "The following is conversation history supplied to this agent.\n" + "Each label identifies the original speaker's role.\n" + "Use this history as context for your assigned task.\n" + "[assistant]: Hello from assistant" + ) # region Test Build Options diff --git a/python/samples/02-agents/providers/anthropic/README.md b/python/samples/02-agents/providers/anthropic/README.md index 0db183490a2..f1857624c3e 100644 --- a/python/samples/02-agents/providers/anthropic/README.md +++ b/python/samples/02-agents/providers/anthropic/README.md @@ -16,6 +16,7 @@ This folder contains examples demonstrating how to use Anthropic's Claude models | File | Description | |------|-------------| | [`anthropic_claude_basic.py`](anthropic_claude_basic.py) | Basic usage of ClaudeAgent with streaming, non-streaming, and custom tools. | +| [`anthropic_claude_sequential_agents.py`](anthropic_claude_sequential_agents.py) | Uses SequentialBuilder to pass role-labeled conversation history from a grammar inspector to a second Claude agent. | | [`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. | diff --git a/python/samples/02-agents/providers/anthropic/anthropic_claude_sequential_agents.py b/python/samples/02-agents/providers/anthropic/anthropic_claude_sequential_agents.py new file mode 100644 index 00000000000..bf6f41e8194 --- /dev/null +++ b/python/samples/02-agents/providers/anthropic/anthropic_claude_sequential_agents.py @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio + +from agent_framework import AgentResponse +from agent_framework_claude import ClaudeAgent +from agent_framework_orchestrations import SequentialBuilder +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + + +async def main() -> None: + """ + Anthropic Claude Sequential Agents Example + + Demonstrate conversation history handover between two Claude agents. + + SequentialBuilder passes the original user message and the grammar inspector's + response to the second agent. ClaudeAgent represents that history as a transcript + with role labels inside a single SDK user message, rather than resuming a shared + Claude session. + """ + agents = [ + ClaudeAgent( + instructions="You are an agent that corrects English grammar mistakes.", + name="grammar_inspector" + ), + ClaudeAgent( + instructions="You are an agent that lists the diff between the participants of this conversation", + name="diff_highlighter" + ) + ] + workflow = SequentialBuilder( + participants=agents, + output_from="all" + ).build() + + prompt = "Yesterday she go to the store and buyed two apple." + result = await workflow.run(prompt) + + print(f"[user]\n{prompt}") + for response in result.get_outputs(): + if isinstance(response, AgentResponse): + for message in response.messages: + author = message.author_name or message.role + print(f"\n[{author}]\n{message.text}") + + +if __name__ == "__main__": + asyncio.run(main())