Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:28:42 +08:00
commit 8a4be47b6e
43 changed files with 10867 additions and 0 deletions

View File

@@ -0,0 +1,436 @@
"""
Claude Code SDK - The SDK Way
This module demonstrates the idiomatic way to use the Claude Code Python SDK
for programmatic agent interactions. It focuses on clean, type-safe patterns
using the SDK's native abstractions.
Key Concepts:
- Use `query()` for one-shot operations
- Use `ClaudeSDKClient` for interactive sessions
- Work directly with SDK message types
- Leverage async/await for clean concurrency
- Configure options for your use case
Example Usage:
# Simple query
async for message in query(prompt="What is 2 + 2?"):
if isinstance(message, AssistantMessage):
print(extract_text(message))
# With options
options = ClaudeCodeOptions(
model="claude-sonnet-4-20250514",
allowed_tools=["Read", "Write"],
permission_mode="bypassPermissions"
)
async for message in query(prompt="Create hello.py", options=options):
process_message(message)
# Interactive session
async with create_session() as client:
await client.query("Debug this error")
async for msg in client.receive_response():
handle_message(msg)
"""
import logging
from pathlib import Path
from typing import AsyncIterator, Optional, List
from contextlib import asynccontextmanager
# Import all SDK components we'll use
from claude_code_sdk import (
# Main functions
query,
ClaudeSDKClient,
# Configuration
ClaudeCodeOptions,
PermissionMode,
# Message types
Message,
AssistantMessage,
UserMessage,
SystemMessage,
ResultMessage,
# Content blocks
ContentBlock,
TextBlock,
ToolUseBlock,
ToolResultBlock,
# Errors
ClaudeSDKError,
CLIConnectionError,
CLINotFoundError,
ProcessError,
)
# Set up logging
logger = logging.getLogger(__name__)
# ============================================================================
# UTILITY FUNCTIONS
# ============================================================================
def extract_text(message: AssistantMessage) -> str:
"""Extract all text content from an assistant message.
The SDK way: Work directly with typed message objects.
Args:
message: AssistantMessage with content blocks
Returns:
Concatenated text from all text blocks
"""
texts = []
for block in message.content:
if isinstance(block, TextBlock):
texts.append(block.text)
return "\n".join(texts)
def extract_tool_uses(message: AssistantMessage) -> List[ToolUseBlock]:
"""Extract all tool use blocks from an assistant message.
Args:
message: AssistantMessage with content blocks
Returns:
List of ToolUseBlock objects
"""
return [
block for block in message.content
if isinstance(block, ToolUseBlock)
]
def get_result_text(messages: List[Message]) -> str:
"""Extract final result text from a list of messages.
Args:
messages: List of messages from a query
Returns:
Result text or assistant responses
"""
# First check for ResultMessage
for msg in reversed(messages):
if isinstance(msg, ResultMessage) and msg.result:
return msg.result
# Otherwise collect assistant text
texts = []
for msg in messages:
if isinstance(msg, AssistantMessage):
text = extract_text(msg)
if text:
texts.append(text)
return "\n".join(texts)
# ============================================================================
# ONE-SHOT QUERIES (The Simple SDK Way)
# ============================================================================
async def simple_query(prompt: str, model: str = "claude-sonnet-4-5-20250929") -> str:
"""Simple one-shot query with text response.
The SDK way: Direct use of query() with minimal setup.
Args:
prompt: What to ask Claude
model: Which model to use
Returns:
Text response from Claude
Example:
response = await simple_query("What is 2 + 2?")
print(response) # "4" or "2 + 2 equals 4"
"""
options = ClaudeCodeOptions(model=model)
texts = []
async for message in query(prompt=prompt, options=options):
if isinstance(message, AssistantMessage):
text = extract_text(message)
if text:
texts.append(text)
return "\n".join(texts) if texts else "No response"
async def query_with_tools(
prompt: str,
allowed_tools: List[str],
working_dir: Optional[Path] = None
) -> AsyncIterator[Message]:
"""Query with specific tools enabled.
The SDK way: Configure options for your use case.
Args:
prompt: What to ask Claude
allowed_tools: List of tool names to allow
working_dir: Optional working directory
Yields:
SDK message objects
Example:
async for msg in query_with_tools(
"Create a Python script",
allowed_tools=["Write", "Read"]
):
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, ToolUseBlock):
print(f"Using tool: {block.name}")
"""
options = ClaudeCodeOptions(
allowed_tools=allowed_tools,
cwd=str(working_dir) if working_dir else None,
permission_mode="bypassPermissions" # For automated workflows
)
async for message in query(prompt=prompt, options=options):
yield message
async def collect_query_response(
prompt: str,
options: Optional[ClaudeCodeOptions] = None
) -> tuple[List[Message], Optional[ResultMessage]]:
"""Collect all messages from a query.
The SDK way: Async iteration with type checking.
Args:
prompt: What to ask Claude
options: Optional configuration
Returns:
Tuple of (all_messages, result_message)
Example:
messages, result = await collect_query_response("List files")
if result and not result.is_error:
print("Success!")
for msg in messages:
process_message(msg)
"""
if options is None:
options = ClaudeCodeOptions()
messages = []
result = None
async for message in query(prompt=prompt, options=options):
messages.append(message)
if isinstance(message, ResultMessage):
result = message
return messages, result
# ============================================================================
# INTERACTIVE SESSIONS (The SDK Client Way)
# ============================================================================
@asynccontextmanager
async def create_session(
model: str = "claude-sonnet-4-5-20250929",
working_dir: Optional[Path] = None
):
"""Create an interactive session with Claude.
The SDK way: Use context managers for resource management.
Args:
model: Which model to use
working_dir: Optional working directory
Yields:
Connected ClaudeSDKClient
Example:
async with create_session() as client:
await client.query("Hello")
async for msg in client.receive_response():
print(msg)
"""
options = ClaudeCodeOptions(
model=model,
cwd=str(working_dir) if working_dir else None,
permission_mode="bypassPermissions"
)
client = ClaudeSDKClient(options=options)
await client.connect()
try:
yield client
finally:
await client.disconnect()
async def interactive_conversation(prompts: List[str]) -> List[Message]:
"""Have an interactive conversation with Claude.
The SDK way: Bidirectional communication with the client.
Args:
prompts: List of prompts to send in sequence
Returns:
All messages from the conversation
Example:
messages = await interactive_conversation([
"What's the weather like?",
"Tell me more about clouds",
"How do they form?"
])
"""
all_messages = []
async with create_session() as client:
for prompt in prompts:
# Send prompt
await client.query(prompt)
# Collect response
async for msg in client.receive_response():
all_messages.append(msg)
if isinstance(msg, ResultMessage):
break
return all_messages
# ============================================================================
# ERROR HANDLING (The SDK Way)
# ============================================================================
async def safe_query(prompt: str) -> tuple[Optional[str], Optional[str]]:
"""Query with comprehensive error handling.
The SDK way: Handle specific SDK exceptions.
Args:
prompt: What to ask Claude
Returns:
Tuple of (response_text, error_message)
Example:
response, error = await safe_query("Help me debug this")
if error:
print(f"Error: {error}")
else:
print(f"Response: {response}")
"""
try:
response = await simple_query(prompt)
return response, None
except CLINotFoundError:
return None, "Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code"
except CLIConnectionError as e:
return None, f"Connection error: {str(e)}"
except ProcessError as e:
return None, f"Process error (exit code {e.exit_code}): {str(e)}"
except ClaudeSDKError as e:
return None, f"SDK error: {str(e)}"
except Exception as e:
return None, f"Unexpected error: {str(e)}"
# ============================================================================
# ADVANCED PATTERNS (The SDK Way)
# ============================================================================
async def stream_with_progress(
prompt: str,
on_text: Optional[callable] = None,
on_tool: Optional[callable] = None
) -> ResultMessage:
"""Stream query with progress callbacks.
The SDK way: Process messages as they arrive.
Args:
prompt: What to ask Claude
on_text: Callback for text blocks (optional)
on_tool: Callback for tool use blocks (optional)
Returns:
Final ResultMessage
Example:
result = await stream_with_progress(
"Analyze this codebase",
on_text=lambda text: print(f"Claude: {text}"),
on_tool=lambda tool: print(f"Using: {tool.name}")
)
print(f"Cost: ${result.total_cost_usd:.4f}")
"""
result = None
async for message in query(prompt=prompt):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock) and on_text:
on_text(block.text)
elif isinstance(block, ToolUseBlock) and on_tool:
on_tool(block)
elif isinstance(message, ResultMessage):
result = message
return result
async def query_with_timeout(prompt: str, timeout_seconds: float = 30) -> Optional[str]:
"""Query with timeout protection.
The SDK way: Use asyncio for timeout control.
Args:
prompt: What to ask Claude
timeout_seconds: Maximum time to wait
Returns:
Response text or None if timeout
Example:
response = await query_with_timeout("Complex analysis", timeout_seconds=60)
if response is None:
print("Query timed out")
"""
import asyncio
try:
# Create the query task
async def _query():
return await simple_query(prompt)
# Run with timeout
response = await asyncio.wait_for(_query(), timeout=timeout_seconds)
return response
except asyncio.TimeoutError:
logger.warning(f"Query timed out after {timeout_seconds} seconds")
return None