Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
1c7b33e
wip
longcw Dec 19, 2025
8eba4e9
add logs
longcw Dec 19, 2025
de63ced
add sms-console
longcw Dec 22, 2025
fedadd3
Merge remote-tracking branch 'origin/main' into longc/text-mode
longcw Dec 22, 2025
e0f387b
fix types
longcw Dec 22, 2025
4e2b715
fix agent handoff
longcw Dec 22, 2025
d8bf768
fix wrapped entrypoint
longcw Dec 22, 2025
5d05117
Merge remote-tracking branch 'origin/main' into longc/text-mode
longcw Jan 5, 2026
a092a42
save session after the text handler
longcw Jan 5, 2026
d6c7065
add multiple responses
longcw Jan 6, 2026
4f22115
add e2e encryption
longcw Jan 8, 2026
30f9711
add get_init_kwargs
longcw Jan 9, 2026
6084764
Merge remote-tracking branch 'origin/main' into longc/text-mode
longcw Jan 13, 2026
fcc3b92
serialize old_agent for AgentTask
longcw Jan 13, 2026
03ac592
add Agent.configure
longcw Jan 16, 2026
7bdae50
Merge remote-tracking branch 'origin/main' into longc/text-mode
longcw Jan 19, 2026
1eff439
hand text message request from lk server (#4553)
longcw Jan 20, 2026
12c6b88
durable scheduler WIP
theomonnom Jan 21, 2026
685e7b8
contextvars already handled
theomonnom Jan 21, 2026
8495cea
export session state as db and compute delta for sync (#4604)
longcw Jan 28, 2026
52f1eb2
durable functions integration (#4647)
longcw Jan 30, 2026
4db5018
update sms cli
longcw Jan 30, 2026
cb731e4
Merge remote-tracking branch 'origin/main' into longc/text-mode
longcw Feb 2, 2026
214f5f7
clean and update session store versions
longcw Feb 2, 2026
09a2026
fix types
longcw Feb 2, 2026
29f13e4
save init kwargs as blob
longcw Feb 2, 2026
363d168
Merge remote-tracking branch 'origin/main' into longc/text-mode
longcw Feb 3, 2026
c763a96
store chat ctx as text
longcw Feb 3, 2026
128ceb8
Merge remote-tracking branch 'origin/main' into longc/text-mode
longcw Feb 4, 2026
acad0a7
ignore config update
longcw Feb 4, 2026
9a1e67e
Merge remote-tracking branch 'origin/main' into longc/text-mode
longcw Feb 6, 2026
54fc75a
add http server for text mode (#4718)
longcw Feb 8, 2026
b4f9246
Merge remote-tracking branch 'origin/main' into longc/text-mode
longcw Feb 8, 2026
2a18d4e
fix cli
longcw Feb 8, 2026
c854820
fix cli
longcw Feb 8, 2026
ba69121
fix drain messages in buffer before close
longcw Feb 9, 2026
970976c
Merge remote-tracking branch 'origin/main' into longc/text-mode
longcw Feb 9, 2026
2511a47
fix SpeechHandle creation
longcw Feb 9, 2026
9c10af2
update endpoint and fix http server close
longcw Feb 9, 2026
b7782f5
update session store and cache
longcw Feb 9, 2026
5ca2442
rename durable and http sever modules
longcw Feb 10, 2026
0bfbd27
add error code to types
longcw Feb 10, 2026
c45f6f6
update protocol for http api
longcw Feb 10, 2026
207b6f2
fix import
longcw Feb 10, 2026
5f7dca2
update to response proto
longcw Feb 11, 2026
e46bd74
handle durable unpickle error
longcw Feb 11, 2026
fbc978f
fix typo
longcw Feb 11, 2026
b3590c9
handle durable function failure
longcw Feb 12, 2026
c5c7f70
fix durable function resume
longcw Feb 12, 2026
9d9179f
remove session started event
longcw Feb 12, 2026
93ab71b
fix _resume_durable_function
longcw Feb 12, 2026
e4683b2
fix types
longcw Feb 13, 2026
19d781a
Merge remote-tracking branch 'origin/main' into longc/text-mode
longcw Feb 23, 2026
6d3979d
fix durable scheduler shutdown and text mode session cleanup
longcw Feb 24, 2026
e346db6
Merge remote-tracking branch 'origin/main' into longc/text-mode
longcw Feb 25, 2026
fcbeda8
Merge remote-tracking branch 'origin/main' into longc/text-mode
longcw Feb 26, 2026
3d7ebea
Merge remote-tracking branch 'origin/main' into longc/text-mode
longcw Mar 10, 2026
58ee25a
use Instructions in example
longcw Mar 10, 2026
8f90a86
add setup_fnc for Agent
longcw Mar 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions examples/voice_agents/sms_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import logging
import os

import aiohttp
from dotenv import load_dotenv

from livekit.agents import (
Agent,
AgentServer,
AgentSession,
EffectCall,
JobContext,
JobExecutorType,
RunContext,
TextMessageContext,
cli,
)
from livekit.agents.beta.workflows import GetEmailTask
from livekit.agents.llm import Instructions, ToolFlag, function_tool
from livekit.plugins import silero
from livekit.plugins.turn_detector.multilingual import MultilingualModel

logger = logging.getLogger("basic-agent")

load_dotenv()

PORT = int(os.getenv("PORT", 8081))


class MyAgent(Agent):
BASE_INSTRUCTIONS = (
"Your name is Kelly. You would interact with users via {modality}. "
"with that in mind keep your responses concise and to the point. "
"{modality_specific}"
"You are curious and friendly, and have a sense of humor. "
"When user want to register for the weather event, call the `register_for_weather` function. "
)

def __init__(self) -> None:
super().__init__(
instructions=Instructions(
audio=self.BASE_INSTRUCTIONS.format(
modality="voice",
modality_specific="do not use emojis, asterisks, markdown, or other special characters in your responses.",
),
text=self.BASE_INSTRUCTIONS.format(
modality="text",
modality_specific="you can use emojis to make your responses more engaging.",
),
),
)

@function_tool
async def get_weather(
self,
latitude: str,
longitude: str,
):
"""Called when the user asks about the weather. This function will return the weather for
the given location. When given a location, please estimate the latitude and longitude of the
location and do not ask the user for them.

Args:
latitude: The latitude of the location
longitude: The longitude of the location
"""

logger.info(f"getting weather for {latitude}, {longitude}")
self.session.say("I'm getting the weather for you...")

url = f"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m"
weather_data = {}
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 200:
data = await response.json()
# response from the function call is returned to the LLM
weather_data = {
"temperature": data["current"]["temperature_2m"],
"temperature_unit": "Celsius",
}
else:
raise Exception(f"Failed to get weather data, status code: {response.status}")

return weather_data

@function_tool(flags=ToolFlag.DURABLE)
async def register_for_weather(self, context: RunContext):
"""Called when the user wants to register for the weather event."""
logger.info("register_for_weather called")

chat_ctx = self.chat_ctx.copy(
exclude_function_call=True, exclude_instructions=True, exclude_config_update=True
)
email_result = await EffectCall(
GetEmailTask(
chat_ctx=chat_ctx,
setup_fnc=lambda agent: agent.configure(llm="openai/gpt-4.1"),
)
)
email_address = email_result.email_address

logger.info(f"User's email address: {email_address}")

return "You are now registered for the weather event."
# context.session.say("You are now registered for the weather event.")


server = AgentServer(port=PORT, job_executor_type=JobExecutorType.THREAD)


@server.text_handler(endpoint="weather")
async def text_handler(ctx: TextMessageContext):
logger.info(f"text message received: {ctx.text}")

session = AgentSession(
llm="openai/gpt-4.1-mini",
# state_passphrase="my-secret-passphrase",
)

start_result = await session.start(
agent=ctx.session_state if ctx.session_state else MyAgent(),
capture_run=True,
wait_run_state=False,
)
async for ev in start_result:
await ctx.send_response(ev)

logger.info(f"running session with text input: {ctx.text}")
async for ev in session.run(user_input=ctx.text):
await ctx.send_response(ev)


@server.rtc_session()
async def entrypoint(ctx: JobContext):
session = AgentSession(
stt="deepgram/nova-3",
llm="openai/gpt-4.1-mini",
tts="cartesia/sonic-2:9626c31c-bec5-4cca-baa8-f8ba9e84c8bc",
turn_detection=MultilingualModel(),
vad=silero.VAD.load(),
preemptive_generation=True,
)

await session.start(agent=MyAgent(), room=ctx.room)

session.generate_reply(instructions="greeting the user", input_modality="audio")


if __name__ == "__main__":
cli.run_app(server)
7 changes: 7 additions & 0 deletions livekit-agents/livekit/agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@
AssignmentTimeoutError,
create_api_error_from_http,
)
from .durable_scheduler import DurableScheduler, EffectCall
from .job import (
AutoSubscribe,
JobContext,
JobExecutorType,
JobProcess,
JobRequest,
TextMessageContext,
get_job_context,
)
from .language import LanguageCode
Expand All @@ -55,6 +57,7 @@
StopResponse,
ToolContext,
ToolError,
ToolFlag,
function_tool,
)
from .plugin import Plugin
Expand Down Expand Up @@ -133,11 +136,13 @@ def __getattr__(name: str) -> typing.Any:
"JobProcess",
"JobContext",
"JobRequest",
"TextMessageContext",
"get_job_context",
"JobExecutorType",
"AutoSubscribe",
"FunctionTool",
"function_tool",
"ToolFlag",
"ProviderTool",
"ChatContext",
"ChatItem",
Expand Down Expand Up @@ -215,6 +220,8 @@ def __getattr__(name: str) -> typing.Any:
"FunctionCallEvent",
"FunctionCallOutputEvent",
"AgentHandoffEvent",
"DurableScheduler",
"EffectCall",
]

# Cleanup docs of unexported modules
Expand Down
24 changes: 24 additions & 0 deletions livekit-agents/livekit/agents/_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from livekit.protocol.agent_pb import agent_text


class AssignmentTimeoutError(Exception):
"""Raised when accepting a job but not receiving an assignment within the specified timeout.
Expand Down Expand Up @@ -102,6 +104,28 @@ class CLIError(Exception):
pass


class TextMessageError(Exception):
def __init__(
self,
message: str,
code: agent_text.TextMessageErrorCode = agent_text.INTERNAL_ERROR,
) -> None:
super().__init__(message)
self._message = message
self._code = code

@property
def message(self) -> str:
return self._message

@property
def code(self) -> agent_text.TextMessageErrorCode:
return self._code

def to_proto(self) -> agent_text.TextMessageError:
return agent_text.TextMessageError(message=self._message, code=self._code)


def create_api_error_from_http(
message: str = "",
*,
Expand Down
24 changes: 23 additions & 1 deletion livekit-agents/livekit/agents/beta/workflows/address.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from ... import llm, stt, tts, vad
from ...llm import Instructions
Expand All @@ -13,6 +14,7 @@
from ...voice.speech_handle import SpeechHandle

if TYPE_CHECKING:
from ...voice.agent import Agent, _AgentState
from ...voice.audio_recognition import TurnDetectionMode


Expand Down Expand Up @@ -75,7 +77,14 @@ def __init__(
tts: NotGivenOr[tts.TTS | None] = NOT_GIVEN,
allow_interruptions: NotGivenOr[bool] = NOT_GIVEN,
require_confirmation: NotGivenOr[bool] = NOT_GIVEN,
*,
setup_fnc: Callable[[Agent], None] | None = None,
) -> None:
self._init_kwargs = {
"extra_instructions": extra_instructions,
"allow_interruptions": allow_interruptions,
"require_confirmation": require_confirmation,
}
confirmation_instructions = (
"Call `confirm_address` after the user confirmed the address is correct."
)
Expand Down Expand Up @@ -106,12 +115,25 @@ def __init__(
llm=llm,
tts=tts,
allow_interruptions=allow_interruptions,
setup_fnc=setup_fnc,
)

self._current_address = ""
self._require_confirmation = require_confirmation
self._address_update_speech_handle: SpeechHandle | None = None

def export_init_kwargs(self) -> dict[str, Any]:
return self._init_kwargs

def _snapshot(self) -> _AgentState:
state = super()._snapshot()
state.extra_state["current_address"] = self._current_address
return state

def _restore(self, state: _AgentState) -> None:
super()._restore(state)
self._current_address = state.extra_state["current_address"]

async def on_enter(self) -> None:
self.session.generate_reply(instructions="Ask the user to provide their address.")

Expand Down
24 changes: 23 additions & 1 deletion livekit-agents/livekit/agents/beta/workflows/email_address.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from __future__ import annotations

import re
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from ... import llm, stt, tts, vad
from ...llm import Instructions
Expand All @@ -14,6 +15,7 @@
from ...voice.speech_handle import SpeechHandle

if TYPE_CHECKING:
from ...voice.agent import Agent, _AgentState
from ...voice.audio_recognition import TurnDetectionMode

EMAIL_REGEX = (
Expand Down Expand Up @@ -72,7 +74,14 @@ def __init__(
tts: NotGivenOr[tts.TTS | None] = NOT_GIVEN,
allow_interruptions: NotGivenOr[bool] = NOT_GIVEN,
require_confirmation: NotGivenOr[bool] = NOT_GIVEN,
*,
setup_fnc: Callable[[Agent], None] | None = None,
) -> None:
self._init_kwargs = {
"extra_instructions": extra_instructions,
"allow_interruptions": allow_interruptions,
"require_confirmation": require_confirmation,
}
confirmation_instructions = (
"Call `confirm_email_address` after the user confirmed the email address is correct."
)
Expand Down Expand Up @@ -103,6 +112,7 @@ def __init__(
llm=llm,
tts=tts,
allow_interruptions=allow_interruptions,
setup_fnc=setup_fnc,
)

self._current_email = ""
Expand All @@ -111,6 +121,9 @@ def __init__(
# used to ignore the call to confirm_email_address in case the LLM is hallucinating and not asking for user confirmation
self._email_update_speech_handle: SpeechHandle | None = None

def export_init_kwargs(self) -> dict[str, Any]:
return self._init_kwargs

async def on_enter(self) -> None:
self.session.generate_reply(instructions="Ask the user to provide an email address.")

Expand Down Expand Up @@ -173,3 +186,12 @@ def _confirmation_required(self, ctx: RunContext) -> bool:
if is_given(self._require_confirmation):
return self._require_confirmation
return ctx.speech_handle.input_details.modality == "audio"

def _snapshot(self) -> _AgentState:
state = super()._snapshot()
state.extra_state["current_email"] = self._current_email
return state

def _restore(self, state: _AgentState) -> None:
super()._restore(state)
self._current_email = state.extra_state["current_email"]
Loading
Loading