From 722e5aba213e3e588e6482ab8735e096a90ae7bf Mon Sep 17 00:00:00 2001 From: Farouk Adeleke Date: Fri, 5 Dec 2025 13:22:15 -0500 Subject: [PATCH] (no commit message) --- README.md | 566 ++++++++++++++++++++++++++++++++++++++++++++++ auto_classes.json | 4 - config.json | 13 +- main.py | 9 - pyproject.toml | 7 - src/agent.py | 19 -- 6 files changed, 578 insertions(+), 40 deletions(-) delete mode 100644 auto_classes.json delete mode 100644 main.py delete mode 100644 pyproject.toml delete mode 100644 src/agent.py diff --git a/README.md b/README.md index e69de29..3eb65f7 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,566 @@ +# ClaudeAgent - DSPy Module for Claude Code SDK + +A DSPy module that wraps the Claude Code Python SDK with a signature-driven interface. Each agent instance maintains a stateful conversation session, making it perfect for multi-turn agentic workflows. + +## Features + +- **Signature-driven** - Use DSPy signatures for type safety and clarity +- **Stateful sessions** - Each agent instance = one conversation session +- **Smart schema handling** - Automatically handles str vs Pydantic outputs +- **Rich outputs** - Get typed results + execution trace + token usage +- **Multi-turn conversations** - Context preserved across calls +- **Output field descriptions** - Automatically enhance prompts +- **Async support** - Both sync and async execution modes + +## Installation + +```bash +# Install with uv +uv add claude-agent-sdk dspy nest-asyncio + +# Or with pip +pip install claude-agent-sdk dspy nest-asyncio +``` + +**Prerequisites:** +- Python 3.10+ +- Claude Code CLI installed (get it from [code.claude.com](https://code.claude.com)) +- Anthropic API key set in `ANTHROPIC_API_KEY` environment variable + +## Quick Start + +### Basic String Output + +```python +import dspy +from claude_agent import ClaudeAgent + +# Define signature +sig = dspy.Signature('message:str -> answer:str') + +# Create agent +agent = ClaudeAgent(sig, working_directory=".") + +# Use it +result = agent(message="What files are in this directory?") +print(result.answer) # String response +print(result.trace) # Execution items +print(result.usage) # Token counts +``` + +### Structured Output with Pydantic + +```python +from pydantic import BaseModel, Field + +class BugReport(BaseModel): + severity: str = Field(description="critical, high, medium, or low") + description: str + affected_files: list[str] + +sig = dspy.Signature('message:str -> report:BugReport') +agent = ClaudeAgent(sig, working_directory=".") + +result = agent(message="Analyze the bug in error.log") +print(result.report.severity) # Typed access! +print(result.report.affected_files) +``` + +## API Reference + +### ClaudeAgent + +```python +class ClaudeAgent(dspy.Module): + def __init__( + self, + signature: str | type[Signature], + working_directory: str, + model: Optional[str] = None, + permission_mode: Optional[str] = None, + allowed_tools: Optional[list[str]] = None, + disallowed_tools: Optional[list[str]] = None, + sandbox: Optional[dict[str, Any]] = None, + system_prompt: Optional[str | dict[str, Any]] = None, + api_key: Optional[str] = None, + **kwargs: Any, + ) +``` + +#### Parameters + +**Required:** + +- **`signature`** (`str | type[Signature]`) + - DSPy signature defining input/output fields + - Must have exactly 1 input field and 1 output field + - Examples: + - String format: `'message:str -> answer:str'` + - Class format: `MySignature` (subclass of `dspy.Signature`) + +- **`working_directory`** (`str`) + - Directory where Claude will execute commands + - Example: `"."`, `"/path/to/project"` + +**Optional:** + +- **`model`** (`Optional[str]`) + - Model to use: `"sonnet"`, `"opus"`, `"haiku"` + - Default: Claude Code default (typically Sonnet) + +- **`permission_mode`** (`Optional[str]`) + - Controls permission behavior: + - `"default"` - Standard permission checks + - `"acceptEdits"` - Auto-accept file edits + - `"plan"` - Planning mode (no execution) + - `"bypassPermissions"` - Bypass all checks (use with caution!) + - Default: `"default"` + +- **`allowed_tools`** (`Optional[list[str]]`) + - List of allowed tool names + - Examples: `["Read", "Write", "Bash", "Glob"]` + - Default: All tools allowed + +- **`disallowed_tools`** (`Optional[list[str]]`) + - List of disallowed tool names + - Default: `[]` + +- **`sandbox`** (`Optional[dict[str, Any]]`) + - Sandbox configuration for command execution + - Example: `{"enabled": True, "network": {"allowLocalBinding": True}}` + - Default: `None` + +- **`system_prompt`** (`Optional[str | dict[str, Any]]`) + - Custom system prompt or preset configuration + - String: Custom prompt + - Dict: Preset config like `{"type": "preset", "preset": "claude_code", "append": "..."}` + - Default: `None` (uses Claude Code default) + +- **`api_key`** (`Optional[str]`) + - Anthropic API key + - Falls back to `ANTHROPIC_API_KEY` environment variable + - Default: `None` + +- **`**kwargs`** - Additional `ClaudeAgentOptions` parameters + +#### Methods + +##### `forward(**kwargs) -> Prediction` + +Execute the agent with an input message. + +**Arguments:** +- `**kwargs` - Must contain the input field specified in signature + +**Returns:** +- `Prediction` object with: + - **Typed output field** - Named according to signature (e.g., `result.answer`) + - **`trace`** - `list[TraceItem]` - Execution trace + - **`usage`** - `Usage` - Token usage statistics + +**Example:** +```python +result = agent(message="Hello") +print(result.answer) # Access typed output +print(result.trace) # List of execution items +print(result.usage) # Token usage stats +``` + +##### `aforward(**kwargs) -> Prediction` + +Async version of `forward()` for use in async contexts. + +**Example:** +```python +async def main(): + result = await agent.aforward(message="Hello") + print(result.answer) +``` + +#### Properties + +##### `session_id: Optional[str]` + +Get the session ID for this agent instance. + +- Returns `None` until first `forward()` call +- Persists across multiple `forward()` calls +- Useful for debugging and logging + +**Example:** +```python +agent = ClaudeAgent(sig, working_directory=".") +print(agent.session_id) # None + +result = agent(message="Hello") +print(agent.session_id) # '0199e95f-2689-7501-a73d-038d77dd7320' +``` + +## Usage Patterns + +### Pattern 1: Multi-turn Conversation + +Each agent instance maintains a stateful session: + +```python +agent = ClaudeAgent(sig, working_directory=".") + +# Turn 1 +result1 = agent(message="What's the main bug?") +print(result1.answer) + +# Turn 2 - has context from Turn 1 +result2 = agent(message="How do we fix it?") +print(result2.answer) + +# Turn 3 - has context from Turn 1 + 2 +result3 = agent(message="Write tests for the fix") +print(result3.answer) + +# All use same session_id +print(agent.session_id) +``` + +### Pattern 2: Fresh Context + +Want a new conversation? Create a new agent: + +```python +# Agent 1 - Task A +agent1 = ClaudeAgent(sig, working_directory=".") +result1 = agent1(message="Analyze bug in module A") + +# Agent 2 - Task B (no context from Agent 1) +agent2 = ClaudeAgent(sig, working_directory=".") +result2 = agent2(message="Analyze bug in module B") +``` + +### Pattern 3: Output Field Descriptions + +Enhance prompts with field descriptions: + +```python +class MySignature(dspy.Signature): + """Analyze code architecture.""" + + message: str = dspy.InputField() + analysis: str = dspy.OutputField( + desc="A detailed markdown report with sections: " + "1) Architecture overview, 2) Key components, 3) Dependencies" + ) + +agent = ClaudeAgent(MySignature, working_directory=".") +result = agent(message="Analyze this codebase") + +# The description is automatically appended to the prompt +``` + +### Pattern 4: Inspecting Execution Trace + +Access detailed execution information: + +```python +from claude_agent import ToolUseItem, ToolResultItem + +result = agent(message="Fix the bug") + +# Filter trace by type +tool_uses = [item for item in result.trace if isinstance(item, ToolUseItem)] +for tool in tool_uses: + print(f"Tool: {tool.tool_name}") + print(f"Input: {tool.tool_input}") + +tool_results = [item for item in result.trace if isinstance(item, ToolResultItem)] +for result_item in tool_results: + print(f"Result: {result_item.content}") + print(f"Error: {result_item.is_error}") +``` + +### Pattern 5: Token Usage Tracking + +Monitor API usage: + +```python +result = agent(message="...") + +print(f"Input tokens: {result.usage.input_tokens}") +print(f"Cached tokens: {result.usage.cached_input_tokens}") +print(f"Output tokens: {result.usage.output_tokens}") +print(f"Total: {result.usage.total_tokens}") +``` + +### Pattern 6: Safe Execution with Permissions + +Control what the agent can do: + +```python +# Read-only (safest) +agent = ClaudeAgent( + sig, + working_directory=".", + permission_mode="default", + allowed_tools=["Read", "Glob", "Grep"], +) + +# Auto-accept file edits +agent = ClaudeAgent( + sig, + working_directory=".", + permission_mode="acceptEdits", + allowed_tools=["Read", "Write", "Edit"], +) + +# Sandbox mode for command execution +agent = ClaudeAgent( + sig, + working_directory=".", + sandbox={"enabled": True}, +) +``` + +## Advanced Examples + +### Example 1: Code Review Agent + +```python +from pydantic import BaseModel, Field + +class CodeReview(BaseModel): + summary: str = Field(description="High-level summary") + issues: list[str] = Field(description="List of issues found") + severity: str = Field(description="critical, high, medium, or low") + recommendations: list[str] = Field(description="Actionable recommendations") + +sig = dspy.Signature('message:str -> review:CodeReview') + +agent = ClaudeAgent( + sig, + working_directory="/path/to/project", + model="sonnet", + permission_mode="default", + allowed_tools=["Read", "Glob", "Grep"], +) + +result = agent(message="Review the changes in src/main.py") + +print(f"Severity: {result.review.severity}") +for issue in result.review.issues: + print(f"- {issue}") +``` + +### Example 2: Iterative Debugging + +```python +sig = dspy.Signature('message:str -> response:str') +agent = ClaudeAgent( + sig, + working_directory=".", + permission_mode="acceptEdits", + allowed_tools=["Read", "Write", "Bash"], +) + +# Turn 1: Find the bug +result1 = agent(message="Find the bug in src/calculator.py") +print(result1.response) + +# Turn 2: Propose a fix +result2 = agent(message="What's the best way to fix it?") +print(result2.response) + +# Turn 3: Implement the fix +result3 = agent(message="Implement the fix") +print(result3.response) + +# Turn 4: Write tests +result4 = agent(message="Write tests for the fix") +print(result4.response) +``` + +### Example 3: Async Usage + +```python +import asyncio + +async def main(): + sig = dspy.Signature('message:str -> answer:str') + agent = ClaudeAgent(sig, working_directory=".") + + # Use aforward in async context + result = await agent.aforward(message="Analyze this code") + print(result.answer) + + # Cleanup + await agent.disconnect() + +asyncio.run(main()) +``` + +## Trace Item Types + +When accessing `result.trace`, you'll see various item types: + +| Type | Fields | Description | +|------|--------|-------------| +| `AgentMessageItem` | `text`, `model` | Agent's text response | +| `ThinkingItem` | `text`, `model` | Agent's internal reasoning | +| `ToolUseItem` | `tool_name`, `tool_input`, `tool_use_id` | Tool invocation | +| `ToolResultItem` | `tool_name`, `tool_use_id`, `content`, `is_error` | Tool result | +| `ErrorItem` | `message`, `error_type` | Error that occurred | + +## How It Works + +### Signature ’ Claude Flow + +``` +1. Define signature: 'message:str -> answer:str' + +2. ClaudeAgent validates (must have 1 input, 1 output) + +3. __init__ creates ClaudeSDKClient with options + +4. forward(message="...") extracts message + +5. If output field has desc ’ append to message + +6. If output type ` str ’ generate JSON schema + +7. Call client.query(message) with optional output_format + +8. Iterate through receive_response(), collect messages + +9. Parse response (JSON if Pydantic, str otherwise) + +10. Return Prediction(output=..., trace=..., usage=...) +``` + +### Output Type Handling + +**String output:** +```python +sig = dspy.Signature('message:str -> answer:str') +# No schema passed to Claude Code +# Response used as-is +``` + +**Pydantic output:** +```python +sig = dspy.Signature('message:str -> report:BugReport') +# JSON schema generated from BugReport +# Schema passed to Claude Code via output_format +# Response parsed with BugReport.model_validate_json() +``` + +## Troubleshooting + +### Error: "ClaudeAgent requires exactly 1 input field" + +Your signature has too many or too few fields. ClaudeAgent expects exactly one input and one output: + +```python +# L Wrong - multiple inputs +sig = dspy.Signature('context:str, question:str -> answer:str') + +#  Correct - single input +sig = dspy.Signature('message:str -> answer:str') +``` + +### Error: "Failed to parse Claude response as MyModel" + +The model returned JSON that doesn't match your Pydantic schema. Check: +1. Schema is valid and clear +2. Field descriptions are helpful +3. Model has enough context to generate correct structure + +### Error: "Claude Code CLI not found" + +Install Claude Code CLI: +```bash +# Visit code.claude.com for installation instructions +# or use npm: +npm install -g @anthropic-ai/claude-code +``` + +### Async event loop issues + +Use `aforward()` when already in an async context: + +```python +# L Don't do this in async context +async def main(): + result = agent(message="...") # Can cause issues + +#  Do this instead +async def main(): + result = await agent.aforward(message="...") +``` + +## Design Philosophy + +### Why 1 input, 1 output? + +ClaudeAgent is designed for conversational agentic workflows. The input is always a message/prompt, and the output is always a response. This keeps the interface simple and predictable. + +For complex inputs, compose them into the message: + +```python +# Instead of: 'context:str, question:str -> answer:str' +message = f"Context: {context}\n\nQuestion: {question}" +result = agent(message=message) +``` + +### Why stateful sessions? + +Agents often need multi-turn context (e.g., "fix the bug" ’ "write tests for it"). Stateful sessions make this natural without manual history management. + +Want fresh context? Create a new agent instance. + +### Why return trace + usage? + +Observability is critical for agentic systems. You need to know: +- What tools were used +- What the agent was thinking +- How many tokens were consumed +- If any errors occurred + +The trace provides full visibility into agent execution. + +## Comparison with CodexAgent + +| Feature | CodexAgent | ClaudeAgent | +|---------|-----------|-------------| +| SDK | OpenAI Codex SDK | Claude Code Python SDK | +| Thread management | Built-in thread ID | Session-based (ClaudeSDKClient) | +| Streaming | Yes | Yes (via receive_response) | +| Async support | No | Yes (aforward) | +| Tool types | Codex-specific | Claude Code tools (Bash, Read, Write, etc.) | +| Sandbox | Simple mode enum | Detailed config dict | +| Permission control | Sandbox modes | Permission modes + allowed_tools | + +## Examples Directory + +Check out the `examples/` directory for more: + +- `basic_string_output.py` - Simple string output +- `pydantic_output.py` - Structured Pydantic output +- `multi_turn_conversation.py` - Multi-turn conversation +- `output_field_description.py` - Using output field descriptions +- `inspect_trace.py` - Inspecting execution trace +- `code_review_agent.py` - Advanced code review agent + +## Contributing + +Issues and PRs welcome! This is an implementation of Claude Code SDK integration with DSPy. + +## License + +See LICENSE file. + +## Related Documentation + +- [Claude Code SDK API Reference](https://docs.claude.com/en/agent-sdk/python) +- [DSPy Documentation](https://dspy-docs.vercel.app/) +- [Claude Code Documentation](https://code.claude.com/docs) + +--- + +**Note:** This is a community implementation of Claude Code SDK integration with DSPy, inspired by the CodexAgent design pattern. diff --git a/auto_classes.json b/auto_classes.json deleted file mode 100644 index e872032..0000000 --- a/auto_classes.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "AutoConfig": "src.agent.ClaudeCodeConfig", - "AutoProgram": "src.agent.ClaudeCodeAgent" -} \ No newline at end of file diff --git a/config.json b/config.json index e52eb33..af38f78 100644 --- a/config.json +++ b/config.json @@ -1,3 +1,14 @@ { - "model": "claude-3-5-sonnet-20241022" + "model": "claude-opus-4-5-20251101", + "signature": "message:str -> output:Output", + "working_directory": ".", + "permission_mode": "acceptEdits", + "allowed_tools": [ + "Read", + "Glob" + ], + "disallowed_tools": null, + "sandbox": null, + "system_prompt": null, + "api_key": null } \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index 5c9e39d..0000000 --- a/main.py +++ /dev/null @@ -1,9 +0,0 @@ -from src.agent import ClaudeCodeConfig, ClaudeCodeAgent - -config = ClaudeCodeConfig() -agent = ClaudeCodeAgent(config) - -if __name__ == "__main__": - result = agent("Implement a todo list application") - agent.push_to_hub("farouk1/claude-code", with_code=True, commit_message="With code") - print(result) diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index d666d8c..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,7 +0,0 @@ -[project] -name = "claude-code" -version = "0.1.0" -description = "Claude Code SDK wrapped in a DSPy module" -readme = "README.md" -requires-python = ">=3.13" -dependencies = ["claude-agent-sdk>=0.1.12", "dspy>=3.0.4", "modaic>=0.7.0"] diff --git a/src/agent.py b/src/agent.py deleted file mode 100644 index 23f8e53..0000000 --- a/src/agent.py +++ /dev/null @@ -1,19 +0,0 @@ -from modaic import PrecompiledProgram, PrecompiledConfig -from dspy.primitives.prediction import Prediction - -class ClaudeCodeConfig(PrecompiledConfig): - model: str = "claude-3-5-sonnet-20241022" - -class ClaudeCodeAgent(PrecompiledProgram): - config: ClaudeCodeConfig - - def __init__(self, config: ClaudeCodeConfig): - super().__init__(config=config) - self.model = config.model - - def forward(self, task: str) -> Prediction: - # TODO: Implement the actual agent logic using self.model - # This is a placeholder - actual implementation would use the model to process the task - return Prediction(output=f"Processed task: {task} using model {self.model}") - -