Skip to content

Commit 6ac8b6e

Browse files
committed
[LiveKit] Adds feature to add custom session name to LiveKit sessions
1 parent f8967ca commit 6ac8b6e

File tree

8 files changed

+189
-22
lines changed

8 files changed

+189
-22
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ See [cookbook/agno_agent.py](cookbook/agno_agent.py) for an example of tracing a
6060

6161
## Version changelog
6262

63+
### 3.12.1
64+
- feat: Added the ability to add custom session name and tags to LiveKit session
65+
- fix: Fixes conversation attachment being added multiple times
66+
6367
### 3.12.0
6468

6569
- feat: Added support for OpenAI Responses API format in addition to Chat Completion API

maxim/logger/livekit/agent_session.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import inspect
33
import traceback
44
import time
5-
from typing import Optional
65
import uuid
76
import weakref
87
from datetime import datetime, timezone
@@ -51,6 +50,20 @@ def intercept_session_start(self: AgentSession, room, room_name, agent: Agent):
5150
time.sleep(0.01)
5251
else:
5352
scribe().debug(f"[Internal][{self.__class__.__name__}] start not signaled within timeout; continuing")
53+
54+
# Check if this is a MaximWrappedAgentSession and extract custom parameters
55+
custom_session_name = "livekit-session" # default name
56+
custom_tags = {}
57+
58+
if hasattr(self, '_maxim_params') and hasattr(self, 'get_all_maxim_params'):
59+
# Use the typed methods for better type safety
60+
maxim_params = self.get_all_maxim_params()
61+
if maxim_params is not None:
62+
custom_session_name = maxim_params.get("session_name")
63+
custom_tags = maxim_params.get("tags", {})
64+
scribe().debug(f"[Internal] Found custom session name: {custom_session_name}")
65+
scribe().debug(f"[Internal] Found custom tags: {custom_tags}")
66+
5467
# getting the room_id
5568
if isinstance(room, str):
5669
room_id = room
@@ -65,9 +78,11 @@ def intercept_session_start(self: AgentSession, room, room_name, agent: Agent):
6578
scribe().debug(f"[Internal] Session key:{id(self)}")
6679
scribe().debug(f"[Internal] Room: {room_id}")
6780
scribe().debug(f"[Internal] Agent: {agent.instructions}")
81+
scribe().debug(f"[Internal] Custom session name: {custom_session_name}")
82+
6883
# creating trace as well
6984
session_id = str(uuid.uuid4())
70-
session = maxim_logger.session({"id": session_id, "name": "livekit-session"})
85+
session = maxim_logger.session({"id": session_id, "name": custom_session_name})
7186
# adding tags to the session
7287
if room_id is not None:
7388
session.add_tag("room_id", str(room_id))
@@ -77,6 +92,10 @@ def intercept_session_start(self: AgentSession, room, room_name, agent: Agent):
7792
session.add_tag("session_id", str(session_id))
7893
if agent is not None:
7994
session.add_tag("agent_id", str(id(agent)))
95+
96+
# Add custom tags from MaximWrappedAgentSession
97+
for key, value in custom_tags.items():
98+
session.add_tag(key, str(value))
8099
# If callback is set, emit the session started event
81100
callback = get_livekit_callback()
82101
if callback is not None:
@@ -97,6 +116,10 @@ def intercept_session_start(self: AgentSession, room, room_name, agent: Agent):
97116
tags["session_id"] = str(id(self))
98117
if agent is not None:
99118
tags["agent_id"] = str(id(agent))
119+
120+
# Add custom tags to trace tags
121+
for key, value in custom_tags.items():
122+
tags[key] = str(value)
100123

101124
trace = session.trace(
102125
{

maxim/logger/livekit/realtime_session.py

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import functools
2020
import inspect
21+
from io import BytesIO
2122
import time
2223
import traceback
2324
from uuid import uuid4
@@ -130,19 +131,56 @@ def handle_off(self):
130131
session_info = get_session_store().get_session_by_rt_session_id(id(self))
131132
if session_info is None:
132133
return
134+
133135
session_id = session_info.mx_session_id
134136
index = session_info.conversation_buffer_index
135-
if session_info.conversation_buffer.tell() == 0:
136-
return
137-
get_maxim_logger().session_add_attachment(
138-
session_id,
139-
FileDataAttachment(
140-
data=pcm16_to_wav_bytes(session_info.conversation_buffer.getvalue()),
141-
tags={"attach-to": "input"},
142-
name=f"Conversation part {index}",
143-
timestamp=int(time.time()),
144-
),
145-
)
137+
138+
# Handle the remaining conversation buffer content
139+
buffer_data = session_info.conversation_buffer.getvalue()
140+
buffer_size = len(buffer_data)
141+
142+
# If buffer is larger than 10MB, split it into chunks
143+
if buffer_size > 10 * 1024 * 1024:
144+
chunk_size = 10 * 1024 * 1024
145+
offset = 0
146+
current_index = index
147+
148+
while offset < buffer_size:
149+
chunk_end = min(offset + chunk_size, buffer_size)
150+
chunk_data = buffer_data[offset:chunk_end]
151+
152+
get_maxim_logger().session_add_attachment(
153+
session_id,
154+
FileDataAttachment(
155+
data=pcm16_to_wav_bytes(chunk_data),
156+
tags={"attach-to": "input"},
157+
name=f"Conversation part {current_index}",
158+
timestamp=int(time.time()),
159+
),
160+
)
161+
162+
offset = chunk_end
163+
current_index += 1
164+
165+
session_info.conversation_buffer_index = current_index
166+
else:
167+
# Buffer is small enough, add as single attachment
168+
get_maxim_logger().session_add_attachment(
169+
session_id,
170+
FileDataAttachment(
171+
data=pcm16_to_wav_bytes(buffer_data),
172+
tags={"attach-to": "input"},
173+
name=f"Conversation",
174+
timestamp=int(time.time()),
175+
),
176+
)
177+
178+
session_info.conversation_buffer_index = index + 1
179+
180+
# Mark attachment as added to prevent duplicates
181+
session_info.conversation_buffer = BytesIO()
182+
get_session_store().set_session(session_info)
183+
146184
get_maxim_logger().session_end(session_id=session_id)
147185

148186

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""
2+
MaximWrappedAgentSession - A wrapper around LiveKit's AgentSession with additional parameters.
3+
4+
This module provides a MaximWrappedAgentSession class that extends the original AgentSession
5+
with the ability to accept additional parameters for Maxim integration. This is useful for
6+
passing custom metadata (like session names and tags) that can be accessed during session
7+
instrumentation.
8+
9+
Usage:
10+
from maxim.logger.livekit import MaximWrappedAgentSession
11+
12+
# Instead of:
13+
# session = AgentSession(turn_detection="manual")
14+
15+
# Use:
16+
session = MaximWrappedAgentSession(
17+
turn_detection="manual",
18+
maxim_params={
19+
"session_name": "my-custom-session",
20+
"tags": {"user_id": "123", "department": "sales"}
21+
}
22+
)
23+
"""
24+
25+
from typing import Any, Dict, Optional, TypedDict
26+
from livekit.agents import AgentSession
27+
28+
29+
class MaximParams(TypedDict, total=False):
30+
"""Type definition for Maxim parameters.
31+
32+
Attributes:
33+
session_name: Optional custom name for the session
34+
tags: Optional dictionary of key-value tags to attach to the session
35+
"""
36+
session_name: str
37+
tags: Dict[str, Any]
38+
39+
40+
class MaximWrappedAgentSession(AgentSession):
41+
"""
42+
A wrapper around LiveKit's AgentSession that accepts additional parameters.
43+
44+
This class extends AgentSession to allow passing custom parameters for Maxim
45+
integration that can be accessed during session instrumentation, particularly
46+
in the intercept_session_start function.
47+
48+
Args:
49+
*args: Positional arguments passed to the original AgentSession
50+
maxim_params: Optional MaximParams with session_name and tags
51+
**kwargs: Keyword arguments passed to the original AgentSession
52+
53+
Example:
54+
session = MaximWrappedAgentSession(
55+
turn_detection="manual",
56+
maxim_params={
57+
"session_name": "customer-support-session",
58+
"tags": {"user_id": "user_123", "department": "sales"}
59+
}
60+
)
61+
"""
62+
63+
def __init__(self, *args, maxim_params: Optional[MaximParams] = None, **kwargs):
64+
"""
65+
Initialize the MaximWrappedAgentSession.
66+
67+
Args:
68+
*args: Positional arguments for AgentSession
69+
maxim_params: MaximParams with optional session_name and tags
70+
**kwargs: Keyword arguments for AgentSession
71+
"""
72+
# Initialize the parent AgentSession with original parameters
73+
super().__init__(*args, **kwargs)
74+
75+
# Store the additional parameters
76+
self._maxim_params: MaximParams = maxim_params or {}
77+
78+
def get_maxim_param(self, key: str, default: Any = None) -> Any:
79+
"""
80+
Get a specific Maxim parameter value.
81+
82+
Args:
83+
key: The parameter key to retrieve
84+
default: Default value if key is not found
85+
86+
Returns:
87+
The parameter value or default if not found
88+
"""
89+
return self._maxim_params.get(key, default)
90+
91+
def get_all_maxim_params(self) -> MaximParams:
92+
"""
93+
Get all Maxim parameters.
94+
95+
Returns:
96+
Copy of all custom parameters
97+
"""
98+
return self._maxim_params.copy()
99+

maxim/tests/test_livekit_interview_agent.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
from livekit import api as livekit_api
77
from livekit.agents import Agent, AgentSession, function_tool
88
from livekit.protocol.room import CreateRoomRequest
9-
from livekit.plugins import google
9+
from livekit.plugins import openai
1010
from maxim import Maxim
1111
from maxim.logger.livekit import instrument_livekit
1212
from tavily import TavilyClient
1313

14+
from maxim.logger.livekit.wrapped_agent_session import MaximWrappedAgentSession
15+
1416
# Load environment variables
1517
dotenv.load_dotenv(override=True)
1618
logging.basicConfig(level=logging.DEBUG)
@@ -126,8 +128,8 @@ async def entrypoint(ctx: agents.JobContext):
126128
)
127129
room = await lkapi.room.create_room(req)
128130
print(f"Room created: {room}")
129-
session = AgentSession(
130-
llm=google.beta.realtime.RealtimeModel(model="gemini-2.0-flash-exp", voice="Puck"),
131+
session = MaximWrappedAgentSession(
132+
llm=openai.realtime.RealtimeModel(voice="alloy"),
131133
)
132134
await session.start(room=room, agent=InterviewAgent(jd))
133135
await ctx.connect()

maxim/tests/test_livekit_ptt.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
from dotenv import load_dotenv
55

66
from livekit import rtc
7-
from livekit.agents import Agent, AgentSession, JobContext, JobRequest, RoomIO, WorkerOptions, cli
7+
from livekit.agents import Agent, JobContext, JobRequest, RoomIO, WorkerOptions, cli
88
from livekit.agents.llm import ChatContext, ChatMessage
99
from livekit.plugins import openai
1010
from maxim import Maxim
11-
from maxim.logger.livekit import instrument_livekit
11+
from maxim.logger.livekit import instrument_livekit, MaximWrappedAgentSession
1212

1313
logger = logging.getLogger("push-to-talk")
1414
logger.setLevel(logging.INFO)
@@ -40,7 +40,7 @@ async def on_user_turn_completed(self, turn_ctx: ChatContext, new_message: ChatM
4040

4141

4242
async def entrypoint(ctx: JobContext):
43-
session = AgentSession(turn_detection="manual")
43+
session = MaximWrappedAgentSession(turn_detection="manual", maxim_params={"session_name": "test-wrapped-session", "tags": {"user_id": "123", "department": "sales"}})
4444
room_io = RoomIO(session, room=ctx.room)
4545
await room_io.start()
4646

maxim/tests/test_livekit_realtime.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from tavily import TavilyClient
1212
from maxim import Maxim
1313
from maxim.logger.livekit import instrument_livekit
14+
from maxim.logger.livekit.wrapped_agent_session import MaximWrappedAgentSession
1415

1516
dotenv.load_dotenv(override=True)
1617
logging.basicConfig(level=logging.DEBUG)
@@ -22,7 +23,6 @@
2223
def on_event(event: str, data: dict):
2324
if event == "maxim.session.started":
2425
session_id = data["session_id"]
25-
logger.session({"id": session_id, "name": "custom session name"})
2626
elif event == "maxim.trace.started":
2727
trace_id = data["trace_id"]
2828
trace = data["trace"]
@@ -80,8 +80,9 @@ async def entrypoint(ctx: agents.JobContext):
8080
req
8181
) # :contentReference[oaicite:0]{index=0}
8282
print(f"Room created: {room}")
83-
session = AgentSession(
83+
session = MaximWrappedAgentSession(
8484
llm=openai.realtime.RealtimeModel(voice="coral"),
85+
maxim_params={"session_name": "realtime-check", "tags": {"user_id": "123", "department": "sales"}},
8586
)
8687
await session.start(room=room, agent=Assistant())
8788
await ctx.connect()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "maxim-py"
7-
version = "3.12.0"
7+
version = "3.12.1"
88
description = "A package that allows you to use the Maxim Python Library to interact with the Maxim Platform."
99
readme = "README.md"
1010
requires-python = ">=3.9.20"

0 commit comments

Comments
 (0)