Help center

Guide: build an internal chatbot on your SOPs

Retrieve the right SOPs with full-text search, turn their cards into clean text, and ground a language model's answer in them with links back to the app.

Business plan Updated
On this page
  1. The flow
  2. Step 1: Search
  3. Step 2: Read the SOPs in full
  4. Step 3: Ground the answer
  5. Make it fast and cheap
  6. Keep it safe

This guide builds the retrieval half of a chatbot that answers questions from your SOPs, for Slack, Teams or an internal page. It uses a read only key and works with any language model.

Note: SOPCompass already has Ask, an AI assistant inside the app that answers from your SOPs. Build your own when you want answers somewhere else, such as a Slack channel or your support tool.

The flow

  1. Search. Send the user’s question to GET /search and take the top few SOPs.
  2. Read. Fetch each with GET /sops/{id} and turn its cards and steps into plain text.
  3. Answer. Give that text to your model with instructions to answer only from it, and cite the SOPs by app_url.

Full-text search matches words, not meaning, so strip the question down to its content words and loosen it with or when an exact search finds nothing.

Python
import os
import re

import requests

BASE = "https://app.sopcompass.com/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['SOPCOMPASS_API_KEY']}"


def search(question, limit=5):
    words = re.findall(r"[\w']+", question.lower())
    strict = " ".join(words)[:200]
    res = session.get(f"{BASE}/search", params={"q": strict, "limit": limit}, timeout=30).json()
    if res.get("data"):
        return res["data"]
    # Nothing matched every word: accept any of them.
    loose = " or ".join(w for w in words if len(w) > 3)[:200]
    if not loose:
        return []
    return session.get(f"{BASE}/search", params={"q": loose, "limit": limit}, timeout=30).json().get("data", [])

Each result has the SOP (with id, title and app_url), a rank and a plain-text snippet.

Step 2: Read the SOPs in full

Python
import html


def to_text(fragment):
    text = re.sub(r"<(br|/p|/li|/h\d)>", "\n", fragment or "")
    text = re.sub(r"<[^>]+>", "", text)
    return html.unescape(text).strip()


def sop_as_text(sop_id):
    sop = session.get(f"{BASE}/sops/{sop_id}", timeout=30).json()["data"]
    parts = [f"# {sop['title']}"]
    if sop["description"]:
        parts.append(sop["description"])
    for card in sop["cards"]:
        parts.append(f"## {card['title']}")
        parts.append(to_text(card["body_html"]))
        for n, step in enumerate(card["steps"], start=1):
            parts.append(f"{n}. {step['title']}\n{to_text(step['html'])}".strip())
    return sop, "\n\n".join(p for p in parts if p)

Stripping tags here is fine because the text goes to a model, not onto a web page.

Step 3: Ground the answer

Python
def build_prompt(question):
    sources = []
    for result in search(question, limit=3):
        sop, text = sop_as_text(result["sop"]["id"])
        sources.append(f"SOURCE [{sop['title']}]({sop['app_url']})\n{text[:12000]}")
    if not sources:
        return None
    return (
        "Answer the question using only the SOPs below. If they don't answer it, say so. "
        "Cite the SOP you used by its link.\n\n"
        + "\n\n---\n\n".join(sources)
        + f"\n\nQuestion: {question}"
    )


prompt = build_prompt("How do we degrease the kitchen hood filters?")
# Send `prompt` to the language model of your choice and post its reply.

When build_prompt returns None, reply that no SOP covers the question instead of letting the model guess.

Make it fast and cheap

  • Cache full SOPs by id and updated_at. Search results include updated_at, so you only need GET /sops/{id} when the SOP changed since you cached it. Or keep a full local copy with the sync guide and only call GET /search live.
  • Mind the rate limit. One question costs up to 1 or 2 searches plus one read per SOP. At 60 requests a minute per key, cache aggressively for a busy channel. See Rate limits.
  • Skip archived SOPs. Search already leaves them out.

Keep it safe

  • The bot sees everything the key’s workspace can see, including every library. Anyone who can message the bot can get an answer from any SOP. If some SOPs are for managers only, don’t expose a bot to everyone, or filter results by library_id or departments before building the prompt.
  • Run the bot on a server. The key must never reach the chat client or a browser.
  • Links in answers use app_url, which only works for people signed in to the workspace.