chatflowphp / dialogue
Dialogue kernel for PHP bots: declared lenses and goals, memory derived from an event journal, prompts compiled under a byte budget, hosted on chatflowphp/core.
Requires
- php: ^8.2
- ext-mbstring: *
- chatflowphp/core: ^2.0
- psr/clock: ^1.0
- symfony/yaml: ^6.4 || ^7.0 || ^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- phpstan/phpstan: ^2.1
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^10.5
Suggests
- phpunit/phpunit: Required by ChatFlow\Dialogue\Testing\DialogueTester.
Provides
None
Conflicts
None
Replaces
None
README
Real-time prompt and context management for PHP bots. Every turn the kernel derives the conversation's memory and position from a journal, chooses what the model should see, compiles the window under a budget, asks the model once (with tools), extracts the facts and records the turn. Nothing is stored as state: it is derived again from the journal each time, which is what makes it measurable, replayable and cheap where it can be.
You declare what must become known (goals) and what the model sees while getting there (lenses) in one folder — the declaration, the knowledge, the skills, the tools, the reference dialogues — and host it on chatflowphp/core, so Telegram and friends come from the core adapters. The library holds no provider client, no agent loop and no state machine: the model is behind one interface, the tools behind another, and both are plain data at the seam.
Ten minutes
composer require chatflowphp/dialogue mkdir -p booking/knowledge booking/evals
booking/process.yaml:
process: booking slots: name: {type: string, scope: subject, ask: "How should we call you?"} phone: {type: phone, scope: subject, ask: "Which number can we text?"} service: {type: enum, scope: run, options: {haircut: "Haircut", beard: "Beard trim"}} slot: {type: string, scope: run, ask: "When would you like to come in?"} goals: - {id: service_chosen, done_when: confirmed(service), lens: welcome} - {id: time_chosen, done_when: confirmed(slot), requires: [service_chosen], lens: time} - {id: contact_known, done_when: "confirmed(name, phone)", lens: contact} lenses: - id: welcome # scripted: no model call kind: route reply: "Hi! What are you coming for?" choices: - {label: "Haircut", set: {service: haircut}} - {label: "Beard trim", set: {service: beard}} - id: time # guided: the model asks for what is still open kind: collect applies: "date, time, when to come" goal: "Agree on a date and time." - id: contact kind: collect applies: "name, phone number" goal: "Get a name and a phone number for the confirmation." - id: faq # a detour that returns by itself kind: consult applies: "address, parking, hours, prices" knowledge: {retrieve: all, lookup: all} farewell: "Done: {service} on {slot} for {name}. See you!"
Put what the bot should know into booking/knowledge/*.md (headings are addresses), check
the folder, then talk to it:
vendor/bin/dialogue lint booking # the declaration against its knowledge; what each lens costs vendor/bin/dialogue evals booking # the reference dialogues in booking/evals, scripted
$environment = Environment::load('booking'); $kernel = new Kernel($environment->process, $environment->corpus, $model, $tools); // your ModelInterface, your ToolRegistry Dialogue::install($application, $kernel, $streams); // a chatflowphp/core Application $application->onCommand('start', fn(Context $ctx) => $ctx->enter(DialogueScene::ID));
$model = new ScriptedModel(); $bot = new DialogueTester(new Kernel($environment->process, $environment->corpus, $model, $tools)); $bot->start()->assertReplied('Hi! What are you coming for?')->assertModelCalls(0); $model->reply('Tuesday 15:00 or Friday 18:00 are free. Which suits you?'); $bot->press('Haircut')->assertGoal('time_chosen')->assertLens('time'); $model->extract(['slots' => ['slot' => ['value' => 'Friday 18:00', 'confirmed' => true]]]); $bot->say('Friday at six')->assertGoal('contact_known');
The complete example — an environment folder with tools, a corpus, a skill, a console chat
and its eval tests — is examples/Barbershop; the walk-through is
getting started.
Where it sits
| chatflowphp/dialogue | an agent framework (Symfony AI Agent, LangGraph) | a flow builder (typebot) | |
|---|---|---|---|
| where the state is | nowhere: derived from the journal every turn | in the graph/agent runtime, per node or thread | in the flow engine, a pointer to the current block |
| who holds the loop | the host, one turn at a time; the kernel yields steps (a model request, a round of tools) and folds the answers | the agent, until it decides to stop | the engine, block by block |
| what moves the conversation | goals reached (done_when over memory); a goal that stops holding reopens by itself |
the model's choices | edges drawn by the author |
| what the model sees | a compiled window: task, knowledge slice, retrieved sections, what is known and open, hints, the transcript tail — under a budget, in two tiers for the cache | whatever the loop accumulates | a prompt per block |
| what is measurable | the turn record: ladder of determinism, cost per lens, cache share, lookup rate, slots by source and corrections, drop-off; evals, replay, shadow | traces | completion funnels |
| the model and the tools | interfaces with data at the seam; any provider, any MCP or PHP tool, wired by the application | the framework's own | the builder's own |
What you get
- Goals, not steps.
done_whenpredicates over memory decide where the conversation is. Order,requires,applies_if,reviewandstrictshape the funnel; nothing else moves it. - Lenses, not prompts. Each lens is a context: task, knowledge slice, slots, tools, buttons,
voice.
scriptedlenses never call the model;guidedones collect;freeones consult. - Memory with provenance. Every value knows who set it, when, and what it replaced. Buttons and tools confirm for free; the model's extractions confirm by rule.
- A journal as the truth. Events on a core stream; memory, position and the chosen lens are folded from it. Replay old dialogues under a new declaration, run in shadow next to a live bot.
- Prompts as structure. Baseline, task, knowledge, what is known, what is open, where the conversation is, hints — packed under a byte budget with the transcript tail last.
- Tools that fit the tick. Read tools run during generation; write tools run after commit,
at least once, with an idempotency key.
bindhides parameters from the model,savereads results back into slots. - Cheap where it can be. Buttons, typed button labels, scripted replies, expression flags and silence rules cost nothing; a model is asked only for what needs one, and the window's stable tier hits the provider's cache.
- Replies checked before they are sent. Guards — a pattern the reply must or must not match, a length — are declared next to the lens; a failing reply is asked for again with what was wrong, what still fails is sent or replaced by the fallback, and every hit is in the turn record and the analytics.
- One folder per process. The declaration,
knowledge/,skills/in the Claude Code format,tools.yaml,.mcp.json,evals/; other folders join throughuses:. - The model measured, not trusted. Reference dialogues run scripted for free and live on your model; live, a judge model rates every reply against criteria you declare next to the evals — one question, no jargon, leads to the body — binary, with a reason, per criterion and per run, so a change of prompt or of model is compared, not guessed. A simulated client plays the user from a case description where a script would run out; the judge is calibrated against replies a person rated.
- A console.
dialogue lint,evals(scripted or live, judged and saved, or only what a change affects),judge(a saved run judged again, no actor call; recorded journals judged run by run; calibration against a person's ratings),compare(two runs of one eval, reply by reply),replay,analytics(the ladder, the cost, and how runs end),journal,case(a regression from a real conversation, without its text),coverage(what no run has touched of the declaration),digest(the week on one page: health, invariants, the judge already written, the runs to open — no model called, no one's text carried),prune(the words out of old journals, the numbers kept),explain(one turn read back: position, ranking, assessment, the window block by block and its proportion, the reply),findings(the register of what went wrong in real conversations, cross-checked against the suite).
Installation
composer require chatflowphp/dialogue
Requires PHP 8.2+, chatflowphp/core 2.x, symfony/yaml, psr/clock and ext-mbstring;
nothing else. You bring the
model: implement ModelInterface for your provider (the example carries a reference client
to copy for any OpenAI-compatible chat/completions endpoint) — or wrap a provider library
such as Symfony AI Platform in fifty lines. Tools
are ToolInterface objects the application registers, from PHP classes or an MCP client.
vendor/bin/dialogue is the console.
Documentation
- Getting started — a folder, a declaration, a model, a host, in ten minutes
- Playbook — the order of work and the design rules learned on a reference project, with the numbers
- Architecture — what the library is and is not, the entities, the seam, the turn as a fold
- The environment folder — knowledge, skills, tools, MCP servers, evals,
uses: - Declaration — the schema: process, lenses, goals, slots, tools, buttons
- A turn — assess, fold, position, score, reply; the weights and why
- Memory and the journal — events, folding, scopes, snapshots
- Knowledge — corpus addressing, lookup, examples, budgets
- Tools and models — read and write tools, binding, the model contract
- Hosting on core — the scene, two-tick turns, timers, buttons
- Testing —
DialogueTester,ScriptedModel, host tests - Texts — the kinds of text in a declaration: source, proof, the checklist for a change
- Evals — reference dialogues as YAML, run scripted in the suite and live by hand
- Replay — old journals through a new declaration, differences turn by turn
- Analytics — the journal reduced to numbers: ladder, cost, slots, routing, drop-off, how runs end
Contributing
Run composer check before a pull request; see CONTRIBUTING.md for the
rules that keep the kernel pure. Releases are listed in CHANGELOG.md.
License
MIT. See LICENSE.