feat(inventory): Qwen vehicle compatibility — store AI unmatched vehicles as text, Celery background sync, fix brand filter fallback, increase vehicle limits

This commit is contained in:
2026-05-18 19:32:35 +00:00
parent 0b1dc89faf
commit 50c0dbe7d4
6 changed files with 330 additions and 109 deletions

View File

@@ -42,16 +42,22 @@ def get_vehicle_fitment(part_number, name, brand):
{'role': 'user', 'content': prompt}
],
'temperature': 0.2,
'max_tokens': 4096,
'max_tokens': 8192,
},
timeout=45,
timeout=120,
)
response.raise_for_status()
raw = response.json()
finish_reason = None
if raw.get('choices') and len(raw['choices']) > 0:
msg = raw['choices'][0].get('message', {})
choice = raw['choices'][0]
msg = choice.get('message', {})
finish_reason = choice.get('finish_reason')
if msg:
content = msg.get('content') or ''
if not content:
# Fallback for reasoning models that return output in reasoning_content
content = msg.get('reasoning_content') or ''
if content:
break
except requests.RequestException as exc:
@@ -62,6 +68,8 @@ def get_vehicle_fitment(part_number, name, brand):
if not content:
err_msg = f'QWEN request failed: {last_error}' if last_error else 'Empty response from QWEN after 3 attempts'
if finish_reason == 'length':
err_msg += ' (response truncated by token limit — consider reducing prompt or increasing max_tokens)'
return {'vehicles': [], 'confidence': 0, 'notes': err_msg}
# Parse JSON from QWEN response (sometimes wrapped in markdown)
@@ -91,32 +99,21 @@ def _build_prompt(part_number, name, brand):
- Nombre/descripcion: {name}
- Marca del fabricante: {brand_str}
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",
"engine_code": "2ZR-FE",
"notes": "Sedan y hatchback"
}}
],
"confidence": 0.92,
"notes": "Compatible con plataforma E170. Verificar traccion delantera."
}}
Devuelve UNICAMENTE un JSON valido (sin markdown, sin backticks, sin texto adicional) con esta estructura:
{{"vehicles":[{{"make":"Toyota","model":"Corolla","year_range":"2014-2019","engine":"1.8L","engine_code":"2ZR-FE","notes":""}}],"confidence":0.92,"notes":""}}
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".
REGLAS OBLIGATORIAS:
1. "make" = marca del vehiculo.
2. "model" = modelo exacto (incluye variante si aplica).
3. USA "year_range" = string "YYYY-YYYY" cuando el MISMO modelo/motor abarca multiples anos consecutivos. Esto ahorra tokens y permite mas resultados.
4. USA "year" = int SOLO cuando sea un ano aislado sin rangos adyacentes.
5. "engine" = descripcion corta del motor (ej: "1.8L", "V6 3.5L"). Si no lo conoces, usa "".
6. "engine_code" = codigo exacto SI LO CONOCES. Si no, usa "".
7. "notes" = string vacio "" para ahorrar tokens, salvo que haya una advertencia critica.
8. Devuelve TODOS los vehiculos compatibles que conozcas. Minimo 1, maximo 200. Para piezas genericas (filtros de aceite, bujias, balatas, amortiguadores) incluye TODOS los modelos aplicables.
9. "confidence" entre 0.0 y 1.0. Valores >0.85 solo si estas muy seguro.
10. Incluye marcas y modelos populares en Mexico cuando apliquen.
11. Si la pieza es universal, indicalo en "notes".
"""
@@ -153,28 +150,42 @@ def _extract_vehicles(parsed):
def _normalize_vehicle(v):
"""Normalize vehicle dict from QWEN to standard keys."""
"""Normalize vehicle dict from QWEN to standard keys.
Supports:
- year: int or str (single year)
- year_range: str like "2003-2008" or "2003-2008"
- legacy: year as range string
"""
make = v.get('make') or v.get('marca') or ''
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 = []
if isinstance(year_raw, int):
years = [year_raw]
elif isinstance(year_raw, str):
# Try range "2003-2008"
m = re.match(r'(\d{4})\s*[-]\s*(\d{4})', year_raw)
# Prefer explicit year_range
year_range = v.get('year_range') or v.get('rango_ano') or ''
if isinstance(year_range, str):
m = re.match(r'(\d{4})\s*[-]\s*(\d{4})', year_range)
if m:
start, end = int(m.group(1)), int(m.group(2))
years = list(range(start, end + 1))
else:
# Try single year
m2 = re.match(r'(\d{4})', year_raw)
if m2:
years = [int(m2.group(1))]
# Fallback to year (int or str)
if not years:
year_raw = v.get('year') or v.get('ano') or v.get('año') or v.get('years') or v.get('anos') or ''
if isinstance(year_raw, int):
years = [year_raw]
elif isinstance(year_raw, str):
m = re.match(r'(\d{4})\s*[-]\s*(\d{4})', year_raw)
if m:
start, end = int(m.group(1)), int(m.group(2))
years = list(range(start, end + 1))
else:
m2 = re.match(r'(\d{4})', year_raw)
if m2:
years = [int(m2.group(1))]
return make, model, years, engine, engine_code
@@ -200,16 +211,30 @@ def _validate_vehicles(vehicles):
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)
If the master DB does not contain the vehicle (e.g. North-American models
missing from TecDoc), the vehicle is returned with mye_id=None so it can
be stored as a text-only QWEN record.
"""
from tenant_db import get_master_conn
try:
master = get_master_conn()
cur = master.cursor()
except Exception:
return []
# Master DB unreachable — return all vehicles as unmatched text
return [
{'make': v.get('make') or v.get('marca') or '',
'model': v.get('model') or v.get('modelo') or '',
'year': v.get('year') or v.get('ano') or v.get('año') or 0,
'engine': v.get('engine') or v.get('motor') or '',
'engine_code': v.get('engine_code') or v.get('codigo_motor') or '',
'mye_id': None}
for v in vehicles
]
validated = []
seen_mye = set()
seen_text = set() # (make, model, year) for text-only dedup
for v in vehicles:
make, model, years, engine, engine_code = _normalize_vehicle(v)
@@ -285,16 +310,30 @@ def _validate_vehicles(vehicles):
matched_myes = [r[0] for r in cur.fetchall()]
# Deduplicate and add to results
for mye_id in matched_myes:
if mye_id not in seen_mye:
seen_mye.add(mye_id)
if matched_myes:
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,
})
else:
# No match in master DB — store as text-only QWEN record
text_key = (make.upper(), model.upper(), year)
if text_key not in seen_text:
seen_text.add(text_key)
validated.append({
'make': make,
'model': model,
'year': year,
'engine': engine,
'engine_code': engine_code,
'mye_id': mye_id,
'mye_id': None,
})
cur.close()