early-butcher-66889
12/16/2025, 6:22 AMtall-airline-16756
12/16/2025, 6:22 AMtall-airline-16756
12/16/2025, 6:23 AMearly-butcher-66889
12/16/2025, 6:24 AMtall-airline-16756
12/16/2025, 6:25 AMLLMAdapter. It models gathering trip details, checking availability, confirming booking, performing booking (or retrieving by reference), and handling user corrections or cancellations.
Notes:
• Treat each node as a LangGraph runnable node (LLM prompt node, API node, or control node).
• Conditional edges decide next states based on extracted slots or API results.
• Integrate this compiled graph as the graph passed to LLMAdapter in an AgentSession.
State machine outline (nodes and edges)
1) Start -> DetermineIntent
• Node: DetermineIntent (LLM)
◦ Prompt: "User input: {user_text}. Determine whether the user wants to: (A) book a new flight, (B) retrieve an existing booking, or (C) something else. Respond in JSON: { intent: 'book'|'retrieve'|'other' }"
• Edges:
◦ intent == 'book' -> GatherTripDetails
◦ intent == 'retrieve' -> GetBookingReference
◦ intent == 'other' -> HandleFallback
2) GatherTripDetails -> ValidateDetails
• Node: GatherTripDetails (LLM)
◦ Prompt: "Collect missing slots for booking: origin, destination, depart_date, return_date(optional), passengers_count, class, any preferences. Return JSON with fields and booleans for missing fields."
• Edge:
◦ If missing fields -> AskForMissingSlots
◦ Else -> ValidateDetails
3) AskForMissingSlots -> GatherTripDetails
• Node: AskForMissingSlots (LLM)
◦ Prompt: "Ask the user only for the missing fields identified: {missing_fields}."
• Edge:
◦ User replies -> GatherTripDetails
4) ValidateDetails -> CheckAvailability
• Node: ValidateDetails (LLM)
◦ Prompt: "Given booking details {slots}, confirm values and detect contradictions. Return { valid: true|false, corrected_slots }."
• Edges:
◦ valid == false -> ConfirmCorrections
◦ valid == true -> CheckAvailability
5) ConfirmCorrections -> GatherTripDetails
• Node: ConfirmCorrections (LLM)
◦ Prompt: "Tell the user the corrections we inferred: {corrected_slots}. Ask to confirm or edit."
• Edges:
◦ user confirms -> CheckAvailability
◦ user edits -> GatherTripDetails
6) CheckAvailability -> PresentOptions or NoAvailability
• Node: CheckAvailability (API)
◦ Action: Call flight search API with slots -> returns list of options or empty
• Edges:
◦ if options found -> PresentOptions
◦ else -> NoAvailability
7) PresentOptions -> ConfirmSelection
• Node: PresentOptions (LLM)
◦ Prompt: "Present top {N} options in concise table and ask user to pick one or request alternatives."
• Edge:
◦ user picks option -> ConfirmSelection
◦ user asks alternatives -> CheckAvailability (with new filters)
8) ConfirmSelection -> ConfirmPassengerDetails
• Node: ConfirmSelection (LLM)
◦ Prompt: "Confirm selected option {option_id} and ask for passenger names and contact info."
• Edge:
◦ collected -> ConfirmPassengerDetails
9) ConfirmPassengerDetails -> FinalConfirmation
• Node: ConfirmPassengerDetails (LLM)
◦ Prompt: "Repeat booking summary: flight option, passengers, contact, price, and payment instructions. Ask user to Confirm, Edit, or Cancel."
• Edges:
◦ Confirm -> PerformBooking
◦ Edit -> GatherTripDetails or ConfirmPassengerDetails
◦ Cancel -> CancelFlow
10) PerformBooking -> BookingSuccess or BookingFailure
• Node: PerformBooking (API)
◦ Action: Call booking API with full payload; returns success with booking_ref or failure/error
• Edges:
◦ success -> BookingSuccess
◦ failure -> BookingFailure
11) BookingSuccess -> End
• Node: BookingSuccess (LLM)
◦ Prompt: "Provide booking reference {booking_ref}, summary, and next steps (email, change/cancel link). Offer to send confirmation via email/SMS."
• Edge:
◦ End
12) BookingFailure -> HandleFailure
• Node: BookingFailure (LLM)
◦ Prompt: "Explain failure reason, offer retry, alternative options, or agent handoff."
• Edge:
◦ retry -> PerformBooking
◦ alternatives -> CheckAvailability
◦ handoff -> End/AgentHandoff
Retrieve flow (from GetBookingReference)
A) GetBookingReference -> LookupBooking
• Node: GetBookingReference (LLM)
◦ Prompt: "Ask for booking reference and email/last name if needed."
• Edge:
◦ collected -> LookupBooking
B) LookupBooking (API) -> FoundBooking or NotFound
• Node: LookupBooking (API)
◦ Action: Call booking lookup API with reference and identity
• Edges:
◦ found -> PresentBooking
◦ not found -> AskForMoreInfo or NotFoundResponse
C) PresentBooking -> BookingActions
• Node: PresentBooking (LLM)
◦ Prompt: "Show booking details and offer actions: view itinerary, change/cancel, re-send confirmation."
• Edges:
◦ user chooses action -> route to appropriate node (ChangeBooking, CancelBooking, ResendConfirmation)
Implementation tips for LiveKit + LangGraph
• Compile this StateGraph and pass it as graph to the LiveKit LLMAdapter.
• Use LLM nodes for slot extraction, confirmation language, and decision prompts; use API nodes for flight search, booking, and lookup.
• Use JSON structured outputs from LLM nodes so conditional edges can parse intent, missing_fields, valid, and other flags.
• Include retries, timeouts, and graceful fallback nodes (HandleFallback, AgentHandoff).
• Use memory nodes if you need to persist user context across turns beyond session variables.
Would you like a runnable code skeleton (Python) that shows: 1) a minimal compiled StateGraph definition for the main nodes, and 2) how to instantiate LLMAdapter(graph=compiled_graph) and pass it into an AgentSession?
Which of those would you like me to produce next?
Sources: LangChain integration guide | LiveKit Docs | Building the all-in-one platform for voice AI agents | Observability data hooks | LiveKit docs | examples/drive-thru | livekit/livekit_composite | DeepWiki | Examples | livekit/livekit_composite | DeepWikiearly-butcher-66889
12/16/2025, 6:27 AMtall-airline-16756
12/16/2025, 6:27 AM# booking_local.py
# Minimal runnable skeleton to validate turn detection + LangGraph workflow behavior.
# NOTE: This is a local simulation: LLM nodes are replaced with simple functions that
# parse/return JSON-like dicts. Replace with real LangGraph/LLM nodes when integrating.
from dataclasses import dataclass, field
from typing import Dict, Any, Optional
import time
import json
# --- Simple "turn detector" simulator ---
# Emulate voice activity endpointing: user types lines; blank line ends a turn.
def simulate_turns():
print("Type user utterances. Press Enter on empty line to simulate end-of-turn. Ctrl+C to quit.")
buffer = []
while True:
try:
line = input()
except EOFError:
return
if line.strip() == "":
if buffer:
utterance = " ".join(buffer).strip()
buffer = []
yield utterance
else:
# double-enter: no input -> ignore
continue
else:
buffer.append(line)
# --- Booking state ---
@dataclass
class BookingState:
intent: Optional[str] = None # 'book' | 'retrieve' | 'other'
slots: Dict[str, Any] = field(default_factory=dict)
chosen_option: Optional[Dict[str, Any]] = None
booking_ref: Optional[str] = None
step: str = "start" # current node name
# --- Mock "LLM" helper functions (replace with real LLM nodes) ---
def detect_intent(user_text: str) -> Dict[str, Any]:
text = user_text.lower()
if "book" in text or "flight" in text or "ticket" in text:
return {"intent": "book"}
if "retrieve" in text or "booking" in text or "reference" in text or "find my" in text:
return {"intent": "retrieve"}
return {"intent": "other"}
def gather_slots_dialog(existing_slots: Dict[str, Any], user_text: Optional[str]) -> Dict[str, Any]:
# naive extraction from user_text
slots = dict(existing_slots)
if user_text:
t = user_text.lower()
if "nyc" in t or "new york" in t:
slots["origin"] = "NYC"
if "sf" in t or "san francisco" in t:
slots["destination"] = "SFO"
if "tomorrow" in t:
slots["depart_date"] = "tomorrow"
if "one" in t and "passenger" in t:
slots["passengers"] = 1
missing = [k for k in ("origin", "destination", "depart_date", "passengers") if k not in slots]
return {"slots": slots, "missing": missing}
def confirm_details_prompt(slots: Dict[str, Any]) -> str:
return f"I have: origin={slots.get('origin')}, destination={slots.get('destination')}, depart_date={slots.get('depart_date')}, passengers={slots.get('passengers')}. Confirm? (yes/no/edit)"
# --- Mock API calls ---
def api_check_availability(slots: Dict[str, Any]) -> Dict[str, Any]:
# always returns two mock options
return {"options": [
{"id": "opt1", "airline": "Acme Air", "price": 199, "depart": "09:00"},
{"id": "opt2", "airline": "Skyways", "price": 249, "depart": "14:30"},
]}
def api_perform_booking(selection: Dict[str, Any], slots: Dict[str, Any]) -> Dict[str, Any]:
# simulate success
return {"success": True, "booking_ref": "BK-123456"}
# --- State machine runner ---
def run_booking_workflow():
state = BookingState()
print("Booking workflow started. Awaiting user input...")
for user_turn in simulate_turns():
print(f"\n--- New turn detected ---\nUser: {user_turn}\n")
# Node: DetermineIntent
if state.step == "start":
out = detect_intent(user_turn)
state.intent = out["intent"]
if state.intent == "book":
state.step = "gather"
elif state.intent == "retrieve":
state.step = "retrieve_lookup"
else:
state.step = "fallback"
# Node: GatherTripDetails
if state.step == "gather":
out = gather_slots_dialog(state.slots, user_turn)
state.slots = out["slots"]
if out["missing"]:
print("Agent: I still need these fields:", out["missing"])
print("Agent: Please provide them.")
state.step = "gather" # stay in gather until filled
else:
print("Agent:", confirm_details_prompt(state.slots))
state.step = "validate_confirm"
# Node: Validate/Confirm
elif state.step == "validate_confirm":
if user_turn.lower().startswith("y") or user_turn.lower().startswith("yes"):
state.step = "check_avail"
elif user_turn.lower().startswith("edit"):
print("Agent: Which part would you like to edit?")
state.step = "gather"
else:
print("Agent: Please reply 'yes' to confirm, 'edit' to change details, or provide corrections.")
state.step = "validate_confirm"
# Node: CheckAvailability
if state.step == "check_avail":
print("Agent: Checking availability...")
api_out = api_check_availability(state.slots)
options = api_out["options"]
print("Agent: Found options:")
for o in options:
print(f" - {o['id']}: {o['airline']} ${o['price']} departs {o['depart']}")
print("Agent: Please pick an option id (e.g., opt1) or ask for alternatives.")
state.step = "present_options"
state._options = {o["id"]: o for o in options}
# Node: PresentOptions / ConfirmSelection
elif state.step == "present_options":
sel = user_turn.strip()
if hasattr(state, "_options") and sel in state._options:
state.chosen_option = state._options[sel]
print(f"Agent: You chose {sel} -> {state.chosen_option}")
print("Agent: Please provide passenger name and contact (e.g., John Doe, <mailto:john@example.com|john@example.com>)")
state.step = "passenger_info"
else:
print("Agent: I didn't recognize that option id. Please pick one shown or type 'options' to repeat.")
state.step = "present_options"
# Node: Collect passenger info -> FinalConfirmation
elif state.step == "passenger_info":
# naive parsing
state.slots["passenger_name"] = user_turn.split(",")[0].strip()
state.slots["contact"] = user_turn.split(",")[1].strip() if "," in user_turn else "unknown"
print("Agent: Summary:")
print(f" Flight: {state.chosen_option}")
print(f" Passenger: {state.slots['passenger_name']}, contact: {state.slots['contact']}")
print("Agent: Confirm booking? (yes/no)")
state.step = "confirm_booking"
elif state.step == "confirm_booking":
if user_turn.lower().startswith("y"):
print("Agent: Performing booking...")
res = api_perform_booking(state.chosen_option, state.slots)
if res.get("success"):
state.booking_ref = res["booking_ref"]
print(f"Agent: Booking confirmed. Reference: {state.booking_ref}")
state.step = "end"
else:
print("Agent: Booking failed:", res.get("error", "unknown"))
state.step = "booking_failed"
else:
print("Agent: Booking canceled or not confirmed. Returning to gather step.")
state.step = "gather"
elif state.step == "retrieve_lookup":
# Ask for reference if not provided
# Attempt naive extraction
possible_ref = None
words = user_turn.split()
for w in words:
if w.startswith("BK-"):
possible_ref = w
if possible_ref:
print(f"Agent: Looking up booking {possible_ref}...")
# simulate found
print("Agent: Found booking:", {"ref": possible_ref, "flight": "Acme Air 09:00", "passenger": "You"})
state.step = "end"
else:
print("Agent: Please provide your booking reference (e.g., BK-123456) and email/last name.")
state.step = "retrieve_lookup"
elif state.step == "fallback":
print("Agent: Sorry, I can help with booking or retrieving bookings. Could you rephrase?")
state.step = "start"
if state.step == "end":
print("Agent: Anything else I can help with? (Say 'book' or 'retrieve' to start another flow.)")
state = BookingState() # reset for next session
print("---- workflow reset ----")
# Entry point
if __name__ == "__main__":
run_booking_workflow()
How to run:
1. python booking_local.py
2. Type messages; press Enter on blank line to indicate the end of a spoken turn. Each completed turn is processed by the state machine.
3. Walk through a booking:
◦ Example turn 1: "I want to book a flight from New York to San Francisco tomorrow"
◦ Press Enter then blank line to end turn.
◦ Follow agent prompts (supply missing slots, pick option id like opt1, provide passenger info "John Doe, john@example.com", confirm "yes").
Next steps I can provide:
• A version that uses real LangGraph/LangChain nodes and compiles a StateGraph for use with LiveKit's LLMAdapter.
• A version that uses actual VAD/endpointing and connects to a LiveKit room for end-to-end audio testing.
Which option would you like next?
Sources: LangChain integration guide | LiveKit Docs | Prompting guide | LiveKit docs | Building the all-in-one platform for voice AI agents | Agents Framework | livekit/livekit_composite | DeepWiki | Building the all-in-one platform for voice AI agentsearly-butcher-66889
12/16/2025, 6:30 AMtall-airline-16756
12/16/2025, 6:31 AMLLMAdapter, so the booking workflow runs inside a LiveKit AgentSession with real turn detection. I’ll provide a concise, runnable Python skeleton that uses LiveKit's Agents SDK + LangGraph and shows per-turn advances driven by VAD/turns.
Before I produce the code, quick clarifying question so I give you the right exact skeleton:
• Do you want the example to run fully locally without network STT/TTS (i.e., simulated STT/TTS + Silero VAD for turn detection), or do you want it to use live cloud STT/TTS plugins (like Deepgram/AssemblyAI and ElevenLabs/Cartesia) and connect to a running LiveKit server/room?
Pick one: 1) Local-only simulation (easy to run, demonstrates turn detection + LangGraph inside AgentSession but uses text or simulated audio), or 2) Full live pipeline (requires LiveKit server, API keys for STT/TTS plugins; demonstrates real audio, VAD, and full integration).
Which would you like?
Sources: Agents (Python) | livekit/livekit_composite | DeepWiki | examples/voice_agents | livekit/livekit_composite | DeepWiki | LangChain integration guide | LiveKit Docs | Agents (Node.js) | livekit/livekit_composite | DeepWiki | Tool definition and use | LiveKit docsearly-butcher-66889
12/16/2025, 6:36 AMtall-airline-16756
12/16/2025, 6:37 AMLLMAdapter) and uses real STT, TTS, and VAD plugins (Deepgram, Cartesia, Silero) like the quickstart. This will run against a LiveKit room so you can verify real turn detection and full audio roundtrip.
Before I provide the code, I need one detail to match your environment:
• Are you running a local LiveKit server (Docker) or using LiveKit Cloud (hosted)?
I’ll produce the exact runnable script and env instructions for whichever you pick. Which one should I target?
Sources: LangChain integration guide | LiveKit Docs | Deepgram STT plugin guide | LiveKit docs | Cartesia TTS plugin guide | LiveKit docs | Agent session | LiveKit docs | LiveKitearly-butcher-66889
12/16/2025, 6:38 AMuv run main.py consoletall-airline-16756
12/16/2025, 6:39 AMuv run main.py console, here's a concise, runnable Python script and exact steps. It uses LangGraph via LLMAdapter, Deepgram STT, Cartesia TTS, and Silero VAD (turn detection). Save as main.py in the folder where your .env.local lives.
main.py
# main.py
import os
from dotenv import load_dotenv
import asyncio
load_dotenv(".env.local")
from livekit import agents
from livekit.agents import JobContext, WorkerOptions, cli
from livekit.plugins import deepgram, cartesia, silero
from livekit.plugins.langchain import LLMAdapter
from langgraph.graph import StateGraph, END
from langchain_core.messages import HumanMessage, AIMessage
# --- Build a simple LangGraph booking workflow ---
def create_booking_workflow():
def ask_destination(state):
state.setdefault("messages", [])
state["messages"].append(AIMessage(content="Where would you like to fly?"))
return state
def ask_date(state):
# capture last user message if present
last = state.get("messages", [])[-1]
if isinstance(last, HumanMessage):
state["destination"] = last.content.strip()
state["messages"].append(AIMessage(content=f"When would you like to travel to {state.get('destination','the destination')}?"))
return state
def ask_budget(state):
last = state.get("messages", [])[-1]
if isinstance(last, HumanMessage):
state["date"] = last.content.strip()
state["messages"].append(AIMessage(content="What's your budget for this trip (USD)?"))
return state
def confirm(state):
last = state.get("messages", [])[-1]
if isinstance(last, HumanMessage):
try:
state["budget"] = float(last.content.strip())
except Exception:
state["budget"] = 0.0
dest = state.get("destination","?")
date = state.get("date","?")
budget = state.get("budget","?")
state["messages"].append(AIMessage(content=f"Booking summary: {dest} on {date}, budget ${budget}. Confirm? (yes/no)"))
return state
def book(state):
# simulate booking -> return confirmation
state["messages"].append(AIMessage(content="✅ Flight booked. Confirmation: LK-123456."))
return state
workflow = StateGraph(dict)
workflow.add_node("ask_destination", ask_destination)
workflow.add_node("ask_date", ask_date)
workflow.add_node("ask_budget", ask_budget)
workflow.add_node("confirm", confirm)
workflow.add_node("book", book)
workflow.add_edge("ask_destination", "ask_date")
workflow.add_edge("ask_date", "ask_budget")
workflow.add_edge("ask_budget", "confirm")
# conditional confirm -> book or end
def confirm_choice(state):
last = state.get("messages", [])[-1]
text = last.content.lower() if isinstance(last, AIMessage) else ""
# We'll route based on the next user reply; use a simple mapping placeholder
return "book"
workflow.add_conditional_edges("confirm", lambda s: "book", {"book": "book"})
workflow.add_edge("book", END)
workflow.set_entry_point("ask_destination")
return workflow.compile()
# --- Agent entrypoint ---
async def entrypoint(ctx: JobContext):
await ctx.connect()
# Plugins: real STT/TTS/VAD
stt = deepgram.STT()
tts = cartesia.TTS()
vad = silero.VAD.load()
# Compile workflow and wrap in LLMAdapter
graph = create_booking_workflow()
llm_adapter = LLMAdapter(graph=graph)
session = agents.AgentSession(
llm=llm_adapter,
stt=stt,
tts=tts,
vad=vad,
min_endpointing_delay=0.5,
max_endpointing_delay=3.0,
)
# Start the agent session in the room
await session.start(ctx.room)
# Kick off the workflow by prompting the first node
# The AgentSession + LLMAdapter will drive the graph on incoming user turns
await session.say("Hi — I can help you book a flight. Where would you like to go?")
if __name__ == "__main__":
cli.run_app(
WorkerOptions(
entrypoint_fnc=entrypoint,
)
)
.env.local (example keys — replace values)
LIVEKIT_URL=wss://<your-project>.livekit.cloud
LIVEKIT_API_KEY=<your_key>
LIVEKIT_API_SECRET=<your_secret>
DEEPGRAM_API_KEY=<your_deepgram_key>
CARTESIA_API_KEY=<your_cartesia_key>
Install deps
• Use uv toolchain (you said you run with uv):
uv add "livekit-agents[langchain,deepgram,cartesia,silero]~=1.2" or pip install livekit-agents livekit-plugins-deepgram livekit-plugins-cartesia livekit-plugins-silero langgraph langchain_core python-dotenv
Run
• From the project folder (with .env.local present):
uv run main.py console
Notes and tips
• The script uses LiveKit Cloud via your LIVEKIT_* vars; console mode will route your local mic to the agent.
• Silero VAD handles turn detection; STT (Deepgram) transcribes each detected turn and LangGraph (via LLMAdapter) advances the graph per turn.
• The provided workflow is a minimal skeleton to validate per-turn behavior; replace node logic with richer LangGraph nodes and structured JSON outputs for robust branching.
• If you want, I can convert the confirm node to parse the user's yes/no reply explicitly (so confirmation routes to book or to end) and provide a slightly more strict state JSON extraction pattern.
Would you like the confirm branching tightened (yes/no detection) now?
Sources: LangChain integration guide | LiveKit Docs | Voice AI quickstart | LiveKit docs | Deepgram STT plugin guide | LiveKit docs | Cartesia TTS plugin guide | LiveKit docs | Silero VAD plugin | LiveKit docsearly-butcher-66889
12/16/2025, 6:49 AMtall-airline-16756
12/16/2025, 6:50 AM