Saltar a contenido

Arquitectura del sistema

Esta página explica cómo está organizado el backend de Camarero IA y por qué. Es una visión panorámica (Diátaxis: explanation): describe los principios, el mapa de módulos y los dos planos de comunicación, y enlaza a las páginas que detallan cada subsistema. Pensada tanto para personas como para LLMs que necesiten un modelo mental del sistema antes de tocar código.

Qué construimos

Camarero IA es un sistema de camarero virtual con IA. El recorrido del cliente:

  1. Escanea un QR en la mesa → se abre una sesión (TableSession).
  2. Conversa con la IA en un chat.
  3. La conversación produce una Comanda (pedido) que pasa por un ciclo de vida con validación de IA y, según la política del restaurante, aprobación del manager.
  4. Cocina la prepara, se sirve y se paga.

Stack: Python 3.12 · FastAPI · SQLAlchemy 2.x async · Alembic · Pydantic v2 · PostgreSQL (asyncpg).

Principio rector: hexagonal modular

El backend sigue arquitectura hexagonal (ports & adapters) aplicada por módulo (decisión ADR-004). En lugar de una sola carpeta domain/ global, cada módulo funcional es un subconjunto autocontenido con sus propias cuatro capas:

app/modules/{modulo}/
├── domain/          # Lógica de negocio pura: entidades, value objects,
│                    #   eventos de dominio, excepciones, puertos (ABCs)
├── application/     # Use cases, commands, queries, DTOs, puertos cross-module
├── infrastructure/  # Repos SQLAlchemy, modelos ORM, mappers, adapters externos
└── interface/       # Routers FastAPI + schemas Pydantic

Regla de dependencia

Cada capa solo importa de la capa inmediatamente interior. Es inviolable:

Permitido Prohibido
Interface → Application Domain → cualquier capa exterior
Application → Domain Application → Infrastructure (usa puertos, no implementaciones)
Infrastructure → Domain (implementa puertos) Módulo A → Módulo B (import directo)

Cómo detectar una violación

Si en cualquier archivo de domain/ aparece un import de sqlalchemy, fastapi, pydantic o de otro módulo, es una violación de arquitectura. El gate de CI make lint-arch (import-linter) la bloquea automáticamente.

Comunicación entre módulos

Los módulos no se importan entre sí directamente (ADR-004). Hay dos mecanismos:

  • Puertos (síncrono, 1→1). El módulo que necesita algo declara un puerto en su propia capa. La implementación (un gateway) vive en el módulo dueño del dato y se cablea en el composition root (app/main.py). Ejemplos reales: register_menu_catalog_gateway, register_session_cart_gateway, register_billing_gateway, register_restaurant_policy_gateway, register_coursing_gateway.
  • Eventos de dominio (asíncrono, 1→N) (ADR-005). Los use cases publican eventos en event_bus (app/shared/domain/event_bus.py); los subscribers reaccionan, registrados vía app/core/event_wiring.py. Los eventos se publican después del commit.

Bootstrap autorizado a cruzar módulos

Solo tres archivos pueden importar de todos los módulos, porque su trabajo es ensamblar la app, no contener lógica: app/main.py (routers + cableado de puertos), app/core/event_wiring.py (subscribers) y el worker runner. Cualquier cuarto candidato requiere un ADR.

Lo transversal sin dueño de dominio vive en app/shared/ (clases base, paginación, i18n, money, identidad AdminUser, puertos infraestructurales, seam del POS/TPV) y el bootstrap en app/core/ (config, BD, lifespan, middleware, observabilidad). Ver Servicios de plataforma.

Mapa de módulos

Cada módulo en app/modules/ y su responsabilidad principal (todos registrados en app/main.py). La columna Detalle enlaza a la página de arquitectura del subsistema cuando existe.

Módulo Responsabilidad Detalle
auth Autenticación JWT de cliente y admin; autenticadores OAuth (Jose). Seguridad
admin_user Gestión de usuarios administradores, roles y permisos. Usuarios admin y roles
restaurant Restaurantes, salas (Room), conocimiento, políticas de aprobación. Mesas y salas
table Mesas y códigos QR (acceso público y admin). Mesas y salas
session TableSession (sesión de mesa), carrito (CartItem), llamadas al manager. Sesiones y carrito
menu Menús, categorías, productos, extras, promociones, conocimiento. Catálogo de menú
chat Conversación con IA (motor legacy + híbrido), árboles de decisión. Chat híbrido
comanda Pedido (Comanda/ComandaLine); state machine y ciclo de vida. Comanda
recommendation Recomendador determinista (capas 0–2) con artefactos precalculados. Recomendador
manager "Control de sala": dashboard del manager en tiempo real (SSE). Control de sala
payment Facturas (Bill) y pagos; pasarela PSP. Pagos
review Valoraciones del cliente y listado admin. Clientes y reseñas
analytics Eventos de comportamiento (capa de medición) y analítica admin. Servicios de plataforma
audit Registro de auditoría de operaciones admin. Servicios de plataforma
customer Datos del cliente final. Clientes y reseñas
errors Registro y consulta de logs de error. Servicios de plataforma

El detalle por módulo (símbolos, repos, puertos) está en el Mapa de módulos.

Los dos planos de comunicación

El sistema habla con el frontend por dos canales complementarios:

1. REST / OpenAPI (petición-respuesta)

Routers FastAPI bajo /api/v1 (cliente) y /api/v1/admin (admin), descritos por el contrato OpenAPI. Es el plano de comandos y consultas síncronas: validar sesión, añadir al carrito, enviar comanda, gestionar menú, aprobar pedidos, etc. Ver la referencia de la API.

El contrato OpenAPI es un activo protegido

Existe una red de tests contract-lock (tests/contract/). Las adiciones aditivas (campos nuevos) se permiten siempre que se cablee el frontend en el mismo cambio; los removals / renames / cambios de tipo de campos existentes rompen clientes desplegados y requieren cuidado. Cómo evolucionarlo: Evolucionar el contrato.

2. SSE (tiempo real, servidor → cliente)

Server-Sent Events (Content-Type: text/event-stream) para flujos unidireccionales que el servidor empuja: el streaming de la respuesta del chat y las actualizaciones en vivo del dashboard del manager. El contrato SSE (nombres de evento + forma del payload) también está versionado y testeado. Ver Tiempo real (SSE) y el inventario de eventos SSE.

Ciclo de vida de una petición de chat

Cuando el cliente envía un mensaje, la respuesta llega por streaming SSE:

  1. El cliente hace POST al router de chat (prefijo real /api/v1/chat/conversations/...). El handler de interface/ traduce la request a un command y delega en el use case.
  2. El use case (StreamAssistantReplyUseCase) orquesta el dominio y abre un stream SSE.
  3. Un dispatcher por modo (ModeDispatchingChatAiResponder) elige el motor según restaurants.chat_mode: legacy (por defecto) o hybrid. Es fail-open a legacy y el rollback es instantáneo volviendo el flag.
  4. El motor emite frames SSE: content (texto), : thinking (keep-alive) y un frame done con cart_actions + course_actions. El motor híbrido añade, de forma aditiva, un frame recommendation con tarjetas estructuradas.
  5. Las acciones de carrito/coursing detectadas se ejecutan vía puertos hacia session y comanda (el chat no importa esos módulos directamente).
  6. Tras el stream, los datos estructurados por-mensaje se persisten en chat_messages.extra_data y se publican por el bus para que el frontend los reciba.

El detalle (motores, árbol de decisión, recomendador) está en Chat híbrido.

Ciclo de vida de una comanda

La Comanda avanza por una state machine explícita con guards en el dominio (comanda/domain/state_machine.py). Camino feliz:

DRAFT → AI_REVIEWING → PENDING_MANAGER → PENDING → IN_PROGRESS → READY → SERVED → CLOSED
  • DRAFT. Borrador editable mientras el cliente compone el pedido.
  • AI_REVIEWING. Validación de IA. Es fail-closed: ante fallo se rechaza o se escala al manager, nunca se auto-aprueba.
  • PENDING_MANAGER. Espera aprobación humana, según la política del restaurante (full / ai_only / none, leída vía puerto de restaurant).
  • PENDING → IN_PROGRESS → READY → SERVED. La cocina la prepara, se marca lista y se sirve.
  • CLOSED. Cerrada (alimenta el flujo de pago).

Los estados de rechazo (AI_REJECTED, MANAGER_REJECTED) y DRAFT son editables: el cliente puede modificar y reenviar. La comanda usa optimistic locking (columna version) para concurrencia. Los hijos (ComandaLine) se modifican solo vía la raíz del agregado.

Detalle completo en Comanda.

Resiliencia y seguridad (resumen)

  • Errores tipados. Las excepciones de dominio heredan de DomainException; los handlers globales de app/main.py las traducen a HTTP (404, 403, 409, 422, 400). HTTPException solo en interface/. Hay handlers específicos para conflictos de comanda (ComandaConflict, InvalidComandaTransition → 409) y de mesa.
  • Multitenancy. Todo endpoint admin con scope pasa por require_restaurant_access; SUPER_ADMIN lo bypassa. Nunca se confía en el restaurant_id del body.
  • Idempotencia. Idempotency-Key en POST críticos (validar sesión, añadir al carrito, llamada al manager, pago).
  • Rate limiting persistente. Middleware RateLimitMiddleware con backend Redis/Valkey si está configurado, BD en su defecto (no in-memory).
  • Trazabilidad. RequestIDMiddleware (capa más externa) acuña/propaga X-Request-ID.
  • Dinero en céntimos (int) en BD; value object Money en el dominio.

Ver Seguridad y multitenancy.

Bootstrap y arranque

El composition root (app/main.py) ensambla la app: registra routers, cablea todos los puertos cross-module y los handlers de excepción. En el lifespan:

  • Crea las tablas y siembra el SUPER_ADMIN si hace falta (bootstrap_superadmin_if_needed).
  • Cablea los subscribers de eventos (wire_subscribers).
  • Si embeddings_enabled, hace warmup de los embeddings Granite (fire-and-forget).
  • Lanza el monitor de fondo del "control de sala" (run_lifecycle_background_task).

El seam del POS/TPV se inyecta aquí: por defecto LocalPOSAdapter responde el contrato POSAdapter desde las propias gateways de la plataforma (sin TPV externo). Detalle en Servicios de plataforma.

Páginas relacionadas