Spaces:
Sleeping
Orsync Scenarist v7.0 β API Documentation
Base URL:
http://localhost:7860Interactive 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
- Project Workflow Overview
- Getting Started β First-Run Sequence
- Core Workflows
- API Endpoints Reference
- Data Models
- 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
429with:{ "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, andCHROMA_URLcan 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, andchroma_dataare the expected volume concepts for durable local container deployments. - Destructive admin mutation endpoints remain protected by the MVP guard:
ENABLE_ADMIN_MUTATIONS=trueis required, andADMIN_TOKENis 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.pyandbackend/app/core/llm_client.py. - Without
OLLAMA_API_KEY, the strategy response uses heuristic fallback, shown byVectorizationModel=fallback-no-api-keyandOptimizedReason=heuristic_fallback_no_api_keywhen optimization is triggered. - With
OLLAMA_API_KEYloaded at backend startup, the latest validation activated the LLM path withVectorizationModel=gemma4:31b-cloud; forced rejected scenarios returnedOptimizedReason=llm_rewriterather thanheuristic_fallback_no_api_key. - Full LLM plus vector database optimization requires setting
OLLAMA_API_KEYand restarting the backend so the new environment is loaded. OLLAMA_HOST=0.0.0.0is 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_errorfallback instead of crashing the page. - The 50-scenario oncology validation called Chroma
campaign_memoryandclinical_evidenceretrieval in the LLM optimization path; three uploaded-context scenarios passedcontext_idand exercised uploaded context chunks. - In the LLM path,
backend/app/services/rag_optimizer.pyuses the submitted campaign text, Chromacampaign_memory, Chromaclinical_evidence, and uploaded context chunks whencontext_idis 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:
{ "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:
{
"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:
{ "status": "queued", "event_id": "uuid-string" }
POST /api/pipeline/dispatch
Manually trigger outbox dispatch (processes one pending event).
Response:
{ "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:
{
"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:
{
"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:
{
"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:
{
"text": "Campaign message text",
"campaign_id": "optional-id"
}
Response:
{
"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:
{
"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:
{
"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:
{
"campaign_text": "...",
"centroids": [[...], [...]],
"covariances": [[[...]], [[...]]],
"cluster_top_doctors": null,
"rejection_distance_threshold": 3.0
}
Response:
{
"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:
{
"campaign_text": "Original campaign text",
"target_cluster": 0,
"target_centroid_vector": [12 floats]
}
Response:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{ "session_id": "abc-123" }
Response:
{ "session_id": "abc-123", "ended": true }
Response:
{
"session_id": "uuid-string",
"offer": { "type": "offer", "sdp": "..." },
"persona": { ... }
}
POST /api/simulation/handshake
Complete WebRTC signaling.
Request Body:
{
"session_id": "uuid-string",
"answer": { "type": "answer", "sdp": "..." }
}
Response:
{ "status": "connected" }
POST /api/simulation/ice-candidate
Exchange ICE candidates (can be called multiple times).
Request Body:
{
"session_id": "uuid-string",
"candidate": { "candidate": "...", "sdpMid": "0", "sdpMLineIndex": 0 }
}
Response:
{ "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:
{
"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:
{
"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:
{ "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:
{
"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:
{
"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:
{
"records": [
{
"code_name": "HCP-00-042",
"cluster_id": 0,
"h_index": 41,
"institution": "Brigham and Women's Hospital",
"topics": ["Biomarker Discovery"]
}
]
}
Response:
{ "status": "ok", "ingested": 1 }
GET /api/graph/doctor/{code_name}
Get a single doctor's full profile from the graph.
Response:
{
"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:
{ "cluster_id": 0, "doctors": [...], "count": 50 }
GET /api/graph/institution/{institution_name}/doctors?limit=50
Get doctors affiliated with an institution.
Response:
{ "institution": "Mount Sinai Hospital", "doctors": [...], "count": 12 }
GET /api/graph/topic/{topic_name}/doctors?limit=50
Get doctors researching a specific topic.
Response:
{ "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:
{
"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:
{
"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:
{ "sessions": [{ "session_id": "...", "persona_id": "...", ... }] }
GET /api/analytics/session/{session_id}
Full session analytics for post-simulation review.
Response:
{
"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:
{ "model_name": "onnx-minilm", "dimension": 384, "backend": "onnx" }
GET /api/stats/projection
Projection bridge (384D β 12D) status.
Response:
{ "ready": true, "input_dim": 384, "output_dim": 12 }
GET /api/stats/cache
Redis cache key counts.
Response:
{
"semantic_cache_keys": 42,
"session_keys": 5,
"simulation_session_keys": 3
}
GET /api/stats/dlq
Dead letter queue depth.
Response:
{ "dlq_depth": 0 }
GET /api/stats/outbox
Pending outbox events count.
Response:
{ "pending_outbox_events": 0 }
4.10 Admin β Embedding Management
GET /admin/embeddings/status
Current embedding model and available alternatives.
Response:
{
"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:
{
"model_name": "pritamdeka/S-PubMedBert-MS-MARCO",
"reindex": true
}
Response:
{
"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:
{
"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:
{ "items": [{ "event_id": "...", "event_type": "...", "payload": {...}, "retries": 3 }] }
POST /admin/dlq/replay
Replay the oldest DLQ item back into the outbox.
Response:
{ "replayed": true, "item": { "event_id": "...", ... } }
5. Data Models
Doctor Profile (Gold Dataset)
{
"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)
{
"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
{
"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
{
"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
{
"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
{
"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 |