Product · medium — a restaurant that also serves companies
The case. Customers order as before. In addition, a firm's ChatGPT agent orders the team's lunch, under a sub-identity created by the firm: 15,000 F CFA without approval, from 11 am to 2 pm, to the office address, with no right to cancel.
What the example shows. An agent on two portals (the web and WhatsApp, where the customer proves
their number), profiles, kinds recognised by a condition, a choice of method
in a playbook, activation by intent, an output filter, a daily budget, a
smart policy that can only ask for a human, who submits, and a refusal sentence rewritten by the
restaurant. And the order itself: one placeOrder call, declared on the playbook, that sends the
dishes with their side dish (a line field), the drink and the cutlery (added lines), in the shape the kitchen
expects (line:).
# Product · medium — CIBLE. The restaurant serves people AND companies' agents.
# Profiles (who), kinds (what), a playbook as tasks with a choice, activation by intent,
# who submits, a daily ceiling, and one smart policy that can only ask for a human.
# The order is ONE call on the playbook (dishes + drink + cutlery in one placeOrder), a line field per dish.
station:
id: teranga
kind: product
name: Teranga Kitchen
guidance: {enforce: 0.85, suggest: 0.5, margin: 0.05, max_mode: enforce}
delegation: # what a client may grant its agents (ADR-029 §11)
restrictions: [max_amount, hours, addresses, deny_skills, confirm_above]
identity: # the identity providers this station accepts (ADR-031)
providers:
phone: {mechanism: otp, verifies: phone, via: convoy, channel: sms} # a code by SMS, sent by Convoy's service
global: # applies to every field of its type, every skill
language: fr
currency: XOF # every `type: price` field
locale: fr-SN
timezone: Africa/Dakar
texts: # Convoy's refusal sentences, in the restaurant's words
fr:
step.waits: "{task} vient après {before} : choisissons d'abord {before}."
apis:
- name: cuisine
openapi_url: https://api.teranga.example.sn/openapi.json
base_url: https://api.teranga.example.sn
auth: {type: api_key, key_name: X-API-Key, key_location: header, key_env: TERANGA_KEY}
operations:
- {id: getMenu, access: read}
- {id: getDrinks, access: read}
- {id: getDeliveryOptions, access: read}
- {id: checkDeliverable, access: read}
- {id: getDeliveryPrice, access: read}
- {id: placeOrder, access: write, risk: medium, reversible: cancelOrder}
- {id: createDelivery, access: write, risk: medium, reversible: false}
- {id: listOrders, access: read}
- {id: cancelOrder, access: write, risk: medium}
profiles:
anonyme:
when: "identity.authenticated == false"
grants: [cuisine.getMenu, cuisine.getDrinks, cuisine.placeOrder, cuisine.getDeliveryOptions, cuisine.checkDeliverable,
cuisine.getDeliveryPrice, cuisine.createDelivery]
client:
when: "identity.authenticated and identity.type == 'person'"
grants: [cuisine.getMenu, cuisine.getDrinks, cuisine.placeOrder, cuisine.getDeliveryOptions, cuisine.checkDeliverable,
cuisine.getDeliveryPrice, cuisine.createDelivery,
cuisine.listOrders, cuisine.cancelOrder]
agent-entreprise: # no cancelOrder: cancelling stays a person's act
when: "identity.type == 'agent' and identity.org != null"
grants: [cuisine.getMenu, cuisine.getDrinks, cuisine.placeOrder, cuisine.getDeliveryOptions, cuisine.checkDeliverable,
cuisine.getDeliveryPrice, cuisine.createDelivery]
policies:
commande-inhabituelle:
type: smart
question: "ce volume ou ce contenu est-il inhabituel pour ce compte ?"
effect: needs-human
threshold: 0.85
on_unavailable: allow
depense-du-jour: # a budget is a rule policy: a counter over several actions
type: rule
budget: {sum: "$selection.total", per: day, scope: identity, max: 60000}
skills:
- name: takeMealSelection
cognitive: {objective: "montrer la carte pour choisir les plats", diagnose: [l'envie, les restrictions]}
invoke: {method: getMenu}
kinds:
dish:
when: "$fields.category != 'nems'"
fields: # filled by the person, line by line; sent by the order's `line:`
variante: {type: id, required: true, label: "Accompagnement"}
nems:
when: "$fields.category == 'nems'"
select: {quantity: {min: 5}}
present:
fields:
name: {from: name, type: text, role: title}
price: {from: price, type: price, role: billed}
image: {from: image_url, type: image}
category: {from: category, type: text} # what each kind's `when` reads
render: {component: carousel} # no submit: the order is the playbook's call
- name: takeDrinkSelection
cognitive: {objective: "proposer une boisson pour accompagner les plats"}
invoke: {method: getDrinks}
kinds: {drink: {}} # its rows are items (CFG-73 leaves skills without kinds open)
present:
fields:
name: {from: name, type: text, role: title}
price: {from: price, type: price, role: billed}
select: {max_items: 1}
render: {component: carousel} # no submit either: same order
- name: takeDeliveryOption
cognitive: {objective: "proposer le retrait ou la livraison"}
params:
order_id: {type: id, set_by: [result]}
invoke: {method: getDeliveryOptions}
kinds:
pickup:
when: "$fields.type == 'pickup'"
submit: [{name: collect, mode: self}]
delivery:
when: "$fields.type == 'delivery'"
select:
guards:
- form:
address: {type: text, required: true, label: "Adresse de livraison"}
phone: {type: phone, required: true, label: "Téléphone", identifies: true} # declared; `phone` above verifies it
- check: checkDeliverable
- update: {price: getDeliveryPrice}
submit:
- name: dispatch
tool: createDelivery
args:
body.address: "$item.fields.address"
body.order_id: "$params.order_id"
body.fee: "$item.price"
per_profile:
agent-entreprise: # an agent delivers only to an address on file
select: {guards: [{name: adresse-enregistree, require: "$item.fields.address in identity.addresses"}]}
present:
fields:
name: {from: name, type: text, role: title}
type: {from: type, type: text} # what each kind's `when` reads
output_filters: {reduce: {address: district}} # the model sees "Plateau, Dakar"
render: {component: cards}
- name: annuler
cognitive: {objective: "annuler une commande pas encore préparée"}
params: {customer: {type: id, set_by: [identity]}} # never the model: only the person's own orders
invoke: {method: listOrders, args: {customer: "$params.customer"}}
present: {fields: {name: {from: label, type: text, role: title}}}
select: {max_items: 1}
submit: [{name: cancel, tool: cancelOrder, args: {body.order_id: "$item.ref"}}]
render: {component: cards}
playbooks:
- name: take-meal-order
goal: "le repas est commandé, retiré ou livré"
activation:
intent: "commander un repas"
tasks:
plats: {skill: takeMealSelection, label: "Vos plats"}
boisson: {skill: takeDrinkSelection, label: "Votre boisson", after: [plats], required: false}
remise:
label: "Retrait ou livraison"
after: [plats]
depends_on: {plats: {on_loss: clear, on_change: revalidate}} # the fee was quoted on these dishes
one_of:
choose: customer
methods:
retrait: {skill: takeDeliveryOption, kind: pickup, label: "Retrait sur place"}
livraison:
skill: takeDeliveryOption
kind: delivery
label: "Livraison"
params: {order_id: "$submitted[order].order_id"} # a call of this playbook
submit: # ONE order for the lines of several tasks, declared once
- &order
name: order
tool: placeOrder
args:
body.items: "$selection[task=plats,boisson]"
body.idempotency_key: "$action.key"
line: # what ONE line looks like to the kitchen
product_id: "$line.ref"
quantity: "$line.quantity"
variant_id: "$line.fields.variante" # left out of a nems or drink line: no `variante` there
extra_lines: # sent with the order, never chosen: cutlery, a catalogue row
- {ref: couverts-avec, quantity: "$submit.couverts", when: "$submit.couverts > 0"}
- {ref: couverts-sans, when: "$submit.couverts == 0"}
output: {order_id: {from: "$output.id", type: id}}
submit_guard:
form: {couverts: {type: quantity, required: true, label: "Combien de couverts ?"}}
confirm: true
- name: group-lunch
goal: "le repas du groupe est commandé et confirmé"
activation:
when: "'agent-entreprise' in profiles"
intent: "commander pour plusieurs personnes"
tasks:
plats: {skill: takeMealSelection, label: "Vos plats", guards: [{policy: depense-du-jour}]}
boisson: {skill: takeDrinkSelection, label: "Votre boisson", after: [plats], required: false}
remise:
label: "Retrait ou livraison"
after: [plats]
one_of:
methods: # choose: customer, the default
retrait: {skill: takeDeliveryOption, kind: pickup, label: "Retrait sur place"}
livraison:
skill: takeDeliveryOption
kind: delivery
label: "Livraison au bureau"
params: {order_id: "$submitted[order].order_id"}
submit: [*order] # another journey, another order: the same call, by anchor
submit_guard:
form: {couverts: {type: quantity, required: true, label: "Combien de couverts ?"}}
guards: [{policy: commande-inhabituelle}]
submitted_by:
- {who: user.approver, when: "total > identity.limits.max_amount"} # set by the company on the sub-identity
- {who: owner}
agent:
name: Teranga
persona: "un hôte chaleureux qui aide à composer le repas"
tone: friendly
portals:
web:
widget: {origins: [https://teranga.example.sn], theme: {primary: "#1B7A4B"}}
pwa: {name: "Teranga & Moi"}
whatsapp:
number: "+221 77 000 00 00"
identity: {auth: phone} # who the person is, as this portal proves it
What happens
| what happens | who decides | result |
|---|---|---|
| "There are 8 of us at the office this lunchtime" | seek_guidance: eligible group-lunch and take-meal-order; model 0.91 / 0.07 | ENFORCE: group-lunch opens, logged |
| the agent adds the delivery first | the playbook's order, said in the restaurant's sentence (texts) | refused: « Retrait ou livraison vient après Vos plats : choisissons d'abord Vos plats. » |
| 8 dishes, 25,500 F CFA | permission, daily budget | reserved |
| 3 nems | the nems kind's quantity: {min: 5} | refused: at least five nems |
| a dish without a side dish | the required line field variante | reserved; the submit waits for the answer for that line |
| submit, "8 covers" | the submit's form, extra_lines | one placeOrder: the dishes, each with its variant_id, the drink, and the line couverts-avec × 8 |
| "deliver to Almadies" | per_profile: address not registered | refused for delivery; pickup remains possible |
| cancel yesterday's order | agent-entreprise lacks cuisine.cancelOrder | refused: reserved for people |
| submit at 25,500 > 15,000 | submitted_by: user.approver | a human: the approver the firm set on the sub-identity |
| the model judges the order unusual | commande-inhabituelle | a human (already the case; it is noted in the log) |
| the model is unavailable | on_unavailable: allow | the policy does not apply; all deterministic rules apply |
Key points
- An order is declared once, on the playbook, next to its
submit_guard: it takes its lines by task ($selection[task=plats,boisson]). Delivery, an action of a single skill, keeps its call on its kind. - The kind says what (pickup or delivery), the profile says who (customer or company agent). Both specialise the same points.
- The agent sees "Plateau, Dakar", never the full address (
output_filters). - The sub-identity's limits are set by the firm on the tenant's page; the station
only declares what may be limited (
station.delegation.restrictions).