Spaces:
Sleeping
Sleeping
KevinIsInCoding
feat: structured eligibility assessment with per-criterion verdicts (#14)
e15864e unverified | # 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` | | |