vin00d's picture
updates to app to display test options
fdfea43
Raw
History Blame Contribute Delete
16.3 kB
from __future__ import annotations
import re
from pathlib import Path
import chainlit as cl
from loguru import logger
from config import CLAIM_PACKETS_ROOT
from graphs import claims_graph, scheduling_graph
CLAIM_DEMO_SCENARIOS = [
{
"claim_id": "CLM-0001",
"scenario": "Clean professional claim",
"expected": "Clean pass; routed to clean_pass_auto_normalization",
"details": "MBR100239 | Bayview Orthopedics | CPT 99213 | ICD-10 M25.561",
},
{
"claim_id": "CLM-0002",
"scenario": "MRI likely needs prior authorization",
"expected": "Prior auth risk; missing priorAuthNumber; routed to prior_auth_exception_review",
"details": "MBR100548 | Golden State Imaging | CPT 73721 | ICD-10 M25.561",
},
{
"claim_id": "CLM-0003",
"scenario": "Coverage inactive on DOS",
"expected": "Eligibility gap; routed to claims_ops_exception_review",
"details": "MBR100791 | East Valley Cardiology | CPT 99214 | ICD-10 E11.9",
},
{
"claim_id": "CLM-0004",
"scenario": "Diagnosis/procedure mismatch",
"expected": "Coding mismatch risk; routed to claims_ops_exception_review",
"details": "MBR101044 | Sunrise PT Group | CPT 93000 | ICD-10 S83.241A",
},
{
"claim_id": "CLM-0005",
"scenario": "Possible duplicate claim",
"expected": "Duplicate risk against CLM-0006; routed to claims_ops_duplicate_review",
"details": "MBR100239 | Bayview Orthopedics | CPT 97110 | ICD-10 M54.50",
},
{
"claim_id": "CLM-0006",
"scenario": "Possible duplicate claim partner",
"expected": "Duplicate risk against CLM-0005; routed to claims_ops_duplicate_review",
"details": "MBR100548 | Golden State Imaging | CPT 97110 | ICD-10 M54.50",
},
{
"claim_id": "CLM-0007",
"scenario": "Attachment control number mismatch",
"expected": "Attachment/documentation mismatch; routed to claims_ops_exception_review",
"details": "MBR100791 | East Valley Cardiology | CPT 27447 | ICD-10 M17.11",
},
{
"claim_id": "CLM-0008",
"scenario": "Out-of-network specialist referral",
"expected": "OON/referral risk; routed to claims_ops_exception_review",
"details": "MBR101044 | Sunrise PT Group | CPT 99204 | ICD-10 I48.91",
},
]
SCHEDULING_DEMO_SCENARIOS = [
{
"case_id": "C9001",
"referral_id": "R5001",
"member_id": "M1001",
"specialty": "Cardiology",
"request": "Schedule member M1001 with an in-network cardiology specialist near San Ramon next week. This is urgent and SMS confirmation is preferred.",
"desired_outcome": "schedule earliest in-network cardiology visit within 7 days",
"constraints": "HMO referral active; urgent; San Ramon preferred; SMS confirmation",
"expected": "Schedule earliest in-network cardiology visit within 7 days using active HMO referral R5001; urgent; San Ramon preferred; SMS confirmation.",
},
{
"case_id": "C9002",
"referral_id": "R5004",
"member_id": "M1004",
"specialty": "Cardiology",
"request": "Schedule member M1004 for a post-discharge in-network cardiology visit before the authorization expires. The location must be wheelchair accessible and within 10 miles if possible, with phone confirmation.",
"desired_outcome": "schedule post-discharge cardiology before authorization expires",
"constraints": "wheelchair accessible; max travel 10 miles; phone confirmation",
"expected": "Schedule post-discharge cardiology for M1004 before authorization A7003 expires; wheelchair-accessible location within 10 miles; phone confirmation.",
},
{
"case_id": "C9003",
"referral_id": "R5005",
"member_id": "M1005",
"specialty": "OB-GYN",
"request": "Schedule member M1005 with an in-network OB-GYN for high-risk pregnancy care, but only if prior authorization is approved and required documentation is complete.",
"desired_outcome": "do not schedule until PA approved or missing documentation collected",
"constraints": "EPO in-network only; high-risk OB-GYN PA pending",
"expected": "Do not schedule until PA is approved and documentation is complete; EPO in-network only; high-risk OB-GYN referral R5005 / auth A7004 still pending.",
},
{
"case_id": "C9004",
"referral_id": "R5006",
"member_id": "M1006",
"specialty": "Neurology",
"request": "Schedule member M1006 with a neurology specialist. If the closest neurologist is not accepting new patients, route this as an exception.",
"desired_outcome": "route exception because closest neurologist is not accepting new patients",
"constraints": "PPO plan; neurology requested; provider new-patient status false",
"expected": "Route as exception when the closest neurologist is not accepting new patients; PPO plan with neurology referral R5006.",
},
{
"case_id": "C9005",
"referral_id": "R5008",
"member_id": "M1008",
"specialty": "Behavioral Health",
"request": "Schedule member M1008 for a telehealth behavioral health or psychiatry evaluation, preferably late afternoon.",
"desired_outcome": "schedule telehealth psychiatry/behavioral health evaluation",
"constraints": "late afternoon; telehealth acceptable; Student PPO",
"expected": "Schedule telehealth behavioral health/psychiatry evaluation for M1008; prefer late afternoon; Student PPO with referral R5008.",
},
]
def find_scheduling_scenario(result: dict) -> dict | None:
"""Match a graph result back to a demo scenario by member id or request text."""
member_id = result.get("member_id") or result.get("extracted_request", {}).get(
"member_id"
)
if member_id:
for item in SCHEDULING_DEMO_SCENARIOS:
if item.get("member_id") == member_id:
return item
request_text = (
result.get("request_text")
or result.get("extracted_request", {}).get("raw_text")
or ""
).strip()
if request_text:
for item in SCHEDULING_DEMO_SCENARIOS:
if item.get("request", "").strip() == request_text:
return item
return None
def render_claim_result(result: dict) -> str:
"""Render scenario-specific claims output for Chainlit."""
claim = result.get("canonical_claim", {})
validation = result.get("validation_results", {})
final = result.get("final_summary", {})
risk = result.get("denial_risk") or final.get("risk", {})
route = result.get("route") or final.get("route", {})
llm_decision = result.get("llm_decision") or final.get("llm_decision", {})
rag_decision = result.get("rag_decision") or final.get("rag_decision", {})
risks = risk.get("risks", []) if isinstance(risk, dict) else []
risk_rows = (
"\n".join(f"- `{r.get('risk')}` — {r.get('severity')}" for r in risks)
or "- None detected"
)
cpts = ", ".join(claim.get("service", {}).get("cpt_codes", []) or [])
icds = ", ".join(claim.get("service", {}).get("icd10_codes", []) or [])
dates = ", ".join(claim.get("service", {}).get("dates", []) or [])
return f"""
## Claims workflow result: `{claim.get('claim_id', result.get('case_id', 'unknown'))}`
| Field | Value |
|---|---|
| Member | `{claim.get('member', {}).get('member_id')}` |
| Provider | {claim.get('provider', {}).get('name')} |
| Network status | `{claim.get('provider', {}).get('network_status')}` |
| CPT | `{cpts}` |
| ICD-10 | `{icds}` |
| Date of service | `{dates}` |
| Eligibility | `{validation.get('eligible')}` |
| Provider valid | `{validation.get('provider_valid')}` |
| Duplicate risk | `{validation.get('duplicate_risk')}` |
| Risk level | `{risk.get('risk_level', llm_decision.get('risk_level', 'unknown'))}` |
| Route | `{route.get('route', llm_decision.get('recommended_route', 'unknown'))}` |
### Risks detected
{risk_rows}
### Retrieval / decision details
- Policy RAG results: `{final.get('policy_context_count', len(result.get('policy_results', {}).get('results', [])))}`
- Similar exception results: `{final.get('similar_exception_count', len(result.get('exception_results', {}).get('results', [])))}`
- RAG decision: {rag_decision.get('reason', 'not available')}
### Audit explanation
{llm_decision.get('explanation', 'Deterministic tools completed the claim workflow.')}
""".strip()
def _format_appointment_option(option: dict, index: int) -> str:
"""Format a ranked option whether it came from state or a summarized payload."""
slot = option.get("slot")
if isinstance(slot, dict):
start = slot.get("start")
location = slot.get("location")
city = slot.get("city")
provider = slot.get("provider")
else:
start = option.get("start_datetime") or option.get("start")
location = option.get("location_name") or option.get("location")
if isinstance(location, dict):
location = location.get("location_name")
city = option.get("city")
provider = option.get("provider_name") or option.get("provider")
rank = option.get("rank", index + 1)
if not any([start, location, city, provider]):
return f"- Option {rank}: appointment details unavailable"
return f"- Option {rank}: {start} at {location} ({city}) with {provider}"
def _scheduling_appointment_options(result: dict) -> list:
"""Prefer graph state options; final_summary may contain LLM placeholders."""
state_options = result.get("appointment_options") or []
final_options = result.get("final_summary", {}).get("appointment_options") or []
def has_details(option: dict) -> bool:
slot = option.get("slot")
if isinstance(slot, dict) and any(slot.values()):
return True
return bool(
option.get("start_datetime")
or option.get("start")
or option.get("location_name")
or option.get("provider_name")
)
if state_options and any(has_details(option) for option in state_options):
return state_options
if final_options and any(has_details(option) for option in final_options):
return final_options
return state_options or final_options
def render_scheduling_result(result: dict) -> str:
"""Render scenario-specific scheduling output for Chainlit."""
final = result.get("final_summary", {})
extracted = result.get("extracted_request", {})
readiness = result.get("schedule_readiness", {})
scenario = find_scheduling_scenario(result)
member_id = result.get("member_id") or extracted.get("member_id")
options = _scheduling_appointment_options(result)
option_rows = (
"\n".join(
_format_appointment_option(option, i) for i, option in enumerate(options)
)
or "- No appointment options returned"
)
issues = (
"\n".join(f"- {issue}" for issue in readiness.get("issues", [])) or "- None"
)
return f"""
## Scheduling workflow result
| Field | Value |
|---|---|
| Case | `{scenario['case_id'] if scenario else 'n/a'}` |
| Member | `{member_id}` |
| Specialty | `{extracted.get('specialty') or (scenario or {}).get('specialty', '')}` |
| Preferred city | `{extracted.get('city') or 'any'}` |
| Ready to schedule | `{readiness.get('ready_to_schedule')}` |
| Recommended action | `{final.get('recommended_action')}` |
### Readiness issues
{issues}
### Appointment options
{option_rows}
### Summary
{final.get('member_facing_summary', 'Deterministic scheduling workflow completed.')}
""".strip()
def render_claim_demo_scenarios() -> str:
rows = [
"| Claim to enter | Scenario | Expected outcome |",
"|---|---|---|",
]
for item in CLAIM_DEMO_SCENARIOS:
rows.append(
f"| `{item['claim_id']}` | {item['scenario']} | {item['expected']} |"
)
return "\n".join(rows)
def render_scheduling_demo_scenarios() -> str:
rows = [
"| Case | Member | Request | Expected outcome |",
"|---|---|---|---|",
]
for item in SCHEDULING_DEMO_SCENARIOS:
rows.append(
f"| {item['case_id']} | {item['member_id']} | `{item['request']}` | {item['expected']} |"
)
return "\n".join(rows)
def resolve_claim_packet_path(user_input: str) -> Path:
"""Resolve a claim number like CLM-0004 to the local packet folder path."""
value = user_input.strip()
# If the user still pastes a full path, keep supporting it.
direct_path = Path(value)
if direct_path.exists():
return direct_path
normalized = value.upper().replace("_", "-")
match = re.search(r"CLM-?0*(\d+)", normalized)
if match:
claim_number = int(match.group(1))
canonical = f"CLM-{claim_number:04d}"
candidates = [
CLAIM_PACKETS_ROOT / canonical,
CLAIM_PACKETS_ROOT / canonical.lower(),
CLAIM_PACKETS_ROOT / canonical.replace("-", "_"),
CLAIM_PACKETS_ROOT / canonical.replace("-", "_").lower(),
CLAIM_PACKETS_ROOT / f"clm_{claim_number:03d}",
CLAIM_PACKETS_ROOT / f"clm-{claim_number:04d}",
]
for candidate in candidates:
if candidate.exists():
return candidate
return CLAIM_PACKETS_ROOT / canonical
# Fallback: append raw input to the packet root.
return CLAIM_PACKETS_ROOT / value
@cl.on_chat_start
async def on_chat_start():
cl.user_session.set("workflow", None)
actions = [
cl.Action(
name="claims",
payload={},
label="Prototype 1: Claims Ingestion & Normalization",
),
cl.Action(
name="scheduling",
payload={},
label="Prototype 2: Scheduling & Admin Assistant",
),
]
await cl.Message(
content="Choose a prototype to run.",
actions=actions,
).send()
@cl.action_callback("claims")
async def on_claims(action: cl.Action):
cl.user_session.set("workflow", "claims")
content = f"""
Claims workflow selected.
Enter only the claim number to run a scenario — for example `CLM-0004`.
{render_claim_demo_scenarios()}
"""
await cl.Message(content=content).send()
@cl.action_callback("scheduling")
async def on_scheduling(action: cl.Action):
cl.user_session.set("workflow", "scheduling")
content = f"""
Scheduling workflow selected.
Copy/paste one of the sample scheduling requests below.
{render_scheduling_demo_scenarios()}
"""
await cl.Message(content=content).send()
@cl.on_message
async def on_message(message: cl.Message):
workflow = cl.user_session.get("workflow")
if not workflow:
await on_chat_start()
return
if workflow == "claims":
claim_input = message.content.strip()
packet_path = resolve_claim_packet_path(claim_input)
logger.info(
f"Running claims graph for claim_input={claim_input}, packet_path={packet_path}"
)
if not packet_path.exists():
await cl.Message(
content=(
f"Claim packet not found for `{claim_input}`.\n\n"
f"I looked under: `{packet_path}`\n\n"
"Enter a claim number such as `CLM-0004`, or verify the packet folder exists under "
f"`{CLAIM_PACKETS_ROOT}`."
)
).send()
return
result = claims_graph.invoke(
{"selected_packet_path": str(packet_path), "messages": []}
)
await cl.Message(content="Claims workflow complete.").send()
await cl.Message(content=render_claim_result(result)).send()
return
if workflow == "scheduling":
logger.info("Running scheduling graph")
result = scheduling_graph.invoke(
{"request_text": message.content, "messages": []}
)
await cl.Message(content="Scheduling workflow complete.").send()
await cl.Message(content=render_scheduling_result(result)).send()
return