feat(whatsapp): QWEN primary AI backend, Hermes fallback, conversation history, vehicle persistence, demo prompts

- Add QWEN (qwen3.6) as primary AI backend with short system prompt
- Hermes remains as fallback with 45s timeout
- Increase QWEN timeout to 35s, max_tokens to 4000
- Add conversation history loading from whatsapp_messages (last 4 msgs)
- Persist detected vehicle in whatsapp_sessions table
- Add 'limpiar chat' / 'nuevo chat' / 'reset' commands to clear history
- Fix CSS conflict: rename whatsapp chat-panel classes to wa-chat-panel
- Fix JS ID conflicts with chat.js widget (waChatPanel, waChatMessages, etc.)
- Improve no-stock response: conversational with alternatives
- Split search_query by | for multi-part lookups
- Add DEMO_PROMPTS.md and DEMO_PROMPTS_V2.md
This commit is contained in:
2026-05-06 20:27:14 +00:00
parent 371d72887e
commit ff45905b49
33 changed files with 3040 additions and 445 deletions

View File

@@ -38,11 +38,11 @@ def get_vehicle_fitment(part_number, name, brand):
json={
'model': QWEN_MODEL,
'messages': [
{'role': 'system', 'content': 'Eres un experto en autopartes mexicanas. Devuelve SIEMPRE JSON valido sin markdown.'},
{'role': 'system', 'content': 'Eres un experto en autopartes mexicanas y del mercado aftermarket norteamericano. Devuelve SIEMPRE JSON valido sin markdown.'},
{'role': 'user', 'content': prompt}
],
'temperature': 0.2,
'max_tokens': 2048,
'max_tokens': 4096,
},
timeout=45,
)
@@ -86,29 +86,37 @@ def get_vehicle_fitment(part_number, name, brand):
def _build_prompt(part_number, name, brand):
brand_str = brand or 'desconocida'
return f"""Dado el siguiente repuesto automotriz:
return f"""Dado el siguiente repuesto automotriz para el mercado mexicano y aftermarket norteamericano:
- Numero de parte: {part_number}
- Nombre: {name}
- Marca del vehiculo: {brand_str}
- Nombre/descripcion: {name}
- Marca del fabricante: {brand_str}
Devuelve UNICAMENTE un JSON valido (sin markdown, sin backticks) con esta estructura exacta:
Devuelve UNICAMENTE un JSON valido (sin markdown, sin backticks, sin texto adicional) con esta estructura exacta:
{{
"vehicles": [
{{"make": "Toyota", "model": "Corolla", "year": 2015, "engine": "1.8L 16V"}},
{{"make": "Toyota", "model": "Matrix", "year": 2014, "engine": "1.8L"}}
{{
"make": "Toyota",
"model": "Corolla",
"year": 2015,
"engine": "1.8L 16V",
"engine_code": "2ZR-FE",
"notes": "Sedan y hatchback"
}}
],
"confidence": 0.92,
"notes": "Compatible con motor 2ZR-FE"
"notes": "Compatible con plataforma E170. Verificar traccion delantera."
}}
Reglas:
1. "make" es la marca del vehiculo (ej: Toyota, Nissan, Ford, Volkswagen).
2. "model" es el modelo exacto.
3. "year" es el ano numerico (int). Si hay rango de anos, usa el ano inicial.
4. "engine" es la descripcion del motor (ej: "1.8L", "2.0L TDI", "V6 3.5L").
5. Devuelve TODOS los vehiculos compatibles que conozcas. Minimo 1, maximo 30.
6. Si no conoces el motor exacto, usa "desconocido".
7. confidence entre 0.0 y 1.0.
Reglas obligatorias:
1. "make" = marca del vehiculo (ej: Toyota, Nissan, Ford, Volkswagen, Chevrolet, Honda, Hyundai, Kia, Mazda, Subaru).
2. "model" = modelo exacto. Si hay variantes (ej: Civic Sedan vs Civic Coupe), incluye la variante.
3. "year" = ano numerico (int). Si hay rango de anos (ej: 2003-2008), genera una entrada POR CADA ANO del rango. NO uses rangos.
4. "engine" = descripcion del motor (ej: "1.8L", "2.0L TDI", "V6 3.5L", "1.6L Turbo"). Si no conoces el motor, usa "desconocido".
5. "engine_code" = codigo exacto del motor SI LO CONOCES (ej: "2ZR-FE", "K24Z7", "EA888"). Si no lo conoces, usa "" (string vacio).
6. Devuelve TODOS los vehiculos compatibles que conozcas. Minimo 1, maximo 100. Para piezas genericas (bujias, filtros, balatas, amortiguadores) incluye TODOS los modelos aplicables.
7. "confidence" entre 0.0 y 1.0. Usa valores altos (>0.85) solo si estas muy seguro.
8. Incluye marcas y modelos populares en Mexico (Nissan Tsuru, VW Sedan/Vocho, Chevy Monza, Ford Ka, etc.) cuando apliquen.
9. Si la pieza es universal o de alta compatibilidad, indicalo en "notes".
"""
@@ -150,6 +158,7 @@ def _normalize_vehicle(v):
model = v.get('model') or v.get('modelo') or ''
year_raw = v.get('year') or v.get('ano') or v.get('año') or v.get('years') or v.get('anos') or ''
engine = v.get('engine') or v.get('motor') or ''
engine_code = v.get('engine_code') or v.get('codigo_motor') or v.get('motor_code') or ''
# Parse year (may be int, string, or range like "2003-2008")
years = []
@@ -167,11 +176,31 @@ def _normalize_vehicle(v):
if m2:
years = [int(m2.group(1))]
return make, model, years, engine
return make, model, years, engine, engine_code
def _extract_displacement(engine):
"""Extract numeric displacement (L) from engine string, e.g. '1.8L 16V' -> 1.8."""
if not engine or engine.lower() == 'desconocido':
return None
# Match patterns like 1.8L, 2.0L, 3.5L, 1.6, etc.
match = re.search(r'(\d+\.?\d*)\s*[Ll]', engine)
if match:
try:
return float(match.group(1))
except ValueError:
return None
return None
def _validate_vehicles(vehicles):
"""Look up each vehicle in master DB and enrich with mye_id."""
"""Look up each vehicle in master DB and enrich with mye_id.
Validation strategy (in order of preference):
1. Exact engine_code match (most precise)
2. Displacement-based match (e.g. all 1.8L engines for that make/model/year)
3. Broad make/model/year match (all engines for that make/model/year)
"""
from tenant_db import get_master_conn
try:
master = get_master_conn()
@@ -183,30 +212,66 @@ def _validate_vehicles(vehicles):
seen_mye = set()
for v in vehicles:
make, model, years, engine = _normalize_vehicle(v)
make, model, years, engine, engine_code = _normalize_vehicle(v)
if not make or not model or not years:
continue
for year in years:
# First try with exact engine match; if no result, fall back to
# make/model/year only. Engine descriptions rarely line up between
# QWEN and the master DB, so the fallback is the common path.
cur.execute("""
SELECT mye.id_mye
FROM model_year_engine mye
JOIN models m ON mye.model_id = m.id_model
JOIN brands b ON m.brand_id = b.id_brand
JOIN years y ON mye.year_id = y.id_year
JOIN engines e ON mye.engine_id = e.id_engine
WHERE b.name_brand ILIKE %s
AND m.name_model ILIKE %s
AND y.year_car = %s
AND e.name_engine ILIKE %s
LIMIT 1
""", (make, f'%{model}%', year, engine or '%'))
row = cur.fetchone()
matched_myes = []
if not row:
# Strategy 1: engine_code match (most precise)
if engine_code:
cur.execute("""
SELECT mye.id_mye
FROM model_year_engine mye
JOIN models m ON mye.model_id = m.id_model
JOIN brands b ON m.brand_id = b.id_brand
JOIN years y ON mye.year_id = y.id_year
JOIN engines e ON mye.engine_id = e.id_engine
WHERE b.name_brand ILIKE %s
AND m.name_model ILIKE %s
AND y.year_car = %s
AND e.engine_code ILIKE %s
""", (make, f'%{model}%', year, f'%{engine_code}%'))
matched_myes = [r[0] for r in cur.fetchall()]
# Strategy 2: displacement-based match
if not matched_myes:
disp = _extract_displacement(engine)
if disp is not None:
disp_pattern = f'{disp:.1f}L'
cur.execute("""
SELECT mye.id_mye
FROM model_year_engine mye
JOIN models m ON mye.model_id = m.id_model
JOIN brands b ON m.brand_id = b.id_brand
JOIN years y ON mye.year_id = y.id_year
JOIN engines e ON mye.engine_id = e.id_engine
WHERE b.name_brand ILIKE %s
AND m.name_model ILIKE %s
AND y.year_car = %s
AND e.name_engine ILIKE %s
""", (make, f'%{model}%', year, f'%{disp_pattern}%'))
matched_myes = [r[0] for r in cur.fetchall()]
# Strategy 3: exact engine string match (legacy)
if not matched_myes and engine and engine.lower() != 'desconocido':
cur.execute("""
SELECT mye.id_mye
FROM model_year_engine mye
JOIN models m ON mye.model_id = m.id_model
JOIN brands b ON m.brand_id = b.id_brand
JOIN years y ON mye.year_id = y.id_year
JOIN engines e ON mye.engine_id = e.id_engine
WHERE b.name_brand ILIKE %s
AND m.name_model ILIKE %s
AND y.year_car = %s
AND e.name_engine ILIKE %s
""", (make, f'%{model}%', year, engine))
matched_myes = [r[0] for r in cur.fetchall()]
# Strategy 4: broad make/model/year fallback (all engines)
if not matched_myes:
cur.execute("""
SELECT mye.id_mye
FROM model_year_engine mye
@@ -216,19 +281,21 @@ def _validate_vehicles(vehicles):
WHERE b.name_brand ILIKE %s
AND m.name_model ILIKE %s
AND y.year_car = %s
LIMIT 1
""", (make, f'%{model}%', year))
row = cur.fetchone()
matched_myes = [r[0] for r in cur.fetchall()]
if row and row[0] not in seen_mye:
seen_mye.add(row[0])
validated.append({
'make': make,
'model': model,
'year': year,
'engine': engine,
'mye_id': row[0],
})
# Deduplicate and add to results
for mye_id in matched_myes:
if mye_id not in seen_mye:
seen_mye.add(mye_id)
validated.append({
'make': make,
'model': model,
'year': year,
'engine': engine,
'engine_code': engine_code,
'mye_id': mye_id,
})
cur.close()
master.close()