Imperal Docs
Recipes

Recipe — extension that calls the LLM

Call an LLM from inside your Webbee extension with ctx.ai.complete — it routes through the user's BYOLLM provider automatically, or the platform LLM otherwise.

This recipe shows an extension that calls an LLM itself (beyond the conversational layer around your handlers) — for content generation inside the handler. If the user has configured BYOLLM, the call runs through their provider automatically; otherwise it uses the platform LLM. Either way you write the same code.

Use case

A "rewrite my message" tool — the user pastes text, picks a tone, and the extension rewrites it.

schemas.py
from pydantic import BaseModel, Field
from typing import Literal

class RewriteParams(BaseModel):
    text: str = Field(description="The text to rewrite.")
    tone: Literal["formal", "casual", "concise", "friendly", "apologetic"] = Field(
        description="Target tone.",
    )
    keep_meaning: bool = Field(
        True,
        description="Preserve the original meaning. Set false to allow restructuring ideas.",
    )
handlers_chat.py
from imperal_sdk import ChatExtension, ActionResult, sdl
from .schemas import RewriteParams

TONE = {
    "formal": "in a formal, professional tone",
    "casual": "in a casual, conversational tone",
    "concise": "as concisely as possible while keeping every important detail",
    "friendly": "to feel warm and friendly",
    "apologetic": "to be apologetic and humble",
}

# The rewritten text is an sdl.Entity: core id/title/kind, plus the Bodied
# facet (content.body) for the rewrite and the AIProvenance facet
# (device.ai_model) recording which model produced it.
class Rewrite(sdl.Entity, sdl.Bodied, sdl.AIProvenance):
    pass

@chat.function(
    "rewrite",
    description="Rewrite a piece of text in a specified tone using the user's configured LLM.",
    action_type="read",
    data_model=Rewrite,
)
async def rewrite(ctx, params: RewriteParams) -> ActionResult:
    instruction = f"Rewrite the following text {TONE[params.tone]}."
    if params.keep_meaning:
        instruction += " Preserve all factual content; don't add or remove information."
    result = await ctx.ai.complete(f"{instruction}\n\n{params.text}")
    return ActionResult.success(
        Rewrite(
            id="rewrite",
            title=f"Rewrite ({params.tone})",
            body=result.text,
            generated_by_ai=True,
            ai_model=result.model,
        ),
        summary=result.text,
    )

ctx.ai.complete(prompt) is the only call you make. The platform decides — per user — whether to route it through their BYOLLM provider or the platform LLM. Your code stays provider-agnostic; there is no separate provider object to reach for and nothing to check.

What you NEVER do

Don't import the anthropic / openai SDK

`ctx.ai.complete(...)` is the supported surface; a direct provider import bypasses BYOLLM routing and isn't allowed.

Don't look for the user's credentials

API keys never reach your extension — not in ctx.user, not anywhere.

Don't loop unbounded

Bound your own multi-call loops; the platform doesn't cap in-handler loops, and cost reviews catch unbounded calls.

Try it in chat

"rewrite this casually: 'Per our previous conversation, attached please find the document for your perusal.'"

The classifier picks rewrite(text=..., tone="casual"). Your handler calls ctx.ai.complete(...) and returns something like "Hey — here's the doc you wanted, take a look when you get a chance."

Billing

If the user is on BYOLLM, the call carries no platform fee — the platform resolves this automatically; your extension doesn't check or set anything. See BYOLLM Pricing.

Variations

class RewriteParams(BaseModel):
    text: str = Field(description="Text to rewrite.")
    tone: Literal["formal", "casual"] = Field(description="Tone.")
    n_variations: int = Field(3, description="Number of rewrites to generate.")

# A list result is a concrete sdl.EntityList[Rewrite].
class RewriteList(sdl.EntityList[Rewrite]):
    pass

@chat.function(
    "rewrite_multiple",
    description="Generate several rewrites of the same text.",
    action_type="read",
    data_model=RewriteList,
)
async def rewrite_multiple(ctx, params: RewriteParams) -> ActionResult:
    items = []
    for i in range(params.n_variations):
        r = await ctx.ai.complete(
            f"Rewrite the following {params.tone}, variation {i + 1}:\n\n{params.text}"
        )
        items.append(
            Rewrite(
                id=f"variation-{i + 1}",
                title=f"Variation {i + 1}",
                body=r.text,
                generated_by_ai=True,
                ai_model=r.model,
            )
        )
    return ActionResult.success(
        RewriteList(items=items, total=len(items), has_more=False),
        summary=f"{len(items)} rewrite(s).",
    )

Ask for JSON in the prompt, parse the response text, and map each item onto an sdl.Entity. An action item is a task — use the Assignable facet (people.assignee) and Schedulable facet (time.due_at):

import json

class ActionItem(sdl.Entity, sdl.Assignable, sdl.Schedulable):
    pass

class ActionItemList(sdl.EntityList[ActionItem]):
    pass

@chat.function(
    "extract_action_items",
    description="Extract action items from a block of text.",
    action_type="read",
    data_model=ActionItemList,
)
async def extract_action_items(ctx, params: RewriteParams) -> ActionResult:
    result = await ctx.ai.complete(
        "Extract action items from the text below as JSON with the shape "
        '{"items": [{"owner": "", "deadline": "", "task": ""}]}. '
        "Output only the JSON.\n\n" + params.text
    )
    try:
        raw = json.loads(result.text)["items"]
    except (ValueError, KeyError):
        raw = []
    items = [
        ActionItem(
            id=str(i),
            title=row.get("task", ""),
            assignee=row.get("owner"),
            due_at=row.get("deadline"),
        )
        for i, row in enumerate(raw)
    ]
    return ActionResult.success(
        ActionItemList(items=items, total=len(items), has_more=False),
        summary=f"{len(items)} action item(s).",
    )

Where to next

On this page