| """ |
| Insta-AutoApp v3 β 6-Agent Pipeline with InferenceClient LLM |
| 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 β RAG retrieval from OEM manual (FAISS + keyword fallback) |
| 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 time |
| from dataclasses import dataclass, field |
| from typing import Optional |
|
|
| import gradio as gr |
| from huggingface_hub import InferenceClient |
|
|
| from config import ( |
| APP_TITLE, APP_DESCRIPTION, 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, HF_API_TOKEN, |
| MAX_RETRIES, RETRY_DELAY, |
| ) |
| from prompts import ( |
| FOLLOWUP_SYSTEM_PROMPT, TRIAGE_SYSTEM_PROMPT, |
| format_vehicle_profile, format_followup_context, format_retrieved_context, |
| ) |
| from rag_pipeline import get_rag_pipeline, initialize_rag |
|
|
| logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") |
| logger = logging.getLogger(__name__) |
|
|
| |
| |
| |
| |
| |
| |
| MODEL_CHAIN = [ |
| os.getenv("HF_MODEL_ID", ""), |
| "deepseek-ai/DeepSeek-V3-0324", |
| "meta-llama/Llama-3.3-70B-Instruct", |
| "Qwen/Qwen2.5-7B-Instruct", |
| "HuggingFaceH4/zephyr-7b-beta", |
| ] |
| MODEL_CHAIN = [m for m in MODEL_CHAIN if m] |
|
|
|
|
| class LLMClient: |
| """InferenceClient wrapper that tries a chain of models until one answers.""" |
|
|
| def __init__(self): |
| self.token = HF_API_TOKEN |
| self.models = MODEL_CHAIN |
| self._client = None |
| self._working_model = None |
| if self.token: |
| try: |
| self._client = InferenceClient(token=self.token, timeout=30) |
| logger.info(f"LLMClient initialized. Chain: {self.models}") |
| except Exception as e: |
| logger.error(f"Failed to initialize InferenceClient: {e}") |
| else: |
| logger.warning("HF_API_TOKEN not set. LLM calls will fail.") |
|
|
| def is_configured(self) -> bool: |
| return self._client is not None |
|
|
| def _try_model(self, model, prompt, max_new_tokens): |
| try: |
| response = self._client.chat_completion( |
| model=model, |
| messages=[{"role": "user", "content": prompt}], |
| max_tokens=max_new_tokens, |
| temperature=0.7, |
| top_p=0.9, |
| ) |
| if hasattr(response, "choices") and response.choices: |
| content = response.choices[0].message.content |
| if content: |
| return content.strip() |
| except Exception as e: |
| logger.warning(f"Model {model} failed: {type(e).__name__}: {str(e)[:200]}") |
| return None |
|
|
| def generate(self, prompt: str, max_new_tokens: int = 1024) -> Optional[str]: |
| if not self._client: |
| return None |
| if self._working_model: |
| result = self._try_model(self._working_model, prompt, max_new_tokens) |
| if result: |
| return result |
| logger.warning(f"Cached model {self._working_model} failed, re-trying chain") |
| self._working_model = None |
| for model in self.models: |
| logger.info(f"Trying model: {model}") |
| result = self._try_model(model, prompt, max_new_tokens) |
| if result: |
| self._working_model = model |
| logger.info(f"β {model} succeeded, caching") |
| return result |
| logger.error(f"All {len(self.models)} models in chain failed") |
| return None |
|
|
|
|
| _llm_client: Optional[LLMClient] = None |
|
|
| def get_llm_client() -> LLMClient: |
| global _llm_client |
| if _llm_client is None: |
| _llm_client = LLMClient() |
| return _llm_client |
|
|
|
|
| |
| |
| |
|
|
| @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, status, msg): |
| 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: |
| MIN_LEN = 8 |
| def process(self, ctx): |
| 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 a bit 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: |
| def process(self, ctx, trim, engine, package, top_type, mileage): |
| 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: |
| MULTI_SYMPTOM_TRIGGERS = [ |
| ("check engine", "4x4"), ("check engine", "transmission"), |
| ("smell", "light"), ("noise", "light"), ("brake", "steering"), |
| ("burning", "light"), ("4x4", "hesitat"), |
| ] |
|
|
| def _needs_clarification(self, symptom): |
| 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): |
| 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: |
| def process(self, ctx): |
| 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) |
| rag = get_rag_pipeline() |
| if not rag.is_loaded(): |
| ctx.retrieved_chunks = [] |
| ctx.trace("RetrievalAgent", "fail", "RAG pipeline not loaded") |
| return ctx |
| chunks = rag.retrieve(query) |
| ctx.retrieved_chunks = chunks |
| mode = "FAISS semantic" if getattr(rag, "_use_faiss", False) else "keyword fallback" |
| ctx.trace("RetrievalAgent", "ok", f"Retrieved {len(chunks)} OEM manual chunks ({mode})") |
| return ctx |
|
|
|
|
| class DiagnosticAgent: |
| def process(self, ctx): |
| 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 keywords)") |
| ctx.triage_fields = fields |
| ctx.trace("DiagnosticAgent", "ok", f"Triage generated β Urgency: {fields.get('urgency', '?')}") |
| return ctx |
|
|
| @staticmethod |
| def _parse(text): |
| 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 = m.group(1).strip().strip("*").strip() |
| val = re.sub(r"\*\*", "", val) |
| 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: |
| 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): |
| 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 retrieved chunks β showing 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): |
| 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; color: #F5EFE6 !important; 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): |
| 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): |
| 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-family:-apple-system,sans-serif; 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(): |
| initialize_rag() |
| 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(""" |
| <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;">Insta-AutoApp</div> |
| <div style="font-size: 14px; color: #F5EFE6 !important; font-style: italic;">OEM-grounded symptom triage for 2023 Ford Bronco owners.</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(""" |
| <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 + keyword fallback</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> |
| """) |
|
|
| 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 v3 (6-Agent Pipeline) β model chain: {MODEL_CHAIN}") |
| app = create_app() |
| app.launch(server_name="0.0.0.0", server_port=7860, share=False) |
|
|