The Menu
Multiple Intents, pipeline composition, and levels — Sandy builds a proper menu system.
Sandy’s shop is growing. She needs a menu system that handles browsing, adding items, and searching.
Defining the Menu
from evoid import Intent, Level
from evoid.core.extend import add_intent
from evoid.core import Context
# Menu data — just a dict, no classes needed
MENU = [
{"id": 1, "name": "BLT", "price": 8.99, "category": "classic"},
{"id": 2, "name": "Club", "price": 9.99, "category": "classic"},
{"id": 3, "name": "Veggie", "price": 7.99, "category": "healthy"},
{"id": 4, "name": "Reuben", "price": 10.99, "category": "premium"},
{"id": 5, "name": "Philly", "price": 11.99, "category": "premium"},
]
# Intents
VIEW_MENU = Intent(
name="view_menu",
level=Level.EPHEMERAL, # Fast, cacheable
)
SEARCH_MENU = Intent(
name="search_menu",
level=Level.EPHEMERAL,
)
ADD_ITEM = Intent(
name="add_item",
level=Level.STANDARD, # Needs persistence
)
# Processors
async def handle_view_menu(ctx) -> dict:
return {"menu": MENU}
async def handle_search(ctx) -> dict:
query = ctx.intent.metadata.get("query", "").lower()
results = [item for item in MENU if query in item["name"].lower()]
return {"results": results, "count": len(results)}
async def handle_add_item(ctx) -> dict:
name = ctx.intent.metadata.get("name")
price = ctx.intent.metadata.get("price", 0.0)
category = ctx.intent.metadata.get("category", "custom")
new_item = {"id": len(MENU) + 1, "name": name, "price": price, "category": category}
MENU.append(new_item)
return {"status": "added", "item": new_item}
# Register
add_intent(VIEW_MENU, handle_view_menu)
add_intent(SEARCH_MENU, handle_search)
add_intent(ADD_ITEM, handle_add_item)
Pipeline Composition
Each Intent gets a pipeline. The default pipeline depends on the level:
# Ephemeral — fast path, just validate
VIEW_MENU = Intent(name="view_menu", level=Level.EPHEMERAL)
# Default pipeline: ("validate",) — 5s timeout
# Standard — balanced, auth check
ADD_ITEM = Intent(name="add_item", level=Level.STANDARD)
# Default pipeline: ("validate", "authorize") — 10s timeout
# Critical — full protection
DELETE_ITEM = Intent(name="delete_item", level=Level.CRITICAL)
# Default pipeline: ("validate", "authorize", "audit", "protect") — 30s timeout
When you use add_intent(), it overrides the default with just your handler. You can compose a custom pipeline with add_intent_with_pipeline() instead:
from evoid.core.extend import add_intent_with_pipeline
add_intent_with_pipeline(
ADD_ITEM,
processors=["validate", "authorize", handle_add_item],
)
# Pipeline: validate → authorize → handle_add_item
Adding Custom Processors
Sandy wants logging on every menu action:
async def log_action(ctx) -> dict:
"""Log every intent execution."""
action = ctx.intent.name
print(f"[LOG] Action: {action}")
ctx.state["logged"] = True
return {"logged": True}
Wire it to specific intents using the extend system:
from evoid.core.extend import before, after
# Add logging BEFORE every menu action
before("view_menu", "log_action")
before("search_menu", "log_action")
before("add_item", "log_action")
Now every menu action runs: log_action → validate → handler.
Levels in Action
Watch how the same action behaves differently with different levels:
# EPHEMERAL — fast, no auth, 5s timeout
# Anyone can search the menu. No identity needed.
SEARCH = Intent(name="search_menu", level=Level.EPHEMERAL)
# Pipeline: validate → handler
# What runs: shape check, then your search code. That's it.
# STANDARD — balanced, auth check, 10s timeout
# Adding an item needs to know who's doing it.
ADD = Intent(name="add_item", level=Level.STANDARD)
# Pipeline: validate → authorize → handler
# What runs: shape check, then "are you an editor?" Then your code.
# CRITICAL — full protection, audit, 30s timeout
# Deleting items from the menu is permanent. Log everything.
DELETE = Intent(name="delete_item", level=Level.CRITICAL)
# Pipeline: validate → authorize → audit → protect → handler
# What runs: shape check, auth, full audit trail, rate limit, then your code.
The level determines:
- Which processors run by default
- How long the pipeline can take
- Whether auditing and protection are enabled
# When you install evoid-redis:
# - EPHEMERAL Intents can use cache (fast, disposable)
# - STANDARD Intents can use cache + disk storage
# The plugins don't know about your menu.
# The level tells the plugins when to activate.
```
Error Handling
When a processor fails, the pipeline stops:
async def handle_add_item(ctx) -> dict:
name = ctx.intent.metadata.get("name")
if not name:
raise ValueError("Item name is required")
price = ctx.intent.metadata.get("price", 0)
if price <= 0:
raise ValueError("Price must be positive")
# ... add item
return {"status": "added"}
result = await execute(ADD_ITEM, name="", price=5)
# result.success = False
# result.error = ValueError("Item name is required")
# result.processors = ("validate",) — only ran until failure
What You Learned
| Concept | What It Is |
|---|---|
| Multiple Intents | Each Intent is independent, gets its own pipeline |
| Pipeline composition | Processors chain together, share Context |
| Levels | Determine default pipeline, timeout, infrastructure |
| Extend system | before() / after() inject processors without changing code |
| Error handling | Pipeline stops on exception, Result captures failure |
Next: Taking Orders
Let’s build a CLI so Sandy can take orders from the command line — Taking Orders.