ESC

Type to search...

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_actionvalidatehandler.

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

ConceptWhat It Is
Multiple IntentsEach Intent is independent, gets its own pipeline
Pipeline compositionProcessors chain together, share Context
LevelsDetermine default pipeline, timeout, infrastructure
Extend systembefore() / after() inject processors without changing code
Error handlingPipeline 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.