# Manual para generar Flujos V2 con un asistente externo

## Propósito

Este documento permite convertir una descripción funcional escrita en lenguaje
natural en un JSON compatible con el motor de Flujos V2 de Sidelink.

El asistente que reciba este manual no forma parte de Sidelink. Su única tarea
es producir un borrador JSON importable. Sidelink continúa siendo la autoridad:
importa el archivo, lo valida, permite revisarlo visualmente y decide si se
activa.

Para generar un flujo se deben entregar juntos:

1. Este manual.
2. El contexto del tenant, siguiendo
   `CONTEXTO_TENANT_FLUJOS_V2.ejemplo.json`.
3. La descripción funcional del flujo deseado.

## Instrucción principal para el asistente

Generá exclusivamente un objeto JSON válido de Flow Standard V2.

- No incluyas Markdown, comentarios ni explicaciones alrededor del JSON.
- No inventes herramientas, áreas, facts, fuentes, normalizadores u operadores.
- Usá solamente las capacidades y áreas declaradas en el contexto del tenant.
- Copiá literalmente las claves canónicas de las áreas.
- Si falta una decisión funcional imprescindible, no la supongas: devolvé un
  JSON con `generation_notes` en la raíz detallando la información pendiente.
  Esta propiedad sirve para la revisión humana y no afecta el runtime actual.
- Nunca afirmes que una acción humana o de backend ocurrió si no existe un step
  `action` que la ejecute y confirme.

## Principios del estándar

### El prompt y el flujo cumplen funciones diferentes

El prompt del área contiene personalidad, reglas de negocio, límites y forma de
redactar. El flujo contiene el paso a paso operativo.

Un prompt puede funcionar sin flujo. Cuando existe un flujo asociado, el flujo
controla preguntas, decisiones, acciones, mensajes operativos y finalización.

La asociación se realiza por la clave con la que se guarda:

- `prompt_ventas` se asocia opcionalmente con `flujo_ventas`.
- `prompt_soporte` se asocia opcionalmente con `flujo_soporte`.

No generes un flujo para un prompt de otra área. La propiedad `activation.intents`
debe incluir el prompt asociado, por ejemplo `prompt_ventas`.

### Los datos no disparan herramientas

Un slot solamente conserva un dato. Nunca implica una acción por sí mismo.

- Detectar un DNI o CUIT no implica ejecutar `buscar_cliente`.
- Detectar coordenadas no implica consultar cobertura.
- Detectar un área mencionada por el cliente no implica transferir.

Para ejecutar una herramienta debe existir un step `action` explícito.

### El runtime es determinista

El runtime sigue las transiciones declaradas. No inventa pasos ni decide qué
herramienta sería conveniente. Por eso todos los caminos deben terminar en un
step conocido y cada decisión debe tener una salida por defecto.

## Estructura raíz

```json
{
  "id": "flujo_ventas_v2",
  "version": 2,
  "name": "Ventas",
  "activation": {
    "intents": ["prompt_ventas"]
  },
  "settings": {
    "entry_step": "pedir_localidad"
  },
  "slots": {},
  "backend_facts": {},
  "steps": []
}
```

Reglas:

- `version` debe ser exactamente `2`.
- `id`, `name` y `settings.entry_step` son obligatorios.
- `settings.entry_step` debe referenciar un step existente.
- `slots` debe ser un objeto, aunque esté vacío.
- `backend_facts` debe ser un objeto, aunque esté vacío.
- `steps` debe ser un array con al menos un step.
- Cada `id` de step debe ser único.
- Usá identificadores técnicos en minúsculas y `snake_case`.

`activation` es metadato de asociación. La validación estructural actual no lo
exige, pero debe declararse para que el flujo sea comprensible y transportable.

## Slots: datos aportados o confirmados por el cliente

Un slot representa información conversacional que el flujo necesita recordar.

Tipos admitidos:

- `string`
- `number`
- `boolean`
- `enum`
- `object`
- `array`

Ejemplo:

```json
{
  "coordenadas": {
    "type": "string",
    "required": true,
    "description": "Coordenadas compartidas por el cliente desde WhatsApp",
    "sources": ["detected.coordinates"],
    "normalize": ["trim"]
  }
}
```

Propiedades habituales:

- `type`: tipo del dato.
- `required`: información descriptiva para el editor y el generador.
- `description`: significado preciso del dato.
- `sources`: fuentes deterministas admitidas.
- `normalize`: normalizadores que se aplican al valor.
- `values`: valores canónicos de un `enum`.
- `aliases`: expresiones que equivalen a cada valor canónico.
- `match_mode`: `exact` o `contains`.

Fuentes disponibles:

- `detected.document`: DNI o CUIT detectado.
- `detected.email`: correo electrónico detectado.
- `detected.coordinates`: coordenadas detectadas.
- `detected.media...`: metadatos estructurados de un adjunto.
- `message.text`: texto normalizado del mensaje.
- `context...`: información confiable provista por el canal o la ejecución.

Normalizadores disponibles:

- `trim`
- `lowercase`
- `uppercase`
- `digits_only`

No inventes fuentes ni normalizadores.

### Enums y aliases

```json
{
  "resultado_prueba": {
    "type": "enum",
    "required": true,
    "description": "Resultado informado luego de realizar la prueba",
    "sources": ["message.text"],
    "match_mode": "contains",
    "values": ["resuelto", "no_resuelto"],
    "aliases": {
      "resuelto": ["ya funciona", "se solucionó", "volvió internet"],
      "no_resuelto": ["sigue igual", "no funciona", "continúa el problema"]
    }
  }
}
```

Las ramas deben comparar contra valores canónicos, nunca contra aliases.

## Backend facts: hechos confirmados por herramientas

Los facts representan resultados confiables del backend. El cliente no puede
crearlos ni confirmarlos mediante lenguaje natural.

```json
{
  "cliente_verificado": {
    "type": "boolean",
    "description": "Buscar cliente confirmó que el cliente existe"
  }
}
```

Capacidades actuales y facts que pueden producir:

### `buscar_cliente`

Entrada:

```json
{
  "dni": {
    "$from": "slot.documento"
  }
}
```

Facts posibles al finalizar correctamente:

- `cliente_verificado`
- `dni_validado`
- `cliente`

`cliente` puede contener datos del sistema de gestión. Solo referencies rutas
internas si el contexto del tenant o un ejemplo verificado confirma que existen.

### `transferir_operador`

Entrada:

```json
{
  "area": {
    "$literal": "tecnico"
  },
  "motivo": {
    "$literal": "Sin servicio"
  },
  "resumen": {
    "$template": "Documento: {{slot.documento}}. Resultado: {{slot.resultado_prueba}}."
  }
}
```

Facts posibles al finalizar correctamente:

- `transferencia_realizada`
- `area_transferencia`

La transferencia requiere `area`. `motivo` y `resumen` son opcionales para el
ejecutor, pero todo flujo real debería enviarlos.

## Áreas y nomenclatura del tenant

Las áreas no son universales. `soporte`, `tecnico`, `cobranza`, `cobros`,
`ventas` o `comercial` pueden ser nombres válidos en tenants distintos, pero no
son intercambiables automáticamente al generar el flujo.

El contexto del tenant debe declarar:

- `key`: clave canónica enviada al backend.
- `label`: nombre mostrado al cliente.
- `aliases`: nombres alternativos reconocidos por la instalación.
- `purpose`: explicación funcional para que el asistente elija correctamente.

Reglas obligatorias:

1. En `input.area.$literal` usá siempre el `key` canónico.
2. Copialo literalmente.
3. No uses el `label` ni un alias dentro del flujo.
4. No traduzcas ni singularices el nombre.
5. No inventes un área aunque semánticamente parezca obvia.
6. Si ninguna área coincide con el destino solicitado, informalo en
   `generation_notes`.

Importante: el validador V2 actual verifica que `area` sea un string, pero aún no
cruza ese valor con las áreas configuradas del tenant. Hasta incorporar esa
validación contextual, la revisión debe comprobarlo expresamente.

## Cobertura y coordenadas

La instalación actual no posee una herramienta automática para consultar
cobertura o factibilidad.

El procedimiento habitual es:

1. Pedir al cliente que comparta su ubicación desde WhatsApp.
2. Capturar `detected.coordinates`.
3. Recopilar los demás datos comerciales necesarios.
4. Transferir al área humana configurada para ventas o factibilidad.
5. Informar que un operador verificará la cobertura.

No generes capacidades como:

- `consultar_cobertura`
- `consultar_factibilidad`
- `validar_zona`
- `buscar_planes_por_coordenadas`

Tampoco generes un fact de cobertura si ninguna herramienta real puede
confirmarlo. Recibir coordenadas prueba que la ubicación fue compartida, no que
exista cobertura.

## Tipos de steps

Solo se admiten:

- `question`
- `action`
- `branch`
- `message`
- `end`

### `question`

Solicita o completa un slot:

```json
{
  "id": "pedir_ubicacion",
  "type": "question",
  "slot": "coordenadas",
  "instruction": "Solicitar que comparta su ubicación actual usando la función de ubicación de WhatsApp.",
  "next_step": "pedir_nombre"
}
```

Reglas:

- `slot` debe existir en `slots`.
- `instruction` es obligatoria.
- `next_step` debe existir.
- Si el slot ya está completo, el runtime avanza sin volver a preguntarlo.
- La instrucción describe qué debe lograr la respuesta, no un texto rígido.

### `message`

Indica un mensaje operativo sin capturar datos:

```json
{
  "id": "explicar_revision",
  "type": "message",
  "instruction": "Informar que la cobertura será revisada por un operador usando la ubicación compartida.",
  "next_step": "derivar_comercial"
}
```

`instruction` y `next_step` son obligatorios.

### `action`

Ejecuta una capacidad registrada:

```json
{
  "id": "derivar_comercial",
  "type": "action",
  "action": "transferir_operador",
  "input": {
    "area": {
      "$literal": "ventas"
    },
    "motivo": {
      "$literal": "Consulta de cobertura"
    },
    "resumen": {
      "$template": "Interesado: {{slot.nombre}}. Localidad: {{slot.localidad}}. Coordenadas: {{slot.coordenadas}}."
    }
  },
  "on_success": "fin",
  "on_failure": "transferencia_fallida"
}
```

Reglas:

- `action` debe existir en el catálogo del tenant.
- `input` es obligatorio y debe respetar el esquema de la capacidad.
- `on_success` y `on_failure` son obligatorios.
- No dirijas éxito y falla al mismo mensaje afirmando que se transfirió.

Una transición puede limpiar datos:

```json
{
  "target": "pedir_documento",
  "clear": ["slot.documento"]
}
```

Solo se pueden limpiar referencias `slot.*`, `fact.*` o `action.*`. No se puede
modificar `context.*`.

### `branch`

Elige un camino mediante condiciones deterministas:

```json
{
  "id": "resolver_resultado",
  "type": "branch",
  "cases": [
    {
      "when": {
        "ref": "slot.resultado_prueba",
        "op": "eq",
        "value": "resuelto"
      },
      "go_to": "cierre_resuelto"
    }
  ],
  "default_next": "derivar_tecnico"
}
```

Operadores disponibles:

- `eq`
- `neq`
- `exists`
- `in`

Condiciones compuestas:

```json
{
  "all": [
    {
      "ref": "fact.cliente_verificado",
      "op": "eq",
      "value": true
    },
    {
      "ref": "slot.categoria",
      "op": "in",
      "value": ["sin_servicio", "lentitud"]
    }
  ]
}
```

También se admiten `any` y `not`.

Cada branch debe tener:

- Al menos un elemento en `cases`.
- Una condición válida por caso.
- Un `go_to` existente por caso.
- `default_next` existente y obligatorio.

### `end`

Finaliza el flujo:

```json
{
  "id": "fin",
  "type": "end"
}
```

Un final no implica que una transferencia o gestión haya sido exitosa. Esa
confirmación proviene del resultado del step `action`.

## Bindings para construir inputs

### Referencia

- Slot: `{ "$from": "slot.documento" }`
- Fact: `{ "$from": "fact.cliente.id" }`
- Contexto: `{ "$from": "context.tenant_id" }`
- Resultado de una action: `{ "$from": "action.buscar_cliente.client" }`

### Literal

```json
{ "$literal": "ventas" }
```

### Template

```json
{
  "$template": "Nombre: {{slot.nombre}}. Coordenadas: {{slot.coordenadas}}."
}
```

Raíces permitidas:

- `slot`
- `fact`
- `context`
- `action`

No combines `$from`, `$literal` o `$template` con otras propiedades en el mismo
objeto.

## Ejemplo completo: ventas con revisión humana de cobertura

El `area.$literal` de este ejemplo debe sustituirse por una clave canónica real
del contexto del tenant.

```json
{
  "id": "flujo_ventas_cobertura_v2",
  "version": 2,
  "name": "Ventas con revisión de cobertura",
  "activation": {
    "intents": ["prompt_ventas"]
  },
  "settings": {
    "entry_step": "pedir_localidad"
  },
  "slots": {
    "localidad": {
      "type": "string",
      "required": true,
      "description": "Localidad donde se solicita el servicio",
      "sources": ["message.text"],
      "normalize": ["trim"]
    },
    "coordenadas": {
      "type": "string",
      "required": true,
      "description": "Coordenadas compartidas desde WhatsApp",
      "sources": ["detected.coordinates"],
      "normalize": ["trim"]
    },
    "nombre_apellido": {
      "type": "string",
      "required": true,
      "description": "Nombre y apellido del interesado",
      "normalize": ["trim"]
    },
    "telefono_alternativo": {
      "type": "string",
      "required": false,
      "description": "Teléfono alternativo informado por el interesado",
      "normalize": ["trim"]
    }
  },
  "backend_facts": {
    "transferencia_realizada": {
      "type": "boolean",
      "description": "El backend confirmó la transferencia"
    },
    "area_transferencia": {
      "type": "string",
      "description": "Área confirmada por la transferencia"
    }
  },
  "steps": [
    {
      "id": "pedir_localidad",
      "type": "question",
      "slot": "localidad",
      "instruction": "Preguntar en qué localidad desea contratar el servicio.",
      "next_step": "pedir_ubicacion"
    },
    {
      "id": "pedir_ubicacion",
      "type": "question",
      "slot": "coordenadas",
      "instruction": "Solicitar que comparta desde WhatsApp la ubicación exacta donde desea instalar el servicio.",
      "next_step": "pedir_nombre"
    },
    {
      "id": "pedir_nombre",
      "type": "question",
      "slot": "nombre_apellido",
      "instruction": "Solicitar nombre y apellido del interesado.",
      "next_step": "explicar_revision"
    },
    {
      "id": "explicar_revision",
      "type": "message",
      "instruction": "Informar que un operador revisará la cobertura usando la ubicación compartida.",
      "next_step": "derivar_ventas"
    },
    {
      "id": "derivar_ventas",
      "type": "action",
      "action": "transferir_operador",
      "input": {
        "area": {
          "$literal": "ventas"
        },
        "motivo": {
          "$literal": "Consulta de cobertura"
        },
        "resumen": {
          "$template": "Interesado: {{slot.nombre_apellido}}. Localidad: {{slot.localidad}}. Coordenadas: {{slot.coordenadas}}."
        }
      },
      "on_success": "fin",
      "on_failure": "informar_falla_transferencia"
    },
    {
      "id": "informar_falla_transferencia",
      "type": "message",
      "instruction": "Informar que no fue posible realizar la derivación en este momento, sin afirmar que quedó transferido.",
      "next_step": "fin"
    },
    {
      "id": "fin",
      "type": "end"
    }
  ]
}
```

## Lista de comprobación antes de devolver el JSON

- [ ] La salida es un único JSON válido, sin bloques Markdown.
- [ ] `version` es `2`.
- [ ] La intención coincide con el prompt y la clave de importación esperada.
- [ ] `entry_step` existe.
- [ ] Todos los steps tienen IDs únicos.
- [ ] Todos los destinos existen.
- [ ] Todos los steps son alcanzables o su aislamiento está justificado.
- [ ] Cada question referencia un slot declarado.
- [ ] Cada branch tiene `default_next`.
- [ ] Solo se usan operadores, fuentes y normalizadores admitidos.
- [ ] Solo se usan capacidades declaradas por el tenant.
- [ ] `transferir_operador.input.area.$literal` coincide exactamente con un
      `areas[].key` del contexto del tenant.
- [ ] Toda action tiene caminos diferentes y coherentes para éxito y falla.
- [ ] Ningún mensaje anuncia éxito antes de la confirmación de backend.
- [ ] Las coordenadas no se interpretan como cobertura confirmada.
- [ ] No se inventaron herramientas de cobertura.
- [ ] El resumen de transferencia usa únicamente slots y facts disponibles.

## Prompt sugerido para usar fuera de Sidelink

Copiar este texto junto con este manual, el contexto del tenant y la descripción:

```text
Convertí la descripción adjunta en un Flow Standard V2 importable en Sidelink.
Cumplí estrictamente el manual y el contexto del tenant. No inventes
capacidades, áreas, facts, fuentes, normalizadores ni operadores. Para
transferir, usá literalmente una clave canónica de areas[].key. La recepción de
coordenadas no confirma cobertura: la revisión es humana. Devolvé únicamente el
objeto JSON final, sin Markdown ni explicaciones.
```
