Building a bot with an AI assistant
The API is small enough that an assistant can write most of a bot in one pass. What an assistant cannot do is guess the contract, and a wrong guess produces code that looks right and fails in testing. Give it the contract first.
Paste the reference prompt, then the prompt for your chat application, then let the assistant work. Both are written to be pasted as they are.
The reference prompt
You are writing a chat bot that connects a chat application to Search2o. Use only the
API described here. Do not invent endpoints, fields or values.
BASE URL
The address of the customer's agent server. Call it BASE. Every call is a POST with a
JSON body. Every response is JSON.
AUTHENTICATION
Calls made on behalf of a person carry that person's integration token:
Authorization: Bearer <token>
Three calls need no token at all: startConnect, getConnectRequest, getConnectToken.
ERRORS
A failure is HTTP 4xx with a body of:
{ "success": false, "error": { "message": "...", "cause": "...", "data": {} } }
Show error.message. There is no error code to branch on.
CONNECTING A PERSON (run once per person, no password is ever handled by the bot)
1. POST BASE/api/auth/startConnect
in : { "clientName": "Slack - Acme workspace" }
out: { "success": true, "connectId": "...", "connectSecret": "...",
"connectUrl": "...", "userCode": "ZUEZFA",
"expiresIn": 600, "pollIntervalSeconds": 5 }
Keep connectSecret private to the bot. Never show it or put it in a link.
2. Show the person a button opening connectUrl, and show userCode next to it.
Tell them to check that the code on the page matches this one.
3. POST BASE/api/auth/getConnectToken every pollIntervalSeconds
in : { "connectId": "...", "connectSecret": "..." }
out: { "status": "pending" | "approved" | "denied" | "expired",
"token": "...", "tokenType": "Bearer", "expiresIn": 31536000,
"userEmail": "...", "userName": "..." }
token is set only when status is approved, and only once. Store it against the
person's chat-application user id. Stop polling on denied or expired.
FINDING AN AGENT
POST BASE/api/exec/search
in : { "query": "the person's question" } at least 8 characters
out: { "success": true,
"searchResults": [ { "agentName": "...", "agentTitle": "..." } ],
"searchBehavior": "executeTopMatch" | "executeOnlyMatch" | "showResults",
"followupBehavior": "executeTopMatch" | "executeOnlyMatch" | "showResults"
| "executePrevious" }
Up to three results. searchBehavior tells you what to do with them for a new question,
followupBehavior for a question inside a conversation that already exists.
executeTopMatch run the first result
executeOnlyMatch run it when there is one result, otherwise show the list
showResults show the list and let the person choose
executePrevious keep using the agent already in this conversation
An empty searchResults means no agent covers that question. Say so.
RUNNING AN AGENT
POST BASE/api/exec/execAgent
in : { "agentName": "...", "inputs": { "query": "the person's question" },
"stream": false, "convid": "..." }
Leave convid out to start a conversation. Pass it to continue one.
out: { "success": true, "convid": "...", "agentName": "...",
"resultCode": "...", "askInput": null,
"output": { "agentName": "...", "parts": [ ... ] } }
resultCode is one of:
success finished; show output.parts
ask it needs input; see ASKING below
unknownConversation the conversation is gone; start a new one
failCommand, errorInAgent, callFailed, timedOut, stopped, unexpected, mustLogin
failed; tell the person, using error.message when present
output.parts is a list. Each part is one of:
{ "contentType": "text", "text": "markdown" }
{ "contentType": "html", "text": "<p>...</p>" }
{ "contentType": "image", "text": "<base64>", "mimeType": "image/png" }
ASKING THE PERSON FOR INPUT
When resultCode is "ask", askInput is:
{ "message": "shown above the fields",
"inputs": [ { "name": "...", "type": "...", "label": "...",
"description": "...", "options": [], "default": null,
"hidden": false } ] }
type is one of str, password, text, chooseOne, chooseMany.
A hidden input is not shown to the person. Send it back unchanged.
Collect the answers and call execAgent again with the same convid and
inputs set to { "<name>": "<answer>", ... }.
CONVERSATIONS
POST BASE/api/user/getConversation { "convid": "..." }
POST BASE/api/user/getUnpinnedConversations { "nextCursor": null, "limit": 25 }
POST BASE/api/user/getPinnedConversations { }
POST BASE/api/user/setConversationTitle { "convid": "...", "title": "..." }
POST BASE/api/user/setPinned { "convid": "...", "pinned": true }
POST BASE/api/user/deleteConversation { "convid": "..." }
RULES YOU MUST FOLLOW
- Store one integration token per chat-application user. Never share one between people.
- Keep your own record of which chat thread belongs to which convid. Search2o holds no
link to the chat application.
- When execAgent returns unknownConversation, forget the stored convid, start a
new conversation and carry on. It is not an error to report.
- Never log a token, never put one in a URL, never post one into a channel.
- An answer is markdown. Convert it to the chat application's own format.
- html parts cannot be shown in any chat application. Convert them to text, or tell the
person the answer is in the Search2o GUI.
- image parts are base64. Upload them using the chat application's file API.
- When searchBehavior or followupBehavior says to show the results, show one button per
agent. Put a short id in the button and keep the question in your own store, because a
button carries little and a question can be long. Replace the message once somebody
chooses, so a second click cannot start a second conversation. Accept the click only
from the person who asked.
- When a run returns unknownConversation, reuse the message you already posted for
the retry. Do not post a second one.The prompt for Slack
Write a Slack bot in <your language> using the reference above.
- Use Socket Mode, so the bot needs no public address and can run inside a private
network next to the agent server.
- Verify Slack's request signature on every event.
- Respond to app mentions and direct messages.
- Reply in a thread. Key the conversation record on team id, channel id and thread_ts.
- On first use by a person, run the connect flow. Post the button and the code in a
direct message, never in a channel.
- Acknowledge the event within three seconds. Post a short "working on it" message, then
edit it with chat.update when the answer arrives. An agent can take a minute, so never
hold the acknowledgement open waiting for it.
- An ask cannot open a modal directly. A modal needs a trigger_id, and a message event
does not carry one. Post a message with an Answer button, and open the modal from the
button click.
- Map the input types to Slack blocks: str to plain text, password to plain text, text to
a multiline input, chooseOne to a static select, chooseMany to a multi select.
- Show search results as buttons in a message, one per agent, and update that message once
somebody chooses.
- Convert markdown to Slack mrkdwn. Bold is *text*, links are <url|label>.
- Upload image parts with files.upload and post them in the thread.The prompt for Microsoft Teams
Write a Microsoft Teams bot in <your language> using the reference above.
- Use the Bot Framework. The bot needs a public HTTPS endpoint, or a tunnel into the
network where the agent server runs.
- Respond to messages in channels and in one to one chats.
- Key the conversation record on the Teams conversation id and the reply chain id.
- On first use by a person, run the connect flow. Send the button as an Adaptive Card,
in a one to one chat rather than a channel.
- Acknowledge the activity at once. Keep the conversation reference, and send the answer
as a proactive message when it arrives, or update the activity. An agent can take a
minute, which is longer than the connector will wait, so never answer inline.
- Render an ask as an Adaptive Card with an input for each field, and a submit action
that returns the answers.
- Teams accepts a useful subset of markdown. Convert what it does not accept.
- Attach image parts as card images or file attachments.The prompt for Google Chat
Write a Google Chat app in <your language> using the reference above.
- Receive events over Pub/Sub, so the app needs no public address and can run inside a
private network next to the agent server.
- Respond to messages in spaces and in direct messages.
- Key the conversation record on the space name and the thread name.
- On first use by a person, run the connect flow. Send the button as a card, in a direct
message rather than a space.
- Reply in the same thread.
- Acknowledge the event at once, then create the answer as a new message with the Chat API
when it arrives. An agent can take a minute, which is longer than the wait allowed for a
reply to the event itself.
- Render an ask as a card with a section per field and a submit button.
- Convert markdown to Google Chat formatting. Bold is *text*, links are <url|label>.
- Upload image parts as card images.Check these before you trust the result
An assistant produces something plausible. These are the points where a plausible answer is wrong, so check each one against the running bot.
- The connect flow never returns the token to the browser. Code that reads a token from a redirect or from the page has invented that.
- The token comes back once. Code that expects to fetch the token again later is wrong.
unknownConversationmeans start a new conversation. Code that treats it as a failure shows people an error when a conversation simply expired.- The question goes in
inputsunder the namequery. The question is not a top-level field. convidis left out to start a conversation. The field is not set to null and not set to an empty string.- An agent that asks needs the same
convidon the next call, or the agent starts again from the beginning. - In Slack, opening a form straight from a message event cannot work. Generated code that does that fails the first time an agent asks for anything.

