Assistant
Overview
The Assistant message powers end-customer-facing automated conversations. Where Interaction and Reception analyse a conversation to support a human operator, the Assistant message decides whether the AI should answer the customer directly, and produces that answer.
Every Assistant request runs through four stages:
- Input Security & Moderation — screens the incoming customer message for moderation issues, sensitive topics, prompt-manipulation attempts and protection violations.
- Triage — determines the answer language, extracts the customer's requests, evaluates automation risk, detects an explicit request to talk to a human, and checks whether an escalation is needed.
- Path Definition — consolidates every signal above into two decisions,
human_handoffandend_conversation, and emits them before the reply is generated. - Response — generates the
automated_replyfollowing the path that was defined.
human_handoff and end_conversation are pushed over the WebSocket as soon as stage 3 completes, ahead of automated_reply. This lets your interface start routing to a human while the reply is still being generated.
API Reference
Endpoint
- WebSocket URL:
wss://ws-{{tenant}}.oco.optave.{{tld}} - REST Endpoint (POST):
https://{{tenant}}.oco.optave.{{tld}}/message/assistant
To be able to send messages, it is necessary to authenticate first.
Request Payload
The assistant message requires a payload containing the conversation history (conversations), connections.threadId, and context.organizationId (your client-specific organization ID). The other fields are optional.
scope.products[].id is optional. When present, retrieval adds a keyword search on that product alongside the usual vector search, which usually produces a more specific answer. Omitting it does not block the AI from answering.
- REST
- WebSocket
{
session: {
sessionId: "a1b2c3d4-e5f6-7890-abcd-123456789012",
channel: { language: "en-US" },
interface: { type: "chat" }
},
request: {
requestId: "a1b2c3d4-e5f6-7890-1234-56789ab2345",
attributes: {
variant: "A"
},
connections: {
journeyId: "",
parentId: "",
threadId: "9e8d7c6b-5a49-3827-1605-948372615abc" // REQUIRED
},
context: { // generated by optave
caseId: "",
departmentId: "",
operatorId: "",
organizationId: "f7e8d9c0-b1a2-3456-7890-123456789abc", // REQUIRED
userId: ""
},
reference: {
ids: [
{ name: "ticket_id", value: "T12345" }
],
labels: [],
tags: []
},
resources: {
codes: [],
links: [],
offers: []
},
scope: {
products: [ // Optional - adds a keyword search on this product
{ id: "PRD-4821" }
],
conversations: [ // REQUIRED
{
conversationId: "",
participants: [
{
participantId: "2c4f8a9b-1d3e-5f70-8293-456789012def",
displayName: "John Doe",
role: "user"
},
{
participantId: "5b8c9d0e-2f4a-6b1c-9d8e-123456789abc",
displayName: "Optave Assistant",
role: "assistant"
}
],
messages: [
{
content: "Is the 3-day tour still available for next weekend?",
participantId: "2c4f8a9b-1d3e-5f70-8293-456789012def",
timestamp: "2024-01-15T10:30:00.000Z"
}
],
metadata: {}
}
],
interactions: [
{
content: "Support ticket T12345 has been created for user John Doe",
id: "1",
name: "ticket_created",
role: "system",
timestamp: "2024-01-15T10:29:45.000Z"
}
]
}
}
}
This is the envelope the Optave JavaScript SDK produces, so sdkVersion carries the SDK release. If you connect to the WebSocket directly, without the SDK, send sdkVersion: "none" instead — the runnable examples further down do exactly that.
{
headers: {
correlationId: "20e35101-61e8-46e0-926d-6af84b378d95",
tenantId: "your-tenant-id-here",
traceId: "405101-61e8-46e0-926d-6af84b378d95",
idempotencyKey: "30e35101-61e8-46e0-926d-6af84b378d95",
identifier: "message",
action: "assistant",
schemaRef: "optave.message.v3",
sdkVersion: "3.6.0",
timestamp: "2025-09-05T16:30:00.000Z"
},
payload: {
session: {
sessionId: "a1b2c3d4-e5f6-7890-abcd-123456789012",
channel: { language: "en-US" },
interface: { type: "chat" }
},
request: {
requestId: "a1b2c3d4-e5f6-7890-1234-56789ab2345",
attributes: {
variant: "A"
},
connections: {
journeyId: "",
parentId: "",
threadId: "9e8d7c6b-5a49-3827-1605-948372615abc" // REQUIRED
},
context: { // generated by optave
caseId: "",
departmentId: "",
operatorId: "",
organizationId: "f7e8d9c0-b1a2-3456-7890-123456789abc", // REQUIRED
userId: ""
},
reference: {
ids: [
{ name: "ticket_id", value: "T12345" }
],
labels: [],
tags: []
},
resources: {
codes: [],
links: [],
offers: []
},
scope: {
products: [ // Optional - adds a keyword search on this product
{ id: "PRD-4821" }
],
conversations: [ // REQUIRED
{
conversationId: "",
participants: [
{
participantId: "2c4f8a9b-1d3e-5f70-8293-456789012def",
displayName: "John Doe",
role: "user"
},
{
participantId: "5b8c9d0e-2f4a-6b1c-9d8e-123456789abc",
displayName: "Optave Assistant",
role: "assistant"
}
],
messages: [
{
content: "Is the 3-day tour still available for next weekend?",
participantId: "2c4f8a9b-1d3e-5f70-8293-456789012def",
timestamp: "2024-01-15T10:30:00.000Z"
}
],
metadata: {}
}
],
interactions: [
{
content: "Support ticket T12345 has been created for user John Doe",
id: "1",
name: "ticket_created",
role: "system",
timestamp: "2024-01-15T10:29:45.000Z"
}
]
}
}
},
action: "message"
}
Responses
🟢 Success (200-201)
Over the WebSocket you receive each superpower as its own message, with a started state followed by a completed state. The Assistant workflow emits, by stage:
Input security & moderation: moderation, moderation_inappropriate, sensitive_topic, identify_language_games, identify_flipping, validate_protections, identify_poem
Triage: answer_language, extract_requests, identify_progress, talk_to_human, automation_risk, verify_activation, check_escalation_need
Path definition: human_handoff, end_conversation
Response: automated_reply
Non-blocking metadata: analyze_sentiment
🔴 Error
Error messages are similar in format to success messages, but contain an error state and a "response" JSON object:
{
action: "superpower",
actionType: "<superpower-name>",
state: "error",
message: {
results: [
{
response: [
{
content: 'Error while fetching results'
}
]
}
]
}
}
Path Decisions
human_handoff
"1" means the conversation must be routed to a human; "0" means the AI may continue. The reasons array tells you why, and flags.reversible tells you whether the decision may change on a later message in the same thread.
{
action: "superpower",
actionType: "human_handoff",
state: "completed",
message: {
results: [
{
superpowerName: ["human_handoff"],
response: [
{
role: "assistant",
content: "1",
reasons: ["business_logic"]
}
],
flags: { reversible: true }
}
]
}
}
| Reason | Meaning |
|---|---|
risk_detection | A security, moderation or risk check was triggered — an inappropriate message, a sensitive topic, a prompt-manipulation attempt, or a high automation risk. |
business_logic | A business rule requires a human — the customer asked to talk to a human, the request is outside the supported scope, the conversation is not progressing, or a recommendation has already been completed. |
escalation_needed | The triage stage explicitly flagged the conversation for escalation. |
These are the only three values reasons can contain.
A conversation is not escalated solely because it is non-English. The answer_language superpower still runs and still decides the language the reply is written in, but it is not a handoff condition. A non-English conversation can still hand off for any of the reasons above — asking to talk to a human, for instance.
flags.reversible is true only when the single reason is business_logic and the underlying rule is itself temporary (for example, the conversation is not yet making progress). Non-reversible decisions are cached against the threadId, so the thread stays with a human for the rest of its life. Any additional reason — or an explicit escalation — makes the decision non-reversible.
end_conversation
"1" means the AI's scope has ended (a recommendation was completed) and the conversation can be wrapped up. It is always non-reversible. When it is "1", the message carries a UI action you can render, and the matching human_handoff message carries a "Talk with a human" action.
{
action: "superpower",
actionType: "end_conversation",
state: "completed",
message: {
results: [
{
superpowerName: ["end_conversation"],
response: [
{
role: "assistant",
content: "1"
}
],
flags: { reversible: false },
actions: {
ui: [{ type: "button", label: "End conversation" }]
}
}
]
}
}
automated_reply
The customer-facing answer. Always check flags.skipReply before rendering it.
{
action: "superpower",
actionType: "automated_reply",
state: "completed",
message: {
results: [
{
superpowerName: ["automated_reply"],
response: [
{
role: "assistant",
content: "One moment while I find a team member to assist you."
}
],
flags: { skipReply: false }
}
]
}
}
When flags.skipReply is true, the content is an internal tracking message and must not be shown or forwarded to the customer. This happens on a handoff during the first interaction, before any agent has replied in the thread.