"""QWEN 3.6 Fitment Service — Vehicle compatibility lookup via AI. Uses a private QWEN server (OpenAI-compatible API) to find compatible vehicles for a given part number + name + brand, then validates them against the master database (model_year_engine). """ import json import re import time import requests from config import QWEN_API_URL, QWEN_API_KEY, QWEN_MODEL def get_vehicle_fitment(part_number, name, brand): """Ask QWEN for compatible vehicles and validate against master DB. Returns: { 'vehicles': [{'make': 'Toyota', 'model': 'Corolla', 'year': 2015, 'engine': '1.8L', 'mye_id': 123}], 'confidence': 0.92, 'notes': '' } """ if not QWEN_API_URL or not QWEN_API_KEY: return {'vehicles': [], 'confidence': 0, 'notes': 'QWEN not configured'} prompt = _build_prompt(part_number, name, brand) content = '' last_error = None for attempt in range(3): try: response = requests.post( f"{QWEN_API_URL}/chat/completions", headers={ 'Authorization': f'Bearer {QWEN_API_KEY}', 'Content-Type': 'application/json', }, json={ 'model': QWEN_MODEL, 'messages': [ {'role': 'system', 'content': 'Eres un experto en autopartes mexicanas. Devuelve SIEMPRE JSON valido sin markdown.'}, {'role': 'user', 'content': prompt} ], 'temperature': 0.2, 'max_tokens': 2048, }, timeout=45, ) response.raise_for_status() raw = response.json() if raw.get('choices') and len(raw['choices']) > 0: msg = raw['choices'][0].get('message', {}) if msg: content = msg.get('content') or '' if content: break except requests.RequestException as exc: last_error = exc # Retry on request failure if attempt < 2: time.sleep(1) if not content: err_msg = f'QWEN request failed: {last_error}' if last_error else 'Empty response from QWEN after 3 attempts' return {'vehicles': [], 'confidence': 0, 'notes': err_msg} # Parse JSON from QWEN response (sometimes wrapped in markdown) parsed = _extract_json(content) if not parsed: return {'vehicles': [], 'confidence': 0, 'notes': 'Invalid JSON from QWEN: ' + str(content)[:200]} # Extract vehicles list from various possible structures vehicles_raw = _extract_vehicles(parsed) confidence = parsed.get('confidence', 0) or parsed.get('confianza', 0) notes = parsed.get('notes', '') or parsed.get('notas', '') or parsed.get('advertencia', '') # Validate vehicles against master DB validated = _validate_vehicles(vehicles_raw) return { 'vehicles': validated, 'confidence': confidence, 'notes': notes, } def _build_prompt(part_number, name, brand): brand_str = brand or 'desconocida' return f"""Dado el siguiente repuesto automotriz: - Numero de parte: {part_number} - Nombre: {name} - Marca del vehiculo: {brand_str} Devuelve UNICAMENTE un JSON valido (sin markdown, sin backticks) con esta estructura exacta: {{ "vehicles": [ {{"make": "Toyota", "model": "Corolla", "year": 2015, "engine": "1.8L 16V"}}, {{"make": "Toyota", "model": "Matrix", "year": 2014, "engine": "1.8L"}} ], "confidence": 0.92, "notes": "Compatible con motor 2ZR-FE" }} 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. """ def _extract_json(text): """Extract JSON object from text, handling markdown fences.""" if text is None: return None text = text.strip() if text.startswith('```'): text = re.sub(r'^```(?:json)?\s*', '', text) text = re.sub(r'\s*```$', '', text) match = re.search(r'\{.*\}', text, re.DOTALL) if not match: return None try: return json.loads(match.group(0)) except json.JSONDecodeError: return None def _extract_vehicles(parsed): """Try multiple possible keys for the vehicle list.""" for key in ['vehicles', 'vehiculos_compatibles', 'vehiculos', 'compatible_vehicles', 'results']: if key in parsed and isinstance(parsed[key], list): return parsed[key] # Deep search for any list containing objects with marca/modelo or make/model for val in parsed.values(): if isinstance(val, list) and len(val) > 0: first = val[0] if isinstance(first, dict): if any(k in first for k in ['make', 'model', 'marca', 'modelo']): return val return [] def _normalize_vehicle(v): """Normalize vehicle dict from QWEN to standard keys.""" 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 '' # 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) 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))] return make, model, years, engine def _validate_vehicles(vehicles): """Look up each vehicle in master DB and enrich with mye_id.""" from tenant_db import get_master_conn try: master = get_master_conn() cur = master.cursor() except Exception: return [] validated = [] seen_mye = set() for v in vehicles: make, model, years, engine = _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() if not row: 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 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() 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], }) cur.close() master.close() return validated