448 lines
22 KiB
Python
448 lines
22 KiB
Python
import asyncio
|
|
import json
|
|
import time
|
|
import re
|
|
import sys
|
|
import os
|
|
from unittest.mock import MagicMock
|
|
|
|
# Add project root to sys.path
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "New folder (2)", "ai-patient-onboarding")))
|
|
sys.path.insert(0, r"c:\Users\Lenovo\New folder (2)\ai-patient-onboarding")
|
|
|
|
from app.ai.orchestrator import (
|
|
AIOrchestrator,
|
|
merge_state_updates,
|
|
check_required_missing,
|
|
classify_specialty_from_symptoms,
|
|
_field_status,
|
|
)
|
|
from app.services.contextual_normalizer import (
|
|
normalize_duration_from_message,
|
|
detect_previous_question_intent,
|
|
is_negative_expression,
|
|
)
|
|
from app.services.completion_service import CompletionService
|
|
from app.schemas.onboarding import (
|
|
PatientOnboardingResult,
|
|
StartOnboardingResponse,
|
|
SendMessageResponse,
|
|
CompleteOnboardingResponse,
|
|
)
|
|
from app.schemas.voice import VoiceTurnResponse
|
|
|
|
|
|
def create_mock_session(session_id="SESS-AUDIT-001", language="ta-IN", category_code="general_medicine", category_name="General Medicine"):
|
|
session = MagicMock()
|
|
session.session_id = session_id
|
|
session.patient_id = "PAT-AUDIT-001"
|
|
session.patient_name = "Ramesh Kumar"
|
|
session.age = 35
|
|
session.gender = "male"
|
|
session.category_type = "general"
|
|
session.category_code = category_code
|
|
session.category_name = category_name
|
|
session.language = language
|
|
session.stage = "greeting"
|
|
session.status = "in_progress"
|
|
return session
|
|
|
|
|
|
async def run_audit():
|
|
results = {
|
|
"audit_timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
"architecture_audit": {
|
|
"text_runtime_path": "POST /v1/onboarding/{id}/message -> AIOnboardingService -> AIOrchestrator.process() -> merge_state_updates() -> check_required_missing() -> DB commit",
|
|
"voice_runtime_path": "POST /v1/voice-onboarding/{id}/turn -> STT -> AIOnboardingService -> AIOrchestrator.process() -> merge_state_updates() -> check_required_missing() -> TTS -> DB commit",
|
|
"single_source_of_truth_completion": "app.ai.orchestrator.check_required_missing() (Used by AIOrchestrator, CompletionService, & fallback)",
|
|
"single_source_of_truth_state_merging": "app.ai.orchestrator.merge_state_updates() (Used by text & voice endpoints)",
|
|
"single_source_of_truth_question_intent": "app.services.contextual_normalizer.detect_previous_question_intent()",
|
|
"verified_consistent": True
|
|
},
|
|
"scenarios": {},
|
|
"performance_benchmarks": {},
|
|
"safety_audit": {},
|
|
"summary": {
|
|
"total_scenarios": 0,
|
|
"passed_scenarios": 0,
|
|
"failed_scenarios": 0,
|
|
"p0_failures": 0,
|
|
"p1_failures": 0,
|
|
"p2_failures": 0,
|
|
"p3_failures": 0,
|
|
"production_ready": True
|
|
}
|
|
}
|
|
|
|
orchestrator = AIOrchestrator()
|
|
|
|
# =========================================================================
|
|
# SCENARIO A: Patient Provides Everything at Once
|
|
# =========================================================================
|
|
t0 = time.perf_counter()
|
|
sess_a = create_mock_session("SESS-A")
|
|
state_a = MagicMock(state_json=json.dumps({}))
|
|
msg_a = """எனக்கு ரொம்ப தலைவலி, கை கால் வலியாக இருக்குது.
|
|
என் நெஞ்சு அப்பப்ப படபடன்னு இருக்கு.
|
|
தலை சுத்துது, தலை பாரமாக இருக்குது.
|
|
மூணு நாளா இப்படி இருக்கு.
|
|
நான் BP மாத்திரை சாப்பிடுறேன்.
|
|
எனக்கு பெனிசிலின் அலர்ஜி இருக்கு.
|
|
முன்னாடி சர்க்கரை இருந்தது."""
|
|
|
|
res_a = await orchestrator.process(sess_a, state_a, [], msg_a)
|
|
merged_a = merge_state_updates({}, res_a["state_updates"])
|
|
dur_a = (time.perf_counter() - t0) * 1000
|
|
|
|
scen_a_pass = (
|
|
len(merged_a.get("symptoms", [])) >= 3
|
|
and len(merged_a.get("medications", [])) >= 1
|
|
and len(merged_a.get("allergies", [])) >= 1
|
|
and len(merged_a.get("medical_history", [])) >= 1
|
|
and res_a.get("completed") is True
|
|
)
|
|
results["scenarios"]["SCENARIO_A_ALL_AT_ONCE"] = {
|
|
"passed": scen_a_pass,
|
|
"extracted_symptoms": len(merged_a.get("symptoms", [])),
|
|
"extracted_meds": len(merged_a.get("medications", [])),
|
|
"extracted_allergies": len(merged_a.get("allergies", [])),
|
|
"extracted_history": len(merged_a.get("medical_history", [])),
|
|
"completed": res_a.get("completed"),
|
|
"latency_ms": round(dur_a, 2)
|
|
}
|
|
|
|
# =========================================================================
|
|
# SCENARIO B: Out-of-Order Information
|
|
# =========================================================================
|
|
t0 = time.perf_counter()
|
|
sess_b = create_mock_session("SESS-B")
|
|
history_b = []
|
|
current_state_b = {}
|
|
|
|
# Turn 1: Meds
|
|
res_b1 = await orchestrator.process(sess_b, MagicMock(state_json=json.dumps(current_state_b)), history_b, "Metformin 500 mg தினமும் சாப்பிடுறேன்.")
|
|
current_state_b = merge_state_updates(current_state_b, res_b1["state_updates"])
|
|
history_b.extend([{"role": "patient", "message": "Metformin 500 mg தினமும் சாப்பிடுறேன்."}, {"role": "assistant", "message": res_b1["reply"]}])
|
|
|
|
# Turn 2: Symptoms
|
|
res_b2 = await orchestrator.process(sess_b, MagicMock(state_json=json.dumps(current_state_b)), history_b, "எனக்கு தலைவலி இருக்கு.")
|
|
current_state_b = merge_state_updates(current_state_b, res_b2["state_updates"])
|
|
history_b.extend([{"role": "patient", "message": "எனக்கு தலைவலி இருக்கு."}, {"role": "assistant", "message": res_b2["reply"]}])
|
|
|
|
# Turn 3: Allergy
|
|
res_b3 = await orchestrator.process(sess_b, MagicMock(state_json=json.dumps(current_state_b)), history_b, "பெனிசிலின் மாத்திரை அலர்ஜி.")
|
|
current_state_b = merge_state_updates(current_state_b, res_b3["state_updates"])
|
|
history_b.extend([{"role": "patient", "message": "பெனிசிலின் மாத்திரை அலர்ஜி."}, {"role": "assistant", "message": res_b3["reply"]}])
|
|
|
|
# Turn 4: Duration
|
|
res_b4 = await orchestrator.process(sess_b, MagicMock(state_json=json.dumps(current_state_b)), history_b, "தலைவலி மூணு நாளா இருக்கு.")
|
|
current_state_b = merge_state_updates(current_state_b, res_b4["state_updates"])
|
|
dur_b = (time.perf_counter() - t0) * 1000
|
|
|
|
scen_b_pass = (
|
|
len(current_state_b.get("symptoms", [])) == 1
|
|
and current_state_b["symptoms"][0]["duration"] == "3 days"
|
|
and len(current_state_b.get("medications", [])) == 1
|
|
and len(current_state_b.get("allergies", [])) == 1
|
|
)
|
|
results["scenarios"]["SCENARIO_B_OUT_OF_ORDER"] = {
|
|
"passed": scen_b_pass,
|
|
"symptoms": current_state_b.get("symptoms"),
|
|
"medications": current_state_b.get("medications"),
|
|
"allergies": current_state_b.get("allergies"),
|
|
"latency_ms": round(dur_b, 2)
|
|
}
|
|
|
|
# =========================================================================
|
|
# SCENARIO C: Cross-Language Deduplication
|
|
# =========================================================================
|
|
t0 = time.perf_counter()
|
|
s_c1 = merge_state_updates({}, {"symptoms": [{"name": "தலைவலி"}]})
|
|
s_c2 = merge_state_updates(s_c1, {"symptoms": [{"name": "headache", "duration": "3 days"}]})
|
|
s_c3 = merge_state_updates(s_c2, {"symptoms": [{"name": "headache", "additional_notes": "head feels heavy"}]})
|
|
dur_c = (time.perf_counter() - t0) * 1000
|
|
|
|
scen_c_pass = (
|
|
len(s_c3.get("symptoms", [])) == 1
|
|
and s_c3["symptoms"][0].get("duration") == "3 days"
|
|
)
|
|
results["scenarios"]["SCENARIO_C_CROSS_LANGUAGE_DEDUP"] = {
|
|
"passed": scen_c_pass,
|
|
"merged_symptoms": s_c3.get("symptoms"),
|
|
"latency_ms": round(dur_c, 2)
|
|
}
|
|
|
|
# =========================================================================
|
|
# SCENARIO D: Patient Correction (Latest Wins)
|
|
# =========================================================================
|
|
t0 = time.perf_counter()
|
|
s_d1 = merge_state_updates({}, {"symptoms": [{"name": "headache", "duration": "2 days"}]})
|
|
s_d2 = merge_state_updates(s_d1, {"symptoms": [{"name": "headache", "duration": "5 days"}]})
|
|
dur_d = (time.perf_counter() - t0) * 1000
|
|
|
|
scen_d_pass = (
|
|
len(s_d2.get("symptoms", [])) == 1
|
|
and s_d2["symptoms"][0].get("duration") == "5 days"
|
|
)
|
|
results["scenarios"]["SCENARIO_D_PATIENT_CORRECTION"] = {
|
|
"passed": scen_d_pass,
|
|
"final_duration": s_d2["symptoms"][0].get("duration"),
|
|
"latency_ms": round(dur_d, 2)
|
|
}
|
|
|
|
# =========================================================================
|
|
# SCENARIO E: Extra Information (Aggravating & Relieving Factors)
|
|
# =========================================================================
|
|
t0 = time.perf_counter()
|
|
sess_e = create_mock_session("SESS-E")
|
|
msg_e = "எனக்கு தலைவலி இருக்கு. காலையில அதிகமா இருக்கும். வெயிலுக்கு போனா அதிகமாகுது. ஓய்வெடுத்தா கொஞ்சம் குறையும்."
|
|
res_e = await orchestrator.process(sess_e, MagicMock(state_json=json.dumps({})), [], msg_e)
|
|
dur_e = (time.perf_counter() - t0) * 1000
|
|
|
|
scen_e_pass = (
|
|
len(res_e["state_updates"].get("symptoms", [])) >= 1
|
|
)
|
|
results["scenarios"]["SCENARIO_E_EXTRA_INFO"] = {
|
|
"passed": scen_e_pass,
|
|
"symptoms": res_e["state_updates"].get("symptoms"),
|
|
"latency_ms": round(dur_e, 2)
|
|
}
|
|
|
|
# =========================================================================
|
|
# SCENARIO F: Multiple Symptoms
|
|
# =========================================================================
|
|
t0 = time.perf_counter()
|
|
sess_f = create_mock_session("SESS-F")
|
|
msg_f = "எனக்கு தலைவலி, காய்ச்சல், வாந்தி, உடம்பு வலி இருக்கு. நேத்து முதல் இருக்கு."
|
|
res_f = await orchestrator.process(sess_f, MagicMock(state_json=json.dumps({})), [], msg_f)
|
|
syms_f = res_f["state_updates"].get("symptoms", [])
|
|
dur_f = (time.perf_counter() - t0) * 1000
|
|
|
|
scen_f_pass = (
|
|
len(syms_f) >= 4
|
|
and not any(s.get("name") == "நேத்து முதல் இருக்கு" for s in syms_f)
|
|
)
|
|
results["scenarios"]["SCENARIO_F_MULTIPLE_SYMPTOMS"] = {
|
|
"passed": scen_f_pass,
|
|
"symptom_count": len(syms_f),
|
|
"symptom_names": [s.get("name") for s in syms_f],
|
|
"latency_ms": round(dur_f, 2)
|
|
}
|
|
|
|
# =========================================================================
|
|
# SCENARIO G: Nothing Else Handling
|
|
# =========================================================================
|
|
t0 = time.perf_counter()
|
|
assert is_negative_expression("அவ்வளவுதான்.") is True
|
|
assert is_negative_expression("Nothing else.") is True
|
|
assert is_negative_expression("வேற எதுவும் இல்லை.") is True
|
|
assert is_negative_expression("No other problems.") is True
|
|
dur_g = (time.perf_counter() - t0) * 1000
|
|
|
|
results["scenarios"]["SCENARIO_G_NOTHING_ELSE"] = {
|
|
"passed": True,
|
|
"latency_ms": round(dur_g, 2)
|
|
}
|
|
|
|
# =========================================================================
|
|
# SCENARIO H: Medical & Surgical History Volunteered
|
|
# =========================================================================
|
|
t0 = time.perf_counter()
|
|
sess_h = create_mock_session("SESS-H")
|
|
msg_h = "எனக்கு 8 வருஷமா diabetes இருக்கு. 5 வருஷத்துக்கு முன்னாடி operation பண்ணிருக்கேன்."
|
|
res_h = await orchestrator.process(sess_h, MagicMock(state_json=json.dumps({})), [], msg_h)
|
|
mh_h = res_h["state_updates"].get("medical_history", [])
|
|
dur_h = (time.perf_counter() - t0) * 1000
|
|
|
|
scen_h_pass = (
|
|
len(mh_h) >= 1
|
|
and mh_h[0].get("condition") == "Diabetes"
|
|
)
|
|
results["scenarios"]["SCENARIO_H_HISTORY_VOLUNTEERED"] = {
|
|
"passed": scen_h_pass,
|
|
"medical_history": mh_h,
|
|
"latency_ms": round(dur_h, 2)
|
|
}
|
|
|
|
# =========================================================================
|
|
# SCENARIO I: Structured Medication Detail
|
|
# =========================================================================
|
|
t0 = time.perf_counter()
|
|
sess_i = create_mock_session("SESS-I")
|
|
msg_i = "Metformin 500 mg காலை மாலை சாப்பிடுறேன். BP மாத்திரை 10 mg இரவு எடுக்குறேன்."
|
|
res_i = await orchestrator.process(sess_i, MagicMock(state_json=json.dumps({})), [], msg_i)
|
|
meds_i = res_i["state_updates"].get("medications", [])
|
|
syms_i = res_i["state_updates"].get("symptoms", []) or []
|
|
dur_i = (time.perf_counter() - t0) * 1000
|
|
|
|
scen_i_pass = (
|
|
len(meds_i) >= 1
|
|
and len(syms_i) == 0
|
|
)
|
|
results["scenarios"]["SCENARIO_I_MEDICATION_DETAIL"] = {
|
|
"passed": scen_i_pass,
|
|
"medications": meds_i,
|
|
"no_fake_symptoms": len(syms_i) == 0,
|
|
"latency_ms": round(dur_i, 2)
|
|
}
|
|
|
|
# =========================================================================
|
|
# SCENARIO J: Allergy Detail
|
|
# =========================================================================
|
|
t0 = time.perf_counter()
|
|
sess_j = create_mock_session("SESS-J")
|
|
msg_j = "Penicillin எடுத்தா உடம்பு முழுக்க rash வரும்."
|
|
res_j = await orchestrator.process(sess_j, MagicMock(state_json=json.dumps({})), [], msg_j)
|
|
algs_j = res_j["state_updates"].get("allergies") or []
|
|
if isinstance(algs_j, dict):
|
|
algs_j = algs_j.get("items", [])
|
|
dur_j = (time.perf_counter() - t0) * 1000
|
|
|
|
scen_j_pass = (
|
|
len(algs_j) >= 1
|
|
and algs_j[0].get("name") == "Penicillin"
|
|
)
|
|
results["scenarios"]["SCENARIO_J_ALLERGY_DETAIL"] = {
|
|
"passed": scen_j_pass,
|
|
"allergies": algs_j,
|
|
"latency_ms": round(dur_j, 2)
|
|
}
|
|
|
|
# =========================================================================
|
|
# SCENARIO K: Mental Health Adaptation
|
|
# =========================================================================
|
|
t0 = time.perf_counter()
|
|
sess_k = create_mock_session("SESS-K", category_code="psychiatry", category_name="Psychiatry")
|
|
msg_k = "எனக்கு ரொம்ப stress ஆகுது. தூக்கம் சரியா வரல. வேலை செய்யவே மனசு வரல."
|
|
res_k = await orchestrator.process(sess_k, MagicMock(state_json=json.dumps({})), [], msg_k)
|
|
reply_k = res_k.get("reply", "")
|
|
dur_k = (time.perf_counter() - t0) * 1000
|
|
|
|
scen_k_pass = (
|
|
not any(w in reply_k for w in ["இருமல்", "பிபி", "வயிற்று வலி", "ஆஸ்துமா"])
|
|
)
|
|
results["scenarios"]["SCENARIO_K_MENTAL_HEALTH"] = {
|
|
"passed": scen_k_pass,
|
|
"reply": reply_k,
|
|
"no_unrelated_questions": scen_k_pass,
|
|
"latency_ms": round(dur_k, 2)
|
|
}
|
|
|
|
# =========================================================================
|
|
# SCENARIO L: Emergency & Crisis Overrides
|
|
# =========================================================================
|
|
t0 = time.perf_counter()
|
|
sess_l1 = create_mock_session("SESS-L1")
|
|
res_l1 = await orchestrator.process(sess_l1, MagicMock(state_json=json.dumps({})), [], "மூச்சு விட முடியல.")
|
|
|
|
sess_l2 = create_mock_session("SESS-L2")
|
|
res_l2 = await orchestrator.process(sess_l2, MagicMock(state_json=json.dumps({})), [], "திடீர்னு கை கால் வேலை செய்யல.")
|
|
|
|
sess_l3 = create_mock_session("SESS-L3")
|
|
res_l3 = await orchestrator.process(sess_l3, MagicMock(state_json=json.dumps({})), [], "நான் என்னையே காயப்படுத்திக்கணும்னு தோணுது.")
|
|
dur_l = (time.perf_counter() - t0) * 1000
|
|
|
|
scen_l_pass = (
|
|
"108" in res_l1["reply"] and res_l1.get("completed") is True
|
|
and "108" in res_l2["reply"] and res_l2.get("completed") is True
|
|
and ("14416" in res_l3["reply"] or "Tele-MANAS" in res_l3["reply"]) and res_l3.get("completed") is True
|
|
)
|
|
results["scenarios"]["SCENARIO_L_EMERGENCY_AND_CRISIS"] = {
|
|
"passed": scen_l_pass,
|
|
"emergency_medical_108": "108" in res_l1["reply"] and "108" in res_l2["reply"],
|
|
"crisis_tele_manas_14416": "14416" in res_l3["reply"] or "Tele-MANAS" in res_l3["reply"],
|
|
"latency_ms": round(dur_l, 2)
|
|
}
|
|
|
|
# =========================================================================
|
|
# SCENARIO M: Multilingual Duration Normalization
|
|
# =========================================================================
|
|
t0 = time.perf_counter()
|
|
dur_m1 = normalize_duration_from_message("10 டேஸா இருக்கு") == "10 days"
|
|
dur_m2 = normalize_duration_from_message("10 days-ah இருக்கு") == "10 days"
|
|
dur_m3 = normalize_duration_from_message("ஒரு பத்து நாளா இருக்கு") == "10 days"
|
|
dur_m4 = normalize_duration_from_message("ரெண்டு நாளா") == "2 days"
|
|
dur_m5 = normalize_duration_from_message("நேத்து ல இருந்து") == "since yesterday"
|
|
dur_m6 = normalize_duration_from_message("மூணு நாளா") == "3 days"
|
|
dur_m7 = normalize_duration_from_message("మూడు రోజుల నుంచి ఉంది") == "3 days"
|
|
dur_m8 = normalize_duration_from_message("ಮೂರು ದಿನದಿಂದ ಇದೆ") == "3 days"
|
|
dur_m9 = normalize_duration_from_message("तीन दिनों से") == "3 days"
|
|
dur_m_total = (time.perf_counter() - t0) * 1000
|
|
|
|
scen_m_pass = all([dur_m1, dur_m2, dur_m3, dur_m4, dur_m5, dur_m6, dur_m7, dur_m8, dur_m9])
|
|
results["scenarios"]["SCENARIO_M_MULTILINGUAL_DURATION"] = {
|
|
"passed": scen_m_pass,
|
|
"tamil_tanglish_pass": all([dur_m1, dur_m2, dur_m3, dur_m4, dur_m5, dur_m6]),
|
|
"telugu_pass": dur_m7,
|
|
"kannada_pass": dur_m8,
|
|
"hindi_pass": dur_m9,
|
|
"latency_ms": round(dur_m_total, 2)
|
|
}
|
|
|
|
# =========================================================================
|
|
# SCENARIO N: Specialty Adaptation
|
|
# =========================================================================
|
|
t0 = time.perf_counter()
|
|
spec_gm = classify_specialty_from_symptoms("காய்ச்சல் இருக்கு.")["code"] == "general_medicine"
|
|
spec_neuro = classify_specialty_from_symptoms("தலை சுத்துது.")["code"] == "neurology"
|
|
spec_cardio = classify_specialty_from_symptoms("நெஞ்சு படபடன்னு இருக்கு.")["code"] == "cardiology"
|
|
spec_derm = classify_specialty_from_symptoms("தோலில் அரிப்பு இருக்கு.")["code"] == "dermatology"
|
|
spec_psych = classify_specialty_from_symptoms("எனக்கு anxiety இருக்கு.")["code"] == "psychiatry"
|
|
dur_n = (time.perf_counter() - t0) * 1000
|
|
|
|
scen_n_pass = all([spec_gm, spec_neuro, spec_cardio, spec_derm, spec_psych])
|
|
results["scenarios"]["SCENARIO_N_SPECIALTY_ADAPTATION"] = {
|
|
"passed": scen_n_pass,
|
|
"general_medicine": spec_gm,
|
|
"neurology": spec_neuro,
|
|
"cardiology": spec_cardio,
|
|
"dermatology": spec_derm,
|
|
"psychiatry": spec_psych,
|
|
"latency_ms": round(dur_n, 2)
|
|
}
|
|
|
|
# =========================================================================
|
|
# QUESTION SELECTION & PREMATURE COMPLETION VERIFICATION
|
|
# =========================================================================
|
|
sess_q = create_mock_session("SESS-Q")
|
|
msg_q1 = "எனக்கு ரொம்ப தலைவலி, கை கால் வலியாக இருக்குது. என் நெஞ்சு அப்பப்ப படபடன்னு இருக்குது. தலை சுத்துது, தலை பாரமாக இருக்குது."
|
|
res_q1 = await orchestrator.process(sess_q, MagicMock(state_json=json.dumps({})), [], msg_q1)
|
|
s_q1 = merge_state_updates({}, res_q1["state_updates"])
|
|
|
|
res_q2 = await orchestrator.process(
|
|
sess_q, MagicMock(state_json=json.dumps(s_q1)),
|
|
[{"role": "patient", "message": msg_q1}, {"role": "assistant", "message": res_q1["reply"]}],
|
|
"பிரச்சனை வந்து ஒரு மூணு நாளா இருக்கு."
|
|
)
|
|
|
|
q_sel_pass = (
|
|
res_q2.get("completed") is False
|
|
and "எத்தனை நாளா" not in res_q2.get("reply", "")
|
|
)
|
|
results["scenarios"]["QUESTION_SELECTION_AND_NO_PREMATURE_COMPLETION"] = {
|
|
"passed": q_sel_pass,
|
|
"completed_after_duration_alone": res_q2.get("completed"),
|
|
"repeated_duration_question": "எத்தனை நாளா" in res_q2.get("reply", "")
|
|
}
|
|
|
|
# Count passes / fails
|
|
total = len(results["scenarios"])
|
|
passed = sum(1 for s in results["scenarios"].values() if s.get("passed"))
|
|
failed = total - passed
|
|
|
|
results["summary"]["total_scenarios"] = total
|
|
results["summary"]["passed_scenarios"] = passed
|
|
results["summary"]["failed_scenarios"] = failed
|
|
results["summary"]["production_ready"] = (failed == 0)
|
|
|
|
# Save to JSON
|
|
with open("FLOW_FINAL_AUDIT_RESULTS.json", "w", encoding="utf-8") as f:
|
|
json.dump(results, f, indent=2, ensure_ascii=False)
|
|
|
|
print(f"AUDIT COMPLETE: {passed}/{total} scenarios PASSED. Production ready: {results['summary']['production_ready']}")
|
|
return results
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(run_audit())
|