Skip to content

Latest commit

 

History

History
302 lines (225 loc) · 12.8 KB

README.md

File metadata and controls

302 lines (225 loc) · 12.8 KB

Calaverita Generator

Add an AI agent to a Flutter app, one small step at a time.

The app writes a funny, rhyming "literary skull" (a calaverita) about a friend or coworker from a name, a job and a tone, and draws a sugar-skull portrait to go with it. You start with a plain Flutter app and end with an agent that fills the form, dresses the skull and shows the finished calaverita in the chat, using the CopilotKit Flutter SDK.

Built with Flutter on the web, Very Good Engineering and Feature-First Clean Architecture.

The steps

Each folder is a complete, runnable snapshot. Every step starts from the one before it, so compare two folders to see exactly what changed.

Folder What the app does What you learn
00-foundations The app with local state and no agent. You fill the form, dress the skull and press a button. The architecture: a domain, a data and a presentation package per feature.
01-shared-state A copilot panel appears. You tell it about your friend and it fills the form and dresses the skull. Shared state: AgentStateBuilder, patchSharedState and setSharedState.
02-generative-ui You ask for the calaverita and the agent writes it: a card with the skull appears in the chat and the verse shows under the skull on the stage. Generative UI: a frontend tool registered with CopilotTool whose builder renders a widget.
03-human-in-the-loop You ask for a calaverita without a tone and the agent asks you how to roast, with buttons in the chat. Human in the loop: a tool registered with CopilotHumanInTheLoop, whose run waits for your answer.

Quick start

Install FVM and Node.js 20 or later, then run one command:

node tool/setup.mjs

It installs Flutter 3.47.0 through FVM, checks your machine, asks which step to run and which backend to use, and starts the app in Chrome. The backend is one of:

  • The shared backend: the one we host for the workshop, at https://calaverita-runtime-641689536796.us-central1.run.app/api/copilotkit. Nothing to set up except the workshop password, which you get at the workshop. It asks for it (hidden as you type), or pass --code <password>.
  • Your own runtime: it asks for your Anthropic API key (hidden as you type) and starts the runtime in runtime/ for you.
  • A custom backend: any CopilotKit runtime you have a URL for, for example one made with CopilotKit's own tool (npx copilotkit with-claude).

node tool/setup.mjs --help lists the options, such as --step 02 --backend local to skip the questions. The rest of this page does the same thing by hand.

Set up

You need:

  • FVM. The repository pins Flutter 3.47.0 in .fvmrc, and the SDK needs Dart 3.13 or later.
  • Node.js 20 or later, for the runtime.
  • An Anthropic API key, only to run your own runtime. The shared backend needs none.
  • Chrome.
fvm install

Then check that everything is in place. It runs the same on Windows, macOS and Linux, and it never prints your key:

node tool/doctor.mjs

It checks your machine and the shared backend. Your own runtime is optional, so what it still lacks is listed as information (·), not as a warning.

Note

Windows: turn on Developer Mode (Settings → System → For developers) before fvm install, because FVM uses symlinks. The commands below work in PowerShell and in a macOS or Linux terminal. Where they differ, both are shown.

Note

The CopilotKit Flutter SDK is not on pub.dev yet, so each snapshot gets it from GitHub, pinned to one commit (see dependency_overrides in the pubspec.yaml at the root of each snapshot). Your first pub get needs to reach VGVentures/copilotkit_flutter_sdk.

Run a step

The app talks to a Copilot Runtime, a small server that sits between the app and the model. Steps 01 to 03 use the one in runtime/.

1. Start the runtime (steps 01 and 02) in a first terminal:

cd runtime
npm install

Create your runtime/.env from the example, then open it and set ANTHROPIC_API_KEY=<your key>:

cp .env.example .env             # macOS and Linux
Copy-Item .env.example .env      # Windows PowerShell
npm start

It prints http://localhost:8200/api/copilotkit. Check it from another terminal with node tool/doctor.mjs.

Important

The key lives only in runtime/.env (git-ignored) or in your shell. Never commit it.

To use another model, set COPILOTKIT_MODEL in runtime/.env. The default is anthropic:claude-sonnet-5-5.

2. Run the app in a second terminal. 00-foundations needs no runtime.

cd 01-shared-state            # or 00-foundations, 02-generative-ui, 03-human-in-the-loop
fvm flutter pub get
cd apps/calaverita_app
fvm flutter run -d chrome

What to try

Each step adds one thing the one before it couldn't do. The Write my calaverita button always uses the offline template, so it works in every step. From step 02 the agent can write a verse too.

To see what a step adds, compare its folder with the one before, from the root of the repository (add --stat for the list of files):

git diff HEAD:00-foundations HEAD:01-shared-state -- ':!pubspec.lock' ':!**/test/**'

Steps 01 to 03 end with a challenge for Claude Code, about five minutes each. They change only the app, so they work with the shared backend too.

00 · Foundations (no agent)

  1. Type a name and a job. The button stays disabled until both are filled.
  2. Pick a tone, then put a hat or glasses on the skull and change its color.
  3. Press Write my calaverita.

You should see: the skull change as you click, and a four-line verse under it. There is no chat and no agent yet.

01 · Shared state (the agent fills the form)

Type in the Copilot panel: "Mario is a chef, dramatic please". You can also click a suggestion.

You should see: the name, the job and the tone fill in, the skull gets dressed, and the Shared state box shows the same values. The agent replies in a sentence and tells you to press Write my calaverita.

Then edit a field by hand and ask "who is this for?". The agent answers with your edit, because it reads the same shared state.

Not yet: the agent can't write the verse. Press Write my calaverita to get the template one.

Challenge: add a nickname to the form. It goes in the shared state with the name and the job, so the agent sees it and fills it in when you say "his nickname is El Sazón". Ask Claude Code: "Add an optional nickname field to the calaverita draft and the form, in 01-shared-state."

02 · Generative UI (the agent writes the calaverita)

Type: "Mario is a chef, dramatic please. Write his calaverita!"

You should see: the form fill in and the skull get dressed, then a card with the skull portrait and a four-line verse appear in the chat. The same verse shows under the skull on the stage.

If the name or the job is missing, the agent asks for it instead.

Challenge: give the calaverita a title. Add a title argument to the showCalaverita tool and show it on the card: the agent reads the tool's parameters, so it writes one with no change to its prompt. Ask Claude Code: "Add a short title argument to the showCalaverita tool in 02-generative-ui and show it at the top of the card."

03 · Human in the loop (the agent asks you)

Type a request without a tone: "Write a calaverita for Mario, a chef". You can also click that suggestion.

You should see: the form fill in with Mario and chef, then a Question card in the chat: "How should I roast Mario?" with Playful, Sarcastic and Dramatic. The agent waits. Tap one, and it sets the tone, writes the calaverita and shows it in the chat and on the stage.

Name a tone in your message ("Write a sarcastic calaverita for Mario, a chef") and it doesn't ask. While the question waits, the copilot isn't "working": the Stop button and the typing dots go away, and the message box points to the card.

Challenge: let the agent ask with colors instead of words. Add a pickColor tool like askUser, whose card shows color swatches, then say "let me pick the skull color". The tool's description is enough for the agent to use it. Ask Claude Code: "Add a pickColor human-in-the-loop tool in 03-human-in-the-loop, like askUser, whose card shows four color swatches and returns the hex color, then set the skull color with it."

Point the app at another backend

The runtime URL is set in one place per snapshot: apps/calaverita_app/lib/app/app_config.dart. To use a hosted backend, change AppConfig.defaultRuntimeUrl, or run with --dart-define=RUNTIME_URL=<url>.

The shared backend (https://calaverita-runtime-641689536796.us-central1.run.app/api/copilotkit) also asks for a password. The app sends it, and a random id of the run, as the X-Workshop-Code and X-Workshop-Session headers on every request:

fvm flutter run -d chrome --dart-define=RUNTIME_URL=<url> --dart-define=WORKSHOP_CODE=<password>

--dart-define compiles the password into the build, so a build made with WORKSHOP_CODE contains the password: anyone who has that web build can read it. Run it locally, and never host a build made with the code.

A runtime on your machine ignores both headers, so leave WORKSHOP_CODE out there. To host the shared backend, deploy runtime/ and set WORKSHOP_CODE and ANTHROPIC_API_KEY on the host. The runtime then answers 401 without the password. GET /info and the CORS preflight stay open.

The runtime also logs one line per run with the session id, and answers 429 once a session passes MAX_RUNS_PER_SESSION runs (default 100) or all sessions pass MAX_RUNS_TOTAL (default 5000). Change WORKSHOP_CODE after the event, and set a spend limit on the Anthropic key: --max-instances=1 limits concurrency, not cost.

A hosted backend that has been idle takes a few seconds to wake up, so the first check or message can be slow. To warm it before the event, run the warm_runtime workflow from the Actions tab.

How the code is organized

Each step is a Dart workspace of small packages:

apps/calaverita_app/               composes the features into an app
features/calaverita/
  calaverita_domain/               models and the repository interface
  calaverita_data/                 the offline template that writes the verse
  calaverita_presentation/         the screen, the cubit and the skull portrait
features/assistant/
  assistant_presentation/          the chat (from step 01)
shared/app_ui/                     the design system

A presentation package never imports a data package, and a feature never reaches into another feature's internals. Run fvm flutter analyze and fvm flutter test inside a snapshot, and fvm dart test in the domain and data packages.

Keep building

  • Give the skull more parts: a beard, a different pattern, a bandana. Add the SVG and the variant in SkullPart, and teach the agent in the prompt.
  • Let the agent ask more: "make it fancier" could offer three skull styles with the askUser tool of step 03.
  • Replace the template repository with a call to the agent, so the Write my calaverita button uses the model too.
  • Save the calaverita as an image, or share it.
  • Run your own agent server. extras/custom-server/server is a small custom one that checks every change the agent makes to the form. Start it (npm install, then npm start) and point the app at it with node tool/setup.mjs --backend custom --url http://localhost:4100/api/copilotkit --agent calaverita.

Troubleshooting

  • "The copilot could not answer": the app can't reach the runtime. Check that it is running and that the URL in app_config.dart matches the one it printed.
  • The runtime starts but every message fails: the model or the key is wrong. Send a run straight to the runtime and read the RUN_ERROR event (see the SDK example README).
  • "The copilot could not answer" on the shared backend: the password is missing or wrong (401).
  • pub get can't fetch the SDK: you need access to the SDK repository.

License

MIT. The skull art and the design system come from calaverita_builder, the fonts in shared/app_ui keep their own licenses, and the CopilotKit and Very Good Ventures names and logos belong to their owners.


Leading a team that wants to ship AI features in Flutter? Talk to Very Good Ventures.