Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

Diário Oficial do Recife - Busca via Chat

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.

How it works

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_id is saved as last_state_id; the next message passes it as the top-level restoreFromStateId field, then the value is overwritten with the new execution id. This chains conversation memory using the supported restoreFromStateId (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 needing execution_id mapping.
  • Subscribed events: flow_started, flow_finished, llm_stream_chunk, tool_usage_started, tool_usage_finished. llm_stream_chunk events that carry a tool_call (tool-argument fragments) are ignored. The final answer is persisted from the flow_finished result.
  • 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 polls GET {DEPLOYMENT_URL}/status/{kickoff_id} and finalizes from the authoritative result. The webhook flow_finished and the watchdog are deduped per kickoff, so whichever lands first wins.

Requirements

  • Python >= 3.13
  • uv
  • ngrok with a reserved domain (default do-recife-chat.ngrok.app) so AMP can reach the webhook endpoint
  • A deployed DoRecifeChatFlow on CrewAI AMP

Setup

  1. Copy .env.example to .env and fill it in:

    Key Purpose
    DEPLOYMENT_URL CrewAI AMP deployment base URL
    DEPLOYMENT_KEY AMP API key (bearer) used to call /kickoff
    WEBHOOK_TOKEN Shared secret AMP sends back as the webhook bearer token
    PUBLIC_BASE_URL Public URL AMP can reach (the ngrok tunnel), e.g. https://do-recife-chat.ngrok.app
    NGROK_DOMAIN Reserved ngrok domain used by bin/start
    PORT Local Flask port (default 5005)
  2. Start everything (installs deps, runs Flask, opens the ngrok tunnel):

    bin/start
  3. Open http://localhost:5005 (or https://do-recife-chat.ngrok.app), create a conversation, and start asking questions.

Running Flask only (no tunnel)

uv sync
uv run python app.py

Without a public tunnel, AMP cannot deliver webhook events, so streaming/responses will not appear.

Deploy to Heroku

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 web at 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. The Procfile already pins --workers 1 and uses threaded workers for concurrent SSE connections.
  • PUBLIC_BASE_URL is optional: when unset, the webhook callback URL is derived per-request from the incoming host (ProxyFix honors Heroku's X-Forwarded-Proto/Host), so AMP webhooks just work on the dyno's *.herokuapp.com URL. 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.

Project layout

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

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages