AI agent integration

Wire ConvoMem into any LLM agent as callable tools — with ready-to-paste tool definitions, SDK references, and channel blueprints.

ConvoMem is designed to work as a set of tools an LLM agent can call. Register the definitions below with your platform (OpenAI, Anthropic, VAPI, Retell, Bland.ai, etc.) and ConvoMem handles identity resolution, session management, and memory extraction.

The agent loop

Every integration follows the same pattern:

1. recall_customer_context   → inject context into system prompt
2. LLM generates reply       → send to user
3. capture_message           → queue memory extraction (fire-and-forget)
   repeat for each turn
4. end_conversation          → on goodbye / issue resolved
   escalate_conversation     → on transfer to human

Tool definitions

The JSON below is OpenAI function-calling format. Map name, description, and parameters into your platform's tool schema — the fields are the same everywhere.

The description is the only instruction the model gets per tool, so wording matters. Tighten the descriptions to your domain, but keep the guarantees — especially the verbatim capture rule.

recall_customer_context

Call at conversation start and again on any turn that references the past, an account, or a preference.

{
  "type": "function",
  "function": {
    "name": "recall_customer_context",
    "description": "Retrieve what is already known about the customer before you reply. CALL IT: (1) once at the start of the conversation, as soon as you have any identifier (phone, email, externalId, or customerId); and (2) again on any turn where the user references the past, a prior issue, an account/order, or a preference. ALWAYS set `topic` to the user's current message or question so the most relevant memories are returned. USE the returned `context` string as ground truth when forming your reply, and the returned `customerId` for later capture/end/escalate calls. Pass `autoCreate: true` so a first-time caller is still registered. This is a read — it never stores anything.",
    "parameters": {
      "type": "object",
      "properties": {
        "phone":      { "type": "string", "description": "Caller phone number, ideally E.164 (+14155550101)." },
        "email":      { "type": "string", "description": "Customer email address." },
        "externalId": { "type": "string", "description": "Your CRM / system customer ID." },
        "customerId": { "type": "string", "description": "ConvoMem customer UUID, if already known." },
        "topic":      { "type": "string", "description": "What to recall — e.g. the user's current question. Enables semantic ranking and returns a prompt-ready context string." },
        "autoCreate": { "type": "boolean", "description": "Create the customer if not found.", "default": true },
        "userName":   { "type": "string", "description": "Customer's display name, to set or backfill the profile when they introduce themselves." }
      }
    }
  }
}
const { context, customer } = await client.lookup(topic, { email, phone, customerId }, { autoCreate: true })

capture_message

Call after every exchange — the user turn and your reply, in order.

{
  "type": "function",
  "function": {
    "name": "capture_message",
    "description": "Record the conversation into ConvoMem's long-term memory. CALL IT after EVERY exchange — i.e. after you send each reply — passing the new turns since your last capture (the user's message AND your assistant reply, in order). Do NOT judge whether a turn is 'important' or 'memorable' and do NOT skip, summarize, paraphrase, or filter turns: send them verbatim and let ConvoMem decide what to extract and de-duplicate. Skipping turns loses memories. Each call appends to the customer's active session automatically, so send only the new turns, not the whole transcript again. This is fire-and-forget — extraction happens in the background, so it returns before facts are searchable.",
    "parameters": {
      "type": "object",
      "properties": {
        "customerId":     { "type": "string", "description": "Customer UUID from recall_customer_context. Preferred identifier." },
        "phoneNumber":    { "type": "string", "description": "Phone identity (use if customerId is unknown)." },
        "email":          { "type": "string", "description": "Email identity (use if customerId is unknown)." },
        "externalId":     { "type": "string", "description": "External / CRM identity (use if customerId is unknown)." },
        "messages":       { "type": "string", "description": "JSON-encoded array of the NEW turns since your last capture, in chronological order, verbatim: [{\"role\":\"user\",\"content\":\"\"},{\"role\":\"assistant\",\"content\":\"\"}]. Include both the user turn and your reply. Never omit a turn." },
        "channel":        { "type": "string", "enum": ["VOICE", "CHAT", "SMS", "EMAIL"], "description": "The channel this conversation is on. Use the same value for the whole conversation.", "default": "CHAT" },
        "userName":       { "type": "string", "description": "The customer's name if they state it during the conversation." },
        "idempotencyKey": { "type": "string", "description": "Optional unique key (≤128 chars) per exchange to make retries safe without double-recording." }
      },
      "required": ["messages"]
    }
  }
}
// fire-and-forget — don't await
client.capture(
  [{ role: 'user', content: userMsg }, { role: 'assistant', content: reply }],
  { customerId: customer.id },
  { channel: 'CHAT' },
).catch(console.error)

Send only new turns each call — not the full transcript. Across the conversation, every turn is captured exactly once.


search_memories

Targeted mid-conversation recall when you already know who the customer is.

{
  "type": "function",
  "function": {
    "name": "search_memories",
    "description": "Search a customer's long-term memories for a specific topic and get a prompt-ready `context` string. Use this — instead of recall_customer_context — when you already know who the customer is and need to answer a specific past-fact question (e.g. 'what plan are they on?', 'did they report this before?'), and you only have a phone, email, or externalId. Set `topic` to the exact thing you're trying to recall. Read-only — stores nothing.",
    "parameters": {
      "type": "object",
      "properties": {
        "topic":      { "type": "string", "description": "Topic or question to find relevant memories for." },
        "phone":      { "type": "string", "description": "Customer phone number." },
        "email":      { "type": "string", "description": "Customer email." },
        "externalId": { "type": "string", "description": "External / CRM identifier." }
      },
      "required": ["topic"]
    }
  }
}
const { context } = await client.lookup(topic, { email, phone, externalId })

get_handoff_brief

Call immediately before transferring to a human agent.

{
  "type": "function",
  "function": {
    "name": "get_handoff_brief",
    "description": "Build a cross-channel briefing (full journey across chat/email/voice/SMS, key memories, sentiment trend, open-issue detection, and an AI-written narrative) for the human who is about to take over. CALL IT immediately before you transfer or escalate, so the agent has full context. Relay the `narrative` and `openIssue` to the human agent; do not read raw memory IDs to the customer. Read-only.",
    "parameters": {
      "type": "object",
      "properties": {
        "phone":      { "type": "string" },
        "email":      { "type": "string" },
        "externalId": { "type": "string" },
        "customerId": { "type": "string" },
        "narrative":  { "type": "string", "enum": ["true", "false"], "description": "Set 'false' to skip the AI narrative for a faster, structured-only brief.", "default": "true" }
      }
    }
  }
}
const handoff = await client.getHandoff({ customerId, email, phone })

end_conversation

Call once, when the conversation is genuinely finished.

{
  "type": "function",
  "function": {
    "name": "end_conversation",
    "description": "Close the active conversation and record how it ended. CALL IT once, when the conversation is genuinely finished — the user says goodbye, confirms their issue is resolved, or the call ends. Do NOT call it mid-conversation or because there is a pause. Capture the final turns with capture_message FIRST, then call this. Set `outcome` to a one-line factual result (what was done or decided), not a transcript. After this, the next message from the customer starts a fresh conversation.",
    "parameters": {
      "type": "object",
      "properties": {
        "customerId":     { "type": "string", "description": "Customer UUID." },
        "conversationId": { "type": "string", "description": "Conversation ID from recall_customer_context or capture_message response." },
        "outcome":        { "type": "string", "description": "Short resolution summary, e.g. 'Booked return flight; customer satisfied.'" }
      },
      "required": ["customerId", "conversationId"]
    }
  }
}
await client.endConversation(conversationId, { customerId }, { outcome: 'Resolved billing issue' })

escalate_conversation

Call when handing off to a human. Pair with get_handoff_brief.

{
  "type": "function",
  "function": {
    "name": "escalate_conversation",
    "description": "Hand the active conversation to a human agent and mark it ESCALATED. CALL IT when the user explicitly asks for a person, is clearly frustrated or upset, or the request is outside what you can resolve. Use this rather than end_conversation when a human still needs to act. Set `reason` to a short factual explanation of why (≤500 chars). Pair this with get_handoff_brief so the human has full context. Capture the final turns first.",
    "parameters": {
      "type": "object",
      "properties": {
        "customerId":     { "type": "string", "description": "Customer UUID." },
        "conversationId": { "type": "string", "description": "Conversation ID from recall_customer_context or capture_message response." },
        "reason":         { "type": "string", "description": "Why it is being escalated (≤500 chars)." }
      },
      "required": ["customerId", "conversationId"]
    }
  }
}
await client.escalateConversation(conversationId, { customerId }, { reason: 'Complex billing dispute' })

URL & SDK reference

ToolAPI endpointTypeScriptPythonRust
recall_customer_contextGET /customers/lookupclient.lookup(topic, identity, { autoCreate: true })client.lookup(topic, identity, auto_create=True)client.lookup(topic, &id, Some(true), None)
capture_messagePOST /captureclient.capture(msgs, identity, { channel })client.capture(msgs, identity, channel=…)client.capture(msgs, &id, channel, …)
search_memoriesGET /customers/memories/lookupclient.lookup(topic, identity)client.lookup(topic, identity)client.lookup(topic, &id, None, None)
get_handoff_briefGET /customers/handoffclient.getHandoff(identity)client.get_handoff(identity)client.get_handoff(&id, None, None)
end_conversationPOST /customers/conversations/endclient.endConversation(convId, identity, { outcome })client.end_conversation(convId, identity)client.end_conversation(convId, &id, outcome)
escalate_conversationPOST /customers/conversations/escalateclient.escalateConversation(convId, identity, { reason })client.escalate_conversation(convId, identity)client.escalate_conversation(convId, &id, reason)

All endpoints under https://api.convomem.com/api/v1. Auth header: X-API-Key: <key>.

SDK docs: TypeScript · Python · Rust


Channel blueprints

Chatbot

Web widget, WhatsApp, Slack — tight recall → reply → capture per message.

chatbot.ts
import OpenAI from 'openai'
import { ConvoMem } from '@optraze/convomem-sdk'
 
const client = new ConvoMem({ apiKey: process.env.CONVOMEM_API_KEY! })
const openai = new OpenAI()
 
async function handleMessage(userMessage: string, session: { email: string }) {
  // 1. Recall — before every reply
  const { context, customer } = await client.lookup(userMessage, {
    email: session.email,
  })
 
  // 2. Reply with context injected
  const completion = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [
      {
        role: 'system',
        content: `You are a helpful support agent.\n\nWhat you know about this customer:\n${context}`,
      },
      { role: 'user', content: userMessage },
    ],
  })
  const reply = completion.choices[0].message.content!
 
  // 3. Capture — fire-and-forget
  client.capture(
    [
      { role: 'user', content: userMessage },
      { role: 'assistant', content: reply },
    ],
    { customerId: customer?.id, email: session.email },
    { channel: 'CHAT' },
  ).catch(console.error)
 
  return reply
}

Voicebot / IVR

Sub-second latency budgets — look up at call start, register tools for inline calls during.

Call start:

call-start.ts
// Look up as soon as the call connects — before the first reply
const { context, customer } = await client.lookup(
  'caller history and preferences',
  { phone: callerPhoneNumber },
)
 
const systemPrompt = `You are a voice support agent.
 
What you know about this caller:
${context || 'No prior history.'}`

Call end:

call-end.ts
// Capture the full transcript, then close the session
await client.capture(transcript, { customerId: customer.id }, { channel: 'VOICE' })
await client.endConversation(
  conversationId,
  { customerId: customer.id },
  { outcome: 'Resolved billing question' },
)

VAPI — add an API Tool: method GET, URL https://api.convomem.com/api/v1/customers/lookup, header X-API-Key: sk-org-…, map caller number to the phone query param.

Bland.ai — register a custom tool:

{
  "name": "lookup_customer",
  "description": "Fetch caller history and memory context.",
  "url": "https://api.convomem.com/api/v1/customers/lookup?phone={{phone}}&autoCreate=true",
  "method": "GET",
  "headers": { "X-API-Key": "sk-org-…" }
}

Email bot

Turn-based with long windows — use the sender address as identity.

email-handler.ts
async function handleInboundEmail(from: string, subject: string, body: string) {
  // 1. Recall context for this sender
  const { context } = await client.lookup(`${subject} ${body}`, { email: from })
 
  // 2. Draft and send reply
  const reply = await generateReply(context, subject, body)
 
  // 3. Capture the exchange
  await client.capture(
    [
      { role: 'user', content: `Subject: ${subject}\n\n${body}` },
      { role: 'assistant', content: reply },
    ],
    { email: from },
    { channel: 'EMAIL' },
  )
 
  // 4. End session if resolved
  if (isResolved(reply)) {
    await client.endConversation(null, { email: from }, {
      outcome: 'Email thread resolved',
    })
  }
 
  return reply
}

Conversation sessions

Conversations idle-close after 15 minutes of inactivity (configurable server-side). You don't need to call endConversation unless you want to:

  • Record an outcome for handoff briefings
  • Trigger the conversation.completed webhook immediately
  • Force a new session to start on the next message

The next capture after a closed session automatically starts a fresh conversation.

What's next

  • Capture — full capture API reference and idempotency
  • Handoff — briefing response fields and injection patterns
  • Webhooks — react to memory.captured and conversation.* events
  • Errors & limits — rate limits, error codes, retry guidance

SDK: TypeScript · Python · Rust