diff --git a/.env.example b/.env.example index 01413a7c..46df5207 100644 --- a/.env.example +++ b/.env.example @@ -1,183 +1,32 @@ -# Auto-generated .env.example from backup -# Values intentionally blank. +# ============================================================================= +# Haive Framework - Environment Configuration +# ============================================================================= +# Copy this file to .env and fill in your values. -AI21_API_KEY= -ALCHEMY_API_KEY= -ALPHAVANTAGE_API_KEY= -AMADEUS_CLIENT_ID= -AMADEUS_CLIENT_SECRET= -ANTHROPIC_API_KEY= -APIFY_API_TOKEN= -APP_ENV= -ASKNEWS_CLIENT_ID= -ASKNEWS_CLIENT_SECRET= -AWS_ACCESS_KEY_ID= -AWS_DEFAULT_REGION= -AWS_ENDPOINT_URL= -AWS_SECRET_ACCESS_KEY= -AZURE_AI_SERVICES_ENDPOINT= -AZURE_AI_SERVICES_KEY= -AZURE_AI_SERVICES_REGION= -AZURE_CLIENT_ID= -AZURE_DEPLOYMENT_NAME= -AZURE_OBJECT_ID= -AZURE_OPENAI_API_BASE= -AZURE_OPENAI_API_KEY= -AZURE_OPENAI_API_VERSION= -AZURE_OPENAI_ENDPOINT= -AZURE_SECRET_ID= -AZURE_TENANT_ID= -BING_SEARCH_URL= -BING_SUPERVISION_KEY= -BRAVE_SEARCH_API_KEY= -CLICKUP_API_KEY= -CLICKUP_CLIENT_ID= -CLICKUP_CLIENT_SECRET= -CLICKUP_REDIRECT_URI= -CLOUDFLARE_ACCOUNT_ID= -CLOUDFLARE_API_TOKEN= -CLOUDFLARE_R2_BUCKET= -CLOUDFLARE_R2_ENDPOINT= -DATAFORSEO_LOGIN= -DATAFORSEO_PASSWORD= -DEBUG= -DEEPL_API_KEY= -DEEPSEEK_API_KEY= -DISCORD_APPLICATION_ID= -DISCORD_BOT_TOKEN= -DISCORD_CLIENT_ID= -DISCORD_PUBLIC_KEY= -DOCS_PACKAGES_BASE_URL= -DOCS_PUBLIC_BASE_URL= -DOCS_ROOT_URL= -DROPBOX_ACCESS_TOKEN= -DROPBOX_APP_KEY= -DROPBOX_APP_SECRET= -DROPBOX_FOLDER_PATH= -ELEVENLABS_API_KEY= -EMBEDDINGS_API_ENDPOINT= -EMBEDDINGS_API_KEY= -EMBEDDINGS_API_REGION= -EMBEDDINGS_API_VERSION= -ETHERSCAN_API_KEY= -FIGMA_ACCESS_TOKEN= -FINANCIAL_DATASETS_API_KEY= -FIREWORKS_AI_API_KEY= -GITHUB_ACCESS_TOKEN= -GITHUB_APP_ID= -GITHUB_APP_PRIVATE_KEY= -GITHUB_BASE_BRANCH= -GITHUB_BASE_URL= -GITHUB_BRANCH= -GITHUB_ORG= -GITHUB_REPOSITORY= -GMAIL_API_KEY= -GMAIL_CLIENT_ID= -GMAIL_CLIENT_SECRET= -GOOGLE_API_KEY= -GOOGLE_BOOKS_API_KEY= -GOOGLE_CSE_ID= -GPLACES_API_KEY= -GROQ_API_KEY= -HAIVE_AGENTS_DOCS_URL= -HAIVE_AGENTS_REPO_URL= -HAIVE_CORE_DOCS_URL= -HAIVE_CORE_REPO_URL= -HAIVE_DATAFLOW_DOCS_URL= -HAIVE_DATAFLOW_REPO_URL= -HAIVE_GAMES_DOCS_URL= -HAIVE_GAMES_REPO_URL= -HAIVE_HAP_DOCS_URL= -HAIVE_HAP_REPO_URL= -HAIVE_MCP_DOCS_URL= -HAIVE_MCP_REPO_URL= -HAIVE_PREBUILT_DOCS_URL= -HAIVE_PREBUILT_REPO_URL= -HAIVE_TOOLS_DOCS_URL= -HAIVE_TOOLS_REPO_URL= -HF_TOKEN= -JIRA_API_KEY= -JIRA_BASE_URL= -JIRA_ISSUE_TYPE= -JIRA_PROJECT_KEY= -MERRIAM_WEBSTER_API_KEY= -MISTRAL_API_KEY= -MONGO_URI= -NEO4J_DATABASE= -NEO4J_PASSWORD= -NEO4J_URI= -NEO4J_USER= -NEWSAPI_KEY= -NOTION_API_KEY= -OCR_AGENT= -OPENAI_API_KEY= -OPENAI_API_TYPE= -OPENAI_API_VERSION= -OPENWEATHERMAP_API_KEY= -OUTLINE_API_KEY= -OUTLINE_INSTANCE_URL= -PEFT_AVAILABLE= -PERPLEXITY_API_KEY= -PINECONE= -POLYGON_API_KEY= -POSTGRES_CONNECTION_STRING= -REDDIT_CLIENT_ID= -REDDIT_CLIENT_SECRET= -REDDIT_USER_AGENT= -SALESFORCE_DOMAIN= -SALESFORCE_PASSWORD= -SALESFORCE_SECURITY_TOKEN= -SALESFORCE_USERNAME= -SCENEX_API_KEY= -SECRET_KEY= -SERPAPI_API_KEY= -SERP_API_KEY= -SLACK_USER_TOKEN= -SPOONACULAR_API_KEY= -STEAM_ID= -STEAM_KEY= -STRIPE_SECRET_KEY= -SUPABASE_ANON_KEY= -SUPABASE_DATABASE_URI= -SUPABASE_DATABASE_URI_SSL= -SUPABASE_DATABASE_URL= -SUPABASE_DBNAME= -SUPABASE_HOST= -SUPABASE_KEY= -SUPABASE_PASSWORD= -SUPABASE_PORT= -SUPABASE_PROJECT_URL= -SUPABASE_SERVICE_KEY= -SUPABASE_URL= -SUPABASE_USER= -TAVILY_API_KEY= -TWILIO_ACCOUNT_SID= -TWILIO_AUTH_TOKEN= -TWILIO_FROM_NUMBER= -TWILIO_TO_NUMBER= -TWITTER_ACCESS_TOKEN= -TWITTER_ACCESS_TOKEN_SECRET= -TWITTER_API_KEY= -TWITTER_API_SECRET= -TWITTER_BEARER_TOKEN= -WOLFRAM_ALPHA_APPID= -_T= -__all__= -__slots__= -doc= -error_msg= -file_path= -file_paths= -found_secrets_file= -msg= -path= -secrets= -secrets_dirs= -secrets_file_str= -secrets_paths= -stacklevel= -sub_folder_path= -sub_secrets= -value= -value_type= -watcher_type= +# -- LangSmith (Observability & Tracing) -------------------------------------- +LANGSMITH_TRACING=true +LANGSMITH_API_KEY=your-langsmith-api-key +LANGSMITH_PROJECT=haive +LANGSMITH_ENDPOINT=https://api.smith.langchain.com + +# -- LLM Provider Keys -------------------------------------------------------- +OPENAI_API_KEY=your-openai-api-key +ANTHROPIC_API_KEY=your-anthropic-api-key + +# Azure OpenAI (optional) +AZURE_OPENAI_API_KEY=your-azure-api-key +AZURE_OPENAI_ENDPOINT=your-azure-endpoint +AZURE_OPENAI_API_VERSION=2024-02-01 + +# Google (optional) +GOOGLE_API_KEY=your-google-api-key + +# -- LangGraph Platform (for remote deployment) -------------------------------- +LANGGRAPH_API_URL=http://localhost:8123 + +# -- PostgreSQL Persistence (optional) ---------------------------------------- +# DB_HOST=localhost +# DB_PORT=5432 +# DB_NAME=haive +# DB_USER=postgres +# DB_PASS=postgres diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8423ba43..f7c3855e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,5 +1,5 @@ --- -name: π Publish Release +name: Publish Release on: release: types: [published] @@ -9,57 +9,124 @@ permissions: concurrency: group: release-${{ github.ref }} cancel-in-progress: true + jobs: - publish: - name: π¦ Publish ${{ matrix.package }} + # Phase 1: Publish haive-core first (no dependencies) + publish-core: + name: Publish haive-core + runs-on: ubuntu-latest + env: + ACTIONS_STEP_DEBUG: ${{ secrets.ACTIONS_DEBUG || 'false' }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.GH_TOKEN }} + - name: Get Package Poetry Info + id: poetry + uses: ./.github/actions/get-package-poetry-info + with: + package: haive-core + - name: Set up Python ${{ steps.poetry.outputs.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ steps.poetry.outputs.python }} + cache: poetry + - name: Install Poetry + run: pipx install poetry==2.1.0 + - name: Configure PyPI Credentials + run: poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} + - name: Build and Publish haive-core + run: | + cd packages/haive-core + poetry build --format wheel + poetry publish --skip-existing + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: haive-core-dist + path: packages/haive-core/dist/ + + # Phase 2: Publish haive-agents (depends on haive-core) + publish-agents: + name: Publish haive-agents + needs: publish-core + runs-on: ubuntu-latest + env: + ACTIONS_STEP_DEBUG: ${{ secrets.ACTIONS_DEBUG || 'false' }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.GH_TOKEN }} + - name: Get Package Poetry Info + id: poetry + uses: ./.github/actions/get-package-poetry-info + with: + package: haive-agents + - name: Set up Python ${{ steps.poetry.outputs.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ steps.poetry.outputs.python }} + cache: poetry + - name: Install Poetry + run: pipx install poetry==2.1.0 + - name: Configure PyPI Credentials + run: poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} + - name: Build and Publish haive-agents + run: | + cd packages/haive-agents + poetry build --format wheel + poetry publish --skip-existing + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: haive-agents-dist + path: packages/haive-agents/dist/ + + # Phase 3: Publish remaining packages in parallel (depend on haive-agents) + publish-remaining: + name: Publish ${{ matrix.package }} + needs: publish-agents runs-on: ubuntu-latest env: ACTIONS_STEP_DEBUG: ${{ secrets.ACTIONS_DEBUG || 'false' }} strategy: + fail-fast: false matrix: package: - - haive-core - - haive-agents - haive-games - haive-tools - haive-prebuilt - haive-dataflow steps: - - name: π₯ Checkout Code + - name: Checkout Code uses: actions/checkout@v4 with: submodules: true token: ${{ secrets.GH_TOKEN }} - - name: π¦ Get Package Poetry Info + - name: Get Package Poetry Info id: poetry uses: ./.github/actions/get-package-poetry-info with: package: ${{ matrix.package }} - - name: π§Ύ Echo Package Metadata - run: | - echo "π¦ Name: ${{ steps.poetry.outputs.name }}" - echo "π Version: ${{ steps.poetry.outputs.version }}" - echo "π§ Authors: ${{ steps.poetry.outputs.authors || 'N/A' }}" - echo "π License: ${{ steps.poetry.outputs.license || 'N/A' }}" - echo "π Description: ${{ steps.poetry.outputs.description || 'N/A' }}" - echo "π Homepage: ${{ steps.poetry.outputs.homepage || 'N/A' }}" - echo "π Repository: ${{ steps.poetry.outputs.repository || 'N/A' }}" - echo "π Docs: ${{ steps.poetry.outputs.documentation || 'N/A' }}" - echo "π· Classifiers: ${{ steps.poetry.outputs.classifiers || 'N/A' }}" - - name: π Set up Python ${{ steps.poetry.outputs.python }} + - name: Set up Python ${{ steps.poetry.outputs.python }} uses: actions/setup-python@v5 with: python-version: ${{ steps.poetry.outputs.python }} cache: poetry - - name: π Install Poetry + - name: Install Poetry run: pipx install poetry==2.1.0 - - name: π Configure PyPI Credentials + - name: Configure PyPI Credentials run: poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} - - name: π¦ Build Package (No Publish) + - name: Build and Publish ${{ matrix.package }} run: | cd packages/${{ matrix.package }} poetry build --format wheel - - name: π€ Upload Package as Artifact + poetry publish --skip-existing + - name: Upload Artifact uses: actions/upload-artifact@v4 with: name: ${{ matrix.package }}-dist diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index eaa367c1..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,61 +0,0 @@ ---- -name: π Publish Release -on: - release: - types: [published] -permissions: - contents: read - id-token: write # Needed for PyPI trusted publishing -concurrency: - group: release-${{ github.ref }} - cancel-in-progress: true -jobs: - publish: - name: π¦ Publish ${{ matrix.package }} - runs-on: ubuntu-latest - env: - ACTIONS_STEP_DEBUG: ${{ secrets.ACTIONS_DEBUG || 'false' }} - strategy: - matrix: - package: - - haive-core - - haive-agents - - haive-games - - haive-tools - - haive-prebuilt - - haive-dataflow - steps: - - name: π₯ Checkout Code - uses: actions/checkout@v4 - with: - submodules: true - token: ${{ secrets.GH_TOKEN }} - - name: π¦ Get Poetry Metadata - id: poetry - uses: ./.github/actions/get-poetry-info - - name: π§Ύ Echo Package Metadata - run: | - echo "π¦ Name: ${{ steps.poetry.outputs.name }}" - echo "π Version: ${{ steps.poetry.outputs.version }}" - echo "π§ Authors: ${{ steps.poetry.outputs.authors }}" - echo "π License: ${{ steps.poetry.outputs.license }}" - echo "π Description: ${{ steps.poetry.outputs.description }}" - echo "π Homepage: ${{ steps.poetry.outputs.homepage }}" - echo "π Repository: ${{ steps.poetry.outputs.repository }}" - echo "π Docs: ${{ steps.poetry.outputs.documentation }}" - echo "π· Classifiers:" - echo "${{ steps.poetry.outputs.classifiers }}" - - name: π Set up Python ${{ steps.poetry.outputs.python }} - uses: actions/setup-python@v5 - with: - python-version: ${{ steps.poetry.outputs.python }} - cache: poetry - - name: π Install Poetry - run: pipx install poetry==1.8.0 - - name: π Configure PyPI Credentials - run: poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }} - - name: π¦ Build and Publish ${{ matrix.package }} - run: |- - cd packages/${{ matrix.package }} - poetry build --format wheel - poetry publish --skip-existing diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 0e1871d8..a3b7049d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,15 +1,18 @@ --- version: 2 build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: python: "3.12" -python: - install: - - method: poetry - path: . # Ensure this is the directory with your pyproject.toml - system_packages: false - extra_requirements: - - docs # Only if you defined `docs` extras in poetry + jobs: + post_create_environment: + - pip install poetry + post_install: + - pip install -e packages/haive-core[docs] + - pip install -e packages/haive-agents + - pip install -e packages/haive-games + - pip install -e packages/haive-tools + - pip install -e packages/haive-prebuilt + - pip install -e packages/haive-dataflow sphinx: - configuration: docs/source/conf.py # Not docs/source + configuration: docs/source/conf.py diff --git a/CLAUDE.md b/CLAUDE.md index 6cf4aa93..3c2898ed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,13 +1,12 @@ # CLAUDE.md - Haive Agent Framework -**Purpose**: Central memory hub for Haive development -**Version**: 4.0 -**Last Updated**: 2025-01-15 +**Version**: 5.0 +**Last Updated**: 2026-04-06 -## π― Project Context +## Project Context -- **Directory**: `/home/will/Projects/haive/backend/haive` -- **Branch**: `feature/fix_everything` +- **Directory**: `/home/will/Projects/haive` +- **Branch**: `final-refactor` - **Structure**: Monorepo with Git submodules (7 packages) - **Core Rules**: - Always use `poetry run` prefix for ALL Python commands @@ -15,1481 +14,522 @@ - Always use explicit imports: `from haive.core.*` - Be EXTREMELY careful with submodules - each is its own repo -## π Essential Documentation +## Guides & Documentation -### π§ Memory Index System +### Agent Design (NEW β 2026-04-06) -- **@memory_index/README.md** - Central memory index for all discoveries -- **@memory_index/quick_reference.md** - Most-used patterns and fixes -- **@memory_index/by_date/** - Chronological memory tracking -- **@project_docs/README.md** - Main project documentation hub +- **@project_docs/guides/agent/AGENT_DESIGN_PATTERNS.md** β How to build agents around BaseGraph, state schemas, SimpleAgent/ReactAgent/MultiAgent patterns, anti-patterns +- **@project_docs/guides/agent/MULTIAGENT_STATE_DESIGN.md** β Complex state schemas for multi-agent systems, sequential/parallel/conditional patterns +- **@project_docs/guides/agent/CUSTOM_NODES_AND_GRAPHS.md** β Custom nodes, graph patterns (branching, parallel, reflection loops), NodeConfig types +- **@project_docs/guides/agent/MEMORY_AGENT_GUIDE.md** β Memory + KG integration, Neo4j Cypher, store namespaces, docker-compose +- **@project_docs/guides/agent/STATE_SCHEMA_NOTES.md** β State flow research, engine injection fix, schema hierarchy -### π Documentation Tools & Solutions +### Architecture -- **@project_docs/pydevelop_docs/AUTOAPI_SOLUTION_IMPLEMENTATION_PLAN.md** - β **READY** AutoAPI hierarchical fix for Issue #4 -- **@project_docs/pydevelop_docs/AUTOAPI_HIERARCHICAL_ORGANIZATION_ANALYSIS.md** - Complete analysis of AutoAPI flattening problem +- **@project_docs/active/architecture/state_schema_engine_gap.md** β How engines flow through state (FIXED) +- **@project_docs/active/architecture/multi_agent_meta_agent_memory_hub.md** β Multi-agent architecture decisions +- **@project_docs/active/architecture/agent_as_tool_pattern.md** β Agent-as-tool composition +- **@project_docs/guides/TOOL_ROUTING_REFACTOR.md** β Tool routing: pydantic_model vs pydantic_tool vs parse_output -### Standards & Guides (Import for details) +### Standards -- @project_docs/active/standards/coding/COMMAND_EXECUTION_GUIDE.md -- @project_docs/active/standards/coding/PYDANTIC_PATTERNS.md -- @project_docs/active/standards/testing/philosophy.md -- @project_docs/active/standards/git/workflow.md -- @project_docs/guides/TOOL_ROUTING_REFACTOR.md - **NEW** Routing changes for structured output +- **@project_docs/active/standards/coding/PYDANTIC_PATTERNS.md** β Pydantic best practices +- **@project_docs/active/standards/testing/philosophy.md** β No-mocks testing +- **@project_docs/active/standards/git/workflow.md** β Git safety protocol -### Architecture & Patterns - -- @project_docs/active/architecture/multi_agent_meta_agent_memory_hub.md -- @project_docs/active/architecture/meta_state_pattern.md -- @project_docs/active/architecture/agent_as_tool_pattern.md -- **Generalized Hook System** - Enhanced base agent with pre/post processing hooks - -## π Current Focus - -- **Active Work**: MultiAgent Sequential Pattern (ReactAgent β SimpleAgent) -- **Issues**: @project_docs/sessions/active/current_issues.md -- **Recent Achievements**: See @memory_index/by_date/2025-01-23/ - -## π― Recent Completed Work (2025-01-29) - -### BaseModel Tool Routing & Mixin Fixes β **COMPLETED** - -**Problem Solved**: Comprehensive fix for BaseModel tool routing and mixin integration issues. - -**Key Fixes**: - -- **StructuredOutputMixin**: Fixed to use `"parse_output"` route instead of deprecated `"structured_output"` -- **AugLLMConfig add_tool()**: Fixed to always sync routes, resolving `with_structured_output()` not setting routes -- **ValidationNodeV2**: Enhanced to handle all three BaseModel routes properly -- **Tool Routing System**: Complete validation of routing patterns - -**Routes Now Working**: - -- `structured_output_model` β `"parse_output"` route -- BaseModel without `__call__` β `"pydantic_model"` route (error case) -- BaseModel with `__call__` β `"pydantic_tool"` route (executable tool) -- BaseModel instances β `"function"` route - -**Testing**: All BaseModel patterns validated with real components, no mocks used. - -**Documentation**: Updated `@project_docs/guides/TOOL_ROUTING_REFACTOR.md` with complete details. - -## π₯ Git Safety Protocol (CRITICAL) - -### Essential Safety Commands - -```bash -# BEFORE ANY WORK -git status && git diff - -# BEFORE COMMITTING -git diff --cached && trunk check --all && poetry run pytest -git add specific_file.py && git commit -m "feat: clear description" - -# SUBMODULE SAFETY -cd packages/haive-{package} && git status && git branch -vv -git branch backup-$(date +%Y%m%d-%H%M%S) # Create backup - -# RECOVERY -git reflog # Find lost commits -git branch recovery-branch HEAD@{n} -``` - -**Key Rules**: Never force push submodules, always create backups, check status before work. - -## π οΈ Most Used Commands - -```bash -# Development (ALWAYS with poetry run) -poetry run python script.py -poetry run pytest packages/haive-agents/tests/ -v -poetry run python -c "from haive.core import *; print('Imports OK')" - -# Quality Checks -trunk check --all -trunk check --fix --all -poetry run mypy packages/ -poetry run ruff check - -# Research Before Coding -find packages/ -name "*.py" | xargs grep -l "YourPattern" | head -5 -``` - -## π¦ Project Structure - Namespaced Polyrepo - -This is a **namespaced polyrepo** - multiple repositories managed as submodules: - -``` -packages/ # Each package is its own Git repository! -βββ haive-core/ # github.com/pr1m8/haive-core (foundation) -βββ haive-agents/ # github.com/pr1m8/haive-agents (agent implementations) -βββ haive-tools/ # github.com/pr1m8/haive-tools (tool implementations) -βββ haive-games/ # github.com/pr1m8/haive-games (game environments) -βββ haive-mcp/ # github.com/pr1m8/haive-mcp (MCP integration) -βββ haive-prebuilt/ # github.com/pr1m8/haive-prebuilt (pre-configured) -βββ haive-dataflow/ # github.com/pr1m8/haive-dataflow (data processing) - -project_docs/ # Documentation in main repo only -βββ active/ # Current standards & architecture -βββ sessions/ # Working memory -βββ {package}/ # Package-specific docs -``` - -### β οΈ Polyrepo Implications: - -1. **Each package has its own**: - - Git history - - Branches - - Tags - - Issues/PRs - - CI/CD - -2. **Working with submodules**: - - ```bash - # Update all submodules - git submodule update --init --recursive - - # Work in a submodule - cd packages/haive-agents - git checkout -b my-feature - # Make changes, commit, push - cd ../.. - git add packages/haive-agents - git commit -m "Update haive-agents submodule" - ``` - -3. **CRITICAL**: Changes in submodules must be: - - Committed in the submodule first - - Pushed to the submodule's remote - - Then referenced in main repo - -## π― Critical Development Rules - -1. **NO MOCKS EVER**: Test with real LLMs, real tools, real components -2. **Poetry Run Everything**: Never run Python directly -3. **Research First**: Check existing patterns before implementing -4. **Explicit Imports**: `from haive.core.engine import X` not `from engine import X` -5. **Pydantic Patterns**: Never override `__init__`, use Field validation -6. **Git Safety**: Always check diff before commits -7. **Use TodoWrite**: For planning and tracking -8. **System vs Human Messages**: System message in AugLLMConfig, human message in ChatPromptTemplate -9. **Agent Composition**: Use MultiAgent for combining agents, not complex inheritance -10. **Keep It Simple**: One line compositions like `MultiAgent([Agent1, Agent2], mode="sequential")` - -## π Quick Code Reference +## Quick Reference ### Essential Imports ```python -# Core from haive.core.engine.aug_llm import AugLLMConfig -from haive.core.schema.prebuilt.messages_state import MessagesState -from haive.core.schema.prebuilt.meta_state import MetaStateSchema - -# Agents - Use V3 versions for enhanced features -from haive.agents.simple.agent_v3 import SimpleAgentV3 +from haive.core.schema.prebuilt.llm_state import LLMState +from haive.agents.simple.agent import SimpleAgent from haive.agents.react.agent import ReactAgent -from haive.agents.rag.base.agent import BaseRAGAgent -from haive.agents.rag.simple.answer_agent import AnswerAgent -from haive.agents.multi.enhanced_multi_agent_v4 import EnhancedMultiAgentV4 - -# Tools -from langchain_core.tools import Tool, tool +from haive.agents.multi.agent import MultiAgent +from haive.agents.memory import create_memory_agent +from haive.agents.utils.trace import run_traced +from langchain_core.tools import tool from langchain_core.messages import HumanMessage, AIMessage ``` -### Agent Configuration Patterns +### Agent Patterns (the 4 you need) ```python -# AugLLMConfig - System message goes HERE, not in prompt template -config = AugLLMConfig() # Uses defaults -config = AugLLMConfig( - temperature=0.7, - max_tokens=1000, - system_message="You are a helpful assistant" # System message in AugLLMConfig -) - -# SimpleAgentV3 - Use V3 for enhanced features -from haive.agents.simple.agent_v3 import SimpleAgentV3 - -agent = SimpleAgentV3( - name="my_agent", - engine=config, - prompt_template=ChatPromptTemplate.from_messages([ - ("system", "System message from AugLLMConfig"), - ("human", "User message template with {variables}") - ]) -) - -# ReactAgent with tools -@tool -def calculator(expression: str) -> str: - """Calculate mathematical expressions.""" - return str(eval(expression)) - -agent = ReactAgent( - name="react_agent", - engine=config, - tools=[calculator] -) - -# RAG Pattern - SIMPLE composition -from haive.agents.rag.base.agent import BaseRAGAgent -from haive.agents.rag.simple.answer_agent import AnswerAgent -from haive.agents.multi.enhanced_multi_agent_v4 import EnhancedMultiAgentV4 - -# Simple RAG = BaseRAGAgent + AnswerAgent in sequence -SimpleRAGAgent = EnhancedMultiAgentV4([BaseRAGAgent, AnswerAgent], mode="sequential") -``` - -### More Agent Examples - -#### 1. Research Assistant Agent - -```python -@tool -def web_search(query: str) -> str: - """Search the web for information.""" - # Implementation here - return f"Search results for: {query}" +# 1. SimpleAgent β conversation, no tools +agent = SimpleAgent(name="writer", engine=AugLLMConfig( + temperature=0.8, system_message="You are a writer." +)) +# 2. ReactAgent β tools + reasoning loop @tool -def document_summarizer(text: str) -> str: - """Summarize long documents.""" - # Implementation here - return f"Summary of: {text[:100]}..." - -research_agent = ReactAgent( - name="research_assistant", - engine=AugLLMConfig( - temperature=0.3, - system_message="You are a thorough research assistant. Always cite sources." - ), - tools=[web_search, document_summarizer] -) - -# Usage -result = research_agent.run("Research the latest developments in AI safety") -``` - -#### 2. Code Review Agent with Structured Output - -```python -class CodeReviewResult(BaseModel): - """Structured code review output.""" - overall_rating: int = Field(ge=1, le=10, description="Code quality rating") - issues: List[str] = Field(description="List of issues found") - suggestions: List[str] = Field(description="Improvement suggestions") - security_concerns: List[str] = Field(default_factory=list) - -code_reviewer = SimpleAgentV3( - name="code_reviewer", - engine=AugLLMConfig( - temperature=0.2, - structured_output_model=CodeReviewResult, - system_message="You are an expert code reviewer focusing on quality, security, and best practices." - ), - prompt_template=ChatPromptTemplate.from_messages([ - ("system", "Review the following code and provide structured feedback."), - ("human", "Code to review:\n\n{code}\n\nLanguage: {language}") - ]) -) - -# Usage -review = code_reviewer.run({ - "code": "def unsafe_function(user_input): exec(user_input)", - "language": "Python" -}) -``` - -#### 3. Customer Service Agent - -```python -class CustomerResponse(BaseModel): - """Structured customer service response.""" - response: str = Field(description="Response to customer") - sentiment: str = Field(description="Customer sentiment: positive/negative/neutral") - urgency: str = Field(description="Urgency level: low/medium/high") - follow_up_needed: bool = Field(description="Whether follow-up is required") - -@tool -def lookup_order(order_id: str) -> str: - """Look up customer order information.""" - return f"Order {order_id} details: Status: Shipped, Date: 2025-01-20" - -@tool -def check_inventory(product_id: str) -> str: - """Check product inventory.""" - return f"Product {product_id}: 15 units in stock" - -customer_service_agent = ReactAgent( - name="customer_service", - engine=AugLLMConfig( - temperature=0.6, - structured_output_model=CustomerResponse, - system_message="You are a helpful customer service representative. Be empathetic and solution-focused." - ), - tools=[lookup_order, check_inventory] -) - -# Usage -response = customer_service_agent.run("My order #12345 hasn't arrived yet and I'm worried") -``` - -#### 4. Content Creation Workflow - -```python -# Multi-agent content creation pipeline -content_planner = SimpleAgentV3( - name="content_planner", - engine=AugLLMConfig( - temperature=0.7, - system_message="You create detailed content plans and outlines." - ) -) - -content_writer = SimpleAgentV3( - name="content_writer", - engine=AugLLMConfig( - temperature=0.8, - system_message="You write engaging, high-quality content based on plans." - ) -) - -content_editor = SimpleAgentV3( - name="content_editor", - engine=AugLLMConfig( - temperature=0.3, - system_message="You edit and refine content for clarity and quality." - ) -) - -# Compose into workflow -ContentCreationWorkflow = EnhancedMultiAgentV4([ - content_planner, - content_writer, - content_editor -], mode="sequential") - -# Usage -final_content = ContentCreationWorkflow.run("Create a blog post about AI in healthcare") -``` - -### Memory Management Patterns - -#### 1. Agent with Persistent Memory - -```python -from haive.core.memory import ConversationBufferMemory, VectorStoreMemory - -# Agent with conversation memory -memory_agent = SimpleAgentV3( - name="memory_agent", - engine=AugLLMConfig(temperature=0.7), - memory=ConversationBufferMemory( - memory_key="chat_history", - return_messages=True, - max_token_limit=2000 - ) -) - -# Usage with memory -result1 = memory_agent.run("My name is Alice and I like Python programming") -result2 = memory_agent.run("What do you remember about me?") # Will remember Alice + Python -``` - -#### 2. RAG Agent with Vector Memory - -```python -from haive.core.memory.vector_store import ChromaVectorStore - -# Create vector store for long-term memory -vector_memory = VectorStoreMemory( - vector_store=ChromaVectorStore( - collection_name="agent_memory", - persist_directory="./memory_data" - ), - memory_key="relevant_context", - input_key="query" -) - -# Agent with vector-based memory -smart_agent = SimpleAgentV3( - name="smart_agent", - engine=AugLLMConfig(temperature=0.6), - memory=vector_memory -) - -# Usage - agent remembers across sessions -smart_agent.run("I work at TechCorp as a data scientist") -smart_agent.run("My favorite tools are pandas and scikit-learn") -# Later session -result = smart_agent.run("What do you know about my work?") # Retrieves relevant memories -``` - -#### 3. Multi-Agent with Shared Memory - -```python -from haive.core.memory import SharedMemory - -# Shared memory between agents -shared_memory = SharedMemory( - memory_type="redis", # or "in_memory", "file" - connection_params={"host": "localhost", "port": 6379} -) - -# Multiple agents sharing memory -researcher = ReactAgent( - name="researcher", - engine=AugLLMConfig(), - tools=[web_search], - memory=shared_memory -) - -writer = SimpleAgentV3( - name="writer", - engine=AugLLMConfig(), - memory=shared_memory -) - -# Usage - agents share context -researcher.run("Research AI trends for 2025") -writer.run("Write an article based on the research") # Access researcher's findings -``` - -#### 4. Memory with Custom Retrieval - -```python -class CustomMemoryRetriever: - """Custom memory retrieval strategy.""" - - def retrieve_relevant_memories(self, query: str, k: int = 5) -> List[str]: - """Retrieve memories relevant to query.""" - # Custom logic for memory retrieval - return ["relevant memory 1", "relevant memory 2"] - - def store_memory(self, content: str, metadata: dict = None): - """Store new memory with metadata.""" - # Custom storage logic - pass - -custom_memory = CustomMemoryRetriever() - -# Agent with custom memory strategy -specialized_agent = SimpleAgentV3( - name="specialized_agent", - engine=AugLLMConfig(), - memory=custom_memory -) -``` - -#### 5. Memory-First Routing Agent - -```python -from haive.agents.memory.routing_agent import MemoryRoutingAgent - -# Agent that routes based on memory context -routing_agent = MemoryRoutingAgent( - name="memory_router", - engine=AugLLMConfig(), - agents={ - "technical": ReactAgent(name="tech", tools=[code_analyzer]), - "creative": SimpleAgentV3(name="creative", engine=creative_config), - "research": ReactAgent(name="research", tools=[web_search]) - }, - routing_strategy="memory_similarity", # Route based on memory similarity - memory=vector_memory -) - -# Usage - routes to appropriate agent based on memory -routing_agent.run("Fix this Python bug") # Routes to technical agent -routing_agent.run("Write a poem") # Routes to creative agent -``` - -#### 6. Hierarchical Memory System - -```python -class HierarchicalMemory: - """Multi-level memory system.""" - - def __init__(self): - self.short_term = ConversationBufferMemory(max_token_limit=1000) - self.working_memory = ConversationBufferMemory(max_token_limit=5000) - self.long_term = VectorStoreMemory(vector_store=ChromaVectorStore()) - - def get_context(self, query: str) -> str: - """Get context from all memory levels.""" - recent = self.short_term.get_relevant_context(query) - working = self.working_memory.get_relevant_context(query) - long_term = self.long_term.get_relevant_context(query) - - return f"Recent: {recent}\nWorking: {working}\nLong-term: {long_term}" - -hierarchical_memory = HierarchicalMemory() - -# Agent with hierarchical memory -advanced_agent = SimpleAgentV3( - name="advanced_agent", - engine=AugLLMConfig(), - memory=hierarchical_memory -) -``` - -## ποΈ How to Write Agents - CORRECT Patterns - -### 1. System vs Human Message Pattern - -```python -# β CORRECT - System message in AugLLMConfig, Human message in ChatPromptTemplate -class AnswerAgent(SimpleAgentV3): - engine: AugLLMConfig = Field( - default_factory=lambda: AugLLMConfig( - temperature=0.7, - system_message="You are a helpful assistant." # System message HERE - ) - ) - - prompt_template: ChatPromptTemplate = Field( - default_factory=lambda: ChatPromptTemplate.from_messages([ - ("system", "System message from AugLLMConfig"), - ("human", "User question: {query}\nContext: {context}") # Human template HERE - ]) - ) - -# β WRONG - Everything in one template -prompt_template = ChatPromptTemplate.from_template( - "System: You are helpful\nHuman: {query}" # DON'T DO THIS -) -``` - -### 2. Agent Composition Pattern - Pydantic Classes - -```python -# β CORRECT - Pydantic class extending EnhancedMultiAgentV4 -class SimpleRAGAgent(EnhancedMultiAgentV4): - """Simple RAG = BaseRAGAgent + AnswerAgent in sequence.""" - - agents: List = Field( - default_factory=lambda: [ - BaseRAGAgent(name="retriever"), - AnswerAgent(name="answerer") - ] - ) - - execution_mode: str = Field(default="sequential") - -# β CORRECT - Collective RAG with multiple sources -class CollectiveRAGAgent(EnhancedMultiAgentV4): - """Collective RAG = Multiple SimpleRAGAgent + SynthesisAgent.""" - - agents: List = Field( - default_factory=lambda: [ - SimpleRAGAgent(name="rag_source_1"), - SimpleRAGAgent(name="rag_source_2"), - SimpleRAGAgent(name="rag_source_3"), - SynthesisAgent(name="synthesizer") - ] - ) - - execution_mode: str = Field(default="parallel_then_sequential") - -# β WRONG - Complex inheritance and post_init -class ComplexRAGAgent(Agent): - def model_post_init(self): - # Don't build complex custom classes - super().model_post_init() - self.agents = [...] # Overcomplicating -``` - -### 3. Base Agent Pattern - Generic Engines - -```python -# β CORRECT - Base agent with generic engine that works by default -class BaseRAGAgent(RetrieverMixin, Agent): - engine: BaseRetrieverConfig | VectorStoreConfig = Field( - default_factory=lambda: VectorStoreConfig( - name="default_vectorstore", - provider="InMemory", - embedding_config=HuggingFaceEmbeddingConfig() # Works by default - ) - ) - -# β CORRECT - Simple agent extension -class AnswerAgent(SimpleAgentV3): - """SimpleAgentV3 with specific RAG configuration.""" - - engine: AugLLMConfig = Field( - default_factory=lambda: AugLLMConfig( - temperature=0.7, - system_message="You are a helpful assistant." - ) - ) - - prompt_template: ChatPromptTemplate = Field(...) - -# β WRONG - Building everything from scratch -class MyComplexAgent(Agent): - def __init__(self): - # Don't reinvent the wheel - pass -``` - -### 4. Testing Pattern (NO MOCKS) - -```python -# β CORRECT - Real components, real execution -def test_simple_rag_agent(): - """Test with REAL components.""" - # Create instance - uses default HuggingFace embeddings - rag_agent = SimpleRAGAgent(name="test_rag") - - # Real execution - result = rag_agent.run("What is machine learning?") - assert isinstance(result, str) - assert len(result) > 0 - - # Verify structure - assert len(rag_agent.agents) == 2 - assert rag_agent.execution_mode == "sequential" - -# β WRONG - Mocks and fake responses -def test_with_mocks(): - mock_llm = Mock() - mock_llm.return_value = "fake response" # NOT REAL TESTING -``` - -## π― Structured Output with Pydantic - -### Basic Pattern - -```python -from pydantic import BaseModel, Field -from typing import List, Optional - -class AnalysisResult(BaseModel): - """Structured output model.""" - sentiment: str = Field(description="Overall sentiment") - confidence: float = Field(ge=0.0, le=1.0, description="Confidence score") - key_themes: List[str] = Field(description="Main themes found") - -# Use in agent -class AnalysisAgent(SimpleAgentV3): - engine: AugLLMConfig = Field( - default_factory=lambda: AugLLMConfig( - temperature=0.3, - structured_output_model=AnalysisResult, # Pydantic model HERE - system_message="You are an expert analyst." - ) - ) -``` - -### Multi-Agent Structured Workflow - -```python -# Define output models for each step -class Plan(BaseModel): - questions: List[str] = Field(description="Research questions") - search_terms: List[str] = Field(description="Search terms") - -class Findings(BaseModel): - results: List[str] = Field(description="Key findings") - sources: List[str] = Field(description="Source references") - -class Report(BaseModel): - summary: str = Field(max_length=500, description="Executive summary") - recommendations: List[str] = Field(description="Action items") - -# Create workflow -StructuredWorkflow = EnhancedMultiAgentV4([ - SimpleAgentV3(engine=AugLLMConfig(structured_output_model=Plan)), - SimpleAgentV3(engine=AugLLMConfig(structured_output_model=Findings)), - SimpleAgentV3(engine=AugLLMConfig(structured_output_model=Report)) -], mode="sequential") -``` - -## π Branch Examples & Implementation Locations - -### Current RAG Implementation - -**Branch**: `feature/fix_everything` - -**Files**: - -- `packages/haive-agents/src/haive/agents/rag/base/agent.py` - BaseRAGAgent with HuggingFace embeddings -- `packages/haive-agents/src/haive/agents/rag/simple/answer_agent.py` - AnswerAgent with document prompt -- `packages/haive-agents/src/haive/agents/rag/simple/agent.py` - SimpleRAGAgent Pydantic class - -**Usage**: - -```python -# Import the Pydantic class -from haive.agents.rag.simple.agent import SimpleRAGAgent - -# Create instance - uses default HuggingFace embeddings -rag_agent = SimpleRAGAgent(name="my_rag") - -# Execute RAG workflow -result = rag_agent.run("What is machine learning?") -print(result) -``` - -### Enhanced Agent Architecture - -**Branch**: `feature/fix_everything` - -**Key Files**: - -- `packages/haive-agents/src/haive/agents/simple/agent_v3.py` - SimpleAgentV3 with hooks -- `packages/haive-agents/src/haive/agents/multi/enhanced_multi_agent_v4.py` - Multi-agent orchestration -- `packages/haive-core/src/haive/core/schema/prebuilt/meta_state.py` - MetaStateSchema - -**Pattern**: - -```python -# Enhanced agents with recompilation, hooks, dynamic tools -from haive.agents.simple.agent_v3 import SimpleAgentV3 - -agent = SimpleAgentV3( - name="enhanced_agent", - engine=AugLLMConfig(system_message="System message here"), - prompt_template=ChatPromptTemplate.from_messages([ - ("system", "From AugLLMConfig"), - ("human", "Template with {variables}") - ]) -) -``` - -### Multi-Agent Patterns - -**Branch**: `feature/fix_everything` - -**Files**: - -- `packages/haive-agents/src/haive/agents/multi/enhanced_multi_agent_v4.py` - Core multi-agent -- `packages/haive-agents/src/haive/agents/rag/simple_rag_agent_v4.py` - RAG V4 example -- `packages/haive-agents/src/haive/agents/rag/collective_rag_agent_v4.py` - Collective RAG - -**Simple Composition**: - -```python -# Sequential: Agent A β Agent B β Agent C -MyWorkflow = EnhancedMultiAgentV4([AgentA, AgentB, AgentC], mode="sequential") -``` - -### Generalized Hook System - -**New in Enhanced Base Agent**: All agents now support comprehensive hook system for monitoring, pre/post processing, reflection, and structured output workflows. - -**Key Features**: - -- Pre/post processing agents with message transformation -- Reflection and grading hooks -- Structured output hooks -- Multi-stage workflow monitoring -- Factory patterns for common use cases - -**Files**: - -- `packages/haive-agents/src/haive/agents/base/hooks.py` - Hook system core -- `packages/haive-agents/src/haive/agents/base/pre_post_agent_mixin.py` - Pre/post processing mixin -- `packages/haive-agents/examples/generalized_hooks_example.py` - Comprehensive examples - -**Basic Hook Usage**: - -```python -from haive.agents.simple.agent_v3 import SimpleAgentV3 -from haive.core.engine.aug_llm import AugLLMConfig - -agent = SimpleAgentV3(name="writer", engine=AugLLMConfig()) - -# Add hooks using decorators -@agent.before_run -def log_start(context): - print(f"Starting {context.agent_name}") - -@agent.after_run -def log_end(context): - print(f"Completed {context.agent_name}") - -@agent.before_reflection -def track_reflection(context): - print("Starting reflection analysis") - -# Execute with hook monitoring -result = await agent.arun("Write a story") -``` - -**Pre/Post Processing Pattern**: - -```python -# Create main agent -main_agent = SimpleAgentV3(name="writer", engine=config) - -# Create reflection agent -reflection_agent = SimpleAgentV3(name="critic", engine=reflection_config) - -# Set up post-processing with message transformation -main_agent.post_agent = reflection_agent -main_agent.use_post_transform = True -main_agent.post_transform_type = "reflection" +def search(query: str) -> str: + '''Search.''' + return f"Results for {query}" -# Execute with automatic pre/post processing -result = await main_agent.arun("Write and improve a story") -``` - -**Factory Pattern for Reflection**: +agent = ReactAgent(name="researcher", engine=AugLLMConfig( + tools=[search], system_message="Use search tool." +), max_iterations=3) -```python -from haive.agents.base.pre_post_agent_mixin import create_reflection_agent +# 3. MultiAgent β compose agents +pipeline = MultiAgent(name="pipeline", + agents=[researcher, writer], execution_mode="sequential") -# Create agent with reflection capabilities -enhanced_agent = create_reflection_agent( - main_agent=SimpleAgentV3(name="writer", engine=config) -) - -result = await enhanced_agent.arun("Complex writing task") +# 4. MemoryAgent β persistent memory + KG +agent = create_memory_agent(name="assistant", user_id="user123", + connection_string="postgresql://haive:haive@localhost/haive") ``` -**Multi-Stage Workflow Monitoring**: +### Debug & Trace ```python -from haive.agents.base.hooks import create_multi_stage_hook - -# Track complex workflows -stages = ["analysis", "grading", "reflection", "improvement"] -hook = create_multi_stage_hook(stages) - -agent.add_hook(HookEvent.PRE_PROCESS, hook) -agent.add_hook(HookEvent.POST_PROCESS, hook) - -# Execute with comprehensive monitoring -result = await agent.arun("Complex analytical task") - -# Parallel then sequential: [A, B, C] β D -MyWorkflow = EnhancedMultiAgentV4([AgentA, AgentB, AgentC, AgentD], mode="parallel_then_sequential") +from haive.agents.utils.trace import run_traced +result = run_traced(agent, "Hello", save_to="traces/") ``` -### Documentation & Memory - -**Branch**: `feature/fix_everything` - -**Key Docs**: - -- `CLAUDE.md` - This file - central memory and patterns -- `project_docs/active/architecture/multi_agent_meta_agent_memory_hub.md` - Multi-agent architecture -- `project_docs/active/architecture/meta_state_pattern.md` - MetaStateSchema guide -- `project_docs/active/standards/testing/philosophy.md` - No-mocks testing - -**Memory References**: +## Critical Development Rules -```python -# Use @ to reference memory documents -# @project_docs/active/architecture/multi_agent_meta_agent_memory_hub.md -# @project_docs/active/standards/testing/philosophy.md -``` - -### Testing Pattern (NO MOCKS) - -```python -def test_agent_real_execution(): - """Test with REAL components.""" - config = AugLLMConfig() - agent = SimpleAgent(engine=config) - - result = agent.run("Hello") - assert isinstance(result, str) - assert len(result) > 0 -``` - -## π§ Development Workflow - -### Essential Steps - -1. **Research First**: `find packages/ -name "*.py" | xargs grep -l "YourPattern"` -2. **Plan with TodoWrite**: Break down tasks into steps -3. **Build & Test Incrementally**: Create minimal β test β add feature β test -4. **Use Real Components**: No mocks, test with actual LLMs and tools - -### Test-Driven Pattern - -```python -# 1. Write test first -def test_my_agent_creation(): - agent = MyAgent() - assert agent is not None - -# 2. Make it pass -class MyAgent: - def __init__(self): - pass - -# 3. Add feature test -def test_my_agent_execution(): - agent = MyAgent(config=AugLLMConfig()) - result = agent.run("Hello") - assert isinstance(result, str) - -# Continue incrementally... -``` - -## π Package Import Hierarchy - -``` -# ALLOWED: -- Core β standard library, third-party -- Agents β core, standard library, third-party -- Tools β core, standard library, third-party -- Games β core, agents, tools, third-party - -# FORBIDDEN: -- Core β agents/tools/games (circular!) -``` - -## π¨ Coding Style & Standards +1. **NO MOCKS EVER**: Test with real LLMs, real tools, real components +2. **Poetry Run Everything**: `poetry run python`, `poetry run pytest` β never run Python directly +3. **Explicit Imports**: `from haive.core.engine import X` not `from engine import X` +4. **Pydantic**: Never override `__init__`, use `model_post_init()` and Field() +5. **Tools in AugLLMConfig**: Pass tools via `AugLLMConfig(tools=[...])`, not `self.tools.append()` +6. **State Schema**: Use `LLMState` when agent has tools (includes engines dict for tool_node) +7. **System Messages**: Go in `AugLLMConfig(system_message=...)`, not ChatPromptTemplate +8. **Agent Composition**: Use MultiAgent, not complex inheritance +9. **Git Safety**: Always diff before commit, commit submodules first, never force push +10. **Async Postgres preferred**: Use PostgresStoreWrapper for production, not InMemoryStore +11. **Research First**: Check existing patterns before implementing β `grep -r "pattern" packages/` +12. **Keep It Simple**: Avoid over-engineering; one line compositions like `MultiAgent([A, B], mode="sequential")` + +## Coding Standards ### Python Code Style ```python -# β CORRECT - Descriptive names, type hints, early returns +# β CORRECT β descriptive names, type hints, early returns def process_agent_response( agent_response: str, - validation_config: ValidationConfig + validation_config: ValidationConfig, ) -> ProcessedResponse: - """Process agent response with validation. - - Args: - agent_response: Raw response from agent - validation_config: Configuration for validation rules - - Returns: - ProcessedResponse with validation results - - Raises: - ValidationError: If response fails validation - """ + """Process agent response with validation.""" if not agent_response: raise ValidationError("Empty response") - if not validation_config.enabled: return ProcessedResponse(content=agent_response, validated=False) + validated = validate_response(agent_response, validation_config) + return ProcessedResponse(content=validated, validated=True) - # Process with validation - validated_content = validate_response(agent_response, validation_config) - return ProcessedResponse( - content=validated_content, - validated=True, - validation_score=validated_content.score - ) - -# β WRONG - Poor naming, no types, nested logic +# β WRONG β poor naming, no types, nested logic def process(resp, config): if resp: if config: if config.enabled: return validate_response(resp, config) - else: - return resp - else: - return resp - else: - return None ``` -### Pydantic Model Patterns +### Pydantic Model Pattern ```python -# β CORRECT - Proper Pydantic usage -class AgentConfig(BaseModel): - """Configuration for agent behavior.""" +from pydantic import BaseModel, Field, ConfigDict, field_validator +class AgentConfig(BaseModel): model_config = ConfigDict( str_strip_whitespace=True, validate_assignment=True, - extra="forbid" + extra="forbid", ) name: str = Field(..., min_length=1, max_length=50) temperature: float = Field(default=0.7, ge=0.0, le=2.0) - max_tokens: Optional[int] = Field(default=None, ge=1) - tools: List[str] = Field(default_factory=list) + tools: list[str] = Field(default_factory=list) @field_validator("name") @classmethod def validate_name(cls, v: str) -> str: - """Validate agent name format.""" if not v.replace("_", "").isalnum(): raise ValueError("Name must be alphanumeric with underscores") return v -# Usage - Pydantic handles initialization automatically -config = AgentConfig(name="my_agent", temperature=0.8) -# Pydantic validates all fields and creates the instance +# β NEVER override __init__ β breaks Pydantic validation +# β NEVER use __init__ for setup β use model_post_init() instead +# β Use model_post_init(self, __context) for post-init setup ``` -### Error Handling Patterns +### Error Handling ```python -# β CORRECT - Structured error handling import logging -from typing import Optional - logger = logging.getLogger(__name__) -def execute_agent_safely( - agent: Agent, - input_data: str -) -> Optional[AgentResponse]: - """Execute agent with comprehensive error handling.""" +def execute_agent_safely(agent: Agent, input_data: str) -> AgentResponse | None: try: - logger.info(f"Executing agent {agent.name} with input length {len(input_data)}") - response = agent.run(input_data) - if not response: logger.warning(f"Agent {agent.name} returned empty response") return None - - logger.info(f"Agent {agent.name} completed successfully") return response - except ValidationError as e: - logger.error(f"Validation error in agent {agent.name}: {e}") - raise AgentValidationError(f"Agent validation failed: {e}") - + logger.error(f"Validation error in {agent.name}: {e}") + raise except Exception as e: - logger.error(f"Unexpected error in agent {agent.name}: {e}") - raise AgentExecutionError(f"Agent execution failed: {e}") + logger.error(f"Unexpected error in {agent.name}: {e}") + raise -# β WRONG - Silent failures, print statements -def bad_execute(agent, input_data): - try: - result = agent.run(input_data) - print(f"Got result: {result}") # Use logger! - return result - except: - print("Something went wrong") # No error context! - return None # Silent failure! +# β WRONG β silent failures, print statements +# Never use print() β use logger +# Never silently return None on error without logging ``` -### Testing Patterns (NO MOCKS) +### System vs Human Message Pattern ```python -# β CORRECT - Real component testing with descriptive names -def test_simple_agent_handles_basic_conversation_with_real_llm(): - """Test SimpleAgent maintains conversation context with real LLM.""" - config = AugLLMConfig(temperature=0.1) # Low for consistency - agent = SimpleAgent(name="test_conversation", engine=config) - - # First exchange - response1 = agent.run("My name is Alice") - assert isinstance(response1, str) - assert len(response1) > 0 - - # Second exchange - should remember context - response2 = agent.run("What's my name?") - assert "alice" in response2.lower() - - # Verify state persistence - assert len(agent.conversation_history) >= 4 # 2 user + 2 assistant - -def test_react_agent_with_real_calculator_tool_integration(): - """Test ReactAgent uses real calculator tool correctly.""" +# β System message in AugLLMConfig +engine = AugLLMConfig( + system_message="You are a helpful assistant.", # System message HERE + temperature=0.7, +) + +# β Human message via input or ChatPromptTemplate +agent.run("User question here") # Becomes HumanMessage automatically + +# β WRONG β mixing system and human in one template string +``` + +## Testing Standards β NO MOCKS + +```python +# β CORRECT β real components, real execution +def test_react_agent_with_calculator(): + """Test with REAL LLM and tools.""" @tool def calculator(expression: str) -> str: - """Real calculator tool.""" + """Calculate.""" return str(eval(expression)) - config = AugLLMConfig(temperature=0.1) agent = ReactAgent( - name="test_calculator", - engine=config, - tools=[calculator] + name="test_calc", + engine=AugLLMConfig(temperature=0.1, tools=[calculator]), + max_iterations=3, ) - result = agent.run("What is 15 * 23?") assert "345" in str(result) - assert agent.tool_calls_made > 0 -# β WRONG - Mocks, vague names, no real testing -def test_agent(): # Vague name! +# β WRONG β mocks, fake responses +def test_with_mocks(): mock_llm = Mock() # NO MOCKS! - mock_llm.return_value = "fake response" - agent = SimpleAgent(llm=mock_llm) - result = agent.run("test") - assert result == "fake response" # Tests nothing real! -``` - -## π¨ Common Pitfalls to Avoid - -1. **Running Python without poetry run** β ImportError -2. **Using mocks in tests** β False confidence -3. **Generic imports** β Use explicit haive.core.\* -4. **Overriding Pydantic **init\*\*\*\* β Breaks validation -5. **Using print() instead of logger** β Poor debugging -6. **git add .** β Stage files individually -7. **Building without testing** β Large broken changes -8. **Not asking for help** β Stuck for hours on solvable problems -9. **Skipping research phase** β Reinventing existing patterns -10. **Testing at the end** β Hard to debug failures -11. **Deleting test files** β Loss of valuable documentation + mock_llm.return_value = "fake" # Tests nothing real! -## π MCP Integration (Recommended) - -### Quick Setup for Common Tools +# Test file organization: +# packages/haive-{package}/tests/category/test_module.py +# NEVER delete test files β they serve as documentation +``` ```bash -# PostgreSQL - Database operations -claude mcp add haive-db -s user -- npx -y @modelcontextprotocol/server-postgres "postgresql://localhost/haive" - -# Filesystem - Enhanced file operations -claude mcp add haive-files -s user -- npx -y @modelcontextprotocol/server-filesystem /home/will/Projects/haive - -# GitHub - Repository management -claude mcp add haive-github -s user -e GITHUB_TOKEN=$GITHUB_TOKEN -- npx -y @modelcontextprotocol/server-github - -# List configured servers -claude mcp list +# Running tests +poetry run pytest packages/haive-agents/tests/ -v +poetry run pytest packages/haive-agents/tests/multi/ -v +poetry run pytest -k "test_react" -v ``` -See: @project_docs/claude_documentation/MCP_SETUP.md for complete setup guide with 8+ servers - -## π When to Ask for Help - -### Don't Stay Stuck - Ask Specific Questions +## Git Safety Protocol -```python -# β GOOD - Specific questions with context -"I'm implementing a ReactAgent with tools but getting ImportError on langchain_core.tools. -I've checked that langchain is installed with poetry show. What should I check next?" +```bash +# BEFORE ANY WORK +git status && git diff -"My agent test passes but the agent isn't actually using the tools I provided. -Here's my test code: [code]. What's the pattern for testing tool usage?" +# BEFORE COMMITTING +git diff --cached +git add specific_file.py # NEVER git add . or git add -A +git commit -m "feat(component): clear description" -"I'm following the MetaStateSchema pattern but getting a Pydantic validation error -when trying to embed my agent. The error is: [error]. How do I fix this?" +# SUBMODULE SAFETY β commit submodule first, then main repo +cd packages/haive-agents +git add ... && git commit -m "feat: ..." && git push origin final-refactor +cd ../.. +git add packages/haive-agents +git commit -m "chore: update haive-agents submodule" -# β BAD - Vague questions -"My code doesn't work" -"I'm getting an error" -"How do I make an agent?" +# RECOVERY +git reflog # Find lost commits +git branch recovery-branch HEAD@{n} ``` -### When to Ask vs When to Research +**Key Rules**: Never force push submodules. Always create backups before major changes. Always check status before work. Stage files individually, not with `.` or `-A`. -```python -# β RESEARCH FIRST - Common patterns -find packages/ -name "*.py" | xargs grep -l "similar_problem" -# Look at existing agent implementations -# Check test files for patterns +## File Organization -# β ASK FOR HELP - After research doesn't work -"I found 3 similar implementations [X, Y, Z] but none handle my specific case of [description]. -What's the best approach for [specific problem]?" +``` +# Test files β mirror source structure +packages/haive-agents/ +βββ src/haive/agents/memory/agent.py +βββ tests/memory/test_agent.py -# β ASK FOR HELP - Time-sensitive issues -"I'm getting a blocking error that's preventing all tests from running: [error]" +# Scripts +scripts/ +βββ maintenance/ # Fix scripts +βββ debug/ # Debug utilities +βββ docs/ # Doc build scripts -# β ASK FOR HELP - Architecture decisions -"Should I extend SimpleAgent or create a new agent type for [use case]?" +# Documentation +project_docs/ +βββ active/architecture/ # Architecture decisions +βββ guides/agent/ # Agent building guides +βββ sessions/ # Working memory ``` -### Debugging Workflow - ```bash -# 1. Check the basics -poetry run python -c "import haive.core; print('Core imports OK')" -poetry run python -c "import haive.agents; print('Agents imports OK')" -git status && git diff +# ALWAYS check if similar file exists before creating +find packages/ -name "*similar_pattern*" -type f -# 2. Run minimal test -poetry run python -c " -from haive.core.engine.aug_llm import AugLLMConfig -from haive.agents.simple.agent import SimpleAgent -config = AugLLMConfig() -agent = SimpleAgent(engine=config) -print('Basic agent creation works') -" - -# 3. If still stuck, ask for help with: -# - What you're trying to do -# - What you've tried -# - Exact error message -# - Minimal reproduction code +# Files that MUST stay in root: CLAUDE.md, README.md, pyproject.toml, docker-compose.yml ``` -## π Quick Debugging - -### Runtime Agent Debugging +## Docstring Standard (Google-style) ```python -# β ALWAYS use debug=True when developing/testing agents -agent = SimpleAgent(name="test_agent", engine=config) -result = agent.run("Hello", debug=True) # Shows detailed execution info - -# For ReactAgent with tools -agent = ReactAgent(name="debug_agent", engine=config, tools=[calculator]) -result = agent.run("Calculate 15 * 23", debug=True) # Shows tool calls, reasoning steps - -# For async agents -result = await agent.arun("Hello", debug=True) # Async version with debug info - -# β ALWAYS logically check outputs -print(f"Agent result: {result}") -print(f"Result type: {type(result)}") -print(f"Result length: {len(str(result))}") - -# Check if result makes sense -if "345" in str(result): - print("β Calculation appears correct") -else: - print("β Expected calculation result not found") - -# For structured outputs, check fields -if hasattr(result, 'content'): - print(f"Content: {result.content}") -if hasattr(result, 'metadata'): - print(f"Metadata: {result.metadata}") -``` +def my_function(param: str, config: Config | None = None) -> Result: + """Brief description of what this does. -### Environment Debugging + Args: + param: Description of parameter. + config: Optional configuration. -```bash -# Check imports work -poetry run python -c "from haive.core import *; from haive.agents import *" + Returns: + Result object with processed data. -# Verify environment -poetry env info -which python # Should show .venv path + Raises: + ValidationError: If response fails validation. -# Fix common issues -poetry install --all-extras -poetry cache clear pypi --all + Examples: + >>> result = my_function("test") + >>> print(result.data) + """ ``` -### Documentation Build Debugging +## Memory & Persistence Patterns -```bash -# Build docs (check for errors) -nox -s docs +### MemoryAgent β Persistent Memory + KG -# Quick build test -poetry run sphinx-build -b html docs/source docs/build/html -W --keep-going +```python +from haive.agents.memory import create_memory_agent -# Check for syntax errors in examples -find packages -name "*.py" -exec python -m py_compile {} \; 2>&1 | grep -E "Error|Sorry" +# Dev (InMemoryStore) +agent = create_memory_agent(name="assistant", user_id="user123") -# Find files with invalid names (spaces, parentheses) -find . -name "*\ *" -o -name "*(*" -o -name "*)*" +# Production (PostgreSQL β preferred) +agent = create_memory_agent(name="assistant", user_id="user123", + connection_string="postgresql://haive:haive@localhost/haive") -# View docs locally -python -m http.server 8003 --directory docs/build/html/ -# Then open http://localhost:8003 +# With Neo4j KG +agent = create_memory_agent(name="assistant", user_id="user123") +agent.connect_neo4j() # Uses NEO4J_URI/USER/PASSWORD env vars +agent.sync_kg_to_neo4j() # Sync existing triples to graph +triples = agent.query_kg("Alice") # Graph query ``` -π **Documentation Memories**: - -- @memory_index/by_task/documentation/ - All documentation-related memories -- @memory_index/by_error/build_errors/ - Build error solutions -- @memory_index/quick_reference.md - Common patterns and fixes - -## π§ͺ Testing: NO MOCKS + Proper Structure - -### π¨ IMPORTANT: Keep Test Files As Documentation +### Store Namespaces -**DO NOT DELETE TEST FILES** - They serve as living documentation of: - -- How the system should behave -- Real usage patterns and examples -- Edge cases and error handling -- Integration between components - -Test files are valuable references for understanding the codebase! - -### Test File Organization +```python +# Memory agent uses 3 namespaces per user: +("user", user_id) # Memories (facts, preferences) +("kg", user_id) # Knowledge graph triples (subject-predicate-object) +("summary", user_id) # Conversation summaries (auto-generated) +# Store API (positional args β NOT keyword): +store.put(namespace_tuple, key, value_dict) +store.search(namespace_tuple, query=query, limit=5) ``` -packages/haive-{package}/ -βββ src/haive/{package}/ -β βββ my_module.py # Your source code -βββ tests/ - βββ test_my_module.py # Test for that module -# ALWAYS: Test files go in packages/haive-*/tests/ -# NEVER: Create test files in root or random locations -# ALWAYS: Keep test files after creation - they're documentation! +### Store Configuration -# For nested modules, mirror the source structure: -packages/haive-agents/ -βββ src/haive/agents/ -β βββ reasoning_and_critique/ -β βββ self_discover/ -β βββ agent.py -βββ tests/ - βββ reasoning_and_critique/ - βββ self_discover/ - βββ test_agent.py # Mirror the directory structure -``` +```python +# Always prefer async Postgres for production +from haive.core.persistence.store.factory import StoreFactory +from haive.core.persistence.store.types import StoreConfig, StoreType -### Running Tests +config = StoreConfig( + type=StoreType.POSTGRES_SYNC, # or POSTGRES_ASYNC + connection_params={"connection_string": "postgresql://haive:haive@localhost/haive"}, + setup_on_init=True, +) +store = StoreFactory.create(config) -```bash -# Run all tests in a package -poetry run pytest packages/haive-agents/tests/ -v +# InMemoryStore for dev ONLY +from langgraph.store.memory import InMemoryStore +store = InMemoryStore() +``` -# Run specific test subdirectory -poetry run pytest packages/haive-agents/tests/multi/ -v -poetry run pytest packages/haive-agents/tests/rag/ -v +### KG Extraction & Neo4j -# Run single test file -poetry run pytest packages/haive-agents/tests/multi/test_simple_multi_agent.py -v +```python +# MemoryAgent auto-extracts KG triples from conversations (post-hook) +# Triples stored in both LangGraph store AND Neo4j (if connected) -# Run specific test function -poetry run pytest packages/haive-agents/tests/multi/test_simple_multi_agent.py::test_sequential_execution -v +# Manual document-level KG extraction: +triples = agent.extract_kg_from_document("Alice works at DeepMind on RL.") -# Run with coverage -poetry run pytest packages/haive-agents/tests/ --cov=haive.agents --cov-report=html +# Raw Cypher queries: +results = agent.query_kg_cypher( + "MATCH (s:Entity)-[r:RELATES_TO]->(o:Entity) RETURN s.name, r.predicate, o.name" +) -# Run tests matching pattern -poetry run pytest -k "test_react" -v +# Neo4j schema: (:Entity {name, type, user_id}) -[:RELATES_TO {predicate}]-> (:Entity) +# See: @project_docs/guides/agent/MEMORY_AGENT_GUIDE.md for full schema ``` -## π File Organization Standards - -### Project File Structure +## Package Structure ``` -haive/ -βββ packages/ # All package code -β βββ haive-core/ -β β βββ src/ # Source code -β β βββ tests/ # Test files organized by module -β β βββ graph/ # Graph-related tests -β β βββ memory/ # Memory system tests -β β βββ schema/ # Schema tests -β β βββ persistence/ # Persistence tests -β βββ haive-agents/ -β β βββ src/ -β β βββ tests/ -β β βββ multi/ # Multi-agent tests -β β βββ rag/ # RAG agent tests -β β βββ planning/ # Planning agent tests -β β βββ research/ # Research agent tests -β β βββ reasoning/ # Reasoning agent tests -β βββ ... -βββ scripts/ # Utility scripts -β βββ maintenance/ # Maintenance and fix scripts -β β βββ docs/ # Documentation build scripts -β β βββ agents/ # Agent enhancement scripts -β βββ debug/ # Debug utilities -βββ project_docs/ # Documentation -β βββ active/ # Current standards -β βββ summaries/ # Implementation summaries -β βββ guides/ # User guides -β βββ build-reports/ # Build and test reports -β βββ issues/ # Issue tracking -β βββ plans/ # Architecture plans -βββ examples/ # Example scripts -βββ docs/ # Sphinx documentation - -# Files that MUST stay in root: -- CLAUDE.md # This file - central memory -- README.md # Project readme -- pyproject.toml # Poetry configuration -- noxfile.py # Nox automation -- .gitignore # Git ignore rules -``` - -### Creating New Files - -```bash -# ALWAYS check if similar file exists first -find packages/ -name "*similar_pattern*" -type f - -# Create test file in correct location -# For agent tests: -touch packages/haive-agents/tests/category/test_new_feature.py - -# For core tests: -touch packages/haive-core/tests/module/test_new_component.py +packages/ # Each is its own Git repo (submodule) +βββ haive-core/ # Foundation: engine, graph, schema, persistence, store +βββ haive-agents/ # Agent implementations: simple, react, multi, memory, rag +βββ haive-tools/ # Tool implementations +βββ haive-games/ # Game environments +βββ haive-mcp/ # MCP integration +βββ haive-prebuilt/ # Pre-configured agents +βββ haive-dataflow/ # Data processing -# For scripts: -touch scripts/maintenance/category/new_script.py +project_docs/ # Documentation (main repo) +βββ active/architecture/ # Architecture decisions +βββ guides/agent/ # Agent building guides (NEW) +βββ guides/tools/ # Tool guides +βββ sessions/ # Working memory -# For documentation: -touch project_docs/category/new_doc.md +demos/ # Demo scripts β EVERY working agent should have one here +βββ agents/ # Agent demos (01-49 + memory_agent_e2e.py) +βββ games/ # Game demos ``` -### Moving Files to Proper Locations +### Submodule Workflow ```bash -# If you accidentally create a test in root: -mv test_something.py packages/haive-agents/tests/appropriate_category/ - -# If you create a debug script in root: -mv fix_something.py scripts/debug/ - -# If you create documentation in root: -mv SOMETHING_SUMMARY.md project_docs/summaries/ +# Work in submodule +cd packages/haive-agents +git add ... && git commit -m "feat: ..." && git push origin final-refactor +cd ../.. +git add packages/haive-agents && git commit -m "chore: update submodule" ``` -## π Documentation Standards - -### Required Patterns +### Import Hierarchy (no circular deps) -- **Google-style docstrings** on all public functions/classes -- **Type hints** on all parameters and returns -- **Examples** in docstrings for complex functions -- **@ references** for memory documents: `@project_docs/path/to/doc.md` - -### Docstring Template - -```python -def my_function(param: str, config: Optional[Config] = None) -> Result: - """Brief description of what this does. - - Args: - param: Description of parameter. - config: Optional configuration. - - Returns: - Result object with processed data. - - Examples: - >>> result = my_function("test") - >>> print(result.data) - """ +``` +Core β standard library, third-party +Agents β core, standard library, third-party +Tools β core, standard library, third-party +Games β core, agents, tools, third-party ``` -### Package README Structure - -````markdown -# Package Name - -Brief description. - -## Installation - -`poetry add package-name` - -## Quick Start +## State Schema Quick Reference -```python -# Basic usage example ``` -```` +StateSchema β engines: dict[str, Engine] (base) +βββ MessagesState β messages: list[BaseMessage] +β βββ ToolState β tools, tool_routes, tool_metadata +β βββ LLMState β full engine mgmt β DEFAULT for agents with tools +β β βββ ReactAgentState β iteration, tool_results +β βββ MultiAgentState β agents, agent_states, agent_outputs +``` -## Features +**Rule**: If agent has tools, state_schema MUST be LLMState or subclass. +The base Agent auto-selects LLMState when `engine.tools` is non-empty. -- Key feature list +## Docker (Postgres + Neo4j) -## Documentation +```bash +docker-compose up -d +# Postgres: postgresql://haive:haive@localhost:5432/haive (pgvector) +# Neo4j: bolt://localhost:7687 (neo4j/haivepass, APOC plugin) +# Neo4j UI: http://localhost:7474 +``` -- Links to guides and API docs +## Common Commands -``` +```bash +poetry run python script.py +poetry run pytest packages/haive-agents/tests/ -v +poetry run python -c "from haive.core import *; print('OK')" + +# Run memory agent e2e +poetry run python demos/agents/memory_agent_e2e.py +poetry run python demos/agents/memory_agent_e2e.py --neo4j +``` + +## Agent Status (2026-04-06) + +### Working (30+) + +| Agent | Type | Notes | +|-------|------|-------| +| SimpleAgent | Foundation | 1660 lines, BaseGraph, v3 | +| ReactAgent | Foundation | 984 lines, extends SimpleAgent | +| MultiAgent | Foundation | 920 lines, sequential/parallel/conditional | +| DynamicSupervisor | Foundation | 220 lines, dynamic agent mgmt | +| Supervisor | Foundation | 577 lines, multi-supervisor | +| MemoryAgent | Memory | KG extraction, Neo4j, auto-summarize | +| LongTermMemory | Memory | 78 lines, ReactAgent + vector store | +| Conversation (6) | Conversation | Base, Collaborative, Debate, Directed, RoundRobin, SocialMedia | +| LLM Compiler | Planning | 300 lines, DAG plan + join + replan | +| ReWOO | Planning | 254 lines, plan all β execute β solve | +| Plan & Execute | Planning | Planner β Executor(tools) β Replanner | +| Reflexion | Reasoning | 271 lines, draft β reflect β revise loop | +| LATS | Reasoning | 265 lines, tree search + backprop | +| Reflection | Reasoning | 473 lines | +| Logic | Reasoning | 140 lines | +| RAG (24 variants) | RAG | Base, Adaptive, Agentic, Dynamic, FLARE, Fusion, HyDE, QueryPlan, SelfReflective, SelfRoute, Speculative, StepBack, + more | +| DocumentLoader (4) | Loader | Base, File, Directory, Web | +| TaskAnalysis | Analysis | Proper Agent inheritance | +| StructuredOutput | Output | Extends SimpleAgent | + +### Needs Fix (import path: `haive.core.engine.agent.agent` β `haive.agents.base.agent`) + +| Agent | Issue | +|-------|-------| +| Filtered RAG | Wrong import path | +| Self-Corrective RAG | Wrong import, no inheritance | +| DB RAG (Graph + SQL) | Wrong import path (2 files) | +| Summarizer (map + iterative) | Wrong import path | +| KG Extraction (iterative + map) | Wrong import path | +| Person Research | Wrong import, no inheritance | +| Open Perplexity | Wrong import, no inheritance | +| Discovery Agent | Circular import | +| ~~Plan & Execute v2~~ | ~~FIXED β now working~~ | + +### Legacy (older pattern, functional as-is) + +Complex Extraction, TNT, Tree of Thoughts, Self-Discover, Storm β use older `haive.core.engine.agent` or DynamicGraph directly. Not broken, just pre-refactor pattern. + +### Next Steps +1. **Neo4j e2e** β docker-compose up, test full memory KG pipeline with Cypher +2. **MemoryStoreManager** β integrate 11 memory types, importance scoring, decay +3. **GraphDBRAG as tool** β NLβCypher queries on memory KG +4. **Discovery Agent** β circular import fix +5. **Demos** β every working agent should have a demo in `demos/agents/` +6. **Postgres default** β prefer async Postgres over InMemoryStore +7. **Legacy imports** β leave as-is if working, don't touch + +## Recent Work (2026-04-06) + +### MemoryAgent Phase 2 Complete +- 4 memory tools: save_memory, search_memory, save_knowledge, search_knowledge +- Auto-context pre-hook: loads memories + KG triples + summaries +- KG extraction post-hook: LLM extracts triples from conversations +- Auto-summarize post-hook: summarizes on token threshold +- Neo4j integration: connect_neo4j() β sync + Cypher queries +- Docker: PostgreSQL (pgvector) + Neo4j (APOC) +- See: @project_docs/guides/agent/MEMORY_AGENT_GUIDE.md + +### Base Agent Fixes +- `_setup_schemas()` defaults to LLMState when tools present +- `execution_mixin` injects engines into invoke_input +- `tool_node_config_v2` passes messages directly (not state.dict()) +- MultiAgent wrapper passes engines to child agents +- See: @project_docs/active/architecture/state_schema_engine_gap.md + +### Agent Trace Utility +- `haive.agents.utils.trace.run_traced(agent, input)` β Rich pretty-print +- Saves traces to JSON for debugging --- -**Remember**: This file loads at every session. Keep frequently-used info here, import the rest! -``` +**Keep this file lean. Detailed guides are in `project_docs/guides/agent/`.** diff --git a/demos/agents/07_supervisor.py b/demos/agents/07_supervisor.py index 1b441587..47958ae5 100644 --- a/demos/agents/07_supervisor.py +++ b/demos/agents/07_supervisor.py @@ -1,6 +1,7 @@ -"""Demo 07: SupervisorAgent. +"""Demo 07: DynamicSupervisor. -Tests: Basic supervisor routing tasks to sub-agents +Tests: Supervisor that routes tasks to sub-agents via tool calls, + with dynamic agent addition and on-the-fly agent creation. """ import asyncio @@ -12,33 +13,58 @@ from haive.core.engine.aug_llm import AugLLMConfig from haive.agents.simple.agent import SimpleAgent +from haive.agents.react.agent import ReactAgent +from haive.agents.dynamic_supervisor.agent import DynamicSupervisor +from langchain_core.tools import tool + + +@tool +def calculator(expression: str) -> str: + """Calculate a mathematical expression.""" + try: + return str(eval(expression, {"__builtins__": {}})) + except Exception as e: + return f"Error: {e}" async def main(): print("=" * 60) - print("Demo 07: Supervisor Agent") + print("Demo 07: DynamicSupervisor") print("=" * 60) - # Try to import supervisor agent (class is DynamicSupervisor) - try: - from haive.agents.supervisor.agent import DynamicSupervisor - print("[OK] DynamicSupervisor imported") - except ImportError as e: - print(f"[BROKEN] DynamicSupervisor import failed: {e}") - return + # Create supervisor + supervisor = DynamicSupervisor( + name="team_lead", + engine=AugLLMConfig(temperature=0.3), + ) + print(f"[OK] Created: {type(supervisor).__name__}") - try: - supervisor = DynamicSupervisor( - name="demo_supervisor", - engine=AugLLMConfig( - temperature=0.3, - system_message="You route tasks to appropriate team members.", - ), - ) - print(f"[OK] Instantiated: {supervisor.name}") - except Exception as e: - print(f"[BROKEN] Instantiation failed: {e}") - return + # Add agents dynamically + math_agent = ReactAgent( + name="math_expert", + engine=AugLLMConfig(temperature=0.1, system_message="You solve math problems. Use the calculator tool."), + tools=[calculator], + ) + supervisor.add_agent(math_agent, "Solves math problems using calculator") + print(f"[OK] Added math_expert") + + writer_agent = SimpleAgent( + name="writer", + engine=AugLLMConfig(temperature=0.8, system_message="You write short creative content."), + ) + supervisor.add_agent(writer_agent, "Writes creative content") + print(f"[OK] Added writer") + + # Create agent on the fly + supervisor.create_agent( + name="translator", + system_message="You translate text to French.", + description="Translates text to French", + ) + print(f"[OK] Created translator on-the-fly") + + print(f"[OK] Agents: {list(supervisor.agents.keys())}") + print(f"[OK] Tools: {[t.name for t in supervisor.tools]}") print("\n[OK] DynamicSupervisor demo complete") diff --git a/demos/agents/10_plan_and_execute.py b/demos/agents/10_plan_and_execute.py index cfaa9bfe..7d08e711 100644 --- a/demos/agents/10_plan_and_execute.py +++ b/demos/agents/10_plan_and_execute.py @@ -1,37 +1,53 @@ """Demo 10: PlanAndExecuteAgent. -Tests: Plan and Execute agent pattern (V2 - newer pattern) +Tests: Plan and Execute agent (Planner β Executor β Replanner) """ -import asyncio import sys sys.path.insert(0, ".") from dotenv import load_dotenv load_dotenv() +from langchain_core.tools import tool +from haive.agents.planning.plan_and_execute import PlanAndExecuteAgent -async def main(): + +@tool +def calculator(expression: str) -> str: + """Calculate a mathematical expression.""" + try: + return str(eval(expression, {"__builtins__": {}})) + except Exception as e: + return f"Error: {e}" + + +def main(): print("=" * 60) print("Demo 10: PlanAndExecuteAgent") print("=" * 60) - try: - from haive.agents.planning.plan_and_execute.simple import PlanAndExecuteAgent - print("[OK] PlanAndExecuteAgent (simple/v2) imported") - except ImportError as e: - print(f"[BROKEN] Import failed: {e}") - return - - try: - agent = PlanAndExecuteAgent.create(name="demo_plan_execute") - print(f"[OK] Instantiated via create()") - except Exception as e: - print(f"[BROKEN] Instantiation failed: {e}") - return + agent = PlanAndExecuteAgent.create( + tools=[calculator], + name="demo_p_and_e", + ) + print(f"[OK] Created: {agent.name}") + print(f" Agents: {[a.name for a in agent.agents]}") + + result = agent.run("What is 25 * 4, then add 50 to that result?") + print(f"[OK] Result type: {type(result).__name__}") + + if hasattr(result, "messages") and result.messages: + for msg in result.messages: + if hasattr(msg, "content") and msg.content: + tc = getattr(msg, "tool_calls", None) + if tc: + print(f" [{type(msg).__name__}]: tools={[c['name'] for c in tc]}") + else: + print(f" [{type(msg).__name__}]: {msg.content[:200]}") print("\n[OK] PlanAndExecuteAgent demo complete") if __name__ == "__main__": - asyncio.run(main()) + main() diff --git a/demos/agents/23_memory.py b/demos/agents/23_memory.py index 3b905d6d..105dab9a 100644 --- a/demos/agents/23_memory.py +++ b/demos/agents/23_memory.py @@ -1,6 +1,6 @@ """Demo 23: Memory Agents. -Tests: Memory system agents (long_term_memory, ltm, memory, memory_reorganized) +Tests: Memory system agents (long_term_memory, memory with search) """ import asyncio @@ -19,12 +19,11 @@ async def main(): memory_modules = [ ("long_term_memory", "haive.agents.long_term_memory.agent"), - ("ltm", "haive.agents.ltm.agent"), ("memory", "haive.agents.memory.agent"), ("memory/quick_search", "haive.agents.memory.search.quick_search.agent"), ("memory/pro_search", "haive.agents.memory.search.pro_search.agent"), ("memory/deep_research", "haive.agents.memory.search.deep_research.agent"), - ("memory_reorg/base", "haive.agents.memory_reorganized.base.agent"), + ("memory/kg_generator", "haive.agents.memory.kg_generator_agent"), ] ok = broken = 0 diff --git a/demos/agents/46_chain_agent.py b/demos/agents/46_chain_agent.py new file mode 100644 index 00000000..26bdc786 --- /dev/null +++ b/demos/agents/46_chain_agent.py @@ -0,0 +1,28 @@ +"""Demo 46: Chain Agent. + +Tests: Declarative chain agent with sequential LLM steps +""" + +import asyncio +import sys +sys.path.insert(0, ".") + +from dotenv import load_dotenv +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 46: Chain Agent") + print("=" * 60) + + from haive.agents.chain import DeclarativeChainAgent, ChainBuilder + + print(f"[OK] DeclarativeChainAgent: {DeclarativeChainAgent.__name__}") + print(f"[OK] ChainBuilder: {ChainBuilder.__name__}") + + print("\n[OK] Chain agent demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/agents/47_task_analysis.py b/demos/agents/47_task_analysis.py new file mode 100644 index 00000000..167b743d --- /dev/null +++ b/demos/agents/47_task_analysis.py @@ -0,0 +1,31 @@ +"""Demo 47: Task Analysis Agent. + +Tests: TaskAnalysisAgent - decomposes complex tasks into subtasks +""" + +import asyncio +import sys +sys.path.insert(0, ".") + +from dotenv import load_dotenv +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 47: Task Analysis Agent") + print("=" * 60) + + from haive.agents.task_analysis.agent import TaskAnalysisAgent + from haive.core.engine.aug_llm import AugLLMConfig + + agent = TaskAnalysisAgent(name="task_analyzer", engine=AugLLMConfig()) + print(f"[OK] TaskAnalysisAgent created") + + g = agent.compile() + print(f"[OK] Compiled: {type(g).__name__}") + print("\n[OK] Task analysis demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/agents/48_discovery.py b/demos/agents/48_discovery.py new file mode 100644 index 00000000..6cfe139a --- /dev/null +++ b/demos/agents/48_discovery.py @@ -0,0 +1,26 @@ +"""Demo 48: Discovery Agent. + +Tests: Discovery agent for exploring and cataloging agent capabilities +""" + +import asyncio +import sys +sys.path.insert(0, ".") + +from dotenv import load_dotenv +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 48: Discovery Agent") + print("=" * 60) + + import haive.agents.discovery as disc + classes = [c for c in dir(disc) if not c.startswith("_")] + print(f"[OK] Discovery module: {classes[:5]}") + print("\n[OK] Discovery demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/agents/49_rewoo.py b/demos/agents/49_rewoo.py new file mode 100644 index 00000000..cc382f5c --- /dev/null +++ b/demos/agents/49_rewoo.py @@ -0,0 +1,34 @@ +"""Demo 49: ReWOO Agent. + +Tests: ReWOO - Reasoning WithOut Observation +Plans all steps upfront, executes in parallel, synthesizes answer. +Only 2 LLM calls total (plan + solve). +""" + +import asyncio +import sys +sys.path.insert(0, ".") + +from dotenv import load_dotenv +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 49: ReWOO - Reasoning WithOut Observation") + print("=" * 60) + + from haive.agents.planning.rewoo.agent import ReWOOAgent + + agent = ReWOOAgent(name="rewoo_demo", tools=[]) + print(f"[OK] Created: {type(agent).__name__}") + + g = agent.compile() + print(f"[OK] Compiled: {type(g).__name__}") + print(f"[OK] Nodes: {list(g.get_graph().nodes.keys())}") + + print("\n[OK] ReWOO demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/agents/50_research_agent.py b/demos/agents/50_research_agent.py new file mode 100644 index 00000000..49213cee --- /dev/null +++ b/demos/agents/50_research_agent.py @@ -0,0 +1,43 @@ +"""Demo 50: Perplexity-style Research Agent. + +Tests: QueryAnalyzer β Researcher (search + RAG) β Synthesizer +""" + +import sys +sys.path.insert(0, ".") + +from dotenv import load_dotenv +load_dotenv() + +from haive.agents.research.perplexity_agent import create_research_agent + + +def main(): + print("=" * 60) + print("Demo 50: Perplexity Research Agent") + print("=" * 60) + + agent = create_research_agent(name="demo_research", max_search_iterations=5) + print(f"[OK] Created: {agent.name}") + print(f" Sub-agents: {[a.name for a in agent.agents]}") + tools = [t.name for t in agent.agents[1].engine.tools if hasattr(t, "name")] + # Dedupe for display + print(f" Researcher tools: {list(dict.fromkeys(tools))}") + + print("\n--- Running research ---") + result = agent.run("What are the main approaches to AI alignment and safety?") + + if hasattr(result, "messages") and result.messages: + for msg in result.messages: + if hasattr(msg, "content") and msg.content: + tc = getattr(msg, "tool_calls", None) + if tc: + print(f" [{type(msg).__name__}]: tools={[c['name'] for c in tc]}") + elif len(msg.content) > 0: + print(f" [{type(msg).__name__}]: {msg.content[:300]}") + + print("\n[OK] Research agent demo complete") + + +if __name__ == "__main__": + main() diff --git a/demos/agents/51_deep_research.py b/demos/agents/51_deep_research.py new file mode 100644 index 00000000..384e8a90 --- /dev/null +++ b/demos/agents/51_deep_research.py @@ -0,0 +1,42 @@ +"""Demo 51: Deep Research Agent. + +5-stage pipeline: Planner β Researcher (search+RAG) β Analyzer (RAG) β FactChecker β Writer +""" + +import sys +sys.path.insert(0, ".") + +from dotenv import load_dotenv +load_dotenv() + +from haive.agents.research.deep_research_agent import create_deep_research_agent + + +def main(): + print("=" * 60) + print("Demo 51: Deep Research Agent") + print("=" * 60) + + agent = create_deep_research_agent( + name="demo_deep", + max_search_iterations=5, + include_fact_check=True, + ) + print(f"[OK] Created: {agent.name}") + print(f" Pipeline: {[a.name for a in agent.agents]}") + + print("\n--- Running deep research ---") + result = agent.run("Compare React and Vue.js frameworks for web development in 2025") + + if hasattr(result, "messages") and result.messages: + # Show only AI responses (skip tool calls and tool messages for clean output) + ai_msgs = [m for m in result.messages if type(m).__name__ == "AIMessage" + and hasattr(m, "content") and m.content and not getattr(m, "tool_calls", None)] + for msg in ai_msgs[-2:]: # Show last 2 AI messages (fact check + report) + print(f"\n [{type(msg).__name__}]: {msg.content[:500]}") + + print("\n[OK] Deep research demo complete") + + +if __name__ == "__main__": + main() diff --git a/demos/agents/memory_agent_e2e.py b/demos/agents/memory_agent_e2e.py new file mode 100644 index 00000000..cedbf2b3 --- /dev/null +++ b/demos/agents/memory_agent_e2e.py @@ -0,0 +1,204 @@ +"""E2E Demo: MemoryAgent with KG extraction, Cypher queries, and Neo4j. + +Tests the full memory pipeline: +1. MemoryAgent saves memories via LLM tool calls +2. KG triples auto-extracted from conversations +3. Memories recalled in subsequent turns +4. KG queried (store-based or Neo4j if available) +5. Auto-summarization on context length threshold + +Usage: + # Store-only mode (always works) + poetry run python demos/agents/memory_agent_e2e.py + + # With Neo4j (requires docker-compose up) + NEO4J_URI=bolt://localhost:7687 poetry run python demos/agents/memory_agent_e2e.py --neo4j +""" + +import argparse +import logging +import sys + +sys.path.insert(0, ".") + +from dotenv import load_dotenv +load_dotenv() + + +def test_store_based_memory(): + """Test memory agent with store-only (no Neo4j).""" + from haive.agents.memory import create_memory_agent + + print("=" * 60) + print("MemoryAgent E2E Test (Store-based)") + print("=" * 60) + + agent = create_memory_agent( + name="mem_e2e", + user_id="test_user", + auto_extract_kg=True, + summarize_threshold=2000, + ) + print(f"[OK] Agent created: {agent.name}") + print(f" Store: {type(agent.get_store()).__name__}") + print(f" Tools: {[t.name for t in agent.engine.tools if hasattr(t, 'name')]}") + + # Turn 1: Share info β LLM should save memories + KG extracted + print("\n--- Turn 1: Share info ---") + r1 = agent.run("My name is Alice. I'm a data scientist at DeepMind working on reinforcement learning.") + _print_messages(r1) + + # Turn 2: More info + print("\n--- Turn 2: More info ---") + r2 = agent.run("I use PyTorch and JAX for my research. I prefer functional programming.") + _print_messages(r2) + + # Turn 3: Recall + print("\n--- Turn 3: Recall ---") + r3 = agent.run("What do you know about me, my work, and my tools?") + _print_messages(r3) + + # Check store contents + print("\n--- Store Contents ---") + store = agent.get_store() + _print_store(store, "test_user") + + # Query KG (store-based fallback) + print("\n--- KG Query (store-based) ---") + triples = agent.query_kg("Alice") + for t in triples: + print(f" {t['subject']} --{t['predicate']}--> {t['object']}") + if not triples: + print(" (no triples found via store search)") + + print("\n[OK] Store-based e2e test complete!") + return agent + + +def test_neo4j_integration(agent): + """Test Neo4j KG integration (requires Neo4j running).""" + print("\n" + "=" * 60) + print("MemoryAgent E2E Test (Neo4j KG)") + print("=" * 60) + + try: + # Connect to Neo4j + kg = agent.connect_neo4j() + print(f"[OK] Neo4j connected") + + # Sync existing KG triples to Neo4j + synced = agent.sync_kg_to_neo4j() + print(f"[OK] Synced {synced} triples to Neo4j") + + # Query entity via Neo4j + print("\n--- Neo4j: Entity query (Alice) ---") + triples = agent.query_kg("Alice") + for t in triples: + print(f" {t['subject']} --{t['predicate']}--> {t['object']}") + + # Neighborhood query + print("\n--- Neo4j: Neighborhood ---") + neighbors = kg.query_neighborhood("Alice") + for n in neighbors: + print(f" {n['center']} -{n['rel1']}-> {n['neighbor']}", end="") + if n.get("hop2_entity"): + print(f" -{n['rel2']}-> {n['hop2_entity']}", end="") + print() + + # Raw Cypher query + print("\n--- Neo4j: Cypher query ---") + cypher = "MATCH (e:Entity)-[r:RELATES_TO]->(t:Entity) RETURN e.name, r.predicate, t.name LIMIT 10" + results = agent.query_kg_cypher(cypher) + for r in results: + print(f" {r}") + + # Stats + stats = kg.get_stats() + print(f"\n--- Neo4j Stats ---") + print(f" Entities: {stats['entities']}") + print(f" Relationships: {stats['relationships']}") + + # Add a turn with Neo4j connected (should auto-sync) + print("\n--- Turn 4: With Neo4j sync ---") + r4 = agent.run("Alice also collaborates with Bob on a paper about multi-agent systems.") + _print_messages(r4) + + # Verify new triples in Neo4j + print("\n--- Neo4j: Updated triples ---") + triples = kg.query_entity("Alice") + for t in triples: + print(f" {t['subject']} --{t['predicate']}--> {t['object']}") + + kg.close() + print("\n[OK] Neo4j e2e test complete!") + + except ImportError as e: + print(f"[SKIP] Neo4j not available: {e}") + print(" Install with: pip install neo4j") + except Exception as e: + print(f"[SKIP] Neo4j connection failed: {e}") + print(" Start with: docker-compose up -d neo4j") + + +def _print_messages(result): + """Print messages from a result.""" + messages = result.messages if hasattr(result, "messages") else [] + for msg in messages: + if hasattr(msg, "content") and msg.content: + tc = getattr(msg, "tool_calls", None) + if tc: + print(f" [{type(msg).__name__}]: tool_calls={[c['name'] for c in tc]}") + else: + print(f" [{type(msg).__name__}]: {msg.content[:200]}") + + +def _print_store(store, user_id): + """Print store contents.""" + # Memories + user_items = store.search(("user", user_id), limit=20) + print(f" Memories ({len(user_items)}):") + for item in user_items: + v = item.value if hasattr(item, "value") else item + print(f" - {v.get('content', str(v))[:80]}") + + # KG triples + kg_items = store.search(("kg", user_id), limit=20) + kg_triples = [i for i in kg_items if (i.value if hasattr(i, "value") else i).get("type") == "kg_triple"] + print(f" KG Triples ({len(kg_triples)}):") + for item in kg_triples: + v = item.value if hasattr(item, "value") else item + print(f" - {v['subject']} {v['predicate']} {v['object']}") + + # Summaries + sum_items = store.search(("summary", user_id), limit=5) + print(f" Summaries ({len(sum_items)}):") + for item in sum_items: + v = item.value if hasattr(item, "value") else item + print(f" - {v.get('content', str(v))[:80]}") + + +def main(): + parser = argparse.ArgumentParser(description="MemoryAgent E2E Demo") + parser.add_argument("--neo4j", action="store_true", help="Test Neo4j integration") + parser.add_argument("--verbose", action="store_true", help="Enable debug logging") + args = parser.parse_args() + + if args.verbose: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.WARNING) + + # Always test store-based + agent = test_store_based_memory() + + # Optionally test Neo4j + if args.neo4j: + test_neo4j_integration(agent) + + print("\n" + "=" * 60) + print("All tests complete!") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/demos/games/28_among_us.py b/demos/games/28_among_us.py new file mode 100644 index 00000000..4c77e2ef --- /dev/null +++ b/demos/games/28_among_us.py @@ -0,0 +1,47 @@ +"""Demo 28: Among Us. + +Tests: AmongUsAgent - multi-player social deduction game +""" + +import asyncio +import sys + +sys.path.insert(0, ".") + +from dotenv import load_dotenv + +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 28: Among Us - Social Deduction Game") + print("=" * 60) + + from haive.games.among_us.factory import create_among_us_game + + players = ["Alice", "Bob", "Charlie", "Dave", "Eve"] + agent = create_among_us_game(player_names=players) + print(f"[OK] Agent created: {type(agent).__name__}") + print(f"[OK] App compiled: {type(agent.app).__name__}") + + # Initialize game + state = agent.state_manager.initialize(player_names=players) + roles = {p: s.role.value for p, s in state.player_states.items()} + print(f"[OK] Game initialized: {len(players)} players") + print(f" Roles: {roles}") + print(f" Map: {state.map_name}") + + impostor = [p for p, r in roles.items() if r == "impostor"] + print(f" Impostor(s): {impostor}") + + # Show task assignments + for p, s in state.player_states.items(): + tasks = [t.description for t in s.tasks[:2]] + print(f" {p}: {tasks}") + + print("\n[OK] Among Us demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/29_strategy_games.py b/demos/games/29_strategy_games.py new file mode 100644 index 00000000..270a92bd --- /dev/null +++ b/demos/games/29_strategy_games.py @@ -0,0 +1,108 @@ +"""Demo 29: Strategy Games. + +Tests: TicTacToe, Connect4, Checkers, Nim, Battleship, Reversi, Mancala, Mastermind +""" + +import asyncio +import sys + +sys.path.insert(0, ".") + +from dotenv import load_dotenv + +load_dotenv() + + +GAMES = [ + ( + "TicTacToe", + "haive.games.tic_tac_toe.agent", + "TicTacToeAgent", + "haive.games.tic_tac_toe.config", + "TicTacToeConfig", + ), + ( + "Connect4", + "haive.games.connect4.agent", + "Connect4Agent", + "haive.games.connect4.config", + "Connect4AgentConfig", + ), + ( + "Checkers", + "haive.games.checkers.agent", + "CheckersAgent", + "haive.games.checkers.config", + "CheckersAgentConfig", + ), + ("Nim", "haive.games.nim.agent", "NimAgent", "haive.games.nim.config", "NimConfig"), + ( + "Battleship", + "haive.games.battleship.agent", + "BattleshipAgent", + "haive.games.battleship.config", + "BattleshipAgentConfig", + ), + ( + "Reversi", + "haive.games.reversi.agent", + "ReversiAgent", + "haive.games.reversi.config", + "ReversiConfig", + ), + ( + "Mancala", + "haive.games.mancala.agent", + "MancalaAgent", + "haive.games.mancala.config", + "MancalaConfig", + ), + ( + "Mastermind", + "haive.games.mastermind.agent", + "MastermindAgent", + "haive.games.mastermind.config", + "MastermindConfig", + ), + ( + "Dominoes", + "haive.games.dominoes.agent", + "DominoesAgent", + "haive.games.dominoes.config", + "DominoesAgentConfig", + ), + ( + "Fox & Geese", + "haive.games.fox_and_geese.agent", + "FoxAndGeeseAgent", + "haive.games.fox_and_geese.config", + "FoxAndGeeseConfig", + ), +] + + +async def main(): + print("=" * 60) + print("Demo 29: Strategy Games") + print("=" * 60) + + import importlib + + for name, agent_mod, agent_cls, config_mod, config_cls in GAMES: + try: + a_mod = importlib.import_module(agent_mod) + c_mod = importlib.import_module(config_mod) + AgentClass = getattr(a_mod, agent_cls) + ConfigClass = getattr(c_mod, config_cls) + + config = ConfigClass(enable_analysis=False, visualize=False) + agent = AgentClass(config) + print(f"[OK] {name}: {agent_cls} created, app={agent.app is not None}") + except Exception as e: + print(f"[FAIL] {name}: {type(e).__name__}: {str(e)[:80]}") + + print(f"\n[OK] Strategy games demo complete ({len(GAMES)} games)") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/30_social_deduction.py b/demos/games/30_social_deduction.py new file mode 100644 index 00000000..2ad08759 --- /dev/null +++ b/demos/games/30_social_deduction.py @@ -0,0 +1,75 @@ +"""Demo 30: Social Deduction & Multi-Player Games. + +Tests: Mafia, Debate, Clue +""" + +import asyncio +import sys + +sys.path.insert(0, ".") + +from dotenv import load_dotenv + +load_dotenv() + + +GAMES = [ + ( + "Mafia", + "haive.games.mafia.agent", + "MafiaAgent", + "haive.games.mafia.config", + "MafiaAgentConfig", + ), + ( + "Debate", + "haive.games.debate.agent", + "DebateAgent", + "haive.games.debate.config", + "DebateAgentConfig", + ), + ( + "Clue", + "haive.games.clue.agent", + "ClueAgent", + "haive.games.clue.config", + "ClueConfig", + ), + ( + "Hold'em", + "haive.games.hold_em.game_agent", + "HoldemGameAgent", + "haive.games.hold_em.config", + "HoldemGameAgentConfig", + ), +] + + +async def main(): + print("=" * 60) + print("Demo 30: Social Deduction & Multi-Player Games") + print("=" * 60) + + import importlib + + for name, agent_mod, agent_cls, config_mod, config_cls in GAMES: + try: + a_mod = importlib.import_module(agent_mod) + c_mod = importlib.import_module(config_mod) + AgentClass = getattr(a_mod, agent_cls) + ConfigClass = getattr(c_mod, config_cls) + + try: + config = ConfigClass(enable_analysis=False, visualize=False) + except Exception: + config = ConfigClass() + agent = AgentClass(config) + print(f"[OK] {name}: {agent_cls} created, app={agent.app is not None}") + except Exception as e: + print(f"[FAIL] {name}: {type(e).__name__}: {str(e)[:80]}") + + print(f"\n[OK] Social deduction demo complete ({len(GAMES)} games)") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/31_tic_tac_toe.py b/demos/games/31_tic_tac_toe.py new file mode 100644 index 00000000..5314a67e --- /dev/null +++ b/demos/games/31_tic_tac_toe.py @@ -0,0 +1,35 @@ +"""Demo 31: Tic-Tac-Toe. + +Tests: TicTacToeAgent - classic 3x3 grid game +""" + +import asyncio +import sys + +sys.path.insert(0, ".") +from dotenv import load_dotenv + +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 31: Tic-Tac-Toe") + print("=" * 60) + + from haive.games.tic_tac_toe.agent import TicTacToeAgent + from haive.games.tic_tac_toe.config import TicTacToeConfig + + config = TicTacToeConfig(enable_analysis=False, visualize=False) + agent = TicTacToeAgent(config) + print(f"[OK] Agent: {type(agent).__name__}") + print(f"[OK] App: {type(agent.app).__name__}") + + state = agent.state_manager.initialize() + print(f"[OK] Board: {state.board}") + print(f"[OK] Status: {state.game_status}") + print("[OK] Tic-Tac-Toe demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/32_connect4.py b/demos/games/32_connect4.py new file mode 100644 index 00000000..311ecb8b --- /dev/null +++ b/demos/games/32_connect4.py @@ -0,0 +1,35 @@ +"""Demo 32: Connect 4. + +Tests: Connect4Agent - four-in-a-row strategy game +""" + +import asyncio +import sys + +sys.path.insert(0, ".") +from dotenv import load_dotenv + +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 32: Connect 4") + print("=" * 60) + + from haive.games.connect4.agent import Connect4Agent + from haive.games.connect4.config import Connect4AgentConfig + + config = Connect4AgentConfig(enable_analysis=False, visualize=False) + agent = Connect4Agent(config) + print(f"[OK] Agent: {type(agent).__name__}") + print(f"[OK] App: {type(agent.app).__name__}") + + state = agent.state_manager.initialize() + print(f"[OK] Board size: {len(state.board)}x{len(state.board[0])}") + print(f"[OK] Status: {state.game_status}") + print("[OK] Connect 4 demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/33_checkers.py b/demos/games/33_checkers.py new file mode 100644 index 00000000..57ffc9e3 --- /dev/null +++ b/demos/games/33_checkers.py @@ -0,0 +1,31 @@ +"""Demo 33: Checkers. + +Tests: CheckersAgent - classic draughts game +""" + +import asyncio +import sys + +sys.path.insert(0, ".") +from dotenv import load_dotenv + +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 33: Checkers") + print("=" * 60) + + from haive.games.checkers.agent import CheckersAgent + from haive.games.checkers.config import CheckersAgentConfig + + config = CheckersAgentConfig(enable_analysis=False, visualize=False) + agent = CheckersAgent(config) + print(f"[OK] Agent: {type(agent).__name__}") + print(f"[OK] App: {type(agent.app).__name__}") + print("[OK] Checkers demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/34_nim.py b/demos/games/34_nim.py new file mode 100644 index 00000000..01c3de8a --- /dev/null +++ b/demos/games/34_nim.py @@ -0,0 +1,29 @@ +"""Demo 34: Nim. + +Tests: NimAgent - mathematical strategy game +""" + +import asyncio +import sys + +sys.path.insert(0, ".") +from dotenv import load_dotenv + +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 34: Nim") + print("=" * 60) + from haive.games.nim.agent import NimAgent + from haive.games.nim.config import NimConfig + + config = NimConfig(enable_analysis=False, visualize=False) + agent = NimAgent(config) + print(f"[OK] Agent: {type(agent).__name__}, App: {type(agent.app).__name__}") + print("[OK] Nim demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/35_battleship.py b/demos/games/35_battleship.py new file mode 100644 index 00000000..a70323cb --- /dev/null +++ b/demos/games/35_battleship.py @@ -0,0 +1,29 @@ +"""Demo 35: Battleship. + +Tests: BattleshipAgent - naval strategy game +""" + +import asyncio +import sys + +sys.path.insert(0, ".") +from dotenv import load_dotenv + +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 35: Battleship") + print("=" * 60) + from haive.games.battleship.agent import BattleshipAgent + from haive.games.battleship.config import BattleshipAgentConfig + + config = BattleshipAgentConfig(enable_analysis=False, visualize=False) + agent = BattleshipAgent(config) + print(f"[OK] Agent: {type(agent).__name__}, App: {type(agent.app).__name__}") + print("[OK] Battleship demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/36_reversi.py b/demos/games/36_reversi.py new file mode 100644 index 00000000..087b6b52 --- /dev/null +++ b/demos/games/36_reversi.py @@ -0,0 +1,29 @@ +"""Demo 36: Reversi. + +Tests: ReversiAgent - disc-flipping strategy game (Othello) +""" + +import asyncio +import sys + +sys.path.insert(0, ".") +from dotenv import load_dotenv + +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 36: Reversi") + print("=" * 60) + from haive.games.reversi.agent import ReversiAgent + from haive.games.reversi.config import ReversiConfig + + config = ReversiConfig(enable_analysis=False, visualize=False) + agent = ReversiAgent(config) + print(f"[OK] Agent: {type(agent).__name__}, App: {type(agent.app).__name__}") + print("[OK] Reversi demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/37_mancala.py b/demos/games/37_mancala.py new file mode 100644 index 00000000..a3b814fc --- /dev/null +++ b/demos/games/37_mancala.py @@ -0,0 +1,29 @@ +"""Demo 37: Mancala. + +Tests: MancalaAgent - seed-sowing board game +""" + +import asyncio +import sys + +sys.path.insert(0, ".") +from dotenv import load_dotenv + +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 37: Mancala") + print("=" * 60) + from haive.games.mancala.agent import MancalaAgent + from haive.games.mancala.config import MancalaConfig + + config = MancalaConfig(enable_analysis=False, visualize=False) + agent = MancalaAgent(config) + print(f"[OK] Agent: {type(agent).__name__}, App: {type(agent.app).__name__}") + print("[OK] Mancala demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/38_mastermind.py b/demos/games/38_mastermind.py new file mode 100644 index 00000000..9bc96775 --- /dev/null +++ b/demos/games/38_mastermind.py @@ -0,0 +1,29 @@ +"""Demo 38: Mastermind. + +Tests: MastermindAgent - code-breaking logic game +""" + +import asyncio +import sys + +sys.path.insert(0, ".") +from dotenv import load_dotenv + +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 38: Mastermind") + print("=" * 60) + from haive.games.mastermind.agent import MastermindAgent + from haive.games.mastermind.config import MastermindConfig + + config = MastermindConfig(enable_analysis=False, visualize=False) + agent = MastermindAgent(config) + print(f"[OK] Agent: {type(agent).__name__}, App: {type(agent.app).__name__}") + print("[OK] Mastermind demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/39_dominoes.py b/demos/games/39_dominoes.py new file mode 100644 index 00000000..6ed0aefd --- /dev/null +++ b/demos/games/39_dominoes.py @@ -0,0 +1,29 @@ +"""Demo 39: Dominoes. + +Tests: DominoesAgent - tile-matching game +""" + +import asyncio +import sys + +sys.path.insert(0, ".") +from dotenv import load_dotenv + +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 39: Dominoes") + print("=" * 60) + from haive.games.dominoes.agent import DominoesAgent + from haive.games.dominoes.config import DominoesAgentConfig + + config = DominoesAgentConfig(enable_analysis=False, visualize=False) + agent = DominoesAgent(config) + print(f"[OK] Agent: {type(agent).__name__}, App: {type(agent.app).__name__}") + print("[OK] Dominoes demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/40_fox_and_geese.py b/demos/games/40_fox_and_geese.py new file mode 100644 index 00000000..5d539666 --- /dev/null +++ b/demos/games/40_fox_and_geese.py @@ -0,0 +1,29 @@ +"""Demo 40: Fox and Geese. + +Tests: FoxAndGeeseAgent - asymmetric hunt game +""" + +import asyncio +import sys + +sys.path.insert(0, ".") +from dotenv import load_dotenv + +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 40: Fox and Geese") + print("=" * 60) + from haive.games.fox_and_geese.agent import FoxAndGeeseAgent + from haive.games.fox_and_geese.config import FoxAndGeeseConfig + + config = FoxAndGeeseConfig(enable_analysis=False, visualize=False) + agent = FoxAndGeeseAgent(config) + print(f"[OK] Agent: {type(agent).__name__}, App: {type(agent.app).__name__}") + print("[OK] Fox and Geese demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/41_mafia.py b/demos/games/41_mafia.py new file mode 100644 index 00000000..1925e5d1 --- /dev/null +++ b/demos/games/41_mafia.py @@ -0,0 +1,32 @@ +"""Demo 41: Mafia. + +Tests: MafiaAgent - social deduction game +""" + +import asyncio +import sys + +sys.path.insert(0, ".") +from dotenv import load_dotenv + +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 41: Mafia") + print("=" * 60) + from haive.games.mafia.agent import MafiaAgent + from haive.games.mafia.config import MafiaAgentConfig + + try: + config = MafiaAgentConfig(enable_analysis=False, visualize=False) + except Exception: + config = MafiaAgentConfig() + agent = MafiaAgent(config) + print(f"[OK] Agent: {type(agent).__name__}, App: {type(agent.app).__name__}") + print("[OK] Mafia demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/42_debate.py b/demos/games/42_debate.py new file mode 100644 index 00000000..41e97211 --- /dev/null +++ b/demos/games/42_debate.py @@ -0,0 +1,32 @@ +"""Demo 42: Debate. + +Tests: DebateAgent - multi-agent structured debate +""" + +import asyncio +import sys + +sys.path.insert(0, ".") +from dotenv import load_dotenv + +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 42: Debate") + print("=" * 60) + from haive.games.debate.agent import DebateAgent + from haive.games.debate.config import DebateAgentConfig + + try: + config = DebateAgentConfig(enable_analysis=False, visualize=False) + except Exception: + config = DebateAgentConfig() + agent = DebateAgent(config) + print(f"[OK] Agent: {type(agent).__name__}, App: {type(agent.app).__name__}") + print("[OK] Debate demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/43_clue.py b/demos/games/43_clue.py new file mode 100644 index 00000000..87f3a82d --- /dev/null +++ b/demos/games/43_clue.py @@ -0,0 +1,32 @@ +"""Demo 43: Clue. + +Tests: ClueAgent - mystery deduction game (Cluedo) +""" + +import asyncio +import sys + +sys.path.insert(0, ".") +from dotenv import load_dotenv + +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 43: Clue") + print("=" * 60) + from haive.games.clue.agent import ClueAgent + from haive.games.clue.config import ClueConfig + + try: + config = ClueConfig(enable_analysis=False, visualize=False) + except Exception: + config = ClueConfig() + agent = ClueAgent(config) + print(f"[OK] Agent: {type(agent).__name__}, App: {type(agent.app).__name__}") + print("[OK] Clue demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/44_holdem.py b/demos/games/44_holdem.py new file mode 100644 index 00000000..0e0e8bf1 --- /dev/null +++ b/demos/games/44_holdem.py @@ -0,0 +1,32 @@ +"""Demo 44: Texas Hold'em. + +Tests: HoldemGameAgent - poker variant +""" + +import asyncio +import sys + +sys.path.insert(0, ".") +from dotenv import load_dotenv + +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 44: Texas Hold'em") + print("=" * 60) + from haive.games.hold_em.config import HoldemGameAgentConfig + from haive.games.hold_em.game_agent import HoldemGameAgent + + try: + config = HoldemGameAgentConfig(enable_analysis=False, visualize=False) + except Exception: + config = HoldemGameAgentConfig() + agent = HoldemGameAgent(config) + print(f"[OK] Agent: {type(agent).__name__}, App: {type(agent.app).__name__}") + print("[OK] Hold'em demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/demos/games/45_flow_free.py b/demos/games/45_flow_free.py new file mode 100644 index 00000000..a70cce33 --- /dev/null +++ b/demos/games/45_flow_free.py @@ -0,0 +1,32 @@ +"""Demo 45: Flow Free. + +Tests: FlowFreeAgent - single-player path puzzle +""" + +import asyncio +import sys + +sys.path.insert(0, ".") +from dotenv import load_dotenv + +load_dotenv() + + +async def main(): + print("=" * 60) + print("Demo 45: Flow Free") + print("=" * 60) + from haive.games.single_player.flow_free.agent import FlowFreeAgent + from haive.games.single_player.flow_free.config import FlowFreeConfig + + try: + config = FlowFreeConfig(enable_analysis=False, visualize=False) + except Exception: + config = FlowFreeConfig() + agent = FlowFreeAgent(config) + print(f"[OK] Agent: {type(agent).__name__}, App: {type(agent.app).__name__}") + print("[OK] Flow Free demo complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..b8caa7b8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +version: "3.9" + +services: + # PostgreSQL for store persistence (memories, KG triples, summaries) + postgres: + image: pgvector/pgvector:pg16 + container_name: haive-postgres + environment: + POSTGRES_USER: haive + POSTGRES_PASSWORD: haive + POSTGRES_DB: haive + ports: + - "5432:5432" + volumes: + - haive-pg-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U haive"] + interval: 5s + timeout: 5s + retries: 5 + + # Neo4j for knowledge graph storage (APOC + GDS plugins) + neo4j: + image: neo4j:5-community + container_name: haive-neo4j + environment: + NEO4J_AUTH: neo4j/haivepass + NEO4J_PLUGINS: '["apoc", "graph-data-science"]' + NEO4J_dbms_security_procedures_unrestricted: apoc.*,gds.* + NEO4J_dbms_security_procedures_allowlist: apoc.*,gds.* + ports: + - "7474:7474" # HTTP browser + - "7687:7687" # Bolt protocol + volumes: + - haive-neo4j-data:/data + healthcheck: + test: ["CMD-SHELL", "cypher-shell -u neo4j -p haivepass 'RETURN 1'"] + interval: 10s + timeout: 10s + retries: 5 + +volumes: + haive-pg-data: + haive-neo4j-data: diff --git a/langgraph.json b/langgraph.json new file mode 100644 index 00000000..e12a22a0 --- /dev/null +++ b/langgraph.json @@ -0,0 +1,59 @@ +{ + "dependencies": [ + "./packages/haive-core", + "./packages/haive-agents", + "./packages/haive-games" + ], + "graphs": { + "simple_agent": "haive.agents.graphs:simple_agent", + "react_agent": "haive.agents.graphs:react_agent", + "multi_agent_sequential": "haive.agents.graphs:multi_agent_sequential", + "supervisor": "haive.agents.graphs:supervisor", + "plan_and_execute": "haive.agents.graphs:plan_and_execute", + "llm_compiler": "haive.agents.graphs:llm_compiler", + "rag_agent": "haive.agents.graphs:rag_agent", + "simple_rag": "haive.agents.graphs:simple_rag", + "dynamic_rag": "haive.agents.graphs:dynamic_rag", + "llm_rag": "haive.agents.graphs:llm_rag", + "step_back_rag": "haive.agents.graphs:step_back_rag", + "flare_rag": "haive.agents.graphs:flare_rag", + "multi_query_rag": "haive.agents.graphs:multi_query_rag", + "fusion_rag": "haive.agents.graphs:fusion_rag", + "speculative_rag": "haive.agents.graphs:speculative_rag", + "document_grading_rag": "haive.agents.graphs:document_grading_rag", + "self_route_rag": "haive.agents.graphs:self_route_rag", + "reflection_agent": "haive.agents.graphs:reflection_agent", + "reasoning_system": "haive.agents.graphs:reasoning_system", + "self_discover_selector": "haive.agents.graphs:self_discover_selector", + "self_discover_adapter": "haive.agents.graphs:self_discover_adapter", + "self_discover_structurer": "haive.agents.graphs:self_discover_structurer", + "self_discover_executor": "haive.agents.graphs:self_discover_executor", + "collaborative_conversation": "haive.agents.graphs:collaborative_conversation", + "debate_conversation": "haive.agents.graphs:debate_conversation", + "memory_agent": "haive.agents.graphs:memory_agent", + "task_analysis": "haive.agents.graphs:task_analysis", + "rewoo": "haive.agents.graphs:rewoo", + "reflexion": "haive.agents.graphs:reflexion", + "round_robin_conversation": "haive.agents.graphs:round_robin_conversation", + "lats": "haive.agents.graphs:lats", + "tic_tac_toe": "haive.games.graphs:tic_tac_toe", + "chess": "haive.games.graphs:chess", + "connect4": "haive.games.graphs:connect4", + "checkers": "haive.games.graphs:checkers", + "nim": "haive.games.graphs:nim", + "battleship": "haive.games.graphs:battleship", + "go": "haive.games.graphs:go", + "reversi": "haive.games.graphs:reversi", + "mancala": "haive.games.graphs:mancala", + "mastermind": "haive.games.graphs:mastermind", + "dominoes": "haive.games.graphs:dominoes", + "fox_and_geese": "haive.games.graphs:fox_and_geese", + "among_us": "haive.games.graphs:among_us", + "debate": "haive.games.graphs:debate", + "mafia": "haive.games.graphs:mafia", + "clue": "haive.games.graphs:clue", + "holdem": "haive.games.graphs:holdem", + "flow_free": "haive.games.graphs:flow_free" + }, + "env": ".env" +} diff --git a/packages/haive-agents b/packages/haive-agents index 0a1d436c..b1eb8909 160000 --- a/packages/haive-agents +++ b/packages/haive-agents @@ -1 +1 @@ -Subproject commit 0a1d436c575b73f62070307f374146b497258ce3 +Subproject commit b1eb890940633c0d407dea841d2056973987af5c diff --git a/packages/haive-core b/packages/haive-core index eabbe3b6..976c4c76 160000 --- a/packages/haive-core +++ b/packages/haive-core @@ -1 +1 @@ -Subproject commit eabbe3b6291bd06ab77b0cb2ca75f128473b6cc2 +Subproject commit 976c4c76d8294c4d0ee3b961deb3957e425b939d diff --git a/packages/haive-games b/packages/haive-games index 5177915e..2e6fc2a3 160000 --- a/packages/haive-games +++ b/packages/haive-games @@ -1 +1 @@ -Subproject commit 5177915e0d3ce1e368a50ab5629adeb001aeff2d +Subproject commit 2e6fc2a3ce899b86579ba0c521e367079256adb6 diff --git a/project_docs/active/architecture/state_schema_engine_gap.md b/project_docs/active/architecture/state_schema_engine_gap.md new file mode 100644 index 00000000..b26dfa3b --- /dev/null +++ b/project_docs/active/architecture/state_schema_engine_gap.md @@ -0,0 +1,132 @@ +# State Schema Engine Gap - Architecture Analysis + +**Created**: 2026-04-06 +**Status**: Partially fixed (runtime workaround), needs architectural fix +**Impact**: All agents with tools β tool_node can't find tools at runtime + +## Problem Summary + +Auto-composed agent state schemas (created by SchemaComposer) do NOT include the `engines` dict field. This means `tool_node` can't find tools at runtime because `state.engines[engine_name]` is always empty. + +## Root Cause Chain + +1. **Agent._setup_schemas()** creates a SchemaComposer and calls `add_fields_from_engine()` for each engine +2. `add_fields_from_engine()` extracts input/output schema fields but **never adds engine management fields** (engines, tools, tool_routes) +3. `SchemaComposer.add_engine_management()` exists (~line 1822) but is **only called** when the base class is StateSchema +4. Auto-composed schemas are built from scratch without a StateSchema base, so the condition never triggers +5. Result: compiled state has NO `engines` field β tool_node gets empty dict β "No tools available" + +## Schema Hierarchy + +All pre-built schemas properly inherit `engines` from StateSchema: + +``` +StateSchema (has engines: dict[str, Engine]) +βββ MessagesState +β βββ ToolState (+ ToolRouteMixin) +β β βββ LLMState (full engine mgmt) +β β β βββ ReactAgentState +β β βββ MultiAgentState (explicit engines + agent hierarchy) +β βββ MemoryAgentState (via ReactAgentState) +βββ Auto-composed schemas β β MISSING engines field +``` + +## Fixes Applied (Runtime Workarounds) + +### Fix 1: Inject engines into invoke_input +**File**: `packages/haive-agents/src/haive/agents/base/mixins/execution_mixin.py` + +```python +# Before _app.invoke(), inject engines so tool_node can find them +if isinstance(invoke_input, dict) and hasattr(self, "engines") and self.engines: + invoke_input.setdefault("engines", self.engines) +``` + +### Fix 2: Fix _prepare_input for empty schemas +**File**: `packages/haive-agents/src/haive/agents/base/mixins/execution_mixin.py` + +When `input_schema` has no `model_fields`, fall back to `{"messages": [HumanMessage(...)]}` instead of returning empty dict. + +### Fix 3: Fix _execute_tools serialization +**File**: `packages/haive-core/src/haive/core/graph/node/tool_node_config_v2.py` + +```python +# Before (broken): state.dict() serializes BaseMessage to plain dicts +state_dict = state if isinstance(state, dict) else state.dict() + +# After (fixed): pass only messages to preserve BaseMessage objects +state_dict = {self.messages_field: messages} +``` + +## Architectural Fix Needed + +### Option A: SchemaComposer always adds engine management +In `SchemaComposer.build()`, always call `add_engine_management()` when engines are present: + +```python +def build(self): + # ... existing build logic ... + if self.engines: + self.add_engine_management() # Always, not just for StateSchema subclasses +``` + +### Option B: Agent._setup_schemas() explicitly adds engines +```python +def _setup_schemas(self): + composer = SchemaComposer(name=f"{self.__class__.__name__}State") + for engine in engine_list: + composer.add_engine(engine) + composer.add_fields_from_engine(engine) + composer.add_engine_management() # Explicitly add engine fields + self.state_schema = composer.build() +``` + +### Option C: Use pre-built LLMState as default +Instead of auto-composing, default to `LLMState` which already has everything: +```python +if not self.state_schema: + self.state_schema = LLMState # Has engines, tools, messages +``` + +## MultiAgent State Patterns + +- **MultiAgentState** uses pre-built state (has engines via ToolState) +- Child agents execute via `_create_agent_wrapper()` β only passes **messages**, NOT engines +- Engine syncing happens in `setup_agent_hierarchy()` validator with namespacing: `engines["agent_name.main"]` +- Tools are **NOT dynamically transferred** between agents β each agent pre-compiles its own + +## Key Files + +| File | What | Lines | +|------|------|-------| +| `haive-agents/base/agent.py` | _setup_schemas() | 313-397 | +| `haive-agents/base/mixins/execution_mixin.py` | invoke with engines | 575-588 | +| `haive-core/schema/schema_composer.py` | add_engine_management | ~1822 | +| `haive-core/schema/state_schema.py` | engines field | 211 | +| `haive-core/graph/node/tool_node_config_v2.py` | _get_tools, _execute_tools | 177-268 | +| `haive-agents/multi/agent.py` | MultiAgent state | 313-382 | +| `haive-core/schema/prebuilt/multi_agent_state.py` | MultiAgentState | full file | + +## Testing + +```python +# Verify engines are in state at runtime +from haive.agents.react.agent import ReactAgent +from haive.core.engine.aug_llm import AugLLMConfig +from langchain_core.tools import tool + +@tool +def calculator(expression: str) -> str: + '''Calculate.''' + return str(eval(expression)) + +agent = ReactAgent( + name="test", + engine=AugLLMConfig(tools=[calculator]), + max_iterations=3, +) + +# This should work after fixes +result = agent.run("What is 15 * 23?") +# Expected: AIMessage with "345" in response +``` diff --git a/project_docs/guides/agent/AGENT_DESIGN_PATTERNS.md b/project_docs/guides/agent/AGENT_DESIGN_PATTERNS.md new file mode 100644 index 00000000..aece4bbb --- /dev/null +++ b/project_docs/guides/agent/AGENT_DESIGN_PATTERNS.md @@ -0,0 +1,304 @@ +# Agent Design Patterns β How to Build Haive Agents + +**Created**: 2026-04-06 +**Purpose**: Comprehensive guide for designing agents around BaseGraph, state schemas, and agent composition + +## Core Architecture + +``` +Agent (Pydantic BaseModel) +βββ engine: AugLLMConfig # LLM + tools + routing +βββ engines: dict[str, Engine] # Named engine registry +βββ state_schema: type # Pydantic model for graph state +βββ graph: BaseGraph # Graph builder (β LangGraph StateGraph) +βββ _app: CompiledStateGraph # Compiled LangGraph (from compile()) +βββ Methods: + βββ build_graph() β BaseGraph # Abstract: define nodes + edges + βββ compile() β CompiledGraph # BaseGraph.to_langgraph() + compile + βββ run(input) β result # _prepare_input β _app.invoke β _process_output +``` + +## How Agents Work (The Flow) + +``` +1. __init__() β Pydantic validates fields +2. model_post_init() β _setup_schemas() generates state_schema +3. build_graph() β Creates BaseGraph with nodes + edges +4. compile() β graph.to_langgraph() β StateGraph β .compile() β _app +5. run(input) β _prepare_input β _app.invoke(input) β _process_output +``` + +### Key: State Schema Determines Everything + +The `state_schema` defines what data flows through the graph. Each node receives +the full state and returns a partial update dict. + +```python +# State = what flows through the graph +class MyState(LLMState): + messages: list[BaseMessage] # Inherited: conversation history + engines: dict[str, Engine] # Inherited: engine registry for tool_node + custom_field: str = "" # Your custom data + +# Node = receives state, returns partial update +def my_node(state: MyState) -> dict: + return {"custom_field": "updated"} +``` + +**Rule: If your agent has tools, use LLMState (or a subclass) as state_schema.** +LLMState includes `engines`, `tools`, `tool_routes` β required by tool_node. + +## Building an Agent: Step by Step + +### 1. SimpleAgent (no tools, just LLM) + +```python +from haive.agents.simple.agent import SimpleAgent +from haive.core.engine.aug_llm import AugLLMConfig + +agent = SimpleAgent( + name="writer", + engine=AugLLMConfig( + temperature=0.8, + system_message="You are a creative writer.", + ), +) +result = agent.run("Write a haiku about AI") +``` + +Graph: `START β agent_node β END` + +### 2. ReactAgent (LLM + tools, reasoning loop) + +```python +from haive.agents.react.agent import ReactAgent +from langchain_core.tools import tool + +@tool +def search(query: str) -> str: + '''Search the web.''' + return f"Results for: {query}" + +agent = ReactAgent( + name="researcher", + engine=AugLLMConfig(tools=[search], system_message="Use search tool."), + max_iterations=5, +) +result = agent.run("Find info about quantum computing") +``` + +Graph: `START β agent_node β [tool_calls?] β tool_node β agent_node β ... β END` + +### 3. MemoryAgent (ReactAgent + persistent memory) + +```python +from haive.agents.memory import create_memory_agent + +agent = create_memory_agent( + name="assistant", + user_id="user123", + connection_string="postgresql://haive:haive@localhost/haive", +) +``` + +### 4. MultiAgent (compose multiple agents) + +```python +from haive.agents.multi.agent import MultiAgent + +multi = MultiAgent( + name="pipeline", + agents=[researcher, writer, reviewer], + execution_mode="sequential", # or "parallel", "conditional" +) +result = multi.run("Create a report on AI safety") +``` + +## BaseGraph: The Graph Builder + +BaseGraph wraps LangGraph's StateGraph with a higher-level API. + +```python +from haive.core.graph.state_graph.base_graph2 import BaseGraph +from langgraph.graph import START, END + +graph = BaseGraph(name="my_graph") +graph.set_state_schema(MyState) + +# Add nodes (NodeConfig objects or callables) +graph.add_node("step1", my_node_function) +graph.add_node("step2", another_node) + +# Add edges +graph.add_edge(START, "step1") +graph.add_edge("step1", "step2") +graph.add_edge("step2", END) + +# Conditional routing +graph.add_conditional_edges("step1", routing_fn, {"a": "step2", "b": END}) + +# Compile +lg = graph.to_langgraph() # β LangGraph StateGraph +app = lg.compile() # β CompiledStateGraph +``` + +### BaseGraph vs Raw StateGraph + +| Feature | BaseGraph | Raw StateGraph | +|---------|-----------|----------------| +| Node configs | GenericEngineNodeConfig, ToolNodeConfig | Plain callables | +| State schema | set_state_schema() | StateGraph(schema) | +| Engine integration | Automatic via node configs | Manual | +| Tool routing | ValidationNode + ToolNode | Build yourself | +| Compilation | to_langgraph() β compile() | compile() | + +**Use BaseGraph** for agents (has engine/tool integration). +**Use raw StateGraph** only for simple workflows without LLM engines. + +## State Schema Design + +### For Agents with Tools β Use LLMState + +```python +from haive.core.schema.prebuilt.llm_state import LLMState + +class MyAgentState(LLMState): + """Extends LLMState with custom fields.""" + plan: str = "" + iteration: int = 0 +``` + +LLMState inherits: messages, engines, tools, tool_routes, token_usage, etc. + +### For Tool-less Agents β MessagesState is fine + +```python +from haive.core.schema.prebuilt.messages_state import MessagesState + +class SimpleState(MessagesState): + custom_data: str = "" +``` + +### For MultiAgent β MultiAgentState + +```python +from haive.core.schema.prebuilt.multi_agent_state import MultiAgentState + +# Already has: agents dict, agent_states, agent_outputs, execution tracking +``` + +### State Schema Rules + +1. **All fields need defaults** β state is initialized empty, nodes return partial updates +2. **messages uses Annotated reducer** β appends instead of replaces +3. **engines must be in state** for tool_node to work (LLMState includes this) +4. **Custom fields** are simple Pydantic fields with defaults + +## MultiAgent Patterns + +### Sequential: A β B β C + +```python +multi = MultiAgent( + name="pipeline", + agents=[planner, executor, reviewer], + execution_mode="sequential", +) +``` + +Each agent sees messages from all prior agents. + +### Parallel: [A, B, C] β merge + +```python +multi = MultiAgent( + name="research", + agents=[analyst1, analyst2, analyst3], + execution_mode="parallel", +) +``` + +All agents run concurrently, results merged. + +### Dynamic (DynamicSupervisor) + +```python +from haive.agents.supervisor.dynamic.dynamic_supervisor import DynamicSupervisor + +supervisor = DynamicSupervisor( + name="coordinator", + engine=AugLLMConfig(system_message="Route tasks to agents."), +) +supervisor.add_agent(math_agent) +supervisor.add_agent(writer_agent) +# Supervisor decides which agent to call based on input +``` + +### How State Flows in MultiAgent + +``` +MultiAgentState +βββ messages: list[BaseMessage] # Shared across all agents +βββ agents: dict[str, Agent] # Agent instances +βββ agent_states: dict[str, dict] # Per-agent state snapshots +βββ agent_outputs: dict[str, Any] # Per-agent outputs +βββ execution_tracking: dict # Execution metadata + +_create_agent_wrapper(): + 1. Extracts messages from MultiAgentState + 2. Injects agent's engines dict + 3. Invokes agent._app.invoke({"messages": [...], "engines": {...}}) + 4. Returns updated messages + agent_states + agent_outputs +``` + +## Custom Agent Pattern + +```python +class MyCustomAgent(ReactAgent): + """Custom agent with specific behavior.""" + + # Custom config fields + my_param: str = Field(default="value") + + # Custom state + # (override state_schema in build_graph or via set_schema=True) + + def build_graph(self) -> BaseGraph: + # Start with parent's graph + graph = super().build_graph() + + # Add custom nodes + graph.add_node("my_custom_step", self._custom_node) + graph.add_edge("agent_node", "my_custom_step") + graph.add_edge("my_custom_step", END) + + return graph + + def _custom_node(self, state): + # Your custom logic + return {"messages": [...]} + + def run(self, input_data, **kwargs): + # Custom pre/post processing + result = super().run(input_data, **kwargs) + # Post-processing + return result +``` + +## Anti-Patterns to Avoid + +1. **Don't override `__init__`** β use `model_post_init()` or Pydantic Fields +2. **Don't use raw StateGraph** when you need tools β use BaseGraph +3. **Don't auto-compose schemas** when tools are present β use LLMState +4. **Don't pass tools via `self.tools`** β pass via `AugLLMConfig(tools=[...])` +5. **Don't call `state.dict()`** in nodes β it serializes BaseMessage to dicts +6. **Don't forget engines in state** β tool_node needs `state.engines[name].tools` + +## Debug & Trace + +```python +from haive.agents.utils.trace import run_traced + +# Pretty-print any agent execution +result = run_traced(agent, "Hello", save_to="traces/") +``` diff --git a/project_docs/guides/agent/AGENT_STATUS_VERIFIED.md b/project_docs/guides/agent/AGENT_STATUS_VERIFIED.md new file mode 100644 index 00000000..d2b45086 --- /dev/null +++ b/project_docs/guides/agent/AGENT_STATUS_VERIFIED.md @@ -0,0 +1,147 @@ +# Agent Status -- Verified 2026-04-06 + +All 55 agents verified for import. Core agents verified for execution with real LLM calls. + +## Foundation Agents + +| Agent | Import | Execution | Notes | +|-------|--------|-----------|-------| +| SimpleAgent | OK | OK | Foundation, conversation + structured output | +| ReactAgent | OK | OK | Tools, reasoning loops | +| MultiAgent | OK | OK | Sequential, parallel, dynamic add/remove/create | +| MemoryAgent | OK | OK | Conversation memory persistence | + +## Supervisor Agents + +| Agent | Import | Execution | Notes | +|-------|--------|-----------|-------| +| DynamicSupervisor | OK | OK | `dynamic_supervisor.agent.DynamicSupervisor` | +| SupervisorAgent | OK | OK | `supervisor.core.supervisor_agent.SupervisorAgent` | +| SimpleSupervisor | OK | OK | `supervisor.core.simple_supervisor.SimpleSupervisor` | + +## Conversation Agents + +| Agent | Import | Execution | Notes | +|-------|--------|-----------|-------| +| BaseConversationAgent | OK | OK | Foundation for all conversation types | +| CollaborativeConversation | OK | OK | Note: directory is `collaberative` (typo) | +| DebateConversation | OK | OK | Structured debate format | +| DirectedConversation | OK | OK | Moderator-directed flow | +| RoundRobinConversation | OK | OK | Sequential turn-taking | +| SocialMediaConversation | OK | OK | Social media simulation | + +## Planning Agents + +| Agent | Import | Execution | Notes | +|-------|--------|-----------|-------| +| LLMCompilerAgent | OK | OK | DAG-based parallel task execution | +| ReWOOAgent | OK | OK | Reasoning Without Observation | +| PlanAndExecuteAgent | OK | OK | Plan then execute pattern | + +## Reasoning Agents + +| Agent | Import | Execution | Notes | +|-------|--------|-----------|-------| +| ReflexionAgent | OK | OK | Self-reflection with memory | +| LATSAgent | OK | OK | Language Agent Tree Search | +| ReflectionAgent | OK | OK | Generate + reflect loop | + +## Discovery & Utility Agents + +| Agent | Import | Execution | Notes | +|-------|--------|-----------|-------| +| ComponentDiscoveryAgent | OK | N/A | Utility agent, not LLM-based | +| DynamicToolSelector | OK | N/A | Tool selection utility | +| SemanticDiscoveryEngine | OK | N/A | Semantic search utility | +| LongTermMemoryAgent | OK | Legacy | Extends old ReactAgent pattern | + +## Chain & Structured Agents + +| Agent | Import | Execution | Notes | +|-------|--------|-----------|-------| +| DeclarativeChainAgent | OK | N/A | Declarative chain builder | +| ChainAgent | OK | N/A | Placeholder (temporarily disabled) | +| StructuredOutputAgent | OK | OK | Structured output with validation | +| TaskAnalysisAgent | OK | OK | Task decomposition | + +## Document Agents + +| Agent | Import | Execution | Notes | +|-------|--------|-----------|-------| +| DocumentLoaderAgent | OK | Legacy | Base document loader | +| FileLoaderAgent | OK | Legacy | File-based loading | +| WebLoaderAgent | OK | Legacy | Web page loading | +| DirectoryLoaderAgent | OK | Legacy | Directory loading | +| DocumentProcessingAgent | OK | Legacy | Document processing pipeline | +| ParallelKGTransformerConfig | OK | Legacy | Knowledge graph extraction config | + +## Document Modifier Agents + +| Agent | Import | Notes | +|-------|--------|-------| +| MapBranchSummarizer | OK | Legacy, parallel map-reduce summarization | +| IterativeSummarizer | OK | Legacy, iterative refinement summarization | +| ParallelKGTransformer | OK | Legacy, parallel KG extraction + merge | +| KGIterativeRefinement | OK | Legacy, iterative KG refinement | +| ComplexExtractionAgent | OK | Legacy, structured data extraction | +| TNTAgent | OK | Legacy, translate-and-transform | +| GraphTransformer | OK | Legacy, LLMGraphTransformer wrapper for docβKG | + +## RAG Agents (22 total) + +| Agent | Import | Notes | +|-------|--------|-------| +| AdaptiveRAGAgent | OK | Adaptive strategy selection | +| AdaptiveToolsRAGAgent | OK | Adaptive tool-based RAG | +| AgenticRAGAgent | OK | ReactAgent with retrieval tools | +| CorrectiveRAGAgent | OK | Self-correcting retrieval | +| DynamicRAGAgent | OK | Multi-source dynamic retrieval | +| FLARERAGAgent | OK | Forward-looking active retrieval | +| RAGFusionAgent | OK | Reciprocal rank fusion | +| HyDERAGAgent | OK | Hypothetical document embeddings | +| LLMRAGAgent | OK | LLM-based RAG | +| MultiQueryRAGAgent | OK | Multiple query variants | +| SelfCorrectiveRAGAgent | OK | Self-correcting | +| SelfReflectiveRAGAgent | OK | Reflective with grading | +| SelfRouteRAGAgent | OK | Query-aware routing | +| SimpleRAGAgent | OK | Basic retriever + answer | +| SpeculativeRAGAgent | OK | Hypothesis + parallel verification | +| StepBackRAGAgent | OK | Abstract query generation | +| DocumentGradingRAGAgent | OK | Document relevance grading | +| HallucinationGraderAgent | OK | Hallucination detection | +| MemoryAwareRAGAgent | OK | RAG with memory context | +| QueryDecomposerAgent | OK | Hierarchical query decomposition | +| FilteredRAGAgent | OK | Filtered retrieval | +| TypedRAGAgent | OK | Typed retrieval | +| MultiStrategyRAGAgent | OK | Multi-strategy | +| QueryPlanningRAGAgent | OK | Query planning | + +## Research Agents + +| Agent | Import | Notes | +|-------|--------|-------| +| STORMAgentConfig | OK | STORM research pipeline config | + +## MultiAgent Verified Capabilities + +| Feature | Status | Notes | +|---------|--------|-------| +| Sequential mode | OK | Agents run in order | +| Parallel mode | OK | Agents run concurrently, results combined | +| Dynamic add_agent | OK | Add agent at runtime | +| Dynamic remove_agent | OK | Remove agent at runtime | +| Dynamic create_agent | OK | Create new agent with system_message + temperature | +| get_agent_names | OK | List current agents | +| get_agent | OK | Get agent by name | + +## Fixes Applied + +1. **chain/__init__.py**: Removed import of missing `examples.py` module, added placeholder stubs +2. **research/storm/__init__.py**: Removed import of missing `example.py` module, added placeholder stub + +## Summary + +- **Total agents verified**: 62 (55 + 7 document modifiers) +- **All imports passing**: 62/62 +- **Fixes applied**: 2 (missing module imports replaced with stubs) +- **No circular import issues found** (discovery agent imports cleanly) diff --git a/project_docs/guides/agent/CUSTOM_NODES_AND_GRAPHS.md b/project_docs/guides/agent/CUSTOM_NODES_AND_GRAPHS.md new file mode 100644 index 00000000..edb0922c --- /dev/null +++ b/project_docs/guides/agent/CUSTOM_NODES_AND_GRAPHS.md @@ -0,0 +1,296 @@ +# Custom Nodes & Graph Design Guide + +**Created**: 2026-04-06 +**Purpose**: How to build custom nodes, graphs, and extend agent functionality + +## Node Types in Haive + +### 1. GenericEngineNodeConfig (LLM execution) + +The standard node for LLM calls. Used by SimpleAgent's `agent_node`. + +```python +from haive.core.graph.node.engine_node_generic import GenericEngineNodeConfig + +agent_node = GenericEngineNodeConfig( + name="agent_node", + engine=my_augllm_config, # Direct engine reference +) +``` + +What it does: +- Extracts messages from state +- Invokes LLM with system message + tools +- Returns `{"messages": [AIMessage(...)]}` + +### 2. ToolNodeConfig (Tool execution) + +Handles tool calls from AIMessage. Used after validation routing. + +```python +from haive.core.graph.node.tool_node_config_v2 import ToolNodeConfig + +tool_node = ToolNodeConfig( + name="tool_node", + engine_name="engine_abc123", # Looks up state.engines[name].tools + # OR + tools=[my_tool1, my_tool2], # Direct tool list (preferred) +) +``` + +What it does: +- Gets tools from state.engines or direct list +- Filters by tool_routes (langchain_tool, function, etc.) +- Delegates to LangGraph's ToolNode for execution +- Returns `{"messages": [ToolMessage(...)]}` + +### 3. ValidationNodeConfigV2 (Routing) + +Routes based on AIMessage content: tool_calls β tool_node, end β END. + +```python +from haive.core.graph.node.validation_node_config_v2 import ValidationNodeConfigV2 + +validation = ValidationNodeConfigV2( + name="validation", + engine_name="engine_abc123", + tool_node="tool_node", + parser_node="parse_output", +) +``` + +### 4. Custom Callable Nodes + +Any function or callable that takes state and returns a dict update. + +```python +def my_custom_node(state): + """Custom processing node.""" + messages = state.get("messages", []) if isinstance(state, dict) else getattr(state, "messages", []) + + # Your logic here + processed = do_something(messages) + + # Return partial state update + return {"custom_field": processed} +``` + +## Building a Custom Graph + +### Step 1: Define State Schema + +```python +from haive.core.schema.prebuilt.llm_state import LLMState +from pydantic import Field + +class MyWorkflowState(LLMState): + """State for my custom workflow.""" + plan: str = "" + iteration: int = 0 + max_iterations: int = 3 + done: bool = False +``` + +### Step 2: Define Nodes + +```python +def plan_node(state: dict) -> dict: + """Create a plan from the user's request.""" + messages = state.get("messages", []) + # Use LLM to create plan (could use agent internally) + return {"plan": "Step 1: ..., Step 2: ...", "iteration": 0} + +def execute_node(state: dict) -> dict: + """Execute one step of the plan.""" + plan = state.get("plan", "") + iteration = state.get("iteration", 0) + # Execute step + return {"iteration": iteration + 1} + +def check_done(state: dict) -> str: + """Route: continue or finish.""" + if state.get("iteration", 0) >= state.get("max_iterations", 3): + return "done" + if state.get("done", False): + return "done" + return "continue" +``` + +### Step 3: Build Graph + +```python +from haive.core.graph.state_graph.base_graph2 import BaseGraph +from langgraph.graph import START, END + +graph = BaseGraph(name="plan_execute") +graph.set_state_schema(MyWorkflowState) + +# Add nodes +graph.add_node("plan", plan_node) +graph.add_node("execute", execute_node) + +# Add edges +graph.add_edge(START, "plan") +graph.add_edge("plan", "execute") + +# Conditional routing +graph.add_conditional_edges("execute", check_done, { + "continue": "execute", # Loop + "done": END, +}) + +# Compile +lg = graph.to_langgraph() +app = lg.compile() + +# Run +result = app.invoke({"messages": [HumanMessage(content="Build a web app")]}) +``` + +### Step 4: Wrap in Agent Class + +```python +class PlanExecuteAgent(ReactAgent): + """Agent that plans and executes iteratively.""" + + max_plan_iterations: int = Field(default=3) + + def build_graph(self) -> BaseGraph: + graph = BaseGraph(name=f"{self.name}_graph") + graph.set_state_schema(MyWorkflowState) + + # LLM node for planning + plan_config = GenericEngineNodeConfig( + name="planner", engine=self.engine, + ) + graph.add_node("planner", plan_config) + + # Custom execution node + graph.add_node("executor", self._execute_step) + + # Routing + graph.add_edge(START, "planner") + graph.add_edge("planner", "executor") + graph.add_conditional_edges("executor", self._check_done, { + "continue": "planner", + "done": END, + }) + + return graph + + def _execute_step(self, state): + # Custom logic + return {"iteration": state.get("iteration", 0) + 1} + + def _check_done(self, state): + if state.get("iteration", 0) >= self.max_plan_iterations: + return "done" + return "continue" +``` + +## Advanced Graph Patterns + +### Branching (Conditional Fan-Out) + +```python +graph.add_conditional_edges("classifier", classify_fn, { + "simple": "simple_handler", + "complex": "complex_handler", + "error": "error_handler", +}) +``` + +### Parallel Processing (LangGraph Send) + +```python +from langgraph.constants import Send + +def fan_out(state): + """Send work to multiple parallel nodes.""" + chunks = state.get("chunks", []) + return [Send("process_chunk", {"chunk": c, "index": i}) + for i, c in enumerate(chunks)] + +graph.add_conditional_edges("splitter", fan_out) +``` + +### Sub-Graphs (Agent as Node) + +```python +def agent_as_node(state): + """Run a full agent as a single node.""" + sub_agent = ReactAgent(name="sub", engine=config, tools=[...]) + result = sub_agent.run(state.get("messages", [])) + return {"messages": result.messages if hasattr(result, "messages") else []} + +graph.add_node("sub_agent", agent_as_node) +``` + +### Reflection Loop + +```python +# Pattern: generate β reflect β revise β check β (loop or end) + +graph.add_edge(START, "generate") +graph.add_edge("generate", "reflect") +graph.add_edge("reflect", "revise") +graph.add_conditional_edges("revise", quality_check, { + "good": END, + "needs_work": "reflect", # Loop back +}) +``` + +## Node Design Rules + +1. **Input**: Receives full state (dict or Pydantic model) +2. **Output**: Returns **partial** update dict (only changed fields) +3. **Messages**: Use `add_messages` reducer β append, don't replace +4. **Errors**: Catch and return error state, don't raise (graph will halt) +5. **Side effects**: OK for logging, store writes, API calls +6. **Idempotent**: Nodes may be retried β design for idempotency + +## Integration with Agent System + +### NodeConfig Objects + +For LLM/tool nodes, use NodeConfig instead of plain functions: + +```python +# LLM node +agent_node = GenericEngineNodeConfig(name="agent", engine=config) + +# Tool node +tool_node = ToolNodeConfig(name="tools", tools=[my_tools]) + +# Validation/routing +validation = ValidationNodeConfigV2(name="route", engine_name=config.name) +``` + +### Using in Agent.build_graph() + +```python +class MyAgent(Agent): + def build_graph(self) -> BaseGraph: + graph = BaseGraph(name=f"{self.name}_graph") + graph.set_state_schema(LLMState) + + # Mix NodeConfig and custom functions + graph.add_node("llm", GenericEngineNodeConfig(name="llm", engine=self.engine)) + graph.add_node("custom", self._my_custom_logic) + graph.add_node("tools", ToolNodeConfig(name="tools", tools=self.engine.tools)) + + graph.add_edge(START, "llm") + graph.add_edge("llm", "custom") + graph.add_edge("custom", "tools") + graph.add_edge("tools", END) + + return graph +``` + +## Related Guides + +- [Agent Design Patterns](AGENT_DESIGN_PATTERNS.md) β How to build agents +- [MultiAgent State Design](MULTIAGENT_STATE_DESIGN.md) β Complex state for multi-agent +- [Memory Agent Guide](MEMORY_AGENT_GUIDE.md) β Memory + KG integration +- [State Schema Notes](STATE_SCHEMA_NOTES.md) β State schema research and bugs +- [State Schema Engine Gap](../../active/architecture/state_schema_engine_gap.md) β Architecture analysis diff --git a/project_docs/guides/agent/MEMORY_AGENT_GUIDE.md b/project_docs/guides/agent/MEMORY_AGENT_GUIDE.md new file mode 100644 index 00000000..4d44d5fe --- /dev/null +++ b/project_docs/guides/agent/MEMORY_AGENT_GUIDE.md @@ -0,0 +1,165 @@ +# Memory Agent Guide + +**Created**: 2026-04-06 +**Status**: Phase 2 complete, e2e verified + +## Overview + +MemoryAgent is a ReactAgent with persistent memory, automatic KG extraction, and auto-summarization. It stores memories and knowledge graph triples in a LangGraph store (InMemoryStore or PostgresStore), with optional Neo4j for graph traversal. + +## Quick Start + +```python +from haive.agents.memory import create_memory_agent + +# Dev mode (InMemoryStore) +agent = create_memory_agent(name="assistant", user_id="user123") + +# Production (PostgreSQL) +agent = create_memory_agent( + name="assistant", + connection_string="postgresql://haive:haive@localhost/haive", + user_id="user123", +) + +# With Neo4j KG +agent = create_memory_agent(name="assistant", user_id="user123") +agent.connect_neo4j() # Uses NEO4J_URI/USER/PASSWORD env vars + +# Run +result = agent.run("My name is Alice and I work at DeepMind.") +# β LLM saves memories + KG triples auto-extracted + +result = agent.run("What do you know about me?") +# β Recalls memories and KG facts +``` + +## Architecture + +``` +create_memory_agent() + βββ Resolves store: explicit > connection_string > InMemoryStore + βββ Creates memory tools bound to store + user_id + βββ Builds AugLLMConfig with tools + βββ Returns MemoryAgent(ReactAgent) + +MemoryAgent.run(input) + βββ PRE-HOOK: _load_memory_context(query) + β βββ Search ("user", user_id) β memories + β βββ Search ("kg", user_id) β KG triples + β βββ Search ("summary", user_id) β summaries + β β Inject into system message + βββ EXECUTE: ReactAgent.run() + β βββ LLM may call: save_memory, search_memory, save_knowledge, search_knowledge + βββ POST-HOOK 1: _extract_and_store_kg(messages) + β βββ SimpleAgent extracts JSON triples from conversation + β βββ Store triples in ("kg", user_id) namespace + β βββ Sync to Neo4j if connected + βββ POST-HOOK 2: Auto-summarize if token_count > threshold + βββ SimpleAgent summarizes conversation + βββ Store in ("summary", user_id) namespace +``` + +## Memory Tools + +| Tool | Purpose | Args | +|------|---------|------| +| `save_memory` | Save facts/preferences about user | content, importance | +| `search_memory` | Recall past memories | query | +| `save_knowledge` | Save KG triple (structured fact) | subject, predicate, object_ | +| `search_knowledge` | Query KG triples | query | + +## Store Namespaces + +| Namespace | Content | Example | +|-----------|---------|---------| +| `("user", user_id)` | User memories | "Alice works at DeepMind" | +| `("kg", user_id)` | KG triples | {subject: "Alice", predicate: "works at", object: "DeepMind"} | +| `("summary", user_id)` | Conversation summaries | "Discussed ML research..." | + +## Neo4j Integration + +```python +# Start Neo4j +# docker-compose up -d neo4j + +# Connect +kg = agent.connect_neo4j() + +# Sync existing triples from store to Neo4j +agent.sync_kg_to_neo4j() + +# Query via graph traversal +triples = agent.query_kg("Alice") +# [{"subject": "Alice", "predicate": "works at", "object": "DeepMind"}, ...] + +# Raw Cypher +results = agent.query_kg_cypher( + "MATCH (s:Entity)-[r:RELATES_TO]->(o:Entity) WHERE s.name = $name RETURN s, r, o", + params={"name": "Alice"} +) + +# Neighborhood (1-2 hops) +neighbors = kg.query_neighborhood("Alice") + +# Shortest path +path = kg.query_path("Alice", "Python") +``` + +### Neo4j Schema + +```cypher +-- Nodes +(:Entity {name, type, user_id, created_at}) +(:Memory {id, content, importance, user_id, created_at}) +(:Summary {id, content, token_count, user_id, created_at}) + +-- Relationships +(Entity)-[:RELATES_TO {predicate, created_at, source}]->(Entity) +(Entity)-[:MENTIONED_IN]->(Memory) +(Memory)-[:SUMMARIZED_BY]->(Summary) + +-- Indexes +CREATE CONSTRAINT entity_name FOR (e:Entity) REQUIRE e.name IS UNIQUE +CREATE INDEX entity_type FOR (e:Entity) ON (e.type) +CREATE INDEX entity_user FOR (e:Entity) ON (e.user_id) +``` + +## Integration with Other Components + +### Document-level KG extraction +```python +# Uses GraphTransformer from document_modifiers +triples = agent.extract_kg_from_document( + "Alice works at DeepMind on reinforcement learning.", + allowed_nodes=["Person", "Organization", "Field"] +) +``` + +### With GraphDBRAG for NLβCypher +The `rag/db_rag/graph_db/` agent can generate Cypher queries from natural language against the same Neo4j instance. + +### With IterativeSummarizer +The `document_modifiers/summarizer/` agents can provide advanced summarization beyond the built-in SimpleAgent summarizer. + +## Docker Setup + +```bash +# Start PostgreSQL + Neo4j +docker-compose up -d + +# Connection strings +# Postgres: postgresql://haive:haive@localhost:5432/haive +# Neo4j: bolt://localhost:7687 (neo4j/haivepass) +# Neo4j UI: http://localhost:7474 +``` + +## E2E Test + +```bash +# Store-only +poetry run python demos/agents/memory_agent_e2e.py + +# With Neo4j +poetry run python demos/agents/memory_agent_e2e.py --neo4j +``` diff --git a/project_docs/guides/agent/MULTIAGENT_STATE_DESIGN.md b/project_docs/guides/agent/MULTIAGENT_STATE_DESIGN.md new file mode 100644 index 00000000..31d32220 --- /dev/null +++ b/project_docs/guides/agent/MULTIAGENT_STATE_DESIGN.md @@ -0,0 +1,242 @@ +# MultiAgent State Design Guide + +**Created**: 2026-04-06 +**Purpose**: How to design complex state schemas for multi-agent systems + +## MultiAgent Architecture + +``` +MultiAgent(Agent) +βββ agents: list[Agent] # Child agents +βββ execution_mode: str # "sequential" | "parallel" | "conditional" +βββ state_schema: MultiAgentState # Pre-built, NOT auto-composed +βββ build_graph() β BaseGraph # Creates wrapper nodes per agent +βββ _create_agent_wrapper() # Bridges MultiAgentState β child state +``` + +## How MultiAgent State Works + +### MultiAgentState (default) + +```python +class MultiAgentState(ToolState): + """State for multi-agent coordination.""" + messages: Annotated[list[BaseMessage], add_messages] # Shared conversation + agents: dict[str, Agent] = {} # Agent instances + agent_states: dict[str, dict] = {} # Per-agent snapshots + agent_outputs: dict[str, Any] = {} # Per-agent results + current_agent: str = "" # Currently executing + execution_order: list[str] = [] # Execution history +``` + +### State Flow: Sequential Mode + +``` +Input: {"messages": [HumanMessage("Write a report")]} + +Step 1: Agent "researcher" + Input: {"messages": [HumanMessage], "engines": researcher.engines} + Output: {"messages": [HumanMessage, AIMessage("Research findings...")]} + +Step 2: Agent "writer" + Input: {"messages": [HumanMessage, AIMessage("Research findings...")], "engines": writer.engines} + Output: {"messages": [HumanMessage, AIMessage, AIMessage("Report: ...")]} + +Final: {"messages": [HumanMessage, AIMessage, AIMessage], "agent_outputs": {...}} +``` + +### State Flow: Parallel Mode + +``` +Input: {"messages": [HumanMessage("Analyze X")]} + +Parallel: + Agent "analyst1" β [HumanMessage, AIMessage("Analysis A")] + Agent "analyst2" β [HumanMessage, AIMessage("Analysis B")] + +Merge: {"messages": [HumanMessage, AIMessage("A"), AIMessage("B")]} +``` + +## Designing Custom Multi-Agent States + +### Pattern 1: Shared Context + +```python +class ResearchPipelineState(MultiAgentState): + """State with shared research context.""" + research_topic: str = "" + findings: list[str] = Field(default_factory=list) + current_phase: str = "planning" # planning β research β writing β review + + # Shared across all agents + domain: str = "" + constraints: list[str] = Field(default_factory=list) +``` + +### Pattern 2: Agent-Specific Sub-States + +```python +class TeamState(MultiAgentState): + """State with per-agent typed sub-states.""" + # Shared + project_goal: str = "" + deadline: str = "" + + # Agent-specific (stored in agent_states dict) + # Access: state.agent_states["planner"]["plan"] + # Access: state.agent_states["coder"]["code"] +``` + +Agent wrappers read from `agent_states[name]` and write back: + +```python +def _custom_wrapper(state, config): + my_state = state.agent_states.get("planner", {}) + plan = my_state.get("plan", "") + # ... execute with plan context ... + return { + "messages": [...], + "agent_states": {**state.agent_states, "planner": {"plan": new_plan}} + } +``` + +### Pattern 3: Conditional Routing State + +```python +class RouterState(MultiAgentState): + """State for conditional agent routing.""" + task_type: str = "" # "math" | "writing" | "research" + complexity: float = 0.0 # 0-1, determines which agent handles it + requires_tools: bool = False + +# Routing function +def route_task(state: RouterState) -> str: + if state.task_type == "math": + return "math_agent" + elif state.complexity > 0.7: + return "expert_agent" + else: + return "simple_agent" +``` + +### Pattern 4: Accumulator State (Map-Reduce) + +```python +class MapReduceState(MultiAgentState): + """State for parallel processing with accumulation.""" + chunks: list[str] = Field(default_factory=list) # Input chunks + chunk_results: list[str] = Field(default_factory=list) # Per-chunk results + final_result: str = "" # Reduced output + current_chunk_idx: int = 0 +``` + +## MultiAgent with Tools + +When child agents have tools, engines must flow through: + +```python +# The wrapper automatically injects engines now: +agent_input = {"messages": messages} +if hasattr(agent, "engines") and agent.engines: + agent_input["engines"] = agent.engines +``` + +So child ReactAgents with tools work correctly in MultiAgent. + +## Building Complex Multi-Agent Systems + +### Research Pipeline Example + +```python +from haive.agents.react.agent import ReactAgent +from haive.agents.simple.agent import SimpleAgent +from haive.agents.multi.agent import MultiAgent +from langchain_core.tools import tool + +@tool +def web_search(query: str) -> str: + '''Search the web.''' + return f"Results for {query}" + +# Step 1: Researcher (ReactAgent with tools) +researcher = ReactAgent( + name="researcher", + engine=AugLLMConfig( + tools=[web_search], + system_message="Research the topic thoroughly. Use web_search.", + ), + max_iterations=3, +) + +# Step 2: Analyzer (SimpleAgent, structured output) +analyzer = SimpleAgent( + name="analyzer", + engine=AugLLMConfig( + temperature=0.2, + system_message="Analyze the research findings. Identify key insights.", + ), +) + +# Step 3: Writer (SimpleAgent) +writer = SimpleAgent( + name="writer", + engine=AugLLMConfig( + temperature=0.7, + system_message="Write a clear, engaging report based on the analysis.", + ), +) + +# Compose +pipeline = MultiAgent( + name="research_pipeline", + agents=[researcher, analyzer, writer], + execution_mode="sequential", +) + +result = pipeline.run("Research AI safety approaches in 2025") +``` + +### Dynamic Supervisor Example + +```python +from haive.agents.supervisor.dynamic.dynamic_supervisor import DynamicSupervisor + +supervisor = DynamicSupervisor( + name="team_lead", + engine=AugLLMConfig( + system_message="You coordinate a team. Route tasks to the right agent.", + ), +) + +# Add agents dynamically +supervisor.add_agent(researcher, description="Researches topics using web search") +supervisor.add_agent(writer, description="Writes reports and content") +supervisor.add_agent(coder, description="Writes and debugs code") + +# Supervisor decides routing +result = supervisor.run("Write a Python script that fetches weather data") +``` + +## State Schema Checklist + +When designing a multi-agent state: + +- [ ] Extend `MultiAgentState` (or `ToolState` if custom) +- [ ] Include `messages: Annotated[list, add_messages]` for conversation +- [ ] All fields have defaults (state starts empty) +- [ ] Shared data in top-level fields +- [ ] Per-agent data in `agent_states` dict +- [ ] If routing needed, add routing fields (task_type, complexity, etc.) +- [ ] If accumulating, add accumulator fields (chunks, results, etc.) + +## Common Pitfalls + +1. **Schema flattening** β Don't merge all agent schemas into one flat schema. Use `agent_states` dict for per-agent data. + +2. **Missing engines** β If child agent has tools but state doesn't have `engines`, tool_node fails. Fixed: wrapper now injects engines. + +3. **Message ordering** β In parallel mode, message order is non-deterministic. Don't rely on position. + +4. **State mutation** β Don't mutate state directly. Return update dicts from nodes. + +5. **Circular dependencies** β Agent A needs output of Agent B, and B needs A. Use conditional routing or iterative patterns instead. diff --git a/project_docs/guides/agent/STATE_SCHEMA_NOTES.md b/project_docs/guides/agent/STATE_SCHEMA_NOTES.md new file mode 100644 index 00000000..f61c59f4 --- /dev/null +++ b/project_docs/guides/agent/STATE_SCHEMA_NOTES.md @@ -0,0 +1,106 @@ +# State Schema Research Notes + +**Created**: 2026-04-06 +**Purpose**: Research findings on state schema composition, engine injection, and tool routing + +## How State Flows Through Agents + +``` +Agent.__init__() + β _setup_schemas() β SchemaComposer β auto-composed state (MISSING engines) + β build_graph() β BaseGraph with state_schema + β compile() β _app = LangGraph CompiledGraph + +Agent.run(input) + β _prepare_input(input) β {"messages": [HumanMessage(...)]} + β execution_mixin injects engines into invoke_input β FIX APPLIED + β _app.invoke(invoke_input) + β agent_node: GenericEngineNodeConfig.__call__(state) + β self.engine (direct ref, works) + β LLM generates AIMessage with tool_calls + β routing: conditional edges check tool_calls + β tool_node: ToolNodeConfig.__call__(state) + β _get_tools(state): looks up state.engines[engine_name].tools + β _execute_tools: ToolNode.invoke({messages: [...]}) +``` + +## Three Bugs Found & Fixed + +### 1. _prepare_input returns empty dict +**Root**: Auto-composed input_schema has no model_fields +**Fix**: Fall back to `{"messages": [HumanMessage(content=input_data)]}` when schema is empty +**File**: `execution_mixin.py` line ~60 + +### 2. _execute_tools serializes messages to dicts +**Root**: `state.dict()` / `model_dump()` converts BaseMessage objects to plain dicts +**Fix**: Pass `{messages_field: messages}` directly instead of full state serialization +**File**: `tool_node_config_v2.py` line ~251 + +### 3. engines not in runtime state +**Root**: Auto-composed schema doesn't include `engines` field (SchemaComposer gap) +**Fix**: Runtime injection in execution_mixin before `_app.invoke()` +**Proper fix needed**: SchemaComposer.build() should always call add_engine_management() +**File**: `execution_mixin.py` line ~583 + +## Schema Composition Deep Dive + +### What SchemaComposer Does +1. Creates dynamic Pydantic model with name like "ReactAgentState" +2. Calls `add_fields_from_engine(engine)` β extracts input/output schemas +3. Calls `build()` β assembles fields into new BaseModel subclass +4. Does NOT call `add_engine_management()` unless base is StateSchema + +### What add_engine_management() Adds +- `tools`: list field +- `tool_instances`: dict field +- `tool_routes`: dict field +- `tool_metadata`: dict field +- `engines`: dict[str, Engine] field β THE CRITICAL ONE + +### Pre-built vs Auto-composed + +| Schema | Has engines | Has tools | Has messages | Source | +|--------|-------------|-----------|--------------|--------| +| StateSchema | β | β | β | Base class | +| MessagesState | β | β | β | Prebuilt | +| ToolState | β | β | β | Prebuilt | +| LLMState | β | β | β | Prebuilt | +| ReactAgentState | β | β | β | Prebuilt | +| MultiAgentState | β | β | β | Prebuilt | +| Auto-composed | β | β | β | SchemaComposer | + +## MultiAgent State Patterns + +### How MultiAgent Handles Engines +- MultiAgentState has explicit `agents: dict[str, Agent]` field +- `setup_agent_hierarchy()` validator syncs engines from all children +- Namespacing: `engines["agent_name.main"]` + fallback `engines["main"]` + +### How Child Agents Execute +- `_create_agent_wrapper()` creates closure over agent instance +- Extracts ONLY messages from parent state +- Invokes child `agent._app.invoke({"messages": [...]})` +- Returns dict with updated messages + agent_states + agent_outputs +- **Tools NOT transferred** between agents + +### State Isolation +- Each child agent has isolated state in `agent_states: dict[str, dict]` +- No schema flattening β agents maintain independence +- Error isolation per agent + +## Recommendations + +### Short-term (done) +- β Runtime engine injection in execution_mixin +- β Fix _prepare_input for empty schemas +- β Fix _execute_tools serialization + +### Medium-term (needed) +- Make SchemaComposer always include engine management when engines present +- Or default to LLMState instead of auto-composing +- Fix MultiAgent wrapper to pass engines to children + +### Long-term +- Unify state schema approach β all agents should use LLMState-based schemas +- Remove auto-composition for agents (keep for generic workflows only) +- Add tool transfer protocol for MultiAgent child communication diff --git a/tools/haive-ui/src/haive_ui/pages/games.py b/tools/haive-ui/src/haive_ui/pages/games.py index 6abe7f69..b3a6b6d0 100644 --- a/tools/haive-ui/src/haive_ui/pages/games.py +++ b/tools/haive-ui/src/haive_ui/pages/games.py @@ -6,6 +6,7 @@ import subprocess import sys import time +import inspect import traceback import streamlit as st @@ -132,7 +133,10 @@ def _render_game_detail(g: GameInfo): st.error(f"Import error: {err}") # Tabs - tab_info, tab_run, tab_source, tab_config = st.tabs(["Info", "Run Game", "Example Source", "Config"]) + tab_play, tab_info, tab_run, tab_source, tab_config = st.tabs(["Play", "Info", "Run Example", "Example Source", "Config"]) + + with tab_play: + _render_play_tab(g, cls, cfg_cls) with tab_info: _render_game_info(g, cls, cfg_cls) @@ -427,3 +431,323 @@ def _render_game_config_tab(g: GameInfo, cfg_cls): except Exception as e: st.warning(f"Could not inspect config: {e}") + + +# -- Play Tab (Interactive Game) --------------------------------------------- + +def _render_play_tab(g: GameInfo, cls, cfg_cls): + """Interactive game play with LLM agents and live board rendering.""" + st.subheader("Play Game") + + if not cls: + st.error("Cannot play - agent class failed to import.") + return + + llm_cfg = st.session_state.llm_config + st.caption(f"LLM: **{llm_cfg['model']}** | Temp: {llm_cfg['temperature']}") + + game_key = f"live_game_{g.name}" + + # Initialize or get game state + col_start, col_reset = st.columns([1, 1]) + with col_start: + start_btn = st.button("Start New Game", type="primary", key=f"start_{g.name}") + with col_reset: + reset_btn = st.button("Reset", key=f"reset_{g.name}") + + if reset_btn: + st.session_state.pop(game_key, None) + st.session_state.pop(f"{game_key}_log", None) + st.rerun() + + if start_btn: + with st.spinner("Creating game agent and compiling graph..."): + try: + agent, state = _create_game(g, cls, cfg_cls) + st.session_state[game_key] = {"agent": agent, "state": state, "step": 0, "done": False} + st.session_state[f"{game_key}_log"] = [] + st.success("Game created! Click 'Next Move' to play.") + except Exception as e: + st.error(f"Failed to create game: {e}") + with st.expander("Details"): + st.code(traceback.format_exc()) + return + + game_data = st.session_state.get(game_key) + if not game_data: + st.info("Click **Start New Game** to begin.") + return + + agent = game_data["agent"] + state = game_data["state"] + step = game_data["step"] + done = game_data["done"] + + # Render game board + _render_game_board(g, state) + + # Game status + game_status = _get_game_status(state) + if game_status: + st.markdown(f"**Status:** {game_status}") + + # Move log + move_log = st.session_state.get(f"{game_key}_log", []) + + if not done: + move_btn = st.button("Next Move", type="primary", key=f"move_{g.name}_{step}") + auto_play = st.checkbox("Auto-play (run full game)", key=f"auto_{g.name}") + + if move_btn or auto_play: + _play_move(g, game_key, agent, state, move_log, auto_play) + else: + st.success("Game Over!") + winner = getattr(state, "winner", None) + if winner: + st.markdown(f"### Winner: **{winner}**") + + # Show move history + if move_log: + with st.expander(f"Move History ({len(move_log)} moves)", expanded=False): + for entry in move_log: + st.markdown(f"- {entry}") + + +def _create_game(g: GameInfo, cls, cfg_cls): + """Create a game agent and initial state.""" + if cfg_cls: + try: + config = cfg_cls(enable_analysis=False, visualize=False) + except Exception: + try: + config = cfg_cls(visualize=False) + except Exception: + config = cfg_cls() + agent = cls(config) + else: + agent = cls() + + # Get initial state + state_module = g.module_path.rsplit(".", 1)[0] + ".state" + try: + import importlib + sm = importlib.import_module(state_module) + # Find the state class + state_cls = None + for name in dir(sm): + obj = getattr(sm, name) + if isinstance(obj, type) and name.endswith("State") and name != "BaseModel": + state_cls = obj + break + if state_cls: + state = state_cls() + else: + state = {} + except Exception: + state = {} + + # Try to initialize the game + if hasattr(agent, "initialize_game"): + try: + state = agent.initialize_game(state) + except Exception: + pass + + return agent, state + + +def _render_game_board(g: GameInfo, state): + """Render the game board based on game type.""" + game_name = g.name.lower().replace(" ", "_").replace("'", "") + + # TicTacToe + if "tic_tac" in game_name: + board = getattr(state, "board", None) + if board: + st.markdown("#### Board") + for r, row in enumerate(board): + cols = st.columns(3) + for c, cell in enumerate(row): + with cols[c]: + val = cell if cell else "." + if val == "X": + st.markdown(f"