Agents Module (src/agents/)
Overview
The agents module provides LLM-powered advisory capabilities via Anthropic’s Claude API. Agents are read-only: they consume data from the feature store, data adapters, and Redis, and produce structured advisory output. They never have access to BrokerAdapter or any execution path.
Module Structure
src/agents/
__init__.py # Lazy imports
base.py # BaseAgent ABC, AgentContext, AgentResult dataclasses
analyst.py # ClaudeAnalystAgent — filing analysis
advisor.py # TradeAdvisorAgent — trade recommendations
briefing.py # PortfolioReviewAgent — daily briefing
context.py # gather_symbol_context() utility
cost_tracker.py # LLM cost/budget tracking + response caching
rate_limiter.py # Token-bucket rate limiter (Redis-backed)
guardrails.py # guardrail decorator, validate_output(), LLMGuardrailsChecker
sentiment_scorer.py # FinBERTSentimentPipeline (optional dep)
prompts/
analyst.py # Filing analysis system prompt + few-shot examples
advisor.py # Trade recommendation system prompt
briefing.py # Portfolio review system prompt
BaseAgent ABC (src/agents/base.py)
class BaseAgent(ABC):
@property
@abstractmethod
def name(self) -> str: ...
@property
@abstractmethod
def description(self) -> str: ...
@abstractmethod
async def run(self, context: AgentContext) -> AgentResult: ...
def get_token_budget(self) -> int:
return get_settings().agent.max_input_tokens
AgentContext is a dataclass: symbol: str | None, data: dict[str, Any]. AgentResult is a dataclass: output: Any, tokens_used: int, cost_usd: float, metadata: dict.
Async Workflow Pattern
Agents use plain async Python — no LangGraph or LangChain. The advisor workflow is linear:
receive signal + symbol
→ gather_symbol_context()
→ check budget + rate limit
→ check response cache (agent:cache:{hash})
→ call Claude API
→ validate output (guardrails)
→ risk gate check (autonomy mode)
→ store result in Redis
→ return TradeRecommendation
Model Selection (Tiered)
| Agent | Model | Rationale |
|---|---|---|
ClaudeAnalystAgent | claude-sonnet-4-20250514 | Dense financial text requires complex reasoning |
TradeAdvisorAgent | claude-haiku-4-5-20251001 | Speed and cost — many calls per day |
PortfolioReviewAgent | claude-sonnet-4-20250514 | Quality matters — one call per day |
Override via config/settings.yaml:
agent:
analyst_model: claude-sonnet-4-20250514
advisor_model: claude-haiku-4-5-20251001
briefing_model: claude-sonnet-4-20250514
Cost Tracker (src/agents/cost_tracker.py)
- Per-model pricing dict (input/output tokens per 1M)
record_usage(agent_name, model_name, input_tokens, output_tokens) -> LLMUsageRecord- Redis atomic INCRBYFLOAT:
agent:cost:daily:{date},agent:cost:monthly:{month} check_budget() -> bool— True if under budgetreject_if_over_budget()— raises exception if daily or monthly budget exceededget_cached_response(prompt_hash) -> str | Noneandcache_response(prompt_hash, response)compute_prompt_hash(prompt, model, **kwargs) -> str— SHA-256
Rate Limiter (src/agents/rate_limiter.py)
Redis-backed token-bucket rate limiter. Key: agent:ratelimit:{endpoint}:{window} where window = current hour timestamp. check_rate_limit(endpoint, limit_per_hour) -> bool — returns False when exceeded. Callers raise HTTPException(429).
Guardrails (src/agents/guardrails.py)
@guardrail # decorator for BaseAgent.run() methods
async def run(self, context):
...
The guardrail decorator calls validate_output() after every run(). validate_output() scans the output string for broker access patterns (BrokerAdapter, place_order, submit_order, etc.) and raises GuardrailViolationError if found.
LLMGuardrailsChecker.verify() runs a self-test (dirty result blocked, clean result passes) and stores the timestamp in risk:guardrails:last_verified.
Adding a New Agent
- Create
src/agents/my_agent.pysubclassingBaseAgent - Implement
name,description,run(context) -> AgentResult - Decorate
runwith@guardrail - Add prompt template in
src/agents/prompts/my_agent.py - Inject
CostTrackerand callreject_if_over_budget()before the Claude call - Add singleton to
src/api/dependencies.py - Write tests in
tests/unit/test_my_agent.py— mock Claude API responses as dicts matching Anthropic SDK format
FinBERT Sentiment Scorer (src/agents/sentiment_scorer.py)
Optional dependency — requires transformers and torch. If not installed, FinBERTSentimentPipeline logs a warning and returns empty results. All downstream code handles empty sentiment gracefully (XGBoost treats missing features as NaN). The scheduler job checks for availability before running.