Chat operations

Written By Stanislas

Last updated 16 days ago


Send messages to your AI agent, manage responses, and control the conversation flow. Learn how to send messages with streaming, regenerate responses, stop generation, and use text-to-speech features.

Overview

Chat operations are the core interactions in a conversation. You can send messages to trigger AI responses, listen to streaming replies in real-time, regenerate unsatisfactory responses, stop long-running generations, and convert responses to audio. This guide covers all the mutations and queries you need for a complete chat experience.

Each message sent can optionally trigger an AI response. Responses stream back character by character, giving users immediate feedback. You can also use synchronous requests if you need the complete response before proceeding.


Prerequisites

Before performing chat operations, ensure you have:

  1. Authenticated with the API β€” Follow the Authentication & Setup guide to get your accessToken and workspaceId

  2. Created or retrieved a session β€” Follow the Managing chat sessions guide to get a sessionId

  3. Configured your GraphQL client β€” Your Apollo Client must include the required authentication headers

  4. Set up subscriptions (optional) β€” For real-time streaming, follow the Real-time subscriptions guide


Getting started

Here's the minimal setup to send a message and receive a response:

Step 1: Send a message

Send a message to the bot and trigger a streaming response.

import { gql } from '@apollo/client';

const SEND_MESSAGE = gql`
  mutation SendNewMessage($newMessageData: NewMessageInput!) {
    sendNewMessage(newMessageData: $newMessageData) {
      id
      message
      createdAt
      sessionId
      sentBy {
        id
        firstName
      }
      isBotReply
    }
  }
`;

const sendMessage = async (client, sessionId, messageText) => {
  const { data } = await client.mutate({
    mutation: SEND_MESSAGE,
    variables: {
      newMessageData: {
        message: messageText,
        sessionId: sessionId,
        isForAiReply: true, // Trigger AI response
      },
    },
  });

  return data.sendNewMessage;
};

// Usage
const userMessage = await sendMessage(client, 67890, 'What is your pricing?');
console.log('Message sent:', userMessage.id);

Step 2: Subscribe to streaming responses

Listen to the AI response as it streams in real-time.

const MESSAGE_STREAM = gql`
  subscription OnMessageStream($sessionId: Float!) {
    onMessageStream(sessionId: $sessionId) {
      messageChunk
      botResponseMessageId
      isStoppable
    }
  }
`;

const subscribeToStream = (client, sessionId, onChunk) => {
  let fullMessage = '';

  return client
    .subscribe({
      query: MESSAGE_STREAM,
      variables: { sessionId },
    })
    .subscribe({
      next: ({ data }) => {
        fullMessage += data.onMessageStream.messageChunk;
        onChunk(fullMessage, data.onMessageStream);
      },
      error: (error) => {
        console.error('Stream error:', error);
      },
      complete: () => {
        console.log('Stream complete');
      },
    });
};

// Usage
const subscription = subscribeToStream(client, 67890, (fullMessage, data) => {
  console.log('Streaming:', fullMessage);
  if (data.isStoppable) {
    console.log('User can stop generation');
  }
});

// Cleanup when done
// subscription.unsubscribe();

Step 3: Stop generation if needed

If the response is taking too long, stop the generation.

const STOP_AI_RESPONSE = gql`
  mutation StopAIResponse($sessionId: Float!, $botId: Float!) {
    stopAIResponse(sessionId: $sessionId, botId: $botId)
  }
`;

const stopGeneration = async (client, sessionId, botId) => {
  const { data } = await client.mutate({
    mutation: STOP_AI_RESPONSE,
    variables: { sessionId, botId },
  });

  return data.stopAIResponse;
};

// Usage
const stopped = await stopGeneration(client, 67890, 123);
console.log('Generation stopped:', stopped);

Sending messages

Send a new message

Send a message to the session and optionally trigger an AI response.

GraphQL Mutation:

mutation SendNewMessage($newMessageData: NewMessageInput!) {
  sendNewMessage(newMessageData: $newMessageData) {
    id
    message
    createdAt
    sessionId
    sentBy {
      id
      firstName
      lastName
    }
    isBotReply
  }
}

Input parameters:

Parameter

Type

Required

Description

message

string

No

Message text to send

sessionId

number

Yes

Session ID (where to send the message)

isForAiReply

boolean

No

If true, triggers AI response

fileIds

number[]

No

IDs of files to attach

mentionedBotId

number

No

Specific bot to mention/invoke

parentMessageId

number

No

Parent message ID (for threading)

botPromptId

number

No

Predefined prompt to use

command

string

No

Special command for the agent

extraConfig

object

No

Additional configuration

Returns: The created message object

Basic example:

const SEND_MESSAGE = gql`
  mutation SendNewMessage($newMessageData: NewMessageInput!) {
    sendNewMessage(newMessageData: $newMessageData) {
      id
      message
      createdAt
      isBotReply
    }
  }
`;

// Simple message without AI response
const message1 = await client.mutate({
  mutation: SEND_MESSAGE,
  variables: {
    newMessageData: {
      message: 'Just saving this note',
      sessionId: 67890,
    },
  },
});

// Message that triggers AI response
const message2 = await client.mutate({
  mutation: SEND_MESSAGE,
  variables: {
    newMessageData: {
      message: 'What can you do for me?',
      sessionId: 67890,
      isForAiReply: true,
    },
  },
});

With file attachments:

const sendMessageWithFiles = async (client, sessionId, messageText, fileIds) => {
  const { data } = await client.mutate({
    mutation: SEND_MESSAGE,
    variables: {
      newMessageData: {
        message: messageText,
        sessionId,
        fileIds, // Array of file IDs
        isForAiReply: true,
      },
    },
  });

  return data.sendNewMessage;
};

// Usage
const message = await sendMessageWithFiles(
  client,
  67890,
  'Please analyze these documents',
  [101, 102, 103]
);

With threading (reply to specific message):

const sendReplyToMessage = async (client, sessionId, messageText, parentMessageId) => {
  const { data } = await client.mutate({
    mutation: SEND_MESSAGE,
    variables: {
      newMessageData: {
        message: messageText,
        sessionId,
        parentMessageId, // Reply to this message
        isForAiReply: true,
      },
    },
  });

  return data.sendNewMessage;
};

// Usage
const reply = await sendReplyToMessage(
  client,
  67890,
  'Can you elaborate on that?',
  12345 // ID of the message to reply to
);

Using a predefined prompt:

const sendWithPrompt = async (client, sessionId, botPromptId) => {
  const { data } = await client.mutate({
    mutation: SEND_MESSAGE,
    variables: {
      newMessageData: {
        sessionId,
        botPromptId, // Use a predefined prompt instead of typing
        isForAiReply: true,
      },
    },
  });

  return data.sendNewMessage;
};

// Usage - useful for quick actions like "Summarize" or "Translate"
const summary = await sendWithPrompt(client, 67890, 456);

Synchronous requests

Send message and wait for complete response

For simple Q&A without streaming, use the synchronous endpoint to get the complete response immediately.

GraphQL Mutation:

mutation SyncBotRequest($newMessageData: NewMessageInput!) {
  syncBotRequest(newMessageData: $newMessageData) {
    text
    botId
    botSlug
    isBotError
    sessionId
    files
    sourceMetadatas
    sources
    responseJson
    jobId
  }
}

Input parameters: Same as SendNewMessage

Returns: Complete response with metadata

Example:

const SYNC_BOT_REQUEST = gql`
  mutation SyncBotRequest($newMessageData: NewMessageInput!) {
    syncBotRequest(newMessageData: $newMessageData) {
      text
      isBotError
      sources
      botSlug
    }
  }
`;

const syncMessage = async (client, sessionId, messageText) => {
  const { data } = await client.mutate({
    mutation: SYNC_BOT_REQUEST,
    variables: {
      newMessageData: {
        message: messageText,
        sessionId,
      },
    },
  });

  return data.syncBotRequest;
};

// Usage
const response = await syncMessage(client, 67890, 'What is 2 + 2?');
console.log('Bot response:', response.text);
console.log('Sources used:', response.sources);

if (response.isBotError) {
  console.error('Bot encountered an error');
}

When to use synchronous requests:

  • Simple Q&A where you need the complete answer before proceeding

  • Batch processing multiple messages

  • When you don't need real-time streaming feedback

  • API integrations where you need the full response immediately

Managing responses

Regenerate a response

If the bot's response wasn't satisfactory, regenerate it.

GraphQL Mutation:

mutation RegenerateResponse($messageId: Float!) {
  regenerateResponse(messageId: $messageId) {
    id
    message
    createdAt
  }
}

Parameters:

  • messageId (required) β€” ID of the bot's response message to regenerate

Returns: The new response message

Example:

const REGENERATE_RESPONSE = gql`
  mutation RegenerateResponse($messageId: Float!) {
    regenerateResponse(messageId: $messageId) {
      id
      message
      createdAt
    }
  }
`;

const regenerateResponse = async (client, messageId) => {
  const { data } = await client.mutate({
    mutation: REGENERATE_RESPONSE,
    variables: { messageId },
  });

  return data.regenerateResponse;
};

// Usage
const newResponse = await regenerateResponse(client, 54321);
console.log('New response generated:', newResponse.id);

Workflow with regenerate:

// User sends a message
const userMessage = await sendMessage(client, sessionId, 'Explain quantum computing');

// Subscribe to streaming response
let botMessageId;
subscribeToStream(client, sessionId, (fullMessage, data) => {
  botMessageId = data.botResponseMessageId;
  updateUI(fullMessage);
});

// Later, user clicks "Regenerate" button
const newResponse = await regenerateResponse(client, botMessageId);
console.log('Response regenerated');

Stop AI response

Stop the ongoing AI response generation.

GraphQL Mutation:

mutation StopAIResponse($sessionId: Float!, $botId: Float!) {
  stopAIResponse(sessionId: $sessionId, botId: $botId)
}

Parameters:

  • sessionId (required) β€” Session ID

  • botId (required) β€” Bot ID

Returns: Boolean (true if stopped successfully)

Example:

const STOP_AI_RESPONSE = gql`
  mutation StopAIResponse($sessionId: Float!, $botId: Float!) {
    stopAIResponse(sessionId: $sessionId, botId: $botId)
  }
`;

const stopGeneration = async (client, sessionId, botId) => {
  const { data } = await client.mutate({
    mutation: STOP_AI_RESPONSE,
    variables: { sessionId, botId },
  });

  return data.stopAIResponse;
};

// Usage
const stopped = await stopGeneration(client, 67890, 123);
if (stopped) {
  console.log('Generation stopped by user');
}

UI implementation with stop button:

let isGenerating = false;
let currentBotId = 123;

const startGeneration = async (sessionId) => {
  isGenerating = true;
  showStopButton();

  subscribeToStream(client, sessionId, (fullMessage, data) => {
    updateMessageUI(fullMessage);
    if (!data.isStoppable) {
      hideStopButton();
    }
  });
};

const handleStopClick = async (sessionId) => {
  if (isGenerating) {
    await stopGeneration(client, sessionId, currentBotId);
    isGenerating = false;
    hideStopButton();
  }
};

Retry failed message

Retry sending a message that failed.

GraphQL Mutation:

mutation RetryBotMessage($sessionId: Float!) {
  retryBotMessage(sessionId: $sessionId)
}

Parameters:

  • sessionId (required) β€” Session ID

Returns: Boolean (true if retry succeeded)

Example:

const RETRY_MESSAGE = gql`
  mutation RetryBotMessage($sessionId: Float!) {
    retryBotMessage(sessionId: $sessionId)
  }
`;

const retryMessage = async (client, sessionId) => {
  const { data } = await client.mutate({
    mutation: RETRY_MESSAGE,
    variables: { sessionId },
  });

  return data.retryBotMessage;
};

// Usage
const success = await retryMessage(client, 67890);
if (success) {
  console.log('Message retry successful');
}

Text-to-speech

Convert message to audio

Convert a message to audio using text-to-speech.

GraphQL Query:

query GetSpeechFromText($messageId: Float!) {
  getSpeechFromText(messageId: $messageId)
}

Parameters:

  • messageId (required) β€” ID of the message to convert

Returns: Audio URL string

Example:

const GET_SPEECH = gql`
  query GetSpeechFromText($messageId: Float!) {
    getSpeechFromText(messageId: $messageId)
  }
`;

const getAudioUrl = async (client, messageId) => {
  const { data } = await client.query({
    query: GET_SPEECH,
    variables: { messageId },
  });

  return data.getSpeechFromText;
};

// Usage
const audioUrl = await getAudioUrl(client, 12345);
const audio = new Audio(audioUrl);
audio.play();

With audio player UI:

const playMessageAudio = async (client, messageId) => {
  try {
    const audioUrl = await getAudioUrl(client, messageId);
    
    const audio = new Audio(audioUrl);
    audio.addEventListener('ended', () => {
      console.log('Audio playback finished');
    });
    
    audio.play();
    return audio;
  } catch (error) {
    console.error('Failed to play audio:', error);
  }
};

// Usage in UI
const audioElement = await playMessageAudio(client, messageId);

Complete chat manager class

Here's a reusable class that encapsulates all chat operations:

class ChatManager {
  constructor(client, sessionId) {
    this.client = client;
    this.sessionId = sessionId;
    this.currentBotId = null;
    this.isGenerating = false;
  }

  async sendMessage(messageText, options = {}) {
    const SEND_MESSAGE = gql`
      mutation SendNewMessage($newMessageData: NewMessageInput!) {
        sendNewMessage(newMessageData: $newMessageData) {
          id
          message
          createdAt
          sessionId
          isBotReply
        }
      }
    `;

    const { data } = await this.client.mutate({
      mutation: SEND_MESSAGE,
      variables: {
        newMessageData: {
          message: messageText,
          sessionId: this.sessionId,
          isForAiReply: true,
          ...options,
        },
      },
    });

    return data.sendNewMessage;
  }

  async sendSyncMessage(messageText, options = {}) {
    const SYNC_REQUEST = gql`
      mutation SyncBotRequest($newMessageData: NewMessageInput!) {
        syncBotRequest(newMessageData: $newMessageData) {
          text
          isBotError
          sources
          botId
        }
      }
    `;

    const { data } = await this.client.mutate({
      mutation: SYNC_REQUEST,
      variables: {
        newMessageData: {
          message: messageText,
          sessionId: this.sessionId,
          ...options,
        },
      },
    });

    return data.syncBotRequest;
  }

  subscribeToStream(onChunk, onComplete) {
    const MESSAGE_STREAM = gql`
      subscription OnMessageStream($sessionId: Float!) {
        onMessageStream(sessionId: $sessionId) {
          messageChunk
          botResponseMessageId
          isStoppable
          botId
        }
      }
    `;

    let fullMessage = '';

    return this.client
      .subscribe({
        query: MESSAGE_STREAM,
        variables: { sessionId: this.sessionId },
      })
      .subscribe({
        next: ({ data }) => {
          fullMessage += data.onMessageStream.messageChunk;
          this.currentBotId = data.onMessageStream.botId;
          this.isGenerating = true;
          onChunk(fullMessage, data.onMessageStream);
        },
        error: (error) => {
          console.error('Stream error:', error);
          this.isGenerating = false;
        },
        complete: () => {
          this.isGenerating = false;
          if (onComplete) onComplete(fullMessage);
        },
      });
  }

  async stopGeneration() {
    if (!this.isGenerating || !this.currentBotId) {
      return false;
    }

    const STOP = gql`
      mutation StopAIResponse($sessionId: Float!, $botId: Float!) {
        stopAIResponse(sessionId: $sessionId, botId: $botId)
      }
    `;

    const { data } = await this.client.mutate({
      mutation: STOP,
      variables: {
        sessionId: this.sessionId,
        botId: this.currentBotId,
      },
    });

    this.isGenerating = false;
    return data.stopAIResponse;
  }

  async regenerateResponse(messageId) {
    const REGENERATE = gql`
      mutation RegenerateResponse($messageId: Float!) {
        regenerateResponse(messageId: $messageId) {
          id
          message
        }
      }
    `;

    const { data } = await this.client.mutate({
      mutation: REGENERATE,
      variables: { messageId },
    });

    return data.regenerateResponse;
  }

  async retryMessage() {
    const RETRY = gql`
      mutation RetryBotMessage($sessionId: Float!) {
        retryBotMessage(sessionId: $sessionId)
      }
    `;

    const { data } = await this.client.mutate({
      mutation: RETRY,
      variables: { sessionId: this.sessionId },
    });

    return data.retryBotMessage;
  }

  async getAudioUrl(messageId) {
    const GET_SPEECH = gql`
      query GetSpeechFromText($messageId: Float!) {
        getSpeechFromText(messageId: $messageId)
      }
    `;

    const { data } = await this.client.query({
      query: GET_SPEECH,
      variables: { messageId },
    });

    return data.getSpeechFromText;
  }

  async playAudio(messageId) {
    const audioUrl = await this.getAudioUrl(messageId);
    const audio = new Audio(audioUrl);
    audio.play();
    return audio;
  }
}

// Usage example
const chatManager = new ChatManager(client, 67890);

// Send a message and listen to streaming
await chatManager.sendMessage('Tell me a story');

const subscription = chatManager.subscribeToStream(
  (fullMessage, data) => {
    console.log('Streaming:', fullMessage);
    updateUI(fullMessage);
  },
  (finalMessage) => {
    console.log('Response complete');
  }
);

// User clicks stop button
await chatManager.stopGeneration();

// User clicks regenerate
await chatManager.regenerateResponse(messageId);

// User clicks speaker icon
await chatManager.playAudio(messageId);

Practical use cases

Real-time chat UI

Build a chat interface with streaming responses:

const chatManager = new ChatManager(client, sessionId);

// User types and sends message
const handleSendMessage = async (userInput) => {
  // Display user message immediately
  addMessageToUI(userInput, 'user');

  // Send message and start streaming
  await chatManager.sendMessage(userInput);

  // Create placeholder for bot response
  let botMessageElement = addMessageToUI('', 'bot');

  // Subscribe to streaming
  chatManager.subscribeToStream(
    (fullMessage) => {
      // Update bot message as it streams
      updateMessageContent(botMessageElement, fullMessage);
    },
    () => {
      // Finalize when complete
      finalizeMessage(botMessageElement);
    }
  );
};

Quick answer without streaming

For simple Q&A, get the complete response immediately:

const handleQuickQuestion = async (question) => {
  try {
    const response = await chatManager.sendSyncMessage(question);
    
    if (response.isBotError) {
      showError('Bot encountered an error');
      return;
    }

    displayAnswer(response.text);
    
    if (response.sources) {
      displaySources(response.sources);
    }
  } catch (error) {
    showError('Failed to get answer');
  }
};

Audio playback for accessibility

Allow users to listen to responses:

const handleAudioButton = async (messageId) => {
  try {
    showLoadingSpinner();
    const audio = await chatManager.playAudio(messageId);
    hideLoadingSpinner();

    audio.addEventListener('ended', () => {
      console.log('Audio finished');
    });
  } catch (error) {
    hideLoadingSpinner();
    showError('Failed to play audio');
  }
};

Best practices

  1. Always set isForAiReply: true when you want an AI response; otherwise, the message is just stored without triggering generation.

  2. Use streaming for better UX β€” Streaming shows responses character by character, giving immediate feedback. Use synchronous only for simple cases.

  3. Handle stop gracefully β€” Show a stop button during generation and handle the stop action cleanly.

  4. Store message IDs β€” Keep track of message IDs for regeneration and audio features.

  5. Validate input before sending β€” Check message length and content before sending to avoid errors.

  6. Implement error handling β€” Always wrap mutations in try-catch blocks and show user-friendly error messages.

  7. Use file attachments wisely β€” Only attach files when necessary; validate file IDs before sending.

  8. Cache audio URLs β€” Store generated audio URLs to avoid regenerating the same audio multiple times.