# Conversation & Analytics

[← Back to the API Reference index](https://kaltura.github.io/intelligent-agents-sdk/reference/api-reference/index.md)


## Converse

```
POST https://genie.nvp1.ovp.kaltura.com/assistant/converse
```

Requires a conversation KS (see [Authentication](https://kaltura.github.io/intelligent-agents-sdk/reference/api/authentication/index.md#authentication)):

```bash
CONV_KS=$(curl -s -X POST "https://www.kaltura.com/api_v3/service/session/action/start" \
  -d "format=1" -d "secret=$AGENTIC_ADMIN_SECRET" \
  -d "partnerId=$AGENTIC_PARTNER_ID" -d "userId=learner-123" -d "type=0" -d "expiry=86400" \
  -d "privileges=geniegpcid:1389,setrole:PLAYBACK_BASE_ROLE" | tr -d '"')
```

```json
{
  "userMessage": "How do I fix video buffering?",
  "threadId": null,
  "sse": false
}
```

| Field | Notes |
|-------|-------|
| `userMessage` | Required |
| `threadId` | Omit for new conversation; pass previous value for memory |
| `sse` | `false` = NDJSON (default); `true` = SSE |
| `model_type` | Optional. The SDK does not set `model_type`; the default is `fast`. |
| `force_experience` | Must be one of `markdown`, `summarization`, `flashcards`, `avatar_only`. Anything else throws a `validation_error` before any network call. A hint about which experience to render, not a guaranteed outcome. |
| `request_vars` | `{{var}}` interpolation values. Needs `allow_client_variables:true` on the intellect. It is on by default (see [Authentication § The Five Services](https://kaltura.github.io/intelligent-agents-sdk/reference/api/authentication/index.md#the-five-services) for what an intellect is). They interpolate into prompts and `api` tool templates. Reserved `sys__*` keys (including `sys__user_id`) are set by the server. The SDK rejects a client-supplied value for them. For how `sys__user_id` gets its value, see [Authentication § Bind a session to a real end-user identity](https://kaltura.github.io/intelligent-agents-sdk/reference/api/authentication/index.md#authentication). Semantics in depth: [Dynamic Data Injection](https://kaltura.github.io/intelligent-agents-sdk/guides/dynamic-data-injection/index.md). |
| `capabilities` | Per-message capability override |

**`allow_client_variables`** must be on to send `request_vars`. It is on by default. Pin it with `mgmt.intellects.setClientVariablesEnabled(configId, true, adminKs)` (WRITE, admin KS; also exposed as `mgmt.intellectConfig.setClientVariablesEnabled`).

With it off, the turn streams back empty on the streaming paths. There is no HTTP error on `converse` and no socket error.

Both session classes (`KalturaAvatarSession`, `KalturaChatSession`) detect the pattern and emit a once-per-session `warning` event (`code: 'empty_turn_with_request_vars'`, variable names only, never values). The management converse helpers map a 403 that reports disabled client variables to a typed `client_variables_disabled` error. `KalturaChatSession.sendText()` can fail either way when it sends request variables: the empty turn with the warning, or a rejected promise with `client_variables_disabled`.

**Stream segments** (each line is a JSON object):

| `type` | Meaning |
|--------|---------|
| `"think"` | Processing (show spinner) |
| `"text"` | Response content. Concatenate `content` fields. On an avatar-enabled intellect, spoken content streams as `"avatar"` (and `"avatar-filler"`) instead, accumulated the same way. |
| `"tool"` / `"tool_response"` | Server tool call + result; `content` carries client commands |
| `"unisphere-tool"` | GenUI widget. `metadata.runtimeName` names the widget |
| `"error"` | The server refused or failed the turn. See [Refusals that arrive with HTTP 200](#refusals-that-arrive-with-http-200). |
| `"user-interruption"` | User barged in |

Key envelope fields: `threadId` (save for follow-ups), `messageId` (save for feedback), `isFinal:true` (stream done).

**Token-kind check timing.** `conversations.send(...)` and `converseOnce(...)` check the token kind before they return. `conversations.stream(...)` is an async generator, so its check runs on the first iteration. Iterate it, or use `send`, to get the check up front.

### Refusals that arrive with HTTP 200

The server reports some refusals in the stream, not in the HTTP status. The status is 200 and the body holds one `"error"` segment. The common case is a `threadId` that belongs to a different user: the segment reads `No permission for thread`, nothing is written, and the turn has no text.

The SDK raises these as a `KalturaError` so an empty reply is never mistaken for success:

| Where | Default | Opt out |
|---|---|---|
| `conversations.send`, `converseOnce` | Throws | `onErrorSegment: 'return'`. The result carries the segments in `errors`. |
| `KalturaChatSession.sendText` | Rejects and emits `error` | `onErrorSegment: 'warn'` in the constructor. Emits `warning` and resolves. |
| `converse` stream, `collectConverse` | Yields every segment and returns them in `errors`. You decide. | n/a |

| `error.code` | Meaning | What to do |
|---|---|---|
| `thread_access_denied` | The `threadId` belongs to a different user. | Start without that `threadId`, or mint the token with the `userId` that created the thread. |
| `stream_error` | Any other `error` segment. | Read `error.detail` and `error.body.errors`. |

`error.body` holds `errors` (the segments), `threadId`, and `partial` (the text, `threadId` and `messageId` that arrived before the refusal). A stream line that is not JSON, such as a proxy error page, becomes an `error` segment too, so it is never dropped.

The same rule applies to every HTTP call. A 200 reply whose body is an error is thrown, with the server text in `error.detail`. The SDK throws for an exception object, `{error: "..."}`, `{success: false}`, and a body with only status and message keys and a status of 400 or above. A 200 reply that starts like JSON but does not parse throws `invalid_response`. A failed call inside a Kaltura multirequest throws `ovp_error` naming the call.

**Stop a running turn:**

| Session | Call |
|---|---|
| `KalturaAvatarSession` | `session.interrupt()` sends a barge-in so the avatar yields the turn. It does not cancel a running turn on the server, and the SDK has no call that does. It throws `invalid_state` when the session is not connected or a tap-to-talk capture is open. |
| `KalturaChatSession` | Pass `signal` to `sendText(text, { signal })` and abort it. The request is cancelled and the turn rejects with code `aborted`. |

---

## Reserved Template Variables (`sys__*`)

The server sets these on every turn. They're available to `{{ ... }}` interpolation in `base_directive` / `prompts[].value` / `glossary` (see [Configure an Intellect](https://kaltura.github.io/intelligent-agents-sdk/reference/api/build/intellect/index.md#configure-an-intellect)) and in a Skill's `instructions` text (see [§ Skills](https://kaltura.github.io/intelligent-agents-sdk/reference/api/management-operations/index.md#skills)), regardless of `allow_client_variables`.

Before any network call, the SDK's own `request_vars` pre-flight guard rejects a client-supplied value for any key in the table below, plus `secrets` (see `request_vars` above):

| Variable | Resolves to | Notes |
|----------|-------------|-------|
| `sys__thread_id` | Current conversation thread id | |
| `sys__message_id` | Current message id | |
| `sys__user_id` | The bound end-user id | Empty by default (a KS with no `userId`). Bind a real identity with `Sessions.createConversationToken({ configId, userId })` or `createAgentToken({ agentId, userId })` (or `createAdminToken({ userId })`) so this variable resolves to a real id instead of staying empty. See [Authentication § Bind a session to a real end-user identity](https://kaltura.github.io/intelligent-agents-sdk/reference/api/authentication/index.md#authentication). |
| `sys__user_message` | The current turn's user text | |
| `sys__is_new_thread` | `true` on the first turn of a new thread, `false` otherwise | |
| `sys__avatar_enabled` | Whether the current thread has a live avatar attached | Used in a skill, prompt block or tool `condition` to gate it to avatar-only sessions, as a bare expression with no `{{ }}`, e.g. `condition: 'sys__avatar_enabled'`. See [Conditions](https://kaltura.github.io/intelligent-agents-sdk/reference/api/build/intellect/index.md#conditions). |
| `sys__avatar_share_screen_enabled` | True while the avatar session has a screen share running. The agent needs `screenShare.enabled` ([agent fields](https://kaltura.github.io/intelligent-agents-sdk/reference/api/build/avatar-and-agent/index.md)). False in plain conversations that never share | Also usable in a skill, prompt block or tool `condition`, e.g. `sys__avatar_enabled and not sys__avatar_share_screen_enabled` to gate a skill to avatar sessions that are NOT sharing a screen. |
| `sys__context_id` | The category/entry id the current context (and its knowledge base, if any) is scoped to | Set via `KalturaAvatarSession`'s `contextId` constructor option, sent on the live socket `join` payload. See [Architecture Reference · Connection and Handshake § The `join` payload](https://kaltura.github.io/intelligent-agents-sdk/reference/architecture-reference/connection-and-handshake/index.md#the-join-payload-step-2-carries-the-agent-config). Empty when no context was set at join. |
| `sys__context_type` | The type of that context (e.g. an `entry` vs. a `category`) | Set via `KalturaAvatarSession`'s `contextType` constructor option, alongside `contextId`; empty when no context was set at join. |
| `sys__ks` | The raw KS (Kaltura Session) for the current request | ⚠️ **Security warning: never reference `sys__ks` in a prompt whose output could be echoed back to a user or logged.** It is a live credential. Rendering it as plain text in a model response, chat transcript, or log can leak that credential. See [Security & Compliance](https://kaltura.github.io/intelligent-agents-sdk/reference/security/index.md#ks-kaltura-session-guidance-for-agents-ac-3--ac-6--ia-2). |
| `sys__user_obj.first_name` / `.last_name` / `.title` / `.company` / `.gender` / `.email` | Attributes of the bound-user object | Verify these resolve with `intellects.previewPrompt()` before shipping a prompt. The rendered preview flags unresolved references with a `reserved_user_attr_unresolved` warning. |
| `secrets.<NAME>` | A named secret configured on the intellect | Write-only. See [§ Secrets](https://kaltura.github.io/intelligent-agents-sdk/reference/api/build/tools-and-secrets/index.md#secrets-write-only). |

---

## Check Status

```
GET https://genie.nvp1.ovp.kaltura.com/assistant/status
```

Returns `{aiConsent, avatar, identifiedUser}`. `avatar` is non-null when the agent has an avatar configured.

---

## Threads

In this SDK, `threads.list`, `get`, `rename`, `delete`, `transcript`, `setAnalysis`, `clearAnalysis` and `messages.list`, `get`, `share` accept an admin, conversation or agent token. A widget token is rejected with `wrong_token_scope` before any request. A raw KS string is sent as is, and the server answers 403 for a widget KS. What each token reaches: [Security & Compliance § Session type](https://kaltura.github.io/intelligent-agents-sdk/reference/security/index.md#session-type). `threads.push`, `messages.report`, `messages.reportSummary`, `feedback.list` and `followups.list` need an admin token. Pager: `{"pageIndex":1,"pageSize":30}`.

| Operation | Endpoint | Body |
|-----------|----------|------|
| List | `POST .../v1/thread/list` | `{"filter":{"objectType":"ListThreadFilter"},"pager":{"pageIndex":1,"pageSize":30}}` |
| Get | `POST .../v1/thread/get` | `{"id":"UUID"}` |
| Rename | `POST .../v1/thread/update` | `{"id":"UUID","title":"New name"}` |
| Delete | `POST .../v1/thread/delete` | `{"thread_ids":["UUID"]}` |
| Transcript | `POST .../v1/thread/get_transcripts` | `{"id":"UUID"}` |

`...` = `https://genie.nvp1.ovp.kaltura.com`

**Thread list response** fields per object: `id`, `title`, `created_at`, `updated_at`, `status`.

**Transcript response:** `{"status":"success","data":"human: …\nai: …"}`. Plain text, one turn per line.

**Delete** returns `{totalCount, objects[]}`: the threads it actually deleted. Any id it did not delete (one that does not exist, or another user's thread under a user session) is left out, and the status is still 200. So check `objects`, not the status: `totalCount: 0` means nothing was deleted.

SDK: `mgmt.threads.{list, get, rename, delete, transcript}`. Three more write operations live only on the SDK (`setAnalysis`, `clearAnalysis`, `push`); see [Management Operations § Threads](https://kaltura.github.io/intelligent-agents-sdk/reference/api/management-operations/index.md#threads) for their request shapes.

> **Compliance note.** See [Security & Compliance](https://kaltura.github.io/intelligent-agents-sdk/reference/security/index.md#shared-responsibility-control-matrix-nist-800-53) for what the SDK provides versus what the operator must configure.

## Session-Completion Signal

Unlike the admin-token thread endpoints above, this one is called from the browser client itself, with the same **conversation KS** (`geniegpcid`) used for every other client-facing call. It mints nothing new and needs no elevated privilege.

| Operation | Endpoint | Body | Auth |
|-----------|----------|------|------|
| Session completed | `POST {genieUrl}/thread/session_completed` | `{"id":"<threadId>"}` | `Authorization: KS <conversation ks>` |

`{genieUrl}` is the session's `genieUrl` option, else the `region` entry (default `nvp1`: `https://genie.nvp1.ovp.kaltura.com`). It has no `/v1` prefix (a different route family from the thread CRUD above). Send it once at the end. Never await it on a page-unload path.

Call this the moment a conversation is genuinely over, so end-of-conversation lifecycle rules (summaries, insights, CRM pushes) fire right away.

`KalturaAvatarSession`/`KalturaChatSession`/`KalturaAgentSession` call this automatically on `disconnect()` (`sessionCompleteOnEnd`, default `true`) and on tab-close/backgrounding/bfcache. See [README.md § Ending a conversation cleanly](https://github.com/kaltura/intelligent-agents-sdk/blob/main/README.md#ending-a-conversation-cleanly-session_completed-signal) for the full config surface.

## Thread History and Context Size

The full transcript is sent as model context on every turn, so each turn carries more context as the thread grows. For long-running work, start a fresh thread per task and delete threads you do not need.

---

## Feedback and Follow-ups (SDK)

Use the SDK for feedback and follow-up suggestions. It handles the routes for you.

- `mgmt.feedback.add({message_id, is_positive, comment?}, convKs)`: thumbs up/down on a message. `message_id` comes from the converse stream.
- `mgmt.feedback.list(ks, opts)`: admin-only feedback listing, filterable by `messageIdEquals`/`messageIdsIn`/`threadIdEquals`/`agentIdEquals`/`isPositiveEquals`. ⚠️ SENSITIVE: contains end-user ids/names + verbatim question/feedback text. Treat as PII; scope and redact before sharing.
- `mgmt.followups.getSuggested(ks)`: starter questions for the partner or agent. The set can differ between calls. Per-answer follow-ups stream inline as `unisphere-tool` segments when `capabilities.generate_followup_questions:"on"`.
- `mgmt.followups.list(ks, opts)`: raw partner-wide follow-up and starter question records (distinct from `getSuggested`'s per-agent shortlist).

---

## Usage Analytics

Partner-scoped read-only CSV. It contains end-user IDs and verbatim questions (treat as PII).

SDK: `mgmt.messages.report(ks)` (raw CSV) / `mgmt.messages.reportSummary(ks)` (volume + feedback ratio + top questions, with a `_meta` provenance receipt).

`mgmt.messages.get(id, ks)` (`POST {genieUrl}/message/get`) fetches one message record directly, without paging through `messages.list()`. A message that does not exist, or that the token cannot read, throws a typed `not_found` or `forbidden` error.

---

## Knowledge Search (MCP)

Your indexed content is available over the [MCP](https://modelcontextprotocol.io) protocol at this one endpoint. This is separate from [MCP Integrations](https://kaltura.github.io/intelligent-agents-sdk/guides/mcp-integrations/index.md), which wires an intellect to *external* MCP servers via `intellectConfig.setMcpServers`.

```
POST https://genie.nvp1.ovp.kaltura.com/mcp/search
{ "query": "adaptive bitrate streaming" }
```

Returns `{status, data}`. A partner with no indexed content returns a `"couldn't find relevant information"` error response. SDK: `mgmt.knowledge.search(query, ks, opts)`.

`opts` passes through five optional tuning params:

| Param | Default |
|---|---|
| `top_n` | 5 |
| `with_line_numbers` | none |
| `margins_in_seconds` | 15 |
| `include_sources` | none |
| `entry_description` | none |

`include_sources:true` changes the success shape's `chapters` from `null` to an array, and `text` to `null`.

