Let a model call an application action¶
An A11 action can serve as an LLM tool. Register the action, allow the model to
call it through interact_with_llm, and return its output before the model
continues. This guide adds a simulated get_weather action to the
basic model interaction.
Three controls stay separate throughout the example:
- the action schema tells the model what it may request;
- the allow-list authorizes the action for this model turn;
- the registry supplies the handler that executes the request.
import os
import a11
from a11.sdk.interact_with_llm import (
INTERACT_WITH_LLM_SCHEMA,
interact_with_llm,
)
from a11.sdk.llm import Interaction, LlmHeaders, Role
from a11.sdk.llm_tools import runner
Write the tool as an action¶
The handler reads its location input and writes a report — here a canned
string instead of a real API call:
async def get_weather(action):
location = await action["location"].consume()
await action["report"].finalize(f"It is 22°C and sunny in {location}.")
Its schema names the ports and describes them; the descriptions are what the model sees when deciding whether and how to call the tool:
GET_WEATHER = a11.ActionSchema(
name="get_weather",
description="Get the current weather for a location.",
inputs={
"location": a11.ActionPortSchema(
name="location",
type="text/plain",
typeinfo=str,
required=True,
description="The city and country or region.",
)
},
outputs={
"report": a11.ActionPortSchema(
name="report",
type="text/plain",
typeinfo=str,
required=True,
description="A short weather report.",
)
},
)
Register it¶
Put the tool in a registry so the interaction can dispatch it by name:
Let the model reach it¶
Three things connect the registry to the model. bind_registry makes the tool
dispatchable; the ALLOWED_LLM_ACTIONS header allow-lists which registered
actions the model may call; and the tool definitions are streamed in on the
tools port so the provider knows the tool's shape:
interact = (
a11.Action(INTERACT_WITH_LLM_SCHEMA)
.bind_handler(interact_with_llm)
.bind_registry(registry)
.set_header(LlmHeaders.PROVIDER.value, "gemini")
.set_header(LlmHeaders.MODEL.value, "gemini-3.5-flash")
.set_header(LlmHeaders.API_KEY.value, os.environ["GEMINI_API_KEY"])
.set_header(LlmHeaders.ALLOWED_LLM_ACTIONS.value, "get_weather")
.run()
)
tool_definitions = runner.get_tool_definitions(registry, ["get_weather"])
Run the turn¶
Feed and read the action as shown in the plain interaction, and stream
the tool definitions onto the tools port:
user_turn = Interaction(
role=Role.USER,
content=[a11.to_chunk({"role": "user", "content": [
{"type": "text", "text": "What's the weather in Paris?"}]})],
)
await interact["interactions"].finalize(user_turn)
await interact["config"].finalize()
tools = interact["tools"]
for tool in tool_definitions:
await tools.put(tool)
await tools.finalize()
When the model decides to call get_weather, interact_with_llm dispatches the
action in this process, streams its report back to the model, and lets the
model continue — so the final text on text_output already reflects the tool
result. Read that output from text_output:
Tool-call lifecycle¶
The model requests a tool call, the registered action runs, and its output returns to the model before the model answers. Because the tool is an ordinary action:
- it could stream progress on
reportinstead of returning one string; - it could itself
.call()a remote action (see local to remote), so a client tool can front a real weather service; - swapping the canned string for a live API call changes nothing about the wiring above.
The complete interactive version — multi-turn, multi-provider, with the tool
registry — is examples/002-llm-interactions.
For applications that receive provider interactions directly, see running requested actions. To offer tools from an MCP server through the same registry, see MCP tools as actions.