I'm trying to build an example using LangChain and...
# ask-ai
e
I'm trying to build an example using LangChain and LangGraph as an entry point but in the documents after specifying everything session.start still requires an agent field. How do I get around this? Do I reference the first agent node?
t
Thanks for reaching out, Arthur. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
You're right to ask — regarding your question about
session.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 Docs
e
Ok this works as long as I put in a minimum agent with no instructions. To end the call, do I then need to pass agent context into the workflow and create a tool call that allows an agent to end the call?
t
You're on the right track — short answer: you do NOT need to manually pass agent context into the LangGraph workflow. Instead define a tool on the Agent that ends the call, and invoke that tool from the graph (the LLMAdapter exposes agent tools to the graph automatically). What to do: 1. Add an
@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):
Copy code
@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 docs
e
Is this method put on the minimum agent I pass into the session.start? how does it know the tool works? This is the code I built using the boilerplate provided on the Livekit website
Copy code
from 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)
t
You're correct to put the
end_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?
e
Please show how we can trigger it instead - right now the prompt isn't triggering it from within LangGraph
t
You're right — when the model doesn't trigger the tool from prompt alone, call the tool programmatically from the workflow node so it reliably executes. Below is a concise example showing two approaches: 1) instructing the LLM to call the tool (prompt guidance) and 2) forcing the tool call from the workflow when you detect the END transition. 1) Make the tool callable by the LLM (prompt guidance) • Ensure the agent exposes the tool name (
end_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):
Copy code
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):
Copy code
# 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:
Copy code
# 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?