improve: Ollama prompt structured into 4 sections with full recall/complaint detail, num_predict 250→700

This commit is contained in:
2026-07-03 12:15:35 -04:00
parent 51a66c2dba
commit 3eee5b0acf
2 changed files with 88 additions and 29 deletions
+2
View File
@@ -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"],
+83 -26
View File
@@ -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", [])
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: