cartesia-line 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cartesia-line might be problematic. Click here for more details.
- cartesia_line-0.0.1.dist-info/METADATA +25 -0
- cartesia_line-0.0.1.dist-info/RECORD +27 -0
- cartesia_line-0.0.1.dist-info/WHEEL +5 -0
- cartesia_line-0.0.1.dist-info/licenses/LICENSE +201 -0
- cartesia_line-0.0.1.dist-info/top_level.txt +1 -0
- line/__init__.py +29 -0
- line/bridge.py +348 -0
- line/bus.py +401 -0
- line/call_request.py +25 -0
- line/events.py +218 -0
- line/harness.py +257 -0
- line/harness_types.py +109 -0
- line/nodes/__init__.py +7 -0
- line/nodes/base.py +60 -0
- line/nodes/conversation_context.py +66 -0
- line/nodes/reasoning.py +223 -0
- line/routes.py +618 -0
- line/tools/__init__.py +9 -0
- line/tools/system_tools.py +120 -0
- line/tools/tool_types.py +39 -0
- line/user_bridge.py +200 -0
- line/utils/__init__.py +0 -0
- line/utils/aio.py +62 -0
- line/utils/gemini_utils.py +152 -0
- line/utils/openai_utils.py +122 -0
- line/voice_agent_app.py +147 -0
- line/voice_agent_system.py +230 -0
line/harness.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ConversationHarness - WebSocket communication layer for agents
|
|
3
|
+
Handles input/output queues and event coordination
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from asyncio import QueueEmpty
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from fastapi import WebSocket, WebSocketDisconnect
|
|
12
|
+
from loguru import logger
|
|
13
|
+
from pydantic import TypeAdapter
|
|
14
|
+
|
|
15
|
+
from line.events import (
|
|
16
|
+
AgentSpeechSent,
|
|
17
|
+
AgentStartedSpeaking,
|
|
18
|
+
AgentStoppedSpeaking,
|
|
19
|
+
UserStartedSpeaking,
|
|
20
|
+
UserStoppedSpeaking,
|
|
21
|
+
UserTranscriptionReceived,
|
|
22
|
+
UserUnknownInputReceived,
|
|
23
|
+
)
|
|
24
|
+
from line.harness_types import (
|
|
25
|
+
AgentSpeechInput,
|
|
26
|
+
AgentStateInput,
|
|
27
|
+
EndCallOutput,
|
|
28
|
+
ErrorOutput,
|
|
29
|
+
InputMessage,
|
|
30
|
+
LogEventOutput,
|
|
31
|
+
LogMetricOutput,
|
|
32
|
+
MessageOutput,
|
|
33
|
+
OutputMessage,
|
|
34
|
+
ToolCallOutput,
|
|
35
|
+
TranscriptionInput,
|
|
36
|
+
TransferOutput,
|
|
37
|
+
UserStateInput,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class State:
|
|
42
|
+
"""User voice states."""
|
|
43
|
+
|
|
44
|
+
SPEAKING = "speaking"
|
|
45
|
+
IDLE = "idle"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ConversationHarness:
|
|
49
|
+
"""
|
|
50
|
+
Manages WebSocket communication, input/output queues, and coordination events
|
|
51
|
+
for reasoning agents. Handles message parsing and event triggering.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
websocket: WebSocket,
|
|
57
|
+
shutdown_event: asyncio.Event,
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
Initialize the conversation harness
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
websocket: FastAPI WebSocket connection
|
|
64
|
+
shutdown_event: Event to signal shutdown
|
|
65
|
+
"""
|
|
66
|
+
self.websocket = websocket
|
|
67
|
+
|
|
68
|
+
# Use provided queues and events
|
|
69
|
+
self.input_queue = asyncio.Queue()
|
|
70
|
+
self.shutdown_event = shutdown_event
|
|
71
|
+
|
|
72
|
+
# Task management
|
|
73
|
+
self.input_task: Optional[asyncio.Task] = None
|
|
74
|
+
|
|
75
|
+
# State tracking
|
|
76
|
+
self.is_running = False
|
|
77
|
+
|
|
78
|
+
async def start(self):
|
|
79
|
+
"""
|
|
80
|
+
Start the harness tasks for input and output processing
|
|
81
|
+
"""
|
|
82
|
+
if self.is_running:
|
|
83
|
+
logger.warning("ConversationHarness already running")
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
self.is_running = True
|
|
87
|
+
logger.debug("Starting ConversationHarness")
|
|
88
|
+
|
|
89
|
+
# Start input and output tasks
|
|
90
|
+
self.input_task = asyncio.create_task(self._input_processor())
|
|
91
|
+
|
|
92
|
+
logger.debug("ConversationHarness started with input/output processors")
|
|
93
|
+
|
|
94
|
+
async def _input_processor(self):
|
|
95
|
+
"""
|
|
96
|
+
Continuously receive messages from WebSocket, parse them, and handle events
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
while not self.shutdown_event.is_set():
|
|
100
|
+
try:
|
|
101
|
+
# Receive message from WebSocket
|
|
102
|
+
message = await self.websocket.receive_json()
|
|
103
|
+
input = TypeAdapter(InputMessage).validate_python(message)
|
|
104
|
+
# Process the message and handle events
|
|
105
|
+
await self.input_queue.put(input)
|
|
106
|
+
|
|
107
|
+
except WebSocketDisconnect:
|
|
108
|
+
logger.info("WebSocket disconnected")
|
|
109
|
+
self.shutdown_event.set()
|
|
110
|
+
break
|
|
111
|
+
except json.JSONDecodeError as e:
|
|
112
|
+
logger.exception(f"Failed to parse JSON message: {e}")
|
|
113
|
+
continue
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.exception(f"Error in input processor: {e}")
|
|
116
|
+
if not self.shutdown_event.is_set():
|
|
117
|
+
await asyncio.sleep(0.1) # Brief pause before retry
|
|
118
|
+
|
|
119
|
+
except asyncio.CancelledError:
|
|
120
|
+
logger.info("Input processor cancelled")
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.exception(f"Unexpected error in input processor: {e}")
|
|
123
|
+
|
|
124
|
+
async def get(self) -> InputMessage:
|
|
125
|
+
"""Get a message from the input queue"""
|
|
126
|
+
return await self.input_queue.get()
|
|
127
|
+
|
|
128
|
+
async def _send(self, output: OutputMessage):
|
|
129
|
+
try:
|
|
130
|
+
if not self.shutdown_event.is_set():
|
|
131
|
+
await self.websocket.send_json(output.model_dump())
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.warning(f"Failed to send message via WebSocket: {e}")
|
|
134
|
+
self.shutdown_event.set()
|
|
135
|
+
|
|
136
|
+
async def end_call(self):
|
|
137
|
+
"""
|
|
138
|
+
Send end_call message and signal shutdown
|
|
139
|
+
"""
|
|
140
|
+
await self._send(EndCallOutput())
|
|
141
|
+
logger.info("End call message sent")
|
|
142
|
+
|
|
143
|
+
async def transfer_call(self, destination: str = ""):
|
|
144
|
+
"""
|
|
145
|
+
Send transfer_call message
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
destination: Optional destination for call transfer
|
|
149
|
+
"""
|
|
150
|
+
await self._send(TransferOutput(target_phone_number=destination))
|
|
151
|
+
logger.info(f"Transfer call message sent to {destination}")
|
|
152
|
+
self.shutdown_event.set()
|
|
153
|
+
|
|
154
|
+
async def send_message(self, message: str):
|
|
155
|
+
"""Send a message via WebSocket with connection state checking"""
|
|
156
|
+
logger.info(f'🤖 Agent said: "{message}"')
|
|
157
|
+
await self._send(MessageOutput(content=message))
|
|
158
|
+
|
|
159
|
+
async def send_error(self, error: str):
|
|
160
|
+
"""Send an error message via WebSocket with connection state checking"""
|
|
161
|
+
await self._send(ErrorOutput(content=error))
|
|
162
|
+
|
|
163
|
+
async def send_tool_call(
|
|
164
|
+
self,
|
|
165
|
+
tool_name: str,
|
|
166
|
+
tool_args: Dict[str, Any],
|
|
167
|
+
tool_call_id: Optional[str] = None,
|
|
168
|
+
result: Optional[str] = None,
|
|
169
|
+
):
|
|
170
|
+
"""Send a tool call result via WebSocket with connection state checking"""
|
|
171
|
+
await self._send(
|
|
172
|
+
ToolCallOutput(
|
|
173
|
+
name=tool_name,
|
|
174
|
+
arguments=tool_args,
|
|
175
|
+
result=result,
|
|
176
|
+
id=tool_call_id,
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
async def log_event(self, event: str, metadata: Optional[Dict[str, Any]] = None):
|
|
181
|
+
"""
|
|
182
|
+
Send a log event via WebSocket
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
event: The event name/type being logged
|
|
186
|
+
metadata: Optional metadata dictionary for the event
|
|
187
|
+
"""
|
|
188
|
+
logger.debug(f"📊 Logging event: {event}" + (f" - {metadata}" if metadata else ""))
|
|
189
|
+
await self._send(LogEventOutput(event=event, metadata=metadata))
|
|
190
|
+
|
|
191
|
+
async def log_metric(self, name: str, value: Any):
|
|
192
|
+
"""
|
|
193
|
+
Send a log metric via WebSocket
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
name: The metric name
|
|
197
|
+
value: The metric value (can be any JSON-serializable type)
|
|
198
|
+
"""
|
|
199
|
+
logger.debug(f"📈 Logging metric: {name}={value}")
|
|
200
|
+
await self._send(LogMetricOutput(name=name, value=value))
|
|
201
|
+
|
|
202
|
+
async def cleanup(self):
|
|
203
|
+
"""
|
|
204
|
+
Clean up resources and stop all tasks
|
|
205
|
+
"""
|
|
206
|
+
logger.info("Cleaning up ConversationHarness")
|
|
207
|
+
|
|
208
|
+
# Signal shutdown
|
|
209
|
+
self.shutdown_event.set()
|
|
210
|
+
self.is_running = False
|
|
211
|
+
|
|
212
|
+
# Cancel tasks
|
|
213
|
+
if self.input_task and not self.input_task.done():
|
|
214
|
+
self.input_task.cancel()
|
|
215
|
+
try:
|
|
216
|
+
await self.input_task
|
|
217
|
+
except asyncio.CancelledError:
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
# Clear any remaining messages in queues
|
|
221
|
+
while not self.input_queue.empty():
|
|
222
|
+
try:
|
|
223
|
+
self.input_queue.get_nowait()
|
|
224
|
+
self.input_queue.task_done()
|
|
225
|
+
except QueueEmpty:
|
|
226
|
+
break
|
|
227
|
+
|
|
228
|
+
logger.info("ConversationHarness cleanup completed")
|
|
229
|
+
|
|
230
|
+
def map_to_events(self, message: InputMessage) -> List[Any]:
|
|
231
|
+
"""Convert harness-specific message to bus events."""
|
|
232
|
+
if isinstance(message, UserStateInput):
|
|
233
|
+
if message.value == State.SPEAKING:
|
|
234
|
+
logger.info("🎤 User started speaking")
|
|
235
|
+
return [UserStartedSpeaking()]
|
|
236
|
+
elif message.value == State.IDLE:
|
|
237
|
+
logger.info("🔇 User stopped speaking")
|
|
238
|
+
return [UserStoppedSpeaking()]
|
|
239
|
+
elif isinstance(message, TranscriptionInput):
|
|
240
|
+
logger.info(f'📝 User said: "{message.content}"')
|
|
241
|
+
return [UserTranscriptionReceived(content=message.content)]
|
|
242
|
+
elif isinstance(message, AgentStateInput):
|
|
243
|
+
if message.value == State.SPEAKING:
|
|
244
|
+
logger.info("🎤 Agent started speaking")
|
|
245
|
+
return [AgentStartedSpeaking()]
|
|
246
|
+
elif message.value == State.IDLE:
|
|
247
|
+
logger.info("🔇 Agent stopped speaking")
|
|
248
|
+
return [AgentStoppedSpeaking()]
|
|
249
|
+
elif isinstance(message, AgentSpeechInput):
|
|
250
|
+
logger.info(f'🗣️ Agent speech sent: "{message.content}"')
|
|
251
|
+
return [AgentSpeechSent(content=message.content)]
|
|
252
|
+
else:
|
|
253
|
+
# Fallback for unknown types.
|
|
254
|
+
logger.warning(f"Unknown message type: {type(message).__name__} ({message.model_dump_json()})")
|
|
255
|
+
return [UserUnknownInputReceived(input_data=message.model_dump_json())]
|
|
256
|
+
|
|
257
|
+
return [] # No events for unhandled states
|
line/harness_types.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from typing import Dict, Literal, Optional, Union
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
########################################################
|
|
6
|
+
# Copied and adapted from Bifrost agent_types.py
|
|
7
|
+
########################################################
|
|
8
|
+
|
|
9
|
+
# Input messages to be sent over the websocket to the user code
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TranscriptionInput(BaseModel):
|
|
13
|
+
content: str
|
|
14
|
+
type: Literal["message"] = "message"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DTMFInput(BaseModel):
|
|
18
|
+
button: str
|
|
19
|
+
type: Literal["dtmf"] = "dtmf"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UserStateInput(BaseModel):
|
|
23
|
+
value: str
|
|
24
|
+
type: Literal["user_state"] = "user_state"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AgentStateInput(BaseModel):
|
|
28
|
+
value: str
|
|
29
|
+
type: Literal["agent_state"] = "agent_state"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ValidationErrorInput(BaseModel):
|
|
33
|
+
error_message: str
|
|
34
|
+
error_type: str
|
|
35
|
+
type: Literal["validation_error"] = "validation_error"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AgentSpeechInput(BaseModel):
|
|
39
|
+
content: str
|
|
40
|
+
type: Literal["agent_speech"] = "agent_speech"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
InputMessage = Union[
|
|
44
|
+
TranscriptionInput,
|
|
45
|
+
DTMFInput,
|
|
46
|
+
UserStateInput,
|
|
47
|
+
AgentStateInput,
|
|
48
|
+
ValidationErrorInput,
|
|
49
|
+
AgentSpeechInput,
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Output messages to be received from the user code
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ErrorOutput(BaseModel):
|
|
57
|
+
type: Literal["error"] = "error"
|
|
58
|
+
content: str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class DTMFOutput(BaseModel):
|
|
62
|
+
type: Literal["dtmf"] = "dtmf"
|
|
63
|
+
button: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class MessageOutput(BaseModel):
|
|
67
|
+
type: Literal["message"] = "message"
|
|
68
|
+
content: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ToolCallOutput(BaseModel):
|
|
72
|
+
type: Literal["tool_call"] = "tool_call"
|
|
73
|
+
name: str
|
|
74
|
+
arguments: Dict[str, object]
|
|
75
|
+
result: Optional[str] = None
|
|
76
|
+
id: Optional[str] = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TransferOutput(BaseModel):
|
|
80
|
+
type: Literal["transfer"] = "transfer"
|
|
81
|
+
target_phone_number: str
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class EndCallOutput(BaseModel):
|
|
85
|
+
type: Literal["end_call"] = "end_call"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class LogEventOutput(BaseModel):
|
|
89
|
+
type: Literal["log_event"] = "log_event"
|
|
90
|
+
event: str
|
|
91
|
+
metadata: Optional[Dict[str, object]] = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class LogMetricOutput(BaseModel):
|
|
95
|
+
type: Literal["log_metric"] = "log_metric"
|
|
96
|
+
name: str
|
|
97
|
+
value: object
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
OutputMessage = Union[
|
|
101
|
+
ErrorOutput,
|
|
102
|
+
DTMFOutput,
|
|
103
|
+
MessageOutput,
|
|
104
|
+
ToolCallOutput,
|
|
105
|
+
TransferOutput,
|
|
106
|
+
EndCallOutput,
|
|
107
|
+
LogEventOutput,
|
|
108
|
+
LogMetricOutput,
|
|
109
|
+
]
|
line/nodes/__init__.py
ADDED
line/nodes/base.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, AsyncGenerator, Optional
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
|
|
6
|
+
from line.bus import Message
|
|
7
|
+
from line.events import EventType
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from line.bridge import Bridge
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Node:
|
|
14
|
+
"""A base class for all nodes.
|
|
15
|
+
|
|
16
|
+
Nodes are the building blocks of the agentic system. They are responsible for:
|
|
17
|
+
- Maintaining state
|
|
18
|
+
- Generating responses
|
|
19
|
+
- Handling tool calls
|
|
20
|
+
- Interrupting the generation process
|
|
21
|
+
|
|
22
|
+
Nodes are stateful, and can be used to build multi-agent workflows.
|
|
23
|
+
|
|
24
|
+
All nodes have an `id` that is used to identify them.
|
|
25
|
+
When a :class:`Bridge` is created from a node, the node's `id` is used to identify the node in the bridge.
|
|
26
|
+
It can be used when filtering by `source`.
|
|
27
|
+
We do not require that nodes have a unique `id`.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, node_id: Optional[str] = None):
|
|
31
|
+
self.id = node_id or uuid4().hex
|
|
32
|
+
self._bridge: Optional[Bridge] = None
|
|
33
|
+
|
|
34
|
+
async def start(self):
|
|
35
|
+
"""Start the node, in an async context.
|
|
36
|
+
|
|
37
|
+
This method is called when the VoiceAgentSystem is started. Use this method to run
|
|
38
|
+
initialization logic that needs to run in an async context (eg, database connections).
|
|
39
|
+
"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def __str__(self):
|
|
43
|
+
return f"{type(self).__name__}(id={self.id})"
|
|
44
|
+
|
|
45
|
+
async def cleanup(self):
|
|
46
|
+
"""Clean up the node."""
|
|
47
|
+
logger.debug(f"{self} cleanup completed")
|
|
48
|
+
|
|
49
|
+
def on_interrupt_generate(self, message: Message) -> None:
|
|
50
|
+
"""Handle interrupt event.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
message: The interrupt message.
|
|
54
|
+
"""
|
|
55
|
+
logger.debug(f"{self} interrupt received.")
|
|
56
|
+
|
|
57
|
+
async def generate(self, message: Message) -> AsyncGenerator[EventType, None]:
|
|
58
|
+
"""Generate a response to the message."""
|
|
59
|
+
raise NotImplementedError("Subclasses must implement `generate`.")
|
|
60
|
+
yield
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ConversationContext - Data structure for conversation state in ReasoningNode template method.
|
|
3
|
+
|
|
4
|
+
This class provides a clean abstraction for conversation data that gets passed
|
|
5
|
+
to specialized processing methods in ReasoningNode subclasses.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, List, Optional
|
|
10
|
+
|
|
11
|
+
from line.events import EventInstance, UserTranscriptionReceived
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ConversationContext:
|
|
16
|
+
"""
|
|
17
|
+
Encapsulates conversation state for ReasoningNode template method pattern.
|
|
18
|
+
|
|
19
|
+
This standardizes how conversation data is passed between the template method
|
|
20
|
+
(ReasoningNode.generate) and specialized processing (process_context).
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
events: List of conversation events
|
|
24
|
+
system_prompt: The system prompt for this reasoning node
|
|
25
|
+
metadata: Additional context data for specialized processing
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
events: List[EventInstance]
|
|
29
|
+
system_prompt: str
|
|
30
|
+
metadata: dict = None
|
|
31
|
+
|
|
32
|
+
def __post_init__(self):
|
|
33
|
+
"""Initialize metadata if not provided."""
|
|
34
|
+
if self.metadata is None:
|
|
35
|
+
self.metadata = {}
|
|
36
|
+
|
|
37
|
+
def format_events(self, max_messages: int = None) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Format conversation messages as a string for LLM prompts.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
max_messages: Maximum number of recent messages to include
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Formatted conversation string
|
|
46
|
+
"""
|
|
47
|
+
events = self.events
|
|
48
|
+
if max_messages is not None:
|
|
49
|
+
events = events[-max_messages:]
|
|
50
|
+
|
|
51
|
+
return "\n".join(f"{type(event)}: {event}" for event in events)
|
|
52
|
+
|
|
53
|
+
def get_latest_user_transcript_message(self) -> Optional[str]:
|
|
54
|
+
"""Get the most recent user message content."""
|
|
55
|
+
for msg in reversed(self.events):
|
|
56
|
+
if isinstance(msg, UserTranscriptionReceived):
|
|
57
|
+
return msg.content
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def get_event_count(self) -> int:
|
|
61
|
+
"""Get total number of messages in context."""
|
|
62
|
+
return len(self.events)
|
|
63
|
+
|
|
64
|
+
def add_metadata(self, key: str, value: Any) -> None:
|
|
65
|
+
"""Add metadata for specialized processing."""
|
|
66
|
+
self.metadata[key] = value
|