Defines the components of an order, and the basic logic for the chatbot to follow when guiding users through the meal-ordering process.
Consult this skill when:
take_order tool in src/tools/take_order.pysrc/models/take_order — record the parsed item and quantityanswer_menu_question — return an answer from menu dataget_error_response — return a predefined error message (off-topic or invalid order)get_non_error_response — return a predefined suggestion or confirmation messagesummarize_order — present the final order summaryget_non_error_response, mapped to situations such as Order Suggestions or Order Confirmations; the LLM is not called to generate theseget_error_response, mapped to specific off-topic scenarios; see Off-Topic Handling belowEvery user message is first classified by the supervisor agent (src/agents/supervisor.py) into one of three intents via SupervisorDecision.intent:
| Intent | Meaning | Tool called |
|---|---|---|
order_entry | User is placing or modifying an order | take_order |
menu_question | User is asking about the menu | answer_menu_question |
off_topic | Input unrelated to ordering | get_error_response |
Order logic only applies when intent is order_entry. The other intents bypass order logic entirely.
The take_order tool enforces two hard rules. Both return an error string instead of recording the item:
menu.json. If not found, an error response is returned listing available items.These errors surface back through the supervisor's response. The LLM does not generate error text — errors are returned directly from the tool as strings.
After take_order succeeds, the response type depends on the current state of the order:
User input classified as order_entry
└─ take_order(item, quantity)
├─ ERROR: item not on menu → get_error_response
├─ ERROR: quantity out of range → get_error_response
└─ SUCCESS: item recorded
├─ Order has ONLY a Main (no Side, no Drink)
│ └─ get_non_error_response("next-step-only-main-ordered")
├─ Order has a Main AND a Side (no Drink)
│ └─ get_non_error_response("next-step-main-and-side-ordered")
├─ Any other successful combination
│ └─ get_non_error_response("next-step-generic")
└─ User says nothing else is wanted
└─ summarize_complete_order → get_non_error_response("ending-comment")
Key rule: Predefined responses are always selected from resource files — the LLM is never called to generate the text of a final response.
When the supervisor classifies input as off_topic:
[OFF-TOPIC] (enforced by the system prompt).track_off_topic middleware detects the prefix and increments off_topic_count in state.get_error_response is called with an error_type and a level. The level is determined by how many off-topic turns have occurred in the session (counted from [OFF-TOPIC] markers in conversation history).off_topic_count reaches OFF_TOPIC_LIMIT (3), track_off_topic terminates the session and routes to __end__.Each off-topic type (sexual-content, prompt-engineering, not-understandable, simply-unrelated) has messages at levels 1 and 2. The third off-topic message — regardless of type — uses the universal "any" entry at level 3:
| Session off-topic count | Level passed | Message selected from |
|---|---|---|
| 1 (first occurrence) | level=1 | errorType=<type>, errorLevel=1 |
| 2 (second occurrence) | level=2 | errorType=<type>, errorLevel=2 |
| 3+ (third or beyond) | level=3 | errorType="any", errorLevel=3 |
The "any" level 3 message is a final warning delivered before track_off_topic ends the session. The LLM is responsible for counting prior [OFF-TOPIC] responses in the conversation history to determine the correct level to pass.
The logged off_topic_count is always the post-increment value. See langchain-middleware-skill for middleware implementation details.