#!/usr/bin/env python3 """LangBot HTTP Bot — interactive playground (public, browser-based). This is a REAL end-to-end demo against the RUNNING LangBot instance on this host. It is NOT a mock and NOT an in-process import: every message you type in the browser is signed and POSTed to the live `http_bot` bot at http://127.0.0.1:5300/bots/, and the bot's replies come back to this server's /callback endpoint over real HTTP, then stream to your browser via SSE. What it does on startup: 1. Reads the LangBot API key + the http_bot bot from data/langbot.db. 2. Configures the bot via the LangBot API (PUT /api/v1/platform/bots/): sets inbound_secret + outbound_secret + callback_url to point back here. (LangBot reloads the bot live — no server restart needed.) 3. Serves a chat page on 0.0.0.0: so you can open it from the internet. Run: ./.venv/bin/python examples/http-bot/playground.py Then open: http://:/ """ from __future__ import annotations import asyncio import json import os import sqlite3 import sys REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) sys.path.insert(0, os.path.join(REPO, 'src')) from aiohttp import web # noqa: E402 import aiohttp # noqa: E402 from langbot.pkg.platform.sources import http_bot_signing as sg # noqa: E402 # ---- config ----------------------------------------------------------------- LANGBOT_BASE = 'http://127.0.0.1:5300' DB_PATH = os.path.join(REPO, 'data', 'langbot.db') PUBLIC_IP = os.environ.get('PUBLIC_IP', '127.0.0.1') PORT = int(os.environ.get('PLAYGROUND_PORT', '8920')) SECRET = 'playground-shared-secret' # SSE subscribers: list of asyncio.Queue subscribers: list[asyncio.Queue] = [] def db_lookup() -> tuple[str, str]: """Return (api_key, http_bot_uuid) from the LangBot DB.""" db = sqlite3.connect(DB_PATH) db.row_factory = sqlite3.Row api_key = db.execute('SELECT key FROM api_keys LIMIT 1').fetchone()['key'] bot = db.execute("SELECT uuid FROM bots WHERE adapter='http_bot' LIMIT 1").fetchone() if not bot: raise SystemExit('No http_bot bot found. Create one in the WebUI first.') return api_key, bot['uuid'] async def configure_bot(api_key: str, bot_uuid: str, callback_url: str): """Point the live bot at this playground via the LangBot API. update_bot() runs a raw SQL UPDATE with whatever keys we send, so we send a MINIMAL payload: only adapter_config (built from scratch, not read back — the GET masks secrets). LangBot reloads + reruns the bot live. """ cfg = { 'inbound_secret': SECRET, 'outbound_secret': SECRET, 'callback_url': callback_url, 'signature_required': True, 'default_session_type': 'person', 'callback_timeout': 15, 'callback_max_retries': 3, } async with aiohttp.ClientSession() as s: async with s.put( f'{LANGBOT_BASE}/api/v1/platform/bots/{bot_uuid}', headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}, json={'adapter_config': cfg}, ) as r: txt = await r.text() print(f'[configure] PUT adapter_config -> {r.status} {txt[:200]}') return r.status < 400 async def broadcast(event: dict): for q in list(subscribers): try: q.put_nowait(event) except Exception: pass # ---- HTTP handlers ---------------------------------------------------------- async def index(request: web.Request): return web.Response(text=PAGE, content_type='text/html') async def send(request: web.Request): """Browser -> here -> signed POST -> live LangBot bot.""" body_in = await request.json() session_id = body_in.get('session_id') or 'playground-1' text = body_in.get('text', '') bot_uuid = request.app['bot_uuid'] payload = { 'session_id': session_id, 'sender': {'id': 'browser-user', 'name': 'You'}, 'message': [{'type': 'Plain', 'text': text}], } raw = json.dumps(payload, ensure_ascii=False).encode() ts, sig = sg.sign(SECRET, raw) url = f'{LANGBOT_BASE}/bots/{bot_uuid}' # echo what we send to the browser timeline await broadcast( {'dir': 'out', 'kind': 'request', 'session_id': session_id, 'text': text, 'url': url, 'sig': sig[:24] + '…'} ) async with aiohttp.ClientSession() as s: async with s.post( url, data=raw, headers={ 'Content-Type': 'application/json', sg.HEADER_TIMESTAMP: ts, sg.HEADER_SIGNATURE: sig, }, ) as r: status = r.status try: jr = await r.json() except Exception: jr = {'raw': await r.text()} await broadcast({'dir': 'in', 'kind': 'ack', 'status': status, 'data': jr}) return web.json_response({'status': status, 'data': jr}) async def callback(request: web.Request): """Live LangBot bot -> here. Verify signature, stream to browser.""" raw = await request.read() ok, why = sg.verify(SECRET, raw, request.headers.get(sg.HEADER_TIMESTAMP), request.headers.get(sg.HEADER_SIGNATURE)) data = json.loads(raw) text = ' '.join(c.get('text', '') for c in data.get('message', []) if c.get('type') == 'Plain') await broadcast( { 'dir': 'in', 'kind': 'reply', 'session_id': data.get('session_id'), 'sequence': data.get('sequence'), 'is_final': data.get('is_final'), 'sig_ok': ok, 'sig_why': why, 'text': text, } ) return web.json_response({'ok': True}) async def events(request: web.Request): """SSE stream to the browser.""" resp = web.StreamResponse( headers={ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', } ) await resp.prepare(request) q: asyncio.Queue = asyncio.Queue() subscribers.append(q) try: await resp.write(b': connected\n\n') while True: try: ev = await asyncio.wait_for(q.get(), timeout=15) await resp.write(f'data: {json.dumps(ev, ensure_ascii=False)}\n\n'.encode()) except asyncio.TimeoutError: await resp.write(b': ping\n\n') except (asyncio.CancelledError, ConnectionResetError): pass finally: if q in subscribers: subscribers.remove(q) return resp PAGE = r""" LangBot HTTP Bot · 调试台
HTTP Bot 调试台examples/http-bot 连接中…

对话 · 真实发往运行中的 http_bot

调试信息

入站地址
/bots/<uuid>
签名 HMAC-SHA256 · X-LB-Signature
会话
""" async def main(): api_key, bot_uuid = db_lookup() callback_url = f'http://{PUBLIC_IP}:{PORT}/callback' print(f'[init] http_bot uuid = {bot_uuid}') print(f'[init] callback_url = {callback_url}') ok = await configure_bot(api_key, bot_uuid, callback_url) if not ok: print('[warn] bot config update failed; check the API key / payload shape') app = web.Application() app['bot_uuid'] = bot_uuid app.router.add_get('/', index) app.router.add_post('/send', send) app.router.add_post('/callback', callback) app.router.add_get('/events', events) runner = web.AppRunner(app) await runner.setup() site = web.TCPSite(runner, '0.0.0.0', PORT) await site.start() print(f'\n ▶ 打开: http://{PUBLIC_IP}:{PORT}/\n') while True: await asyncio.sleep(3600) if __name__ == '__main__': asyncio.run(main())