Un agente que conversa y un grafo que redacta: el intake de guria.lat
Cómo funciona el chat de guria.lat: un agente de LangChain junta los datos del proyecto y un grafo de LangGraph arma el brief y la propuesta.
Si abriste el chat de este sitio, hablaste con un agente. Le cuentas qué pasa en tu negocio, te hace un par de preguntas y, cuando el problema está claro, te muestra un resumen para que lo confirmes. Lo que pasa después no lo ves: en segundo plano se arma un brief interno para el equipo y una propuesta en PDF, y los dos llegan a nuestro correo para revisarlos antes de responderte.
Lo construí con LangChain y LangGraph. La decisión que más pesó fue partirlo en dos piezas con trabajos distintos.
Conversar es abierto, redactar no
Una conversación con un posible cliente no tiene guion. Hay quien llega con el problema bien definido y quien escribe “quiero una app con IA”. El asistente tiene que decidir qué preguntar, cuándo ya tiene lo suficiente y cuándo cortar. Eso es trabajo para un agente: create_agent de LangChain con dos herramientas, enviar_intake y cerrar_conversacion, y un checkpointer en SQLite que guarda cada conversación por thread_id. El navegador manda solo el mensaje nuevo y la historia vive en el servidor.
Redactar el brief, en cambio, es un proceso conocido: ver si el intake alcanza para estimar, buscar proyectos parecidos, estimar, revisar y escribir la propuesta. Ahí no quiero que el modelo decida el orden. Es un StateGraph fijo y el modelo solo trabaja dentro de algunos nodos.
evaluar → triage ─(vaga)──→ pedir_datos
└(clara)─→ buscar ─┬→ redactar ⇄ revisar ─(encaja)→ componer ⇄ revisar_propuesta
└→ marketing (en paralelo con redactar)
redactar escribe el brief como lo haría un product owner con criterio técnico: si es viable, qué se puede reutilizar, qué depende del cliente y qué hipótesis prueba la primera etapa. marketing lee el mismo intake desde el lado de quien compra: qué le duele de verdad, qué objeciones va a tener y por dónde conviene empezar la propuesta. Como solo necesita el intake, corre al mismo tiempo que redactar. En LangGraph eso se resuelve con dos aristas que salen del mismo nodo.
Las reglas van en código
Al principio todo estaba en el prompt: no dar precios, no inventar proyectos, no presionar. El modelo lo respetaba casi siempre, y casi siempre no alcanza para algo que va a leer un cliente. Ahora lo que se puede verificar se verifica con código.
revisar rechaza el brief si las horas mínimas superan a las máximas, si cita un proyecto que no existe o si dice “no viable hoy” y al mismo tiempo marca que encaja. Cuando falla, el borrador vuelve a redactar con la lista de problemas, dos veces como máximo. Si el brief concluye que el proyecto no encaja con lo que hacemos, no se escribe propuesta y el equipo decide leyendo el brief.
Con la propuesta pasa lo mismo, con reglas más estrictas porque la lee el cliente:
PRECIO = re.compile(r"(US\$|R\$|\$|€|\b(usd|brl)\b|\d\s*(mil\s+)?(dólares?|dolares?|reais|reales)\b)", re.I)
PLAZO = re.compile(r"\b\d+\s*(semanas?|meses|mes|días?|dias?|mês)\b", re.I)
Si aparece una moneda, un plazo, un guion largo o una frase de presión como “últimos cupos”, la propuesta vuelve a componer. La palabra “reales” tenía una trampa: también es adjetivo (“turnos reales”, “dados reais”), así que la expresión solo la cuenta como moneda cuando va pegada a un número.
El modelo nunca pone el precio
El brief interno lleva el esfuerzo en horas por entregable. El código convierte esas horas en costo y semanas con la tarifa del equipo, que vive en una variable de entorno, así que el número final nunca sale del modelo. La propuesta usa otro esquema que no tiene campos de precio ni de fecha: el modelo no tiene dónde escribirlos. Su único llamado a la acción es agendar una reunión online de 30 minutos, y el dinero se conversa ahí.
El asistente del chat tampoco pregunta por el presupuesto. Un presupuesto dicho al principio ancla la estimación, y prefiero que primero se entienda el problema.
El diseño lo pone el código
El modelo llena un esquema con el contenido de la propuesta y deck.py lo convierte en diapositivas HTML con la identidad visual del sitio, que Chromium headless pasa a PDF. El modelo no toca tipografía, colores ni maquetación, así que todas las propuestas se ven iguales y ninguna sale rota.
Todo se escribe en disco antes de mandar el correo. Si el envío falla, el intake, el brief y la propuesta ya están guardados y el contacto no se pierde.
Aprende, pero con alguien en el medio
Cada conversación queda guardada, pero ninguna cambia al agente de forma directa. Si lo que escribe un visitante pudiera modificar su comportamiento, envenenarlo sería trivial.
Cada noche un proceso clasifica las conversaciones con Jev, de TypeSafe: en qué categoría cae cada una, qué objeciones aparecieron y qué pregunta hizo el cliente sin recibir respuesta. Esa pregunta se elige entre los mensajes reales del cliente, el modelo no la redacta. Los lunes llega un reporte con conversión, objeciones y conversaciones que quedaron a mitad. Con eso ajusto el prompt o una regla, y el cambio pasa por git como cualquier otro.
Cómo sé que sigue funcionando
Un cambio en el prompt puede arreglar una cosa y romper otra. Para notarlo tengo evals de conversaciones completas: un modelo hace de cliente, con una personalidad tomada de un archivo de personas, y conversa con el agente hasta que la sesión termina. Después se mide la conversación. Lo que se ve en el texto se revisa con código: que no use voseo, que no pregunte por el presupuesto, que no mande markdown (el widget no lo muestra). Otro modelo hace de juez solo donde una expresión regular no alcanza. Cada corrida queda en LangSmith como experimento, así que puedo comparar antes y después de un cambio.
Lo que hay alrededor
La API es FastAPI y responde en streaming. En producción corre en Coolify al lado del sitio, y Caddy le pasa /api/* por la red interna: el navegador habla con el mismo dominio y no hace falta configurar CORS. Los límites también los pone la API, no el modelo: 2.000 caracteres por mensaje, 30 mensajes por sesión y 60 por IP por hora. Una sesión cerrada recibe un texto fijo y nunca llega al modelo.
Si quieres verlo funcionando, abre el chat y cuéntale un proyecto real. El equipo te escribe con la propuesta después de revisarla.