Files
Autoparts-DB/pos/services/ai_chat.py
consultoria-as 8539720645 fix(pos): retry con backoff para rate limit 429 de OpenRouter
3 reintentos con espera 5s/10s/15s. Si agota reintentos,
muestra mensaje amigable en vez de error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:48:13 +00:00

113 lines
4.5 KiB
Python

# /home/Autopartes/pos/services/ai_chat.py
"""AI Chat service using OpenRouter for parts lookup assistance."""
import requests
import json
from config import OPENROUTER_API_KEY
OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
# ⚠️ SOLO MODELOS GRATUITOS — No cambiar a modelos de pago.
# El modelo DEBE terminar en ":free" para garantizar costo $0.
# Alternativas gratuitas: "meta-llama/llama-4-scout:free", "google/gemma-3-27b-it:free"
MODEL = "qwen/qwen3.6-plus-preview:free"
def _validate_model(model_id):
"""Ensure only free models are used. Raises if model is not free."""
if not model_id.endswith(':free'):
raise ValueError(f"BLOQUEADO: Solo se permiten modelos gratuitos (:free). Modelo '{model_id}' no es gratuito.")
SYSTEM_PROMPT = """Eres un asistente de refaccionaria automotriz mexicana. Tu trabajo es ayudar a encontrar autopartes.
IMPORTANTE: Responde SIEMPRE en formato JSON valido con esta estructura:
{
"message": "Tu respuesta al usuario en español",
"search_query": "termino de busqueda EN INGLES para el catalogo",
"vehicle": {"brand": "TOYOTA", "model": "Corolla", "year": 2020}
}
Reglas OBLIGATORIAS:
1. "search_query" SIEMPRE debe tener un valor cuando el usuario menciona una parte. NUNCA dejes null si el usuario pide algo.
2. "search_query" debe estar EN INGLES porque el catalogo TecDoc tiene nombres en ingles. Traducciones comunes:
- Balatas/Pastillas de freno = "Brake Pad"
- Discos de freno = "Brake Disc"
- Amortiguador = "Shock Absorber"
- Filtro de aceite = "Oil Filter"
- Filtro de aire = "Air Filter"
- Bujias = "Spark Plug"
- Banda serpentina = "V-Belt" o "Serpentine Belt"
- Bomba de agua = "Water Pump"
- Alternador = "Alternator"
- Radiador = "Radiator"
- Sensor de oxigeno = "Oxygen Sensor"
- Terminal de direccion = "Tie Rod End"
- Bomba de gasolina = "Fuel Pump"
- Clutch/Embrague = "Clutch Kit"
- Mofle/Escape = "Exhaust"
- Inyector = "Injector"
3. "vehicle" extrae marca, modelo y ano. La marca en MAYUSCULAS.
4. Nombres mexicanos: Tsuru = TSURU, Aveo = AVEO, Jetta = JETTA, Pointer = POINTER, Chevy = CORSA, Vocho = BEETLE.
5. No preguntes mas info si ya puedes buscar. Si el usuario dice "balatas para Tsuru 2015", busca directo.
6. "message" es breve y directo: "Buscando balatas para Nissan Tsuru 2015..."
"""
def chat(user_message, conversation_history=None):
"""Send a message to the AI and get a response with search suggestions."""
_validate_model(MODEL) # Block paid models
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
if conversation_history:
messages.extend(conversation_history)
messages.append({"role": "user", "content": user_message})
import time
max_retries = 3
for attempt in range(max_retries):
try:
resp = requests.post(
OPENROUTER_URL,
headers={
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
"Content-Type": "application/json",
},
json={
"model": MODEL,
"messages": messages,
"max_tokens": 500,
"temperature": 0.3,
},
timeout=20,
)
if resp.status_code == 429:
# Rate limited — wait and retry
wait = (attempt + 1) * 5 # 5s, 10s, 15s
if attempt < max_retries - 1:
time.sleep(wait)
continue
return {"message": "El asistente está ocupado. Intenta de nuevo en unos segundos.", "search_query": None, "vehicle": None}
resp.raise_for_status()
data = resp.json()
content = data["choices"][0]["message"]["content"]
# Try to parse JSON response
try:
stripped = content.strip()
if stripped.startswith("```"):
lines = stripped.split("\n")
json_str = "\n".join(lines[1:-1])
parsed = json.loads(json_str)
else:
parsed = json.loads(stripped)
return parsed
except (json.JSONDecodeError, IndexError):
return {"message": content, "search_query": None, "vehicle": None}
except Exception as e:
if attempt < max_retries - 1:
continue
return {
"message": f"Error de conexion: {str(e)}",
"search_query": None,
"vehicle": None,
}