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