Bri Flow

Spread the love

Con la llegada de la IA, proyectos que antes eran inalcanzables a nivel individual hoy son posibles gracias a herramientas como github copilot. Bri Flow nace de una necesidad concreta: diseñar flujos conversacionales fijos con lógica compleja, integrables con agentes, formularios y sistemas RAG, manteniendo control, seguridad y extensibilidad. En este artículo explico cómo lo hice, qué decisiones técnicas tomé

Motivación y enfoque general

Problema inicial Las herramientas cerradas que probé no encajaban: poca flexibilidad para lógica avanzada, difícil integración con LLMs y RAG, y escasa capacidad para reutilizar subflujos. Además, necesitaba que los flujos fueran deterministas y auditables.

Decisión clave Representar el flujo como JSON estructurado. Con ese contrato claro, cualquier backend puede interpretar y ejecutar la lógica. Arquitectura y flujo de ejecución

Componentes principales

  • Editor visual: interfaz para crear nodos y conexiones; exporta JSON con la estructura del flujo.
  • Backend (.NET): intérprete del JSON, expression engine, funciones nativas, ejecución de llamadas REST, integración con agentes/LLMs y gestión de sesiones.
  • Frontend: consume la API del backend; recibe JSON procesado que indica qué componente renderizar (botón, formulario, carrusel, mensaje, etc.).

Flujo de datos (conceptual) Editor → JSON → Backend (.NET interpreta y ejecuta) → Frontend (renderiza según JSON procesado) → Usuario. El backend mantiene la sesión del usuario, el estado del flujo y filtra contenido por idioma cuando procede.

Idea central

Las expresiones no son código externo: son cadenas definidas dentro del JSON que exporta el editor. El backend recibe ese JSON, extrae las expresiones y las evalúa en tiempo de ejecución contra el contexto de la sesión y las variables del flujo. Por eso fue necesario diseñar un lenguaje/expression engine propio: las expresiones deben ser flexibles, seguras, deterministas y controlables desde el backend.

Beneficios prácticos

  • Procesar listas devueltas por servicios REST directamente en el flujo.
  • Evitar código ad-hoc en cada integración: la lógica se define en expresiones dentro del JSON.
  • Reutilización y trazabilidad: toda la lógica queda en el flujo exportado.

Ejemplos prácticos y fragmento JSON

A continuación un ejemplo simplificado que ilustra cómo se modela un flujo en JSON y cómo se usan expresiones para controlar la ejecución.

{
    "schema_version": 2,
    "flow_id": "principal",
    "version": "1.0.223",
    "name": "principal",
    "description": "Flujo  ejemplo",
   "locales":  ["es","pt","en"],
   "start_node":  "start",
   "nodes":  {
       "start":  {
          "id":  "start",
          "type":  "start",
          "next":  { "flow_id":  "principal",  "node_id":  "response_1" },
           "variables": [
              {  "name":  "selected_button_button_1", "defaultValue":  "",  "isList":  false }
           ]
      },
       "response_1":  {
          "id":  "response_1",
          "type":  "response",
          "next":  {  "flow_id": "principal",  "node_id":  "agent_call_1"  },
          "i18n":  {
             "es":  {  "text":  ["hola"] },
              "pt":  {  "text": ["oi"]  },
              "en":  { "text":  ["hi"]  }
          }
       },
       "agent_call_1": {
           "id": "agent_call_1",
           "type": "agent_call",
           "next": {  "flow_id":  "principal",  "node_id": "button_1"  },
          "save_as":  "agent_agent_call_1",
          "model":  {
              "provider":  "azure-openai",
             "deployment":  "gpt-4o-mini",
              "temperature":  0.2,
             "max_tokens":  800
          }
       },
       "button_1":  {
          "id":  "button_1",
          "type":  "button",
          "save_as":  "selected_button_button_1",
          "options":  [
             {  "target":  {  "flow_id": "",  "node_id":  "end_1"  }, "label":  "Opción  1",  "value": "Opción  1"  },
              { "target":  {  "flow_id":  "", "node_id":  "end_1"  },  "label": "Opción  2",  "value":  "Opcion 2"  }
          ]
       },
       "end_1":  { "id":  "end_1",  "type":  "end", "next":  null  }
   }
}

 

Ejemplo LINQ-like (expresión dentro del engine)

itemsVar.where(x => x.active).select(x => x.id).orderBy(x => x)

 

La pieza final es el frontend: llama a la API y renderiza los componentes según la estructura del JSON recibido; el backend se encarga de procesar la información y devolverla lista para pintar, adaptando la respuesta a la sesión del usuario y al idioma seleccionado al iniciar el flujo.

 

Deja un comentario