brief-book-57365
12/18/2025, 5:00 PMtall-airline-16756
12/18/2025, 5:00 PMtall-airline-16756
12/18/2025, 5:01 PMfrom livekit.agents import Agent, function_tool, ImageContent, RunContext
class ImageAgent(Agent):
@function_tool()
async def add_image_to_conversation(
self,
context: RunContext,
image_url: str,
description: str = "User uploaded an image."
):
# wrap the image
image_content = ImageContent(image=image_url)
# copy and update chat context with a user message that includes the image
new_ctx = self.chat_ctx.copy()
new_ctx.add_message(role="user", content=[description, image_content])
# persist updated context so the agent and LLM see the image
await self.update_chat_ctx(new_ctx)
# optional: ask the agent to immediately describe the image
await self.session.generate_reply(instructions=f"Describe the image at {image_url}")
Key points:
• Use ImageContent to attach an image (URL or base64) to a chat message.
• Add that message to a copy of the current chat context and call update_chat_ctx so the image becomes part of conversation history.
• To get a description immediately, call generate_reply with an instruction to describe the image.
• The agent’s LLM must support vision inputs for image understanding.
If you'd like, tell me:
• which LLM/provider you plan to use (so I can confirm vision support), and
• whether you want the agent to describe the image automatically or wait for a human/agent to trigger the description. Sources: Vision | LiveKit Docs | Large language models (LLM) | LiveKit Docsbrief-book-57365
12/18/2025, 5:06 PMtall-airline-16756
12/18/2025, 5:06 PMfrom livekit.agents import function_tool, Agent, RunContext, ImageContent
@function_tool()
async def add_image_to_conversation(
context: RunContext,
image_url: str,
description: str = ""
):
image = ImageContent(image=image_url)
context.chat_ctx.add_message(role="user", content=[description or "User uploaded an image.", image])
return f"Image from {image_url} added to conversation."
Register with an agent at construction:
class MyAgent(Agent):
def __init__(self):
super().__init__(instructions="You analyze images.", tools=[add_image_to_conversation])
Or add to an existing agent instance dynamically:
await agent.update_tools(agent.tools + [add_image_to_conversation])
Node.js (module-level tool)
import { llm } from '@livekit/agents';
import { z } from 'zod';
export const addImageToConversation = llm.tool({
description: 'Add image to conversation',
parameters: z.object({ imageUrl: z.string().url(), description: z.string().optional() }),
execute: async ({ imageUrl, description }, { ctx }) => {
ctx.chatCtx.addMessage({
role: 'user',
content: [{ type: 'text', text: description || 'User uploaded an image.' },
{ type: 'image_url', image_url: { url: imageUrl } }]
});
return `Image from ${imageUrl} added to conversation.`;
},
});
Attach to an agent:
class MyAgent extends voice.Agent {
constructor() {
super({ instructions: 'You analyze images.', tools: { addImageToConversation } });
}
}
Key points
• External tools have access to RunContext / ctx.chatCtx so they can add messages directly.
• You can reuse the tool across agents by passing it in tools or updating an agent's tools dynamically.
• If you want the agent to describe immediately, the tool can also trigger generate_reply (Python) or call session methods after adding the image.
Which language (Python or Node) and whether you want the agent to auto-describe the image or wait for a prompt? I’ll provide a tailored snippet.
Sources: Tool definition and use | LiveKit docsbrief-book-57365
12/18/2025, 5:11 PMtall-airline-16756
12/18/2025, 5:11 PMcontext.session.chat_ctx (not context.chat_ctx) inside a tool, then persist changes with await context.session.update_chat_ctx(...).
Quick corrected example (module-level tool):
from livekit.agents import function_tool, ImageContent, RunContext
@function_tool()
async def add_image_to_conversation(context: RunContext, image_url: str, description: str = ""):
chat_ctx = context.session.chat_ctx
chat_ctx.add_message(
role="user",
content=[description or "User uploaded an image.", ImageContent(image=image_url)]
)
await context.session.update_chat_ctx(chat_ctx)
# optional: trigger an immediate description
await context.session.generate_reply(instructions=f"Describe the image at {image_url}")
return f"Image from {image_url} added to conversation."
If you want this tool used by an agent, pass it in tools when constructing the agent or call await agent.update_tools(...) to add it dynamically.
Do you want the agent to auto-describe the image (use generate_reply) or wait for an explicit prompt? Also tell me which language/runtime you're using if you want a tailored snippet.
Sources: Tool definition and use | LiveKit docs | Pipeline nodes and hooks | LiveKit docs | Vision | LiveKit Docsbrief-book-57365
12/18/2025, 5:17 PMtall-airline-16756
12/18/2025, 5:17 PMfrom livekit.agents import function_tool, ImageContent, RunContext
@function_tool()
async def add_image_to_conversation(context: RunContext, image_url: str, description: str = ""):
# get a copy of the current chat context from the agent or session
chat_ctx = context.agent.chat_ctx.copy() # or: context.session.chat_ctx.copy()
# add the image message
chat_ctx.add_message(
role="user",
content=[description or "User uploaded an image.", ImageContent(image=image_url)]
)
# persist the updated context via the agent (or session)
await context.agent.update_chat_ctx(chat_ctx)
# await context.session.update_chat_ctx(chat_ctx) # alternative
# optional: trigger immediate description
await context.session.generate_reply(instructions=f"Describe the image at {image_url}")
return f"Image from {image_url} added to conversation."
Notes:
• Use context.agent.chat_ctx or context.session.chat_ctx (not context.chat_ctx).
• Call await context.agent.update_chat_ctx(...) or await context.session.update_chat_ctx(...) to persist changes — update_chat_ctx exists on the agent/session, not on the RunContext itself.
• Use ImageContent for the image payload; if you need to download/validate the image first, use the images plugin before constructing ImageContent.
Do you want the tool to auto-describe images, or should it just add them and wait for a separate prompt? Also tell me if you want the Node.js variant.
Sources: Tool definition and use | LiveKit docs | Agents and handoffs | LiveKit docsbrief-book-57365
12/19/2025, 1:28 PMtall-airline-16756
12/19/2025, 1:28 PMImageContent with the image (URL or base64).
• Add a user message containing that ImageContent to the chat context obtained from context.session.chat_ctx or context.agent.chat_ctx.
• Persist the updated context with await context.session.update_chat_ctx(chat_ctx) or await context.agent.update_chat_ctx(chat_ctx).
• Optionally call await context.session.generate_reply(...) to have the agent describe it immediately.
If you want, tell me:
• which language (Python or Node.js) and
• whether you want the agent to auto-describe the image or wait for a prompt,
and I’ll provide a minimal exact snippet for your setup.
Sources: Tool definition and use | LiveKit docs | External data and RAG | LiveKit docs | Vision | LiveKit docsbrief-book-57365
12/22/2025, 9:05 AMtall-airline-16756
12/22/2025, 9:05 AMchat_ctx = context.session.chat_ctx.copy()), then calling await context.session.update_chat_ctx(chat_ctx) (or context.agent.update_chat_ctx)? Include that snippet.
4. If you can, include the full stack trace or the surrounding error lines.
While you gather that, check these quick points:
• Ensure you call .copy() on the chat context before mutating it.
• Each message added must be a valid ChatMessage / content entry (text or ImageContent). Avoid passing incomplete objects or None in the content list.
• If you build a message dict manually, ensure all required fields (type/text or image payload) are present.
• If you use plugins to download/convert images, confirm the returned image data is non-empty before wrapping in ImageContent.
Send the code and stack trace and I’ll point out the exact problematic line and a corrected snippet.brief-book-57365
12/22/2025, 9:35 AMtall-airline-16756
12/22/2025, 9:35 AMchat_ctx (the snippet that calls chat_ctx.add_message(...) or constructs message dicts).
2. The minimal snippet that copies and updates the chat context (how you obtain chat_ctx, call .copy(), then call await context.session.update_chat_ctx(...) or await context.agent.update_chat_ctx(...)).
3. Which LiveKit packages and versions you have installed (pip freeze lines for livekit packages or version numbers).
Quick checks you can run now (most common causes):
• Ensure you call chat_ctx = context.session.chat_ctx.copy() (or context.agent.chat_ctx.copy()) before mutating.
• When adding a message, pass valid content items. Use high-level helpers instead of raw dicts where possible. Example valid content forms:
◦ Plain text: content=["some text"]
◦ ImageContent: content=[ImageContent(image=image_url)]
◦ Mixed: content=["desc", ImageContent(image=image_url)]
• Don’t append None or partially-built dicts into content. Every content entry must include the necessary fields (text or image payload).
• If you construct ChatMessage or content as dicts, include id, role, and content entries in the correct format — better to call chat_ctx.add_message(role="user", content=[...]) which sets required fields.
• If you download/transform the image, confirm the result is non-empty and valid before wrapping in ImageContent.
If you paste the small code snippets I asked for, I’ll point out the exact problematic line and provide a corrected snippet you can drop in.brief-book-57365
12/22/2025, 9:37 AMtall-airline-16756
12/22/2025, 9:37 AMctx.session.history is not the right chat-context object to copy. Use ctx.session.chat_ctx or ctx.agent.chat_ctx (or ctx.session.current_agent.chat_ctx if that’s how you access the agent). Copy that before mutating.
2) Ensure you persist via the same object that owns update_chat_ctx
• Call await ctx.session.update_chat_ctx(...) or await ctx.session.current_agent.update_chat_ctx(...) — whichever is the owner in your runtime. Don’t mix history with chat_ctx when creating events for the OpenAI realtime plugin.
Minimal corrected pattern (drop-in):
# get a proper copy of the chat context
chat_ctx = ctx.session.chat_ctx.copy()
# add the image message (ImageContent must be a valid object)
chat_ctx.add_message(
role="user",
content=[ImageContent(image=file_url)]
)
# persist via the session (or agent) that owns update_chat_ctx
await ctx.session.update_chat_ctx(chat_ctx)
Why this fixes the UnboundLocalError
• The OpenAI realtime conversion expects well-formed ChatItems from chat_ctx. Using the correct chat_ctx and persisting it with the matching update_chat_ctx ensures every message has the required fields so the conversion code can create conversation items without hitting an uninitialized local variable.
If that still errors, please paste:
• The exact object you get when you print type(ctx.session.chat_ctx) and repr(chat_ctx) after copying, and
• The LiveKit packages/versions you have (pip freeze lines for livekit packages).
I’ll inspect the object shape and point out any missing fields.