(no commit message)

This commit is contained in:
2025-12-05 13:34:01 -05:00
parent 722e5aba21
commit d776acd439

349
README.md
View File

@@ -1,4 +1,4 @@
# ClaudeAgent - DSPy Module for Claude Code SDK # ClaudeCode - 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. 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.
@@ -11,15 +11,16 @@ A DSPy module that wraps the Claude Code Python SDK with a signature-driven inte
- **Multi-turn conversations** - Context preserved across calls - **Multi-turn conversations** - Context preserved across calls
- **Output field descriptions** - Automatically enhance prompts - **Output field descriptions** - Automatically enhance prompts
- **Async support** - Both sync and async execution modes - **Async support** - Both sync and async execution modes
- **Modaic Hub Integration** - Push and pull agents from Modaic Hub
## Installation ## Installation
```bash ```bash
# Install with uv # Install with uv
uv add claude-agent-sdk dspy nest-asyncio uv add claude-agent-sdk dspy modaic nest-asyncio
# Or with pip # Or with pip
pip install claude-agent-sdk dspy nest-asyncio pip install claude-agent-sdk dspy modaic nest-asyncio
``` ```
**Prerequisites:** **Prerequisites:**
@@ -27,19 +28,77 @@ pip install claude-agent-sdk dspy nest-asyncio
- Claude Code CLI installed (get it from [code.claude.com](https://code.claude.com)) - Claude Code CLI installed (get it from [code.claude.com](https://code.claude.com))
- Anthropic API key set in `ANTHROPIC_API_KEY` environment variable - Anthropic API key set in `ANTHROPIC_API_KEY` environment variable
## Quick Start ## Quick Start with Modaic Hub (Recommended)
The fastest way to use ClaudeCode is to pull a pre-configured agent from Modaic Hub.
### 1. Set up environment
```bash
# Copy the example file
cp .env.example .env
# Edit .env with your keys
ANTHROPIC_API_KEY="<YOUR_ANTHROPIC_API_KEY>"
MODAIC_TOKEN="<YOUR_MODAIC_TOKEN>" # Optional, for pushing to hub
```
### 2. Load from Modaic Hub
```python
from modaic import AutoProgram
from pydantic import BaseModel
class FileList(BaseModel):
files: list[str]
# Load pre-compiled agent from hub
agent = AutoProgram.from_precompiled(
"farouk1/claude-code",
config={
"signature": "message:str -> output:FileList",
"working_directory": ".",
}
)
# Use it!
result = agent(message="List Python files here")
print(result.output.files) # Typed access
print(result.usage) # Token usage
```
### 3. Override Config Options
```python
# Load with custom configuration
agent = AutoProgram.from_precompiled(
"farouk1/claude-code",
config={
"signature": "message:str -> answer:str",
"model": "sonnet",
"permission_mode": "acceptEdits",
"allowed_tools": ["Read", "Write", "Bash"],
}
)
```
## Local Development
For local development and creating your own agents:
### Basic String Output ### Basic String Output
```python ```python
import dspy from claude_dspy import ClaudeCode, ClaudeCodeConfig
from claude_agent import ClaudeAgent
# Define signature # Create config
sig = dspy.Signature('message:str -> answer:str') config = ClaudeCodeConfig(
signature="message:str -> answer:str",
working_directory="."
)
# Create agent # Create agent
agent = ClaudeAgent(sig, working_directory=".") agent = ClaudeCode(config)
# Use it # Use it
result = agent(message="What files are in this directory?") result = agent(message="What files are in this directory?")
@@ -51,6 +110,7 @@ print(result.usage) # Token counts
### Structured Output with Pydantic ### Structured Output with Pydantic
```python ```python
from claude_dspy import ClaudeCode, ClaudeCodeConfig
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
class BugReport(BaseModel): class BugReport(BaseModel):
@@ -58,94 +118,92 @@ class BugReport(BaseModel):
description: str description: str
affected_files: list[str] affected_files: list[str]
sig = dspy.Signature('message:str -> report:BugReport') # Create config with Pydantic output
agent = ClaudeAgent(sig, working_directory=".") config = ClaudeCodeConfig(
signature="message:str -> report:BugReport",
working_directory="."
)
agent = ClaudeCode(config)
result = agent(message="Analyze the bug in error.log") result = agent(message="Analyze the bug in error.log")
print(result.report.severity) # Typed access! print(result.report.severity) # Typed access!
print(result.report.affected_files) print(result.report.affected_files)
``` ```
### Push to Modaic Hub
```python
from claude_dspy import ClaudeCode, ClaudeCodeConfig
# Create your agent
config = ClaudeCodeConfig(
signature="message:str -> answer:str",
working_directory=".",
model="claude-opus-4-5-20251101",
permission_mode="acceptEdits",
)
agent = ClaudeCode(config)
# Test it locally
result = agent(message="Test my agent")
print(result.answer)
# Push to Modaic Hub
agent.push_to_hub("your-username/your-agent-name")
```
## API Reference ## API Reference
### ClaudeAgent ### ClaudeCodeConfig
Configuration object for ClaudeCode agents.
```python ```python
class ClaudeAgent(dspy.Module): class ClaudeCodeConfig:
def __init__( def __init__(
self, self,
signature: str | type[Signature], signature: str | type[Signature], # Required
working_directory: str, working_directory: str = ".", # Default: "."
model: Optional[str] = None, model: str = "claude-opus-4-5-20251101", # Default model
permission_mode: Optional[str] = None, permission_mode: str | None = None, # Optional
allowed_tools: Optional[list[str]] = None, allowed_tools: list[str] | None = None, # Optional
disallowed_tools: Optional[list[str]] = None, disallowed_tools: list[str] | None = None, # Optional
sandbox: Optional[dict[str, Any]] = None, sandbox: dict[str, Any] | None = None, # Optional
system_prompt: Optional[str | dict[str, Any]] = None, system_prompt: str | dict | None = None, # Optional
api_key: Optional[str] = None, api_key: str | None = None, # Optional (uses env var)
**kwargs: Any,
) )
``` ```
#### Parameters **Parameters:**
**Required:** - **`signature`** (required) - DSPy signature defining input/output fields (must have exactly 1 input and 1 output)
- **`working_directory`** - Directory where Claude will execute commands (default: `"."`)
- **`model`** - Claude model to use (default: `"claude-opus-4-5-20251101"`)
- **`permission_mode`** - Permission mode: `"default"`, `"acceptEdits"`, `"plan"`, `"bypassPermissions"`
- **`allowed_tools`** - List of allowed tool names (e.g., `["Read", "Write", "Bash"]`)
- **`disallowed_tools`** - List of disallowed tool names
- **`sandbox`** - Sandbox configuration dict
- **`system_prompt`** - Custom system prompt or preset config
- **`api_key`** - Anthropic API key (falls back to `ANTHROPIC_API_KEY` env var)
- **`signature`** (`str | type[Signature]`) ### ClaudeCode
- 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`) Main agent class.
- Directory where Claude will execute commands
- Example: `"."`, `"/path/to/project"`
**Optional:** ```python
class ClaudeCode(PrecompiledProgram):
def __init__(self, config: ClaudeCodeConfig)
```
- **`model`** (`Optional[str]`) **Parameters:**
- Model to use: `"sonnet"`, `"opus"`, `"haiku"`
- Default: Claude Code default (typically Sonnet)
- **`permission_mode`** (`Optional[str]`) - **`config`** - ClaudeCodeConfig instance with agent configuration
- 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 #### Methods
##### `forward(**kwargs) -> Prediction` ##### `__call__(**kwargs) -> Prediction` (or `forward`)
Execute the agent with an input message. Execute the agent with an input message.
@@ -160,19 +218,42 @@ Execute the agent with an input message.
**Example:** **Example:**
```python ```python
config = ClaudeCodeConfig(
signature="message:str -> answer:str",
working_directory="."
)
agent = ClaudeCode(config)
result = agent(message="Hello") result = agent(message="Hello")
print(result.answer) # Access typed output print(result.answer) # Access typed output
print(result.trace) # List of execution items print(result.trace) # List of execution items
print(result.usage) # Token usage stats print(result.usage) # Token usage stats
``` ```
##### `push_to_hub(repo_id: str) -> None`
Push the agent to Modaic Hub.
**Arguments:**
- `repo_id` - Repository ID in format "username/repo-name"
**Example:**
```python
agent.push_to_hub("your-username/your-agent")
```
##### `aforward(**kwargs) -> Prediction` ##### `aforward(**kwargs) -> Prediction`
Async version of `forward()` for use in async contexts. Async version of `__call__()` for use in async contexts.
**Example:** **Example:**
```python ```python
async def main(): async def main():
config = ClaudeCodeConfig(
signature="message:str -> answer:str",
working_directory="."
)
agent = ClaudeCode(config)
result = await agent.aforward(message="Hello") result = await agent.aforward(message="Hello")
print(result.answer) print(result.answer)
``` ```
@@ -183,17 +264,31 @@ async def main():
Get the session ID for this agent instance. Get the session ID for this agent instance.
- Returns `None` until first `forward()` call - Returns `None` until first call
- Persists across multiple `forward()` calls - Persists across multiple calls
- Useful for debugging and logging - Useful for debugging and logging
**Example:** **Example:**
```python ```python
agent = ClaudeAgent(sig, working_directory=".") config = ClaudeCodeConfig(
signature="message:str -> answer:str",
working_directory="."
)
agent = ClaudeCode(config)
print(agent.session_id) # None print(agent.session_id) # None
result = agent(message="Hello") result = agent(message="Hello")
print(agent.session_id) # '0199e95f-2689-7501-a73d-038d77dd7320' print(agent.session_id) # 'eb1b2f39-e04c-4506-9398-b50053b1fd83'
```
##### `config: ClaudeCodeConfig`
Access to the agent's configuration.
```python
print(agent.config.model) # 'claude-opus-4-5-20251101'
print(agent.config.working_directory) # '.'
``` ```
## Usage Patterns ## Usage Patterns
@@ -203,7 +298,13 @@ print(agent.session_id) # '0199e95f-2689-7501-a73d-038d77dd7320'
Each agent instance maintains a stateful session: Each agent instance maintains a stateful session:
```python ```python
agent = ClaudeAgent(sig, working_directory=".") from claude_dspy import ClaudeCode, ClaudeCodeConfig
config = ClaudeCodeConfig(
signature="message:str -> answer:str",
working_directory=".",
)
agent = ClaudeCode(config)
# Turn 1 # Turn 1
result1 = agent(message="What's the main bug?") result1 = agent(message="What's the main bug?")
@@ -226,12 +327,19 @@ print(agent.session_id)
Want a new conversation? Create a new agent: Want a new conversation? Create a new agent:
```python ```python
from claude_dspy import ClaudeCode, ClaudeCodeConfig
config = ClaudeCodeConfig(
signature="message:str -> answer:str",
working_directory=".",
)
# Agent 1 - Task A # Agent 1 - Task A
agent1 = ClaudeAgent(sig, working_directory=".") agent1 = ClaudeCode(config)
result1 = agent1(message="Analyze bug in module A") result1 = agent1(message="Analyze bug in module A")
# Agent 2 - Task B (no context from Agent 1) # Agent 2 - Task B (no context from Agent 1)
agent2 = ClaudeAgent(sig, working_directory=".") agent2 = ClaudeCode(config)
result2 = agent2(message="Analyze bug in module B") result2 = agent2(message="Analyze bug in module B")
``` ```
@@ -240,6 +348,9 @@ result2 = agent2(message="Analyze bug in module B")
Enhance prompts with field descriptions: Enhance prompts with field descriptions:
```python ```python
import dspy
from claude_dspy import ClaudeCode, ClaudeCodeConfig
class MySignature(dspy.Signature): class MySignature(dspy.Signature):
"""Analyze code architecture.""" """Analyze code architecture."""
@@ -249,7 +360,11 @@ class MySignature(dspy.Signature):
"1) Architecture overview, 2) Key components, 3) Dependencies" "1) Architecture overview, 2) Key components, 3) Dependencies"
) )
agent = ClaudeAgent(MySignature, working_directory=".") config = ClaudeCodeConfig(
signature=MySignature,
working_directory=".",
)
agent = ClaudeCode(config)
result = agent(message="Analyze this codebase") result = agent(message="Analyze this codebase")
# The description is automatically appended to the prompt # The description is automatically appended to the prompt
@@ -260,7 +375,13 @@ result = agent(message="Analyze this codebase")
Access detailed execution information: Access detailed execution information:
```python ```python
from claude_agent import ToolUseItem, ToolResultItem from claude_dspy import ClaudeCode, ClaudeCodeConfig, ToolUseItem, ToolResultItem
config = ClaudeCodeConfig(
signature="message:str -> answer:str",
working_directory=".",
)
agent = ClaudeCode(config)
result = agent(message="Fix the bug") result = agent(message="Fix the bug")
@@ -294,28 +415,33 @@ print(f"Total: {result.usage.total_tokens}")
Control what the agent can do: Control what the agent can do:
```python ```python
from claude_dspy import ClaudeCode, ClaudeCodeConfig
# Read-only (safest) # Read-only (safest)
agent = ClaudeAgent( config = ClaudeCodeConfig(
sig, signature="message:str -> answer:str",
working_directory=".", working_directory=".",
permission_mode="default", permission_mode="default",
allowed_tools=["Read", "Glob", "Grep"], allowed_tools=["Read", "Glob", "Grep"],
) )
agent = ClaudeCode(config)
# Auto-accept file edits # Auto-accept file edits
agent = ClaudeAgent( config = ClaudeCodeConfig(
sig, signature="message:str -> answer:str",
working_directory=".", working_directory=".",
permission_mode="acceptEdits", permission_mode="acceptEdits",
allowed_tools=["Read", "Write", "Edit"], allowed_tools=["Read", "Write", "Edit"],
) )
agent = ClaudeCode(config)
# Sandbox mode for command execution # Sandbox mode for command execution
agent = ClaudeAgent( config = ClaudeCodeConfig(
sig, signature="message:str -> answer:str",
working_directory=".", working_directory=".",
sandbox={"enabled": True}, sandbox={"enabled": True},
) )
agent = ClaudeCode(config)
``` ```
## Advanced Examples ## Advanced Examples
@@ -324,6 +450,7 @@ agent = ClaudeAgent(
```python ```python
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from claude_dspy import ClaudeCode, ClaudeCodeConfig
class CodeReview(BaseModel): class CodeReview(BaseModel):
summary: str = Field(description="High-level summary") summary: str = Field(description="High-level summary")
@@ -331,15 +458,14 @@ class CodeReview(BaseModel):
severity: str = Field(description="critical, high, medium, or low") severity: str = Field(description="critical, high, medium, or low")
recommendations: list[str] = Field(description="Actionable recommendations") recommendations: list[str] = Field(description="Actionable recommendations")
sig = dspy.Signature('message:str -> review:CodeReview') config = ClaudeCodeConfig(
signature="message:str -> review:CodeReview",
agent = ClaudeAgent(
sig,
working_directory="/path/to/project", working_directory="/path/to/project",
model="sonnet", model="sonnet",
permission_mode="default", permission_mode="default",
allowed_tools=["Read", "Glob", "Grep"], allowed_tools=["Read", "Glob", "Grep"],
) )
agent = ClaudeCode(config)
result = agent(message="Review the changes in src/main.py") result = agent(message="Review the changes in src/main.py")
@@ -351,13 +477,15 @@ for issue in result.review.issues:
### Example 2: Iterative Debugging ### Example 2: Iterative Debugging
```python ```python
sig = dspy.Signature('message:str -> response:str') from claude_dspy import ClaudeCode, ClaudeCodeConfig
agent = ClaudeAgent(
sig, config = ClaudeCodeConfig(
signature="message:str -> response:str",
working_directory=".", working_directory=".",
permission_mode="acceptEdits", permission_mode="acceptEdits",
allowed_tools=["Read", "Write", "Bash"], allowed_tools=["Read", "Write", "Bash"],
) )
agent = ClaudeCode(config)
# Turn 1: Find the bug # Turn 1: Find the bug
result1 = agent(message="Find the bug in src/calculator.py") result1 = agent(message="Find the bug in src/calculator.py")
@@ -380,10 +508,14 @@ print(result4.response)
```python ```python
import asyncio import asyncio
from claude_dspy import ClaudeCode, ClaudeCodeConfig
async def main(): async def main():
sig = dspy.Signature('message:str -> answer:str') config = ClaudeCodeConfig(
agent = ClaudeAgent(sig, working_directory=".") signature="message:str -> answer:str",
working_directory=".",
)
agent = ClaudeCode(config)
# Use aforward in async context # Use aforward in async context
result = await agent.aforward(message="Analyze this code") result = await agent.aforward(message="Analyze this code")
@@ -409,20 +541,20 @@ When accessing `result.trace`, you'll see various item types:
## How It Works ## How It Works
### Signature <20> Claude Flow ### Signature <20> Claude Flow
``` ```
1. Define signature: 'message:str -> answer:str' 1. Define signature: 'message:str -> answer:str'
2. ClaudeAgent validates (must have 1 input, 1 output) 2. ClaudeCode validates (must have 1 input, 1 output)
3. __init__ creates ClaudeSDKClient with options 3. __init__ creates ClaudeSDKClient with options
4. forward(message="...") extracts message 4. forward(message="...") extracts message
5. If output field has desc <20> append to message 5. If output field has desc <20> append to message
6. If output type ` str <20> generate JSON schema 6. If output type ` str <20> generate JSON schema
7. Call client.query(message) with optional output_format 7. Call client.query(message) with optional output_format
@@ -452,9 +584,9 @@ sig = dspy.Signature('message:str -> report:BugReport')
## Troubleshooting ## Troubleshooting
### Error: "ClaudeAgent requires exactly 1 input field" ### Error: "ClaudeCode requires exactly 1 input field"
Your signature has too many or too few fields. ClaudeAgent expects exactly one input and one output: Your signature has too many or too few fields. ClaudeCode expects exactly one input and one output:
```python ```python
# L Wrong - multiple inputs # L Wrong - multiple inputs
@@ -498,7 +630,7 @@ async def main():
### Why 1 input, 1 output? ### 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. ClaudeCode 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: For complex inputs, compose them into the message:
@@ -510,7 +642,7 @@ result = agent(message=message)
### Why stateful sessions? ### Why stateful sessions?
Agents often need multi-turn context (e.g., "fix the bug" <20> "write tests for it"). Stateful sessions make this natural without manual history management. Agents often need multi-turn context (e.g., "fix the bug" <20> "write tests for it"). Stateful sessions make this natural without manual history management.
Want fresh context? Create a new agent instance. Want fresh context? Create a new agent instance.
@@ -526,7 +658,7 @@ The trace provides full visibility into agent execution.
## Comparison with CodexAgent ## Comparison with CodexAgent
| Feature | CodexAgent | ClaudeAgent | | Feature | CodexAgent | ClaudeCode |
|---------|-----------|-------------| |---------|-----------|-------------|
| SDK | OpenAI Codex SDK | Claude Code Python SDK | | SDK | OpenAI Codex SDK | Claude Code Python SDK |
| Thread management | Built-in thread ID | Session-based (ClaudeSDKClient) | | Thread management | Built-in thread ID | Session-based (ClaudeSDKClient) |
@@ -535,6 +667,7 @@ The trace provides full visibility into agent execution.
| Tool types | Codex-specific | Claude Code tools (Bash, Read, Write, etc.) | | Tool types | Codex-specific | Claude Code tools (Bash, Read, Write, etc.) |
| Sandbox | Simple mode enum | Detailed config dict | | Sandbox | Simple mode enum | Detailed config dict |
| Permission control | Sandbox modes | Permission modes + allowed_tools | | Permission control | Sandbox modes | Permission modes + allowed_tools |
| Configuration | Direct parameters | Config object (ClaudeCodeConfig) |
## Examples Directory ## Examples Directory