# Sugerencias con IA

Rellena un ticket con IA a partir de texto, audio o imágenes, recibe el resultado por consulta periódica o en tiempo real y registra qué aceptó la persona.

Las sugerencias con IA proponen el título, la descripción, la categoría y la prioridad de un ticket a partir de lo que cuenta la persona: un texto, una nota de voz o capturas de pantalla. Tu aplicación muestra la propuesta, la persona la acepta o la corrige, y con eso [abres el ticket](https://os.utilia.ai/docs/developers/sdk/tickets). La generación es una tarea en segundo plano: la pides, y recoges el resultado consultando su estado o escuchándolo en tiempo real. Funciona con la clave de tu aplicación externa, siempre desde tu servidor.

**De la descripción de la persona al ticket propuesto**

Participantes: Persona, Tu aplicación y UTILIA OS.

1. Persona → Tu aplicación: Cuenta el problema. Por escrito, con una nota de voz o con capturas.
2. Tu aplicación → UTILIA OS: `POST /api/external/v1/tickets/ai-suggest`. Encola la tarea con el texto, el audio o las imágenes.
3. UTILIA OS → Tu aplicación (respuesta): jobId
4. Tu aplicación → UTILIA OS: `GET /api/external/v1/tickets/ai-suggest/:jobId/stream`. Abre la conexión de eventos.
5. UTILIA OS → Tu aplicación (respuesta): Eventos progress y completed. completed trae suggestions, la transcripción si hubo audio y analyticsEventId.
6. Tu aplicación → Persona: Muestra la propuesta
7. Persona → Tu aplicación: La acepta, la corrige o la descarta
8. Tu aplicación → UTILIA OS: `POST /api/external/v1/tickets/ai-track-action`. trackAiAction registra la decisión con analyticsEventId.

_La forma en tiempo real; con getSuggestions, en lugar de la conexión de eventos, el SDK consulta el estado cada segundo._

## Antes de empezar

Todos los métodos de IA exigen el scope `support:tickets:ai`, además del aplicativo Tickets de soporte y la capacidad «Tickets propios» en el plan. Si mandas audio, primero se transcribe y las sugerencias se generan sobre esa transcripción; para transcribir sin pedir sugerencias, mira [Transcripción de audio](https://os.utilia.ai/docs/developers/sdk/ai-transcription).

## Qué envías y qué recibes

| JavaScript | Python | Qué es |
| - | - | - |
| `userId` | `user_id` | Obligatorio: el `externalId` de la persona. |
| `text` | `text` | El problema contado por escrito. Hace falta `text` o audio. |
| `audioBase64`, `audioFilename` | `audio_base64`, `audio_filename` | Nota de voz en base64 y su nombre con extensión (`webm`, `mp4`, `mp3` o `wav`). |
| `existingFields` | `existing_fields` | Lo que ya está escrito en el formulario (`title`, `description`, `category`, `priority`) y el contexto (`currentUrl`, `userAgent`, `appVersion`), para afinar la propuesta. |
| `images` | `images` | Hasta 5 capturas en base64, cada una con `base64`, `filename` y `mimeType` (`mime_type`). |

El resultado trae `suggestions` (con `title`, `description`, `category` y `priority`; cualquiera puede venir vacío), `transcription` si mandaste audio, y `analyticsEventId` (`analytics_event_id`), que usarás después para registrar qué hizo la persona con la propuesta.

## Tres formas de recoger el resultado

| Forma | JavaScript | Python | Cuándo |
| - | - | - | - |
| Todo en uno, consultando | `getSuggestions(datos, opciones?)` | `get_suggestions(datos, opciones, on_progress=...)` | La más sencilla. |
| En tiempo real | `streamSuggestions(datos, opciones?)` | `stream_suggestions(datos, opciones)` o `stream_suggestions_handle(datos, opciones)` | Progreso inmediato y opción de cancelar. |
| Paso a paso | `requestSuggestions(datos)` y `getSuggestionsStatus(jobId)` | `request_suggestions(datos)` y `get_suggestions_status(job_id)` | Si quieres controlar tú cada consulta. |

### Todo en uno

`getSuggestions` encola la tarea y consulta su estado hasta que termina. Opciones: intervalo entre consultas (1 s por defecto), espera máxima (60 s) y función de progreso. En JavaScript van en milisegundos (`pollInterval`, `maxWait`, `onProgress`); en Python, en segundos (`GetSuggestionsOptions(poll_interval, max_wait)`) y el progreso como `on_progress`. Si la tarea falla, no se encuentra o se agota la espera, lanza un error normal (`Error` en JavaScript, `RuntimeError` en Python) con el motivo.

#### JavaScript

```typescript
const result = await sdk.ai.getSuggestions(
  {
    userId: 'usr-456',
    text: 'No puedo descargar las facturas del mes de enero',
    existingFields: { currentUrl: 'https://app.ejemplo.com/facturas', appVersion: '2.1.0' },
  },
  { pollInterval: 500, maxWait: 30000, onProgress: (p) => console.log(`${p}%`) },
);

console.log(result?.suggestions?.title);
console.log(result?.suggestions?.category); // BUG, CONSULTA o SUGERENCIA
```

#### Python

```python
from utilia_sdk import AiSuggestInput, GetSuggestionsOptions

result = await sdk.ai.get_suggestions(
    AiSuggestInput(
        user_id="usr-456",
        text="No puedo descargar las facturas del mes de enero",
        existing_fields={"currentUrl": "https://app.ejemplo.com/facturas", "appVersion": "2.1.0"},
    ),
    GetSuggestionsOptions(poll_interval=0.5, max_wait=30.0),
    on_progress=lambda p: print(f"{p}%"),
)

if result and result.suggestions:
    print(result.suggestions.title)
    print(result.suggestions.category)  # BUG, CONSULTA o SUGERENCIA
```

### En tiempo real

Abre una conexión de eventos (Server-Sent Events) y recibe el progreso y el resultado en cuanto están listos, sin consultas repetidas. Opciones: función de progreso, función para errores no fatales y tiempo máximo (120 s por defecto; en milisegundos en JavaScript y en segundos en Python).

- **JavaScript**: `streamSuggestions` devuelve un objeto con `result` (una promesa con el resultado) y `abort()` para cancelar. Necesita `EventSource`: en Node.js, instala un polyfill como `eventsource` y asígnalo a `globalThis.EventSource`.
- **Python**: `stream_suggestions` devuelve directamente el resultado. Si necesitas poder cancelar, `stream_suggestions_handle` devuelve un objeto con `result` (una tarea que esperas con `await`) y `abort()`.

#### JavaScript

```typescript
const handle = await sdk.ai.streamSuggestions(
  {
    userId: 'usr-456',
    text: 'El botón de pago no responde',
    images: [{ base64: capturaBase64, filename: 'captura.png', mimeType: 'image/png' }],
  },
  { onProgress: (p) => console.log(`${p}%`), timeout: 60000 },
);

// handle.abort() si la persona cancela
const result = await handle.result;
console.log(result?.suggestions?.title);
```

#### Python

```python
from utilia_sdk import AiSuggestInput, ImageBase64, SseStreamOptions

handle = await sdk.ai.stream_suggestions_handle(
    AiSuggestInput(
        user_id="usr-456",
        text="El botón de pago no responde",
        images=[ImageBase64(base64=captura_b64, filename="captura.png", mime_type="image/png")],
    ),
    SseStreamOptions(on_progress=lambda p: print(f"{p}%"), timeout=60.0),
)

# handle.abort() si la persona cancela
result = await handle.result
if result and result.suggestions:
    print(result.suggestions.title)
```

Desde la 4.2.0, la clave de tu aplicación no viaja en la dirección de la conexión: en JavaScript, como `EventSource` no admite cabeceras, el SDK pide antes un pase de un solo uso atado a ese trabajo y abre la conexión con él; en Python, la clave va en la cabecera `X-Api-Key`. Aun así, la clave es un secreto de servidor: usa estos métodos desde tu servidor, nunca desde el navegador de tus usuarios. Si necesitas abrir la conexión por tu cuenta, pide el pase con [`createStreamToken`](https://os.utilia.ai/docs/developers/sdk/tickets#abrir-la-conexión-por-tu-cuenta).

> **Atención: Si consumes los eventos sin el SDK**
>
> El servidor envía todos los eventos con el tipo genérico `message`, sin línea `event:`. El tipo real (`progress`, `completed`, `failed` o `heartbeat`) va dentro del JSON, en el campo `event`. Escuchar `addEventListener('completed', ...)` no recibe nada nunca. El formato exacto está en [Endpoints de sugerencias con IA](https://os.utilia.ai/docs/developers/api/ticket-ai-suggestions).

### Paso a paso

`requestSuggestions` encola la tarea y devuelve su identificador: `{ jobId }` en JavaScript y el identificador como texto en Python. `getSuggestionsStatus` devuelve el estado: `pending`, `processing`, `completed` (con el resultado en `result`), `failed` (con el motivo en `error`) o `not_found`, y el progreso de 0 a 100.

#### JavaScript

```typescript
const { jobId } = await sdk.ai.requestSuggestions({ userId: 'usr-456', text: 'No carga el panel' });

let status = await sdk.ai.getSuggestionsStatus(jobId);
while (status.status === 'pending' || status.status === 'processing') {
  await new Promise((r) => setTimeout(r, 1000));
  status = await sdk.ai.getSuggestionsStatus(jobId);
}
if (status.status === 'completed') console.log(status.result?.suggestions);
```

#### Python

```python
import asyncio

from utilia_sdk import AiSuggestInput

job_id = await sdk.ai.request_suggestions(AiSuggestInput(user_id="usr-456", text="No carga el panel"))

status = await sdk.ai.get_suggestions_status(job_id)
while status.status in ("pending", "processing"):
    await asyncio.sleep(1)
    status = await sdk.ai.get_suggestions_status(job_id)
if status.status == "completed" and status.result:
    print(status.result.suggestions)
```

## Registrar qué hizo la persona

Después de enseñar la propuesta, registra con `trackAiAction` (`track_ai_action`) si la aceptó, la corrigió o la descartó. Así tu organización puede medir si las sugerencias ayudan.

| JavaScript | Python | Qué es |
| - | - | - |
| `eventId` | `event_id` | El `analyticsEventId` del resultado. Obligatorio. |
| `userAction` | `user_action` | `ACCEPTED_ALL`, `ACCEPTED_PARTIAL`, `REJECTED`, `MODIFIED` o `IGNORED`. Obligatorio. |
| `fieldsAccepted`, `fieldsModified`, `fieldsRejected` | `fields_accepted`, `fields_modified`, `fields_rejected` | Qué campos aceptó, cambió o rechazó. |
| `timeToDecisionMs` | `time_to_decision_ms` | Milisegundos desde que vio la propuesta hasta que decidió. |
| `source` | `source` | `SDK_EXTERNAL` o `WIDGET`. |

### JavaScript

```typescript
await sdk.ai.trackAiAction({
  eventId: result!.analyticsEventId!,
  userAction: 'ACCEPTED_PARTIAL',
  fieldsAccepted: ['title', 'description'],
  fieldsModified: ['priority'],
  timeToDecisionMs: Date.now() - shownAt,
  source: 'SDK_EXTERNAL',
});
```

### Python

```python
from utilia_sdk import TrackAiActionInput

await sdk.ai.track_ai_action(
    TrackAiActionInput(
        event_id=result.analytics_event_id,
        user_action="ACCEPTED_PARTIAL",
        fields_accepted=["title", "description"],
        fields_modified=["priority"],
        time_to_decision_ms=decision_ms,
        source="SDK_EXTERNAL",
    )
)
```

## Avisar de fallos del cliente

`reportError` (`report_error`) envía a UTILIA OS un fallo de tu integración de IA o de tu formulario por la [telemetría ligera](https://os.utilia.ai/docs/developers/api/client-errors), con gravedad baja y componente `sdk` si no indicas otro. Hace un solo intento y no lanza nunca: si el aviso falla, se descarta en silencio. Acepta `message` (obligatorio) y, si quieres, `stack`, `code`, `component`, `endpoint`, `method`, `requestId`, `url`, `userAgent` y, desde la 4.6.0, `release` y `environment`.

También acepta `context` (en JavaScript, un objeto o un texto, que viaja como `detail`), pero esa ruta no lo guarda. Si necesitas contexto, agrupación o la respuesta de cada error, usa `sdk.errors.capture` ([Capturar los errores de tu aplicación](https://os.utilia.ai/docs/developers/sdk/error-reporting)).

### JavaScript

```typescript
await sdk.ai.reportError({
  message: 'No se pudieron generar las sugerencias',
  code: 'SUGGESTIONS_FAILED',
  component: 'formulario-soporte',
});
```

### Python

```python
from utilia_sdk import ReportClientErrorParams

await sdk.ai.report_error(
    ReportClientErrorParams(
        message="No se pudieron generar las sugerencias",
        code="SUGGESTIONS_FAILED",
        component="formulario-soporte",
    )
)
```

## Siguiente paso

- [Tickets de soporte](https://os.utilia.ai/docs/developers/sdk/tickets): Abre el ticket con la propuesta que la persona ha aceptado.
- [Transcripción de audio](https://os.utilia.ai/docs/developers/sdk/ai-transcription): Convierte notas de voz en texto sin pedir sugerencias.
