# Orsync Scenarist v7.0 — API Documentation > Base URL: `http://localhost:7860` > Interactive Docs: `http://localhost:7860/docs` (Swagger UI) > OpenAPI Schema: `http://localhost:7860/openapi.json` Read/demo endpoints are available for local development. Destructive admin mutation endpoints are disabled by default unless `ENABLE_ADMIN_MUTATIONS=true`; when `ADMIN_TOKEN` is configured, those mutation endpoints also require the `X-Admin-Token` header or an admin bearer token. This is an MVP guard, not a full production auth/RBAC system. --- ## Table of Contents 1. [Project Workflow Overview](#1-project-workflow-overview) 2. [Getting Started — First-Run Sequence](#2-getting-started--first-run-sequence) 3. [Core Workflows](#3-core-workflows) - [Campaign Analysis Workflow](#31-campaign-analysis-workflow) - [Simulation Workflow](#32-simulation-workflow) - [HCP Exploration Workflow](#33-hcp-exploration-workflow) 4. [API Endpoints Reference](#4-api-endpoints-reference) - [Health](#41-health) - [Pipeline — Data Ingestion](#42-pipeline--data-ingestion) - [Strategy — Campaign Engine](#43-strategy--campaign-engine) - [Persona — Doctor Archetypes](#44-persona--doctor-archetypes) - [Simulation — Roleplay Engine](#45-simulation--roleplay-engine) - [MOHP — Objection Detection](#46-mohp--objection-detection) - [Graph — Knowledge Graph](#47-graph--knowledge-graph) - [Analytics — Session History](#48-analytics--session-history) - [Stats — System Metrics](#49-stats--system-metrics) - [Admin — Embedding Management](#410-admin--embedding-management) - [Admin — Dead Letter Queue](#411-admin--dead-letter-queue) 5. [Data Models](#5-data-models) 6. [Error Handling](#6-error-handling) --- ## 1. Project Workflow Overview Orsync Scenarist is a pharma strategic intelligence platform. The system works in three phases: ``` ┌──────────────────────────────────────────────────────────────────────┐ │ PHASE 1: DATA SETUP │ │ │ │ POST /api/pipeline/seed │ │ └─→ Loads ~480 HCP (doctor) profiles into Neo4j knowledge graph │ │ │ ├──────────────────────────────────────────────────────────────────────┤ │ PHASE 2: CAMPAIGN ANALYSIS │ │ │ │ POST /api/strategy/full-evaluate { campaign_text: "..." } │ │ └─→ Vectorizes campaign text into 12 behavioral dimensions │ │ └─→ Runs GMM clustering on the HCP population │ │ └─→ Computes Mahalanobis distance to each cluster centroid │ │ └─→ Returns heatmap, cluster cards, rejection check │ │ └─→ Auto-optimizes campaign if rejected │ │ │ ├──────────────────────────────────────────────────────────────────────┤ │ PHASE 3: SIMULATION & TRAINING │ │ │ │ GET /api/persona/from-cluster/{id} → pick a doctor persona │ │ POST /api/simulation/start → begin roleplay session │ │ POST /api/simulation/turn (repeat) → conversation loop │ │ POST /api/mohp/evaluate → compliance check per turn │ │ GET /api/analytics/session/{id} → post-session review │ │ │ └──────────────────────────────────────────────────────────────────────┘ ``` ### 12 Canonical Strategy Feature Keys Campaign vectors and HCP behavioral vectors use the same ordered 12D feature space. The internal order is canonical and must not be alphabetized: | # | FEATURE_KEY | |---|-------------| | 0 | `therapeutic_focus` | | 1 | `messaging_tone` | | 2 | `target_seniority` | | 3 | `channel_preference` | | 4 | `kol_alignment` | | 5 | `trial_phase_relevance` | | 6 | `formulary_impact` | | 7 | `patient_population_size` | | 8 | `competitive_positioning` | | 9 | `regulatory_stage` | | 10 | `budget_tier` | | 11 | `urgency_score` | ### Strategy Clusters Strategy `cluster_cards` are dynamic. The Strategy pipeline fits or reuses cached GMM/PCA artifacts over HCP behavioral features and returns however many clusters the fitted model selected for the current HCP data/filter/cache key. The latest live smoke showed 10 `cluster_cards`. Older four-archetype language may still apply to the compatibility persona endpoint, but Strategy, War Room, Analytics, and HCP Explorer should not assume exactly four clusters. Strategy cluster display names are deterministic and unique by `cluster_id` for the supported dynamic cluster range, using oncology-relevant segment labels such as `Evidence-Driven KOLs`, `Digital Oncology Adopters`, and `Precision-Medicine Champions`. The four legacy archetype names are reserved for the older persona compatibility endpoint and should not be treated as Strategy, War Room, Analytics, or HCP Explorer labels. ### Infrastructure, Persistence, and Runtime Guards - Expensive endpoints are rate limited by default. This includes Strategy vectorization/full evaluation/optimization, simulation turns, and MOHP evaluation. A rate-limited request returns HTTP `429` with: `{ "error": "rate_limited", "retry_after_seconds": 12 }`. - Redis-backed session updates use atomic mutation logic with Redis `WATCH`/transaction retries, with a per-session local lock fallback when Redis transactions are unavailable. This avoids losing rapid simulation turns or analytics appends in normal MVP operation. - `REDIS_URL`, `NEO4J_URI`, `NEO4J_USERNAME`, `NEO4J_PASSWORD`, `CHROMA_HOST`, `CHROMA_PORT`, and `CHROMA_URL` can point to local Docker services or managed external services. Startup diagnostics report local vs external mode without logging passwords. - The backend Dockerfile still starts local Redis, Neo4j, and Chroma for development/demo use. Production deployments should mount persistent volumes or use managed services: `redis_data`, `neo4j_data`, and `chroma_data` are the expected volume concepts for durable local container deployments. - Destructive admin mutation endpoints remain protected by the MVP guard: `ENABLE_ADMIN_MUTATIONS=true` is required, and `ADMIN_TOKEN` is enforced when configured. This is not full auth/RBAC. --- ## 2. Getting Started — First-Run Sequence After starting the server, call these endpoints in order: ``` Step 1 — Verify server is running: GET /healthz → { "status": "ok" } Step 2 — Seed the knowledge graph with doctor data: POST /api/pipeline/seed → { "status": "seeded", "records_loaded": 480, "records_ingested": 480 } Step 3 — Verify doctors loaded: GET /api/graph/cluster/0/doctors?limit=3 → { "cluster_id": 0, "doctors": [...], "count": 3 } Step 4 — Run your first campaign analysis: POST /api/strategy/full-evaluate { "campaign_text": "Our Phase 3 trial demonstrated 40% improvement in PFS..." } Step 5 — Check system stats: GET /api/stats/embedding GET /api/stats/projection GET /api/stats/cache ``` --- ## 3. Core Workflows ### 3.1 Campaign Analysis Workflow This is the primary use case. A user submits campaign text and gets a full strategic analysis. #### Current LLM, Optimization, and Cluster Behavior - Ollama Cloud is wired through `backend/app/core/config.py` and `backend/app/core/llm_client.py`. - Without `OLLAMA_API_KEY`, the strategy response uses heuristic fallback, shown by `VectorizationModel=fallback-no-api-key` and `OptimizedReason=heuristic_fallback_no_api_key` when optimization is triggered. - With `OLLAMA_API_KEY` loaded at backend startup, the latest validation activated the LLM path with `VectorizationModel=gemma4:31b-cloud`; forced rejected scenarios returned `OptimizedReason=llm_rewrite` rather than `heuristic_fallback_no_api_key`. - Full LLM plus vector database optimization requires setting `OLLAMA_API_KEY` and restarting the backend so the new environment is loaded. - `OLLAMA_HOST=0.0.0.0` is treated as a server bind address, not a client target. With an Ollama Cloud key present, the backend resolves that to the cloud endpoint; without a key, it resolves to local loopback for local Ollama development. - If the provider rejects the key/model or is unreachable, Strategy returns an explicit `VectorizationModel=fallback-llm-error` / `OptimizedReason=heuristic_fallback_llm_error` fallback instead of crashing the page. - The 50-scenario oncology validation called Chroma `campaign_memory` and `clinical_evidence` retrieval in the LLM optimization path; three uploaded-context scenarios passed `context_id` and exercised uploaded context chunks. - In the LLM path, `backend/app/services/rag_optimizer.py` uses the submitted campaign text, Chroma `campaign_memory`, Chroma `clinical_evidence`, and uploaded context chunks when `context_id` is passed. That assembled context is sent to the LLM. - In fallback mode, optimization does not use the vector database or uploaded document context for the rewrite. - Doctor/HCP features are used to build, select, and rank clusters before optimization. The optimizer receives the chosen cluster and target vector; it does not optimize directly over raw doctor rows. - Strategy clusters are fitted and cached from doctor/HCP data. They do not refit based only on campaign text. Campaign text can change ranking, probabilities, and the best-fit cluster, but not the fitted cluster model itself. - Clusters can change when doctor data changes, a region filter changes the HCP population, the adapter/schema/min_k/data source changes, or the cache/model version changes. ``` Frontend Backend ─────── ─────── │ │ │ POST /api/strategy/full-evaluate │ │ { "campaign_text": "...", │ │ "region": "egypt" } │ │ ─────────────────────────────────────────>│ │ │ 1. Load HCP population │ │ 2. Reuse/fit cached dynamic GMM artifacts │ │ 3. Vectorize campaign (→12D) │ │ 4. Project to PCA space │ │ 5. Compute Mahalanobis distances │ │ 6. Check rejection threshold │<──────────────────────────────────────────│ │ { │ │ campaign_vector_12d: [...], │ │ cluster_cards: [...], │ │ heatmap: {...}, │ │ rejected: false, │ │ optimized: null │ │ } │ │ │ │ (If rejected=true, show optimized text) │ │ │ │ User clicks on Cluster 0 card │ │ │ │ GET /api/strategy/cluster/0/doctors │ │ ?limit=50®ion=egypt │ │ ─────────────────────────────────────────>│ │<──────────────────────────────────────────│ │ { doctors: [...], total_in_cluster: 120 }│ │ │ │ User clicks on a specific doctor │ │ │ │ GET /api/persona/HCP-00-042 │ │ ─────────────────────────────────────────>│ │<──────────────────────────────────────────│ │ { codeName, traits, h_index, ... } │ │ │ │ User saves successful campaign │ │ │ │ POST /api/strategy/memory/store │ │ { campaign_text, success_score: 0.85 } │ │ ─────────────────────────────────────────>│ │<──────────────────────────────────────────│ │ { stored: true, campaign_id: "..." } │ ``` ### 3.2 Simulation Workflow A rep practices a pitch against an AI doctor persona in a turn-based conversation. ``` Frontend Backend ─────── ─────── │ │ │ Step 1: Pick a persona │ │ GET /api/persona/from-cluster/0 │ │ ─────────────────────────────────────────>│ │<──────────────────────────────────────────│ │ { codeName: "HCP-00-042", traits: [...] }│ │ │ │ Step 2: Start session │ │ POST /api/simulation/start │ │ { "persona_id": "HCP-00-042" } │ │ ─────────────────────────────────────────>│ │<──────────────────────────────────────────│ │ { session_id: "abc-123", offer: {...} } │ │ │ │ Step 3: WebRTC handshake │ │ POST /api/simulation/handshake │ │ { session_id: "abc-123", answer: {...} } │ │ ─────────────────────────────────────────>│ │<──────────────────────────────────────────│ │ { status: "connected" } │ │ │ │ Step 4: Conversation loop (repeat) │ │ ┌────────────────────────────────────┐ │ │ │ POST /api/simulation/turn │ │ │ │ { session_id: "abc-123", │ │ │ │ input_text: "Doctor, our..." } │ │ │ │────────────────────────────────────>│ │ │ │<───────────────────────────────────│ │ │ │ { reply_text: "Interesting...", │ │ │ │ emotion: "skeptical", │ │ │ │ adherence_score: 0.72, │ │ │ │ objections: [...] } │ │ │ └────────────────────────────────────┘ │ │ │ │ Step 5: (Optional) Check compliance │ │ POST /api/mohp/evaluate │ │ { session_id, input_text, cluster_id:0 } │ │ ─────────────────────────────────────────>│ │<──────────────────────────────────────────│ │ { objections: [{severity, guideline}] } │ │ │ │ Step 6: Review session │ │ GET /api/analytics/session/abc-123 │ │ ─────────────────────────────────────────>│ │<──────────────────────────────────────────│ │ { durationMs, adherenceScore, │ │ emotionTimeline, conversation } │ ``` ### 3.3 HCP Exploration Workflow Browse dynamic Strategy HCP segments, doctor-level behavioral fields, and legacy graph research context where needed. The launch HCP Explorer first calls `GET /api/strategy/clusters` and then `GET /api/strategy/cluster/{cluster_id}/doctors?limit=50`; graph endpoints remain available for institution/topic research context. ``` 1. GET /api/graph/institutions/summary?limit=20 → Top 20 institutions by doctor count 2. GET /api/graph/institution/Brigham%20and%20Women's%20Hospital/doctors?limit=50 → All doctors at that institution 3. GET /api/graph/doctor/HCP-00-042 → Full doctor profile (h-index, publications, topics, institution) 4. GET /api/graph/overlap?code_name_a=HCP-00-001&code_name_b=HCP-00-042 → Shared research topics between two doctors 5. GET /api/graph/topic/Biomarker%20Discovery/doctors?limit=50 → All doctors researching a given topic ``` --- ## 4. API Endpoints Reference ### 4.1 Health #### `GET /healthz` Server health check. **Response:** ```json { "status": "ok" } ``` --- ### 4.2 Pipeline — Data Ingestion #### `POST /api/pipeline/seed` Loads the gold-layer doctor dataset (~480 profiles) from `data/gold/doctors_unified.json` into Neo4j. **Call this once after first setup.** **Response:** ```json { "status": "seeded", "records_loaded": 480, "records_ingested": 480, "source_file": "C:\\code\\...\\data\\gold\\doctors_unified.json" } ``` #### `POST /api/pipeline/ingest` Queue an arbitrary event for outbox processing. **Request Body:** Any JSON object. **Response:** ```json { "status": "queued", "event_id": "uuid-string" } ``` #### `POST /api/pipeline/dispatch` Manually trigger outbox dispatch (processes one pending event). **Response:** ```json { "processed": true } ``` --- ### 4.3 Strategy — Campaign Engine #### `POST /api/strategy/full-evaluate` ⭐ Primary Endpoint The all-in-one campaign analysis endpoint. This is the **main entry point** for the campaign workflow. **Request Body:** ```json { "campaign_text": "Our Phase 3 trial demonstrated...", "rejection_distance_threshold": 3.0, "region": "egypt" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `campaign_text` | string | Yes | Campaign message text (min 1 char) | | `rejection_distance_threshold` | float | No | Override threshold (default: 3.0, must be > 0) | | `region` | string \| null | No | Filter HCPs: `"egypt"`, `"saudi"`, `"gulf"`, or `null` for all | **Response:** ```json { "campaign_vector_12d": [0.82, 0.65, 0.71, 0.30, 0.55, 0.40, 0.78, 0.25, 0.60, 0.45, 0.50, 0.70], "campaign_vector_pca": [1.23, -0.67, 0.45], "gmm": { "k": 10, "data_source": "bronze_dynamic_master", "feature_names": ["therapeutic_focus", "..."], "scaler_center": [0.5, "..."], "scaler_scale": [0.2, "..."], "n_pca_components": 3, "pca_explained_variance_ratio": [0.45, 0.25, 0.15], "centroids": [[...], [...], [...], [...]], "covariances": [[[...]], [[...]], [[...]], [[...]]] }, "projection": { "path": "scaled_pca", "fallback_used": false, "input_dimensions": 12, "output_dimensions": 3 }, "heatmap": { "ranking": [ { "cluster_id": 1, "label": "Digital Oncology Adopters", "distance": 1.23, "probability": 0.45, "top_doctors": [] } ] }, "cluster_cards": [ { "id": "c0", "cluster_id": 0, "name": "Evidence-Driven KOLs", "score": 35.0, "distance": 2.1, "probability": 0.35 } ], "rejection_distance_threshold": 3.0, "rejected": false, "optimized": null, "vectorization_model": "gemma4:31b-cloud" } ``` When `rejected` is `true`, `optimized` will contain: ```json { "original_text": "...", "optimized_text": "...", "target_cluster": 0, "improvements": ["Added evidence-based language", "..."] } ``` --- #### `POST /api/strategy/vectorize` Convert campaign text into a 12-dimensional feature vector. **Request Body:** ```json { "text": "Campaign message text", "campaign_id": "optional-id" } ``` **Response:** ```json { "normalized_features": { "therapeutic_focus": 0.82, "messaging_tone": 0.65, "target_seniority": 0.71, "channel_preference": 0.45, "kol_alignment": 0.50, "trial_phase_relevance": 0.70 }, "embedding": [...], "embedding_model": "onnx-minilm", "model": "gemma4:31b-cloud" } ``` --- #### `POST /api/strategy/heatmap` Build a heatmap from a pre-computed campaign vector and GMM centroids/covariances. **Request Body:** ```json { "campaign_vector": [12 floats], "centroids": [[...], [...]], "covariances": [[[...]], [[...]]], "cluster_top_doctors": { "0": ["HCP-00-001"], "1": ["HCP-01-042"] } } ``` | Field | Type | Required | Validation | |-------|------|----------|------------| | `campaign_vector` | float[] | Yes | Min 1 element | | `centroids` | float[][] | Yes | Must match `campaign_vector` dims | | `covariances` | float[][][] | Yes | Must match `centroids` count and dims | | `cluster_top_doctors` | dict | No | Cluster ID → list of doctor code_names | **Response:** ```json { "ranking": [ { "cluster_id": 0, "label": "...", "distance": 1.5, "probability": 0.35, "top_doctors": [...] } ] } ``` --- #### `POST /api/strategy/evaluate` Advanced evaluation with custom centroids (BYO GMM results). **Request Body:** ```json { "campaign_text": "...", "centroids": [[...], [...]], "covariances": [[[...]], [[...]]], "cluster_top_doctors": null, "rejection_distance_threshold": 3.0 } ``` **Response:** ```json { "campaign_vector": [12 floats], "heatmap": { "ranking": [...] }, "rejection_distance_threshold": 3.0, "rejected": false, "optimized": null } ``` --- #### `POST /api/strategy/optimize` Rewrite a campaign to better target a specific cluster using LLM. **Request Body:** ```json { "campaign_text": "Original campaign text", "target_cluster": 0, "target_centroid_vector": [12 floats] } ``` **Response:** ```json { "original_text": "...", "optimized_text": "...", "target_cluster": 0, "improvements": ["Added evidence-based language"] } ``` --- #### `POST /api/strategy/memory/store` Store a campaign outcome for future RAG-based optimization. **Request Body:** ```json { "campaign_text": "...", "campaign_id": "optional-id", "outcome": "success", "success_score": 0.85, "is_successful": true, "cluster_id": 0, "extra_metadata": {} } ``` | Field | Type | Required | Default | |-------|------|----------|---------| | `campaign_text` | string | Yes | — | | `campaign_id` | string | No | Auto-generated UUID | | `outcome` | string | No | `""` | | `success_score` | float | No | `0.0` (range 0.0–1.0) | | `is_successful` | bool | No | Derived from score/outcome | | `cluster_id` | int | No | `null` | | `extra_metadata` | dict | No | `{}` | **Response:** ```json { "stored": true, "campaign_id": "campaign_abc123", "embedding_model": "onnx-minilm", "is_successful": true } ``` --- #### `GET /api/strategy/clusters` Return dynamic Strategy cluster metadata for launch-path UIs such as War Room and HCP selection. These labels are the same oncology-relevant dynamic labels used by Strategy `cluster_cards`; they are not the old fixed four-archetype persona labels. **Query Parameters:** | Param | Type | Default | Values | |-------|------|---------|--------| | `region` | string | null | `egypt`, `saudi`, `gulf` | **Response:** ```json { "k": 10, "source": "bronze_dynamic_master", "region": null, "total_in_db": 480, "clusters": [ { "cluster_id": 1, "name": "Digital Oncology Adopters", "description": "Digital-ready oncology adoption segment", "total_in_cluster": 64 } ] } ``` --- #### `GET /api/strategy/cluster/{cluster_id}/doctors` Get doctors from the master CSV database assigned to a GMM cluster. **Path Parameters:** `cluster_id` (int) **Query Parameters:** | Param | Type | Default | Range | |-------|------|---------|-------| | `limit` | int | 50 | 1–200 | | `region` | string | null | `egypt`, `saudi`, `gulf` | **Response:** ```json { "cluster_id": 0, "total_in_cluster": 120, "total_in_db": 480, "k": 10, "region": "egypt", "doctors": [ { "cluster_id": 0, "name": "HCP-00-042", "region": "egypt", "headline": "Professor of Oncology", "location": "Cairo, Egypt", "company": "Cairo University Hospital", "job": "Senior Oncology Physician", "school": "Cairo University", "school_degree": "MD, PhD", "primary_specialty": "Oncology", "seniority_level": "Senior", "highest_academic_degree": "PhD", "total_years_experience": 22, "expected_age": 50, "age_group": "45-54", "current_role_tenure": 8, "kol_status": true, "digital_presence": true, "academic_affiliation": true, "workplace_category": "Academic Medical Center", "institutional_tier": true, "adoption_profile": "Early_Adopter", "channel_preference": "High_Digital", "patient_volume_proxy": "High", "skepticism_level": "Medium", "cognitive_processing_style": "Empirical" } ] } ``` --- ### 4.4 Persona — Doctor Archetypes #### `GET /api/persona/from-cluster/{cluster_id}` Generate a persona profile from a legacy cluster behavioral template. The launch-path War Room now uses `/api/strategy/clusters` and dynamic Strategy cluster names for HCP selection; this older persona endpoint remains available for compatibility and should not be used to infer Strategy `cluster_cards` count. **Path Parameters:** `cluster_id` (int, 0–3) **Response:** ```json { "codeName": "HCP-00-042", "clusterId": 0, "clusterLabel": "The Academic Skeptic", "traits": [ { "axis": "Scientific Rigor", "value": 0.923 }, { "axis": "Innovation Appetite", "value": 0.312 }, { "axis": "Guideline Adherence", "value": 0.891 }, { "axis": "Price Sensitivity", "value": 0.187 }, { "axis": "Risk Tolerance", "value": 0.162 }, { "axis": "Peer Influence", "value": 0.715 }, { "axis": "Evidence Threshold", "value": 0.935 }, { "axis": "Formulary Weight", "value": 0.389 }, { "axis": "Patient Centricity", "value": 0.512 }, { "axis": "Digital Readiness", "value": 0.215 }, { "axis": "KOL Alignment", "value": 0.838 }, { "axis": "Trial Participation", "value": 0.887 } ] } ``` --- #### `GET /api/persona/{code_name}` Get a specific doctor's persona profile with academic metrics. **Path Parameters:** `code_name` (string, e.g., `"HCP-00-042"`) **Response:** ```json { "codeName": "HCP-00-042", "clusterId": 0, "clusterLabel": "The Academic Skeptic", "traits": [...], "h_index": 41, "works_count": 129, "cited_by_count": 6958, "years_active": 16 } ``` --- ### 4.5 Simulation — Roleplay Engine #### `POST /api/simulation/start` Start a new simulation session with a doctor persona. War Room should pass the selected dynamic cluster name in `campaign_snapshot.cluster_name` so Analytics can display the same Strategy segment label later. **Request Body:** ```json { "persona_id": "HCP-00-042", "campaign_id": "camp-001", "campaign_snapshot": { "cluster_id": 1, "cluster_name": "Digital Oncology Adopters", "source": "war_room" } } ``` | Field | Type | Required | |-------|------|----------| | `persona_id` | string | Yes (min 1 char) | | `campaign_id` | string | No | | `campaign_snapshot` | object | No | --- #### `POST /api/simulation/end` Mark a simulation session as ended so Analytics no longer treats it as active. **Request Body:** ```json { "session_id": "abc-123" } ``` **Response:** ```json { "session_id": "abc-123", "ended": true } ``` **Response:** ```json { "session_id": "uuid-string", "offer": { "type": "offer", "sdp": "..." }, "persona": { ... } } ``` --- #### `POST /api/simulation/handshake` Complete WebRTC signaling. **Request Body:** ```json { "session_id": "uuid-string", "answer": { "type": "answer", "sdp": "..." } } ``` **Response:** ```json { "status": "connected" } ``` --- #### `POST /api/simulation/ice-candidate` Exchange ICE candidates (can be called multiple times). **Request Body:** ```json { "session_id": "uuid-string", "candidate": { "candidate": "...", "sdpMid": "0", "sdpMLineIndex": 0 } } ``` **Response:** ```json { "accepted": true } ``` --- #### `POST /api/simulation/turn` Send the rep's message and receive the AI persona's response. This is the **core conversation loop** — call repeatedly. **Request Body:** ```json { "session_id": "uuid-string", "input_text": "Doctor, our Phase 3 trial showed a 40% improvement in PFS...", "input_audio_base64": "" } ``` | Field | Type | Required | |-------|------|----------| | `session_id` | string | Yes | | `input_text` | string | No (empty = audio only) | | `input_audio_base64` | string | No (base64-encoded audio) | **Response:** ```json { "reply_text": "Interesting, but I'd like to see the subgroup analysis...", "reply_audio_base64": "...", "emotion": "skeptical", "adherence_score": 0.72, "talking_points_delivered": 3, "talking_points_total": 5, "objections": [...], "session_summary": { ... } } ``` --- #### `GET /api/simulation/cache/{cache_key}` Check the semantic cache for a previously computed response. **Response:** ```json { "hit": true, "value": "cached response text" } ``` --- ### 4.6 MOHP — Objection Detection #### `POST /api/mohp/evaluate` Analyze a rep's statement for possible oncology medical/compliance objections. MOHP is a deterministic rule baseline plus optional Ollama/Chroma clinical evidence enhancement when `OLLAMA_API_KEY` and the provider are available. It is not a regulatory approval engine and not patient medical advice. **Request Body:** ```json { "session_id": "uuid-string", "input_text": "This drug is 100% effective with no side effects", "cluster_id": 0, "persona_id": "" } ``` | Field | Type | Required | Default | |-------|------|----------|---------| | `session_id` | string | Yes | — | | `input_text` | string | Yes | — | | `cluster_id` | int | No | `0` | | `persona_id` | string | No | `""` | **Response:** ```json { "session_id": "uuid-string", "objections": [ { "id": "mohp-a1b2c3d4", "timestamp": 1713355200000, "objection": "Clarify evidence strength before making survival claims.", "guideline": "retrieved clinical evidence", "severity": "medium", "safer_phrasing": "Use qualified evidence language.", "evidence_gap": "Head-to-head survival evidence was not retrieved." } ], "count": 1, "mohp_mode": "llm_rag_enhanced", "llm_enhancement_used": true, "clinical_evidence_retrieval_called": true, "clinical_evidence_count": 1, "retrieval_empty": false, "llm_error": false } ``` **Severity levels:** `"low"`, `"medium"`, `"high"` If `OLLAMA_API_KEY` is missing or the provider fails, the endpoint returns `mohp_mode="rule_fallback"` and `llm_enhancement_used=false`. If clinical retrieval is empty, `retrieval_empty=true` and the response must not invent guideline names. **Rule baseline databases by legacy cluster profile:** - **Cluster 0** (Academic Skeptic): NCCN evidence, ICH E9 statistics, FDA endpoint guidance, EBM hierarchy - **Cluster 1** (Commercial Adopter): Novel mechanism validation, digital health, rapid adoption - **Cluster 2** (Guideline Loyalist): On-label requirements, guideline standards, risk-benefit framework - **Cluster 3** (Price-Sensitive): Health economics, formulary positioning, patient access --- ### 4.7 Graph — Knowledge Graph #### `POST /api/graph/ingest` Ingest doctor records into the Neo4j knowledge graph. **Request Body:** ```json { "records": [ { "code_name": "HCP-00-042", "cluster_id": 0, "h_index": 41, "institution": "Brigham and Women's Hospital", "topics": ["Biomarker Discovery"] } ] } ``` **Response:** ```json { "status": "ok", "ingested": 1 } ``` --- #### `GET /api/graph/doctor/{code_name}` Get a single doctor's full profile from the graph. **Response:** ```json { "code_name": "HCP-00-042", "source": "seed_synthetic", "cluster_id": 0, "h_index": 41, "works_count": 129, "cited_by_count": 6958, "i10_index": 78, "years_active": 16, "institution": "Brigham and Women's Hospital", "institution_type": "academic_medical_center", "institution_country": "US", "topics": ["Biomarker Discovery", "Minimal Residual Disease"] } ``` --- #### `GET /api/graph/cluster/{cluster_id}/doctors?limit=50` Get doctors by GMM cluster (ranked by h-index). **Query:** `limit` (int, 1–500, default 50) **Response:** ```json { "cluster_id": 0, "doctors": [...], "count": 50 } ``` --- #### `GET /api/graph/institution/{institution_name}/doctors?limit=50` Get doctors affiliated with an institution. **Response:** ```json { "institution": "Mount Sinai Hospital", "doctors": [...], "count": 12 } ``` --- #### `GET /api/graph/topic/{topic_name}/doctors?limit=50` Get doctors researching a specific topic. **Response:** ```json { "topic": "Biomarker Discovery", "doctors": [...], "count": 8 } ``` --- #### `GET /api/graph/institutions/summary?limit=20` Get institutions ranked by affiliated doctor count. **Query:** `limit` (int, 1–100, default 20) **Response:** ```json { "institutions": [ { "name": "Brigham and Women's Hospital", "doctor_count": 15, "avg_h_index": 38.2 } ] } ``` --- #### `GET /api/graph/overlap?code_name_a=HCP-00-001&code_name_b=HCP-00-042` Find shared research topics between two doctors. **Query Parameters (required):** `code_name_a`, `code_name_b` **Response:** ```json { "doctor_a": "HCP-00-001", "doctor_b": "HCP-00-042", "shared_topics": ["Biomarker Discovery"], "count": 1 } ``` --- ### 4.8 Analytics — Session History #### `GET /api/analytics/sessions?limit=50` List real simulation sessions ordered by recency. Empty stores return an empty array; the API no longer injects fake/demo analytics sessions into the live UI. **Query:** `limit` (int, 1–200, default 50) **Response:** ```json { "sessions": [{ "session_id": "...", "persona_id": "...", ... }] } ``` --- #### `GET /api/analytics/session/{session_id}` Full session analytics for post-simulation review. **Response:** ```json { "sessionId": "abc-123", "personaId": "HCP-00-042", "campaignId": "camp-001", "clusterId": 0, "durationMs": 245000, "adherenceScore": 0.72, "emotionTimeline": [ { "timestamp": 1234, "emotion": "neutral" }, { "timestamp": 5678, "emotion": "skeptical" } ], "totalPoints": 5, "deliveredPoints": 3, "objections": [ { "id": "mohp-...", "severity": "medium", "guideline": "..." } ], "conversation": [ { "role": "rep", "text": "Doctor, our Phase 3..." }, { "role": "persona", "text": "Interesting, but I'd like..." } ] } ``` --- ### 4.9 Stats — System Metrics #### `GET /api/stats/embedding` Active embedding model info. **Response:** ```json { "model_name": "onnx-minilm", "dimension": 384, "backend": "onnx" } ``` #### `GET /api/stats/projection` Projection bridge (384D → 12D) status. **Response:** ```json { "ready": true, "input_dim": 384, "output_dim": 12 } ``` #### `GET /api/stats/cache` Redis cache key counts. **Response:** ```json { "semantic_cache_keys": 42, "session_keys": 5, "simulation_session_keys": 3 } ``` #### `GET /api/stats/dlq` Dead letter queue depth. **Response:** ```json { "dlq_depth": 0 } ``` #### `GET /api/stats/outbox` Pending outbox events count. **Response:** ```json { "pending_outbox_events": 0 } ``` --- ### 4.10 Admin — Embedding Management #### `GET /admin/embeddings/status` Current embedding model and available alternatives. **Response:** ```json { "model_name": "onnx-minilm", "dimension": 384, "known_models": { "onnx-minilm": { "description": "all-MiniLM-L6-v2 via ONNX (384-dim, local)", "dimension": 384 }, "ollama:nomic-embed-text": { "description": "Nomic Embed Text via Ollama (768-dim)", "dimension": 768 }, "pritamdeka/S-PubMedBert-MS-MARCO": { "description": "PubMedBERT for medical search (768-dim)", "dimension": 768 }, "NeuML/pubmedbert-base-embeddings": { "description": "PubMedBERT base biomedical (768-dim)", "dimension": 768 } } } ``` #### `POST /admin/embeddings/swap` Hot-swap the embedding model at runtime. **Request Body:** ```json { "model_name": "pritamdeka/S-PubMedBert-MS-MARCO", "reindex": true } ``` **Response:** ```json { "previous_model": "onnx-minilm", "new_model": "pritamdeka/S-PubMedBert-MS-MARCO", "reindex": { "collections_reindexed": 1, "documents_reindexed": 42 } } ``` #### `POST /admin/embeddings/reindex` Re-embed all ChromaDB collections with the current model. **Response:** ```json { "collections": { "campaign_memory": { "documents_reindexed": 42 } }, "model": { "name": "onnx-minilm", "dimension": 384 } } ``` --- ### 4.11 Admin — Dead Letter Queue #### `GET /admin/dlq?limit=100` List failed events in the dead letter queue. **Response:** ```json { "items": [{ "event_id": "...", "event_type": "...", "payload": {...}, "retries": 3 }] } ``` #### `POST /admin/dlq/replay` Replay the oldest DLQ item back into the outbox. **Response:** ```json { "replayed": true, "item": { "event_id": "...", ... } } ``` --- ## 5. Data Models ### Doctor Profile (Gold Dataset) ```json { "code_name": "HCP-00-042", "source": "seed_synthetic", "cluster_id": 0, "h_index": 41, "works_count": 129, "cited_by_count": 6958, "i10_index": 78, "years_active": 16, "institution": "Brigham and Women's Hospital", "institution_type": "academic_medical_center", "institution_country": "US", "topics": ["Biomarker Discovery", "Minimal Residual Disease"] } ``` ### Campaign Feature Vector (12D) ```json { "therapeutic_focus": 0.82, "messaging_tone": 0.65, "target_seniority": 0.71, "channel_preference": 0.45, "kol_alignment": 0.50, "trial_phase_relevance": 0.70, "formulary_impact": 0.25, "patient_population_size": 0.60, "competitive_positioning": 0.55, "regulatory_stage": 0.78, "budget_tier": 0.30, "urgency_score": 0.40 } ``` ### Heatmap Ranking Entry ```json { "cluster_id": 0, "label": "Evidence-Driven KOLs", "distance": 1.5, "probability": 0.35, "top_doctors": ["HCP-00-042", "HCP-00-015"] } ``` - **distance**: Mahalanobis distance — lower = better fit - **probability**: Softmax over distances — higher = better fit ### Cluster Card ```json { "id": "c0", "cluster_id": 0, "name": "Evidence-Driven KOLs", "score": 35.0, "distance": 2.1, "probability": 0.35 } ``` - **score**: `probability x 100` (percentage fit) - Strategy cluster card names are dynamic and unique by `cluster_id`; do not hardcode four fixed Strategy labels in clients. ### Outbox Event ```json { "event_id": "uuid-string", "event_type": "gold.ingest", "payload": { ... }, "retries": 0 } ``` --- ## 6. Error Handling All errors use standard HTTP status codes with JSON bodies. | Code | Meaning | When | |------|---------|------| | `400` | Bad Request | Invalid request body, dimension mismatch, missing required fields | | `404` | Not Found | Doctor/session/resource not found | | `409` | Conflict | Duplicate resource (e.g., username already exists) | | `429` | Too Many Requests | Rate limit exceeded for expensive LLM/vectorization endpoints | | `422` | Unprocessable Entity | Pydantic validation failure | | `500` | Internal Server Error | GMM clustering failure, Neo4j connection error, etc. | ### 422 Validation Error Shape ```json { "detail": [ { "type": "string_too_short", "loc": ["body", "campaign_text"], "msg": "String should have at least 1 character", "input": "", "ctx": { "min_length": 1 } } ] } ``` ### Complete Endpoint Index | # | Method | Path | Tag | |---|--------|------|-----| | 1 | `GET` | `/healthz` | health | | 2 | `POST` | `/api/pipeline/seed` | pipeline | | 3 | `POST` | `/api/pipeline/ingest` | pipeline | | 4 | `POST` | `/api/pipeline/dispatch` | pipeline | | 5 | `POST` | `/api/strategy/full-evaluate` | strategy | | 6 | `POST` | `/api/strategy/vectorize` | strategy | | 7 | `POST` | `/api/strategy/heatmap` | strategy | | 8 | `POST` | `/api/strategy/evaluate` | strategy | | 9 | `POST` | `/api/strategy/optimize` | strategy | | 10 | `POST` | `/api/strategy/memory/store` | strategy | | 11 | `GET` | `/api/strategy/clusters` | strategy | | 12 | `GET` | `/api/strategy/cluster/{cluster_id}/doctors` | strategy | | 13 | `GET` | `/api/persona/from-cluster/{cluster_id}` | persona | | 14 | `GET` | `/api/persona/{code_name}` | persona | | 15 | `POST` | `/api/simulation/start` | simulation | | 16 | `POST` | `/api/simulation/end` | simulation | | 17 | `POST` | `/api/simulation/handshake` | simulation | | 18 | `POST` | `/api/simulation/ice-candidate` | simulation | | 19 | `POST` | `/api/simulation/turn` | simulation | | 20 | `GET` | `/api/simulation/cache/{cache_key}` | simulation | | 21 | `POST` | `/api/mohp/evaluate` | mohp | | 22 | `POST` | `/api/graph/ingest` | graph | | 23 | `GET` | `/api/graph/doctor/{code_name}` | graph | | 24 | `GET` | `/api/graph/cluster/{cluster_id}/doctors` | graph | | 25 | `GET` | `/api/graph/institution/{institution_name}/doctors` | graph | | 26 | `GET` | `/api/graph/topic/{topic_name}/doctors` | graph | | 27 | `GET` | `/api/graph/institutions/summary` | graph | | 28 | `GET` | `/api/graph/overlap` | graph | | 29 | `GET` | `/api/analytics/sessions` | analytics | | 30 | `GET` | `/api/analytics/session/{session_id}` | analytics | | 31 | `GET` | `/api/stats/embedding` | stats | | 32 | `GET` | `/api/stats/projection` | stats | | 33 | `GET` | `/api/stats/cache` | stats | | 34 | `GET` | `/api/stats/dlq` | stats | | 35 | `GET` | `/api/stats/outbox` | stats | | 36 | `GET` | `/admin/embeddings/status` | admin | | 37 | `POST` | `/admin/embeddings/swap` | admin | | 38 | `POST` | `/admin/embeddings/reindex` | admin | | 39 | `GET` | `/admin/dlq` | admin | | 40 | `POST` | `/admin/dlq/replay` | admin |