beacon-trial-finder / CLAUDE.md
KevinIsInCoding
feat: structured eligibility assessment with per-criterion verdicts (#14)
e15864e unverified
|
Raw
History Blame Contribute Delete
4.56 kB
# Beacon β€” Architecture Guide for Claude Code
## Layer Responsibilities
### `app.py` β€” UI wiring only
`app.py` owns **Gradio state and event wiring**. Nothing else.
Allowed:
- `gr.State`, `gr.Chatbot`, `gr.Textbox`, and other Gradio components
- Event handlers that call into `agents.*` and yield streaming updates
- Language switching, layout, and display formatting
Not allowed:
- Direct calls to `trials_api`, `prompts`, `tools`, `models`, or `config`
- Business rules, data transformations, or domain logic
- Any knowledge of what a `PatientProfile` field means
If you find yourself importing a non-agent module into `app.py`, that logic belongs in an agent or a business-logic module instead.
---
### `clinical_trials_guru.py` β€” business wiring only
`clinical_trials_guru.py` owns **orchestration and re-exports**. It is the single public facade for CLI callers and external scripts.
Allowed:
- `BeaconState` TypedDict and LangGraph graph construction (`_build_graph`)
- CLI entry point (`guru_main`)
- Re-exporting symbols from internal modules so callers import one place
Not allowed:
- UI logic, Gradio imports, or display formatting
- New business logic that belongs in `agents/`, `models.py`, `tools.py`, etc.
- Duplicating functions that already live in `agents/`
When you add a new public symbol (function, class, constant), expose it here so the rest of the codebase has one stable import surface.
---
### `agents/` β€” orchestration with streaming support
Each agent module (`intake.py`, `research.py`) provides two variants:
- A **blocking** function (`run_*`) for CLI / LangGraph nodes
- A **streaming generator** (`stream_*`) for the Gradio web UI
Keep agents free of Gradio types. They yield plain strings and return domain objects (`PatientProfile`, `str`).
---
## Design Principle: Load Only Necessary Context
**Never inject all disease benchmarks into context when only one disease is relevant.**
### Rule
Context for a disease-specific benchmark set must not be loaded until the disease is identified. Once the patient's disease is known (via the `identify_disease` tool), load only that disease's profile and pass it to the LLM. Do not pre-load or concatenate profiles for other diseases.
### Rationale
- Each disease JSON carries benchmark definitions, ranges, and guidance text. Loading all 8+ profiles wastes tokens and dilutes the system prompt.
- The `identify_disease` tool call is the earliest reliable signal of disease identity. Everything after that point can be scoped to one profile.
- This keeps context proportional to the task and reduces the risk of cross-disease confusion in the LLM.
### How it works today
`prompts.py` eagerly reads all disease files into `_ALL_DISEASES` at module import, but `lookup_disease_profile(standardized_name)` returns **only one profile** β€” the matched disease. The intake agent calls this function after the `identify_disease` tool fires and injects only the returned benchmarks into the conversation.
### How to stay compliant
- After `identify_disease` resolves, call `lookup_disease_profile(standardized_name)` and use the returned dict exclusively.
- Do not iterate over `_ALL_DISEASES` to build a multi-disease context block.
- If you need to add a new disease, add its JSON under `data/diseases/` β€” do not hardcode benchmark lists in prompts or agents.
- For features that genuinely need cross-disease comparison (e.g., a disease selector UI), load lazily: fetch each profile only when selected, not all at startup.
---
## Module Map
| File | Layer | Imports allowed from |
|---|---|---|
| `app.py` | UI | `agents.*`, `translations`, `models` (type hints only) |
| `clinical_trials_guru.py` | Business facade | Everything |
| `agents/intake.py` | Agent | `models`, `prompts`, `tools`, `config`, `llm`, `translations` |
| `agents/research.py` | Agent | `models`, `prompts`, `tools`, `config`, `llm`, `trials_api`, `translations` |
| `models.py` | Domain model | stdlib, `requests` |
| `prompts.py` | Prompt builder | stdlib, `data/diseases/*.json` |
| `tools.py` | Tool schema loader | stdlib, `data/tools/*.json` |
| `trials_api.py` | External API | stdlib, `models`, `config` |
| `config.py` | Constants | stdlib |
| `translations.py` | i18n | stdlib |
---
## Entry Points
| Entry | File | UI | Flow |
|---|---|---|---|
| Web | `app.py` | Gradio | `intake_greeting` β†’ `stream_intake_turn` β†’ `stream_research_agent` |
| CLI | `main.py` β†’ `clinical_trials_guru.py` | Rich console | LangGraph: `run_intake_agent` β†’ `run_research_agent` |