early-butcher-66889
12/16/2025, 4:32 AMtall-airline-16756
12/16/2025, 4:32 AMtall-airline-16756
12/16/2025, 4:32 AMsession.start requiring an agent field:
• You cannot bypass the agent parameter — session.start requires an agent.
• Do NOT reference a "first agent node" from LangGraph. Instead, compile your LangGraph workflow and wrap it with the LLMAdapter (or langchain.LLMAdapter) so the graph drives behavior.
• Provide a minimal LiveKit Agent as a placeholder (e.g., simple instructions). The real logic comes from the LangGraph via the adapter; the placeholder agent just satisfies the required parameter.
If you want, share the short snippet you have for your graph + session.start and I’ll point out exactly where to add the minimal agent and adapter.
Sources: Agent session | LiveKit docs | LangChain integration guide | LiveKit Docsearly-butcher-66889
12/16/2025, 4:47 AMtall-airline-16756
12/16/2025, 4:47 AM@function_tool on your Agent that deletes the room (or otherwise hangs up). Use get_job_context() to access the session/room and call ctx.api.room.delete_room(...).
2. If you want the agent to speak a final message, generate the reply then call await context.wait_for_playout() before deleting the room.
3. In your LangGraph workflow, let the LLM call that end_call tool when it decides to terminate the conversation.
Minimal example (conceptual):
@function_tool()
async def end_call(self, context: RunContext):
await context.session.generate_reply(instructions="Thanks — goodbye!")
await context.wait_for_playout()
ctx = get_job_context()
if ctx:
await ctx.api.room.delete_room(api.DeleteRoomRequest(room=ctx.room.name))
This keeps your graph logic clean — no manual injection of agent context — and lets the graph/LLM trigger a proper hangup via the agent tool. Want me to review your current end_call tool or workflow snippet?
Sources: Agents telephony integration | LiveKit docs | LangChain integration guide | LiveKit Docs | Tool definition and use | LiveKit docsearly-butcher-66889
12/16/2025, 4:57 AMfrom dotenv import load_dotenv
from livekit import agents, rtc
from livekit.agents import AgentServer,AgentSession, Agent, room_io, RunContext
from livekit.agents.job import get_job_context
from livekit.agents.llm import function_tool
from livekit.plugins import noise_cancellation, silero, langchain
from livekit.plugins.turn_detector.multilingual import MultilingualModel
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
from operator import add
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage
load_dotenv(".env.local")
class MinimumAgent(Agent):
def __init__(self) -> None:
super().__init__(
instructions="",
)
@function_tool()
async def end_call(self, context: RunContext):
await context.session.generate_reply(instructions="Thanks — goodbye!")
await context.wait_for_playout()
ctx = get_job_context()
if ctx:
await ctx.api.room.delete_room(livekit_api.DeleteRoomRequest(room=ctx.room.name))
class AgentState(TypedDict):
messages: Annotated[list, add]
# Initialize the LLM once for reuse
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
def greet_user(state: AgentState) -> AgentState:
"""Node that greets the user and introduces the AI as a joke specialist"""
# Use LLM to generate a friendly greeting
prompt = "Greet the user warmly and introduce yourself as an AI assistant specializing in telling jokes. Keep it brief and friendly (1-2 sentences)."
response = llm.invoke([HumanMessage(content=prompt)])
greeting = response.content if hasattr(response, 'content') else str(response)
return {
"messages": [AIMessage(content=greeting)]
}
def tell_joke(state: AgentState) -> AgentState:
"""Node that uses an LLM to tell a joke"""
# Create a prompt asking for a joke
prompt = "Tell me a funny joke. Keep it clean and appropriate. Then after telling the joke, end the call with the tool end_call."
# Get the latest user message if available
if state.get("messages") and len(state["messages"]) > 0:
# Extract the last user message
last_message = state["messages"][-1]
if isinstance(last_message, dict):
user_content = last_message.get("content", "")
elif hasattr(last_message, 'content'):
user_content = last_message.content
else:
user_content = str(last_message)
# If user asked for a joke or mentioned a topic, incorporate it
if user_content and "joke" not in user_content.lower():
prompt = f"Tell me a funny joke about: {user_content}. Keep it clean and appropriate."
# Generate joke using LLM
response = llm.invoke([HumanMessage(content=prompt)])
joke_content = response.content if hasattr(response, 'content') else str(response)
return {
"messages": [AIMessage(content=joke_content)]
}
def create_workflow():
workflow = StateGraph(AgentState)
# Add the greeting node
workflow.add_node("greet_user", greet_user)
# Add the joke-telling node
workflow.add_node("tell_joke", tell_joke)
# Set entry point to greeting
workflow.set_entry_point("greet_user")
# Chain: greet -> tell_joke -> END
workflow.add_edge("greet_user", "tell_joke")
workflow.add_edge("tell_joke", END)
return workflow.compile()
server = AgentServer()
@server.rtc_session()
async def my_agent(ctx: agents.JobContext):
session = AgentSession(
stt="assemblyai/universal-streaming:en",
llm=langchain.LLMAdapter(
graph=create_workflow()
),
tts="cartesia/sonic-3:9626c31c-bec5-4cca-baa8-f8ba9e84c8bc",
vad=silero.VAD.load(),
turn_detection=MultilingualModel(),
)
await session.start(
room=ctx.room,
agent=MinimumAgent(),
room_options=room_io.RoomOptions(
audio_input=room_io.AudioInputOptions(
noise_cancellation=lambda params: noise_cancellation.BVCTelephony() if params.participant.kind == rtc.ParticipantKind.PARTICIPANT_KIND_SIP else noise_cancellation.BVC(),
),
),
)
# Trigger the workflow proactively so the agent speaks first
await session.generate_reply()
if __name__ == "__main__":
agents.cli.run_app(server)tall-airline-16756
12/16/2025, 4:57 AMend_call tool on the minimal agent you pass into session.start. A few concise confirmations and one small fix for your snippet:
• Put @function_tool() methods (like end_call) on the Agent instance you pass to session.start() — that is how tools are registered.
• The langchain.LLMAdapter exposes the agent's tools to the graph/LLM automatically, so your LangGraph nodes can trigger the end_call tool without you manually injecting agent context.
• When the LLM decides to end the call, it should call the end_call tool; the tool runs inside the agent environment and can access get_job_context() and context.session to generate final speech and delete the room.
Small correction in your code:
• Replace livekit_api.DeleteRoomRequest with the correct import/name you use for the room delete request (you currently reference livekit_api but haven't imported it). For example, if you use from livekit import api then call api.DeleteRoomRequest(room=ctx.room.name). Otherwise import the correct module before use.
Optional improvement:
• If you want the LLM to explicitly invoke the tool, include an instruction in the prompt like "When finished, call the tool `end_call`" so the model chooses the tool. Otherwise you can detect the END node and programmatically call end_call from the workflow runtime.
If you want, I can:
• Suggest the exact import line for your environment, or
• Show a short example of how to trigger the end_call tool from a LangGraph node. Which would help more?early-butcher-66889
12/16/2025, 5:00 AMtall-airline-16756
12/16/2025, 5:00 AMend_call) and include a clear instruction in the prompt that the model should invoke that tool when it wants to end the call.
Example change inside your tell_joke node (call the LLM and let it decide to call the tool):
def tell_joke(state: AgentState) -> AgentState:
prompt = (
"Tell a short clean joke. If you decide the conversation should end after the joke, "
"respond with EXACTLY: [CALL_TOOL:end_call]. Otherwise just return the joke text."
)
response = llm.invoke([HumanMessage(content=prompt)])
content = response.content if hasattr(response, "content") else str(response)
# If model indicates a tool call, return a marker so the graph runtime can act
if "[CALL_TOOL:end_call]" in content:
return {"messages": [AIMessage(content=content)], "end_call": True}
return {"messages": [AIMessage(content=content)]}
• Your agent/tool system will see the LLM output and, if it contains the marker, invoke the end_call tool. This is a simple prompt-based protocol that the runtime can parse.
2) Force the tool from the workflow runtime on END
• If you want the workflow to guarantee hanging up when reaching the END node, call the agent tool directly from the workflow runtime (server-side), not via the LLM output. This is the more reliable option.
Add a small helper to invoke the tool from Python using the session/agent context. Example conceptual pattern (place where you run the compiled workflow):
# After workflow node returns a state that leads to END:
result_state = tell_joke(state)
if result_state.get("end_call") or next_node_is_END:
# call the agent tool directly using the session's tool runner
# session is your AgentSession instance available in the job context
await MinimumAgent().end_call(context=session.run_context) # conceptual
Concrete way inside your Agent job function
• The AgentSession/Agent system exposes tools via the agent instance in the running job. So inside your @server.rtc_session() handler, after you run/advance the graph and detect the END node, call the tool on the running agent context:
# inside your rtc_session handler, after creating and starting session
# run the workflow and inspect state transitions; when reaching END:
await session.agent.end_call(context=session.run_context)
Notes and practical tips
• Ensure @function_tool() methods accept context: RunContext and use get_job_context() or context to access ctx.api.room.delete_room.
• Use await context.session.generate_reply(...) and await context.wait_for_playout() inside end_call to speak a final message before deleting the room.
• If you rely on LLM-invoked tools, use a strict token/marker (like [CALL_TOOL:end_call]) so your runtime can detect and map it to the real tool call.
• The most reliable approach: have the graph/runtime call the tool when the workflow reaches the END node (server-side), and optionally let the LLM suggest ending by returning a flag the runtime checks.
If you'd like, I can:
• Show the exact small change in your current code to call end_call from the job handler when the graph reaches END, or
• Show how to parse the LLM marker and map it to a tool invocation in your runtime. Which do you prefer?