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

  1. Creation: Initialize a new conversation instance based on a service

  2. Interaction: Full exchanges between user and agent (user message + agent response)

  3. Monitoring: Track agent actions and dynamic behaviors

  4. Integration: Connect with external systems based on conversation events

  5. 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:

Event Type
Type Field Value
Description

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 the conversation-created event is required for all subsequent interactions

  • Handle errors gracefully: error events indicate the entire operation has been rolled back

  • Wait 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>
}

Emitted when certain predefined actions are taken during the state machine navigation. The action field will be any of the following three strings:

  • state-transition

  • active-memory-retrieval

  • reflection-generation

You can use the below endpoint to retrieve dynamic behavior set versions:

Get dynamic behavior set versions

get

Get the versions of a dynamic behavior set.

Permissions

This endpoint requires the following permissions:

  • DynamicBehaviorInstruction:GetDynamicBehaviorInstruction for the dynamic behavior set to retrieve.
Authorizations
Path parameters
organizationstringRequired
dynamic_behavior_set_idstringRequired

The ID of the dynamic behavior set.

Pattern: ^[a-f0-9]{24}$
Query parameters
versionany ofOptional

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.

Example: 1
stringOptional
or
nullOptional
limitinteger · min: 1 · max: 10Optional

The maximum number of dynamic behavior set versions to return.

Default: 10
continuation_tokenintegerOptional

The continuation token from the previous request used to retrieve the next page of dynamic behavior set versions.

Default: 0
sort_bystring[]Optional

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.

Default: []
Header parameters
x-mongo-cluster-nameany ofOptional

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.

stringOptional
or
nullOptional
Sec-WebSocket-Protocolstring[]OptionalDefault: []
Responses
200

Succeeded.

application/json
get
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 metrics

post

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.
Authorizations
Path parameters
organizationstringRequired
Header parameters
x-mongo-cluster-nameany ofOptional

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.

stringOptional
or
nullOptional
Sec-WebSocket-Protocolstring[]OptionalDefault: []
Body
metric_idsstring[] · min: 1 · max: 10Required

The IDs of the metrics to evaluate.

conversation_idstringRequired

The ID of the conversation to evaluate the metrics for.

Pattern: ^[a-f0-9]{24}$
evaluate_to_interaction_idany ofOptional

If specified, only messages up to (and including) this interaction will be evaluated.

stringOptionalPattern: ^[a-f0-9]{24}$
or
nullOptional
Responses
200

Succeeded.

application/json
post
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 conversation

post

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:

  • CurrentAgentActionEvents are only emitted if the authenticated user has the Conversation:GetInteractionInsights permission.
Authorizations
Path parameters
organizationstringRequired
Query parameters
response_formatstring · enumRequired

The format of the response that will be sent to the user.

Possible values:
audio_formatany ofOptional

The format of the audio response, if response_format is set to voice.

string · enumOptionalPossible values:
or
nullOptional
current_agent_action_typestringOptional

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 (?!).

Default: ^.+$
Header parameters
x-mongo-cluster-nameany ofOptional

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.

stringOptional
or
nullOptional
Sec-WebSocket-Protocolstring[]OptionalDefault: []
Body
service_idstringRequired

The identifier of the service to create a conversation in.

Pattern: ^[a-f0-9]{24}$
service_version_set_namestringOptional

The version set of the service to use. If not provided, the release version set is used.

Default: release
initial_messageany ofOptional

The initial user or agent inner thought message to send to the service. If not provided, the conversation will start with an agent message.

string · min: 1Optional
or
nullOptional
initial_message_typeany ofOptional

The type of the initial_message. Can only be specified if initial_message is provided.

string · enumOptionalPossible values:
or
nullOptional
Responses
201

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.

application/x-ndjson
Responseone of
or
or
or
or
post
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 format

  • Form Field: Include a single form field named recorded_message containing UTF-8 encoded text

  • Streaming: 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

Event Type
Type Field Value
Description

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

Interact with a conversation

post

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 CurrentAgentActionEvents 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:

  • CurrentAgentActionEvents are only emitted if the authenticated user has the Conversation:GetInteractionInsights permission.
Authorizations
Path parameters
conversation_idstringRequired

The identifier of the conversation to send a message to.

Pattern: ^[a-f0-9]{24}$
organizationstringRequired
Query parameters
request_formatstring · enumRequired

The format in which the user message is delivered to the server.

Possible values:
response_formatstring · enumRequired

The format of the response that will be sent to the user.

Possible values:
request_audio_configany ofOptional

Configuration for the user message audio. This is only required if request_format is set to voice.

one ofOptional
or
or
nullOptional
audio_formatany ofOptional

The format of the audio response, if response_format is set to voice.

string · enumOptionalPossible values:
or
nullOptional
current_agent_action_typestringOptional

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 (?!).

Default: ^.+$
Header parameters
x-mongo-cluster-nameany ofOptional

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.

stringOptional
or
nullOptional
Sec-WebSocket-Protocolstring[]OptionalDefault: []
Responses
200

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.

application/x-ndjson
Responseany of
or
or
or
or
or
post
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

  1. Always set Accept: application/x-ndjson when you expect a text response stream.\

    • If you set response_format=voice, use Accept: audio/mpeg (MP3) or Accept: audio/wav accordingly.

  2. 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.

  3. 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.

  4. Handle errors gracefully: Always check for error events in the stream and handle them appropriately.

  5. 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:

  1. Encode the user's recording as a WAV or FLAC fragment

  2. POST it as the recorded_message form field with request_format=voice

  3. Set response_format=voice and choose an Accept header:

    • audio/mpeg → MP3, most efficient for mobile playback

    • audio/wav → uncompressed PCM, lowest latency for small clips

  4. 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

  1. Required Headers: Have you set all three required headers?\

    • Authorization: Bearer <JWT>\

    • Accept: application/x-ndjson (or audio/*)\

    • Content-Type handled automatically by FormData, do not hard-code multipart/form-data; boundary=... yourself

  2. Query String: Is the query-string correct? request_format=text|voice and response_format=text|voice must match the formats you actually send/expect

  3. Stream Handling: Are you waiting for interaction-complete, not for the socket to close? An open HTTP/2 connection ≠ timeout

  4. Conversation 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:

  1. Automatically: When the service determines the conversation should finish (based on user intent, service completion, etc.)

  2. 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.).

Finish a conversation

post

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.
Authorizations
Path parameters
conversation_idstringRequired

The identifier of the conversation to finish.

Pattern: ^[a-f0-9]{24}$
organizationstringRequired
Header parameters
x-mongo-cluster-nameany ofOptional

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.

stringOptional
or
nullOptional
Sec-WebSocket-Protocolstring[]OptionalDefault: []
Responses
204

Succeeded.

post
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:

  1. 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

  2. Resume Option (Recommended): When a user returns, offer options to:

    • Resume the existing conversation

    • Start a new conversation (which requires ending the current one)

  3. 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?