Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/lffiesco-svg/gastromovil/llms.txt

Use this file to discover all available pages before exploring further.

GastroMóvil ships an AI-powered menu assistant that lets customers ask natural-language questions about available dishes, filter by restaurant or category, and request a surprise recommendation. The chatbot is backed by LangChain’s ChatGroq integration pointing at Groq’s Llama 3.3 70B Versatile model. On each request the backend fetches all available products from the database, formats them into a structured prompt context, and forwards the conversation — including the last six exchanges — to the model.

Architecture Overview

Browser / Client

      │  POST /api/chatbot/
      │  { "mensaje": "...", "historial": [...], "categoria": "...", "restaurante": "..." }

ChatView (DRF APIView)

      ├── Producto.objects.filter(disponible=True)  ← DB query
      ├── construir_contexto_productos()            ← format + optional filter

      ├── [surprise mode?]
      │       ├── random.choice(productos)
      │       └── responder_razon(producto) → Groq (short appetizing phrase)

      └── [normal mode]
              └── responder_chat() → Groq (full conversation)

                        └── Response: { "respuesta": "..." }

LLM Configuration

The model is initialised once at module load time in chatbot/ai/chatbot.py:
from langchain_groq import ChatGroq

llm = ChatGroq(
    model="llama-3.3-70b-versatile",
    temperature=0.5,
    groq_api_key=os.getenv("GROQ_API_KEY")
)
temperature=0.5 balances consistency (low temperature) with enough variability to make recommendations feel natural rather than robotic.

Product Context Building

Before calling the LLM, construir_contexto_productos() converts the database result set into a plain-text block. Optional categoria and restaurante parameters filter the list first:
def construir_contexto_productos(productos, categoria=None, restaurante=None):
    filtrados = productos

    if categoria:
        filtrados = [p for p in filtrados
                     if p['categoria__nombre'] and
                     p['categoria__nombre'].lower() == categoria.lower()]
    if restaurante:
        filtrados = [p for p in filtrados
                     if p['categoria__restaurante__nombre'] and
                     p['categoria__restaurante__nombre'].lower() == restaurante.lower()]

    if not filtrados:
        return "No hay productos disponibles con ese filtro."

    lineas = []
    for p in filtrados:
        restaurante_id = p['categoria__restaurante__id']
        categoria_id   = p['categoria__id']
        url            = f"{FRONTEND_BASE_URL}/restaurantes/restaurante/{restaurante_id}/#cat-{categoria_id}"
        descripcion    = f" | Descripcion: {p['descripcion']}" if p.get('descripcion') else ""
        imagen_url     = f"http://localhost:8000/media/{p['imagen']}" if p.get('imagen') else ''
        lineas.append(
            f"- ID:{p['id']} | {p['nombre']} | ${p['precio']}"
            f" | Categoria: {p['categoria__nombre'] or 'Sin categoria'}"
            f" | Restaurante: {p['categoria__restaurante__nombre'] or 'Sin restaurante'}"
            f"{descripcion}"
            f" | URL: {url}"
            f" | IMAGEN: {imagen_url}"
        )
    return "\n".join(lineas)
Each product line exposes: ID, name, price, category, restaurant, description, a deep-link URL to the restaurant page anchored to the category section, and the Cloudinary image URL.

Request & Response Format

Request Payload

{
  "mensaje":     "¿Cuáles hamburguesas tienen disponibles?",
  "historial":   [
    { "role": "user",      "content": "Hola" },
    { "role": "assistant", "content": "¡Hola! ¿En qué te puedo ayudar?" }
  ],
  "categoria":   "Hamburguesas",
  "restaurante": ""
}
FieldTypeRequiredDescription
mensajestring (max 500)The user’s current message
historialarray of {role, content} objectsPrevious conversation turns; last 6 are kept
categoriastringFilter products to this category name
restaurantestringFilter products to this restaurant name

Normal Response

{
  "respuesta": "<p class=\"text-gray-700\">Tenemos estas hamburguesas disponibles:</p>\n<div class=\"mb-3\">..."
}
The respuesta value is HTML styled with Tailwind CSS classes, as instructed by the system prompt. The frontend renders it directly as inner HTML.

Conversation History

The responder_chat function prepends SystemMessage(content=SYSTEM_PROMPT) and then appends the last 6 messages from historial as alternating HumanMessage / AIMessage objects before appending the current user message:
mensajes = [SystemMessage(content=SYSTEM_PROMPT)]

if historial:
    for msg in historial[-6:]:
        if msg['role'] == 'user':
            mensajes.append(HumanMessage(content=msg['content']))
        else:
            mensajes.append(AIMessage(content=msg['content']))

mensajes.append(HumanMessage(content=prompt_usuario))
respuesta = llm.invoke(mensajes)
The 6-message window keeps token usage predictable while still allowing the model to resolve pronouns and follow-ups like “¿y cuánto cuesta el segundo?” from the previous exchange.

Surprise Mode

If the user’s message contains any of the trigger words — sorpréndeme, sorprendeme, recomiéndame, recomiendame, or sorpresa — the chatbot switches to surprise mode:
1

Pick a Random Product

random.choice(productos) selects one product from the full available list (no category or restaurant filter is applied in surprise mode).
2

Ask the LLM for a Short Reason

responder_razon(producto) sends a focused prompt to the LLM asking for a maximum of 20 words explaining why the user should order this dish:
def responder_razon(producto):
    prompt = (
        f"En máximo 20 palabras, dime por qué debería pedir '{nombre}' "
        f"({descripcion}) del restaurante {restaurante}. "
        "Solo la frase, sin comillas."
    )
    respuesta = llm.invoke([HumanMessage(content=prompt)])
    return respuesta.content.strip()
3

Return a Structured JSON Response

The view wraps the product data and the LLM’s phrase in a JSON string and returns it inside the standard respuesta field:
{
  "respuesta": "{\"modo\": \"sorpresa\", \"producto_id\": 14, \"nombre\": \"Smash Burger\", \"precio\": \"18000.00\", \"restaurante\": \"Burger Bros\", \"descripcion\": \"Doble carne con queso cheddar\", \"url\": \"http://…/restaurantes/restaurante/3/#cat-7\", \"imagen\": \"http://…/media/smash.png\", \"razon\": \"Jugosa, crujiente y con queso que se derrite — una explosión de sabor.\"}"
}
The frontend detects the "modo": "sorpresa" key in the parsed JSON and renders a product card rather than the standard HTML chat bubble.

System Prompt Behaviour

The SYSTEM_PROMPT in chatbot/ai/prompts.py constrains the model’s behaviour in several important ways:
  • List available products and their prices
  • Tell the user which restaurant a product belongs to
  • Filter by category, restaurant, or price range
  • Give recommendations based only on the product list passed in context
  • Use product descriptions to add context to answers
  • Take orders, confirm purchases, or process payments
  • Invent products, prices, restaurants, or URLs not present in the context
  • Answer questions about topics unrelated to the menu
The model is instructed to respond exclusively in HTML with Tailwind CSS classes, never in Markdown. Product mentions must follow a specific <div> template that includes the product name, price, restaurant name, and an orange CTA button linking to the restaurant page. Plain text uses <p class="text-gray-700">.

Error Handling

If the Groq API call raises an exception (network error, rate limit, etc.), responder_chat catches it and returns a safe fallback string:
try:
    respuesta = llm.invoke(mensajes)
    return respuesta.content
except Exception as e:
    print(f"[ERROR Groq]: {e}")
    return "Error al procesar tu consulta. Intenta de nuevo mas tarde."
Similarly, responder_razon falls back to a generic phrase:
except:
    return "¡Una opción deliciosa que no te puedes perder!"
The chatbot endpoint (POST /api/chatbot/) does not currently require authentication. Any client that can reach the API can query it without a session.

Build docs developers (and LLMs) love