This repo uses lightweight CLI smoke tests for the ACP and MCP layers. Keep these steps in sync as the interfaces evolve.
Install the repo in editable mode so the CLI entrypoints are on your PATH and changes take effect immediately:
pip install -e ".[dev]"Editable mode means Python imports the local source tree directly. You do not need to reinstall after edits; just re-run the commands. Manage this per environment (venv/conda) and remove with pip uninstall study-agent if needed.
Dependency notes:
pyproject.tomlis the source of truth for the Python package and the optionaldevextras.environment.ymlbootstraps the Conda or Micromamba environment used by Docker and many local setups.uv.lockis intentionally not tracked. If you preferuv, generate a local lockfile after cloning withuv lock.
Use pytest's built-in verbosity:
pytest -vOr enable per-test progress lines via environment variable:
STUDY_AGENT_PYTEST_PROGRESS=1 pytestYou can also set PYTEST_OPTS and doit will pass it through:
PYTEST_OPTS="-vv -rA -s" doit run_all_testspytest -m acpcovers ACP flow tests (including phenotype flow).pytest -m mcpcovers MCP tool tests (including prompt bundles and search weights).
List tasks:
doit listCommon tasks but see doit list for the most current set:
doit install
doit test_unit
doit test_core
doit test_acp
doit test_allTask dependencies:
test_unitdepends ontest_coreandtest_acp
Start the ACP shim with core fallback enabled:
STUDY_AGENT_ALLOW_CORE_FALLBACK=1 study-agent-acpIn another shell:
curl -s http://127.0.0.1:8765/health
curl -s http://127.0.0.1:8765/tools
curl -s -X POST http://127.0.0.1:8765/tools/call \
-H 'Content-Type: application/json' \
-d '{"name":"cohort_lint","arguments":{"cohort":{"PrimaryCriteria":{"ObservationWindow":{"PriorDays":0}}}}}'Notes:
- PowerShell aliases
curltoInvoke-WebRequest. Usecurl.exefor real curl, or useInvoke-RestMethodbelow. - Use here-strings to keep JSON readable.
Start ACP with verbose logging (server + LLM):
$env:STUDY_AGENT_ALLOW_CORE_FALLBACK = "1"
$env:STUDY_AGENT_DEBUG = "1"
$env:LLM_LOG = "1"
study-agent-acpIf you launch from outside the repo root, set STUDY_AGENT_BASE_DIR so relative paths (index, banner, outputs) resolve correctly:
$env:STUDY_AGENT_BASE_DIR = "C:\path\to\OHDSI-Study-Agent"Windows note: ACP defaults MCP to oneshot mode on Windows to avoid stdio lockups. You can also set it explicitly:
$env:STUDY_AGENT_MCP_ONESHOT = "1"ACP uses a threaded HTTP server by default. To disable threading:
$env:STUDY_AGENT_THREADING = "0"Health/tools checks:
curl.exe -s http://127.0.0.1:8765/health
curl.exe -s http://127.0.0.1:8765/tools
curl.exe -s http://127.0.0.1:8765/servicesTool call (Invoke-RestMethod):
$body = @'
{"name":"cohort_lint","arguments":{"cohort":{"PrimaryCriteria":{"ObservationWindow":{"PriorDays":0}}}}}
'@
Invoke-RestMethod `
-Method Post `
-Uri http://127.0.0.1:8765/tools/call `
-Headers @{ "Content-Type" = "application/json" } `
-Body $bodyTool call (curl.exe):
$body = @'
{"name":"cohort_lint","arguments":{"cohort":{"PrimaryCriteria":{"ObservationWindow":{"PriorDays":0}}}}}
'@
curl.exe -s -X POST http://127.0.0.1:8765/tools/call `
-H "Content-Type: application/json" `
-d $bodyStart ACP with an MCP tool server:
STUDY_AGENT_MCP_COMMAND=study-agent-mcp STUDY_AGENT_MCP_ARGS="" study-agent-acpThis uses stdio MCP mode. If you use HTTP MCP, do not set STUDY_AGENT_MCP_COMMAND.
HTTP MCP mode (recommended for cross-platform stability):
export MCP_TRANSPORT=http
export MCP_HOST=127.0.0.1
export MCP_PORT=8790
export MCP_PATH=/mcp
study-agent-mcpThen in a second shell:
export STUDY_AGENT_MCP_URL="http://127.0.0.1:8790/mcp"
study-agent-acpNote: STUDY_AGENT_MCP_URL must include the port (e.g. :8790).
When set, ACP uses HTTP and ignores STUDY_AGENT_MCP_COMMAND.
PowerShell (Windows) MCP HTTP mode:
$env:MCP_TRANSPORT = "http"
$env:MCP_HOST = "127.0.0.1"
$env:MCP_PORT = "8790"
$env:MCP_PATH = "/mcp"
study-agent-mcpThen in a second PowerShell:
$env:STUDY_AGENT_MCP_URL = "http://127.0.0.1:8790/mcp"
study-agent-acpHealth check (PowerShell):
Invoke-RestMethod -Uri http://127.0.0.1:8765/healthBuilt-in rotating service logging:
export STUDY_AGENT_LOG_DIR="/tmp/study-agent-logs"
export ACP_LOG_LEVEL=DEBUG
export MCP_LOG_LEVEL=DEBUGACP writes study-agent-acp.log; MCP writes study-agent-mcp.log.
Use ACP_LOG_FILE or MCP_LOG_FILE to override the exact file path.
Rotation is controlled by STUDY_AGENT_LOG_MAX_BYTES and STUDY_AGENT_LOG_BACKUP_COUNT.
Windows logging via shell redirection still works if desired:
study-agent-mcp 1> mcp.out.log 2> mcp.err.log
study-agent-acp 1> acp.out.log 2> acp.err.logOr using Start-Process:
Start-Process study-agent-mcp -RedirectStandardOutput mcp.out.log -RedirectStandardError mcp.err.log
Start-Process study-agent-acp -RedirectStandardOutput acp.out.log -RedirectStandardError acp.err.logRecommended MCP environment (use absolute paths for stability):
export PHENOTYPE_INDEX_DIR="/absolute/path/to/phenotype_index"
export EMBED_URL="http://localhost:3000/ollama/api/embed"
export EMBED_MODEL="qwen3-embedding:4b"Optional host/port override:
STUDY_AGENT_HOST=0.0.0.0 STUDY_AGENT_PORT=9000 study-agent-acpThen run the same curl commands as above.
Health check now includes MCP index preflight details under mcp_index:
curl -s http://127.0.0.1:8765/healthEnsure MCP is running and set LLM env vars for an OpenAI-compatible endpoint:
export LLM_API_URL="http://localhost:3000/api/chat/completions"
export LLM_API_KEY="..."
export LLM_MODEL="gemma3:4b"
export LLM_DRY_RUN=0
export LLM_USE_RESPONSES=0
export LLM_LOG=1
export LLM_TIMEOUT=300
export STUDY_AGENT_MCP_TIMEOUT=240
export ACP_TIMEOUT=360
export EMBED_TIMEOUT=120
export LLM_CANDIDATE_LIMIT=5
export LLM_RECOMMENDATION_MAX_RESULTS=3LLM_LOG=1 enables verbose LLM logging in the ACP logger (config, prompt, raw response).
For full payload capture during debugging, also set LLM_LOG_RESPONSE=1.
For OpenWebUI using /api/chat/completions, keep LLM_USE_RESPONSES=0 (the Responses API schema is not supported and can yield empty outputs).
Recommended timeout ladder: ACP_TIMEOUT > LLM_TIMEOUT > STUDY_AGENT_MCP_TIMEOUT.
Then call:
curl -s -X POST http://127.0.0.1:8765/flows/phenotype_recommendation \
-H 'Content-Type: application/json' \
-d '{"study_intent":"Identify clinical risk factors for older adult patients who experience an adverse event of acute gastro-intenstinal (GI) bleeding", "top_k":20, "max_results":10,"candidate_limit":10}'Expected recommendation responses now include llm_used, llm_status, fallback_reason, fallback_mode, and diagnostics. If the LLM path fails to parse or validate, ACP still returns status: ok with an explicit machine-readable fallback reason instead of silently degrading.
Use the automated calibration task to derive environment-specific starting values for EMBED_TIMEOUT, STUDY_AGENT_MCP_TIMEOUT, LLM_TIMEOUT, and ACP_TIMEOUT:
doit calibrate_timeoutsWhat it does:
- starts MCP and ACP if they are not already running
- warms up and samples
phenotype_intent_split,phenotype_recommendation_advice, andphenotype_recommendation - tests multiple recommendation prompt sizes using
TIMEOUT_CALIBRATION_CANDIDATE_LIMITS(default3,5,8) - uses ACP diagnostics plus MCP embedding debug logs to recommend timeouts with safety margins
Useful overrides:
export TIMEOUT_CALIBRATION_RUNS=3
export TIMEOUT_CALIBRATION_CANDIDATE_LIMITS=3,5,8
export TIMEOUT_CALIBRATION_ENV_PATH=/tmp/study_agent_timeout_recommendations.env
export TIMEOUT_CALIBRATION_JSON_PATH=/tmp/study_agent_timeout_recommendations.json
doit calibrate_timeoutsOutputs:
.envfragment with recommended timeout values- JSON summary with observed p95 timings, fallback statuses, and per-run details
Interpretation notes:
- If the calibration run reports repeated
llm_status != ok, fix LLM parsing/compatibility first rather than only raising timeouts. - If larger
candidate_limitvalues sharply increase latency, prefer a smallerLLM_CANDIDATE_LIMITbefore increasingLLM_TIMEOUT. - Treat the generated values as good starting points for that environment, not universal maxima.
Phenotype intent split (target/outcome statements):
curl -s -X POST http://127.0.0.1:8765/flows/phenotype_intent_split \
-H 'Content-Type: application/json' \
-d '{"study_intent":"Identify clinical risk factors for older adult patients who experience an adverse event of acute gastro-intenstinal (GI) bleeding"}'PowerShell (Windows) equivalent:
$body = @{
study_intent = "Identify clinical risk factors for older adult patients who experience an adverse event of acute gastro-intenstinal (GI) bleeding"
} | ConvertTo-Json
Invoke-RestMethod `
-Method Post `
-Uri http://127.0.0.1:8765/flows/phenotype_intent_split `
-Headers @{ "Content-Type" = "application/json" } `
-Body $body `
-TimeoutSec 180Cohort methods intent split (target/comparator/outcome statements):
curl -s -X POST http://127.0.0.1:8765/flows/cohort_methods_intent_split \
-H 'Content-Type: application/json' \
-d '{"study_intent":"What is the risk of angioedema or acute myocardial infarction in new users of ACE inhibitors compared to new users of thiazide and thiazide-like diuretics?"}'PowerShell (Windows) equivalent:
$body = @{
study_intent = "What is the risk of angioedema or acute myocardial infarction in new users of ACE inhibitors compared to new users of thiazide and thiazide-like diuretics?"
} | ConvertTo-Json
Invoke-RestMethod `
-Method Post `
-Uri http://127.0.0.1:8765/flows/cohort_methods_intent_split `
-Headers @{ "Content-Type" = "application/json" } `
-Body $body `
-TimeoutSec 180Cohort methods specifications recommendation (analytic settings):
curl -s -X POST http://127.0.0.1:8765/flows/cohort_methods_specifications_recommendation \
-H 'Content-Type: application/json' \
-d '{"analytic_settings_description":"Compare sitagliptin new users vs glipizide new users for acute myocardial infarction. Use a 365-day washout, intent-to-treat follow-up, 1:1 propensity score matching on standardized logit with a caliper of 0.2, and a Cox model.","study_intent":"Comparative effectiveness study on CV outcomes."}' | python -m json.toolPowerShell (Windows) equivalent:
$body = @{
analytic_settings_description = "Compare sitagliptin new users vs glipizide new users for acute myocardial infarction. Use a 365-day washout, intent-to-treat follow-up, 1:1 propensity score matching on standardized logit with a caliper of 0.2, and a Cox model."
study_intent = "Comparative effectiveness study on CV outcomes."
} | ConvertTo-Json
Invoke-RestMethod `
-Method Post `
-Uri http://127.0.0.1:8765/flows/cohort_methods_specifications_recommendation `
-Headers @{ "Content-Type" = "application/json" } `
-Body $body `
-TimeoutSec 240Expected responses include status, recommendation, cohort_methods_specifications, section_rationales, and diagnostics. Valid top-level statuses are ok, schema_validation_error, and llm_parse_error; parse or section validation failures should return a backfilled recommendation with diagnostics rather than an unstructured response.
For local non-live coverage of the route, input model, validation, and mocked ACP flow:
pytest tests/test_acp_cohort_methods_route.py \
tests/test_cohort_methods_specs_models.py \
tests/test_cohort_methods_spec_validation.py \
tests/test_acp_cohort_methods_flow.pyPhenotype improvements:
curl -s -X POST http://127.0.0.1:8765/flows/phenotype_improvements \
-H 'Content-Type: application/json' \
-d '{"protocol_text":"Example protocol text","cohorts":[{"id":1,"name":"Example"}],"characterization_previews":[]}'Using file paths:
curl -s -X POST http://127.0.0.1:8765/flows/phenotype_improvements \
-H 'Content-Type: application/json' \
-d '{"protocol_path":"scripts/protocol.md","cohort_paths":["scripts/1197_Acute_gastrointestinal_bleeding.json"]}'Concept sets review:
curl -s -X POST http://127.0.0.1:8765/flows/concept_sets_review \
-H 'Content-Type: application/json' \
-d '{"concept_set":{"items":[]},"study_intent":"Example intent"}'Cohort critique (general design):
curl -s -X POST http://127.0.0.1:8765/flows/cohort_critique_general_design \
-H 'Content-Type: application/json' \
-d '{"cohort":{"PrimaryCriteria":{}}}'Using file paths:
curl -s -X POST http://127.0.0.1:8765/flows/concept_sets_review \
-H 'Content-Type: application/json' \
-d '{"concept_set_path":"scripts/concept_set.json","study_intent":"Example intent"}'
curl -s -X POST http://127.0.0.1:8765/flows/cohort_critique_general_design \
-H 'Content-Type: application/json' \
-d '{"cohort_path":"scripts/cohort_definition.json"}'Phenotype validation review (single patient):
curl -s -X POST http://127.0.0.1:8765/flows/phenotype_validation_review \
-H 'Content-Type: application/json' \
-d '{"disease_name":"Gastrointestinal bleeding","keeper_row":{"age":44,"gender":"Male","visitContext":"Inpatient Visit","presentation":"Gastrointestinal hemorrhage","priorDisease":"Peptic ulcer","symptoms":"","comorbidities":"","priorDrugs":"celecoxib","priorTreatmentProcedures":"","diagnosticProcedures":"","measurements":"","alternativeDiagnosis":"","afterDisease":"","afterDrugs":"Naproxen","afterTreatmentProcedures":""}}'Important:
case_rowmust already be in the compact canonical case format expected by Study Agentcandidate_itemsare the only structured ranking universecontext_itemsandcase_metadatamay influence reasoning and narrative but are not ranked by defaultindex_eventis assumed to have occurred and must never be ranked as a causesource_typemust currently besignal_validationorpatient_profile- sanitization is fail-closed before any LLM call
- optional enrichment tools may be hinted via
tool_hints, but the flow must still work without them
Positive test path using signal_validation with compact case_row and optional tool hints:
curl -s -X POST http://127.0.0.1:8765/flows/case_causal_review -H 'Content-Type: application/json' -d '{
"adverse_event_name": "Cystitis",
"source_type": "signal_validation",
"allowed_domains": ["drug_exposures", "conditions"],
"case_row": {
"case_id": "25196051",
"case_summary": "Single suspect-drug spontaneous report with cystitis and additional hepatic reactions.",
"index_event": {
"label": "Cystitis",
"source_record_id": "reaction-4",
"domain": "index_event",
"why_observed": "Selected adverse event present in reported reactions"
},
"candidate_items": [
{
"domain": "drug_exposures",
"label": "Ketamine hydrochloride",
"source_record_id": "drug-1",
"source_kind": "reported_drug",
"why_observed": "Primary suspect drug in spontaneous report",
"subrole": "primary_suspect",
"annotations": {
"concept_set_match": false,
"ingredient_concept_id": 123,
"reported_indication": "Substance use",
"approved_indications": [],
"label_mentions_event": true,
"box_warning_mentions_event": false,
"has_disproportional_signal": true
}
}
],
"context_items": [
{
"domain": "conditions",
"label": "Drug abuse",
"source_record_id": "reaction-5",
"source_kind": "reported_reaction",
"why_observed": "Additional reported reaction in same case",
"subrole": "contextual_factor",
"annotations": {
"concept_set_match": false
}
}
],
"case_metadata": {
"age": "3 years",
"sex": "male",
"reporter_type": "health professional",
"reporting_country": "GB",
"serious": true,
"seriousness_flags": ["other"],
"literature_reference_present": true,
"timing_granularity": "coarse"
},
"annotations": {
"concept_set_id": "uuid",
"concept_set_version": 1,
"concept_set_available_domains": ["doi", "alternativeDiagnosis", "symptoms", "drugs"]
},
"tool_hints": {
"available_expansions": [
"get_case_review_concept_set_domain",
"get_case_review_drug_signal_details",
"get_case_review_drug_label_details",
"get_case_review_report_literature_stub"
],
"prefetch_expansions": [
"get_case_review_drug_signal_details",
"get_case_review_report_literature_stub"
]
}
}
}' | python -m json.toolPositive test path using patient_profile with candidate_items and context_items kept separate:
curl -s -X POST http://127.0.0.1:8765/flows/case_causal_review -H 'Content-Type: application/json' -d '{
"adverse_event_name": "Hepatic failure",
"source_type": "patient_profile",
"allowed_domains": ["drug_exposures", "conditions", "measurements"],
"case_row": {
"case_id": "profile-17",
"case_summary": "Progressive liver injury after recent medication changes.",
"index_event": {
"label": "Hepatic failure",
"source_record_id": "event-1",
"domain": "index_event",
"why_observed": "Selected event of interest in the patient profile"
},
"candidate_items": [
{
"domain": "drug_exposures",
"label": "Valproate",
"source_record_id": "drug-17",
"source_kind": "medication_exposure",
"why_observed": "Recent active exposure before liver injury",
"subrole": "primary_suspect",
"annotations": {
"label_mentions_event": true,
"has_disproportional_signal": false
}
}
],
"context_items": [
{
"domain": "conditions",
"label": "Chronic liver disease",
"source_record_id": "cond-3",
"source_kind": "condition_occurrence",
"why_observed": "Pre-existing condition",
"subrole": "vulnerability_factor",
"annotations": {
"concept_set_match": true
}
},
{
"domain": "measurements",
"label": "ALT 622 U/L",
"source_record_id": "meas-8",
"source_kind": "lab_measurement",
"why_observed": "Observed during index event window",
"subrole": "proximate_marker",
"annotations": {}
}
],
"case_metadata": {
"sex": "female",
"timing_granularity": "coarse"
},
"annotations": {
"concept_set_id": "cs-1",
"concept_set_version": 2,
"concept_set_available_domains": ["drugs", "alternativeDiagnosis"]
},
"tool_hints": {
"available_expansions": ["get_case_review_drug_label_details"],
"prefetch_expansions": []
}
}
}' | python -m json.toolValidation check for unsupported source_type:
curl -i -s -X POST http://127.0.0.1:8765/flows/case_causal_review -H 'Content-Type: application/json' -d '{
"adverse_event_name": "Gastrointestinal bleeding",
"source_type": "faers_raw",
"case_row": {
"case_id": "case-1",
"index_event": {
"domain": "index_event",
"label": "Gastrointestinal bleeding",
"source_record_id": "reaction-1"
},
"candidate_items": [
{
"domain": "drug_exposures",
"label": "Warfarin",
"source_record_id": "drug-1"
}
]
}
}'Expected result: HTTP 400 with source_type must be signal_validation or patient_profile.
Direct enrichment tool checks through Study Agent (/tools/call):
Assumptions:
- ACP is running on
http://127.0.0.1:8765 - MCP is running with
PV_COPILOT_HOSTandPV_COPILOT_PORTalready configured - dev mode is being used with no pv-copilot auth requirement
- if you configured
PV_COPILOT_BASE_URLinstead, these commands do not change
Concept-set domain lookup:
curl -s -X POST http://127.0.0.1:8765/tools/call -H 'Content-Type: application/json' -d '{
"name": "get_case_review_concept_set_domain",
"arguments": {
"concept_set_id": "uuid",
"concept_set_version": 1,
"domain_name": "doi",
"limit": 10
}
}' | python -m json.toolDrug signal details lookup:
curl -s -X POST http://127.0.0.1:8765/tools/call -H 'Content-Type: application/json' -d '{
"name": "get_case_review_drug_signal_details",
"arguments": {
"source_type": "signal_validation",
"adverse_event_name": "Cystitis",
"source_record_id": "drug-1",
"report_lookup_key": {
"primaryid": "25196051",
"isr": null
},
"adverse_event_concept_id": 4172256,
"ingredient_concept_id": 123,
"ingred_rxcui": "11289"
}
}' | python -m json.toolDrug label details lookup:
curl -s -X POST http://127.0.0.1:8765/tools/call -H 'Content-Type: application/json' -d '{
"name": "get_case_review_drug_label_details",
"arguments": {
"source_type": "signal_validation",
"adverse_event_name": "Cystitis",
"source_record_id": "drug-1",
"report_lookup_key": "25196051",
"adverse_event_concept_id": 4172256,
"adverse_event_meddra_id": "10011735",
"ingredient_concept_id": 123,
"ingred_rxcui": "11289",
"mention_limit": 5
}
}' | python -m json.toolReport literature stub lookup:
curl -s -X POST http://127.0.0.1:8765/tools/call -H 'Content-Type: application/json' -d '{
"name": "get_case_review_report_literature_stub",
"arguments": {
"source_type": "signal_validation",
"case_id": "25196051",
"report_lookup_key": "25196051"
}
}' | python -m json.toolPatient-profile compatibility check for a non-fatal unsupported response:
curl -s -X POST http://127.0.0.1:8765/tools/call -H 'Content-Type: application/json' -d '{
"name": "get_case_review_drug_signal_details",
"arguments": {
"source_type": "patient_profile",
"adverse_event_name": "Hepatic failure",
"source_record_id": "drug-17"
}
}' | python -m json.toolExpected result: tool-level status may be ok, not_found, unsupported, or unavailable. unsupported and not_found are valid non-fatal outcomes.
End-to-end flow check with optional enrichment enabled:
curl -s -X POST http://127.0.0.1:8765/flows/case_causal_review -H 'Content-Type: application/json' -d '{
"adverse_event_name": "Cystitis",
"source_type": "signal_validation",
"allowed_domains": ["drug_exposures", "conditions"],
"case_row": {
"case_id": "25196051",
"case_summary": "Single suspect-drug spontaneous report with cystitis and additional hepatic reactions.",
"index_event": {
"label": "Cystitis",
"source_record_id": "reaction-4",
"domain": "index_event",
"why_observed": "Selected adverse event present in reported reactions",
"annotations": {
"adverse_event_meddra_id": "10011735"
}
},
"candidate_items": [
{
"domain": "drug_exposures",
"label": "Ketamine hydrochloride",
"source_record_id": "drug-1",
"source_kind": "reported_drug",
"why_observed": "Primary suspect drug in spontaneous report",
"subrole": "primary_suspect",
"annotations": {
"ingredient_concept_id": 123,
"report_lookup_key": "25196051",
"label_mentions_event": true,
"has_disproportional_signal": true
}
}
],
"context_items": [
{
"domain": "conditions",
"label": "Drug abuse",
"source_record_id": "reaction-5",
"source_kind": "reported_reaction",
"why_observed": "Additional reported reaction in same case",
"subrole": "contextual_factor",
"annotations": {}
}
],
"case_metadata": {
"literature_reference_present": true,
"reporter_type": "health professional",
"timing_granularity": "coarse"
},
"annotations": {
"concept_set_id": "uuid",
"concept_set_version": 1,
"concept_set_available_domains": ["drugs", "symptoms"],
"report_lookup_key": "25196051"
},
"tool_hints": {
"available_expansions": [
"get_case_review_concept_set_domain",
"get_case_review_drug_signal_details",
"get_case_review_drug_label_details",
"get_case_review_report_literature_stub"
],
"prefetch_expansions": [
"get_case_review_drug_signal_details",
"get_case_review_drug_label_details",
"get_case_review_report_literature_stub"
]
}
}
}' | python -m json.toolCheck diagnostics.optional_enrichment in the response to confirm which enrichment tools were called and what they returned.
This flow is now usable end to end.
Supported provider patterns:
- Hecate-backed vocabulary search plus Hecate Phoebe expansion
- air-gapped
generic_search_apivocabulary search plus DB-backed concept enrichment and Phoebe recommendations
Important:
- restart ACP and MCP after code changes or environment changes affecting provider selection
keeper_concept_sets_generatedoes not use patient-level datakeeper_profiles_generateis deterministic only and does not call the LLM
export VOCAB_SEARCH_PROVIDER=hecate_api
export VOCAB_SEARCH_URL="https://hecate.pantheon-hds.com/api/search_standard"
export PHOEBE_PROVIDER=hecate_api
export PHOEBE_URL_TEMPLATE="https://hecate.pantheon-hds.com/api/concepts/{concept_id}/phoebe"Run the flow:
curl -s -X POST http://127.0.0.1:8765/flows/keeper_concept_sets_generate \
-H 'Content-Type: application/json' \
-d '{"phenotype":"Gastrointestinal bleeding","domain_keys":["doi","alternativeDiagnosis","symptoms"],"candidate_limit":10,"include_diagnostics":true}' | python -m json.toolThis flow is now implemented for the first deterministic slice.
What it does:
- calls MCP
keeper_profile_extractto query OMOP CDM and build Keeper-style long-form profile records - calls MCP
keeper_profile_to_rowsto convert those records into row-oriented review payloads - does not call the LLM
Important:
- row-level patient data remains on the deterministic MCP side
- downstream
phenotype_validation_reviewmust still receive sanitized rows only - the current sampling mode is deterministic head-of-cohort, not random
Example:
curl -s -X POST http://127.0.0.1:8765/flows/keeper_profiles_generate \
-H 'Content-Type: application/json' \
-d '{
"cdm_database_schema": "cdm",
"cohort_database_schema": "results",
"cohort_table": "cohort",
"cohort_definition_id": 123,
"sample_size": 5,
"phenotype_name": "Gastrointestinal bleeding",
"remove_pii": true,
"keeper_concept_sets": [
{
"conceptId": 192671,
"conceptName": "Gastrointestinal hemorrhage",
"vocabularyId": "SNOMED",
"conceptSetName": "doi",
"target": "Disease of interest"
}
]
}' | python -m json.toolDirect MCP tool checks through ACP:
curl -s -X POST http://127.0.0.1:8765/tools/call \
-H 'Content-Type: application/json' \
-d '{
"name": "keeper_concept_set_bundle",
"arguments": {
"phenotype": "Gastrointestinal bleeding",
"domain_key": "doi",
"target": "Disease of interest"
}
}' | python -m json.toolcurl -s -X POST http://127.0.0.1:8765/tools/call \
-H 'Content-Type: application/json' \
-d '{
"name": "vocab_search_standard",
"arguments": {
"query": "gastrointestinal hemorrhage",
"domains": ["Condition"],
"concept_classes": [],
"limit": 5,
"provider": "hecate_api"
}
}' | python -m json.toolcurl -s -X POST http://127.0.0.1:8765/tools/call \
-H 'Content-Type: application/json' \
-d '{
"name": "phoebe_related_concepts",
"arguments": {
"concept_ids": [192671],
"relationship_ids": [],
"provider": "hecate_api"
}
}' | python -m json.toolUse this when the embedding service is local and returns sparse concept rows that need OMOP metadata enrichment from the vocabulary database.
export VOCAB_SEARCH_PROVIDER=generic_search_api
export VOCAB_SEARCH_URL="http://127.0.0.1:30080/search"
export VOCAB_SEARCH_QUERY_PREFIX="Instruction: retrieve the concepts most related to the query. Query: "
export VOCAB_METADATA_PROVIDER=db
export PHOEBE_PROVIDER=db
export OMOP_DB_ENGINE='<sqlalchemy engine url>'
export VOCAB_DATABASE_SCHEMA=vocabulary
export PHOEBE_DB_TABLE=concept_recommended
export VOCAB_CONCEPT_TABLE=conceptTest sparse search:
curl -s -X POST http://127.0.0.1:8765/tools/call \
-H 'Content-Type: application/json' \
-d '{
"name": "vocab_search_standard",
"arguments": {
"query": "intracranial hemorrhage",
"domains": ["Condition"],
"concept_classes": [],
"limit": 5,
"provider": "generic_search_api"
}
}' | python -m json.toolTest DB-backed Phoebe:
curl -s -X POST http://127.0.0.1:8765/tools/call \
-H 'Content-Type: application/json' \
-d '{
"name": "phoebe_related_concepts",
"arguments": {
"concept_ids": [192671],
"relationship_ids": ["Patient context"],
"provider": "db"
}
}' | python -m json.toolTest DB-backed enrichment/filtering for sparse rows:
curl -s -X POST http://127.0.0.1:8765/tools/call \
-H 'Content-Type: application/json' \
-d '{
"name": "vocab_filter_standard_concepts",
"arguments": {
"concepts": [
{"conceptId": 439847, "score": 0.98}
],
"domains": ["Condition"],
"concept_classes": [],
"provider": "db"
}
}' | python -m json.toolcurl -s -X POST http://127.0.0.1:8765/tools/call \
-H 'Content-Type: application/json' \
-d '{
"name": "vocab_fetch_concepts",
"arguments": {
"concept_ids": [439847],
"concepts": [
{"conceptId": 439847, "score": 0.98}
],
"provider": "db"
}
}' | python -m json.toolRun the flow with the air-gapped provider path:
curl -s -X POST http://127.0.0.1:8765/flows/keeper_concept_sets_generate \
-H 'Content-Type: application/json' \
-d '{"phenotype":"Intracranial hemorrhage","domain_keys":["doi"],"candidate_limit":5,"vocab_search_provider":"generic_search_api","phoebe_provider":"db","include_diagnostics":true}' | python -m json.toolMake sure the LLM shim config.yaml is configured for the target provider/model.
Example Bedrock naming may require the us. prefix.
export LLM_MODEL=bedrock:us.anthropic.claude-opus-4-5-20251101-v1:0curl -s -X POST http://127.0.0.1:8765/flows/keeper_concept_sets_generate \
-H 'Content-Type: application/json' \
-d '{"phenotype":"Gastrointestinal bleeding","domain_keys":["doi","alternativeDiagnosis","symptoms"],"candidate_limit":10,"include_diagnostics":true}' | python -m json.toolRun the Python smoke test via doit:
doit smoke_phenotype_flowIf you want doit to spin up MCP over HTTP automatically, set:
export STUDY_AGENT_MCP_URL="http://127.0.0.1:8790/mcp"
export STUDY_AGENT_MCP_MANAGED=1
export MCP_START_TIMEOUT=3Note: the smoke tasks set ACP_URL internally per flow. Avoid exporting a global ACP_URL unless you intend to override the target flow.
doit smoke_concept_sets_review_flowdoit smoke_cohort_critique_flowThis live ACP + MCP smoke test requires LLM credentials, because the flow asks the LLM to map free-text cohort-method analytic settings into the CohortMethod specification shape:
export LLM_API_KEY="..."
doit smoke_cohort_methods_specs_recommend_flowIf you want doit to start MCP over HTTP automatically, use the same managed MCP settings as the phenotype flow smoke test:
export STUDY_AGENT_MCP_URL="http://127.0.0.1:8790/mcp"
export STUDY_AGENT_MCP_MANAGED=1
export MCP_START_TIMEOUT=3
export LLM_API_KEY="..."
doit smoke_cohort_methods_specs_recommend_flowThe smoke test posts to /flows/cohort_methods_specifications_recommendation and checks that the response status is one of ok, schema_validation_error, or llm_parse_error, and that recommendation.raw_description is present.
doit smoke_phenotype_validation_review_flowdoit smoke_keeper_concept_sets_generate_flowpython -c "import study_agent_mcp; print('mcp import ok')"This checks index paths and runs a simple search, without ACP.
python mcp_server/scripts/mcp_probe.py --query "acute GI bleed in hospitalized patients" --top-k 5PowerShell (Windows) equivalent:
python mcp_server/scripts/mcp_probe.py --query "acute GI bleed in hospitalized patients" --top-k 5Print and sort environment variables (PowerShell):
Get-ChildItem Env: | Sort-Object NameUse the /services endpoint (or the helper task) to list ACP services:
doit list_servicesPress Ctrl+C in the terminal running study-agent-acp to stop ACP.
If MCP is running as a separate HTTP process, stop ACP first, then stop MCP.
If ACP started MCP via STUDY_AGENT_MCP_COMMAND, stopping ACP should also close the managed MCP subprocess.