Chat now fetches tenant inventory summary (brands, counts, low-stock) and injects it into the AI system prompt so responses prioritize local stock. VIN decoder uses free NHTSA vPIC API to decode 17-char VINs and auto-fills the vehicle selector dropdowns when a catalog match is found. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,10 +89,80 @@ El search_query SIEMPRE debe ser en ingles (el catalogo TecDoc esta en ingles).
|
||||
"""
|
||||
|
||||
|
||||
def chat(user_message, conversation_history=None):
|
||||
"""Send a message to the AI and get a response with search suggestions."""
|
||||
def get_inventory_context(tenant_conn, branch_id=None):
|
||||
"""Build a summary string of the tenant's inventory for AI context.
|
||||
|
||||
Returns a string like:
|
||||
Este negocio tiene 1234 productos en inventario.
|
||||
Categorias: BOSCH (45), MONROE (32), ACDelco (28), ...
|
||||
Productos con stock bajo (<=3): 15
|
||||
"""
|
||||
cur = tenant_conn.cursor()
|
||||
try:
|
||||
# Total items
|
||||
where = "i.is_active = true"
|
||||
params = []
|
||||
if branch_id:
|
||||
where += " AND i.branch_id = %s"
|
||||
params.append(branch_id)
|
||||
|
||||
cur.execute(f"SELECT COUNT(*) FROM inventory i WHERE {where}", params)
|
||||
total = cur.fetchone()[0] or 0
|
||||
|
||||
if total == 0:
|
||||
return "CONTEXTO DEL INVENTARIO:\nEste negocio aun no tiene productos en inventario."
|
||||
|
||||
# Top brands with counts
|
||||
cur.execute(f"""
|
||||
SELECT i.brand, COUNT(*) as cnt
|
||||
FROM inventory i
|
||||
WHERE {where} AND i.brand IS NOT NULL AND i.brand != ''
|
||||
GROUP BY i.brand
|
||||
ORDER BY cnt DESC
|
||||
LIMIT 15
|
||||
""", params)
|
||||
brands = cur.fetchall()
|
||||
brand_list = ", ".join(f"{row[0]} ({row[1]})" for row in brands if row[0])
|
||||
|
||||
# Products with low stock (<=3)
|
||||
cur.execute(f"""
|
||||
SELECT COUNT(*) FROM inventory i
|
||||
WHERE {where}
|
||||
AND COALESCE((SELECT SUM(quantity) FROM inventory_operations WHERE inventory_id = i.id), 0) <= 3
|
||||
""", params)
|
||||
low_stock = cur.fetchone()[0] or 0
|
||||
|
||||
lines = [
|
||||
"CONTEXTO DEL INVENTARIO:",
|
||||
f"Este negocio tiene {total} productos en inventario.",
|
||||
]
|
||||
if brand_list:
|
||||
lines.append(f"Marcas disponibles: {brand_list}")
|
||||
lines.append(f"Productos con stock bajo (<=3 unidades): {low_stock}")
|
||||
lines.append("IMPORTANTE: Cuando busques partes, SIEMPRE prioriza lo que el negocio tiene en inventario local.")
|
||||
|
||||
return "\n".join(lines)
|
||||
except Exception:
|
||||
return ""
|
||||
finally:
|
||||
cur.close()
|
||||
|
||||
|
||||
def chat(user_message, conversation_history=None, inventory_context=None):
|
||||
"""Send a message to the AI and get a response with search suggestions.
|
||||
|
||||
Args:
|
||||
user_message: The user's chat message.
|
||||
conversation_history: Previous messages in the conversation.
|
||||
inventory_context: Optional inventory summary string to inject into the system prompt.
|
||||
"""
|
||||
_validate_model(MODEL) # Block paid models
|
||||
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
||||
|
||||
system_content = SYSTEM_PROMPT
|
||||
if inventory_context:
|
||||
system_content = SYSTEM_PROMPT + "\n\n" + inventory_context
|
||||
|
||||
messages = [{"role": "system", "content": system_content}]
|
||||
if conversation_history:
|
||||
messages.extend(conversation_history)
|
||||
messages.append({"role": "user", "content": user_message})
|
||||
|
||||
54
pos/services/vin_decoder.py
Normal file
54
pos/services/vin_decoder.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""VIN Decoder using NHTSA vPIC API (free, no key needed)."""
|
||||
import re
|
||||
import requests
|
||||
|
||||
|
||||
def decode_vin(vin):
|
||||
"""Decode a VIN number using NHTSA API.
|
||||
Returns: {make, model, year, engine, body_type, plant_country, fuel_type, error}
|
||||
"""
|
||||
vin = (vin or "").strip().upper()
|
||||
|
||||
# Validate: 17 alphanumeric chars, no I/O/Q
|
||||
if not re.match(r'^[A-HJ-NPR-Z0-9]{17}$', vin):
|
||||
return {"error": "VIN debe tener exactamente 17 caracteres alfanumericos (sin I, O, Q)."}
|
||||
|
||||
url = f"https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVinValues/{vin}?format=json"
|
||||
resp = requests.get(url, timeout=10)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()["Results"][0]
|
||||
|
||||
error_text = data.get("ErrorText", "") or ""
|
||||
# NHTSA returns error codes like "0 - ..." for no error
|
||||
has_real_error = error_text and not error_text.startswith("0")
|
||||
|
||||
displacement = data.get("DisplacementL", "") or ""
|
||||
cylinders = data.get("EngineCylinders", "") or ""
|
||||
engine_parts = []
|
||||
if displacement:
|
||||
engine_parts.append(f"{displacement}L")
|
||||
if cylinders:
|
||||
engine_parts.append(f"{cylinders}cyl")
|
||||
engine_str = " ".join(engine_parts)
|
||||
|
||||
model_year = data.get("ModelYear", "")
|
||||
year_int = None
|
||||
if model_year:
|
||||
try:
|
||||
year_int = int(model_year)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return {
|
||||
"vin": vin,
|
||||
"make": data.get("Make", "") or "",
|
||||
"model": data.get("Model", "") or "",
|
||||
"year": year_int,
|
||||
"engine": engine_str,
|
||||
"body_type": data.get("BodyClass", "") or "",
|
||||
"plant_country": data.get("PlantCountry", "") or "",
|
||||
"fuel_type": data.get("FuelTypePrimary", "") or "",
|
||||
"drive_type": data.get("DriveType", "") or "",
|
||||
"trim": data.get("Trim", "") or "",
|
||||
"error": error_text if has_real_error else "",
|
||||
}
|
||||
Reference in New Issue
Block a user