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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user