| """ |
| Insta-AutoApp β 6-Agent Pipeline Architecture |
| AI-powered symptom triage for 2023 Ford Bronco owners. |
| |
| Agents: |
| 1. IntakeAgent β validates & normalizes user symptom input |
| 2. ProfileAgent β injects vehicle profile context |
| 3. ClarificationAgent β generates Bronco-specific follow-up questions |
| 4. RetrievalAgent β FAISS semantic retrieval from OEM manual |
| 5. DiagnosticAgent β LLM-powered triage producing structured 4-field output |
| 6. PresentationAgent β formats branded Triage Card with safety disclaimer |
| |
| Team Data Mavericks Β· Nasser Chaudhry Β· Miriam Camacho Β· Neil Driscoll |
| ANLY 601 Β· Mays Business School Β· Texas A&M University |
| """ |
|
|
| import html |
| import logging |
| import os |
| import re |
| import sys |
| from dataclasses import dataclass, field |
| from typing import Optional |
|
|
| import gradio as gr |
|
|
| from config import ( |
| APP_TITLE, APP_SUBTITLE, DISCLAIMER_BANNER, DISCLAIMER_RESPONSE, |
| ERROR_API_UNAVAILABLE, ERROR_NOT_IN_MANUAL, |
| TRIM_OPTIONS, ENGINE_OPTIONS, PACKAGE_OPTIONS, TOP_TYPE_OPTIONS, |
| MILEAGE_MIN, MILEAGE_MAX, MILEAGE_DEFAULT, |
| FALLBACK_FOLLOWUP_QUESTIONS, SAFETY_CRITICAL_KEYWORDS, |
| FAISS_INDEX_PATH, LLM_PROVIDER, LLM_MODEL, |
| ) |
| from prompts import ( |
| FOLLOWUP_SYSTEM_PROMPT, TRIAGE_SYSTEM_PROMPT, |
| format_vehicle_profile, format_followup_context, format_retrieved_context, |
| ) |
| from llm_client import get_llm_client |
| from rag_pipeline import get_retriever, initialize_rag |
|
|
| logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") |
| logger = logging.getLogger(__name__) |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class PipelineContext: |
| raw_symptom: str = "" |
| normalized_symptom: str = "" |
| vehicle_profile: str = "" |
| is_valid: bool = False |
| validation_error: str = "" |
| followup_questions: list = field(default_factory=list) |
| followup_answers: list = field(default_factory=list) |
| using_fallback_questions: bool = False |
| retrieved_chunks: list = field(default_factory=list) |
| triage_fields: dict = field(default_factory=dict) |
| html_output: str = "" |
| pipeline_trace: list = field(default_factory=list) |
| safety_flagged: bool = False |
|
|
| def trace(self, agent: str, status: str, msg: str): |
| icon = {"ok": "β", "warn": "β ", "skip": "β", "fail": "β"}.get(status, "Β·") |
| self.pipeline_trace.append({"agent": agent, "status": status, "icon": icon, "msg": msg}) |
| logger.info(f"[{agent}] {status.upper()}: {msg}") |
|
|
|
|
| |
| |
| |
|
|
| class IntakeAgent: |
| """Validates and normalizes user symptom input.""" |
| MIN_LEN = 8 |
|
|
| def process(self, ctx: PipelineContext) -> PipelineContext: |
| raw = (ctx.raw_symptom or "").strip() |
| if not raw: |
| ctx.validation_error = "Please describe your symptom before submitting." |
| ctx.is_valid = False |
| ctx.trace("IntakeAgent", "fail", "Empty input rejected") |
| return ctx |
| if len(raw) < self.MIN_LEN: |
| ctx.validation_error = "Please provide more detail about what's happening." |
| ctx.is_valid = False |
| ctx.trace("IntakeAgent", "fail", f"Input too short ({len(raw)} chars)") |
| return ctx |
| ctx.normalized_symptom = re.sub(r"\s+", " ", raw) |
| ctx.is_valid = True |
| ctx.trace("IntakeAgent", "ok", f"Normalized {len(raw)} chars of input") |
| if any(kw in ctx.normalized_symptom.lower() for kw in SAFETY_CRITICAL_KEYWORDS): |
| ctx.safety_flagged = True |
| ctx.trace("IntakeAgent", "warn", "Safety-critical keywords detected β conservative bias engaged") |
| return ctx |
|
|
|
|
| |
| |
| |
|
|
| class ProfileAgent: |
| """Formats vehicle profile into structured context.""" |
|
|
| def process(self, ctx: PipelineContext, trim, engine, package, top_type, mileage) -> PipelineContext: |
| ctx.vehicle_profile = format_vehicle_profile(trim, engine, package, top_type, mileage) |
| try: |
| mi = int(mileage) if mileage else 0 |
| except (TypeError, ValueError): |
| mi = 0 |
| ctx.trace("ProfileAgent", "ok", f"{trim} Β· {engine} Β· {package} Β· {mi:,} mi") |
| return ctx |
|
|
|
|
| |
| |
| |
|
|
| class ClarificationAgent: |
| """Generates Bronco-specific follow-up questions when input is ambiguous.""" |
|
|
| MULTI_SYMPTOM_TRIGGERS = [ |
| ("check engine", "4x4"), ("check engine", "transmission"), |
| ("smell", "light"), ("noise", "light"), ("brake", "steering"), |
| ("burning", "light"), ("4x4", "hesitat"), |
| ] |
|
|
| def _needs_clarification(self, symptom: str) -> bool: |
| s = symptom.lower() |
| for a, b in self.MULTI_SYMPTOM_TRIGGERS: |
| if a in s and b in s: |
| return True |
| if len(symptom.split()) < 12: |
| return True |
| if any(p in s for p in ["something", "weird", "strange", "acting up", "off"]): |
| return True |
| return False |
|
|
| def process(self, ctx: PipelineContext) -> PipelineContext: |
| if not self._needs_clarification(ctx.normalized_symptom): |
| ctx.trace("ClarificationAgent", "skip", "Input specific enough β follow-ups skipped") |
| return ctx |
|
|
| llm = get_llm_client() |
| if not llm.is_configured(): |
| ctx.followup_questions = FALLBACK_FOLLOWUP_QUESTIONS.copy() |
| ctx.using_fallback_questions = True |
| ctx.trace("ClarificationAgent", "warn", "LLM not configured β using standard follow-ups") |
| return ctx |
|
|
| prompt = FOLLOWUP_SYSTEM_PROMPT.format( |
| vehicle_profile=ctx.vehicle_profile, symptom=ctx.normalized_symptom, |
| ) |
| response = llm.generate(prompt, max_new_tokens=256) |
| if response is None: |
| ctx.followup_questions = FALLBACK_FOLLOWUP_QUESTIONS.copy() |
| ctx.using_fallback_questions = True |
| ctx.trace("ClarificationAgent", "warn", "LLM call failed β using standard follow-ups") |
| return ctx |
|
|
| questions = [] |
| for line in response.strip().split("\n"): |
| line = line.strip().lstrip("0123456789.)-β’ ").strip() |
| if line and len(line) > 10 and "?" in line: |
| questions.append(line) |
| questions = questions[:2] |
|
|
| if not questions: |
| ctx.followup_questions = FALLBACK_FOLLOWUP_QUESTIONS.copy() |
| ctx.using_fallback_questions = True |
| ctx.trace("ClarificationAgent", "warn", "No valid questions parsed β using fallback") |
| else: |
| ctx.followup_questions = questions |
| ctx.trace("ClarificationAgent", "ok", f"Generated {len(questions)} Bronco-specific follow-up(s)") |
| return ctx |
|
|
|
|
| |
| |
| |
|
|
| class RetrievalAgent: |
| """FAISS semantic retrieval from OEM manual.""" |
|
|
| def process(self, ctx: PipelineContext) -> PipelineContext: |
| parts = [ctx.normalized_symptom] |
| for q, a in zip(ctx.followup_questions[:len(ctx.followup_answers)], ctx.followup_answers): |
| parts.append(f"{q} {a}") |
| query = " ".join(parts) |
|
|
| retriever = get_retriever() |
| if not retriever.is_loaded(): |
| ctx.retrieved_chunks = [] |
| ctx.trace("RetrievalAgent", "fail", "FAISS index not loaded") |
| return ctx |
|
|
| chunks = retriever.retrieve(query) |
| ctx.retrieved_chunks = chunks |
| ctx.trace("RetrievalAgent", "ok", f"Retrieved {len(chunks)} OEM manual chunks (FAISS semantic)") |
| return ctx |
|
|
|
|
| |
| |
| |
|
|
| class DiagnosticAgent: |
| """LLM-powered triage producing structured 4-field output.""" |
|
|
| def process(self, ctx: PipelineContext) -> PipelineContext: |
| llm = get_llm_client() |
| if not llm.is_configured(): |
| ctx.trace("DiagnosticAgent", "fail", "LLM not configured") |
| return ctx |
|
|
| followup_ctx = format_followup_context( |
| ctx.followup_questions[:len(ctx.followup_answers)], ctx.followup_answers, |
| ) |
| prompt = TRIAGE_SYSTEM_PROMPT.format( |
| vehicle_profile=ctx.vehicle_profile, |
| symptom=ctx.normalized_symptom, |
| followup_context=followup_ctx, |
| retrieved_context=format_retrieved_context(ctx.retrieved_chunks), |
| ) |
| response = llm.generate(prompt, max_new_tokens=1024) |
| if response is None: |
| ctx.trace("DiagnosticAgent", "fail", "LLM generation failed after retries") |
| return ctx |
|
|
| fields = self._parse(response) |
|
|
| |
| if ctx.safety_flagged and fields.get("urgency", "").lower() in ("safe", "monitor"): |
| fields["urgency"] = "Urgent" |
| ctx.trace("DiagnosticAgent", "warn", "Urgency escalated to Urgent (safety-critical)") |
|
|
| ctx.triage_fields = fields |
| ctx.trace("DiagnosticAgent", "ok", f"Triage generated β Urgency: {fields.get('urgency', '?')}") |
| return ctx |
|
|
| @staticmethod |
| def _parse(text: str) -> dict: |
| fields = {"urgency": "", "meaning": "", "next_step": "", "citation": ""} |
| patterns = { |
| "urgency": r"(?:urgency(?:\s+level)?|\*\*urgency[^*]*\*\*)\s*[:\-]?\s*(.+?)(?=\n|$)", |
| "meaning": r"(?:likely\s+meaning|meaning|cause)\s*[:\-]?\s*(.+?)(?=\n(?:recommended|next|oem|citation|\*\*)|\Z)", |
| "next_step": r"(?:recommended\s+next\s+step|next\s+step|action)\s*[:\-]?\s*(.+?)(?=\n(?:oem|citation|\*\*)|\Z)", |
| "citation": r"(?:oem\s+citation|citation|source|reference)\s*[:\-]?\s*(.+?)(?=\n\n|\Z)", |
| } |
| for key, pat in patterns.items(): |
| m = re.search(pat, text, re.IGNORECASE | re.DOTALL) |
| if m: |
| val = re.sub(r"\*\*", "", m.group(1).strip().strip("*")) |
| fields[key] = val[:800] |
| if not any(fields.values()): |
| fields["meaning"] = text.strip()[:500] |
| fields["urgency"] = "Monitor" |
| fields["next_step"] = "Consult a Ford-certified technician for inspection." |
| fields["citation"] = "See 2023 Ford Bronco Owner's Manual." |
| return fields |
|
|
|
|
| |
| |
| |
|
|
| class PresentationAgent: |
| """Formats branded Triage Card with urgency color-coding and disclaimer.""" |
|
|
| URGENCY_STYLES = { |
| "safe": ("#1F7A3A", "#E8F5EB", "SAFE"), |
| "monitor": ("#B68B00", "#FFF7D6", "MONITOR"), |
| "urgent": ("#C84A1A", "#FFEDE0", "URGENT"), |
| "do not drive": ("#A01818", "#FDE6E6", "DO NOT DRIVE"), |
| } |
|
|
| def process(self, ctx: PipelineContext) -> PipelineContext: |
| if not ctx.triage_fields: |
| ctx.html_output = self._error_card(ERROR_API_UNAVAILABLE) |
| ctx.trace("PresentationAgent", "fail", "No triage fields to render") |
| return ctx |
| if not ctx.retrieved_chunks: |
| ctx.html_output = self._error_card(ERROR_NOT_IN_MANUAL) |
| ctx.trace("PresentationAgent", "warn", "No chunks retrieved β not-in-manual notice") |
| return ctx |
| ctx.html_output = self._triage_card(ctx.triage_fields) |
| ctx.trace("PresentationAgent", "ok", "Triage card rendered") |
| return ctx |
|
|
| def _triage_card(self, f: dict) -> str: |
| urg_key = f.get("urgency", "monitor").lower().strip() |
| matched = "monitor" |
| for k in self.URGENCY_STYLES: |
| if k in urg_key: |
| matched = k |
| break |
| fg, bg, label = self.URGENCY_STYLES[matched] |
| esc = lambda s: html.escape(s or "β") |
| return f""" |
| <div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;max-width:780px;"> |
| <div style="background:#FFFFFF;border:1px solid #E8D9C0;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(26,42,68,0.08);"> |
| <div style="background:#1A2A44;padding:14px 20px;display:flex;justify-content:space-between;align-items:center;"> |
| <div style="font-size:11px;font-weight:700;letter-spacing:2px;color:#E88A5C !important;">TRIAGE RESULT</div> |
| <div style="font-size:11px;color:#E8D9C0 !important;">OEM-GROUNDED Β· 2023 FORD BRONCO</div> |
| </div> |
| <div style="background:{bg};padding:18px 20px;border-bottom:3px solid {fg};"> |
| <div style="font-size:10px;letter-spacing:2px;color:{fg};font-weight:700;margin-bottom:4px;">URGENCY LEVEL</div> |
| <div style="font-size:28px;font-weight:800;color:{fg};font-family:Georgia,serif;">{label}</div> |
| </div> |
| <div style="padding:20px;background:#FFFFFF;"> |
| <div style="margin-bottom:18px;"> |
| <div style="font-size:10px;letter-spacing:2px;color:#B04A2C;font-weight:700;margin-bottom:6px;">LIKELY MEANING</div> |
| <div style="font-size:15px;color:#1A2A44;line-height:1.5;">{esc(f.get('meaning'))}</div> |
| </div> |
| <div style="margin-bottom:18px;"> |
| <div style="font-size:10px;letter-spacing:2px;color:#B04A2C;font-weight:700;margin-bottom:6px;">RECOMMENDED NEXT STEP</div> |
| <div style="font-size:15px;color:#1A2A44;line-height:1.5;font-weight:500;">{esc(f.get('next_step'))}</div> |
| </div> |
| <div style="background:#F5EFE6;padding:12px 14px;border-left:3px solid #1A2A44;border-radius:3px;"> |
| <div style="font-size:10px;letter-spacing:2px;color:#8B7355;font-weight:700;margin-bottom:4px;">OEM CITATION</div> |
| <div style="font-size:13px;color:#1A2A44;font-style:italic;">{esc(f.get('citation'))}</div> |
| </div> |
| </div> |
| <div style="background:#FFF4E5;border-top:1px solid #E8D9C0;padding:10px 20px;font-size:11px;color:#8B5A00;line-height:1.4;"> |
| β <strong>{esc(DISCLAIMER_RESPONSE)}</strong> |
| </div> |
| </div> |
| </div>""" |
|
|
| def _error_card(self, msg: str) -> str: |
| return f""" |
| <div style="font-family:-apple-system,sans-serif;max-width:780px;background:#FDE6E6;border:1px solid #A01818;border-radius:8px;padding:20px;"> |
| <div style="font-size:11px;letter-spacing:2px;color:#A01818;font-weight:700;margin-bottom:8px;">NOTICE</div> |
| <div style="font-size:15px;color:#1A2A44;">{html.escape(msg)}</div> |
| </div>""" |
|
|
|
|
| |
| |
| |
|
|
| class TriagePipeline: |
| def __init__(self): |
| self.a1 = IntakeAgent() |
| self.a2 = ProfileAgent() |
| self.a3 = ClarificationAgent() |
| self.a4 = RetrievalAgent() |
| self.a5 = DiagnosticAgent() |
| self.a6 = PresentationAgent() |
|
|
| def stage1(self, symptom, trim, engine, package, top_type, mileage): |
| ctx = PipelineContext(raw_symptom=symptom) |
| ctx = self.a1.process(ctx) |
| if not ctx.is_valid: |
| return ctx |
| ctx = self.a2.process(ctx, trim, engine, package, top_type, mileage) |
| ctx = self.a3.process(ctx) |
| return ctx |
|
|
| def stage2(self, ctx): |
| ctx = self.a4.process(ctx) |
| ctx = self.a5.process(ctx) |
| ctx = self.a6.process(ctx) |
| return ctx |
|
|
|
|
| PIPELINE = TriagePipeline() |
|
|
|
|
| |
| |
| |
|
|
| def render_trace(trace: list) -> str: |
| if not trace: |
| return "<div style='color:#8B7355;font-style:italic;padding:12px;'>Pipeline has not run yet.</div>" |
| colors = {"ok": "#1F7A3A", "warn": "#B68B00", "skip": "#8B7355", "fail": "#A01818"} |
| rows = [] |
| for e in trace: |
| c = colors.get(e["status"], "#1A2A44") |
| rows.append(f"""<div style="display:flex;gap:10px;padding:8px 12px;border-left:3px solid {c};background:#FFFFFF;margin-bottom:4px;font-size:12px;border-radius:0 3px 3px 0;"> |
| <span style="color:{c};font-weight:700;min-width:18px;">{e['icon']}</span> |
| <span style="color:#B04A2C;font-weight:700;min-width:160px;">{html.escape(e['agent'])}</span> |
| <span style="color:#1A2A44;">{html.escape(e['msg'])}</span> |
| </div>""") |
| return f"<div style='background:#F5EFE6;padding:10px;border-radius:6px;'>{''.join(rows)}</div>" |
|
|
|
|
| |
| |
| |
|
|
| def on_submit_symptom(symptom, trim, engine, package, top_type, mileage): |
| ctx = PIPELINE.stage1(symptom, trim, engine, package, top_type, mileage) |
| trace_html = render_trace(ctx.pipeline_trace) |
| if not ctx.is_valid: |
| return ( |
| gr.update(value=f"<div style='color:#A01818;padding:10px;'>β {html.escape(ctx.validation_error)}</div>", visible=True), |
| gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), |
| "", "", trace_html, ctx, |
| ) |
| if not ctx.followup_questions: |
| ctx = PIPELINE.stage2(ctx) |
| return ( |
| gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), |
| gr.update(value=ctx.html_output, visible=True), |
| "", "", render_trace(ctx.pipeline_trace), ctx, |
| ) |
| q_md = "**Quick clarifying questions:**\n\n" |
| if ctx.using_fallback_questions: |
| q_md += "_(using standard Bronco follow-ups)_\n\n" |
| for i, q in enumerate(ctx.followup_questions, 1): |
| q_md += f"{i}. {q}\n\n" |
| return ( |
| gr.update(visible=False), |
| gr.update(value=q_md, visible=True), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| "", "", trace_html, ctx, |
| ) |
|
|
|
|
| def on_submit_followup(answer1, answer2, ctx): |
| if ctx is None: |
| return gr.update(visible=False), gr.update(visible=False), "", ctx |
| answers = [] |
| if len(ctx.followup_questions) >= 1 and answer1.strip(): |
| answers.append(answer1.strip()) |
| if len(ctx.followup_questions) >= 2 and answer2.strip(): |
| answers.append(answer2.strip()) |
| ctx.followup_answers = answers |
| ctx = PIPELINE.stage2(ctx) |
| return ( |
| gr.update(visible=False), |
| gr.update(value=ctx.html_output, visible=True), |
| render_trace(ctx.pipeline_trace), |
| ctx, |
| ) |
|
|
|
|
| def on_new_query(): |
| return ( |
| "", "", "", 0, |
| gr.update(value="", visible=False), |
| gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), |
| "<div style='color:#8B7355;font-style:italic;padding:12px;'>Pipeline has not run yet.</div>", |
| None, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| CUSTOM_CSS = """ |
| .gradio-container { background: #F5EFE6 !important; } |
| .gr-button-primary { background: #B04A2C !important; border: none !important; color: #F5EFE6 !important; font-weight: 700 !important; } |
| .gr-button-primary:hover { background: #8F3A20 !important; } |
| """ |
|
|
|
|
| def create_app(): |
| |
| rag_ready = True |
| if not os.path.exists(FAISS_INDEX_PATH): |
| logger.error( |
| f"\n{'='*60}\n" |
| f"FAISS index not found at {FAISS_INDEX_PATH}\n" |
| f"You must run 'python ingest.py' first to vectorize the OEM manual.\n" |
| f"{'='*60}" |
| ) |
| if os.getenv("SPACE_ID"): |
| rag_ready = False |
| else: |
| sys.exit(1) |
|
|
| if rag_ready and not initialize_rag(): |
| logger.error("Failed to initialize RAG pipeline.") |
| if os.getenv("SPACE_ID"): |
| rag_ready = False |
| else: |
| sys.exit(1) |
|
|
| with gr.Blocks(title=APP_TITLE, theme=gr.themes.Soft(primary_hue="orange", neutral_hue="stone"), css=CUSTOM_CSS) as app: |
| state = gr.State(None) |
|
|
| |
| gr.HTML(f""" |
| <div style="background:linear-gradient(135deg,#1A2A44 0%,#0F1A2E 100%) !important;padding:24px 28px;border-radius:10px;margin-bottom:12px;"> |
| <div style="font-size:11px;letter-spacing:3px;color:#E88A5C !important;font-weight:700;margin-bottom:8px;">AIAAS Β· AUTOMOTIVE Β· 6-AGENT PIPELINE</div> |
| <div style="font-family:Georgia,'Times New Roman',serif;font-size:38px;font-weight:800;line-height:1.1;color:#FFFFFF !important;margin-bottom:6px;">{APP_TITLE}</div> |
| <div style="font-size:14px;color:#F5EFE6 !important;font-style:italic;">{APP_SUBTITLE}</div> |
| </div>""") |
| gr.HTML(f"""<div style="background:#FFF4E5;border-left:4px solid #B04A2C;padding:12px 16px;font-size:13px;color:#8B5A00;border-radius:0 4px 4px 0;margin-bottom:16px;">β {html.escape(DISCLAIMER_BANNER)}</div>""") |
|
|
| with gr.Row(): |
| with gr.Column(scale=3): |
| with gr.Accordion("π Vehicle Profile", open=True): |
| with gr.Row(): |
| trim = gr.Dropdown(choices=TRIM_OPTIONS, value=TRIM_OPTIONS[0], label="Trim Level") |
| engine = gr.Dropdown(choices=ENGINE_OPTIONS, value=ENGINE_OPTIONS[0], label="Engine") |
| with gr.Row(): |
| package = gr.Dropdown(choices=PACKAGE_OPTIONS, value=PACKAGE_OPTIONS[0], label="Package") |
| top_type = gr.Dropdown(choices=TOP_TYPE_OPTIONS, value=TOP_TYPE_OPTIONS[0], label="Top Type") |
| mileage = gr.Number(value=MILEAGE_DEFAULT, minimum=MILEAGE_MIN, maximum=MILEAGE_MAX, label="Mileage", precision=0) |
|
|
| gr.Markdown("### π Describe Your Symptom") |
| symptom_input = gr.Textbox( |
| placeholder="Example: My check engine light came on and the truck feels sluggish in 4H.", |
| label="What's happening with your vehicle?", lines=4, |
| ) |
| submit_btn = gr.Button("π Run 6-Agent Triage Pipeline", variant="primary", size="lg") |
| status_output = gr.HTML(visible=False) |
| followup_display = gr.Markdown(visible=False) |
| with gr.Group(visible=False) as followup_group: |
| answer1 = gr.Textbox(label="Answer 1", lines=2) |
| answer2 = gr.Textbox(label="Answer 2 (if shown)", lines=2) |
| followup_submit_btn = gr.Button("π Submit Answers & Continue Pipeline", variant="primary") |
| triage_output = gr.HTML(visible=False) |
| new_query_btn = gr.Button("π New Query", variant="secondary") |
|
|
| with gr.Column(scale=2): |
| gr.HTML("""<div style='font-size:10px;letter-spacing:2px;color:#B04A2C;font-weight:700;margin-bottom:4px;'>LIVE PIPELINE TRACE</div> |
| <div style='font-size:13px;color:#1A2A44;margin-bottom:8px;'>Six agents. Every step visible.</div>""") |
| trace_display = gr.HTML(value="<div style='color:#8B7355;font-style:italic;padding:12px;'>Pipeline has not run yet.</div>") |
|
|
| gr.HTML(f""" |
| <div style='margin-top:20px;padding:16px;background:#1A2A44 !important;border-radius:6px;font-size:12px;'> |
| <div style='letter-spacing:2px;color:#E88A5C !important;font-weight:700;margin-bottom:10px;font-size:11px;'>6-AGENT ARCHITECTURE</div> |
| <div style='line-height:1.9;'> |
| <div style='color:#F5EFE6 !important;'><span style='color:#E88A5C !important;font-weight:700;'>1.</span> <strong style='color:#FFFFFF !important;'>IntakeAgent</strong> <span style='color:#D4C5A9 !important;'>β validate & normalize</span></div> |
| <div style='color:#F5EFE6 !important;'><span style='color:#E88A5C !important;font-weight:700;'>2.</span> <strong style='color:#FFFFFF !important;'>ProfileAgent</strong> <span style='color:#D4C5A9 !important;'>β vehicle context</span></div> |
| <div style='color:#F5EFE6 !important;'><span style='color:#E88A5C !important;font-weight:700;'>3.</span> <strong style='color:#FFFFFF !important;'>ClarificationAgent</strong> <span style='color:#D4C5A9 !important;'>β Bronco follow-ups</span></div> |
| <div style='color:#F5EFE6 !important;'><span style='color:#E88A5C !important;font-weight:700;'>4.</span> <strong style='color:#FFFFFF !important;'>RetrievalAgent</strong> <span style='color:#D4C5A9 !important;'>β FAISS semantic search</span></div> |
| <div style='color:#F5EFE6 !important;'><span style='color:#E88A5C !important;font-weight:700;'>5.</span> <strong style='color:#FFFFFF !important;'>DiagnosticAgent</strong> <span style='color:#D4C5A9 !important;'>β LLM triage + safety bias</span></div> |
| <div style='color:#F5EFE6 !important;'><span style='color:#E88A5C !important;font-weight:700;'>6.</span> <strong style='color:#FFFFFF !important;'>PresentationAgent</strong> <span style='color:#D4C5A9 !important;'>β branded Triage Card</span></div> |
| </div> |
| <div style='margin-top:12px;font-size:10px;color:#D4C5A9 !important;'>LLM: {LLM_PROVIDER} / {LLM_MODEL}</div> |
| </div>""") |
|
|
| gr.HTML(""" |
| <div style='margin-top:24px;padding:14px;font-size:11px;color:#8B7355;text-align:center;border-top:1px solid #E8D9C0;'> |
| <strong>Team Data Mavericks</strong> Β· Nasser Chaudhry Β· Miriam Camacho Β· Neil Driscoll Β· ANLY 601 Β· Mays Business School, Texas A&M |
| </div>""") |
|
|
| |
| submit_btn.click( |
| fn=on_submit_symptom, |
| inputs=[symptom_input, trim, engine, package, top_type, mileage], |
| outputs=[status_output, followup_display, followup_group, triage_output, answer1, answer2, trace_display, state], |
| ) |
| followup_submit_btn.click( |
| fn=on_submit_followup, inputs=[answer1, answer2, state], |
| outputs=[followup_group, triage_output, trace_display, state], |
| ) |
| new_query_btn.click( |
| fn=on_new_query, inputs=[], |
| outputs=[symptom_input, answer1, answer2, mileage, status_output, followup_display, followup_group, triage_output, trace_display, state], |
| ) |
|
|
| return app |
|
|
|
|
| if __name__ == "__main__": |
| logger.info(f"Starting Insta-AutoApp β {LLM_PROVIDER} / {LLM_MODEL}") |
| app = create_app() |
| app.launch(server_name="0.0.0.0", server_port=7861, share=False) |
|
|