mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-22 13:34:24 +00:00
c85b9401b8
A first-class, vendor-neutral message-platform adapter (http_bot) for
server-to-server integrations (LangBot Space ticketing et al). Drives a
pipeline over plain HTTP with no long-lived connection:
- Inbound: signed POST to the existing unified webhook route /bots/<uuid>,
carrying a caller-defined session_id mapped to the LangBot launcher id via
get_launcher_id -> per-session isolation. Preserves pipeline-native N->1
aggregation for free.
- Outbound: each reply_message / reply_message_chunk becomes one signed
callback POST to the config-only callback_url, delivered in per-session
sequence order with retry/backoff -> 1->M multi-reply.
- Sub-paths: /reset (drop a session) and /sync (block for the collapsed reply).
- Auth: symmetric HMAC-SHA256 both directions (timestamp + replay window),
no JWT/Turnstile, no socket.
Decisions: callback URL is config-only (SSRF closed); reset + sync shipped;
Python + TS reference clients shipped (signing verified byte-identical 3-way).
No core changes: the unified webhook router, aggregator, query pool and
pipeline are untouched. Adapter is auto-discovered from platform/sources/.
Adds:
src/langbot/pkg/platform/sources/http_bot.{py,yaml,svg}
src/langbot/pkg/platform/sources/http_bot_signing.py
docs/platforms/http-bot.md, docs/http-bot-openapi.json
examples/http-bot/{client.py,client.ts,README.md}
Updates docs/HTTP_BOT_ADAPTER_DESIGN.md (status: implemented).
199 lines
7.9 KiB
JSON
199 lines
7.9 KiB
JSON
{
|
|
"openapi": "3.0.3",
|
|
"info": {
|
|
"title": "LangBot HTTP Bot Adapter",
|
|
"version": "1.0.0",
|
|
"description": "Server-to-server HTTP integration for a LangBot pipeline. Inbound messages are POSTed to the unified webhook route; replies are delivered to a configured callback URL (one POST per reply part). All requests are HMAC-SHA256 signed. See docs/platforms/http-bot.md."
|
|
},
|
|
"paths": {
|
|
"/bots/{bot_uuid}": {
|
|
"post": {
|
|
"summary": "Push a message into the pipeline (fire-and-collect)",
|
|
"description": "Returns 202 immediately. Replies arrive asynchronously on the configured callback URL. Reuse the same session_id within the aggregation window to merge multiple messages into one turn (N->1).",
|
|
"parameters": [
|
|
{ "$ref": "#/components/parameters/BotUuid" },
|
|
{ "$ref": "#/components/parameters/Timestamp" },
|
|
{ "$ref": "#/components/parameters/Signature" },
|
|
{ "$ref": "#/components/parameters/Idempotency" }
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/InboundMessage" } } }
|
|
},
|
|
"responses": {
|
|
"202": {
|
|
"description": "Accepted (queued for the pipeline)",
|
|
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcceptedResponse" } } }
|
|
},
|
|
"400": { "$ref": "#/components/responses/Error" },
|
|
"401": { "$ref": "#/components/responses/Error" },
|
|
"409": { "$ref": "#/components/responses/Error" },
|
|
"413": { "$ref": "#/components/responses/Error" }
|
|
}
|
|
}
|
|
},
|
|
"/bots/{bot_uuid}/sync": {
|
|
"post": {
|
|
"summary": "Push a message and wait for the collapsed reply",
|
|
"description": "Blocking convenience mode. Waits for is_final and returns all reply parts collapsed into one array. Lossy (no sequence/streaming). One in-flight sync per session_id.",
|
|
"parameters": [
|
|
{ "$ref": "#/components/parameters/BotUuid" },
|
|
{ "$ref": "#/components/parameters/Timestamp" },
|
|
{ "$ref": "#/components/parameters/Signature" }
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/InboundMessage" } } }
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "The collapsed reply",
|
|
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/SyncResponse" } } }
|
|
},
|
|
"400": { "$ref": "#/components/responses/Error" },
|
|
"401": { "$ref": "#/components/responses/Error" }
|
|
}
|
|
}
|
|
},
|
|
"/bots/{bot_uuid}/reset": {
|
|
"post": {
|
|
"summary": "Reset a session's conversation",
|
|
"parameters": [
|
|
{ "$ref": "#/components/parameters/BotUuid" },
|
|
{ "$ref": "#/components/parameters/Timestamp" },
|
|
{ "$ref": "#/components/parameters/Signature" }
|
|
],
|
|
"requestBody": {
|
|
"required": true,
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"required": ["session_id"],
|
|
"properties": {
|
|
"session_id": { "type": "string" },
|
|
"session_type": { "type": "string", "enum": ["person", "group"] }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"200": { "description": "Reset done" },
|
|
"400": { "$ref": "#/components/responses/Error" },
|
|
"401": { "$ref": "#/components/responses/Error" }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"components": {
|
|
"parameters": {
|
|
"BotUuid": {
|
|
"name": "bot_uuid", "in": "path", "required": true,
|
|
"schema": { "type": "string", "format": "uuid" }
|
|
},
|
|
"Timestamp": {
|
|
"name": "X-LB-Timestamp", "in": "header", "required": true,
|
|
"description": "Unix seconds; rejected if more than +/-300s from server time.",
|
|
"schema": { "type": "string" }
|
|
},
|
|
"Signature": {
|
|
"name": "X-LB-Signature", "in": "header", "required": true,
|
|
"description": "sha256=<hex> of HMAC-SHA256(secret, \"{timestamp}.\" + raw_body).",
|
|
"schema": { "type": "string" }
|
|
},
|
|
"Idempotency": {
|
|
"name": "X-LB-Idempotency-Key", "in": "header", "required": false,
|
|
"description": "Dedup key; a repeat within the dedup window returns 409.",
|
|
"schema": { "type": "string" }
|
|
}
|
|
},
|
|
"schemas": {
|
|
"Segment": {
|
|
"type": "object",
|
|
"required": ["type"],
|
|
"properties": {
|
|
"type": { "type": "string", "enum": ["Plain", "Image", "Voice", "File", "At", "Quote"] },
|
|
"text": { "type": "string", "description": "For type=Plain." },
|
|
"url": { "type": "string", "description": "For media types." },
|
|
"base64": { "type": "string", "description": "For media types (data URI or raw base64)." }
|
|
}
|
|
},
|
|
"InboundMessage": {
|
|
"type": "object",
|
|
"required": ["session_id", "message"],
|
|
"properties": {
|
|
"session_id": { "type": "string", "description": "Caller-defined; maps 1:1 to a LangBot session." },
|
|
"session_type": { "type": "string", "enum": ["person", "group"], "default": "person" },
|
|
"sender": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": { "type": "string" },
|
|
"name": { "type": "string" },
|
|
"group_name": { "type": "string", "description": "For session_type=group." }
|
|
}
|
|
},
|
|
"message": { "type": "array", "items": { "$ref": "#/components/schemas/Segment" } }
|
|
}
|
|
},
|
|
"AcceptedResponse": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": { "type": "integer", "example": 0 },
|
|
"msg": { "type": "string", "example": "accepted" },
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"session_id": { "type": "string" },
|
|
"accepted_message_id": { "type": "string", "example": "in_01H..." },
|
|
"aggregating": { "type": "boolean" }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"SyncResponse": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": { "type": "integer", "example": 0 },
|
|
"msg": { "type": "string", "example": "ok" },
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"session_id": { "type": "string" },
|
|
"reply_to": { "type": "string" },
|
|
"message": { "type": "array", "items": { "$ref": "#/components/schemas/Segment" } }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"Callback": {
|
|
"type": "object",
|
|
"description": "Delivered by LangBot to your callback_url, one POST per reply part. Signed with the outbound secret.",
|
|
"properties": {
|
|
"session_id": { "type": "string" },
|
|
"reply_to": { "type": "string", "description": "The accepted_message_id this answers." },
|
|
"sequence": { "type": "integer", "description": "1-based ordinal within the turn." },
|
|
"is_final": { "type": "boolean", "description": "True on the last part of the turn." },
|
|
"stream": { "type": "boolean" },
|
|
"message": { "type": "array", "items": { "$ref": "#/components/schemas/Segment" } },
|
|
"timestamp": { "type": "string", "format": "date-time" }
|
|
}
|
|
},
|
|
"ErrorEnvelope": {
|
|
"type": "object",
|
|
"properties": {
|
|
"code": { "type": "integer", "example": 40101 },
|
|
"msg": { "type": "string", "example": "invalid signature: signature_mismatch" },
|
|
"data": { "nullable": true }
|
|
}
|
|
}
|
|
},
|
|
"responses": {
|
|
"Error": {
|
|
"description": "Error envelope",
|
|
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }
|
|
}
|
|
}
|
|
}
|
|
}
|