diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..d10959f2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,79 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview +deepagents is a Python package implementing "Deep Agent" architecture for creating intelligent agents capable of handling complex, multi-step tasks. It uses LangGraph for agent orchestration and Claude Sonnet 4 as the default LLM. + +## Common Development Commands + +### Setup +```bash +# Install in development mode +pip install -e . + +# Install dependencies +pip install langgraph>=0.2.6 langchain-anthropic>=0.1.23 langchain>=0.2.14 +``` + +### Running Examples +```bash +# Run the research agent example +cd examples/research +pip install -r requirements.txt +langgraph test # Run with LangGraph Studio +``` + +### Building the Package +```bash +# Build distribution +python -m build + +# Upload to PyPI (requires credentials) +python -m twine upload dist/* +``` + +### Running Tests +```bash +# Install test dependencies +pip install -e ".[dev]" + +# Run all tests +pytest + +# Run with coverage +pytest --cov=deepagents + +# Run specific test file +pytest tests/test_create_deep_agent.py + +# Run specific test class or method +pytest tests/test_create_deep_agent.py::TestCreateDeepAgent::test_create_deep_agent_happy_path +``` + +## Architecture Overview + +### Core Components +- **`src/deepagents/graph.py`**: Main agent creation logic using LangGraph's `create_react_agent` +- **`src/deepagents/state.py`**: State management with `DeepAgentState` extending LangGraph's `AgentState` +- **`src/deepagents/tools.py`**: Built-in tools (file operations, todo management) +- **`src/deepagents/sub_agent.py`**: Sub-agent spawning and task delegation +- **`src/deepagents/prompts.py`**: System prompts and tool descriptions + +### Key Design Patterns +1. **Mock Filesystem**: Uses LangGraph state instead of real files for safety. Files are stored in the agent's state with a file reducer for merging. +2. **Sub-Agent Architecture**: Main agent can spawn specialized sub-agents with isolated context and specific tool access. +3. **State Management**: Uses TypedDict for structured state with `todos` list and `files` dictionary. +4. **Tool System**: Tools are LangChain-compatible functions that modify agent state. + +### Important Implementation Details +- Default model is Claude Sonnet 4 (`claude-sonnet-4-20250514`) with 64k token limit +- Sub-agents receive quarantined context to prevent information leakage +- File operations validate path safety and perform string replacement validation +- Todo management tracks task status (pending, in_progress, completed) + +## Development Notes +- Test suite is in `tests/` directory - run with `pytest` +- No linting/formatting configuration - consider adding black, flake8, mypy +- Package version is in `pyproject.toml` - update when releasing +- Examples in `examples/` directory demonstrate usage patterns \ No newline at end of file diff --git a/README.md b/README.md index 7731e7c6..b46b64ee 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,58 @@ You can also specify [custom sub agents](#subagents--optional-) with their own i Sub agents are useful for ["context quarantine"](https://www.dbreunig.com/2025/06/26/how-to-fix-your-context.html#context-quarantine) (to help not pollute the overall context of the main agent) as well as custom instructions. +## Development + +This project includes a test suite and build tools for development. You'll need Python 3.11 or higher installed on your system. + +### Setting Up a Development Environment + +1. Create a virtual environment (use one of these names which are already in `.gitignore`): + ```bash + python -m venv venv # or .venv, env, or ENV + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` + +2. Install the package with development dependencies: + ```bash + pip install -e ".[dev]" + ``` + +### Running Tests + +Run all tests: +```bash +pytest +``` + +Run tests with coverage report: +```bash +pytest --cov=deepagents +``` + +Run a specific test file: +```bash +pytest tests/test_create_deep_agent.py +``` + +### Building the Package + +To build the package for distribution: + +1. Install the build tool: + ```bash + pip install build + ``` + +2. Build the package: + ```bash + python -m build + ``` + +This will create both wheel and source distributions in the `dist/` directory: +- `deepagents-{version}-py3-none-any.whl` - wheel distribution +- `deepagents-{version}.tar.gz` - source distribution + ## Roadmap - [ ] Allow users to customize full system prompt - [ ] Code cleanliness (type hinting, docstrings, formating) diff --git a/pyproject.toml b/pyproject.toml index 7ba5fe77..bd3d5acf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "deepagents" version = "0.0.3" description = "General purpose 'deep agent' with sub-agent spawning, todo list capabilities, and mock file system. Built on LangGraph." readme = "README.md" -license = { text = "MIT" } +license = "MIT" requires-python = ">=3.11,<4.0" dependencies = [ "langgraph>=0.2.6", @@ -11,6 +11,12 @@ dependencies = [ "langchain>=0.2.14", ] +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "pytest-cov>=6.0.0", +] + [build-system] requires = ["setuptools>=73.0.0", "wheel"] @@ -23,3 +29,10 @@ packages = ["deepagents"] [tool.setuptools.package-data] "*" = ["py.typed"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "-v" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_create_deep_agent.py b/tests/test_create_deep_agent.py new file mode 100644 index 00000000..33b5e42e --- /dev/null +++ b/tests/test_create_deep_agent.py @@ -0,0 +1,161 @@ +import pytest +from unittest.mock import patch, MagicMock +from langchain_core.tools import tool +from langgraph.graph.state import CompiledStateGraph + +from deepagents import create_deep_agent, DeepAgentState, SubAgent + + +class TestCreateDeepAgent: + """Test suite for create_deep_agent function.""" + + @patch("deepagents.graph.get_default_model") + def test_create_deep_agent_happy_path(self, mock_get_default_model): + """Test creating a deep agent with basic configuration.""" + mock_model = MagicMock() + mock_get_default_model.return_value = mock_model + + @tool + def sample_tool(query: str) -> str: + """A sample tool for testing.""" + return f"Result for: {query}" + + instructions = "You are a helpful AI assistant for testing purposes." + + agent = create_deep_agent(tools=[sample_tool], instructions=instructions) + + assert agent is not None + assert isinstance(agent, CompiledStateGraph) + mock_get_default_model.assert_called_once() + + @patch("deepagents.graph.get_default_model") + def test_create_deep_agent_with_subagents(self, mock_get_default_model): + """Test creating a deep agent with subagents.""" + mock_model = MagicMock() + mock_get_default_model.return_value = mock_model + + research_agent = SubAgent( + name="research", + description="Research information on the web", + prompt="You are a research assistant. Focus on finding accurate information.", + tools=[], + ) + + agent = create_deep_agent( + tools=[], + instructions="Main agent with subagents", + subagents=[research_agent], + ) + + assert agent is not None + assert isinstance(agent, CompiledStateGraph) + mock_get_default_model.assert_called_once() + + @patch("deepagents.graph.get_default_model") + def test_create_deep_agent_with_custom_model(self, mock_get_default_model): + """Test creating a deep agent with a custom model.""" + custom_model = MagicMock() + + agent = create_deep_agent( + tools=[], instructions="Agent with custom model", model=custom_model + ) + + assert agent is not None + assert isinstance(agent, CompiledStateGraph) + mock_get_default_model.assert_not_called() + + @patch("deepagents.graph.get_default_model") + def test_create_deep_agent_with_custom_state_schema(self, mock_get_default_model): + """Test creating a deep agent with custom state schema.""" + mock_model = MagicMock() + mock_get_default_model.return_value = mock_model + + class CustomAgentState(DeepAgentState): + custom_field: str = "" + + agent = create_deep_agent( + tools=[], + instructions="Agent with custom state", + state_schema=CustomAgentState, + ) + + assert agent is not None + assert isinstance(agent, CompiledStateGraph) + mock_get_default_model.assert_called_once() + + @patch("deepagents.graph.get_default_model") + def test_create_deep_agent_with_multiple_tools(self, mock_get_default_model): + """Test creating a deep agent with multiple custom tools.""" + mock_model = MagicMock() + mock_get_default_model.return_value = mock_model + + @tool + def calculator(expression: str) -> str: + """Calculate mathematical expressions.""" + return f"Calculated: {expression}" + + @tool + def translator(text: str, target_language: str) -> str: + """Translate text to target language.""" + return f"Translated '{text}' to {target_language}" + + agent = create_deep_agent( + tools=[calculator, translator], instructions="Multi-tool agent for testing" + ) + + assert agent is not None + assert isinstance(agent, CompiledStateGraph) + mock_get_default_model.assert_called_once() + + @patch("deepagents.graph.get_default_model") + def test_create_deep_agent_empty_tools(self, mock_get_default_model): + """Test creating a deep agent with no additional tools.""" + mock_model = MagicMock() + mock_get_default_model.return_value = mock_model + + agent = create_deep_agent( + tools=[], instructions="Basic agent with only built-in tools" + ) + + assert agent is not None + assert isinstance(agent, CompiledStateGraph) + mock_get_default_model.assert_called_once() + + @patch("deepagents.graph.get_default_model") + def test_create_deep_agent_complex_scenario(self, mock_get_default_model): + """Test creating a deep agent with all features combined.""" + mock_model = MagicMock() + mock_get_default_model.return_value = mock_model + + @tool + def web_search(query: str) -> str: + """Search the web for information.""" + return f"Search results for: {query}" + + coding_agent = SubAgent( + name="coder", + description="Write and review code", + prompt="You are an expert programmer.", + tools=[], + ) + + writer_agent = SubAgent( + name="writer", + description="Write and edit text content", + prompt="You are a professional writer.", + tools=[], + ) + + class ProjectState(DeepAgentState): + project_name: str = "test_project" + + agent = create_deep_agent( + tools=[web_search], + instructions="You are a project manager coordinating various tasks.", + subagents=[coding_agent, writer_agent], + state_schema=ProjectState, + ) + + assert agent is not None + assert isinstance(agent, CompiledStateGraph) + mock_get_default_model.assert_called_once()