diff --git a/README.md b/README.md index 3eb65f7..fbe67f3 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 - **Output field descriptions** - Automatically enhance prompts - **Async support** - Both sync and async execution modes +- **Modaic Hub Integration** - Push and pull agents from Modaic Hub ## Installation ```bash # Install with uv -uv add claude-agent-sdk dspy nest-asyncio +uv add claude-agent-sdk dspy modaic nest-asyncio # Or with pip -pip install claude-agent-sdk dspy nest-asyncio +pip install claude-agent-sdk dspy modaic nest-asyncio ``` **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)) - 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="" +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 ```python -import dspy -from claude_agent import ClaudeAgent +from claude_dspy import ClaudeCode, ClaudeCodeConfig -# Define signature -sig = dspy.Signature('message:str -> answer:str') +# Create config +config = ClaudeCodeConfig( + signature="message:str -> answer:str", + working_directory="." +) # Create agent -agent = ClaudeAgent(sig, working_directory=".") +agent = ClaudeCode(config) # Use it result = agent(message="What files are in this directory?") @@ -51,6 +110,7 @@ print(result.usage) # Token counts ### Structured Output with Pydantic ```python +from claude_dspy import ClaudeCode, ClaudeCodeConfig from pydantic import BaseModel, Field class BugReport(BaseModel): @@ -58,94 +118,92 @@ class BugReport(BaseModel): description: str affected_files: list[str] -sig = dspy.Signature('message:str -> report:BugReport') -agent = ClaudeAgent(sig, working_directory=".") +# Create config with Pydantic output +config = ClaudeCodeConfig( + signature="message:str -> report:BugReport", + working_directory="." +) + +agent = ClaudeCode(config) 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) ``` +### 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 -### ClaudeAgent +### ClaudeCodeConfig + +Configuration object for ClaudeCode agents. ```python -class ClaudeAgent(dspy.Module): +class ClaudeCodeConfig: 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, + signature: str | type[Signature], # Required + working_directory: str = ".", # Default: "." + model: str = "claude-opus-4-5-20251101", # Default model + permission_mode: str | None = None, # Optional + allowed_tools: list[str] | None = None, # Optional + disallowed_tools: list[str] | None = None, # Optional + sandbox: dict[str, Any] | None = None, # Optional + system_prompt: str | dict | None = None, # Optional + api_key: str | None = None, # Optional (uses env var) ) ``` -#### 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]`) - - 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`) +### ClaudeCode -- **`working_directory`** (`str`) - - Directory where Claude will execute commands - - Example: `"."`, `"/path/to/project"` +Main agent class. -**Optional:** +```python +class ClaudeCode(PrecompiledProgram): + def __init__(self, config: ClaudeCodeConfig) +``` -- **`model`** (`Optional[str]`) - - Model to use: `"sonnet"`, `"opus"`, `"haiku"` - - Default: Claude Code default (typically Sonnet) +**Parameters:** -- **`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 +- **`config`** - ClaudeCodeConfig instance with agent configuration #### Methods -##### `forward(**kwargs) -> Prediction` +##### `__call__(**kwargs) -> Prediction` (or `forward`) Execute the agent with an input message. @@ -160,19 +218,42 @@ Execute the agent with an input message. **Example:** ```python +config = ClaudeCodeConfig( + signature="message:str -> answer:str", + working_directory="." +) +agent = ClaudeCode(config) + result = agent(message="Hello") print(result.answer) # Access typed output print(result.trace) # List of execution items 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` -Async version of `forward()` for use in async contexts. +Async version of `__call__()` for use in async contexts. **Example:** ```python async def main(): + config = ClaudeCodeConfig( + signature="message:str -> answer:str", + working_directory="." + ) + agent = ClaudeCode(config) result = await agent.aforward(message="Hello") print(result.answer) ``` @@ -183,17 +264,31 @@ async def main(): Get the session ID for this agent instance. -- Returns `None` until first `forward()` call -- Persists across multiple `forward()` calls +- Returns `None` until first call +- Persists across multiple calls - Useful for debugging and logging **Example:** ```python -agent = ClaudeAgent(sig, working_directory=".") +config = ClaudeCodeConfig( + signature="message:str -> answer:str", + working_directory="." +) +agent = ClaudeCode(config) + print(agent.session_id) # None 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 @@ -203,7 +298,13 @@ print(agent.session_id) # '0199e95f-2689-7501-a73d-038d77dd7320' Each agent instance maintains a stateful session: ```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 result1 = agent(message="What's the main bug?") @@ -226,12 +327,19 @@ print(agent.session_id) Want a new conversation? Create a new agent: ```python +from claude_dspy import ClaudeCode, ClaudeCodeConfig + +config = ClaudeCodeConfig( + signature="message:str -> answer:str", + working_directory=".", +) + # Agent 1 - Task A -agent1 = ClaudeAgent(sig, working_directory=".") +agent1 = ClaudeCode(config) result1 = agent1(message="Analyze bug in module A") # 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") ``` @@ -240,6 +348,9 @@ result2 = agent2(message="Analyze bug in module B") Enhance prompts with field descriptions: ```python +import dspy +from claude_dspy import ClaudeCode, ClaudeCodeConfig + class MySignature(dspy.Signature): """Analyze code architecture.""" @@ -249,7 +360,11 @@ class MySignature(dspy.Signature): "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") # The description is automatically appended to the prompt @@ -260,7 +375,13 @@ result = agent(message="Analyze this codebase") Access detailed execution information: ```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") @@ -294,28 +415,33 @@ print(f"Total: {result.usage.total_tokens}") Control what the agent can do: ```python +from claude_dspy import ClaudeCode, ClaudeCodeConfig + # Read-only (safest) -agent = ClaudeAgent( - sig, +config = ClaudeCodeConfig( + signature="message:str -> answer:str", working_directory=".", permission_mode="default", allowed_tools=["Read", "Glob", "Grep"], ) +agent = ClaudeCode(config) # Auto-accept file edits -agent = ClaudeAgent( - sig, +config = ClaudeCodeConfig( + signature="message:str -> answer:str", working_directory=".", permission_mode="acceptEdits", allowed_tools=["Read", "Write", "Edit"], ) +agent = ClaudeCode(config) # Sandbox mode for command execution -agent = ClaudeAgent( - sig, +config = ClaudeCodeConfig( + signature="message:str -> answer:str", working_directory=".", sandbox={"enabled": True}, ) +agent = ClaudeCode(config) ``` ## Advanced Examples @@ -324,6 +450,7 @@ agent = ClaudeAgent( ```python from pydantic import BaseModel, Field +from claude_dspy import ClaudeCode, ClaudeCodeConfig class CodeReview(BaseModel): summary: str = Field(description="High-level summary") @@ -331,15 +458,14 @@ class CodeReview(BaseModel): 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, +config = ClaudeCodeConfig( + signature="message:str -> review:CodeReview", working_directory="/path/to/project", model="sonnet", permission_mode="default", allowed_tools=["Read", "Glob", "Grep"], ) +agent = ClaudeCode(config) result = agent(message="Review the changes in src/main.py") @@ -351,13 +477,15 @@ for issue in result.review.issues: ### Example 2: Iterative Debugging ```python -sig = dspy.Signature('message:str -> response:str') -agent = ClaudeAgent( - sig, +from claude_dspy import ClaudeCode, ClaudeCodeConfig + +config = ClaudeCodeConfig( + signature="message:str -> response:str", working_directory=".", permission_mode="acceptEdits", allowed_tools=["Read", "Write", "Bash"], ) +agent = ClaudeCode(config) # Turn 1: Find the bug result1 = agent(message="Find the bug in src/calculator.py") @@ -380,10 +508,14 @@ print(result4.response) ```python import asyncio +from claude_dspy import ClaudeCode, ClaudeCodeConfig async def main(): - sig = dspy.Signature('message:str -> answer:str') - agent = ClaudeAgent(sig, working_directory=".") + config = ClaudeCodeConfig( + signature="message:str -> answer:str", + working_directory=".", + ) + agent = ClaudeCode(config) # Use aforward in async context 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 -### Signature ’ Claude Flow +### Signature � Claude Flow ``` 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 4. forward(message="...") extracts message -5. If output field has desc ’ append to message +5. If output field has desc � append to message -6. If output type ` str ’ generate JSON schema +6. If output type ` str � generate JSON schema 7. Call client.query(message) with optional output_format @@ -452,9 +584,9 @@ sig = dspy.Signature('message:str -> report:BugReport') ## 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 # L Wrong - multiple inputs @@ -498,7 +630,7 @@ async def main(): ### 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: @@ -510,7 +642,7 @@ 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. +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. @@ -526,7 +658,7 @@ The trace provides full visibility into agent execution. ## Comparison with CodexAgent -| Feature | CodexAgent | ClaudeAgent | +| Feature | CodexAgent | ClaudeCode | |---------|-----------|-------------| | SDK | OpenAI Codex SDK | Claude Code Python SDK | | 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.) | | Sandbox | Simple mode enum | Detailed config dict | | Permission control | Sandbox modes | Permission modes + allowed_tools | +| Configuration | Direct parameters | Config object (ClaudeCodeConfig) | ## Examples Directory