"""
# ---------------------------------------------------------------------
# MODEL LOADING (LAZY)
# ---------------------------------------------------------------------
_severity_pipe = None
_ner_pipe = None
def _get_severity_pipe():
global _severity_pipe
if _severity_pipe is None:
from transformers import (
AutoTokenizer,
AutoModelForSequenceClassification,
pipeline as hf_pipeline,
)
MODEL_NAME = "Alanouette/sentinelplus-adr-severity"
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForSequenceClassification.from_pretrained(MODEL_NAME)
_severity_pipe = hf_pipeline(
"text-classification",
model=model,
tokenizer=tokenizer,
top_k=None,
truncation=True,
max_length=512,
)
return _severity_pipe
def _get_ner_pipe():
global _ner_pipe
if _ner_pipe is None:
from transformers import pipeline as hf_pipeline
_ner_pipe = hf_pipeline(
"token-classification",
model="Clinical-AI-Apollo/Medical-NER",
aggregation_strategy="simple",
)
return _ner_pipe
# ---------------------------------------------------------------------
# MODEL FUNCTIONS
# ---------------------------------------------------------------------
_DRUG_KEYS = {"drug", "medication", "chemical", "medicine", "substance", "pharma"}
_DISEASE_KEYS = {"disease", "disorder", "condition", "diagnosis", "pathology"}
_TIMING_KEYS = {"time", "date", "duration", "temporal", "frequency", "period"}
LIVE_AGENT_MODEL = os.getenv("SENTINELPLUS_AGENT_MODEL", "gpt-4.1-mini")
ENABLE_LIVE_AGENT = os.getenv("SENTINELPLUS_LIVE_AGENT", "1").lower() not in {"0", "false", "no"}
TRIAGE_AGENT_SYSTEM_PROMPT = """
You are SentinelPlus ADR Triage Agent, a narrow adverse drug reaction documentation assistant.
Allowed scope:
- Summarize possible adverse drug reaction documentation from the provided case context.
- Return priority, next step, explanation, missing information, and mock FDA Form 3500B fields.
- Ask or suggest clarifying documentation questions about symptoms, timing, medications, product details, allergies, medical history, and reporter fields.
- Recommend human clinician/pharmacist review, urgent provider review, emergency care for red-flag symptoms, or completion of missing report fields.
Hard prohibitions:
- Do not diagnose.
- Do not state that a medication caused the event.
- Do not tell the user to start, stop, restart, skip, increase, decrease, or replace any medication.
- Do not prescribe, dose, recommend, compare, or select prescription medications.
- Do not answer unrelated requests such as restaurants, travel, entertainment, shopping, coding, homework, finance, or legal advice.
- Do not invent facts that are not in the case context. Use "Not on file" or add the field to missing_information.
Tone:
- Plain language.
- Human-in-the-loop.
- Documentation and triage support only.
""".strip()
TRIAGE_QA_SYSTEM_PROMPT = """
You are SentinelPlus ADR Triage Agent in live Q&A mode.
Answer only questions about the user's adverse drug reaction documentation, symptom clarification,
medication/report context, missing information, or what to discuss with a human clinician.
You must refuse off-topic requests. You must not diagnose or give prescription advice.
For medication changes, advise the user to contact their prescribing clinician or pharmacist.
For severe or emergency symptoms, advise urgent medical care.
""".strip()
OFF_TOPIC_KEYWORDS = {
"restaurant", "restaurants", "pizza", "burger", "hotel", "flight", "vacation",
"movie", "music", "sports", "stock", "crypto", "homework", "essay", "code",
"python", "javascript", "recipe", "dating", "shopping", "weather",
}
PRESCRIPTION_ADVICE_PATTERNS = [
r"\bshould\s+i\s+(stop|start|take|skip|restart|continue|increase|decrease|double|halve)\b",
r"\bcan\s+i\s+(stop|start|take|skip|restart|continue|increase|decrease|double|halve)\b",
r"\bwhat\s+(dose|dosage)\b",
r"\bprescribe\b",
r"\brecommend\s+(a|another|different)?\s*(medication|drug|dose|dosage)\b",
]
ADR_ALLOWED_KEYWORDS = {
"adr", "adverse", "reaction", "side effect", "symptom", "symptoms", "medication",
"medicine", "drug", "dose", "timing", "rash", "hives", "nausea", "vomit",
"diarrhea", "pain", "swelling", "breathing", "dizzy", "doctor", "clinician",
"pharmacist", "report", "medwatch", "fda", "3500b", "allergy", "allergies",
"hospital", "urgent", "severe", "mild", "moderate", "biometric", "heart rate",
"sleep", "mobility", "missing", "clarify", "form",
}
def _openai_client():
if not ENABLE_LIVE_AGENT or not os.getenv("OPENAI_API_KEY"):
return None
try:
from openai import OpenAI
return OpenAI()
except Exception:
return None
def _friendly_live_ai_error(exc: Exception) -> str:
text = str(exc).lower()
if "insufficient_quota" in text or "exceeded your current quota" in text or "error code: 429" in text:
return (
"Live AI Q&A is connected, but the OpenAI API key has no available quota right now. "
"The local SentinelPlus triage workflow is still available. To enable live responses, "
"update billing/quota for the OpenAI project behind the Hugging Face `OPENAI_API_KEY` secret "
"or replace that secret with a key that has active quota."
)
if "authentication" in text or "api key" in text or "401" in text:
return (
"Live AI Q&A could not authenticate with OpenAI. Check the Hugging Face `OPENAI_API_KEY` "
"secret and make sure it is current."
)
return (
"Live AI Q&A is temporarily unavailable. The local SentinelPlus triage workflow is still available, "
"and all outputs continue to require human clinical review."
)
def _list_text(items: List[Dict], name_key: str) -> str:
return "\n".join(
f"- {item.get(name_key, 'Not listed')}, {item.get('Dose', 'dose not listed')}, "
f"started {item.get('Start Date', 'date not listed')} ({item.get('Status', 'status not listed')})"
for item in items
) or "None listed"
def fallback_form_3500b_fields(case: Dict, severity: Dict) -> Dict:
profile = case["profile"]
rx = case["medications"]["rx"]
otc = case["medications"]["otc"]
testimony = case.get("testimony", "")
suspected = case.get("suspected_medication", "")
suspected_l = suspected.lower()
primary_rx = next(
(m for m in rx if suspected_l and suspected_l in m.get("Medication", "").lower()),
rx[0] if rx else {},
)
primary_otc = next(
(m for m in otc if suspected_l and suspected_l in m.get("Product", "").lower()),
{},
)
primary_product = primary_rx or primary_otc
product = case.get("product_details", {})
reporter = case.get("reporter", {})
is_severe = severity["label"] == "Severe ADR"
hospitalized = "hospital" in testimony.lower() and is_severe
name_parts = case["patient_name"].strip().split()
return {
"problem_type_hurt_or_side_effect": True,
"problem_type_product_use_error": False,
"problem_type_product_quality": False,
"problem_type_switching_maker": False,
"outcome_hospitalization": hospitalized,
"outcome_required_help_prevent_harm": not is_severe,
"outcome_disability": False,
"outcome_birth_defect": False,
"outcome_life_threatening": is_severe,
"outcome_death": False,
"outcome_other_serious_event": False,
"date_problem_occurred": case["checkins"][0]["Date"] if case.get("checkins") else "Not on file",
"event_narrative": testimony[:4000],
"relevant_tests": "Vitals, medication list, allergy list, and encounter notes available in EHR.",
"product_available": product.get("product_available", "Yes"),
"product_picture_available": product.get("product_picture_available", "No"),
"product_name": suspected or primary_rx.get("Medication") or primary_otc.get("Product", "Not listed"),
"place_and_date_of_purchase": product.get("place_and_date_of_purchase", "Patient pharmacy record on file"),
"therapy_ongoing": primary_rx.get("Status", "").lower() in ("ongoing", "continuing"),
"manufacturer": product.get("manufacturer", "Manufacturer from product label on file"),
"product_type": product.get("product_type", "Drug or Biologic"),
"expiration_date": product.get("expiration_date", "On file"),
"lot_number": product.get("lot_number", "On file"),
"ndc_number": product.get("ndc_number", "On file"),
"strength": primary_product.get("Dose", "Not listed"),
"quantity": product.get("quantity", "1 dose"),
"frequency": product.get("frequency", primary_product.get("Dose", "As directed")),
"route": product.get("route", "By mouth"),
"therapy_start_date": primary_product.get("Start Date", "Not listed"),
"therapy_stop_date": product.get("therapy_stop_date", "Ongoing at time of report"),
"therapy_reduced_date": product.get("therapy_reduced_date", "Not applicable"),
"duration": product.get("duration", "On file"),
"reason_for_use": profile.get("condition", "Not on file"),
"problem_stopped_after_reduced_or_stopped": product.get("problem_stopped_after_reduced_or_stopped", "On file"),
"problem_returned_after_restart": product.get("problem_returned_after_restart", "Did not restart"),
"patient_initials": profile.get("initials") or ". ".join(p[0].upper() for p in name_parts if p and p[0].isalpha()) + ".",
"patient_sex": profile.get("sex", "Not on file"),
"patient_age": profile.get("age", "Not on file"),
"patient_dob": profile.get("dob", "Not on file"),
"patient_weight": profile.get("weight", "Not on file"),
"patient_race_ethnicity": profile.get("race", "Not on file"),
"known_medical_conditions": profile.get("relevant_history", "Not on file"),
"allergies": profile.get("allergies", "Not on file"),
"other_important_information": case["biometrics"].get("summary", "Not on file"),
"otc_medications": "\n".join(f"- {m['Product']}, {m['Dose']}" for m in otc) or "None listed",
"current_prescriptions": _list_text(rx, "Medication"),
"reporter_last_name": reporter.get("last_name", "SentinelPlus"),
"reporter_first_name": reporter.get("first_name", "Automated System"),
"reporter_address": reporter.get("address", profile.get("address", "On file")),
"reporter_city_state": reporter.get("city_state", profile.get("city_state", "On file")),
"reporter_zip": reporter.get("zip", profile.get("zip", "On file")),
"reporter_country": reporter.get("country", profile.get("country", "United States")),
"reporter_phone": reporter.get("phone", profile.get("phone", "On file")),
"reporter_email": reporter.get("email", profile.get("email", "On file")),
"reporter_today_date": reporter.get("today_date", "2026-05-13"),
"reported_to_manufacturer": reporter.get("reported_to_manufacturer", False),
"do_not_disclose_identity": reporter.get("do_not_disclose_identity", False),
}
def build_agent_context(severity: Dict, extraction: Dict, case: Dict) -> Dict:
profile = case["profile"]
return {
"purpose": "ADR documentation triage and mock FDA Form 3500B preparation only",
"severity_model_output": severity,
"ner_evidence": extraction,
"patient_profile": {
"age": profile.get("age"),
"sex": profile.get("sex"),
"height": profile.get("height"),
"weight": profile.get("weight"),
"race_ethnicity": profile.get("race"),
"condition": profile.get("condition"),
"allergies": profile.get("allergies"),
"relevant_history": profile.get("relevant_history"),
},
"suspected_medication": case.get("suspected_medication"),
"medications": case["medications"],
"product_details": case.get("product_details", {}),
"biometrics": case["biometrics"],
"checkins": case.get("checkins", []),
"reporter": case.get("reporter", {}),
"form_3500b_prefill": fallback_form_3500b_fields(case, severity),
"patient_testimony": case.get("testimony", ""),
"allowed_form": "FDA Form 3500B consumer voluntary report fields from Sections A, B, C, E, and F",
}
def is_question_in_scope(question: str) -> Tuple[bool, str]:
q = question.lower().strip()
if not q:
return False, "Ask a question about symptoms, medication context, missing report details, or the ADR situation."
if any(word in q for word in OFF_TOPIC_KEYWORDS) and not any(word in q for word in ADR_ALLOWED_KEYWORDS):
return False, "I can only help with ADR documentation, symptom clarification, medication-context questions, and MedWatch report preparation."
if any(re.search(pattern, q) for pattern in PRESCRIPTION_ADVICE_PATTERNS):
return False, (
"I cannot recommend starting, stopping, changing, or dosing a medication. "
"For medication decisions, contact the prescribing clinician or pharmacist. "
"If symptoms feel severe or life-threatening, seek urgent medical care."
)
if not any(word in q for word in ADR_ALLOWED_KEYWORDS):
return False, "I can only answer questions related to this ADR case and report preparation."
return True, ""
def heuristic_severity_from_text(text: str) -> Dict:
t = text.lower()
severe_cues = [
"911", "emergency", "emergency room", "emergency department", "urgent care",
"could barely stand", "trouble walking", "difficulty walking", "swollen",
"swelling", "breathing faster", "breathing problems", "chest", "lightheaded",
"face looked puffy", "lips felt swollen",
]
score = 0.82 if any(cue in t for cue in severe_cues) else 0.18
label = "Severe ADR" if score >= 0.5 else "No Severe ADR Detected"
return {
"label": label,
"probability": score,
"drivers": [],
"note": "Fallback severity estimate from testimony cues.",
}
def _terms_found(text: str, terms: List[str]) -> List[str]:
t = text.lower()
found = [term for term in terms if term.lower() in t]
return sorted(set(found), key=lambda item: (t.find(item.lower()), item))
def run_severity_model(text: str) -> Dict:
"""
Binary ADR severity classifier.
SAFE (0) = no severe ADR, INJECTION (1) = severe ADR detected.
"""
try:
pipe = _get_severity_pipe()
raw = pipe(text)
except Exception:
return heuristic_severity_from_text(text)
# top_k=None returns [[{...}, {...}]]; normalize to a flat list of score dicts
result = raw[0] if isinstance(raw[0], list) else raw
inj_entry = next(
(s for s in result if "INJECTION" in s["label"].upper() or s["label"] in ("LABEL_1", "1")),
None,
)
inj_score = inj_entry["score"] if inj_entry else max(s["score"] for s in result)
label = "Severe ADR" if inj_score >= 0.5 else "No Severe ADR Detected"
return {
"label": label,
"probability": round(inj_score, 3),
"drivers": [], # populated from NER output in analyze()
"note": "Output from trained DeBERTa v2 ADR severity classifier (SAFE / INJECTION).",
}
def run_medical_extraction_model(text: str) -> Dict:
"""Extract medical entities using Clinical-AI-Apollo/Medical-NER."""
try:
pipe = _get_ner_pipe()
entities = pipe(text)
except Exception:
entities = []
medications, symptoms, timing, patient_context = [], [], [], []
for ent in entities:
grp = ent["entity_group"].lower()
word = ent["word"].strip().strip("##")
if not word:
continue
if any(k in grp for k in _DRUG_KEYS):
medications.append(word)
elif any(k in grp for k in _DISEASE_KEYS):
patient_context.append(word)
elif any(k in grp for k in _TIMING_KEYS):
timing.append(word)
else:
symptoms.append(word)
t = text.lower()
for pattern in TIMING_PATTERNS:
timing.extend(re.findall(pattern, t, flags=re.IGNORECASE))
medications.extend(_terms_found(text, MEDICATION_TERMS))
symptoms.extend(_terms_found(text, SYMPTOM_TERMS))
return {
"medications": sorted(set(medications)) or ["Not detected"],
"symptoms": sorted(set(symptoms)) or ["Not detected"],
"timing": sorted(set(timing)) or ["Not detected"],
"patient_context": sorted(set(patient_context)) or ["Not detected"],
}
# ---------------------------------------------------------------------
# AGENTIC TRIAGE LAYER
# ---------------------------------------------------------------------
def run_live_triage_agent(severity: Dict, extraction: Dict, case: Dict) -> Optional[Dict]:
client = _openai_client()
if client is None:
return None
try:
from pydantic import BaseModel, Field
class Form3500BFields(BaseModel):
problem_type_hurt_or_side_effect: bool
problem_type_product_use_error: bool
problem_type_product_quality: bool
problem_type_switching_maker: bool
outcome_hospitalization: bool
outcome_required_help_prevent_harm: bool
outcome_disability: bool
outcome_birth_defect: bool
outcome_life_threatening: bool
outcome_death: bool
outcome_other_serious_event: bool
date_problem_occurred: str
event_narrative: str = Field(max_length=4000)
relevant_tests: str
product_available: str
product_picture_available: str
product_name: str
place_and_date_of_purchase: str
therapy_ongoing: bool
manufacturer: str
product_type: str
expiration_date: str
lot_number: str
ndc_number: str
strength: str
quantity: str
frequency: str
route: str
therapy_start_date: str
therapy_stop_date: str
therapy_reduced_date: str
duration: str
reason_for_use: str
problem_stopped_after_reduced_or_stopped: str
problem_returned_after_restart: str
patient_initials: str
patient_sex: str
patient_age: str
patient_dob: str
patient_weight: str
patient_race_ethnicity: str
known_medical_conditions: str
allergies: str
other_important_information: str
otc_medications: str
current_prescriptions: str
reporter_last_name: str
reporter_first_name: str
reporter_address: str
reporter_city_state: str
reporter_zip: str
reporter_country: str
reporter_phone: str
reporter_email: str
reporter_today_date: str
reported_to_manufacturer: bool
do_not_disclose_identity: bool
class AgentTriageOutput(BaseModel):
priority: Literal[
"Urgent provider review",
"Provider review - non-urgent",
"Continue monitoring / routine review",
]
next_step: str
explanation: str
missing: List[str]
form_3500b: Form3500BFields
response = client.responses.parse(
model=LIVE_AGENT_MODEL,
input=[
{"role": "system", "content": TRIAGE_AGENT_SYSTEM_PROMPT},
{
"role": "user",
"content": json.dumps(
build_agent_context(severity, extraction, case),
ensure_ascii=True,
),
},
],
text_format=AgentTriageOutput,
max_output_tokens=2500,
safety_identifier=f"sentinelplus_demo_{case.get('patient_name', 'unknown').replace(' ', '_')}",
)
parsed = response.output_parsed
if parsed is None:
return None
result = parsed.model_dump()
result["source"] = "live_ai_agent"
return result
except Exception:
return None
def triage_agent(severity: Dict, extraction: Dict, case: Dict) -> Dict:
live = run_live_triage_agent(severity, extraction, case)
if live:
return live
is_severe = severity["label"] == "Severe ADR"
missing = []
if extraction["medications"] == ["Not detected"]:
missing.append("Confirm suspected medication name")
if extraction["timing"] == ["Not detected"]:
missing.append("Confirm symptom onset date or timing after medication use")
if extraction["symptoms"] == ["Not detected"]:
missing.append("Confirm symptom description")
if not case["medications"]["otc"]:
missing.append("Confirm OTC medication and supplement use")
if is_severe:
priority = "Urgent provider review"
next_step = "Notify care team immediately and prepare high-priority ADR report for human review."
else:
priority = "Provider review — non-urgent"
next_step = "Continue monitoring. Prepare ADR report and confirm any missing details before submission."
return {
"priority": priority,
"next_step": next_step,
"missing": missing or ["No major missing fields detected"],
"explanation": build_agent_explanation(severity, extraction, case),
"form_3500b": fallback_form_3500b_fields(case, severity),
"source": "local_guardrailed_fallback",
}
def build_agent_explanation(severity: Dict, extraction: Dict, case: Dict) -> str:
label = severity["label"]
driver_text = ", ".join(severity["drivers"])
bio_flags = ", ".join(case["biometrics"]["flags"]) or "none flagged"
meds = ", ".join(extraction["medications"])
symptoms = ", ".join(extraction["symptoms"])
timing = ", ".join(extraction["timing"])
return (
f"Severity was classified as **{label}** because the testimony included: {driver_text}. "
f"SentinelPlus also considered medication context ({meds}), symptoms ({symptoms}), "
f"timing ({timing}), and biometric flags ({bio_flags})."
)
# ---------------------------------------------------------------------
# DISPLAY BUILDERS
# ---------------------------------------------------------------------
def build_severity_html(severity: Dict) -> str:
label = severity["label"]
is_severe = label == "Severe ADR"
color = "#c0392b" if is_severe else "#27ae60"
bg = "#fdf0ef" if is_severe else "#edfaf1"
icon = "🚨" if is_severe else "✅"
pulse = "animation:pulse 1.5s ease-in-out infinite;" if is_severe else ""
drivers_html = "".join(
f'
{d}
'
for d in severity.get("drivers", [])
)
return f"""
"""
def build_fda_3500b_html(case: Dict, severity: Dict, form_fields: Optional[Dict] = None) -> str:
profile = case["profile"]
rx = case["medications"]["rx"]
otc = case["medications"]["otc"]
testimony = case.get("testimony", "")
form = form_fields or fallback_form_3500b_fields(case, severity)
name_parts = case["patient_name"].strip().split()
initials = ". ".join(p[0].upper() for p in name_parts if p and p[0].isalpha()) + "."
is_severe = severity["label"] == "Severe ADR"
hospitalized = "hospital" in testimony.lower() and is_severe
def chk(val: bool) -> str:
return "☑" if val else "☐"
def filled(val: str) -> str:
return (
f'{escape(str(val))}'
)
def nof() -> str:
return 'Not on file'
def field(name: str, fallback: str = "Not on file") -> str:
value = form.get(name, fallback)
if value is None or value == "":
return fallback
return str(value)
def bool_field(name: str, fallback: bool = False) -> bool:
return bool(form.get(name, fallback))
date_occurred = field("date_problem_occurred", case["checkins"][0]["Date"] if case.get("checkins") else "Not on file")
narrative = escape(field("event_narrative", testimony[:4000])[:4000]) if testimony or form else ""
primary_rx = rx[0] if rx else {}
is_ongoing = bool_field("therapy_ongoing", primary_rx.get("Status", "").lower() in ("ongoing", "continuing"))
otc_text = "\n".join(f"• {m['Product']}, {m['Dose']}" for m in otc) or "None listed"
rx_text = "\n".join(
f"• {m['Medication']}, {m['Dose']}, started {m['Start Date']} ({m['Status']})"
for m in rx
) or "None listed"
css = """"""
return css + f"""
Form FDA 3500B (09/2025) | OMB Control No. 0910-0291 | Expiration Date: 09/30/2027 | Pre-filled by SentinelPlus - Demonstration only
FDA
MedWatch FORM 3500B
U.S. Dept. of Health and Human Services - Food and Drug Administration
Consumer Voluntary Reporting of Adverse Events, Product Problems and Medication Errors
Section A – About the Problem
1. What kind of problem was it?
{chk(bool_field("problem_type_hurt_or_side_effect", True))} Were hurt or had a bad side effect
{chk(bool_field("problem_type_product_use_error"))} Used a product incorrectly
{chk(bool_field("problem_type_product_quality"))} Noticed a problem with quality
{chk(bool_field("problem_type_switching_maker"))} Problems after switching product maker
2. Did any of the following happen?
{chk(bool_field("outcome_hospitalization", hospitalized))} Hospitalization
{chk(bool_field("outcome_required_help_prevent_harm", not is_severe))} Required help to prevent permanent harm
{chk(bool_field("outcome_disability"))} Disability or health problem
{chk(bool_field("outcome_birth_defect"))} Birth defect
{chk(bool_field("outcome_life_threatening", is_severe))} Life-threatening
{chk(bool_field("outcome_death"))} Death
{chk(bool_field("outcome_other_serious_event"))} Other serious medical event
3. Date the problem occurred
{filled(date_occurred)}
4. Tell us what happened
{narrative}
5. Relevant tests / laboratory results
{escape(field("relevant_tests"))}
Section B – Product Availability
1. Do you still have the product?
{filled(field("product_available"))}
2. Do you have a picture of the product?
{filled(field("product_picture_available"))}
Section C – About the Products
1. Name of product
{filled(field("product_name", primary_rx.get("Medication", "Not listed"))) if primary_rx or field("product_name") != "Not on file" else nof()}
{filled(field("patient_dob", profile.get("dob",""))) if field("patient_dob", profile.get("dob","")) else nof()}
5. Weight
{filled(field("patient_weight", profile.get("weight",""))) if field("patient_weight", profile.get("weight","")) else nof()}
6. Race / Ethnicity
{chk(field("patient_race_ethnicity", profile.get("race",""))=="American Indian or Alaska Native")} American Indian or Alaska Native
{chk(field("patient_race_ethnicity", profile.get("race",""))=="Asian")} Asian
{chk(field("patient_race_ethnicity", profile.get("race",""))=="Black or African American")} Black or African American
{chk(field("patient_race_ethnicity", profile.get("race",""))=="Hispanic or Latino")} Hispanic or Latino
{chk(field("patient_race_ethnicity", profile.get("race",""))=="White")} White
7. Known medical conditions
{filled(field("known_medical_conditions", profile.get("relevant_history",""))) if field("known_medical_conditions", profile.get("relevant_history","")) else nof()}
8. Allergies
{filled(field("allergies", profile.get("allergies",""))) if field("allergies", profile.get("allergies","")) else nof()}
10. OTC medications, vitamins, supplements
{escape(otc_text)}
11. Current prescription medications
{escape(rx_text)}
Section F – About the Person Filling Out This Form
{filled(field("reporter_today_date", "Pre-filled by SentinelPlus"))}
10. Reported to manufacturer?
{chk(bool_field("reported_to_manufacturer"))} Yes {chk(not bool_field("reported_to_manufacturer"))} No
11. Identity disclosure preference
{chk(bool_field("do_not_disclose_identity"))} Do not disclose identity
"""
def highlight_text(text: str, extraction: Dict, severity: Dict) -> str:
terms = []
for bucket in ["symptoms", "medications", "timing"]:
terms.extend(v for v in extraction.get(bucket, []) if v != "Not detected")
terms.extend(severity.get("drivers", []))
terms = sorted(set(terms), key=len, reverse=True)
safe = escape(text)
for term in terms:
if not term:
continue
pattern = re.compile(re.escape(escape(term)), re.IGNORECASE)
safe = pattern.sub(lambda m: f"{m.group(0)}", safe)
return f"""
{safe}
Highlighted terms were detected by the extraction model or flagged as severity evidence cues.
"""
def _chat_append(history: List[Dict], question: str, answer: str) -> List[Dict]:
return history + [
{"role": "user", "content": question or ""},
{"role": "assistant", "content": answer},
]
def answer_triage_question(question: str, history: List[Dict], analysis_state: Dict, testimony: str):
history = history or []
allowed, message = is_question_in_scope(question or "")
if not allowed:
return "", _chat_append(history, question, message)
try:
analysis_state = analysis_state or {}
if not analysis_state.get("triage"):
return "", _chat_append(
history,
question,
"Run Analyze Updated Testimony first so I can answer from the current ADR analysis.",
)
if (testimony or "").strip() != analysis_state.get("testimony", ""):
return "", _chat_append(
history,
question,
"The testimony has changed since the last analysis. Click Analyze Updated Testimony, then ask your question again.",
)
case = analysis_state["case"]
severity = analysis_state["severity"]
extraction = analysis_state["extraction"]
triage = analysis_state["triage"]
client = _openai_client()
if client is None:
fallback = (
"Live AI Q&A is not connected yet. Based on the local triage output, "
f"priority is: {triage['priority']}. The safest next step is: {triage['next_step']} "
"I can help identify missing report details once OPENAI_API_KEY is configured."
)
return "", _chat_append(history, question, fallback)
response = client.responses.create(
model=LIVE_AGENT_MODEL,
input=[
{"role": "system", "content": TRIAGE_QA_SYSTEM_PROMPT},
{
"role": "user",
"content": json.dumps(
{
"case_context": build_agent_context(severity, extraction, case),
"triage_output": {
"priority": triage.get("priority"),
"next_step": triage.get("next_step"),
"explanation": triage.get("explanation"),
"missing": triage.get("missing"),
},
"recent_chat": history[-8:],
"user_question": question,
},
ensure_ascii=True,
),
},
],
max_output_tokens=500,
safety_identifier=f"sentinelplus_qa_{case.get('patient_name', 'unknown').replace(' ', '_')}",
)
answer = getattr(response, "output_text", "").strip()
if not answer:
answer = "I could not generate a live response. Please ask a question about symptoms, timing, medications, or missing report details."
return "", _chat_append(history, question, answer)
except Exception as exc:
return "", _chat_append(history, question, _friendly_live_ai_error(exc))
# ---------------------------------------------------------------------
# MAIN ANALYSIS PIPELINE
# ---------------------------------------------------------------------
def analyze(case_name: str, testimony: str):
try:
case = DEMO_CASES[case_name].copy()
testimony = (testimony or "").strip()
if not testimony:
msg = (
'