Skip to content

Notebooks and apps

Available · pip install agent-adapter (no extra)

Jupyter, Streamlit and Gradio already run an event loop, each in its own way. Pick the call that fits it: await, or run_sync.

File Does
examples/backend_common.py the shared tools and call(backend, tool, arguments, run_id=, call_id=)
examples/backend_notebook.py a Jupyter cell body, a Streamlit page, a Gradio handler
tests/docs/test_backend_handler_examples.py runs them offline (Streamlit through a fake st)

Data enters as a widget value or a cell, goes through call to the executor, and leaves as the value you display.

flowchart TD
  Q{"who owns the loop?"} -- "Jupyter / IPython" --> J["top-level await"]
  Q -- "Gradio async handler" --> G["await inside the handler"]
  Q -- "Streamlit script (sync, reruns)" --> S["run_sync per call<br/>backend in st.cache_resource"]

Jupyter

async def notebook_cell(backend: Backend) -> float:
    """In a Jupyter cell, write the body at top level: `rate = await shipping_rate(...)`."""
    return await shipping_rate(backend, "us")

A cell already runs inside IPython's loop: write await shipping_rate(...) at top level. Never asyncio.run there (it raises), and no nest_asyncio. In a sync helper, run_sync works too: it runs the work in a worker thread and blocks the cell until done.

Streamlit

def streamlit_page(st: Any) -> None:
    """A Streamlit script: the backend is cached across reruns, each call is one run_sync."""
    backend = st.cache_resource(build_backend)()  # once per server process
    region = st.selectbox("Region", ["eu", "us", "other"])
    rate = run_sync(shipping_rate(backend, region), timeout=TIMEOUT_S)  # never asyncio.run here
    st.metric("Shipping %", rate)
Rule Why
st.cache_resource for the backend the script reruns on every widget change; the executor and store live for the server process
run_sync(work, timeout=) per call the script is sync; each call gets a fresh loop and its LoopLocal clients closed
many calls per rerun put them in one run_sync (run_many), or cache a long-lived loop (LoopRunner) with st.cache_resource
st.cache_resource is shared by every session per-user state goes in st.session_state, never in the backend

Gradio

def gradio_handler(backend: Backend) -> Any:
    """A Gradio `async def` event handler: Gradio awaits it on its own loop."""

    async def rate(region: str) -> str:
        return f"{await shipping_rate(backend, region)}%"

    return rate

Gradio awaits async def handlers on its own loop, so await directly. Sync handlers run in Gradio's thread pool: use run_sync there.

Approvals

A notebook or app with a person in front of it can decide at once: build its executor with CallbackApproval and ask in the UI. For review by someone else, park with DeferredApproval and a shared store, as the web guides do.

See also: Runtime, Sync code and backends.