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.
Conversation Lifecycle Overview
Creation: Initialize a new conversation with a service
Interaction: Exchange messages between user and agent
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
To initiate a new conversation with a user:
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>"
}'
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 = "<USER JWT>";
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");
}
The response takes the form of a body stream (MDN) containing New line Delimited JSON (ndjson):
// example response stream
{"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"}
It will include several event types as denoted by the type
field:
ConversationCreatedEvent
conversation-created
Provides the conversation_id
and confirms successful creation
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 emit_current_agent_action_event
query parameter is set to True
. Describes the action the agent's taking to produce the response.
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.
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",
"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}$
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"
}
]
}
Important Conversation Management Rules
A user cannot have multiple active conversations for the same service
The
conversation_id
must be stored and used for all subsequent interactionsThe conversation object should be persisted for at least the duration of the active session
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.
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
POST /v1/{organization}/conversation/ HTTP/1.1
Host: api.amigo.ai
Authorization: Bearer JWT
Content-Type: application/json
Accept: */*
Content-Length: 58
{
"service_id": "text",
"service_version_set_name": "release"
}
{
"type": "conversation-created",
"conversation_id": "text"
}
Interacting with a Conversation
After creating a conversation, you can send user 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=<UTF-8 ENCODED USER MESSAGE>'
Request Format
The request body must use
multipart/form-data
formatInclude a single form field named
recorded_message
containing UTF-8 encoded textThe body can be sent as a stream to reduce latency, as processing begins as soon as chunks are received
The response takes the form of a body stream:
// Example response stream
{"type":"user-message-available","message_id":"67d872693eb91293ecfae8b8","user_message":"I'm good"}
{"type":"new-message","message":"","message_metadata":[],"transcript_alignment":null,"stop":false,"sequence_number":0,"message_id":"67d8726b3eb91293ecfae8bd"}
{"type":"new-message","message":"Great","message_metadata":[],"transcript_alignment":null,"stop":false,"sequence_number":1,"message_id":"67d8726b3eb91293ecfae8bd"}
Response Stream Events
UserMessageAvailableEvent
user-message-available
First event in response, includes the user message if sent as text.
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 emit_current_agent_action_event
query parameter is set to True
. 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.
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.
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"
}
How to Consume the application/x‑ndjson
Stream
application/x‑ndjson
Stream/conversation/{conversation_id}/interact
returns an ND‑JSON stream (one JSON object per line, ‑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. 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.
Below is a minimal Node 18+ example (TypeScript/JavaScript) that shows the correct pattern. The helper parseNdjsonStream()
is reproduced in full below so you can copy‑paste without pulling in any external dependencies.
// Node 18+: run with --experimental-fetch OR upgrade to Node 20 (global fetch & FormData).
// If your runtime already provides these globals you can delete the two imports.
import fetch from "node-fetch"; // npm i node-fetch@3 (ES modules only)
import FormData from "form-data"; // npm i form-data
const ORG_ID = "<YOUR‑ORG‑ID>";
const CONVERSATION = "<CONVERSATION-ID>";
const AUTH_TOKEN = "<USER JWT>";
/**
* Async generator that yields parsed JSON objects from an ND‑JSON stream.
*/
async function* parseNdjsonStream(stream: NodeJS.ReadableStream) {
const decoder = new TextDecoder();
let buffer = "";
for await (const chunk of stream as any) {
buffer += decoder.decode(chunk, { stream: true });
let nl;
while ((nl = buffer.indexOf("\n")) !== -1) {
const line = buffer.slice(0, nl).trim();
buffer = buffer.slice(nl + 1);
if (!line) continue;
yield JSON.parse(line);
}
}
if (buffer.trim()) yield JSON.parse(buffer);
}
export async function sendMessage(text: string) {
const form = new FormData();
form.append("recorded_message", text);
const url = `https://api.amigo.ai/v1/${ORG_ID}/conversation/${CONVERSATION}/interact` +
"?request_format=text&response_format=text&emit_current_agent_action_event=true";
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${AUTH_TOKEN}`,
Accept: "application/x-ndjson",
...form.getHeaders(),
},
body: form,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
for await (const evt of parseNdjsonStream(response.body!)) {
console.log(`[event] ${evt.type}`, evt);
switch (evt.type) {
case "interaction-complete":
case "end-session":
response.body!.cancel();
return;
case "error":
console.error("Server error", evt);
response.body!.cancel();
return;
}
}
}
// Run `node basic_interaction.ts` directly
if (import.meta.url === `file://${process.argv[1]}`) {
await sendMessage("Hello there, June!");
}
Adding a Hard Timeout (Optional)
If you want to guarantee that your UI never waits more than N seconds for a turn to complete, wrap the fetch in an AbortController
:
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 15_000); // 15‑second guard‑rail
const resp = await fetch(url, {
method: "POST",
body: form,
headers: { /* … */ },
signal: ctrl.signal,
});
clearTimeout(timer);
If the controller aborts, fetch()
rejects with DOMException: AbortError
; show an appropriate error to the user and offer a retry.
Tips & Gotchas
Always set
Accept: application/x-ndjson
when you expect a text response stream. • If you setresponse_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.If you plan to send the next user message immediately, you can keep the connection open and reuse it – simply start another
/interact
request.
Voice‑Note (≠ Voice‑to‑Voice) Interactions
Amigo currently supports voice notes: you send audio, the agent responds with synthesised audio.
It 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 play‑back. •audio/wav
→ uncompressed PCM, lowest latency for small clips.Parse the ND‑JSON events exactly as for text—
new-message
contains base‑64 audio chunks—and play them back in order.
Example (browser / React Native):
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
}
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‑Start Checklist (Keep This Handy!)
🟢 Before You Hit Send
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.Is the query‑string correct?
request_format=text|voice
andresponse_format=text|voice
must match the formats you actually send/expect.Are you waiting for
interaction-complete
, not for the socket to close? An open HTTP/2 connection ≠ timeout.Did you remember that one user = one active conversation per service? Handle the "already started" error by resuming or finishing the existing conversation.
🟡 Troubleshooting
• error
event received
{"type": "error", "code": 422, "message": "Request format mismatch"}
Log the full event. 2. Fix the request. 3. Retry the entire turn—the server has already rolled it back.
• HTTP 415 / 406 → Your Accept
or Content‑Type
is wrong.
• Stream seems silent → Are you parsing per‑line? The first bytes might be small; buffer until you hit .
🛑 Never
• 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 (Straight from amigo‑examples)
1. Fire‑and‑forget Notification
You only care about the agent's final reply; intermediate new-message
chunks are ignored.
for await (const evt of parseNdjsonStream(resp.body!)) {
if (evt.type === "interaction-complete") {
console.log("assistant says:", evt.final_message);
break;
}
}
2. Real‑time "Type‑as‑you‑stream" UI
let current = "";
for await (const evt of parseNdjsonStream(resp.body!)) {
if (evt.type === "new-message") {
current += evt.message;
redraw(current); // e.g. setState in React
}
if (evt.type === "interaction-complete") break;
}
3. Server‑Side Relay (Backend collects, then pushes to client)
app.post("/amigo-proxy", async (req, res) => {
const amigoResp = await fetch(/* … */);
// Pipe raw ND‑JSON straight through:
res.setHeader("Content-Type", "application/x-ndjson");
amigoResp.body!.pipe(res);
});
Useful when the browser environment cannot (yet) consume streaming fetches—your backend remains the only component that needs the Amigo secret.
4. Finishing Idle Conversations Automatically
const INACTIVITY_MS = 30 * 60_000; // 30 min
setInterval(async () => {
for (const convo of getActiveConversations()) {
if (Date.now() - convo.lastUserActivity > INACTIVITY_MS) {
await finishConversation(convo.id);
}
}
}, 5 * 60_000);
Keeps your database clean and avoids users hitting the "already in a conversation" error weeks later.
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.
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.
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.
Last updated
Was this helpful?