Companion UI to
do_recife_chat_flow, the CrewAI flow this app drives via AMP.
A thin Flask + Server-Sent-Events (SSE) chat UI for a CrewAI AMP flow (DoRecifeChatFlow) that answers questions over the Diário Oficial do Recife. Solarized Light theme, pt-BR interface, vanilla JS frontend with live word-by-word streaming and tool-activity indicators.
Browser ──POST message──► Flask ──POST /kickoff (+webhooks block)──► CrewAI AMP
Browser ◄────SSE──────── Flask ◄──realtime event batches──── AMP (/api/webhook/<channel_id>)
- Each channel is an independent conversation. The most recent AMP execution id is stored per channel (
last_state_id). - On the first message in a channel, no restore id is sent. Every kickoff response's
kickoff_idis saved aslast_state_id; the next message passes it as the top-levelrestoreFromStateIdfield, then the value is overwritten with the new execution id. This chains conversation memory using the supportedrestoreFromStateId(fork) mechanism — see the migration guide. - The kickoff body registers a per-kickoff realtime webhook whose URL encodes the channel id (
{PUBLIC_BASE_URL}/api/webhook/<channel_id>), so events route to the right channel without needingexecution_idmapping. - Subscribed events:
flow_started,flow_finished,llm_stream_chunk,tool_usage_started,tool_usage_finished.llm_stream_chunkevents that carry atool_call(tool-argument fragments) are ignored. The final answer is persisted from theflow_finishedresult. - Watchdog fallback: realtime webhooks have no delivery/ordering guarantee, so the tail of a stream (or even
flow_finished) can be dropped. After each kickoff a thread watches for webhook silence; if no event arrives for ~10s and the execution isn't done, it pollsGET {DEPLOYMENT_URL}/status/{kickoff_id}and finalizes from the authoritative result. The webhookflow_finishedand the watchdog are deduped per kickoff, so whichever lands first wins.
- Python >= 3.13
- uv
- ngrok with a reserved domain (default
do-recife-chat.ngrok.app) so AMP can reach the webhook endpoint - A deployed
DoRecifeChatFlowon CrewAI AMP
-
Copy
.env.exampleto.envand fill it in:Key Purpose DEPLOYMENT_URLCrewAI AMP deployment base URL DEPLOYMENT_KEYAMP API key (bearer) used to call /kickoffWEBHOOK_TOKENShared secret AMP sends back as the webhook bearer token PUBLIC_BASE_URLPublic URL AMP can reach (the ngrok tunnel), e.g. https://do-recife-chat.ngrok.appNGROK_DOMAINReserved ngrok domain used by bin/startPORTLocal Flask port (default 5005) -
Start everything (installs deps, runs Flask, opens the ngrok tunnel):
bin/start
-
Open
http://localhost:5005(orhttps://do-recife-chat.ngrok.app), create a conversation, and start asking questions.
uv sync
uv run python app.pyWithout a public tunnel, AMP cannot deliver webhook events, so streaming/responses will not appear.
The repo is Heroku-ready: the Python buildpack installs from uv.lock (via pyproject.toml + .python-version) and Procfile serves the app with gunicorn. No ngrok needed — the public dyno URL is the webhook target.
heroku create your-app
heroku config:set DEPLOYMENT_URL=... DEPLOYMENT_KEY=... WEBHOOK_TOKEN=...
git push heroku main- Keep
webat a single dyno (heroku ps:scale web=1). SSE subscribers, the status watchdog, and in-flight response state live in-process, so multiple workers/dynos break streaming. TheProcfilealready pins--workers 1and uses threaded workers for concurrent SSE connections. PUBLIC_BASE_URLis optional: when unset, the webhook callback URL is derived per-request from the incoming host (ProxyFixhonors Heroku'sX-Forwarded-Proto/Host), so AMP webhooks just work on the dyno's*.herokuapp.comURL. Set it only behind a custom domain.- SQLite lives on the dyno's ephemeral disk and resets on every restart/redeploy — fine for a demo, not durable storage.
app.py Flask server: pages, channel API, kickoff, SSE, webhook receiver
db/__init__.py SQLite layer (channels + messages, last_state_id chaining)
templates/index.html
static/css/style.css Solarized Light theme
static/js/app.js Frontend: SSE handling, streaming renderer, tool activity
static/img/ logo.png (horizontal), logo_mark.jpg (crest), tool.svg
bin/start Launches Flask + ngrok (local dev)
Procfile Heroku web process (gunicorn, single worker)
app.json Heroku config-var manifest / one-click deploy