Conversation Creation + Management
This guide covers the complete lifecycle of conversations in the Amigo platform, from creation and interaction to monitoring agent actions and system integration.
Overview
Conversations are the primary way users interact with Amigo AI agents. Each conversation is a session between a user and an agent that represents an instance of a service-defined experience, containing a sequence of messages and associated metadata.
Key Concepts
Conversation: A session between a user and an agent that is an instance of a conversation based on a service
Service: An experience (e.g., PCP check-in, customer support, etc.) that defines the agent's behavior and capabilities
Agent: The AI entity that responds to users within a conversation
Message: A single message from either the user or agent
Interaction: A full exchange consisting of a user message and the agent's complete response
Version Set: A deployment environment for a service (e.g., "release", "staging")
Conversation Lifecycle
Creation: Initialize a new conversation instance based on a service
Interaction: Full exchanges between user and agent (user message + agent response)
Monitoring: Track agent actions and dynamic behaviors
Integration: Connect with external systems based on conversation events
Termination: End conversations automatically or manually
Creating a New Conversation
You can initiate a new conversation in three ways: by letting the agent start with the first message, by providing an initial user message to begin the interaction, or by starting with an external event message.
Prerequisites
Before creating a conversation, ensure you have:
A valid JWT token for user authentication
The service ID for the AI agent you want to interact with
Your organization ID
A token refresh mechanism (recommended for production)
Authentication & Token Management
For production applications, ensure you have a valid JWT token for authentication. See the Authentication Guide for detailed token management patterns.
Basic Requirements:
Valid JWT token for user authentication
Organization ID for your Amigo instance
Service ID for the AI agent you want to interact with
Important: All API requests require the Authorization: Bearer <AUTH-TOKEN-OF-USER>
header.
Agent-First Conversations
When no initial_message
is provided, the agent will send the first message. This is useful for creating greeting-based interactions:
curl --request POST \
--url 'https://api.amigo.ai/v1/<YOUR-ORG-ID>/conversation/?response_format=text' \
--header 'Authorization: Bearer <AUTH-TOKEN-OF-USER>' \
--header 'Accept: application/x-ndjson' \
--header 'Content-Type: application/json' \
--data '{
"service_version_set_name": "release",
"service_id": "<SERVICE_ID>"
}'
User-First Conversations
To start with a user message, include the initial_message
field. This approach skips the agent's greeting and jumps directly to addressing the user's need:
curl --request POST \
--url 'https://api.amigo.ai/v1/<YOUR-ORG-ID>/conversation/?response_format=text' \
--header 'Authorization: Bearer <AUTH-TOKEN-OF-USER>' \
--header 'Accept: application/x-ndjson' \
--header 'Content-Type: application/json' \
--data '{
"service_version_set_name": "release",
"service_id": "<SERVICE_ID>",
"initial_message": "Hello, I need help with my account settings",
"initial_message_type": "user-message"
}'
When to use user-first conversations:
Jumping straight to the user's specific need
Pre-populating conversations from user actions (e.g., help buttons)
Creating contextual conversations based on user intent
Implementing chatbots that respond to specific triggers
External Event Messages
You can start a conversation with an external event message by including both the initial_message
and initial_message_type
fields, where initial_message_type
is set to "external-event"
:
curl --request POST \
--url 'https://api.amigo.ai/v1/<YOUR-ORG-ID>/conversation/?response_format=text' \
--header 'Authorization: Bearer <AUTH-TOKEN-OF-USER>' \
--header 'Accept: application/x-ndjson' \
--header 'Content-Type: application/json' \
--data '{
"service_version_set_name": "release",
"service_id": "<SERVICE_ID>",
"initial_message": "URGENT: System failure detected in payment processing",
"initial_message_type": "external-event"
}'
When to use external event messages:
System-generated notifications and alerts
Automated workflow triggers
Emergency scenario initiation
External system integration events
Equivalent TypeScript / Node 18+ example (re‑uses the parseNdjsonStream
helper defined later in this document):
import fetch from "node-fetch"; // remove if your environment already has global fetch
import { parseNdjsonStream } from "./parseNdjsonStream"; // or copy the helper from the example below
export async function createConversation() {
const ORG_ID = "<YOUR‑ORG‑ID>";
const AUTH_TOKEN = "<AUTH-TOKEN-OF-USER>";
const body = {
service_version_set_name: "release",
service_id: "<SERVICE_ID>",
};
const resp = await fetch(`https://api.amigo.ai/v1/${ORG_ID}/conversation/?response_format=text`, {
method: "POST",
headers: {
Authorization: `Bearer ${AUTH_TOKEN}`,
"Content-Type": "application/json",
Accept: "application/x-ndjson",
},
body: JSON.stringify(body),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
for await (const evt of parseNdjsonStream(resp.body!)) {
if (evt.type === "conversation-created") {
return evt.conversation_id as string;
}
if (evt.type === "error") {
throw new Error(evt.message ?? "server error");
}
}
throw new Error("conversation-created event not received");
}
// User-first conversation with initial message
export async function createConversationWithMessage(initialMessage: string) {
const ORG_ID = "<YOUR‑ORG‑ID>";
const AUTH_TOKEN = "<AUTH-TOKEN-OF-USER>";
const body = {
service_version_set_name: "release",
service_id: "<SERVICE_ID>",
initial_message: initialMessage,
initial_message_type: "user-message",
};
const resp = await fetch(`https://api.amigo.ai/v1/${ORG_ID}/conversation/?response_format=text`, {
method: "POST",
headers: {
Authorization: `Bearer ${AUTH_TOKEN}`,
"Content-Type": "application/json",
Accept: "application/x-ndjson",
},
body: JSON.stringify(body),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
for await (const evt of parseNdjsonStream(resp.body!)) {
if (evt.type === "conversation-created") {
return evt.conversation_id as string;
}
if (evt.type === "error") {
throw new Error(evt.message ?? "server error");
}
}
throw new Error("conversation-created event not received");
}
// External event conversation with predefined message
export async function createConversationWithExternalEvent(eventMessage: string) {
const ORG_ID = "<YOUR‑ORG‑ID>";
const AUTH_TOKEN = "<AUTH-TOKEN-OF-USER>";
const body = {
service_version_set_name: "release",
service_id: "<SERVICE_ID>",
initial_message: eventMessage,
initial_message_type: "external-event",
};
const resp = await fetch(`https://api.amigo.ai/v1/${ORG_ID}/conversation/?response_format=text`, {
method: "POST",
headers: {
Authorization: `Bearer ${AUTH_TOKEN}`,
"Content-Type": "application/json",
Accept: "application/x-ndjson",
},
body: JSON.stringify(body),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
for await (const evt of parseNdjsonStream(resp.body!)) {
if (evt.type === "conversation-created") {
return evt.conversation_id as string;
}
if (evt.type === "error") {
throw new Error(evt.message ?? "server error");
}
}
throw new Error("conversation-created event not received");
}
The response takes the form of a body stream (MDN) containing New line Delimited JSON (ndjson):
// Agent-first conversation (no initial_message)
{"type":"conversation-created","conversation_id":"67d871c23eb91293ecfae8b0"}
{"type":"new-message","message":"","message_metadata":[],"transcript_alignment":null,"stop":false,"sequence_number":0,"message_id":"67d871c23eb91293ecfae8b2"}
{"type":"new-message","message":"Hi","message_metadata":[],"transcript_alignment":null,"stop":false,"sequence_number":1,"message_id":"67d871c23eb91293ecfae8b2"}
{"type":"new-message","message":" there! How can I help you today?","message_metadata":[],"transcript_alignment":null,"stop":false,"sequence_number":2,"message_id":"67d871c23eb91293ecfae8b2"}
{"type":"interaction-complete"}
Response Stream Events
The stream includes several event types as denoted by the type
field:
ConversationCreatedEvent
conversation-created
Provides the conversation_id
and confirms successful creation
UserMessageAvailableEvent
user-message-available
Only appears when initial_message
is provided. Contains the initial message and its ID. For initial_message_type: "user-message"
, this represents a user message. For initial_message_type: "external-event"
, this represents an external event message.
ErrorEvent
error
Indicates an internal error occurred. When this happens, none of the messages/artifacts in this interaction persist, and the entire API call is rolled back
NewMessageEvent
new-message
Contains message chunks (text or voice based on parameters)
InteractionCompleteEvent
interaction-complete
Signals successful stream completion
CurrentAgentActionEvent
current-agent-action
Only emitted if the current_agent_action_type
query parameter is set. Describes the action the agent's taking to produce the response
Important Notes
Store the conversation ID: The
conversation_id
from theconversation-created
event is required for all subsequent interactionsHandle errors gracefully:
error
events indicate the entire operation has been rolled backWait for completion: Always process events until you receive
interaction-complete
One active conversation per service: Users cannot have multiple active conversations for the same service
Understanding the agent's actions:
You can use the CurrentAgentActionEvent
to understand the agent's actions as it's generating the response. The event follows the schema below:
{
"type": "current-agent-action",
"action": <See action below>
}
You can use the below endpoint to retrieve dynamic behavior set versions:
Get the versions of a dynamic behavior set.
Permissions
This endpoint requires the following permissions:
DynamicBehaviorInstruction:GetDynamicBehaviorInstruction
for the dynamic behavior set to retrieve.
The ID of the dynamic behavior set.
^[a-f0-9]{24}$
The versions of the dynamic behavior set to retrieve. One can specify an exact version to retrieve, which is either the version number or latest
, which retrieves the latest version. Alternatively, one can specify a range of inclusive lower and upper bound for the version number separated by -
, and every version within the range would be retrieved.
1
The maximum number of dynamic behavior set versions to return.
10
The continuation token from the previous request used to retrieve the next page of dynamic behavior set versions.
0
The fields to sort the versions by. Supported fields are version
. Specify a +
before the field name to indicate ascending sorting and -
for descending sorting. Multiple fields can be specified to break ties.
[]
The Mongo cluster name to perform this request in. This is usually not needed unless the organization does not exist yet in the Amigo organization infra config database.
[]
Succeeded.
Invalid authorization credentials.
Missing required permissions.
Specified organization or dynamic behavior set is not found.
Invalid request path parameter or request query parameter failed validation.
The user has exceeded the rate limit of 500 requests per minute for this endpoint.
The service is going through temporary maintenance.
GET /v1/{organization}/dynamic_behavior_set/{dynamic_behavior_set_id}/version/ HTTP/1.1
Host: api.amigo.ai
Authorization: Bearer JWT
Accept: */*
{
"dynamic_behavior_set_versions": [
{
"_id": "text",
"org_id": "text",
"created_at": "2025-08-27T16:06:19.770Z",
"updated_at": "2025-08-27T16:06:19.770Z",
"dynamic_behavior_set_id": "text",
"version": 1,
"conversation_triggers": [
"text"
],
"action": {
"type": "text",
"instruction": "text",
"overrides_instructions": true
}
}
],
"has_more": true,
"continuation_token": 1
}
You can use the below endpoint to compute metrics:
Evaluate the specified metrics for the given conversation, optionally up to the specified interaction.
Permissions
This endpoint requires the following permissions:
Metric:EvaluateMetric
for the metrics.Metric:GetMetricEvaluationResult
for the metric results.
The Mongo cluster name to perform this request in. This is usually not needed unless the organization does not exist yet in the Amigo organization infra config database.
[]
The IDs of the metrics to evaluate.
The ID of the conversation to evaluate the metrics for.
^[a-f0-9]{24}$
If specified, only messages up to (and including) this interaction will be evaluated.
^[a-f0-9]{24}$
Succeeded.
The conversation has no interactions.
Invalid authorization credentials.
Missing required permissions.
Specified organization, conversation, interaction, or metric is not found.
Invalid request path parameter or request body failed validation.
The user has exceeded the rate limit of 20 requests per minute for this endpoint.
The service is going through temporary maintenance.
POST /v1/{organization}/metric/evaluate HTTP/1.1
Host: api.amigo.ai
Authorization: Bearer JWT
Content-Type: application/json
Accept: */*
Content-Length: 84
{
"metric_ids": [
"text"
],
"conversation_id": "text",
"evaluate_to_interaction_id": "text"
}
{
"metrics": [
{
"metric_id": "text",
"name": "text",
"value": 1,
"references": [
"text"
],
"justification": "text"
}
]
}
Create a new conversation and start it. The user must not have any unfinished conversations that belong to the same service.
Permissions
This endpoint requires the following permissions:
Conversation.CreateConversation
for the new conversation.
This endpoint may be impacted by the following permissions:
CurrentAgentActionEvent
s are only emitted if the authenticated user has theConversation:GetInteractionInsights
permission.
The format of the response that will be sent to the user.
The format of the audio response, if response_format
is set to voice
.
A regex for filtering the type of the current agent action to return. By default, all are returned. If you don't want to receive any events, set this to a regex that matches nothing, for instance (?!)
.
^.+$
The Mongo cluster name to perform this request in. This is usually not needed unless the organization does not exist yet in the Amigo organization infra config database.
[]
The identifier of the service to create a conversation in.
^[a-f0-9]{24}$
The version set of the service to use. If not provided, the release
version set is used.
release
The initial user or agent inner thought message to send to the service. If not provided, the conversation will start with an agent message.
The type of the initial_message
. Can only be specified if initial_message
is provided.
Succeeded. The response will be a stream of events in JSON format separated by newlines. The server will transmit an event as soon as one is available, so the client should respond to the events as soon as one arrives, and keep listening until the server closes the connection.
Attempting to start a conversation when other unfinished conversations exist, or the preferred language does not support voice response, or the audio_format
field is not set when voice output is requested.
Invalid authorization credentials.
Missing required permissions.
Specified organization, service, or version set is not found.
A related operation is in progress.
Invalid request path parameter or request body failed validation.
The user has exceeded the rate limit of 5 requests per minute for this endpoint.
The service is going through temporary maintenance.
POST /v1/{organization}/conversation/ HTTP/1.1
Host: api.amigo.ai
Authorization: Bearer JWT
Content-Type: application/json
Accept: */*
Content-Length: 121
{
"service_id": "text",
"service_version_set_name": "release",
"initial_message": "text",
"initial_message_type": "user-message"
}
{
"type": "conversation-created",
"conversation_id": "text"
}
Interacting with a Conversation
After creating a conversation, you can send user messages to continue the interaction.
Sending Messages
curl --request POST \
--url 'https://api.amigo.ai/v1/<YOUR-ORG-ID>/conversation/<CONVERSATION-ID>/interact?request_format=text&response_format=text' \
--header 'Authorization: Bearer <AUTH-TOKEN-OF-USER>' \
--header 'Accept: application/x-ndjson' \
--header 'Content-Type: multipart/form-data' \
--form 'recorded_message=How can I reset my password?'
Request Format Requirements
Content-Type: Must use
multipart/form-data
formatForm Field: Include a single form field named
recorded_message
containing UTF-8 encoded textStreaming: The body can be sent as a stream to reduce latency, as processing begins as soon as chunks are received
Query Parameters:
request_format=text
(for text input)response_format=text
(for text output)current_agent_action_type
(optional, for filtering agent action events)
Response Stream
The response is a stream of NDJSON events:
// Example interaction response stream
{"type":"user-message-available","message_id":"67d872693eb91293ecfae8b8","user_message":"How can I reset my password?"}
{"type":"new-message","message":"","message_metadata":[],"transcript_alignment":null,"stop":false,"sequence_number":0,"message_id":"67d8726b3eb91293ecfae8bd"}
{"type":"new-message","message":"To reset your password, you can:","message_metadata":[],"transcript_alignment":null,"stop":false,"sequence_number":1,"message_id":"67d8726b3eb91293ecfae8bd"}
{"type":"new-message","message":"\n1. Visit the login page\n2. Click 'Forgot Password'\n3. Enter your email address","message_metadata":[],"transcript_alignment":null,"stop":false,"sequence_number":2,"message_id":"67d8726b3eb91293ecfae8bd"}
{"type":"interaction-complete"}
Interaction Response Events
UserMessageAvailableEvent
user-message-available
First event in response, includes the user message if sent as text
NewMessageEvent
new-message
Contains message chunks from the agent response
ErrorEvent
error
Indicates an internal error occurred. When this happens, none of the messages/artifacts in this interaction persist, and the entire API call is rolled back
InteractionCompleteEvent
interaction-complete
Signals the interaction is complete
EndSessionEvent
end-session
Optional event if conversation automatically ends
CurrentAgentActionEvent
current-agent-action
Only emitted if the current_agent_action_type
query parameter is set. Describes the action the agent's taking to produce the response
Send a new user message to the conversation. The endpoint will perform analysis and generate an agent message in response.
If request_format
is text
, the request body must follow multipart/form-data
with precise one form field called recorded_message
that corresponds to UTF-8 encoded
bytes of the text message. If request_format
is voice
, the entire request body must be the bytes of the voice recording in audio/wav
or audio/mpeg
(MP3) format. The body
can be sent as a stream, and the endpoint will start processing chunks as they're received, which will reduce latency.
A UserMessageAvailableEvent
will be the first event in the response, which includes the user message if it's sent as text, or the transcribed message if it's sent as voice.
A series of CurrentAgentActionEvent
s will follow, which indicates steps in the agent's thinking process. Then the agent message is generated sequentially in pieces, with each piece
being sent as a NewMessageEvent
in the response. After all the pieces are sent, an InteractionCompleteEvent
is sent. The response might end here, or, if the conversation automatically
ends (for instance, because the user message indicates the user wants to end the session), an EndSessionEvent
would be emitted, while the conversation is marked as finished and the post-conversation
analysis asynchronously initiated. The connection will then terminate.
Any further action on the conversation is only allowed after the connection is terminated.
A 200 status code doesn't indicate the successful completion of this endpoint, because the status code is transmitted before the stream starts. At any point during the stream,
an ErrorEvent
might be sent, which indicates that an error has occurred. The connection will be immediately closed after.
This endpoint can only be called on a conversation that has started but not finished.
Permissions
This endpoint requires the following permissions:
User:UpdateUserInfo
on the user who started the conversation.Conversation:InteractWithConversation
on the conversation.
This endpoint may be impacted by the following permissions:
CurrentAgentActionEvent
s are only emitted if the authenticated user has theConversation:GetInteractionInsights
permission.
The identifier of the conversation to send a message to.
^[a-f0-9]{24}$
The format in which the user message is delivered to the server.
The format of the response that will be sent to the user.
Configuration for the user message audio. This is only required if request_format
is set to voice
.
The format of the audio response, if response_format
is set to voice
.
A regex for filtering the type of the current agent action to return. By default, all are returned. If you don't want to receive any events, set this to a regex that matches nothing, for instance (?!)
.
^.+$
The Mongo cluster name to perform this request in. This is usually not needed unless the organization does not exist yet in the Amigo organization infra config database.
[]
Succeeded. The response will be a stream of events in JSON format separated by newlines. The server will transmit an event as soon as one is available, so the client should respond to the events as soon as one arrives, and keep listening until the server closes the connection.
The user message is empty, or the preferred language does not support voice transcription or response, or the audio_format
field is not set when voice output is requested.
Invalid authorization credentials.
Missing required permissions.
Specified organization or conversation is not found.
The specified conversation is already finished, or a related operation is in process.
The format of the supplied audio file is not supported.
Invalid request path parameter failed validation.
The user has exceeded the rate limit of 15 requests per minute for this endpoint.
The service is going through temporary maintenance.
POST /v1/{organization}/conversation/{conversation_id}/interact HTTP/1.1
Host: api.amigo.ai
Authorization: Bearer JWT
Accept: */*
{
"type": "interaction-complete",
"message_id": "text",
"interaction_id": "text",
"full_message": "text",
"conversation_completed": true
}
Processing the NDJSON Stream
/conversation/{conversation_id}/interact
returns an NDJSON stream (one JSON object per line, newline-separated).
The server keeps the underlying HTTP/2 connection open after the last event of a turn so that you can immediately begin the next turn without paying the cost of a new TLS/TCP handshake.
Important Stream Handling Rules
Do not wait for the socket to close — instead, decide in your client logic when you are done processing a turn. The final event of every successful turn is always:
interaction-complete
– the agent finished speaking/thinking for this user turn
If the conversation is over (for example the agent decides to end-session) you will also receive:
end-session
Once you have seen one of those events you can safely stop listening, cancel the stream, or start the next request on the same connection.
TypeScript/JavaScript Implementation
Here's a complete Node.js example showing the correct pattern:
// For Node 18+: run with --experimental-fetch OR upgrade to Node 20
import fetch from "node-fetch"; // npm i node-fetch@3 (ES modules only)
import FormData from "form-data"; // npm i form-data
interface InteractionConfig {
orgId: string;
conversationId: string;
authToken: string;
enableDebugLogging?: boolean;
abortController?: AbortController;
}
interface StreamHandlers {
onNewMessage?: (event: NewMessageEvent) => void;
onInteractionComplete?: (event: InteractionCompleteEvent) => void;
onConversationCreated?: (event: ConversationCreatedEvent) => void;
onError?: (event: ErrorEvent) => void;
onEndSession?: () => void;
onUserMessageAvailable?: (event: any) => void;
}
/**
* Production-ready NDJSON stream parser with error handling
* Based on real frontend implementation
*/
class StreamProcessor {
private decoder = new TextDecoder('utf-8');
private abortController?: AbortController;
constructor(abortController?: AbortController) {
this.abortController = abortController;
}
async processStream(
stream: ReadableStream,
handlers: StreamHandlers,
enableDebugLogging: boolean = false
): Promise<void> {
const reader = stream.getReader();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (this.abortController?.signal.aborted) {
throw new Error('Stream aborted');
}
buffer += this.decoder.decode(value, { stream: true });
// Process complete lines
let newlineIndex;
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
const line = buffer.slice(0, newlineIndex).trim();
buffer = buffer.slice(newlineIndex + 1);
if (!line) continue;
try {
const event = JSON.parse(line) as StreamEvent;
if (enableDebugLogging) {
console.debug(`${event.type} event received`, event);
}
this.handleEvent(event, handlers);
} catch (parseError) {
console.error('Failed to parse stream event:', parseError, { line });
handlers.onError?.({
type: 'error',
message: 'Failed to parse stream event',
code: 422
} as ErrorEvent);
}
}
}
// Process any remaining buffer content
if (buffer.trim()) {
try {
const event = JSON.parse(buffer) as StreamEvent;
this.handleEvent(event, handlers);
} catch (parseError) {
console.error('Failed to parse final stream event:', parseError);
}
}
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
console.warn('Stream request aborted');
} else {
console.error('Stream processing error:', error);
handlers.onError?.({
type: 'error',
message: error instanceof Error ? error.message : 'Stream processing error'
} as ErrorEvent);
}
} finally {
reader.releaseLock();
}
}
private handleEvent(event: StreamEvent, handlers: StreamHandlers): void {
switch (event.type) {
case 'new-message':
handlers.onNewMessage?.(event as NewMessageEvent);
break;
case 'interaction-complete':
handlers.onInteractionComplete?.(event as InteractionCompleteEvent);
break;
case 'conversation-created':
handlers.onConversationCreated?.(event as ConversationCreatedEvent);
break;
case 'error':
handlers.onError?.(event as ErrorEvent);
break;
case 'end-session':
handlers.onEndSession?.();
break;
case 'user-message-available':
handlers.onUserMessageAvailable?.(event);
break;
default:
console.debug('Unhandled event type:', event.type, event);
}
}
}
// Enhanced message sending with production patterns
async sendMessage(
config: InteractionConfig,
text: string,
handlers: StreamHandlers = {}
): Promise<Result<void, ApiError>> {
await this.beforeRequest();
const { orgId, conversationId, enableDebugLogging = false, abortController } = config;
try {
const form = new FormData();
form.append("recorded_message", text);
const url = `https://api.amigo.ai/v1/${orgId}/conversation/${conversationId}/interact` +
"?request_format=text&response_format=text";
const response = await fetch(url, {
method: "POST",
headers: {
...(await this.getAuthHeaders()),
Accept: "application/x-ndjson",
},
body: form,
signal: abortController?.signal,
});
if (!response.ok) {
const errorText = await response.text();
return {
success: false,
error: {
type: 'InvalidResponseError',
message: `HTTP ${response.status}: ${errorText}`,
detail: { status: response.status, body: errorText }
}
};
}
if (!response.body) {
return {
success: false,
error: {
type: 'InvalidResponseError',
message: 'No response body available'
}
};
}
// Enhanced stream processing with proper error handling
const processor = new StreamProcessor(abortController);
let fullMessage = "";
let isComplete = false;
const enhancedHandlers: StreamHandlers = {
onNewMessage: (event) => {
fullMessage += event.message;
handlers.onNewMessage?.(event);
},
onInteractionComplete: (event) => {
isComplete = true;
if (enableDebugLogging) {
console.log('Full response:', fullMessage);
}
handlers.onInteractionComplete?.(event);
},
onError: (event) => {
handlers.onError?.(event);
},
onEndSession: () => {
isComplete = true;
handlers.onEndSession?.();
},
...handlers
};
await processor.processStream(response.body, enhancedHandlers, enableDebugLogging);
if (!isComplete) {
return {
success: false,
error: {
type: 'UnknownError',
message: 'Stream ended without completion event'
}
};
}
return { success: true, data: undefined };
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
return {
success: false,
error: {
type: 'NetworkError',
message: 'Request was aborted'
}
};
}
return {
success: false,
error: {
type: 'NetworkError',
message: error instanceof Error ? error.message : 'Unknown network error',
detail: error
}
};
}
}
}
}
// Example usage with production patterns
const conversationService = new ConversationService('your-jwt-token');
// Create conversation with proper error handling
const createResult = await conversationService.createConversation({
orgId: 'your-org-id',
serviceId: 'your-service-id',
authToken: 'your-jwt-token'
});
if (!createResult.success) {
console.error('Failed to create conversation:', createResult.error);
return;
}
const conversationId = createResult.data;
// Send message with handlers
const sendResult = await conversationService.sendMessage(
{
orgId: 'your-org-id',
conversationId,
authToken: 'your-jwt-token',
enableDebugLogging: true
},
'Hello there!',
{
onNewMessage: (event) => console.log('New message chunk:', event.message),
onInteractionComplete: () => console.log('Interaction finished'),
onError: (event) => console.error('Stream error:', event.message)
}
);
if (!sendResult.success) {
console.error('Failed to send message:', sendResult.error);
}
Adding Timeout Protection
Production-ready timeout handling with proper cleanup:
class TimeoutManager {
private timer?: NodeJS.Timeout;
private abortController = new AbortController();
constructor(private timeoutMs: number = 15_000) {}
start(): AbortController {
this.timer = setTimeout(() => {
this.abortController.abort();
}, this.timeoutMs);
return this.abortController;
}
clear(): void {
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
}
}
isAborted(): boolean {
return this.abortController.signal.aborted;
}
}
// Usage in conversation service
const sendMessageWithTimeout = async (
config: InteractionConfig,
text: string,
timeoutMs: number = 15_000
): Promise<Result<void, ApiError>> => {
const timeoutManager = new TimeoutManager(timeoutMs);
const abortController = timeoutManager.start();
try {
const result = await conversationService.sendMessage(
{ ...config, abortController },
text,
{
onNewMessage: (event) => console.log('Chunk:', event.message),
onInteractionComplete: () => timeoutManager.clear(),
onError: (event) => {
timeoutManager.clear();
console.error('Stream error:', event.message);
}
}
);
timeoutManager.clear();
return result;
} catch (error) {
timeoutManager.clear();
if (error instanceof Error && error.name === 'AbortError') {
return {
success: false,
error: {
type: 'NetworkError',
message: 'Request timed out after ' + timeoutMs + 'ms'
}
};
}
return {
success: false,
error: {
type: 'NetworkError',
message: error instanceof Error ? error.message : 'Unknown error',
detail: error
}
};
}
};
Best Practices
Always set
Accept: application/x-ndjson
when you expect a text response stream.\If you set
response_format=voice
, useAccept: audio/mpeg
(MP3) orAccept: audio/wav
accordingly.
The connection staying open is not a timeout. It is normal HTTP/2 behaviour. Abort the request yourself (e.g. via
AbortController
) if you want a hard upper bound.Reuse connections: If you plan to send the next user message immediately, you can keep the connection open and reuse it – simply start another
/interact
request.Handle errors gracefully: Always check for
error
events in the stream and handle them appropriately.Store message IDs: Keep track of message IDs for debugging and conversation history management.
Voice Interactions
Amigo supports voice notes: you send audio, the agent responds with synthesized audio.
This is not a full duplex "voice-to-voice" phone call. Treat each /interact
turn as an async voice note exchange:
Encode the user's recording as a WAV or FLAC fragment
POST it as the
recorded_message
form field withrequest_format=voice
Set
response_format=voice
and choose anAccept
header:audio/mpeg
→ MP3, most efficient for mobile playbackaudio/wav
→ uncompressed PCM, lowest latency for small clips
Parse the NDJSON events exactly as for text—
new-message
contains base64 audio chunks—and play them back in order
Voice Response Processing Example
if (evt.type === "new-message" && typeof evt.message === "string" && evt.message) {
const blob = Uint8Array.from(atob(evt.message), c => c.charCodeAt(0));
playAudio(blob.buffer); // your audio player implementation
}
Because each request/response represents one voice note, you can easily build push-to-talk or walkie-talkie style UX while re-using the same streaming infrastructure described above.
Quick Reference Checklist
Before You Send a Request
Required Headers: Have you set all three required headers?\
Authorization: Bearer <JWT>
\Accept: application/x-ndjson
(oraudio/*
)\Content-Type
handled automatically byFormData
, do not hard-codemultipart/form-data; boundary=...
yourself
Query String: Is the query-string correct?
request_format=text|voice
andresponse_format=text|voice
must match the formats you actually send/expectStream Handling: Are you waiting for
interaction-complete
, not for the socket to close? An open HTTP/2 connection ≠ timeoutConversation Limits: Did you remember that one user = one active conversation per service? Handle the "already started" error by resuming or finishing the existing conversation
Common Issues and Solutions
Error Events
{"type": "error", "code": 422, "message": "Request format mismatch"}
Solution: Log the full event, fix the request, retry the entire turn—the server has already rolled it back.
HTTP 415 / 406 Errors
Cause: Your Accept
or Content-Type
header is incorrect
Solution: Verify headers match your request/response formats
Silent Stream
Cause: Not parsing per-line; first bytes might be small Solution: Buffer until you hit newline character
Never Do This
Never assume the transport will close on success\
Never create a new conversation when one is still active—finish or resume instead
Common Integration Patterns
1. Collecting Full Response
If you only need the agent's complete response:
let fullMessage = "";
for await (const evt of parseNdjsonStream(resp.body!)) {
if (evt.type === "new-message") {
fullMessage += evt.message;
}
if (evt.type === "interaction-complete") {
console.log("Complete response:", fullMessage);
break;
}
}
2. Real-time Streaming
For live updates as the agent responds:
let current = "";
for await (const evt of parseNdjsonStream(resp.body!)) {
if (evt.type === "new-message") {
current += evt.message;
updateUI(current); // Update your interface
}
if (evt.type === "interaction-complete") break;
}
3. Error Handling
Always handle potential errors in the stream:
for await (const evt of parseNdjsonStream(resp.body!)) {
switch (evt.type) {
case "new-message":
// Handle message chunk
break;
case "interaction-complete":
// Handle completion
break;
case "error":
console.error("Stream error:", evt.message);
break;
}
}
Conversation Lifecycle Management
Understanding Conversation States
Started: Conversation is active and can receive interactions
Finished: Conversation has ended and cannot receive further interactions
Automatic vs. Manual Finish
A conversation can finish in two ways:
Automatically: When the service determines the conversation should finish (based on user intent, service completion, etc.)
Manually: When explicitly ended via the finish conversation API (e.g. due to timeout or user action)
Finishing a Conversation
To manually finish a conversation:
curl --request POST \
--url 'https://api.amigo.ai/v1/<YOUR-ORG-ID>/conversation/<CONVERSATION-ID>/finish/' \
--header 'Authorization: Bearer <AUTH-TOKEN-OF-USER>' \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '{}'
Important: This endpoint should only be called:
On started, non-finished conversations
After previous conversation API calls have completed
When the conversation hasn't automatically finished during interaction
When a conversation finishes, post-conversation analysis is initiated asynchronously (memory generation, user model updates, etc.).
Conclude a conversation and asynchronously initiate post-conversation analysis.
This endpoint should only be called on a started, non-finished conversation. It can only be called when the previous Start a conversation
and
Interact with a conversation
calls have finished.
If the conversation has no messages, the conversation is deleted.
It's possible for some conversations to automatically finish during an Interact with a conversation
call (for instance, if the user explicitly sends a message
indicating that they're done with the conversation). In that case, this endpoint shouldn't be called, as the Interact with a conversation
endpoint automatically
wraps up the conversation.
Permissions
This endpoint requires the following permissions:
User:UpdateUserInfo
on the user who started the conversation.Conversation:InteractWithConversation
on the conversation.
The identifier of the conversation to finish.
^[a-f0-9]{24}$
The Mongo cluster name to perform this request in. This is usually not needed unless the organization does not exist yet in the Amigo organization infra config database.
[]
Succeeded.
The conversation has no interactions.
Invalid authorization credentials.
Missing required permissions.
Specified organization or conversation is not found.
The specified conversation is already finished, or a related operation is in process.
Invalid request path parameter failed validation.
The user has exceeded the rate limit of 5 requests per minute for this endpoint.
The service is going through temporary maintenance.
POST /v1/{organization}/conversation/{conversation_id}/finish/ HTTP/1.1
Host: api.amigo.ai
Authorization: Bearer JWT
Accept: */*
No content
Managing "Dangling" Conversations
Conversations never time out automatically. You have several options for handling inactive conversations:
Timeout Approach: Track user's last interaction time and call the finish endpoint after a defined period
Caution: This may terminate conversations users intend to continue
Resume Option (Recommended): When a user returns, offer options to:
Resume the existing conversation
Start a new conversation (which requires ending the current one)
Error Handling: If your application attempts to create a new conversation while one is active:
Catch the error response
Prompt the user to either resume or end the existing conversation
The Amigo system prevents users from having multiple ongoing conversations of the same service type, ensuring conversation integrity.
Was this helpful?