(refactor) Refactor base structure towards v1 implementation
This commit is contained in:
parent
b0dc05c909
commit
155112f533
|
|
@ -0,0 +1,470 @@
|
|||
# Agent Directory Template
|
||||
|
||||
This document defines the standard directory structure for all Honcho agents. Following this template ensures consistency across the codebase and makes agents easier to understand, test, and maintain.
|
||||
|
||||
## Standard Directory Structure
|
||||
|
||||
```
|
||||
agent_name/
|
||||
├── __init__.py # Package initialization and exports
|
||||
├── agent.py # Main agent class (inherits from BaseAgent)
|
||||
├── config.py # Agent-specific configuration (optional)
|
||||
├── prompts.py # Agent-specific prompt templates (optional)
|
||||
├── tools.py # Agent-specific tool definitions (optional)
|
||||
└── README.md # Agent documentation (optional)
|
||||
```
|
||||
|
||||
## File Descriptions
|
||||
|
||||
### `__init__.py` (Required)
|
||||
|
||||
Package initialization that exports the main agent class and any public APIs.
|
||||
|
||||
**Template:**
|
||||
```python
|
||||
"""
|
||||
[Agent Name] - [Brief description of agent's purpose]
|
||||
|
||||
This module implements [describe what the agent does].
|
||||
"""
|
||||
|
||||
from .agent import AgentNameAgent
|
||||
|
||||
__all__ = [
|
||||
"AgentNameAgent",
|
||||
]
|
||||
```
|
||||
|
||||
### `agent.py` (Required)
|
||||
|
||||
Main agent implementation that inherits from `BaseAgent`.
|
||||
|
||||
**Template:**
|
||||
```python
|
||||
"""
|
||||
Main implementation of the [Agent Name] agent.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.agents.shared import BaseAgent, AgentConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AgentNameAgent(BaseAgent):
|
||||
"""
|
||||
[Brief description of agent's purpose and functionality]
|
||||
|
||||
This agent is responsible for [describe responsibilities].
|
||||
|
||||
Attributes:
|
||||
db: Database session for agent operations
|
||||
config: Agent configuration
|
||||
[additional agent-specific attributes]
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
config: AgentConfig | None = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initialize the [Agent Name] agent.
|
||||
|
||||
Args:
|
||||
db: SQLAlchemy async database session
|
||||
config: Agent-specific configuration
|
||||
**kwargs: Additional agent-specific parameters
|
||||
"""
|
||||
super().__init__(db, config, **kwargs)
|
||||
# Initialize agent-specific attributes here
|
||||
|
||||
async def execute(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute the agent's main task.
|
||||
|
||||
Args:
|
||||
input_data: Dictionary containing:
|
||||
- [list required input fields]
|
||||
|
||||
Returns:
|
||||
Dictionary containing:
|
||||
- [list output fields]
|
||||
|
||||
Raises:
|
||||
ValueError: If input data is invalid
|
||||
RuntimeError: If agent execution fails
|
||||
"""
|
||||
# Implement agent logic here
|
||||
logger.info(f"[{self.agent_type}] Executing with input: {input_data.keys()}")
|
||||
|
||||
# Example implementation:
|
||||
result = await self._process_input(input_data)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"result": result,
|
||||
}
|
||||
|
||||
def validate_input(self, input_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Validate the input data before execution.
|
||||
|
||||
Args:
|
||||
input_data: Dictionary containing input data to validate
|
||||
|
||||
Returns:
|
||||
True if input is valid
|
||||
|
||||
Raises:
|
||||
ValueError: If input validation fails with a descriptive error message
|
||||
"""
|
||||
# Implement validation logic
|
||||
required_fields = [] # Define required fields
|
||||
|
||||
for field in required_fields:
|
||||
if field not in input_data:
|
||||
raise ValueError(f"Missing required field: {field}")
|
||||
|
||||
return True
|
||||
|
||||
async def _process_input(self, input_data: Dict[str, Any]) -> Any:
|
||||
"""
|
||||
Private helper method for processing input.
|
||||
|
||||
Args:
|
||||
input_data: Validated input data
|
||||
|
||||
Returns:
|
||||
Processed result
|
||||
"""
|
||||
# Implement processing logic
|
||||
pass
|
||||
```
|
||||
|
||||
### `config.py` (Optional)
|
||||
|
||||
Agent-specific configuration class that extends `AgentConfig`.
|
||||
|
||||
**Template:**
|
||||
```python
|
||||
"""
|
||||
Configuration for the [Agent Name] agent.
|
||||
"""
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from src.agents.shared import AgentConfig
|
||||
|
||||
|
||||
class AgentNameConfig(AgentConfig):
|
||||
"""
|
||||
Configuration for the [Agent Name] agent.
|
||||
|
||||
Extends the base AgentConfig with agent-specific parameters.
|
||||
"""
|
||||
|
||||
# Agent-specific configuration fields
|
||||
param_name: str = Field(
|
||||
default="default_value",
|
||||
description="Description of parameter",
|
||||
)
|
||||
|
||||
another_param: int = Field(
|
||||
default=10,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Description with validation constraints",
|
||||
)
|
||||
|
||||
class Config:
|
||||
"""Pydantic configuration."""
|
||||
validate_assignment = True
|
||||
```
|
||||
|
||||
### `prompts.py` (Optional)
|
||||
|
||||
Agent-specific prompt templates and formatting functions.
|
||||
|
||||
**Template:**
|
||||
```python
|
||||
"""
|
||||
Prompt templates for the [Agent Name] agent.
|
||||
"""
|
||||
|
||||
from src.agents.shared import format_system_prompt
|
||||
|
||||
|
||||
def get_agent_system_prompt() -> str:
|
||||
"""
|
||||
Get the system prompt for the [Agent Name] agent.
|
||||
|
||||
Returns:
|
||||
Formatted system prompt
|
||||
"""
|
||||
return format_system_prompt(
|
||||
role="[agent role description]",
|
||||
task_description="[detailed task description]",
|
||||
guidelines=[
|
||||
"guideline 1",
|
||||
"guideline 2",
|
||||
],
|
||||
constraints=[
|
||||
"constraint 1",
|
||||
"constraint 2",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def format_agent_specific_prompt(data: dict) -> str:
|
||||
"""
|
||||
Format an agent-specific prompt with data.
|
||||
|
||||
Args:
|
||||
data: Data to include in the prompt
|
||||
|
||||
Returns:
|
||||
Formatted prompt string
|
||||
"""
|
||||
# Implement agent-specific formatting
|
||||
return f"Custom prompt with {data}"
|
||||
```
|
||||
|
||||
### `tools.py` (Optional)
|
||||
|
||||
Agent-specific tool definitions for LLM tool calling.
|
||||
|
||||
**Template:**
|
||||
```python
|
||||
"""
|
||||
Tool definitions for the [Agent Name] agent.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
from src.agents.shared import create_tool_definition
|
||||
|
||||
|
||||
def get_agent_tools() -> list[Dict[str, Any]]:
|
||||
"""
|
||||
Get tool definitions for the [Agent Name] agent.
|
||||
|
||||
Returns:
|
||||
List of tool definitions
|
||||
"""
|
||||
return [
|
||||
create_tool_definition(
|
||||
name="tool_name",
|
||||
description="What the tool does",
|
||||
parameters={
|
||||
"param1": {
|
||||
"type": "string",
|
||||
"description": "Parameter description",
|
||||
},
|
||||
},
|
||||
required=["param1"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def execute_tool(
|
||||
tool_name: str,
|
||||
arguments: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute an agent tool.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool to execute
|
||||
arguments: Tool arguments
|
||||
context: Execution context (db, config, etc.)
|
||||
|
||||
Returns:
|
||||
Tool execution result
|
||||
"""
|
||||
if tool_name == "tool_name":
|
||||
return await _execute_tool_name(arguments, context)
|
||||
|
||||
raise ValueError(f"Unknown tool: {tool_name}")
|
||||
|
||||
|
||||
async def _execute_tool_name(
|
||||
arguments: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""Execute specific tool."""
|
||||
# Implement tool logic
|
||||
return {"result": "success"}
|
||||
```
|
||||
|
||||
## Shared Infrastructure Usage
|
||||
|
||||
All agents should leverage the shared infrastructure in `src/agents/shared/`:
|
||||
|
||||
### BaseAgent
|
||||
|
||||
```python
|
||||
from src.agents.shared import BaseAgent
|
||||
|
||||
class MyAgent(BaseAgent):
|
||||
async def execute(self, input_data):
|
||||
# Implementation
|
||||
pass
|
||||
|
||||
def validate_input(self, input_data):
|
||||
# Validation
|
||||
return True
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```python
|
||||
from src.agents.shared import AgentConfig, ExtractorConfig
|
||||
|
||||
# Use base config
|
||||
config = AgentConfig(model="gpt-4o", temperature=0.7)
|
||||
|
||||
# Or use specialized config
|
||||
config = ExtractorConfig(temperature=0.3)
|
||||
```
|
||||
|
||||
### Prompt Utilities
|
||||
|
||||
```python
|
||||
from src.agents.shared import (
|
||||
format_system_prompt,
|
||||
format_context_section,
|
||||
format_provenance_chain,
|
||||
truncate_text,
|
||||
)
|
||||
|
||||
prompt = format_system_prompt(
|
||||
role="data processor",
|
||||
task_description="Process and analyze data",
|
||||
)
|
||||
```
|
||||
|
||||
### Tool Utilities
|
||||
|
||||
```python
|
||||
from src.agents.shared import (
|
||||
create_tool_definition,
|
||||
validate_tool_call,
|
||||
extract_tool_arguments,
|
||||
format_tool_result,
|
||||
)
|
||||
|
||||
tool = create_tool_definition(
|
||||
name="search",
|
||||
description="Search for data",
|
||||
parameters={"query": {"type": "string"}},
|
||||
)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Each agent should have corresponding tests in `tests/agents/[agent_name]/`:
|
||||
|
||||
```
|
||||
tests/
|
||||
└── agents/
|
||||
└── agent_name/
|
||||
├── __init__.py
|
||||
├── test_agent.py # Main agent tests
|
||||
├── test_config.py # Configuration tests
|
||||
└── test_integration.py # Integration tests
|
||||
```
|
||||
|
||||
### Test Template
|
||||
|
||||
```python
|
||||
"""Tests for [Agent Name] agent."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from src.agents.agent_name import AgentNameAgent
|
||||
|
||||
|
||||
class TestAgentName:
|
||||
"""Test suite for [Agent Name] agent."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db(self):
|
||||
"""Create mock database session."""
|
||||
return MagicMock()
|
||||
|
||||
@pytest.fixture
|
||||
def agent(self, mock_db):
|
||||
"""Create agent instance."""
|
||||
return AgentNameAgent(db=mock_db)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_success(self, agent):
|
||||
"""Test successful execution."""
|
||||
input_data = {"required_field": "value"}
|
||||
result = await agent.execute(input_data)
|
||||
|
||||
assert result["success"] is True
|
||||
|
||||
def test_validate_input_valid(self, agent):
|
||||
"""Test input validation with valid data."""
|
||||
input_data = {"required_field": "value"}
|
||||
assert agent.validate_input(input_data) is True
|
||||
|
||||
def test_validate_input_invalid(self, agent):
|
||||
"""Test input validation with invalid data."""
|
||||
input_data = {}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
agent.validate_input(input_data)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Inheritance**: Always inherit from `BaseAgent` for consistency
|
||||
2. **Type Hints**: Use comprehensive type annotations throughout
|
||||
3. **Logging**: Use structured logging with `logger.info/debug/error`
|
||||
4. **Error Handling**: Raise specific exceptions with descriptive messages
|
||||
5. **Documentation**: Include docstrings for all public methods
|
||||
6. **Configuration**: Use Pydantic models for type-safe configuration
|
||||
7. **Testing**: Write comprehensive unit and integration tests
|
||||
8. **Async**: Use async/await for all I/O operations
|
||||
9. **Shared Utilities**: Leverage shared infrastructure instead of duplicating code
|
||||
10. **Naming Conventions**: Use snake_case for functions/variables, PascalCase for classes
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
When converting an existing agent to this template:
|
||||
|
||||
- [ ] Create new directory structure
|
||||
- [ ] Implement BaseAgent inheritance
|
||||
- [ ] Move configuration to config.py
|
||||
- [ ] Extract prompts to prompts.py
|
||||
- [ ] Extract tools to tools.py
|
||||
- [ ] Update imports in dependent files
|
||||
- [ ] Write/update tests
|
||||
- [ ] Update documentation
|
||||
- [ ] Verify all functionality works
|
||||
- [ ] Remove old agent files
|
||||
|
||||
## Example Agents
|
||||
|
||||
See these agents for reference implementations:
|
||||
|
||||
- **Extractor** (`src/agents/extractor/`): Premise extraction and memory formation
|
||||
- **Dialectic** (`src/agents/dialectic/`): Query answering with context retrieval
|
||||
- **Dreamer** (`src/agents/dreamer/`): Memory consolidation and improvement
|
||||
|
||||
## Questions?
|
||||
|
||||
For questions or clarifications about the agent template, see:
|
||||
- `src/agents/shared/base_agent.py` - BaseAgent implementation
|
||||
- `AGENT_DEVELOPMENT.md` - Detailed development guidelines (Phase 0B.5)
|
||||
- `TODO.md` - Implementation plan and progress tracking
|
||||
|
|
@ -11,7 +11,7 @@ from collections.abc import AsyncIterator
|
|||
from src import crud
|
||||
from src.config import ReasoningLevel
|
||||
from src.dependencies import tracked_db
|
||||
from src.dialectic.core import DialecticAgent
|
||||
from src.agents.dialectic.core import DialecticAgent
|
||||
from src.utils.config_helpers import get_configuration
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -15,7 +15,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
|
||||
from src import crud, prometheus
|
||||
from src.config import ReasoningLevel, settings
|
||||
from src.dialectic import prompts
|
||||
from src.agents.dialectic import prompts
|
||||
from src.utils.agent_tools import DIALECTIC_TOOLS, create_tool_executor, search_memory
|
||||
from src.utils.clients import (
|
||||
HonchoLLMCallResponse,
|
||||
|
|
@ -163,7 +163,7 @@ class DreamScheduler:
|
|||
"""Execute the dream by enqueueing it and updating collection metadata."""
|
||||
# Import here to avoid circular dependency
|
||||
from src import crud
|
||||
from src.deriver.enqueue import enqueue_dream
|
||||
from src.agents.extractor.enqueue import enqueue_dream
|
||||
from src.utils.config_helpers import get_configuration
|
||||
|
||||
# Find the most recent session and get current document count
|
||||
|
|
@ -4,7 +4,7 @@ import sentry_sdk
|
|||
|
||||
from src.config import settings
|
||||
from src.dependencies import tracked_db
|
||||
from src.dreamer.orchestrator import run_dream
|
||||
from src.agents.dreamer.orchestrator import run_dream
|
||||
from src.schemas import DreamType
|
||||
from src.utils.queue_payload import DreamPayload
|
||||
|
||||
|
|
@ -19,8 +19,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
|
||||
from src import crud
|
||||
from src.config import settings
|
||||
from src.dreamer.specialists import SPECIALISTS
|
||||
from src.dreamer.surprisal import SurprisalScore # type: ignore
|
||||
from src.agents.dreamer.specialists import SPECIALISTS
|
||||
from src.agents.dreamer.surprisal import SurprisalScore # type: ignore
|
||||
from src.exceptions import SpecialistExecutionError, SurprisalError
|
||||
from src.utils.config_helpers import get_configuration
|
||||
from src.utils.logging import (
|
||||
|
|
@ -102,7 +102,7 @@ async def run_dream(
|
|||
if settings.DREAM.SURPRISAL.ENABLED:
|
||||
logger.info(f"[{run_id}] Phase 0: Computing surprisal scores")
|
||||
try:
|
||||
from src.dreamer.surprisal import sample_observations_with_surprisal
|
||||
from src.agents.dreamer.surprisal import sample_observations_with_surprisal
|
||||
|
||||
high_surprisal_obs = await sample_observations_with_surprisal(
|
||||
db=db,
|
||||
|
|
@ -18,7 +18,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
from src import models
|
||||
from src.config import settings
|
||||
from src.crud.document import get_all_documents
|
||||
from src.dreamer.trees import SurprisalTree, create_tree
|
||||
from src.agents.dreamer.trees import SurprisalTree, create_tree
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -6,8 +6,8 @@ from sqlalchemy import select
|
|||
|
||||
from src import crud, models
|
||||
from src.dependencies import tracked_db
|
||||
from src.deriver.deriver import process_representation_tasks_batch
|
||||
from src.dreamer.dreamer import process_dream
|
||||
from src.agents.extractor.deriver import process_representation_tasks_batch
|
||||
from src.agents.dreamer.dreamer import process_dream
|
||||
from src.exceptions import ResourceNotFoundException
|
||||
from src.models import Message
|
||||
from src.schemas import ResolvedConfiguration
|
||||
|
|
@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
from src import crud, models, schemas
|
||||
from src.config import settings
|
||||
from src.dependencies import tracked_db
|
||||
from src.dreamer.dream_scheduler import get_dream_scheduler
|
||||
from src.agents.dreamer.dream_scheduler import get_dream_scheduler
|
||||
from src.exceptions import ValidationException
|
||||
from src.models import QueueItem
|
||||
from src.schemas import MessageConfiguration, ResolvedConfiguration
|
||||
|
|
@ -19,8 +19,8 @@ from src import models, prometheus
|
|||
from src.cache.client import close_cache, init_cache
|
||||
from src.config import settings
|
||||
from src.dependencies import tracked_db
|
||||
from src.deriver.consumer import process_item, process_representation_batch
|
||||
from src.dreamer.dream_scheduler import (
|
||||
from src.agents.extractor.consumer import process_item, process_representation_batch
|
||||
from src.agents.dreamer.dream_scheduler import (
|
||||
DreamScheduler,
|
||||
get_dream_scheduler,
|
||||
set_dream_scheduler,
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
"""
|
||||
Shared agent infrastructure for Honcho.
|
||||
|
||||
This module provides base classes, utilities, and common functionality
|
||||
shared across all agents in the Honcho system.
|
||||
"""
|
||||
|
||||
from .base_agent import BaseAgent
|
||||
from .config import (
|
||||
AbducerConfig,
|
||||
AgentConfig,
|
||||
DialecticConfig,
|
||||
DreamerConfig,
|
||||
ExtractorConfig,
|
||||
FalsifierConfig,
|
||||
InductorConfig,
|
||||
PredictorConfig,
|
||||
create_config_from_dict,
|
||||
)
|
||||
from .prompts import (
|
||||
format_context_section,
|
||||
format_peer_info,
|
||||
format_provenance_chain,
|
||||
format_system_prompt,
|
||||
truncate_text,
|
||||
)
|
||||
from .tools import (
|
||||
create_tool_definition,
|
||||
extract_tool_arguments,
|
||||
format_tool_result,
|
||||
validate_tool_call,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Base classes
|
||||
"BaseAgent",
|
||||
"AgentConfig",
|
||||
# Agent configs
|
||||
"ExtractorConfig",
|
||||
"AbducerConfig",
|
||||
"PredictorConfig",
|
||||
"FalsifierConfig",
|
||||
"InductorConfig",
|
||||
"DialecticConfig",
|
||||
"DreamerConfig",
|
||||
# Config utilities
|
||||
"create_config_from_dict",
|
||||
# Prompt utilities
|
||||
"format_system_prompt",
|
||||
"format_context_section",
|
||||
"format_provenance_chain",
|
||||
"format_peer_info",
|
||||
"truncate_text",
|
||||
# Tool utilities
|
||||
"create_tool_definition",
|
||||
"validate_tool_call",
|
||||
"extract_tool_arguments",
|
||||
"format_tool_result",
|
||||
]
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
"""
|
||||
Base Agent class for Honcho agents.
|
||||
|
||||
This module defines the abstract base class that all Honcho agents should inherit from,
|
||||
providing a consistent interface and common functionality.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseAgent(ABC):
|
||||
"""
|
||||
Abstract base class for all Honcho agents.
|
||||
|
||||
All agents should inherit from this class and implement the required abstract methods.
|
||||
This ensures a consistent interface across all agents and provides common functionality
|
||||
for logging, error handling, and provenance tracking.
|
||||
|
||||
Attributes:
|
||||
db: Database session for agent operations
|
||||
config: Agent-specific configuration
|
||||
agent_type: String identifier for the agent type (e.g., "abducer", "predictor")
|
||||
"""
|
||||
|
||||
def __init__(self, db: AsyncSession, config: Any = None, **kwargs):
|
||||
"""
|
||||
Initialize the base agent.
|
||||
|
||||
Args:
|
||||
db: SQLAlchemy async database session
|
||||
config: Agent-specific configuration object
|
||||
**kwargs: Additional agent-specific parameters
|
||||
"""
|
||||
self.db = db
|
||||
self.config = config
|
||||
self.agent_type = self.__class__.__name__.lower()
|
||||
|
||||
# Store additional kwargs for agent-specific parameters
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
logger.debug(f"Initialized {self.agent_type} agent")
|
||||
|
||||
@abstractmethod
|
||||
async def execute(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute the agent's main task.
|
||||
|
||||
This is the primary method that performs the agent's work. All agents
|
||||
must implement this method with their specific logic.
|
||||
|
||||
Args:
|
||||
input_data: Dictionary containing the input data for the agent.
|
||||
The structure depends on the specific agent type.
|
||||
|
||||
Returns:
|
||||
Dictionary containing the agent's output. The structure depends
|
||||
on the specific agent type.
|
||||
|
||||
Raises:
|
||||
ValueError: If input data is invalid
|
||||
RuntimeError: If agent execution fails
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def validate_input(self, input_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Validate the input data before execution.
|
||||
|
||||
This method should check that the input_data dictionary contains
|
||||
all required fields and that values are of the correct type.
|
||||
|
||||
Args:
|
||||
input_data: Dictionary containing input data to validate
|
||||
|
||||
Returns:
|
||||
True if input is valid, False otherwise
|
||||
|
||||
Raises:
|
||||
ValueError: If input validation fails with a descriptive error message
|
||||
"""
|
||||
pass
|
||||
|
||||
async def trace_execution(
|
||||
self,
|
||||
input_data: Dict[str, Any],
|
||||
output: Dict[str, Any],
|
||||
metadata: Dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Record execution trace for provenance tracking.
|
||||
|
||||
This method records the agent's execution for training data generation
|
||||
and debugging purposes. The default implementation logs basic information,
|
||||
but agents can override this to provide more detailed tracing.
|
||||
|
||||
Args:
|
||||
input_data: The input provided to the agent
|
||||
output: The output produced by the agent
|
||||
metadata: Optional additional metadata about the execution
|
||||
(e.g., execution time, model used, tokens consumed)
|
||||
|
||||
Note:
|
||||
This is optional and has a default implementation that logs basic info.
|
||||
Agents can override this for more detailed provenance tracking.
|
||||
"""
|
||||
logger.info(
|
||||
f"[{self.agent_type}] Execution trace",
|
||||
extra={
|
||||
"agent_type": self.agent_type,
|
||||
"input_keys": list(input_data.keys()),
|
||||
"output_keys": list(output.keys()),
|
||||
"metadata": metadata or {},
|
||||
},
|
||||
)
|
||||
|
||||
async def pre_execute(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Hook called before execute(). Can be used for setup, validation, etc.
|
||||
|
||||
Args:
|
||||
input_data: The input data that will be passed to execute()
|
||||
|
||||
Returns:
|
||||
Modified input_data (or original if no modifications needed)
|
||||
|
||||
Note:
|
||||
This is optional and has a default implementation that validates input.
|
||||
Agents can override this for additional pre-processing.
|
||||
"""
|
||||
if not self.validate_input(input_data):
|
||||
raise ValueError(f"Invalid input for {self.agent_type} agent")
|
||||
return input_data
|
||||
|
||||
async def post_execute(
|
||||
self, input_data: Dict[str, Any], output: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Hook called after execute(). Can be used for cleanup, logging, etc.
|
||||
|
||||
Args:
|
||||
input_data: The input data that was passed to execute()
|
||||
output: The output produced by execute()
|
||||
|
||||
Returns:
|
||||
Modified output (or original if no modifications needed)
|
||||
|
||||
Note:
|
||||
This is optional and has a default implementation that traces execution.
|
||||
Agents can override this for additional post-processing.
|
||||
"""
|
||||
await self.trace_execution(input_data, output)
|
||||
return output
|
||||
|
||||
async def run(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Run the full agent execution pipeline with hooks.
|
||||
|
||||
This method orchestrates the full execution flow:
|
||||
1. pre_execute (validation, setup)
|
||||
2. execute (main agent logic)
|
||||
3. post_execute (cleanup, tracing)
|
||||
|
||||
Args:
|
||||
input_data: Dictionary containing input data
|
||||
|
||||
Returns:
|
||||
Dictionary containing agent output
|
||||
|
||||
Raises:
|
||||
ValueError: If input validation fails
|
||||
RuntimeError: If execution fails
|
||||
"""
|
||||
try:
|
||||
# Pre-execution hook
|
||||
validated_input = await self.pre_execute(input_data)
|
||||
|
||||
# Main execution
|
||||
output = await self.execute(validated_input)
|
||||
|
||||
# Post-execution hook
|
||||
final_output = await self.post_execute(validated_input, output)
|
||||
|
||||
return final_output
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[{self.agent_type}] Execution failed: {str(e)}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of the agent."""
|
||||
return f"<{self.__class__.__name__} type={self.agent_type}>"
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
"""
|
||||
Configuration base classes for Honcho agents.
|
||||
|
||||
This module provides base configuration classes and utilities
|
||||
for agent configuration management.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class AgentConfig(BaseModel):
|
||||
"""
|
||||
Base configuration class for all Honcho agents.
|
||||
|
||||
All agent-specific configurations should inherit from this class
|
||||
to ensure consistent configuration management.
|
||||
"""
|
||||
|
||||
model: str = Field(
|
||||
default="gpt-4o",
|
||||
description="LLM model to use for this agent",
|
||||
)
|
||||
|
||||
temperature: float = Field(
|
||||
default=0.7,
|
||||
ge=0.0,
|
||||
le=2.0,
|
||||
description="Temperature for LLM sampling",
|
||||
)
|
||||
|
||||
max_tokens: int | None = Field(
|
||||
default=None,
|
||||
description="Maximum tokens for LLM response (None = no limit)",
|
||||
)
|
||||
|
||||
timeout: int = Field(
|
||||
default=60,
|
||||
gt=0,
|
||||
description="Timeout in seconds for agent execution",
|
||||
)
|
||||
|
||||
retry_attempts: int = Field(
|
||||
default=3,
|
||||
ge=0,
|
||||
description="Number of retry attempts on failure",
|
||||
)
|
||||
|
||||
enable_tracing: bool = Field(
|
||||
default=True,
|
||||
description="Enable provenance tracing for this agent",
|
||||
)
|
||||
|
||||
class Config:
|
||||
"""Pydantic configuration."""
|
||||
|
||||
validate_assignment = True
|
||||
extra = "forbid"
|
||||
|
||||
|
||||
class ExtractorConfig(AgentConfig):
|
||||
"""Configuration for the Extractor agent."""
|
||||
|
||||
model: str = "gpt-4o-mini"
|
||||
temperature: float = 0.3 # Lower temperature for more consistent extraction
|
||||
|
||||
|
||||
class AbducerConfig(AgentConfig):
|
||||
"""Configuration for the Abducer agent (hypothesis generation)."""
|
||||
|
||||
model: str = "gpt-4o"
|
||||
temperature: float = 0.7
|
||||
max_hypotheses: int = Field(
|
||||
default=5,
|
||||
ge=1,
|
||||
le=10,
|
||||
description="Maximum number of hypotheses to generate",
|
||||
)
|
||||
min_hypotheses: int = Field(
|
||||
default=1,
|
||||
ge=1,
|
||||
description="Minimum number of hypotheses to generate",
|
||||
)
|
||||
|
||||
|
||||
class PredictorConfig(AgentConfig):
|
||||
"""Configuration for the Predictor agent (blind prediction generation)."""
|
||||
|
||||
model: str = "gpt-4o"
|
||||
temperature: float = 0.8
|
||||
max_predictions_per_hypothesis: int = Field(
|
||||
default=5,
|
||||
ge=2,
|
||||
le=10,
|
||||
description="Maximum predictions per hypothesis",
|
||||
)
|
||||
min_predictions_per_hypothesis: int = Field(
|
||||
default=2,
|
||||
ge=1,
|
||||
description="Minimum predictions per hypothesis",
|
||||
)
|
||||
enforce_blindness: bool = Field(
|
||||
default=True,
|
||||
description="Strictly enforce that predictor doesn't see premises",
|
||||
)
|
||||
|
||||
|
||||
class FalsifierConfig(AgentConfig):
|
||||
"""Configuration for the Falsifier agent (contradiction search)."""
|
||||
|
||||
model: str = "gpt-4o"
|
||||
temperature: float = 0.5
|
||||
max_search_iterations: int = Field(
|
||||
default=7,
|
||||
ge=1,
|
||||
le=20,
|
||||
description="Maximum adversarial search iterations",
|
||||
)
|
||||
early_stop_on_contradiction: bool = Field(
|
||||
default=True,
|
||||
description="Stop searching once a contradiction is found",
|
||||
)
|
||||
search_creativity: float = Field(
|
||||
default=0.7,
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
description="Creativity level for adversarial search queries",
|
||||
)
|
||||
|
||||
|
||||
class InductorConfig(AgentConfig):
|
||||
"""Configuration for the Inductor agent (pattern extraction)."""
|
||||
|
||||
model: str = "gpt-4o"
|
||||
temperature: float = 0.6
|
||||
min_sources_per_induction: int = Field(
|
||||
default=2,
|
||||
ge=2,
|
||||
description="Minimum source predictions needed for an induction",
|
||||
)
|
||||
clustering_threshold: float = Field(
|
||||
default=0.6,
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
description="Similarity threshold for prediction clustering",
|
||||
)
|
||||
|
||||
|
||||
class DialecticConfig(AgentConfig):
|
||||
"""Configuration for the Dialectic agent (query answering)."""
|
||||
|
||||
model: str = "gpt-4o"
|
||||
temperature: float = 0.7
|
||||
max_tool_iterations: int = Field(
|
||||
default=10,
|
||||
ge=1,
|
||||
le=50,
|
||||
description="Maximum tool calling iterations",
|
||||
)
|
||||
|
||||
|
||||
class DreamerConfig(AgentConfig):
|
||||
"""Configuration for the Dreamer agent (orchestration)."""
|
||||
|
||||
model: str = "gpt-4o"
|
||||
temperature: float = 0.7
|
||||
enable_surprisal: bool = Field(
|
||||
default=True,
|
||||
description="Enable surprisal-based sampling",
|
||||
)
|
||||
|
||||
|
||||
def create_config_from_dict(
|
||||
config_class: type[AgentConfig],
|
||||
config_dict: Dict[str, Any],
|
||||
) -> AgentConfig:
|
||||
"""
|
||||
Create an agent configuration from a dictionary.
|
||||
|
||||
Args:
|
||||
config_class: The configuration class to instantiate
|
||||
config_dict: Dictionary of configuration values
|
||||
|
||||
Returns:
|
||||
Instantiated configuration object
|
||||
|
||||
Raises:
|
||||
ValueError: If configuration is invalid
|
||||
"""
|
||||
try:
|
||||
return config_class(**config_dict)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid configuration: {e}")
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
"""
|
||||
Shared prompt utilities for Honcho agents.
|
||||
|
||||
This module provides common prompt formatting functions and templates
|
||||
that can be used across multiple agents.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def format_system_prompt(
|
||||
role: str,
|
||||
task_description: str,
|
||||
guidelines: list[str] | None = None,
|
||||
constraints: list[str] | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Format a system prompt for an agent.
|
||||
|
||||
Args:
|
||||
role: The role of the agent (e.g., "hypothesis generator", "falsifier")
|
||||
task_description: Description of the agent's main task
|
||||
guidelines: Optional list of guidelines for the agent to follow
|
||||
constraints: Optional list of constraints the agent must respect
|
||||
|
||||
Returns:
|
||||
Formatted system prompt string
|
||||
"""
|
||||
prompt_parts = [
|
||||
f"You are a {role}.",
|
||||
"",
|
||||
task_description,
|
||||
]
|
||||
|
||||
if guidelines:
|
||||
prompt_parts.extend([
|
||||
"",
|
||||
"Guidelines:",
|
||||
*[f"- {guideline}" for guideline in guidelines],
|
||||
])
|
||||
|
||||
if constraints:
|
||||
prompt_parts.extend([
|
||||
"",
|
||||
"Constraints:",
|
||||
*[f"- {constraint}" for constraint in constraints],
|
||||
])
|
||||
|
||||
return "\n".join(prompt_parts)
|
||||
|
||||
|
||||
def format_context_section(
|
||||
title: str,
|
||||
items: list[str] | list[dict[str, Any]],
|
||||
item_formatter: Any = None,
|
||||
) -> str:
|
||||
"""
|
||||
Format a context section with a title and items.
|
||||
|
||||
Args:
|
||||
title: Title for the context section
|
||||
items: List of items to include (strings or dicts)
|
||||
item_formatter: Optional function to format each item
|
||||
|
||||
Returns:
|
||||
Formatted context section
|
||||
"""
|
||||
if not items:
|
||||
return f"{title}:\n(none)"
|
||||
|
||||
formatted_items = []
|
||||
for i, item in enumerate(items, 1):
|
||||
if item_formatter:
|
||||
formatted_items.append(item_formatter(item, i))
|
||||
elif isinstance(item, str):
|
||||
formatted_items.append(f"{i}. {item}")
|
||||
elif isinstance(item, dict):
|
||||
# Default dict formatting
|
||||
formatted_items.append(f"{i}. {item.get('content', str(item))}")
|
||||
else:
|
||||
formatted_items.append(f"{i}. {str(item)}")
|
||||
|
||||
return f"{title}:\n" + "\n".join(formatted_items)
|
||||
|
||||
|
||||
def format_provenance_chain(
|
||||
entity: str,
|
||||
sources: list[str],
|
||||
source_type: str = "premise",
|
||||
) -> str:
|
||||
"""
|
||||
Format provenance information showing the chain of reasoning.
|
||||
|
||||
Args:
|
||||
entity: The entity (hypothesis, prediction, etc.)
|
||||
sources: List of source IDs or descriptions
|
||||
source_type: Type of sources (e.g., "premise", "hypothesis", "prediction")
|
||||
|
||||
Returns:
|
||||
Formatted provenance chain
|
||||
"""
|
||||
if not sources:
|
||||
return f"{entity} (no sources)"
|
||||
|
||||
source_list = "\n".join([f" - {source}" for source in sources])
|
||||
return f"{entity}\n Based on {len(sources)} {source_type}(s):\n{source_list}"
|
||||
|
||||
|
||||
def truncate_text(
|
||||
text: str,
|
||||
max_length: int = 500,
|
||||
suffix: str = "...",
|
||||
) -> str:
|
||||
"""
|
||||
Truncate text to a maximum length.
|
||||
|
||||
Args:
|
||||
text: Text to truncate
|
||||
max_length: Maximum length (including suffix)
|
||||
suffix: Suffix to add when truncating
|
||||
|
||||
Returns:
|
||||
Truncated text
|
||||
"""
|
||||
if len(text) <= max_length:
|
||||
return text
|
||||
|
||||
return text[: max_length - len(suffix)] + suffix
|
||||
|
||||
|
||||
def format_peer_info(
|
||||
observer: str,
|
||||
observed: str,
|
||||
observer_card: list[str] | None = None,
|
||||
observed_card: list[str] | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Format peer information for agent context.
|
||||
|
||||
Args:
|
||||
observer: ID of the observing peer
|
||||
observed: ID of the observed peer
|
||||
observer_card: Optional biographical info about observer
|
||||
observed_card: Optional biographical info about observed
|
||||
|
||||
Returns:
|
||||
Formatted peer information
|
||||
"""
|
||||
lines = [
|
||||
f"Observer: {observer}",
|
||||
f"Observed: {observed}",
|
||||
]
|
||||
|
||||
if observer_card:
|
||||
lines.append(
|
||||
"Observer Background:\n "
|
||||
+ "\n ".join(observer_card)
|
||||
)
|
||||
|
||||
if observed_card:
|
||||
lines.append(
|
||||
"Observed Background:\n "
|
||||
+ "\n ".join(observed_card)
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
"""
|
||||
Shared tool definitions and utilities for Honcho agents.
|
||||
|
||||
This module provides common tool-related functionality that can be
|
||||
used across multiple agents.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
def create_tool_definition(
|
||||
name: str,
|
||||
description: str,
|
||||
parameters: Dict[str, Any],
|
||||
required: list[str] | None = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a standardized tool definition for LLM tool calling.
|
||||
|
||||
Args:
|
||||
name: Tool name (snake_case)
|
||||
description: Clear description of what the tool does
|
||||
parameters: Dictionary defining the parameters and their types
|
||||
required: List of required parameter names
|
||||
|
||||
Returns:
|
||||
Tool definition in the format expected by LLM APIs
|
||||
|
||||
Example:
|
||||
>>> create_tool_definition(
|
||||
... name="search_premises",
|
||||
... description="Search for premises matching a query",
|
||||
... parameters={
|
||||
... "query": {"type": "string", "description": "Search query"},
|
||||
... "limit": {"type": "integer", "description": "Max results"},
|
||||
... },
|
||||
... required=["query"],
|
||||
... )
|
||||
"""
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": parameters,
|
||||
"required": required or [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def validate_tool_call(
|
||||
tool_call: Dict[str, Any],
|
||||
expected_tools: list[str],
|
||||
) -> bool:
|
||||
"""
|
||||
Validate that a tool call is well-formed and uses a known tool.
|
||||
|
||||
Args:
|
||||
tool_call: The tool call to validate
|
||||
expected_tools: List of valid tool names
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
if not isinstance(tool_call, dict):
|
||||
return False
|
||||
|
||||
if "function" not in tool_call:
|
||||
return False
|
||||
|
||||
function = tool_call["function"]
|
||||
if not isinstance(function, dict):
|
||||
return False
|
||||
|
||||
if "name" not in function:
|
||||
return False
|
||||
|
||||
return function["name"] in expected_tools
|
||||
|
||||
|
||||
def extract_tool_arguments(
|
||||
tool_call: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract arguments from a tool call.
|
||||
|
||||
Args:
|
||||
tool_call: Tool call dictionary
|
||||
|
||||
Returns:
|
||||
Dictionary of arguments
|
||||
|
||||
Raises:
|
||||
ValueError: If tool call is malformed
|
||||
"""
|
||||
try:
|
||||
return tool_call["function"]["arguments"]
|
||||
except (KeyError, TypeError) as e:
|
||||
raise ValueError(f"Malformed tool call: {e}")
|
||||
|
||||
|
||||
def format_tool_result(
|
||||
tool_name: str,
|
||||
result: Any,
|
||||
error: str | None = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Format a tool execution result.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool that was called
|
||||
result: The result from the tool execution
|
||||
error: Optional error message if execution failed
|
||||
|
||||
Returns:
|
||||
Formatted tool result
|
||||
"""
|
||||
if error:
|
||||
return {
|
||||
"tool": tool_name,
|
||||
"success": False,
|
||||
"error": error,
|
||||
}
|
||||
|
||||
return {
|
||||
"tool": tool_name,
|
||||
"success": True,
|
||||
"result": result,
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
from src import crud, exceptions, models, schemas
|
||||
from src.config import settings
|
||||
from src.dependencies import tracked_db
|
||||
from src.dreamer.dream_scheduler import check_and_schedule_dream
|
||||
from src.agents.dreamer.dream_scheduler import check_and_schedule_dream
|
||||
from src.embedding_client import embedding_client
|
||||
from src.schemas import ResolvedConfiguration
|
||||
from src.utils.formatting import format_datetime_utc
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from sqlalchemy.orm.attributes import flag_modified
|
|||
from src import crud, prometheus, schemas
|
||||
from src.config import settings
|
||||
from src.dependencies import db
|
||||
from src.deriver import enqueue
|
||||
from src.agents.extractor import enqueue
|
||||
from src.exceptions import FileTooLargeError, ResourceNotFoundException
|
||||
from src.security import require_auth
|
||||
from src.utils.files import process_file_uploads_for_messages
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
from src import crud, prometheus, schemas
|
||||
from src.config import settings
|
||||
from src.dependencies import db, tracked_db
|
||||
from src.dialectic.chat import agentic_chat, agentic_chat_stream
|
||||
from src.agents.dialectic.chat import agentic_chat, agentic_chat_stream
|
||||
from src.exceptions import AuthenticationException, ResourceNotFoundException
|
||||
from src.security import JWTParams, require_auth
|
||||
from src.utils.search import search
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
|
||||
from src import config, crud, schemas
|
||||
from src.dependencies import db, tracked_db
|
||||
from src.deriver.enqueue import enqueue_deletion
|
||||
from src.agents.extractor.enqueue import enqueue_deletion
|
||||
from src.exceptions import (
|
||||
AuthenticationException,
|
||||
ResourceNotFoundException,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
from src import crud, models, schemas
|
||||
from src.config import settings
|
||||
from src.dependencies import db
|
||||
from src.deriver.enqueue import enqueue_dream
|
||||
from src.agents.extractor.enqueue import enqueue_dream
|
||||
from src.exceptions import AuthenticationException
|
||||
from src.security import JWTParams, require_auth
|
||||
from src.utils.search import search
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import numpy as np
|
|||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from src.config import settings
|
||||
from src.deriver.prompts import minimal_deriver_prompt
|
||||
from src.agents.extractor.prompts import minimal_deriver_prompt
|
||||
from src.embedding_client import EmbeddingClient
|
||||
from src.utils.clients import honcho_llm_call
|
||||
from src.utils.representation import PromptRepresentation
|
||||
|
|
|
|||
|
|
@ -288,7 +288,7 @@ async def create_active_queue_session(db_session: AsyncSession) -> Callable[...,
|
|||
@pytest.fixture
|
||||
def mock_queue_manager(monkeypatch: pytest.MonkeyPatch) -> AsyncMock: # pyright: ignore[reportUnusedParameter]
|
||||
"""Mock the queue manager to avoid actual queue processing"""
|
||||
from src.deriver.queue_manager import QueueManager
|
||||
from src.agents.extractor.queue_manager import QueueManager
|
||||
|
||||
# Create a mock queue manager
|
||||
mock_manager = AsyncMock(spec=QueueManager)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
|
||||
from src import models
|
||||
from src.config import settings
|
||||
from src.deriver.queue_manager import QueueManager, WorkerOwnership
|
||||
from src.agents.extractor.queue_manager import QueueManager, WorkerOwnership
|
||||
from src.utils.work_unit import construct_work_unit_key
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch
|
|||
|
||||
import pytest
|
||||
|
||||
from src.dreamer.dream_scheduler import DreamScheduler, set_dream_scheduler
|
||||
from src.agents.dreamer.dream_scheduler import DreamScheduler, set_dream_scheduler
|
||||
from src.schemas import DreamType
|
||||
from src.utils.work_unit import construct_work_unit_key
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ from sqlalchemy import func, select
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import crud, models, schemas
|
||||
from src.deriver import enqueue
|
||||
from src.deriver.enqueue import generate_queue_records
|
||||
from src.agents.extractor import enqueue
|
||||
from src.agents.extractor.enqueue import generate_queue_records
|
||||
from src.models import Peer, QueueItem, Workspace
|
||||
|
||||
|
||||
|
|
@ -962,7 +962,7 @@ class TestGetEffectiveObserveMeFunction:
|
|||
|
||||
def test_sender_missing_from_configuration_uses_default(self):
|
||||
"""Test that missing sender uses default observe_me=True"""
|
||||
from src.deriver.enqueue import get_effective_observe_me
|
||||
from src.agents.extractor.enqueue import get_effective_observe_me
|
||||
|
||||
# Empty peer configuration dict simulates sender who left after sending message
|
||||
peers_with_configuration: dict[str, list[dict[str, Any]]] = {}
|
||||
|
|
@ -974,7 +974,7 @@ class TestGetEffectiveObserveMeFunction:
|
|||
|
||||
def test_sender_with_empty_configurations_uses_default(self):
|
||||
"""Test that sender with empty peer and session configs uses default"""
|
||||
from src.deriver.enqueue import get_effective_observe_me
|
||||
from src.agents.extractor.enqueue import get_effective_observe_me
|
||||
|
||||
# Sender present but with empty configurations
|
||||
peers_with_configuration: dict[str, list[dict[str, Any]]] = {
|
||||
|
|
@ -988,7 +988,7 @@ class TestGetEffectiveObserveMeFunction:
|
|||
|
||||
def test_sender_with_peer_config_observe_me_false(self):
|
||||
"""Test that peer config observe_me=False is respected"""
|
||||
from src.deriver.enqueue import get_effective_observe_me
|
||||
from src.agents.extractor.enqueue import get_effective_observe_me
|
||||
|
||||
peers_with_configuration = {
|
||||
"sender": [{"observe_me": False}, {}] # Peer config with observe_me=False
|
||||
|
|
@ -1000,7 +1000,7 @@ class TestGetEffectiveObserveMeFunction:
|
|||
|
||||
def test_session_config_overrides_peer_config(self):
|
||||
"""Test that session peer config takes precedence over peer config"""
|
||||
from src.deriver.enqueue import get_effective_observe_me
|
||||
from src.agents.extractor.enqueue import get_effective_observe_me
|
||||
|
||||
peers_with_configuration = {
|
||||
"sender": [
|
||||
|
|
@ -1015,7 +1015,7 @@ class TestGetEffectiveObserveMeFunction:
|
|||
|
||||
def test_session_config_none_falls_back_to_peer_config(self):
|
||||
"""Test that session config with None observe_me falls back to peer config"""
|
||||
from src.deriver.enqueue import get_effective_observe_me
|
||||
from src.agents.extractor.enqueue import get_effective_observe_me
|
||||
|
||||
peers_with_configuration = {
|
||||
"sender": [
|
||||
|
|
@ -1030,7 +1030,7 @@ class TestGetEffectiveObserveMeFunction:
|
|||
|
||||
def test_mixed_configurations_with_active_status(self):
|
||||
"""Test various configuration combinations with active status"""
|
||||
from src.deriver.enqueue import get_effective_observe_me
|
||||
from src.agents.extractor.enqueue import get_effective_observe_me
|
||||
|
||||
# Test cases: (peer_config, session_config, expected_result)
|
||||
test_cases: list[tuple[dict[str, Any] | None, dict[str, Any] | None, bool]] = [
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ class TestDeriverIngestionMetrics:
|
|||
metric_checker: MetricDeltaChecker,
|
||||
):
|
||||
"""Verify OUTPUT_TOTAL tokens match response.output_tokens from LLM."""
|
||||
from src.deriver.deriver import process_representation_tasks_batch
|
||||
from src.agents.extractor.deriver import process_representation_tasks_batch
|
||||
|
||||
workspace, peer = sample_data
|
||||
session = await create_test_session_with_peer(db_session, workspace, peer)
|
||||
|
|
@ -255,8 +255,8 @@ class TestDeriverIngestionMetrics:
|
|||
metric_checker: MetricDeltaChecker,
|
||||
):
|
||||
"""Verify PROMPT component is tracked for ingestion input."""
|
||||
from src.deriver.deriver import process_representation_tasks_batch
|
||||
from src.deriver.prompts import estimate_minimal_deriver_prompt_tokens
|
||||
from src.agents.extractor.deriver import process_representation_tasks_batch
|
||||
from src.agents.extractor.prompts import estimate_minimal_deriver_prompt_tokens
|
||||
|
||||
workspace, peer = sample_data
|
||||
session = await create_test_session_with_peer(db_session, workspace, peer)
|
||||
|
|
@ -309,7 +309,7 @@ class TestDeriverIngestionMetrics:
|
|||
metric_checker: MetricDeltaChecker,
|
||||
):
|
||||
"""Verify MESSAGES component is tracked for ingestion input."""
|
||||
from src.deriver.deriver import process_representation_tasks_batch
|
||||
from src.agents.extractor.deriver import process_representation_tasks_batch
|
||||
|
||||
workspace, peer = sample_data
|
||||
session = await create_test_session_with_peer(db_session, workspace, peer)
|
||||
|
|
@ -648,7 +648,7 @@ class TestDialecticTokenMetrics:
|
|||
metric_checker: MetricDeltaChecker,
|
||||
):
|
||||
"""Verify INPUT tokens are tracked from LLM response."""
|
||||
from src.dialectic.core import DialecticAgent
|
||||
from src.agents.dialectic.core import DialecticAgent
|
||||
|
||||
workspace, peer = sample_data
|
||||
session = await create_test_session_with_peer(db_session, workspace, peer)
|
||||
|
|
@ -695,7 +695,7 @@ class TestDialecticTokenMetrics:
|
|||
metric_checker: MetricDeltaChecker,
|
||||
):
|
||||
"""Verify OUTPUT tokens are tracked from LLM response."""
|
||||
from src.dialectic.core import DialecticAgent
|
||||
from src.agents.dialectic.core import DialecticAgent
|
||||
|
||||
workspace, peer = sample_data
|
||||
session = await create_test_session_with_peer(db_session, workspace, peer)
|
||||
|
|
@ -743,7 +743,7 @@ class TestDialecticTokenMetrics:
|
|||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
"""Verify metrics are NOT emitted when METRICS_ENABLED=False."""
|
||||
from src.dialectic.core import DialecticAgent
|
||||
from src.agents.dialectic.core import DialecticAgent
|
||||
|
||||
# Explicitly disable metrics
|
||||
monkeypatch.setattr("src.prometheus.METRICS_ENABLED", False)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,190 @@
|
|||
"""
|
||||
Tests for BaseAgent implementation.
|
||||
|
||||
Test criteria:
|
||||
- TC-0B.1: BaseAgent can be instantiated (with mock abstract methods)
|
||||
- TC-0B.2: BaseAgent methods work correctly (run, pre_execute, post_execute, trace_execution)
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from typing import Any, Dict
|
||||
from unittest.mock import MagicMock, AsyncMock
|
||||
|
||||
from src.agents.shared import BaseAgent
|
||||
|
||||
|
||||
class TestAgent(BaseAgent):
|
||||
"""Concrete implementation of BaseAgent for testing."""
|
||||
|
||||
def __init__(self, db, config=None, **kwargs):
|
||||
super().__init__(db, config, **kwargs)
|
||||
self.execution_log = []
|
||||
|
||||
async def execute(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Mock execute implementation."""
|
||||
self.execution_log.append("execute")
|
||||
return {
|
||||
"result": "success",
|
||||
"processed": input_data.get("data", ""),
|
||||
}
|
||||
|
||||
def validate_input(self, input_data: Dict[str, Any]) -> bool:
|
||||
"""Mock validate_input implementation."""
|
||||
self.execution_log.append("validate_input")
|
||||
return "data" in input_data
|
||||
|
||||
|
||||
class TestBaseAgent:
|
||||
"""Test suite for BaseAgent functionality."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db(self):
|
||||
"""Create mock database session."""
|
||||
db = MagicMock()
|
||||
return db
|
||||
|
||||
@pytest.fixture
|
||||
def test_agent(self, mock_db):
|
||||
"""Create TestAgent instance."""
|
||||
return TestAgent(db=mock_db, config=None)
|
||||
|
||||
def test_agent_initialization(self, mock_db):
|
||||
"""TC-0B.1: BaseAgent can be instantiated."""
|
||||
agent = TestAgent(db=mock_db, config=None)
|
||||
|
||||
assert agent.db == mock_db
|
||||
assert agent.config is None
|
||||
assert agent.agent_type == "testagent"
|
||||
assert hasattr(agent, "execution_log")
|
||||
|
||||
def test_agent_initialization_with_config(self, mock_db):
|
||||
"""TC-0B.1: BaseAgent can be instantiated with config."""
|
||||
from src.agents.shared import AgentConfig
|
||||
|
||||
config = AgentConfig(
|
||||
model="gpt-4o-mini",
|
||||
temperature=0.5,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
agent = TestAgent(db=mock_db, config=config)
|
||||
|
||||
assert agent.config == config
|
||||
assert agent.config.model == "gpt-4o-mini"
|
||||
assert agent.config.temperature == 0.5
|
||||
|
||||
def test_agent_initialization_with_kwargs(self, mock_db):
|
||||
"""TC-0B.1: BaseAgent can be instantiated with additional kwargs."""
|
||||
agent = TestAgent(
|
||||
db=mock_db,
|
||||
config=None,
|
||||
custom_param="test_value",
|
||||
another_param=42,
|
||||
)
|
||||
|
||||
assert agent.custom_param == "test_value"
|
||||
assert agent.another_param == 42
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_method(self, test_agent):
|
||||
"""TC-0B.2: Execute method works correctly."""
|
||||
input_data = {"data": "test input"}
|
||||
output = await test_agent.execute(input_data)
|
||||
|
||||
assert output["result"] == "success"
|
||||
assert output["processed"] == "test input"
|
||||
assert "execute" in test_agent.execution_log
|
||||
|
||||
def test_validate_input_method(self, test_agent):
|
||||
"""TC-0B.2: Validate input method works correctly."""
|
||||
valid_input = {"data": "test"}
|
||||
invalid_input = {"wrong_key": "test"}
|
||||
|
||||
assert test_agent.validate_input(valid_input) is True
|
||||
assert test_agent.validate_input(invalid_input) is False
|
||||
assert test_agent.execution_log.count("validate_input") == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pre_execute_hook(self, test_agent):
|
||||
"""TC-0B.2: Pre-execute hook validates input."""
|
||||
valid_input = {"data": "test"}
|
||||
result = await test_agent.pre_execute(valid_input)
|
||||
|
||||
assert result == valid_input
|
||||
assert "validate_input" in test_agent.execution_log
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pre_execute_hook_invalid_input(self, test_agent):
|
||||
"""TC-0B.2: Pre-execute hook raises error for invalid input."""
|
||||
invalid_input = {"wrong_key": "test"}
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid input for testagent agent"):
|
||||
await test_agent.pre_execute(invalid_input)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_execute_hook(self, test_agent, mock_db):
|
||||
"""TC-0B.2: Post-execute hook traces execution."""
|
||||
input_data = {"data": "test"}
|
||||
output = {"result": "success"}
|
||||
|
||||
result = await test_agent.post_execute(input_data, output)
|
||||
|
||||
assert result == output
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trace_execution(self, test_agent):
|
||||
"""TC-0B.2: Trace execution logs correctly."""
|
||||
input_data = {"data": "test"}
|
||||
output = {"result": "success"}
|
||||
metadata = {"execution_time": 1.5}
|
||||
|
||||
# Should not raise any errors
|
||||
await test_agent.trace_execution(input_data, output, metadata)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_pipeline_success(self, test_agent):
|
||||
"""TC-0B.2: Full run pipeline executes correctly."""
|
||||
input_data = {"data": "test input"}
|
||||
|
||||
# Clear execution log
|
||||
test_agent.execution_log = []
|
||||
|
||||
output = await test_agent.run(input_data)
|
||||
|
||||
assert output["result"] == "success"
|
||||
assert output["processed"] == "test input"
|
||||
|
||||
# Verify execution order
|
||||
assert "validate_input" in test_agent.execution_log
|
||||
assert "execute" in test_agent.execution_log
|
||||
assert test_agent.execution_log.index("validate_input") < test_agent.execution_log.index("execute")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_pipeline_validation_failure(self, test_agent):
|
||||
"""TC-0B.2: Run pipeline fails on invalid input."""
|
||||
invalid_input = {"wrong_key": "test"}
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid input for testagent agent"):
|
||||
await test_agent.run(invalid_input)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_pipeline_execution_failure(self, mock_db):
|
||||
"""TC-0B.2: Run pipeline handles execution errors."""
|
||||
|
||||
class FailingAgent(BaseAgent):
|
||||
async def execute(self, input_data):
|
||||
raise RuntimeError("Execution failed")
|
||||
|
||||
def validate_input(self, input_data):
|
||||
return True
|
||||
|
||||
agent = FailingAgent(db=mock_db)
|
||||
|
||||
with pytest.raises(RuntimeError, match="Execution failed"):
|
||||
await agent.run({"data": "test"})
|
||||
|
||||
def test_agent_repr(self, test_agent):
|
||||
"""Test agent string representation."""
|
||||
repr_str = repr(test_agent)
|
||||
assert "TestAgent" in repr_str
|
||||
assert "testagent" in repr_str
|
||||
Loading…
Reference in New Issue