diff --git a/README.md b/README.md index 1fcb30d..90927d3 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,12 @@ anyio.run(main) `query()` is an async function for querying Claude Code. It returns an `AsyncIterator` of response messages. See [src/claude_agent_sdk/query.py](src/claude_agent_sdk/query.py). ```python -from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, TextBlock +from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage # Simple query async for message in query(prompt="Hello Claude"): if isinstance(message, AssistantMessage): - for block in message.content: - if isinstance(block, TextBlock): - print(block.text) + print(message) # With options options = ClaudeAgentOptions( diff --git a/examples/quick_start.py b/examples/quick_start.py index 3f12855..9cecf1b 100644 --- a/examples/quick_start.py +++ b/examples/quick_start.py @@ -18,9 +18,7 @@ async def basic_example(): async for message in query(prompt="What is 2 + 2?"): if isinstance(message, AssistantMessage): - for block in message.content: - if isinstance(block, TextBlock): - print(f"Claude: {block.text}") + print(f"Claude: {message}") print() @@ -37,9 +35,7 @@ async def with_options_example(): prompt="Explain what Python is in one sentence.", options=options ): if isinstance(message, AssistantMessage): - for block in message.content: - if isinstance(block, TextBlock): - print(f"Claude: {block.text}") + print(f"Claude: {message}") print() @@ -57,9 +53,7 @@ async def with_tools_example(): options=options, ): if isinstance(message, AssistantMessage): - for block in message.content: - if isinstance(block, TextBlock): - print(f"Claude: {block.text}") + print(f"Claude: {message}") elif isinstance(message, ResultMessage) and message.total_cost_usd > 0: print(f"\nCost: ${message.total_cost_usd:.4f}") print() diff --git a/examples/streaming_mode.py b/examples/streaming_mode.py index c949ad3..674083c 100755 --- a/examples/streaming_mode.py +++ b/examples/streaming_mode.py @@ -42,13 +42,9 @@ def display_message(msg): - ResultMessage: "Result ended" + cost if available """ if isinstance(msg, UserMessage): - for block in msg.content: - if isinstance(block, TextBlock): - print(f"User: {block.text}") + print(f"User: {msg}") elif isinstance(msg, AssistantMessage): - for block in msg.content: - if isinstance(block, TextBlock): - print(f"Claude: {block.text}") + print(f"Claude: {msg}") elif isinstance(msg, SystemMessage): # Ignore system messages pass diff --git a/examples/streaming_mode_ipython.py b/examples/streaming_mode_ipython.py index aa63994..17bf403 100644 --- a/examples/streaming_mode_ipython.py +++ b/examples/streaming_mode_ipython.py @@ -22,9 +22,7 @@ async for msg in client.receive_response(): if isinstance(msg, AssistantMessage): - for block in msg.content: - if isinstance(block, TextBlock): - print(f"Claude: {block.text}") + print(f"Claude: {msg}") # ============================================================================ @@ -41,9 +39,7 @@ async def send_and_receive(prompt): await client.query(prompt) async for msg in client.receive_response(): if isinstance(msg, AssistantMessage): - for block in msg.content: - if isinstance(block, TextBlock): - print(f"Claude: {block.text}") + print(f"Claude: {msg}") await send_and_receive("Tell me a short joke") print("\n---\n") @@ -65,9 +61,7 @@ async def send_and_receive(prompt): async def get_response(): async for msg in client.receive_response(): if isinstance(msg, AssistantMessage): - for block in msg.content: - if isinstance(block, TextBlock): - print(f"Claude: {block.text}") + print(f"Claude: {msg}") # Use it multiple times @@ -106,9 +100,7 @@ async def consume_messages(): async for msg in client.receive_messages(): messages_received.append(msg) if isinstance(msg, AssistantMessage): - for block in msg.content: - if isinstance(block, TextBlock): - print(f"Claude: {block.text}") + print(f"Claude: {msg}") # Check if we got a result after interrupt if isinstance(msg, ResultMessage) and interrupt_sent: @@ -154,9 +146,7 @@ async def consume_messages(): async for msg in client.receive_response(): messages.append(msg) if isinstance(msg, AssistantMessage): - for block in msg.content: - if isinstance(block, TextBlock): - print(f"Claude: {block.text}") + print(f"Claude: {msg}") except asyncio.TimeoutError: print("Request timed out after 20 seconds") @@ -201,9 +191,7 @@ async def message_generator(): async for msg in client.receive_response(): if isinstance(msg, AssistantMessage): - for block in msg.content: - if isinstance(block, TextBlock): - print(f"Claude: {block.text}") + print(f"Claude: {msg}") # ============================================================================ @@ -222,8 +210,6 @@ async def message_generator(): # Process them afterwards for msg in messages: if isinstance(msg, AssistantMessage): - for block in msg.content: - if isinstance(block, TextBlock): - print(f"Claude: {block.text}") + print(f"Claude: {msg}") elif isinstance(msg, ResultMessage): print(f"Total messages: {len(messages)}") diff --git a/pyproject.toml b/pyproject.toml index cc9b99d..653fce8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "claude-agent-sdk" -version = "0.1.6" +version = "0.1.7" description = "Python SDK for Claude Code" readme = "README.md" requires-python = ">=3.10" diff --git a/src/claude_agent_sdk/_version.py b/src/claude_agent_sdk/_version.py index 89470e1..9bc0608 100644 --- a/src/claude_agent_sdk/_version.py +++ b/src/claude_agent_sdk/_version.py @@ -1,3 +1,3 @@ """Version information for claude-agent-sdk.""" -__version__ = "0.1.6" +__version__ = "0.1.7" diff --git a/src/claude_agent_sdk/types.py b/src/claude_agent_sdk/types.py index e375bee..7572953 100644 --- a/src/claude_agent_sdk/types.py +++ b/src/claude_agent_sdk/types.py @@ -11,6 +11,17 @@ if TYPE_CHECKING: from mcp.server import Server as McpServer +# Repr/str truncation length for content blocks +_REPR_MAX_LENGTH = 50 + + +def _truncate_repr(text: str) -> str: + """Truncate text for repr output if it exceeds _REPR_MAX_LENGTH.""" + if len(text) > _REPR_MAX_LENGTH: + return text[:_REPR_MAX_LENGTH] + "..." + return text + + # Permission modes PermissionMode = Literal["default", "acceptEdits", "plan", "bypassPermissions"] @@ -127,6 +138,18 @@ class PermissionResultAllow: updated_input: dict[str, Any] | None = None updated_permissions: list[PermissionUpdate] | None = None + def __repr__(self) -> str: + """Return technical representation showing structure.""" + return ( + f"PermissionResultAllow(behavior='allow', " + f"updated_input={self.updated_input is not None}, " + f"updated_permissions={len(self.updated_permissions) if self.updated_permissions else 0})" + ) + + def __str__(self) -> str: + """Return user-friendly permission result info.""" + return "PermissionResultAllow" + @dataclass class PermissionResultDeny: @@ -136,6 +159,18 @@ class PermissionResultDeny: message: str = "" interrupt: bool = False + def __repr__(self) -> str: + """Return technical representation showing structure.""" + message_preview = _truncate_repr(self.message) + return ( + f"PermissionResultDeny(behavior='deny', message={message_preview!r}, " + f"interrupt={self.interrupt})" + ) + + def __str__(self) -> str: + """Return user-friendly permission result info.""" + return f"PermissionResultDeny: {self.message}" + PermissionResult = PermissionResultAllow | PermissionResultDeny @@ -423,6 +458,15 @@ class TextBlock: text: str + def __repr__(self) -> str: + """Return technical representation showing structure.""" + preview = _truncate_repr(self.text) + return f"TextBlock({preview!r})" + + def __str__(self) -> str: + """Return user-friendly text content.""" + return self.text + @dataclass class ThinkingBlock: @@ -431,6 +475,15 @@ class ThinkingBlock: thinking: str signature: str + def __repr__(self) -> str: + """Return technical representation showing structure.""" + thinking_preview = _truncate_repr(self.thinking) + return f"ThinkingBlock(thinking={thinking_preview!r}, signature={self.signature!r})" + + def __str__(self) -> str: + """Return user-friendly thinking content.""" + return self.thinking + @dataclass class ToolUseBlock: @@ -440,6 +493,17 @@ class ToolUseBlock: name: str input: dict[str, Any] + def __repr__(self) -> str: + """Return technical representation showing structure.""" + input_preview = _truncate_repr(str(self.input)) + return ( + f"ToolUseBlock(id={self.id!r}, name={self.name!r}, input={input_preview})" + ) + + def __str__(self) -> str: + """Return user-friendly tool usage info.""" + return f"Tool: {self.name} (ID: {self.id})" + @dataclass class ToolResultBlock: @@ -449,6 +513,22 @@ class ToolResultBlock: content: str | list[dict[str, Any]] | None = None is_error: bool | None = None + def __repr__(self) -> str: + """Return technical representation showing structure.""" + if isinstance(self.content, str): + content_preview = _truncate_repr(self.content) + else: + content_preview = _truncate_repr(str(self.content)) + return ( + f"ToolResultBlock(tool_use_id={self.tool_use_id!r}, " + f"content={content_preview!r}, is_error={self.is_error})" + ) + + def __str__(self) -> str: + """Return user-friendly tool result info.""" + status = "error" if self.is_error else "success" + return f"Tool Result [{status}] (ID: {self.tool_use_id})" + ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock @@ -461,6 +541,25 @@ class UserMessage: content: str | list[ContentBlock] parent_tool_use_id: str | None = None + def __repr__(self) -> str: + """Return technical representation showing structure.""" + if isinstance(self.content, str): + content_preview = _truncate_repr(self.content) + else: + # Show repr of each content block + blocks_repr = ", ".join(repr(b) for b in self.content) + content_preview = f"[{blocks_repr}]" + return f"UserMessage(content={content_preview!r})" + + def __str__(self) -> str: + """Return user-friendly message content.""" + if isinstance(self.content, str): + return self.content + else: + # Use str() of each content block for user-friendly output + block_strs = "\n ".join(str(b) for b in self.content) + return f"UserMessage:\n {block_strs}" + @dataclass class AssistantMessage: @@ -470,6 +569,19 @@ class AssistantMessage: model: str parent_tool_use_id: str | None = None + def __repr__(self) -> str: + """Return technical representation showing structure.""" + # Show repr of each content block + blocks_repr = ", ".join(repr(b) for b in self.content) + content_preview = f"[{blocks_repr}]" + return f"AssistantMessage(model={self.model!r}, content={content_preview})" + + def __str__(self) -> str: + """Return user-friendly message summary.""" + # Use str() of each content block for user-friendly output + block_strs = "\n ".join(str(b) for b in self.content) + return f"AssistantMessage from {self.model}:\n {block_strs}" + @dataclass class SystemMessage: @@ -478,6 +590,15 @@ class SystemMessage: subtype: str data: dict[str, Any] + def __repr__(self) -> str: + """Return technical representation showing structure.""" + data_preview = _truncate_repr(str(self.data)) + return f"SystemMessage(subtype={self.subtype!r}, data={data_preview})" + + def __str__(self) -> str: + """Return user-friendly system message info.""" + return f"SystemMessage [{self.subtype}]" + @dataclass class ResultMessage: @@ -493,6 +614,22 @@ class ResultMessage: usage: dict[str, Any] | None = None result: str | None = None + def __repr__(self) -> str: + """Return technical representation showing structure.""" + return ( + f"ResultMessage(subtype={self.subtype!r}, duration_ms={self.duration_ms}, " + f"is_error={self.is_error}, num_turns={self.num_turns}, " + f"session_id={self.session_id!r})" + ) + + def __str__(self) -> str: + """Return user-friendly result message info.""" + status = "error" if self.is_error else "success" + return ( + f"ResultMessage [{status}] - {self.num_turns} turn(s), " + f"{self.duration_ms}ms (API: {self.duration_api_ms}ms)" + ) + @dataclass class StreamEvent: @@ -503,6 +640,19 @@ class StreamEvent: event: dict[str, Any] # The raw Anthropic API stream event parent_tool_use_id: str | None = None + def __repr__(self) -> str: + """Return technical representation showing structure.""" + event_preview = _truncate_repr(str(self.event)) + return ( + f"StreamEvent(uuid={self.uuid!r}, session_id={self.session_id!r}, " + f"event={event_preview})" + ) + + def __str__(self) -> str: + """Return user-friendly stream event info.""" + event_type = self.event.get("type", "unknown") + return f"StreamEvent [{event_type}] ({self.uuid[:8]}...)" + Message = UserMessage | AssistantMessage | SystemMessage | ResultMessage | StreamEvent diff --git a/tests/test_types.py b/tests/test_types.py index 21a84da..a39b735 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -6,6 +6,10 @@ ResultMessage, ) from claude_agent_sdk.types import ( + PermissionResultAllow, + PermissionResultDeny, + StreamEvent, + SystemMessage, TextBlock, ThinkingBlock, ToolResultBlock, @@ -149,3 +153,90 @@ def test_claude_code_options_with_model_specification(self): ) assert options.model == "claude-sonnet-4-5" assert options.permission_prompt_tool_name == "CustomTool" + + +class TestReprStr: + """Test __repr__ and __str__ methods for all types.""" + + def test_text_block_repr_str(self): + """Test TextBlock repr and str.""" + b = TextBlock(text="Hello, world!") + assert "TextBlock" in repr(b) and "Hello" in repr(b) + assert str(b) == "Hello, world!" + + def test_thinking_block_repr_str(self): + """Test ThinkingBlock repr and str.""" + b = ThinkingBlock(thinking="Let me think...", signature="sig-1") + assert "ThinkingBlock" in repr(b) and "sig-1" in repr(b) + assert str(b) == "Let me think..." + + def test_tool_use_block_repr_str(self): + """Test ToolUseBlock repr and str.""" + b = ToolUseBlock(id="tool-1", name="Read", input={"path": "file.txt"}) + assert "ToolUseBlock" in repr(b) and "Read" in repr(b) + assert "Tool: Read" in str(b) and "tool-1" in str(b) + + def test_tool_result_block_repr_str(self): + """Test ToolResultBlock repr and str.""" + b = ToolResultBlock(tool_use_id="tool-1", content="Result", is_error=False) + assert "ToolResultBlock" in repr(b) and "tool-1" in repr(b) + assert "success" in str(b) and "tool-1" in str(b) + + def test_user_message_string_repr_str(self): + """Test UserMessage with string content repr and str.""" + m = UserMessage(content="Hello") + assert "UserMessage" in repr(m) and "Hello" in repr(m) + assert str(m) == "Hello" + + def test_user_message_blocks_repr_str(self): + """Test UserMessage with blocks repr and str.""" + m = UserMessage( + content=[TextBlock(text="Hi"), ToolUseBlock(id="t1", name="Bash", input={})] + ) + assert "TextBlock" in repr(m) and "ToolUseBlock" in repr(m) + assert "UserMessage:" in str(m) and "Bash" in str(m) + + def test_assistant_message_repr_str(self): + """Test AssistantMessage repr and str.""" + m = AssistantMessage( + content=[TextBlock(text="Response")], model="claude-sonnet" + ) + assert "AssistantMessage" in repr(m) and "TextBlock" in repr(m) + assert "from claude-sonnet" in str(m) and "Response" in str(m) + + def test_system_message_repr_str(self): + """Test SystemMessage repr and str.""" + m = SystemMessage(subtype="test_type", data={"key": "value"}) + assert "SystemMessage" in repr(m) and "test_type" in repr(m) + assert "test_type" in str(m) + + def test_result_message_repr_str(self): + """Test ResultMessage repr and str.""" + m = ResultMessage( + subtype="ok", + duration_ms=100, + duration_api_ms=80, + is_error=False, + num_turns=1, + session_id="s1", + ) + assert "ResultMessage" in repr(m) and "s1" in repr(m) + assert "success" in str(m) and "1 turn" in str(m) + + def test_stream_event_repr_str(self): + """Test StreamEvent repr and str.""" + e = StreamEvent(uuid="uuid-123", session_id="s1", event={"type": "message"}) + assert "StreamEvent" in repr(e) and "uuid-123" in repr(e) + assert "message" in str(e) and "uuid-123" in str(e) + + def test_permission_result_allow_repr_str(self): + """Test PermissionResultAllow repr and str.""" + p = PermissionResultAllow() + assert "PermissionResultAllow" in repr(p) + assert "PermissionResultAllow" in str(p) + + def test_permission_result_deny_repr_str(self): + """Test PermissionResultDeny repr and str.""" + p = PermissionResultDeny(message="Not allowed") + assert "PermissionResultDeny" in repr(p) and "Not allowed" in repr(p) + assert "Not allowed" in str(p)