Skip to content

Commit 4ac5d4c

Browse files
docs: add AGENTS.md for AI agent guidance
Covers essential commands, architecture flow, non-obvious relationships, testing strategy, and CI pipeline gotchas. Co-Authored-By: AdaL <adal@sylph.ai>
1 parent 244c8f3 commit 4ac5d4c

1 file changed

Lines changed: 187 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# AGENTS.md
2+
3+
This file provides guidance to agents (i.e., ADAL) when working with code in this repository.
4+
5+
---
6+
7+
## Essential Commands
8+
9+
### Python (uv — always use `uv run`, never bare `python`/`pytest`)
10+
11+
```bash
12+
# Install all deps including dev
13+
uv sync --extra dev
14+
15+
# Run all tests
16+
uv run pytest tests/ -v --tb=short
17+
18+
# Run a single test
19+
uv run pytest tests/test_api.py::test_analyze_success_mocked -v
20+
21+
# Lint (must pass — CI fails on errors)
22+
uv run ruff check src/ tests/
23+
24+
# Format check (must pass — CI fails)
25+
uv run ruff format --check src/ tests/
26+
27+
# Auto-fix lint + format
28+
uv run ruff check --fix src/ tests/ && uv run ruff format src/ tests/
29+
30+
# Start dev server
31+
uv run uvicorn src.api.main:app --reload --port 8000
32+
```
33+
34+
**Gotcha:** `ruff format --check` is a separate CI step from `ruff check`. Both must pass. Always run both before committing.
35+
36+
**Gotcha:** The `[tool.ruff.lint] ignore = ["E402"]` suppression in `pyproject.toml` is intentional — every agent/data module calls `load_dotenv(override=True)` before imports so API keys are available at module init time. Do not remove this or restructure those imports.
37+
38+
### Docker
39+
40+
```bash
41+
docker build -t rosetta-research-agent:ci .
42+
docker run -d --name rra \
43+
-e GROQ_API_KEY=your_key \
44+
-e ENABLE_IPFS=false \
45+
-p 8000:8000 \
46+
rosetta-research-agent:ci
47+
curl http://localhost:8000/health
48+
```
49+
50+
### Quick API smoke test (server must be running)
51+
52+
```bash
53+
curl -s http://localhost:8000/health | python3 -m json.tool
54+
curl -s http://localhost:8000/api/v1/desks | python3 -m json.tool
55+
curl -s -X POST http://localhost:8000/api/v1/analyze \
56+
-H "Content-Type: application/json" \
57+
-d '{"ticker": "AAPL", "desk": "us"}' | python3 -m json.tool
58+
```
59+
60+
---
61+
62+
## Environment Variables
63+
64+
| Variable | Required | Purpose |
65+
|---|---|---|
66+
| `GROQ_API_KEY` | **Yes** (US + Crypto) | Llama-3.3-70B for US/Crypto desks |
67+
| `DEEPSEEK_API_KEY` | No | China desk (DeepSeek V4 Pro, native Chinese) |
68+
| `GOOGLE_API_KEY` | No | EU + Japan desks (Gemini 2.5 Flash) |
69+
| `TUSHARE_TOKEN` | No | Premium A-share data for China desk |
70+
| `FINANCIAL_DATASETS_API_KEY` | No | SEC filings for US desk |
71+
| `ENABLE_IPFS` | No | Set `"true"` to pin traces |
72+
| `PINATA_JWT` | No | Required if `ENABLE_IPFS=true` |
73+
| `STORACHA_SIDECAR_URL` | No | Default `http://localhost:3030` |
74+
| `ENABLE_ONCHAIN` | No | Arc L1 recording |
75+
76+
CI runs with `GROQ_API_KEY=test-key-placeholder` and `ENABLE_IPFS=false` — all LLM calls are mocked in tests so no real key is needed.
77+
78+
---
79+
80+
## Architecture: Request → InvestmentThesis
81+
82+
```
83+
POST /api/v1/analyze {"ticker": "AAPL", "desk": "us"}
84+
85+
86+
AnalyzeRequest (Pydantic, extra="forbid")
87+
• ticker: allowlist regex [A-Za-z0-9._/-]{1,24}
88+
• desk: must be in VALID_DESKS
89+
• timeout_seconds: ge=15.0, le=600.0
90+
91+
92+
_get_agent(desk) → lazy-import USAgent / ChinaAgent / EUAgent / JapanAgent / CryptoAgent
93+
94+
95+
agent.analyze(ticker) ← asyncio.wait_for() with timeout
96+
┌─────────────────────────────────────────────────────┐
97+
│ RegionalAgent.analyze() [base_agent.py] │
98+
│ 1. get_data_sources(ticker) → raw market data dict │
99+
│ 2. For each sub_agent_role in sub_agent_roles: │
100+
│ • Build prompt via SUB_AGENT_TEMPLATE │
101+
│ • adal.Generator call (LLM) │
102+
│ • PydanticJsonParser → ReasoningBlock │
103+
│ • On malformed JSON: one-shot json_repair pass │
104+
│ 3. Build synthesis prompt (all ReasoningBlocks) │
105+
│ 4. adal.Generator call → InvestmentThesis JSON │
106+
│ 5. PydanticJsonParser strips unknown fields FIRST │
107+
│ then validates (guards against LLM hallucination)│
108+
└─────────────────────────────────────────────────────┘
109+
110+
111+
Optional: MultiPinner.pin() [persistence/multi_pinner.py]
112+
• Serializes with orjson OPT_SORT_KEYS (deterministic bytes → same CID)
113+
• Uploads in parallel to Pinata + Storacha
114+
• Quorum: require=2 — both must succeed
115+
• Asserts both return identical CID (content-addressing invariant)
116+
117+
118+
AnalyzeResponse → stored in _thesis_store[thesis_id] for GET /thesis/{id}
119+
```
120+
121+
---
122+
123+
## Non-Obvious Relationships
124+
125+
### PydanticJsonParser — why it exists
126+
AdalFlow's built-in `JsonOutputParser` requires `adal.DataClass`. Domain models are Pydantic `BaseModel` (FastAPI + web3 interop). `PydanticJsonParser` in `base_agent.py` bridges this: it strips LLM-hallucinated extra fields before Pydantic validation so `extra="forbid"` doesn't crash on novel LLM outputs. **Never bypass this parser or feed raw LLM output directly to Pydantic models.**
127+
128+
### JSON repair flow
129+
If the LLM returns malformed JSON (truncated, trailing commas, etc.), `base_agent.py` runs a deterministic one-shot repair pass before the Pydantic parse. This is intentional and tested — do not remove it. The repair uses `json_repair` library (imported lazily inside the method).
130+
131+
### load_dotenv placement
132+
All agent and data module files call `load_dotenv(override=True)` at module top, **before** `import adalflow as adal`. This is required: AdalFlow's model clients read API keys at import time. Ruff E402 is suppressed for exactly this pattern.
133+
134+
### model_name on agents
135+
`AnalyzeResponse.model_used` is populated via `getattr(agent, "model_name", None)` with an `isinstance(…, str)` guard. In tests, the agent is a `MagicMock` — without the guard `model_used` would receive a `MagicMock` object and fail `AnalyzeResponse` validation.
136+
137+
### _thesis_store
138+
In-memory dict keyed by `thesis_id` (UUID). Not persisted across restarts. Swap for Redis/Postgres in production. Used only by `GET /api/v1/thesis/{id}`.
139+
140+
### Sub-agent roles per desk
141+
Each `RegionalAgent` subclass declares `sub_agent_roles` as a class-level tuple. The base class iterates them sequentially (not in parallel) to maintain reasoning chain coherence. Adding a new role = add to the tuple + handle the role's prompt in `build_synthesis_prompt`.
142+
143+
---
144+
145+
## Key Entry Points
146+
147+
| What | Where |
148+
|---|---|
149+
| FastAPI app + all endpoints | `src/api/main.py` |
150+
| Base agent logic, JSON repair, sanitizers | `src/agents/base_agent.py` |
151+
| US desk (reference implementation) | `src/agents/us_agent.py` |
152+
| All domain schemas (InvestmentThesis, ReasoningBlock, etc.) | `src/reasoning/trace_schema.py` |
153+
| IPFS dual-provider pinning | `src/persistence/multi_pinner.py` |
154+
| All unit tests | `tests/test_api.py` |
155+
| Dependencies + ruff config | `pyproject.toml` |
156+
| Agentalent.ai agent profile | `docs/agentalent_profile.md` |
157+
158+
---
159+
160+
## Testing Strategy
161+
162+
- All 22 tests run **without real API keys** — LLM calls are mocked via `unittest.mock.AsyncMock`
163+
- `_get_agent` is patched at `src.api.main._get_agent` (not at the agent module level)
164+
- Mock agent must be a `MagicMock` (not `AsyncMock`) with `.analyze = AsyncMock(return_value=mock_thesis)`
165+
- `mock_thesis` must be a real `InvestmentThesis` Pydantic instance (not a dict) — `AnalyzeResponse` calls `.direction.value`, `.confidence_score`, `.reasoning_blocks`, `.timestamp.isoformat()`
166+
- The `AsyncMock` coroutine warning in test output is a pytest-asyncio artefact, not a real failure
167+
168+
---
169+
170+
## CI Pipeline (GitHub Actions)
171+
172+
Two jobs — both must pass:
173+
174+
1. **test** (matrix: Python 3.12 + 3.13)
175+
- `uv sync --extra dev`
176+
- `ruff check src/ tests/`**hard fail**
177+
- `ruff format --check src/ tests/`**hard fail**
178+
- `pytest -v --tb=short tests/`
179+
180+
2. **docker** (needs: test)
181+
- `docker build -t rosetta-research-agent:ci .`
182+
- Container smoke test: starts container, `sleep 8`, `curl --fail http://localhost:8000/health`
183+
184+
**Common CI failure causes:**
185+
- Formatting drift → run `uv run ruff format src/ tests/` locally before push
186+
- New unused import → `uv run ruff check --fix src/ tests/`
187+
- Docker: Dockerfile uses `pip install -e ".[full]"` — if a new dep in `[full]` extras has a build dependency, add it to the `RUN apt-get` step

0 commit comments

Comments
 (0)