Skip to content

Talk to a model with interact_with_llm

Most agent work is a conversation with a model. A11 ships that conversation as an ordinary action, interact_with_llm, whose ports stream the model's output as it arrives. You drive it exactly like any other action: feed its input ports, read its output ports.

Alongside import a11 this page needs a few names from the SDK, which is where the model helpers live:

import a11
from a11.sdk.interact_with_llm import (
    INTERACT_WITH_LLM_SCHEMA,
    interact_with_llm,
)
from a11.sdk.llm import Interaction, LlmHeaders, Role

Build and start the action

Construct the action from its schema, bind the handler, and set the provider / model / key as headers. .run() starts it in the background and hands back the running action, whose ports you now read and write:

import os

interact = (
    a11.Action(INTERACT_WITH_LLM_SCHEMA)
    .bind_handler(interact_with_llm)
    .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"])
    .run()
)

Ports are async nodes, reached with interact["<port>"] — the same AsyncNode you met in streaming.

Stream the reply as it arrives

The assistant's visible text lands, already extracted from the raw provider events, on the text_output port. Draining it in a task lets tokens print while the rest of the interaction is still in flight:

async def stream_text():
    async for chunk in interact["text_output"]:
        print(chunk, end="", flush=True)


stream_task = asyncio.create_task(stream_text())

Feed the conversation in

The input side takes three ports:

  • interactions — the conversation so far, ending with the new user turn;
  • config — model settings (defaults are fine, so we just open and close it);
  • tools — tool definitions; here there are none, so we close it empty.

An Interaction is a role plus content chunks:

user_turn = Interaction(
    role=Role.USER,
    content=[a11.to_chunk({"role": "user",
                           "content": [{"type": "text", "text": "Hi!"}]})],
)

async with (
    interact["interactions"] as interactions,
    interact["config"],
    interact["tools"] as tools,
):
    await interactions.put_final(user_turn)   # marks the input turn final
    await tools.put_null_final()              # "no tools this time"

Closing each port (the async with exit) tells the handler that side is complete.

Collect the result

The turns the model produced — its text, and any tool calls — arrive on new_interactions. Read it to completion, then await the streaming task so the last tokens have printed:

new_interactions = []
async for interaction in interact["new_interactions"]:
    new_interactions.append(interaction)
await stream_task

Keep new_interactions around and prepend them (plus the user turn) to the next call's interactions to carry the conversation forward.

Putting it together

import asyncio
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


async def ask(text: str) -> None:
    interact = (
        a11.Action(INTERACT_WITH_LLM_SCHEMA)
        .bind_handler(interact_with_llm)
        .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"])
        .run()
    )

    async def stream_text():
        async for chunk in interact["text_output"]:
            print(chunk, end="", flush=True)

    stream_task = asyncio.create_task(stream_text())

    user_turn = Interaction(
        role=Role.USER,
        content=[a11.to_chunk({"role": "user",
                               "content": [{"type": "text", "text": text}]})],
    )
    async with (
        interact["interactions"] as interactions,
        interact["config"],
        interact["tools"] as tools,
    ):
        await interactions.put_final(user_turn)
        await tools.put_null_final()

    async for _ in interact["new_interactions"]:
        pass
    await stream_task


asyncio.run(ask("Say hello in three languages."))

The full multi-turn, multi-provider version is examples/002-llm-interactions.

Next: the model call above ran in your process. See how to move it behind a server you call over the network, then give the model a tool it can call back into.