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