Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Each specialized agent inherits from `BaseAgent`, which defines the shared execu

### Agent-specific documentation

- [Add more agents](docs/agents/add-agents-md)
- [Chat Agent](docs/agents/chat-agent.md)
- [CRM Agent](docs/agents/crm-agent.md)
- [Finance Agent](docs/agents/finance-agent.md)
Expand Down
91 changes: 91 additions & 0 deletions app/src/agents/docs/agents/add-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
This guide explains how to expand **Perry's** capabilities by adding new specialized agents. The architecture is designed to be modular, relying on a base class, a centralized factory and a semantic router.

## 1: Create the agent class

Every new agent must reside in `ingest-api/app/src/agents/` and inherit from **`BaseAgent`**. You must implement the `act()` method as an **`AsyncGenerator`** to support the "Thinking" (reasoning) steps and the final response.

**Example: `ingest-api/app/src/agents/marketing_agent.py`**
```python
import json
import re
import ast
import logfire
from contextlib import asynccontextmanager
from typing import Optional, AsyncGenerator, Union
from pydantic import BaseModel, ValidationError, Field
from pydantic_ai import Agent
from src.agents.base_agent import BaseAgent, Message, Failed, AgentState, PerryThought

class MarketingAgent(BaseAgent):
async def act(self) -> AsyncGenerator[Union[PerryThought, Message, Failed], None]:
# 1. Emit reasoning steps
yield PerryThought(content="Analyzing brand guidelines...", internal_step="check_brand")

# 2. Logic...

# 3. Return final result
yield Message(
sender=self.state.agent_name,
receiver="user",
content="Marketing campaign drafted successfully."
)
```

---

## 2: Register in the agent factory

The **`AgentFactory`** handles the instantiation of agents and manages their state to avoid circular imports and unnecessary memory overhead. Update `ingest-api/app/src/services/agent_factory.py`:

1. Add a new `elif` block for your agent.
2. Import the class **inside** the method to prevent circular dependencies.

```python
def get_or_create_agent(self, agent_name: str) -> BaseAgent:
if agent_name == "finance_agent":
from src.agents.finance_agent import FinanceAgent
agent_class = FinanceAgent
# ... other agents ...
elif agent_name == "marketing_agent": # Add your new agent here
from src.agents.marketing_agent import MarketingAgent
agent_class = MarketingAgent
else:
raise ValueError(f"Agent '{agent_name}' is not registered.")

state = AgentState(agent_name=agent_name)
self.instances[agent_name] = agent_class(state=state)
return self.instances[agent_name]
```

## 3: Configure semantic routing

To ensure Perry knows when to trigger your new agent, you must configure the **Semantic Router**.

### A. Update `utterances.json`
Add a list of example phrases (utterances) that describe the user's intent when they want to talk to your new agent.

```json
{
"marketing_agent": [
"Create a marketing campaign",
"Draft a social media post",
"What is our current brand strategy?",
"Help me with marketing materials"
]
}
```

### B. Update `semantic_router_service.py`
Define the new route and add it to the router's active layers.

```python
# 1. Define the route using the JSON data
marketing_route = Route(
name="marketing_agent",
utterances=utterances_data["marketing_agent"]
)

# 2. Add it to the routes list
routes = [finance_route, crm_route, calendar_route, chat_route, marketing_route]
route_layer = SemanticRouter(encoder=encoder, routes=routes, auto_sync="local")
```
Loading