Skip to main content
Version: Current

Product · medium — a restaurant

The case. You choose dishes, then pickup at the counter or delivery. Delivery asks for an address, checks that delivery is possible, computes a price. Pickup asks for nothing.

What the example shows. Two skills with kinds, a dependency between them, two kinds recognised by their when, one handled separately (kinds.pickup), a playbook whose second task is a choice (one_of: pickup or delivery), idempotency, a confirmation before any submit, and a phone number that identifies the room and is then proved with a code.

examples/actuelle/product-medium.yaml
# Product · medium — a restaurant: dishes, then pickup OR delivery.
# Two skills, one dependency (no delivery for nothing), one kind that differs (pickup is free and
# asks nothing), one playbook whose second task is a choice — pickup or delivery — a confirmation
# before anything is filed.
station:
id: teranga
name: Teranga Kitchen
identity:
providers:
sms: {mechanism: otp, verifies: phone, via: tenant, send: sendOtp, check: checkOtp}

global:
language: fr
currency: XOF
locale: fr-SN

domain:
domain_type: restaurant
entity_name: plat
entity_name_plural: plats


agent:
tone: friendly
name: Teranga
persona: "un hôte chaleureux qui aide à composer le repas"
rules:
- "Ne jamais inventer un plat : uniquement ce que le menu renvoie."
- "Les prix sont en francs CFA (ex : 8 500 F CFA)."
- "Nous livrons à Dakar uniquement ; ailleurs, proposer le retrait au comptoir."
portals:
web:
widget:
origins: [https://teranga.example.sn]

apis:
- name: cuisine
openapi_url: https://api.teranga.example.sn/openapi.json
base_url: https://api.teranga.example.sn
operations:
- {id: getMenu, access: read}
- {id: placeOrder, access: write}
- {id: getDeliveryOptions, access: read}
- {id: checkDeliverable, access: read}
- {id: getDeliveryPrice, access: read}
- {id: createDelivery, access: write, reversible: false}
- {id: sendOtp, access: write}
- {id: checkOtp, access: write}
auth: {type: api_key, key_name: X-API-Key, key_location: header, key_env: TERANGA_KEY}

skills:
- name: takeMealSelection
auto_trigger: take-meal-order
cognitive:
objective: "montrer la carte pour choisir les plats"
diagnose: [l'envie du moment, les restrictions alimentaires]
invoke: {method: getMenu}
kinds: {dish: {}}
present:
fields:
name: {from: name, type: text, role: title}
price: {from: price, type: price, role: billed}
image: {from: image_url, type: image}
submit:
- name: order
mode: request
tool: placeOrder
args:
body.items: "$selection[kind=dish]"
body.idempotency_key: "$action.key" # a retry never orders twice
line_fields: {ref: slug, quantity: quantity}

render: {component: carousel}
- name: takeDeliveryOption
cognitive:
objective: "proposer le retrait ou la livraison"
invoke: {method: getDeliveryOptions}
kinds: # told by the row's `type`, a presented field
pickup:
when: "$fields.type == 'pickup'"
select: {} # no address, no check, no fee
submit: [{name: collect, mode: self}] # collected at the counter: filed, nothing dispatched
delivery:
when: "$fields.type == 'delivery'"
depends_on: takeMealSelection # no delivery for an empty order…
on_loss: clear # …and it leaves with the last dish
present:
fields:
name: {from: name, type: text, role: title}
type: {from: type, type: text}
select:
max_items: 1 # one option: choosing delivery replaces pickup
guards:
- form:
address: {type: text, required: true, label: "Adresse de livraison"}
phone: {type: phone, required: true, label: "Téléphone du livreur", identifies: true}
- check: checkDeliverable
- update: {price: getDeliveryPrice}
submit:
- name: dispatch
mode: request
tool: createDelivery
args:
body.address: "$item.fields.address"
body.phone: "$item.fields.phone"
body.items: "$selection[kind=dish]"
body.fee: "$item.price" # the amount the guard quoted
render: {component: cards}

playbooks:
- name: take-meal-order
goal: "commander un repas : les plats, puis le retrait ou la livraison"
tasks:
plats: {skill: takeMealSelection, label: "Vos plats"}
remise:
label: "Retrait ou livraison"
after: [plats]
one_of: # two methods, one skill: each is one of its kinds
retrait: {skill: takeDeliveryOption, kind: pickup}
livraison: {skill: takeDeliveryOption, kind: delivery}
choose: customer # the person picks (the default)

catalog:
from_skill: takeMealSelection
mode: live

submit_guard:
confirm: true

What happens​

stepCoreresult
the agent shows the menuauto_trigger opens take-meal-orderthe journey shows: "Your dishes", "Pickup or delivery"
a yassa addedtask "Your dishes" donetotal 8 500 F CFA
"Finalise"task "Pickup or delivery" (remise) required, emptyrefused: "Retrait ou livraison : étape requise de cette commande — ajoutez d'abord pickup ou delivery" (the station speaks French)
pickup chosenthe row has type: pickup, so kinds.pickup, the retrait method: no form, no feeready
delivery chosen insteadselect.max_items: 1; step_open checks the selection without the pickup it replacesthe livraison method replaces retrait, and this is announced; address form, checkDeliverable, getDeliveryPrice
"Finalise" + confirmationplaceOrder, then createDelivery with body.feeorder placed, receipt in the room
the phone given for deliveryidentifies: true, written +221…the room keeps it; no form asks it again
"send me a code"send_verification_code → sendOtpthe restaurant sends a text; the model sees neither the number nor the code
the code typedcheck_verification_code → checkOtpnumber verified, the room belongs to the customer
the last dish removed (before the submit)depends_on + on_loss: cleardelivery goes with it

Key points​

  • kinds.pickup, with its select: {} and its mode: self submit, is what prevents charging delivery to someone who comes to collect their meal.
  • body.fee: "$item.price" passes on the computed price. Without it, Core refuses the station: the customer would have seen an amount the restaurant does not receive.
  • remise is a one_of of two methods on one skill, each one of its kinds: only one does the task, and a selection that kept both could not be submitted.
  • after: [plats] opens "Pickup or delivery" once the dishes are chosen, within the journey; depends_on on the skill holds the same dependency, even outside the journey.