What this service does
The Blade AI Chatbot Integration API is a thin, opinionated gateway
that sits between your frontend and a LangGraph-based
deepagents runtime. Your client posts a single message;
the server orchestrates the model, calls MCP tools, dispatches UI
actions back to you, remembers the conversation, and streams
everything over Server-Sent Events.
Real-time streaming
Token-by-token SSE. Show output as the model produces it — no polling, no websockets.
Tool calling
MCP tools run server-side. UI tools are dispatched to your client as structured actions.
Memory built in
Pass a stable conversation_id and the agent remembers previous turns.
Swap models freely
OpenRouter or Ollama backends, switchable per session, project, or globally at runtime.
Quick start
The only endpoint you strictly need is the chat one. Send a JSON message, read an SSE stream, render tokens as they arrive.
1. Base URL
In production, put this API behind your own gateway/domain. Locally the server listens on port 8100.
# Local dev
http://localhost:8100
# Production (behind your gateway)
https://your-gateway.example.com
2. Fire your first message
curl -N -X POST http://localhost:8100/utils/ai-agent/chat \
-H "Content-Type: application/json" \
-d '{
"message": "Hello, what can you do?",
"conversation_id": "demo-session-1"
}'
The -N flag disables cURL buffering so you can watch
the SSE stream arrive line by line.
Chat endpoint
| Method | Path | Content | Response |
|---|---|---|---|
| POST | /utils/ai-agent/chat |
application/json | text/event-stream |
Request body
messagestring required- The end user's message. Must be non-empty.
conversation_idstring optional- Stable identifier for the chat session — typically a client-generated UUID (
crypto.randomUUID()). Reuse the same value across turns so the agent recalls prior context. Omit to make each turn stateless. Never reuse the same id across different end users — they would share history. project_idstring optional- Identifier for the project/workspace the user is in. Used as a scope key for provider overrides. Not validated against any external system.
urlstring optional- The URL of the screen the user is currently viewing. The agent uses it to infer context (e.g. which file is open).
layersarray<{id, name}> optional- Currently visible layers of the open drawing / document. Lets the model resolve human names to real IDs when calling UI tools.
Minimal request
{
"message": "Summarize the current document",
"conversation_id": "5b2f0b6c-08a2-4b4b-9c3d-3e0f8a5b6c7d",
"project_id": "proj_42",
"url": "https://app.example.com/viewer/pdf?baseFileId=abc"
}
SSE event schema
Every line the server sends looks like data: {...}\n\n
— standard Server-Sent Events. Each JSON payload carries a
type field so you can dispatch on it.
Event type | Fields | What to do |
|---|---|---|
token |
content: string chunk |
Append to the assistant bubble as it arrives. |
tool_start |
tool: MCP tool name |
Optionally show a "thinking / searching…" indicator. |
tool_call |
tool, params, requiresConfirm |
The agent wants your UI to perform an action. Dispatch it in your app. |
table |
Structured tabular data | Render as a table (optional — falls back to text if ignored). |
done |
— | Turn finished. Close the stream / re-enable input. |
Sample stream
data: {"type":"token","content":"Sure, "}
data: {"type":"token","content":"let me open page 3."}
data: {"type":"tool_call","tool":"go_to_page","params":{"pageNumber":3},"requiresConfirm":false}
data: {"type":"done"}
Client examples
Three ready-to-paste snippets that consume the SSE stream and dispatch UI actions.
// Works in modern browsers, Node 18+, and Deno.
async function chat(message, conversationId, onEvent) {
const res = await fetch("/utils/ai-agent/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message, conversation_id: conversationId }),
});
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
let idx;
while ((idx = buffer.indexOf("\n\n")) >= 0) {
const frame = buffer.slice(0, idx).trim();
buffer = buffer.slice(idx + 2);
if (!frame.startsWith("data:")) continue;
const event = JSON.parse(frame.slice(5).trim());
onEvent(event);
}
}
}
// Usage
const convoId = crypto.randomUUID();
chat("Hello!", convoId, (evt) => {
if (evt.type === "token") appendToBubble(evt.content);
if (evt.type === "tool_call") dispatchUiAction(evt.tool, evt.params);
if (evt.type === "done") enableInput();
});
import json, uuid, httpx
def chat(message: str, convo_id: str):
payload = {"message": message, "conversation_id": convo_id}
url = "http://localhost:8100/utils/ai-agent/chat"
with httpx.stream("POST", url, json=payload, timeout=None) as r:
for line in r.iter_lines():
if not line or not line.startswith("data:"):
continue
event = json.loads(line[5:].strip())
if event["type"] == "token":
print(event["content"], end="", flush=True)
elif event["type"] == "tool_call":
print(f"\n[ui-action] {event['tool']} {event['params']}")
elif event["type"] == "done":
print()
chat("Hello!", str(uuid.uuid4()))
curl -N -X POST http://localhost:8100/utils/ai-agent/chat \
-H "Content-Type: application/json" \
-d '{
"message": "What tools do you have?",
"conversation_id": "demo-1"
}'
EventSource.
The browser EventSource API only supports GET
requests, so it can't send the JSON body. Use fetch
with a streaming reader as shown above.
Conversations & memory
The server persists turn history in MongoDB, keyed by
conversation_id. On every request it feeds the last
few turns back into the model as context.
- Generate a fresh UUID per user session with
crypto.randomUUID()and reuse it for every turn in that session. - Two different users must never share the same
conversation_id— they would see each other's history. - Conversations are automatically deleted after a configurable idle period (TTL), so old sessions don't linger.
- Omit
conversation_identirely to make a turn stateless (nothing is stored, nothing is recalled).
UI tools
When the agent decides a UI action would help, it emits a
tool_call SSE event. Your frontend is responsible for
actually performing the action — navigating, highlighting,
toggling a layer, whatever your app supports.
You declare the available UI tools once via
POST /utils/ai-agent/ui-tools. Each tool is an
OpenAI-style function schema. The list is stored server-side and
picked up on the next turn.
{
"tools": [
{
"type": "function",
"function": {
"name": "go_to_page",
"description": "Jump the PDF viewer to a specific page in the open drawing.",
"parameters": {
"type": "object",
"properties": {
"pageNumber": { "type": "integer", "description": "Page to navigate to" }
},
"required": ["pageNumber"]
}
}
}
]
}
Then, in your SSE handler, listen for events with
type: "tool_call" and route evt.tool +
evt.params into your action registry:
const ACTIONS = {
go_to_page: ({ pageNumber }) => viewer.jumpTo(pageNumber),
highlight: ({ id }) => viewer.flash(id),
};
function dispatchUiAction(name, params) {
const fn = ACTIONS[name];
if (fn) fn(params);
else console.warn("unknown UI tool", name);
}
Provider & model
The agent runs on either OpenRouter (any OpenAI-compatible model) or a self-hosted Ollama server. You can switch model globally, or override it per project or per conversation — useful for A/B tests or giving certain users access to a stronger model.
| Method | Path | Purpose |
|---|---|---|
| GET | /utils/ai-agent/provider |
Inspect the effective provider & model. |
| POST | /utils/ai-agent/provider |
Override provider & model at global / project / session scope. |
Resolution order (highest wins): session → project → global → environment default.
{
"provider": "openrouter",
"model": "anthropic/claude-sonnet-4.5",
"scope": "project",
"scope_id": "proj_42"
}
Provider & API-key configuration also lives in the admin dashboard so you don't have to redeploy to change models.
Health & operations
| Method | Path | What it returns |
|---|---|---|
| GET | /health |
MongoDB reachability + overall status (200 healthy / 503 degraded). |
| GET | /docs |
Interactive Swagger UI for every endpoint. |
| GET | /redoc |
ReDoc reference for the same schema. |
| GET | /admin |
Password-protected dashboard: provider config, MCP endpoint, UI tools, live test chat. |
CORS
Browser origins are whitelisted via the WEB_URL
environment variable (comma-separated) or a regex in
CORS_ORIGIN_REGEX. Use WEB_URL=* in
local dev if you need any origin.
Timeouts
Chat requests get a generous 10-minute ceiling because a single
agent turn can involve several tool round-trips. All other
endpoints use the default REQUEST_TIMEOUT.
POST /utils/ai-agent/chat. Everything else —
models, tools, MCP configuration — can be tuned from
/admin without touching your client code.