Skip to main content
Version: Target · roadmap

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:).

examples/cible/product-medium.yaml
# 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 happenswho decidesresult
"There are 8 of us at the office this lunchtime"seek_guidance: eligible group-lunch and take-meal-order; model 0.91 / 0.07ENFORCE: group-lunch opens, logged
the agent adds the delivery firstthe 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 CFApermission, daily budgetreserved
3 nemsthe nems kind's quantity: {min: 5}refused: at least five nems
a dish without a side dishthe required line field variantereserved; the submit waits for the answer for that line
submit, "8 covers"the submit's form, extra_linesone placeOrder: the dishes, each with its variant_id, the drink, and the line couverts-avec × 8
"deliver to Almadies"per_profile: address not registeredrefused for delivery; pickup remains possible
cancel yesterday's orderagent-entreprise lacks cuisine.cancelOrderrefused: reserved for people
submit at 25,500 > 15,000submitted_by: user.approvera human: the approver the firm set on the sub-identity
the model judges the order unusualcommande-inhabituellea human (already the case; it is noted in the log)
the model is unavailableon_unavailable: allowthe 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).