A conversational chatbot built with CrewAI Flows, deployed to CrewAI AMP, with a Discord-style web UI driven by Flask and vanilla JS.
- User types a message in the browser.
- Flask saves it to SQLite, returns
202, and fires a backgroundPOST /kickoffto AMP with the channel'sconversation_idas the flow stateid+user_message. - AMP runs the
ConversationalFlow—@persist()restores the state (including priormessages) using the flow'sid, appends the new user message, and hands off to theMessageClassifierAgent. - The classifier categorizes the message and either responds directly (for simple queries) or routes the flow to the
ImageCreationCreworInternetSearchCrew. - The active agent reasons over the conversation and calls tools (
SendMessageToUser,NanoBananaImageGeneration, etc.). Each tool emits events through theConversationalEventBus. - The
ConversationalEventListenercatches those events (LLMStreamChunkEvent,LLMThinkingChunkEvent,ToolUsageStartedEvent,ToolUsageFinishedEvent,ToolUsageErrorEvent,ImageGenerated,FlowFinishedEvent), stamps them withconversation_id, and dispatches them towebhook.sitevia theDispatcherclient. webhook.siteXHR-redirects each event to the Flask server's/api/webhookendpoint.- Flask persists each event immediately as it arrives (thinking chunks via upsert, tool usage and messages as individual rows) and broadcasts all events to connected browsers via SSE.
ConversationalFlow is a CrewAI Flow[ConversationalState] with two steps:
| Step | Decorator | What it does |
|---|---|---|
load_initial_context |
@start() |
Registers the event listener and appends the new user message to state. |
classify_message |
@router(load_initial_context) |
Runs MessageClassifierAgent to determine intent ("SIMPLE", "IMAGE_CREATION_UPDATE", "INTERNET_SEARCH", "CREWAI_DOCS"). |
handle_image_creation |
@listen("IMAGE_CREATION_UPDATE") |
Instantiates and executes the ImageCreationCrew. |
handle_internet_search |
@listen("INTERNET_SEARCH") |
Instantiates and executes the InternetSearchCrew. |
handle_crewai_docs |
@listen("CREWAI_DOCS") |
Instantiates and executes the CrewaiDocsCrew. |
handle_simple_message |
@listen("SIMPLE") |
No-op, the classifier handles direct responses for simple queries. |
finalize |
@listen(or_(handle_simple_message, handle_image_creation, handle_internet_search, handle_crewai_docs)) |
Returns the serialized state to finish the flow. |
The flow is decorated with @persist(), which automatically saves and restores ConversationalState across kickoffs using the flow state's id (a UUID passed by the UI as the conversation identifier). State fields: user_message and messages (the full conversation history, accumulated over time).
agents/message_classifier_agent.py
- Agent:
Message Classifierpowered bygemini/gemini-3-flash-preview. - Skills:
skills/(discoversuser-communicationand other relevant skills). - Role: Triage the request, emit a quick "routing" acknowledgement, and return a
ClassificationResultwhich controls the Flow router. If the request is simple, it answers the user directly. Questions about the CrewAI framework are always routed toCREWAI_DOCS.
crews/image_creation_crew.py
- Agent:
CrewAI Image Creation Assistantpowered bygemini/gemini-3.1-pro-preview, equipped with image tools. - Skills:
skills/user-communication,skills/image-generation. - Task: Interpret the request, generate/edit images via Gemini, and communicate progress using the
SendMessageToUserTool.
crews/internet_search_crew.py
- Agent:
CrewAI Internet Research Assistantpowered bygemini/gemini-3.1-pro-preview, equipped with internet search/scraping tools. - Skills:
skills/user-communication,skills/internet-searching. - Task: Formulate search queries, evaluate findings, and synthesize clear answers with sources using the
SendMessageToUserTool.
crews/crewai_docs_crew.py
- Agent:
CrewAI Documentation Expertpowered bygemini/gemini-3.1-pro-preview, equipped with a MongoDB vector search tool over the official CrewAI docs. - Skills:
skills/crewai-docs. - Task: Search the docs vector database for relevant sections and synthesize accurate, detailed answers with code examples when appropriate.
| Tool | Description |
|---|---|
SendMessageToUserTool |
Appends the agent's reply to flow state. Content reaches the user via llm_stream_chunk events and is persisted on tool_usage_finished. |
NanoBananaImageGenerationTool |
Generates an image via Gemini (gemini-3.1-flash-image-preview), emits an ImageGenerated (base64) event. |
NanoBananaImageEditingTool |
Edits an existing image via Gemini with a text prompt, same event pattern as generation. |
MongoDBVectorSearchTool |
Searches the CrewAI documentation stored in a MongoDB Atlas vector index. |
SerperDevTool / ScrapeWebsiteTool |
Web search and scraping (from crewai_tools). |
The event system is the bridge between the CrewAI agent running in AMP and the external UI.
Custom event types (events/types/):
ImageGenerated— carriesresult.image(base64-encoded PNG).
ConversationalEventBus (events/conversational_event_bus.py):
- Wraps the CrewAI event bus. Tools call
append_message()/emit_image_generated()on it. - Appends messages to flow state (persisted automatically by
@persist()).
ConversationalEventListener (events/listeners/conversational_event_listener.py):
- A
BaseEventListenerthat subscribes toImageGenerated,LLMStreamChunkEvent,LLMThinkingChunkEvent,FlowFinishedEvent,ToolUsageStartedEvent,ToolUsageFinishedEvent, andToolUsageErrorEvent. - Stamps each event with
source_fingerprintandfingerprint_metadata.conversation_id. - Dispatches every event to the external webhook via the
Dispatcherclient.
Dispatcher (events/clients/dispatcher.py):
- Simple HTTP POST client that sends JSON event payloads to
DISPATCHER_URL(webhook.site) with bearer auth.
Flask app with a Discord-inspired dark theme.
- Channels: each channel maps to a
conversation_id(UUID generated on creation). Multiple concurrent conversations. - SQLite (
db/chatbot.db): stores channels and messages withevent_id-based deduplication. Thinking chunks are upserted incrementally; tool usage and assistant messages are persisted immediately as individual rows. - SSE: each browser tab subscribes to
/api/channels/<id>/eventsfor real-time updates. - Webhook receiver (
POST /api/webhook): receives events forwarded from webhook.site, matches them to channels viafingerprint_metadata.conversation_id, persists each event immediately to SQLite, and broadcasts to connected browsers via SSE. - Frontend (
static/js/app.js): renders messages with markdown support (viamarked.js), displays base64 images inline, shows agent thinking as faded inline messages (togglable via "Show Thoughts"), displays tool usage with inline wrench icons ("Using X..." → "Used X for Ns"), and shows a "CrewAI is executing your request..." banner during active flows.
Requirements: Python >= 3.10, < 3.14 · uv · ngrok (with crewai-chatbot.ngrok.io domain)
- Copy
.env.exampleto.envand fill in the keys:
| Key | Purpose |
|---|---|
GEMINI_API_KEY |
LLM (classifier, crews) and image generation / editing |
SERPER_API_KEY |
Web search via Serper |
MONGODB_CONNECTION_STRING |
MongoDB Atlas connection string for docs vector search |
MONGODB_DATABASE_NAME |
MongoDB database name |
MONGODB_COLLECTION_NAME |
MongoDB collection name |
DISPATCHER_URL |
Webhook.site endpoint for event forwarding |
DISPATCHER_KEY |
Bearer token for the dispatcher |
DEPLOYMENT_URL |
CrewAI AMP deployment URL |
DEPLOYMENT_KEY |
CrewAI AMP API key |
- Deploy the flow to CrewAI AMP:
crewai deploy- Configure
webhook.siteto XHR-redirect incoming events tohttps://crewai-chatbot.ngrok.io/api/webhook. - Start the UI:
bin/startThis installs dependencies, wakes up AMP, starts Flask on port 5005, and opens an ngrok tunnel.
5. Open http://localhost:5005 (or https://crewai-chatbot.ngrok.io), create a channel, and start chatting.

