diff --git a/main.py b/main.py index 371117e..8ab492b 100644 --- a/main.py +++ b/main.py @@ -183,6 +183,8 @@ async def fetch( "bid": bid or "—", "vpic": vpic, "risk": risk, + "recalls": recalls, + "complaints": complaints, "critical_recall_count": extended["critical_recall_count"], "engine_complaints": extended["engine_complaints"], "transmission_complaints": extended["transmission_complaints"], diff --git a/src/ai/ollama_analyzer.py b/src/ai/ollama_analyzer.py index 5cbc11c..f0949b1 100644 --- a/src/ai/ollama_analyzer.py +++ b/src/ai/ollama_analyzer.py @@ -4,42 +4,99 @@ OLLAMA_URL = "http://localhost:11434/api/generate" PRIMARY_MODEL = "DealAnalyzer" FALLBACK_MODEL = "qwen2.5:14b" -_OLLAMA_OPTIONS = {"temperature": 0.3, "num_predict": 250} +_OLLAMA_OPTIONS = {"temperature": 0.3, "num_predict": 700} def _build_prompt(data: dict) -> str: - # Sanitize string values to prevent prompt injection via control sequences - def s(val) -> str: + def s(val, maxlen=200) -> str: if val is None: return "N/A" - cleaned = str(val).replace("\n", " ").replace("\r", " ").strip() - return cleaned[:200] # cap length per field + return str(val).replace("\n", " ").replace("\r", " ").strip()[:maxlen] - vpic = data.get("vpic", {}) - risk = data.get("risk", {}) - alerts = data.get("alerts", []) + vpic = data.get("vpic", {}) + risk = data.get("risk", {}) + alerts = data.get("alerts", []) + recalls = data.get("recalls", []) + complaints = data.get("complaints", []) - return ( - "Eres un experto en evaluación de vehículos usados en subastas americanas (Copart, IAAI).\n" - "Analiza estos datos y genera un resumen ejecutivo en español de máximo 150 palabras.\n" - "Sé directo y específico. Termina con una recomendación clara: COMPRAR, INVESTIGAR MÁS, o EVITAR.\n\n" - "DATOS DEL VEHÍCULO:\n" - f"- VIN: {s(data.get('vin'))}\n" - f"- Vehículo: {s(vpic.get('ModelYear'))} {s(vpic.get('Make'))} {s(vpic.get('Model'))} {s(vpic.get('Trim'))}\n" - f"- Odómetro: {s(data.get('odometer'))} millas\n" - f"- Daño primario: {s(data.get('primary_damage'))}\n" - f"- Daño secundario: {s(data.get('secondary_damage'))}\n" - f"- Tipo de título: {s(data.get('title'))}\n" - f"- Bid actual: ${s(data.get('bid'))}\n" - f"- Recalls activos NHTSA: {s(risk.get('recall_count'))}\n" - f"- Recalls críticos: {s(data.get('critical_recall_count'))}\n" - f"- Total quejas propietarios: {s(risk.get('complaint_count'))}\n" - f"- Quejas de motor: {s(data.get('engine_complaints'))}\n" - f"- Quejas de transmisión: {s(data.get('transmission_complaints'))}\n" - f"- Score de riesgo calculado: {s(risk.get('score'))}/100\n" - f"- Alertas detectadas: {s('; '.join(alerts) if alerts else 'Ninguna')}\n\n" - "RESUMEN EJECUTIVO:\n" - ) + # Top recall names (component + campaign) + recall_lines = [] + for r in recalls[:8]: + comp = s(r.get("Component") or r.get("component") or "", 80) + campaign = s(r.get("NHTSACampaignNumber") or r.get("campaignNumber") or "", 20) + summary = s(r.get("Summary") or r.get("summary") or "", 120) + if comp or summary: + recall_lines.append(f" • [{campaign}] {comp}: {summary}") + recalls_text = "\n".join(recall_lines) if recall_lines else " Ninguno" + + # Complaint component breakdown (top 6) + from collections import Counter + comp_counter: Counter = Counter() + for c in complaints: + raw = (c.get("components") or c.get("Component") or c.get("component") or "") + for part in raw.split(","): + part = part.strip() + if part: + comp_counter[part[:50]] += 1 + complaint_breakdown = "\n".join( + f" • {comp}: {cnt} quejas" + for comp, cnt in comp_counter.most_common(6) + ) if comp_counter else " Sin datos de componentes" + + critical_count = data.get("critical_recall_count", 0) + engine_q = data.get("engine_complaints", 0) + trans_q = data.get("transmission_complaints", 0) + alerts_text = "; ".join(alerts) if alerts else "Ninguna" + + vehicle = f"{s(vpic.get('ModelYear'))} {s(vpic.get('Make'))} {s(vpic.get('Model'))} {s(vpic.get('Trim'))}".strip() + + return f"""Eres un experto en evaluación de vehículos usados en subastas americanas (Copart, IAAI). +Tienes amplio conocimiento sobre problemas comunes por marca/modelo, qué tan costosas son las reparaciones, +y cómo interpretar los datos de NHTSA para tomar decisiones de compra. + +Analiza los siguientes datos y genera un análisis estructurado en español. +Sé directo, técnico y específico. Explica el SIGNIFICADO real de cada dato, no solo lo repitas. + +═══════════════════════════════════════════ +DATOS DEL VEHÍCULO +═══════════════════════════════════════════ +Vehículo : {vehicle} +VIN : {s(data.get('vin'))} +Odómetro : {s(data.get('odometer'))} millas +Daño prim.: {s(data.get('primary_damage'))} +Daño sec. : {s(data.get('secondary_damage'))} +Título : {s(data.get('title'))} +Bid actual: ${s(data.get('bid'))} +Score riesgo: {risk.get('score', 'N/A')}/100 + +RECALLS NHTSA ({risk.get('recall_count', 0)} total, {critical_count} críticos de motor/transmisión/frenos): +{recalls_text} + +QUEJAS DE PROPIETARIOS ({risk.get('complaint_count', 0)} total): +{complaint_breakdown} + → Quejas de motor: {engine_q} + → Quejas de transmisión: {trans_q} + +ALERTAS DETECTADAS: {alerts_text} +═══════════════════════════════════════════ + +Genera el análisis con EXACTAMENTE estas 4 secciones: + +**CONDICIÓN DEL VEHÍCULO** +Explica qué significa el daño primario/secundario en términos prácticos: qué componentes pueden estar afectados, qué tan costosa puede ser la reparación, si el tipo de título implica restricciones de registro o seguro. + +**ANÁLISIS DE RECALLS Y QUEJAS** +Explica los recalls más importantes: qué falla concreta describen, si afectan seguridad crítica (motor, frenos, dirección, airbag) o son administrativos (etiquetas, software menor). Interpreta el patrón de quejas: si hay muchas quejas de motor significa un problema sistémico del modelo, no casos aislados. Menciona si este volumen de quejas es alto o normal para este modelo/año. + +**FACTORES FINANCIEROS** +Evalúa si el bid tiene sentido dado el daño, el odómetro, el título y los problemas conocidos. Estima el rango de reparación si aplica. Menciona si el modelo tiene piezas costosas o escasas. + +**RECOMENDACIÓN FINAL** +Termina con exactamente una de estas tres palabras en mayúsculas: COMPRAR, INVESTIGAR MÁS, o EVITAR. +Justifica en 2 líneas por qué. + +ANÁLISIS: +""" async def analyze_vehicle(data: dict) -> str | None: