|
| 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