From 1fb55db9f93e3df8b67f7d9e0010f17d18aecd01 Mon Sep 17 00:00:00 2001 From: Jim Schaff Date: Wed, 1 Jul 2026 11:26:12 -0400 Subject: [PATCH 1/5] refactor: move backend tests out of app/ and fix pytest/CI import path Relocate the test suite from backend/app/tests/ (inside the source package) to a top-level backend/tests/ directory, and make imports work without per-file sys.path hacks. - Move backend/app/tests/ -> backend/tests/ - Add [tool.pytest.ini_options] pythonpath = ["."] to backend/pyproject.toml so `app` is importable with rootdir at backend/ - Remove the `sys.path.insert(...)` hack and unused `sys`/`os` imports from both test files (their removal from test_llms_service.py had been breaking `pytest` collection with ModuleNotFoundError: No module named 'app') - CI: run pytest from backend/ (was backend/app/) and write the secrets .env to backend/.env so config.py's env_file=".env" resolves it - Add CLAUDE.md documenting architecture, commands, auth, and this layout Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XpNzobVi83p3YKL9sqtGwZ --- .github/workflows/ci.yml | 32 +++---- CLAUDE.md | 86 +++++++++++++++++++ backend/pyproject.toml | 5 ++ backend/{app => }/tests/__init__.py | 0 backend/{app => }/tests/test_llms_service.py | 4 - .../{app => }/tests/test_vcelldb_service.py | 4 - 6 files changed, 107 insertions(+), 24 deletions(-) create mode 100644 CLAUDE.md rename backend/{app => }/tests/__init__.py (100%) rename backend/{app => }/tests/test_llms_service.py (96%) rename backend/{app => }/tests/test_vcelldb_service.py (95%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33224e7..26f0c59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,27 +36,27 @@ jobs: - name: Create .env file with GitHub Secrets run: | - echo "FRONTEND_URL=${{ secrets.FRONTEND_URL }}" >> backend/app/.env + echo "FRONTEND_URL=${{ secrets.FRONTEND_URL }}" >> backend/.env # LLM Provider - echo "PROVIDER=${{ secrets.PROVIDER }}" >> backend/app/.env - + echo "PROVIDER=${{ secrets.PROVIDER }}" >> backend/.env + # Azure OpenAI Configuration - echo "AZURE_API_VERSION=${{ secrets.AZURE_API_VERSION }}" >> backend/app/.env - echo "AZURE_ENDPOINT=${{ secrets.AZURE_ENDPOINT }}" >> backend/app/.env - echo "AZURE_API_KEY=${{ secrets.AZURE_API_KEY }}" >> backend/app/.env - echo "AZURE_DEPLOYMENT_NAME=${{ secrets.AZURE_DEPLOYMENT_NAME }}" >> backend/app/.env - echo "AZURE_EMBEDDING_DEPLOYMENT_NAME=${{ secrets.AZURE_EMBEDDING_DEPLOYMENT_NAME }}" >> backend/app/.env - + echo "AZURE_API_VERSION=${{ secrets.AZURE_API_VERSION }}" >> backend/.env + echo "AZURE_ENDPOINT=${{ secrets.AZURE_ENDPOINT }}" >> backend/.env + echo "AZURE_API_KEY=${{ secrets.AZURE_API_KEY }}" >> backend/.env + echo "AZURE_DEPLOYMENT_NAME=${{ secrets.AZURE_DEPLOYMENT_NAME }}" >> backend/.env + echo "AZURE_EMBEDDING_DEPLOYMENT_NAME=${{ secrets.AZURE_EMBEDDING_DEPLOYMENT_NAME }}" >> backend/.env + # Qdrant Configuration - echo "QDRANT_URL=${{ secrets.QDRANT_URL }}" >> backend/app/.env - echo "QDRANT_COLLECTION_NAME=${{ secrets.QDRANT_COLLECTION_NAME }}" >> backend/app/.env - + echo "QDRANT_URL=${{ secrets.QDRANT_URL }}" >> backend/.env + echo "QDRANT_COLLECTION_NAME=${{ secrets.QDRANT_COLLECTION_NAME }}" >> backend/.env + # Langfuse Configuration - echo "LANGFUSE_SECRET_KEY=${{ secrets.LANGFUSE_SECRET_KEY }}" >> backend/app/.env - echo "LANGFUSE_PUBLIC_KEY=${{ secrets.LANGFUSE_PUBLIC_KEY }}" >> backend/app/.env - echo "LANGFUSE_HOST=${{ secrets.LANGFUSE_HOST }}" >> backend/app/.env + echo "LANGFUSE_SECRET_KEY=${{ secrets.LANGFUSE_SECRET_KEY }}" >> backend/.env + echo "LANGFUSE_PUBLIC_KEY=${{ secrets.LANGFUSE_PUBLIC_KEY }}" >> backend/.env + echo "LANGFUSE_HOST=${{ secrets.LANGFUSE_HOST }}" >> backend/.env - name: Run Tests with Poetry run: | - cd backend/app + cd backend poetry run pytest tests/ --maxfail=1 --disable-warnings -q diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..77aaf44 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,86 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +VCell-AI is an AI-powered web application for discovering, analyzing, and exploring biomodels from the VCell database. It combines a Next.js 15 frontend with a FastAPI backend, using Qdrant for vector search and OpenAI/Azure for LLM capabilities (with Langfuse for observability). + +## Architecture + +**Monorepo with two main services:** + +- **`frontend/`** — Next.js 15 (App Router), TypeScript, Tailwind CSS, ShadCN/Radix UI components +- **`backend/`** — FastAPI (Python 3.12+), Poetry for dependency management, routes → controllers → services layered architecture + +**Backend layers follow:** `routes/` (API endpoints) → `controllers/` (HTTP mapping + `HTTPException` handling) → `services/` (business logic, external API calls). Controllers call services directly. FastAPI `Depends()` is used only for **auth** (see below), not for wiring services. Pydantic request models live in `schemas/`; many responses are plain dicts. Everything is `async`. + +**Key backend routers** (registered in `app/main.py`; note only two have URL prefixes): +- `vcelldb_router` — mounted at **root** (e.g. `/biomodel`, `/biomodel/{bmId}/biomodel.vcml`). Proxies the VCell API for biomodels, simulations, VCML/SBML files, diagrams, publications. +- `llms_router` — mounted at **root**. LLM queries with tool calling, biomodel analysis, VCML/diagram analysis. **All routes require a valid Auth0 bearer token** via `Depends(verify_auth0_token)`. +- `knowledge_base_router` — prefix `/kb`. Knowledge base CRUD (PDF/text upload, collection management, similarity search). +- `qdrant_router` — prefix `/qdrant`. Direct vector database operations. +- `users_router` — mounted at **root**. `POST /users/me` verifies the Auth0 token and upserts the user into Supabase (`sync_auth0_user`). + +**Auth (Auth0 + Supabase):** Backend verifies Auth0 RS256 JWTs in `core/auth.py` — `verify_auth0_token` (a FastAPI dependency) fetches Auth0's JWKS via `PyJWKClient` (cached lazily) and validates audience/issuer. User rows are synced into Supabase (`services/users_service.py`, `users` table, upsert on `auth0_sub`). The frontend uses `@auth0/nextjs-auth0`: `middleware.ts` gates all non-public routes (only `/` and `/auth/*` are public) and redirects to `/auth/login`; `lib/auth0.ts` configures the client; `components/auth-sync.tsx` calls `POST /users/me` with the access token after login. Frontend `fetch()` calls to protected backend routes must include `Authorization: Bearer ` (from `getAccessToken()`). + +**Frontend page routes:** +- `/chat` — AI chatbot interface +- `/search/[bmid]` — Biomodel search results +- `/analyze/[id]` — Model analysis tools (VCML, diagram, simulation analysis) +- `/diagrams`, `/sbml`, `/vcml`, `/bngl` — File format viewers +- `/admin/knowledge-base` — KB management dashboard + +**Infrastructure:** Docker Compose orchestrates frontend (port 3000), backend (port 8000), and Qdrant (port 6333). Langfuse is available as a git submodule with its own docker-compose. + +## Build & Development Commands + +### Frontend (`frontend/`) +```bash +npm install # Install dependencies +npm run dev # Dev server at http://localhost:3000 +npm run build # Production build +npm run start # Start production server +npm run lint # ESLint +``` + +### Backend (`backend/`) +```bash +poetry install --no-root # Install dependencies +cd backend && poetry run uvicorn app.main:app --reload # Dev server at http://localhost:8000 +cd backend && poetry run pytest tests/ # Run all tests (run from backend/; tests live in backend/tests/) +cd backend && poetry run pytest tests/test_vcelldb_service.py # Single test file +cd backend && poetry run pytest tests/test_llms_service.py::TestLLMsService::test_analyse_vcml_success # Single test +``` + +### Docker (full stack) +```bash +docker compose up --build -d # Start all services +docker compose down # Stop all services +``` + +## Environment Variables + +**Backend** (`backend/.env`) — loaded by Pydantic `BaseSettings` (`backend/app/core/config.py`, `env_file=".env"`), resolved relative to the working directory; run the backend and tests from `backend/` so it's found. Keys: `FRONTEND_URL`, `PROVIDER` (azure/local), Azure OpenAI keys (`AZURE_API_VERSION`, `AZURE_ENDPOINT`, `AZURE_API_KEY`, `AZURE_DEPLOYMENT_NAME`, `AZURE_EMBEDDING_DEPLOYMENT_NAME`), Qdrant config (`QDRANT_URL`, `QDRANT_COLLECTION_NAME`), Langfuse keys (`LANGFUSE_SECRET_KEY`, `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_HOST`), Auth0 (`AUTH0_DOMAIN`, `AUTH0_AUDIENCE`) and Supabase (`SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`) — the last four are `Optional` in `config.py` but required at runtime for auth/user-sync to work. `config.py` uses `SettingsConfigDict(env_file=".env", extra="ignore")`. See `backend/.env.example`. + +**Frontend** (`frontend/.env`): `NEXT_PUBLIC_API_URL` (backend URL), plus the standard `@auth0/nextjs-auth0` v4 server vars (`AUTH0_SECRET`, `APP_BASE_URL`, `AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`, `AUTH0_CLIENT_SECRET`, and `AUTH0_AUDIENCE` — read in `lib/auth0.ts`). Note: there is currently **no `frontend/.env.example`** committed. + +## Key Implementation Details + +- The LLM system prompt (`backend/app/utils/system_prompt.py`) defines the VCell BioModel Assistant persona +- LLM tool calling: tool definitions live in `backend/app/utils/tools_utils.py` (`fetch_biomodels_tool`, `fetch_simulation_details_tool`, `search_knowledge_base_tool`), dispatched by `execute_tool`; schemas in `schemas/tool_schema.py` +- OpenAI, Qdrant, and Supabase clients are **module-level singletons** in `backend/app/core/singleton.py` (`get_openai_client`, `get_qdrant_client`, `get_supabase_client`) — grab them via the `get_*` accessors, don't re-instantiate +- Services are wrapped with Langfuse `@observe` for LLM/tracing observability; the OpenAI client is the Langfuse-wrapped Azure client +- Knowledge base uses RAG via Qdrant vector search: LangChain text splitter + Azure OpenAI embeddings; collection is auto-created on FastAPI startup (`app/main.py` `startup_event`). `populate_db.ipynb` seeds it +- Frontend talks to the backend via bare `fetch()` calls in client components (no shared API client, no React Query/SWR); base URL comes from `NEXT_PUBLIC_API_URL` +- Frontend uses KaTeX (`rehype-katex`/`remark-math`) for math rendering; forms use react-hook-form + Zod; UI is ShadCN/Radix +- ESLint, TypeScript errors, and image optimization are all disabled during Next.js builds (`next.config.mjs`) +- CORS is open to all origins in the FastAPI backend + +## Contributing + +- Use conventional commits: `feat:`, `fix:`, `docs:`, `style:`, `refactor:`, `test:`, `chore:` +- Create feature branches from `main` +- Tests live in `backend/tests/` and run from `backend/`. Import resolution relies on `[tool.pytest.ini_options] pythonpath = ["."]` in `backend/pyproject.toml` (rootdir = `backend/`, so `app` is importable) — no `sys.path` hacks in test files +- CI (`.github/workflows/ci.yml`, "Backend CI") runs `poetry run pytest tests/ --maxfail=1 --disable-warnings -q` from `backend/` on push/PR to `main`; env is injected from GitHub Secrets into `backend/.env`. No frontend job. No linter/formatter step runs in CI (`black` is a dev dep but unconfigured) +- Clone with `--recurse-submodules` to get the Langfuse submodule diff --git a/backend/pyproject.toml b/backend/pyproject.toml index bf716fa..373f03b 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -33,6 +33,11 @@ pytest = "^8.4.0" httpx = "^0.28.1" pytest-asyncio = "^1.0.0" +[tool.pytest.ini_options] +# rootdir resolves to this file's dir (backend/), so "." puts backend/ on +# sys.path and makes `import app` work regardless of the cwd pytest runs from. +pythonpath = ["."] + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/backend/app/tests/__init__.py b/backend/tests/__init__.py similarity index 100% rename from backend/app/tests/__init__.py rename to backend/tests/__init__.py diff --git a/backend/app/tests/test_llms_service.py b/backend/tests/test_llms_service.py similarity index 96% rename from backend/app/tests/test_llms_service.py rename to backend/tests/test_llms_service.py index 15bf549..0641db5 100644 --- a/backend/app/tests/test_llms_service.py +++ b/backend/tests/test_llms_service.py @@ -1,12 +1,8 @@ -import sys -import os import pytest # This tells pytest that all tests in the file should run in asyncio mode. pytestmark = pytest.mark.asyncio -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - from app.services.llms_service import ( get_llm_response, get_response_with_tools, diff --git a/backend/app/tests/test_vcelldb_service.py b/backend/tests/test_vcelldb_service.py similarity index 95% rename from backend/app/tests/test_vcelldb_service.py rename to backend/tests/test_vcelldb_service.py index a55e024..a3329fa 100644 --- a/backend/app/tests/test_vcelldb_service.py +++ b/backend/tests/test_vcelldb_service.py @@ -1,12 +1,8 @@ -import sys -import os import pytest # This tells pytest that all tests in the file should run in asyncio mode. pytestmark = pytest.mark.asyncio -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) - from app.services.vcelldb_service import ( fetch_biomodels, fetch_simulation_details, From 0e2b706d78efc0d55d43de356e07203b4db6d4b6 Mon Sep 17 00:00:00 2001 From: Jim Schaff Date: Wed, 1 Jul 2026 11:27:11 -0400 Subject: [PATCH 2/5] docs: add architecture and development branch reference docs Add docs/architecture.md (design goals, ADR template) and docs/development_branch.md (summary of the development branch's BMDB integration work vs. main). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XpNzobVi83p3YKL9sqtGwZ --- docs/architecture.md | 199 +++++++++++++++++++++++++++++++++++++ docs/development_branch.md | 125 +++++++++++++++++++++++ 2 files changed, 324 insertions(+) create mode 100644 docs/architecture.md create mode 100644 docs/development_branch.md diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..8ff657d --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,199 @@ +# VCell-AI Architecture + +## Design Goals + +- Provide an AI-powered conversational interface for discovering, analyzing, and exploring biomodels in the VCell database +- Allow researchers to ask natural-language questions and receive technically precise, citation-backed answers about computational biology models +- Integrate a knowledge base (RAG) so the assistant can draw on VCell documentation and literature, not just the biomodel database +- Keep the system observable end-to-end via Langfuse tracing + +## Architectural Decisions + + + +### ADR-001: Placeholder + +_No architectural decision records have been captured yet. Add entries here as significant choices are made (e.g., switching LLM providers, changing the chunking strategy, adding streaming)._ + +## System Overview + +``` +┌──────────────┐ JSON/REST ┌──────────────────────┐ +│ Frontend │ ◄──────────────────────► │ Backend │ +│ Next.js 15 │ port 3000 → 8000 │ FastAPI │ +│ TypeScript │ │ Python 3.12 │ +└──────────────┘ └───────┬──────────────┘ + │ + ┌─────────────┼─────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌────────────┐ + │ Azure │ │ VCell │ │ Qdrant │ + │ OpenAI │ │ API │ │ Vector DB │ + └──────────┘ └──────────┘ └────────────┘ + │ + ▼ + ┌──────────┐ + │ Langfuse │ + │ Tracing │ + └──────────┘ +``` + +### Frontend (Next.js 15, App Router) + +Key pages: + +| Route | Purpose | +|---|---| +| `/chat` | General-purpose AI chatbot | +| `/search/[bmid]` | Biomodel search results | +| `/analyze/[id]` | Deep analysis of a single biomodel (diagram, VCML, follow-up Q&A) | +| `/admin/knowledge-base` | Upload/manage documents in the RAG knowledge base | +| `/diagrams`, `/sbml`, `/vcml` | File-format viewers (BNGL is offered as a download from `/sbml` and `/search/[bmid]`, not its own page) | + +The main interactive component is `ChatBox` (`components/ChatBox.tsx`). It manages conversation state locally, sends the full conversation history to the backend on each turn, and post-processes the response to turn biomodel IDs into clickable links. + +### Backend (FastAPI) + +Layered as **routes → controllers → services**: + +- **Routes** (`app/routes/`) — HTTP endpoint definitions and request parsing +- **Controllers** (`app/controllers/`) — orchestration and error handling +- **Services** (`app/services/`) — business logic and external API calls + +Route groups (only `/kb` and `/qdrant` are mounted under a prefix in `main.py`; the LLM and VCellDB routers are mounted at the root): + +| Group | Mount | Representative endpoints | +|---|---|---| +| LLM | (no prefix) | `POST /query`, `POST /analyse/{id}`, `POST /analyse/{id}/vcml`, `POST /analyse/{id}/diagram` | +| VCellDB | (no prefix) | `GET /biomodel`, `GET /biomodel/{id}/simulations`, `GET /biomodel/{id}/biomodel.vcml`, `GET /biomodel/{id}/biomodel.sbml`, `GET /biomodel/{id}/diagram`, `GET /publications` | +| Knowledge base | `/kb` | `POST /kb/upload-pdf`, `POST /kb/upload-text`, `GET /kb/files`, `GET /kb/similar` | +| Qdrant | `/qdrant` | `POST /qdrant/collection`, `POST /qdrant/points`, `POST /qdrant/search` | + +Singletons (`app/core/singleton.py`) hold the Azure OpenAI client (wrapped by Langfuse for automatic tracing) and the Qdrant client. + +### External Dependencies + +- **Azure OpenAI** — LLM completions and text embeddings (model names configured via env vars) +- **VCell API** (`vcell.cam.uchc.edu/api/v0`) — authoritative source for biomodel metadata, VCML/SBML files, diagrams, and publications +- **Qdrant** — vector database for knowledge-base embeddings; runs as a Docker service on port 6333 +- **Langfuse** — observability platform; every LLM call and service function is traced via `@observe` decorators and the Langfuse-wrapped OpenAI client + +## Request / Response Flows + +### Chat query with tool calling + +This is the primary interaction pattern. The LLM decides which tools to call based on the user's question, executes them, then synthesizes a final answer. + +``` +User types message in ChatBox + │ + ▼ +Frontend builds conversation_history array +(all prior messages including system context) + │ + ▼ +POST /query { conversation_history: [...] } + │ + ▼ +llms_router → llms_controller → llms_service.get_response_with_tools() + │ + ▼ + ┌──────────────────────────────────────────────┐ + │ LLM Call #1: "Retrieve Tools" │ + │ (system_prompt + history + tool definitions) │ + │ tool_choice = "auto" │ + └──────────────────┬───────────────────────────┘ + │ + LLM returns tool_calls[] + │ + ▼ + ┌──────────────────────────────────────────────┐ + │ Tool Execution Loop │ + │ For each tool_call: │ + │ execute_tool(name, args) │ + │ ├─ fetch_biomodels → VCell API │ + │ ├─ fetch_simulation_details → VCell API │ + │ ├─ get_vcml_file → VCell API │ + │ ├─ fetch_publications → VCell API │ + │ └─ search_vcell_knowledge_base │ + │ → embed query → Qdrant search │ + │ │ + │ Append each result as a tool message │ + └──────────────────┬───────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────────────┐ + │ LLM Call #2: "Process Tool Results" │ + │ (full message history including tool output)│ + └──────────────────┬───────────────────────────┘ + │ + ▼ +Return { response: "...", bmkeys: ["id1", "id2", ...] } + │ + ▼ +Frontend formats biomodel IDs as links → renders with MarkdownRenderer +``` + +**Notes:** +- There is no streaming; the frontend shows a loading spinner until the full response arrives. +- Biomodel keys (`bmkeys`) are extracted from tool results so the frontend can hyperlink them. +- VCML content is sanitized (ImageData tags stripped) before being sent to the LLM. +- VCML fetch has retry logic with exponential backoff (up to 3 attempts). + +### Biomodel analysis page + +When a user navigates to `/analyze/[id]`, three requests fire in parallel: + +``` +GET /biomodel?bmId={id} → biomodel metadata +POST /analyse/{id}/diagram → LLM vision analysis of the diagram image +POST /analyse/{id}?user_prompt=... → LLM analysis of the biomodel + VCML +``` + +The results seed the ChatBox with an initial analysis. The user can then ask follow-up questions or use quick actions (e.g., "Describe parameters", "Describe species") which route through the same tool-calling flow above, with a prompt prefix scoping the question to the specific biomodel. + +### Knowledge-base ingestion (RAG) + +``` +POST /kb/upload-pdf (multipart file upload) + │ + ▼ +Extract text with MarkItDown (PDF → Markdown) + │ + ▼ +Chunk with RecursiveCharacterTextSplitter + (chunk_size=1250, overlap=250) + │ + ▼ +For each chunk: + embed_text(chunk) → Azure OpenAI embeddings (1536-dim) + upsert to Qdrant with payload { file_name, chunk, chunk_index, total_chunks } +``` + +When the LLM calls `search_vcell_knowledge_base` during a chat: + +``` +embed_text(query) → cosine similarity search in Qdrant → top-k chunks returned as tool result +``` + +## LLM Tools + +Five tools are available to the LLM (defined in `app/utils/tools_utils.py`, all with `strict=True` JSON schemas): + +| Tool | Purpose | +|---|---| +| `fetch_biomodels` | Search/filter VCell biomodel database by name, owner, category, date range | +| `fetch_simulation_details` | Get simulation data for a specific biomodel | +| `get_vcml_file` | Retrieve and sanitize the VCML model definition | +| `fetch_publications` | Get VCell-related publications | +| `search_vcell_knowledge_base` | RAG search over uploaded documents in Qdrant | + +The system prompt (`app/utils/system_prompt.py`) instructs the LLM on when and how to use each tool — for example, always calling `get_vcml_file` when asked to describe parameters, species, or reactions. diff --git a/docs/development_branch.md b/docs/development_branch.md new file mode 100644 index 0000000..ad5cefe --- /dev/null +++ b/docs/development_branch.md @@ -0,0 +1,125 @@ +# Development Branch Summary + +Summary of the `development` branch contribution relative to `main`. This branch adds BioModels Database (BMDB) integration as a second data source alongside the existing VCell database, along with performance optimizations to the LLM tool-calling pipeline. + +**Stats:** 60 commits, 26 files changed, ~1,667 lines added, ~254 lines removed (vs. `origin/main`). + +## Features + +### 1. BioModels Database (BMDB) Integration + +The core feature is a second biomodel data source — the EBI BioModels Database (`biomodels.org`) — available alongside the existing VCell database (VCDB). + +**Backend:** +- New route group (`bmdb_router.py`) with endpoints: `GET /search`, `GET /get-xml`, `GET /model-info` +- New controller (`bmdb_controller.py`) and schema (`bmdb_schema.py` with `BMDBRequestParams`) +- Three new service functions in `databases_service.py`: + - `fetch_bmdb_models()` — searches BioModels via `biomodels.org/search?query=...&format=json` + - `get_xml_file()` — downloads SBML XML for a BIOMD/MODEL ID with retry logic + - `get_bmdb_model_info()` — fetches model metadata by ID +- New LLM endpoint `POST /bmdb-search` for BMDB-specific queries with tool calling +- Two new LLM tools: `fetch_bmdb_models` and `get_xml_file` (in a separate `BMDB_TOOLS` list) +- `get_response_with_tools()` now takes a `database` parameter ("vcdb" or "bmdb") to select which tool set the LLM receives + +**Frontend:** +- `ChatBox` component extended with database selection checkboxes (VCell DB / BioModels DB) +- Dual-send capability: when both databases are selected, queries go to both `/query` and `/bmdb-search` in parallel +- Separate `handleSendMessage()` (VCDB) and `handleSendMessageBMDB()` (BMDB) functions +- Biomodel ID linking adapted per database: VCDB links go to `/search/{id}`, BMDB links go to `biomodels.org/{id}` +- Quick action buttons change based on selected database(s): `VCellActions`, `bmdbActions`, or combined +- `/search/[bmid]` page detects BMDB IDs (prefix `BIOMD` or `MODEL`) and renders a BMDB-specific detail view with: + - Model name, author, description (parsed from XML via `DOMParser`) + - Downloadable files list + - Diagram image (fetched as PNG from BMDB, converted to base64) + - AI Analysis tab with BMDB-scoped ChatBox +- `/search` page updated with a new search box and results rendering for BMDB queries +- `/chat` page updated with database-specific quick action sections + +### 2. LLM Response Time Optimizations + +Several changes target reducing latency in the tool-calling pipeline: + +- **Tool subset selection:** Instead of sending all tools to the LLM, `select_tools_for_prompt()` uses regex pattern matching on the user's message to choose a relevant subset (`DB_TOOLS`, `KB_TOOLS`, `PUB_TOOLS`) +- **Direct chat path:** `should_use_tools()` detects simple conversational prompts (e.g., "summarize this") and bypasses tool calling entirely via `_direct_chat_completion()` +- **Concurrent tool execution:** Tool calls are now executed with `asyncio.gather()` instead of sequentially +- **Result summarization:** `summarize_tool_result()` truncates model descriptions to 200 chars and limits to 5 models to reduce token count in the second LLM call +- **Reduced default result count:** `maxRows` lowered from 1000 to 25 (with min=1, max=50) +- **Conversation history trimming:** Frontend sends only system messages + last 5 non-system messages instead of the full history + +### 3. UI Enhancements + +- **Stop button:** Aborts in-flight requests using `AbortController` +- **Conversation persistence:** Chat history saved to `localStorage`, conversations listed in sidebar, loadable via `?conversation=` query param +- **Sidebar updates:** Real-time conversation list, loaded from `localStorage` on `conversation-updated` events +- **Assistant message width:** Changed from `max-w-[80%]` to `w-full` for better readability +- **Tool timing summary:** Each response includes timing breakdown (tool selection, execution, final LLM call, total) displayed in the chat + +### 4. Minor Changes + +- Renamed `vcelldb_service.py` to `databases_service.py` to generalize for multiple data sources +- Renamed `supplementalActions` prop to `VCellActions` +- Date filter parameters (`savedLow`, `savedHigh`) commented out throughout +- Admin settings page updated with setup instructions +- `Suspense` wrapper added around chat page for `useSearchParams` compatibility +- System prompt broadened from "VCell BioModel Assistant" to "mathematical modeler in biology", then **split into three files** (`system_prompt.py` shared, `vcdb_system_prompt.py`, `bmdb_system_prompt.py`) so each path can be tuned independently +- System prompt adds explicit formatting templates for VCDB and BMDB biomodel listings, plus a rule that any model element with an `identifiers.org` link must render as an underlined hyperlink (and only those elements should be hyperlinked) +- Frontend logs the number of rows fetched for biomodel list queries +- Added `NEXT_PUBLIC_API_URL_BMDB` environment variable for direct BMDB API calls from frontend + +## Code Style Observations + +- Significant amount of debug logging (`print("DEBUG20: ...")`, `console.log("PPPPPP: ...")`) remains in the code — intended for development, should be cleaned up before merge +- Commented-out code blocks throughout (`// old implementation`, `# "savedLow"`, etc.) rather than being removed +- `handleSendMessageBMDB()` is largely a copy of `handleSendMessage()` with BMDB-specific changes — these share ~80% of their logic +- Inconsistent indentation in some Python files (mix of 3-space and 4-space in `tools_utils.py` and `llms_service.py`) +- Some unused imports added (`from multiprocessing import process`, `import requests` in `llms_router.py`) + +## Validation + +- No new tests were added for the BMDB integration +- The existing test file (`test_vcelldb_service.py`) had its import updated from `vcelldb_service` to `databases_service` +- CI workflow (`ci.yml`) runs existing backend tests on push/PR to main +- The branch has not been validated against the CI pipeline for BMDB-specific functionality + +## Architecture Impact + +``` + ┌──────────────────────┐ + │ ChatBox │ + │ ┌────────────────┐ │ + │ │ useVCDB toggle │ │ + │ │ useBMDB toggle │ │ + │ └───┬────────┬───┘ │ + └──────┼────────┼──────┘ + │ │ + POST /query │ │ POST /bmdb-search + ▼ ▼ + ┌──────────────────────┐ + │ llms_router.py │ + │ database="vcdb" │ + │ database="bmdb" │ + └──────────┬───────────┘ + │ + ┌──────────▼───────────┐ + │ llms_service.py │ + │ get_response_with_ │ + │ tools(history, db) │ + │ │ + │ if db == "vcdb": │ + │ select_tools_for_ │ + │ prompt() → │ + │ DB/KB/PUB subsets │ + │ │ + │ if db == "bmdb": │ + │ BMDB_TOOLS │ + └──────────┬───────────┘ + │ + ┌─────────────┬──┴──────────────┐ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────────┐ + │ VCell │ │ BioModels│ │ Qdrant │ + │ API │ │ API │ │ (RAG) │ + └──────────┘ └──────────┘ └──────────────┘ +``` + +The branch introduces a database-selection dimension that flows through the entire stack: UI toggle → separate API endpoint → tool set selection → database-specific service calls. The two database paths are largely parallel rather than abstracted behind a common interface. From c242ad12aba9cd4dce7248439385066abc3cb7ce Mon Sep 17 00:00:00 2001 From: Jim Schaff Date: Wed, 1 Jul 2026 11:29:13 -0400 Subject: [PATCH 3/5] docs: clarify development_branch.md describes an unmerged branch The doc referenced code (bmdb_router.py, databases_service.py, etc.) that exists only on origin/development, which could read as if that code were present on this branch. Add a scope note stating the code is not on main / this branch, record that development diverged at 5d95ca1 (before the Auth0 work in PR #67) and is not rebased, and clarify the stats are the three-dot diff (development's own changes since the merge base). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XpNzobVi83p3YKL9sqtGwZ --- docs/development_branch.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/development_branch.md b/docs/development_branch.md index ad5cefe..33041ff 100644 --- a/docs/development_branch.md +++ b/docs/development_branch.md @@ -1,8 +1,12 @@ # Development Branch Summary -Summary of the `development` branch contribution relative to `main`. This branch adds BioModels Database (BMDB) integration as a second data source alongside the existing VCell database, along with performance optimizations to the LLM tool-calling pipeline. +Summary of the unmerged `origin/development` branch, which adds BioModels Database (BMDB) integration as a second data source alongside the existing VCell database, along with performance optimizations to the LLM tool-calling pipeline. -**Stats:** 60 commits, 26 files changed, ~1,667 lines added, ~254 lines removed (vs. `origin/main`). +> **Scope note:** This document describes work that lives **only** on the `origin/development` branch. None of the code referenced below (`bmdb_router.py`, `databases_service.py`, etc.) exists on `main` or in the branch this doc is committed to — do not expect to find it here. +> +> `development` diverged from `main` at commit `5d95ca1`, **before** the Auth0/Supabase authentication work (PR #67) was merged into `main`. It has not been rebased since, so it does not include that auth work, and a direct `git diff main development` reports extra churn (44 files) from main's newer commits. The figures below count only development's own additions since the divergence point. + +**Stats (three-dot `origin/main...origin/development`, i.e. development's own changes since the merge base `5d95ca1`):** 60 commits, 26 files changed, ~1,667 lines added, ~254 lines removed. ## Features From f7589a6a0bd67378c3c45c5e196a9356e3b1fad3 Mon Sep 17 00:00:00 2001 From: Jim Schaff Date: Wed, 1 Jul 2026 11:40:41 -0400 Subject: [PATCH 4/5] test: skip live LLM integration tests unless explicitly opted in The test_llms_service.py tests call Azure OpenAI and assert on live model output. CI cannot run them: the configured Azure endpoint is not resolvable from GitHub Actions runners (ConnectError: Name or service not known), which has kept Backend CI red. Gate them behind RUN_LLM_INTEGRATION_TESTS=1 so they are skipped in CI (and by default locally) and run only when a developer opts in against a reachable endpoint. The VCell API tests (public endpoint, no secrets) continue to run in CI. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XpNzobVi83p3YKL9sqtGwZ --- backend/tests/test_llms_service.py | 35 ++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/backend/tests/test_llms_service.py b/backend/tests/test_llms_service.py index 0641db5..5e9b7e3 100644 --- a/backend/tests/test_llms_service.py +++ b/backend/tests/test_llms_service.py @@ -1,14 +1,31 @@ -import pytest +import os -# This tells pytest that all tests in the file should run in asyncio mode. -pytestmark = pytest.mark.asyncio +import pytest -from app.services.llms_service import ( - get_llm_response, - get_response_with_tools, - analyse_vcml, - analyse_diagram, -) +# These are LIVE integration tests: they call Azure OpenAI (which in turn hits +# the VCell API) and assert on real model output. They require a *reachable* +# Azure endpoint plus valid credentials. CI cannot run them — the configured +# Azure endpoint is not resolvable from GitHub Actions runners — so they are +# opt-in via an explicit flag and skipped everywhere else. Run them locally with: +# RUN_LLM_INTEGRATION_TESTS=1 poetry run pytest tests/test_llms_service.py +_RUN_LLM_TESTS = os.getenv("RUN_LLM_INTEGRATION_TESTS") == "1" + +if _RUN_LLM_TESTS: + from app.services.llms_service import ( + get_llm_response, + get_response_with_tools, + analyse_vcml, + analyse_diagram, + ) + +# asyncio mode for all tests here; skip the whole file unless explicitly opted in. +pytestmark = [ + pytest.mark.asyncio, + pytest.mark.skipif( + not _RUN_LLM_TESTS, + reason="Live LLM integration tests; set RUN_LLM_INTEGRATION_TESTS=1 to run", + ), +] class TestLLMsService: From 1a2825f821336adc62be4748d4dfd5513f4806ea Mon Sep 17 00:00:00 2001 From: Jim Schaff Date: Wed, 1 Jul 2026 11:43:31 -0400 Subject: [PATCH 5/5] ci: bump checkout and setup-python actions to current majors Update actions/checkout@v2 -> @v4 and actions/setup-python@v2 -> @v5 to clear the Node 20 deprecation warnings on the runner. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XpNzobVi83p3YKL9sqtGwZ --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26f0c59..3715431 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,10 +14,10 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: '3.12'