A Flutter chat app backed by a FastAPI service that runs Claude in a tool-use loop. The app never holds the Anthropic API key — it only ever talks to the backend over HTTP. The backend owns the key, the tool loop, and the validation and audit hooks around every tool call.
- The Flutter app posts
{ user_id, conversation_id, messages }toPOST /chat.conversation_idis empty on the first turn. agent/core.pyrunsrun_conversation(): it loads the server-side state for that conversation (or starts a new one), then calls the Anthropic API with the conversation, the system prompt, and the five tool schemas, up to 10 iterations.- When Claude asks for a tool, the input is validated against a Pydantic
model, passed through
pre_hook(auth check, refund policy, identity verification), run, then passed throughpost_hook(marks the customer verified, writes refund audit records). - Tool errors are classified
retryableor not. Two non-retryable errors in a row (validation failures, blocked calls) end the conversation early with a message telling the user to escalate, instead of burning iterations. - The loop ends when Claude replies with text instead of a tool call, and
the reply, the full message history, and the
conversation_idgo back to the app, which sends the id on every later turn.
dash_support/
pubspec.yaml Dart workspace root; the `melos:` key holds the dev scripts
.vscode/launch.json Chrome and mobile run configs
apps/
dash/ the Flutter app
lib/
main.dart
bootstrap.dart
app/
chat/
bloc/ ChatBloc: events in, states out
view/
packages/
chat_repository/ HTTP client + models, talks to the backend
backend/
requirements.txt
contracts/
chat.openapi.yaml the /chat request and response shape
agent/
core.py the Claude tool-use loop, retry/escalation logic
hooks.py pre_hook (auth, policy), post_hook (audit, state)
tools.py get_customer, lookup_order, get_orders,
process_refund, escalate_to_human
api/
main.py FastAPI app, CORS
routes/
health.py GET /health
chat.py POST /chat
evals/
run_evals.py behavioral evals against the live agent
compare_models.py same evals, run across multiple models
report.py PDF report generation
cd backend
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp ../.env.example .env # then put your real key in this file
uvicorn api.main:app --reload --port 8000Check it: curl http://localhost:8000/health
dart pub global activate melos
melos bootstrap
cd apps/dash
flutter run -d chrome --dart-define=BASE_URL=http://localhost:8000Or open the repo in VS Code and pick the "chrome" run config from
.vscode/launch.json.
The base URL comes from a dart-define named BASE_URL. It defaults to
http://10.0.2.2:8000 in main.dart, which is
how the Android emulator reaches the host machine. Chrome, desktop, and the
iOS simulator should pass http://localhost:8000 explicitly, as above.
The backend reads its config from a .env file in backend/, loaded by
python-dotenv in agent/core.py. Shell exports
take priority over .env, so setting the variable in your shell overrides
the file.
Copy the template and fill in your real key:
cp .env.example backend/.envANTHROPIC_API_KEY— required, no default. From the Anthropic console. Without it, every request to/chatfails.CLAUDE_MODEL— optional, defaults toclaude-sonnet-4-6. Overrides which model the agent calls.
backend/.env is gitignored — never commit it. .env.example (at the repo
root) is the checked-in template and should only ever hold placeholder
values.
melos run evals and melos run evals:compare also call the live Anthropic
API, so they need ANTHROPIC_API_KEY set the same way.
All five tools live in agent/tools.py, each
backed by a Pydantic input model and an in-memory mock store (swap the
marked spots for a real database, payment provider, and help desk).
| Tool | Purpose |
|---|---|
get_customer |
Look up a customer by id or email. Verifying identity here unlocks refunds. |
lookup_order |
Fetch one order, optionally confirming it belongs to a given customer. |
get_orders |
List a customer's recent orders, newest first, up to max_items. |
process_refund |
Refund an order. Rejects orders that don't exist, are still in transit, aren't refundable, or where the amount exceeds the order total. |
escalate_to_human |
Create a help desk ticket and hand off the conversation. |
agent/hooks.py wraps every tool call:
pre_hookblocks unauthenticated requests, requiresget_customer+lookup_orderbefore anyprocess_refund, and caps refunds atREFUND_LIMIT(200.0), directing anything larger toescalate_to_human.post_hookmarks the conversation's customer as verified onceget_customersucceeds, and appends successful refunds to an in-memory audit trail.
The state the hooks read (ctx, holding customer_verified) lives on the
server in _SESSIONS in agent/core.py, keyed by
conversation_id. That way verification from one turn still counts on the
next, and the client can't forge it by editing the message history. An
unknown id, or one that belongs to a different user_id, starts a fresh
conversation. _SESSIONS is in memory, so restarting the backend clears it.
The system prompt in core.py scopes the agent to Dash order/refund/account
topics, tells it to verify identity before refunding, and to escalate
immediately when a customer expresses frustration or asks for a human —
regardless of phrasing.
It also covers the refund edge cases. When a customer reports a technical
problem, the agent asks whether they want a repair or a refund before doing
anything else, and a repair goes to escalate_to_human. A refund blocked
because the order is in transit gets a "wait for delivery" answer, and one
blocked by the limit gets escalated.
Defined under the melos: key in pubspec.yaml.
| Script | What it does |
|---|---|
melos run app |
Runs the Flutter app against the local backend |
melos run backend |
Starts FastAPI on port 8000 (needs the venv active) |
melos run analyze |
dart analyze across the workspace |
melos run analyze:backend |
Lints the Python backend with ruff |
melos run test |
Runs Dart and Flutter tests |
melos run test:backend |
Runs the Python tests with pytest |
melos run check |
Runs all four analyze/test scripts above |
melos run evals |
Behavioral evals against the live agent, builds a PDF report (real API calls, not part of check) |
melos run evals:compare |
Evals across multiple models with a comparison report (real API calls, not part of check) |
