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/bus.py
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bus - Typed event routing system for agent communication.
|
|
3
|
+
|
|
4
|
+
Routes typed events between agents using broadcast or request-response patterns.
|
|
5
|
+
Provides type-safe event handling with Pydantic validation and fluent subscription API.
|
|
6
|
+
|
|
7
|
+
Examples:
|
|
8
|
+
Create and start bus::
|
|
9
|
+
|
|
10
|
+
bus = Bus()
|
|
11
|
+
await bus.start()
|
|
12
|
+
|
|
13
|
+
Register agents::
|
|
14
|
+
|
|
15
|
+
bus.register_bridge("clipboard", clipboard_bridge)
|
|
16
|
+
|
|
17
|
+
Send typed events::
|
|
18
|
+
|
|
19
|
+
await bus.broadcast(FormCompleted(node_id="intake", status="done"))
|
|
20
|
+
response = await bus.call(ToolCall(node_id="agent", tool_name="calculator"))
|
|
21
|
+
|
|
22
|
+
Subscribe to events::
|
|
23
|
+
bridge.on(UserTranscriptionReceived, source="intake").map(handler)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import asyncio
|
|
27
|
+
import time
|
|
28
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
29
|
+
import uuid
|
|
30
|
+
|
|
31
|
+
from loguru import logger
|
|
32
|
+
from pydantic import BaseModel, Field
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from line.bridge import Bridge
|
|
36
|
+
|
|
37
|
+
from line.events import AgentHandoff, Authorize, ToolCall
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Message(BaseModel):
|
|
41
|
+
"""Message sent between agents through the bus."""
|
|
42
|
+
|
|
43
|
+
# The unique identifier for the message.
|
|
44
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
45
|
+
|
|
46
|
+
# The source of the message.
|
|
47
|
+
source: str
|
|
48
|
+
# The event being sent.
|
|
49
|
+
event: Any
|
|
50
|
+
|
|
51
|
+
# The timestamp when the message was created.
|
|
52
|
+
timestamp: float = Field(default_factory=time.time)
|
|
53
|
+
|
|
54
|
+
def __str__(self) -> str:
|
|
55
|
+
return (
|
|
56
|
+
f"f{type(self)}(id={self.id}, "
|
|
57
|
+
f"source={self.source}, "
|
|
58
|
+
f"event={type(self.event)}({self.event}), "
|
|
59
|
+
f"timestamp={self.timestamp})"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Bus:
|
|
64
|
+
"""
|
|
65
|
+
Routes messages between agents in memory.
|
|
66
|
+
|
|
67
|
+
Handles broadcast events and request-response calls.
|
|
68
|
+
Provides event-driven communication between agents and bridges.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
# ================================================================
|
|
72
|
+
# Lifecycle Methods
|
|
73
|
+
# ================================================================
|
|
74
|
+
|
|
75
|
+
def __init__(self, max_queue_size: int = 1000):
|
|
76
|
+
"""
|
|
77
|
+
Create agent bus.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
max_queue_size: Max messages before dropping.
|
|
81
|
+
"""
|
|
82
|
+
self.max_queue_size = max_queue_size # Prevents memory overflow during high message volume.
|
|
83
|
+
|
|
84
|
+
self.message_queue = asyncio.Queue(
|
|
85
|
+
maxsize=max_queue_size
|
|
86
|
+
) # Decouples message sending from processing for async handling.
|
|
87
|
+
self.running = False # Prevents router from processing messages during shutdown.
|
|
88
|
+
self.router_task: Optional[asyncio.Task] = None # Allows graceful cancellation during cleanup.
|
|
89
|
+
|
|
90
|
+
self.bridges: Dict[str, "Bridge"] = {} # node_id → Bridge instance.
|
|
91
|
+
|
|
92
|
+
self.pending_requests: Dict[
|
|
93
|
+
str, asyncio.Future
|
|
94
|
+
] = {} # Allows synchronous-style calls over async message bus.
|
|
95
|
+
|
|
96
|
+
self.shutdown_event = asyncio.Event() # Ensures all tasks stop together.
|
|
97
|
+
|
|
98
|
+
async def start(self) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Start the message router.
|
|
101
|
+
|
|
102
|
+
Initializes background task for message routing between bridges.
|
|
103
|
+
"""
|
|
104
|
+
if self.running:
|
|
105
|
+
logger.warning("Bus already running")
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
self.running = True
|
|
109
|
+
|
|
110
|
+
# Log system state summary before starting router
|
|
111
|
+
self._log_system_summary()
|
|
112
|
+
|
|
113
|
+
self.router_task = asyncio.create_task(self._message_router())
|
|
114
|
+
|
|
115
|
+
logger.info("Bus message router started")
|
|
116
|
+
|
|
117
|
+
async def cleanup(self) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Stop message routing and clean up resources.
|
|
120
|
+
|
|
121
|
+
Cancels all background tasks, cleans up pending requests,
|
|
122
|
+
and ensures graceful shutdown of all bus components.
|
|
123
|
+
"""
|
|
124
|
+
logger.info("Cleaning up Bus")
|
|
125
|
+
self.running = False
|
|
126
|
+
self.shutdown_event.set()
|
|
127
|
+
|
|
128
|
+
# Cancel router task.
|
|
129
|
+
if self.router_task and not self.router_task.done():
|
|
130
|
+
self.router_task.cancel()
|
|
131
|
+
try:
|
|
132
|
+
await self.router_task
|
|
133
|
+
except asyncio.CancelledError:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
# Cancel pending requests to prevent hanging futures.
|
|
137
|
+
for future in self.pending_requests.values():
|
|
138
|
+
if not future.done():
|
|
139
|
+
future.cancel()
|
|
140
|
+
|
|
141
|
+
self.pending_requests.clear()
|
|
142
|
+
logger.info("Bus cleanup completed")
|
|
143
|
+
|
|
144
|
+
def _log_system_summary(self) -> None:
|
|
145
|
+
"""Log a clean visual summary of the Bus state before starting."""
|
|
146
|
+
bridge_count = len(self.bridges)
|
|
147
|
+
|
|
148
|
+
# Build visual representation
|
|
149
|
+
summary_lines = [
|
|
150
|
+
"🚌 Bus System Ready",
|
|
151
|
+
"=" * 60,
|
|
152
|
+
f"📊 System Overview: {bridge_count} bridges registered",
|
|
153
|
+
"",
|
|
154
|
+
"🏗️ System Architecture:",
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
# Group bridges by type for better visualization
|
|
158
|
+
node_bridges = []
|
|
159
|
+
system_bridges = []
|
|
160
|
+
|
|
161
|
+
for name in sorted(self.bridges.keys()):
|
|
162
|
+
bridge = self.bridges[name]
|
|
163
|
+
route_count = len(getattr(bridge, "routes", {}))
|
|
164
|
+
|
|
165
|
+
# Get route patterns for this bridge
|
|
166
|
+
route_patterns = []
|
|
167
|
+
if hasattr(bridge, "routes"):
|
|
168
|
+
route_patterns = list(bridge.routes.keys())[:3] # Show first 3 routes
|
|
169
|
+
if len(bridge.routes) > 3:
|
|
170
|
+
route_patterns.append("...")
|
|
171
|
+
|
|
172
|
+
auth_info = ""
|
|
173
|
+
if hasattr(bridge, "authorized_nodes") and bridge.authorized_nodes:
|
|
174
|
+
auth_nodes = sorted(bridge.authorized_nodes)
|
|
175
|
+
auth_info = f" 🔐[{', '.join(auth_nodes)}]"
|
|
176
|
+
|
|
177
|
+
route_info = f"({route_count} routes)" if route_count else "(no routes)"
|
|
178
|
+
pattern_info = f" → {route_patterns}" if route_patterns else ""
|
|
179
|
+
|
|
180
|
+
bridge_line = f" 📡 {name:<12} {route_info}{auth_info}{pattern_info}"
|
|
181
|
+
|
|
182
|
+
if name in ["user", "tools", "state"]:
|
|
183
|
+
system_bridges.append(bridge_line)
|
|
184
|
+
else:
|
|
185
|
+
node_bridges.append(bridge_line)
|
|
186
|
+
|
|
187
|
+
# Add system bridges
|
|
188
|
+
if system_bridges:
|
|
189
|
+
summary_lines.append(" 🔧 System Bridges:")
|
|
190
|
+
summary_lines.extend(system_bridges)
|
|
191
|
+
summary_lines.append("")
|
|
192
|
+
|
|
193
|
+
# Add node bridges
|
|
194
|
+
if node_bridges:
|
|
195
|
+
summary_lines.append(" 🧠 Agent Bridges:")
|
|
196
|
+
summary_lines.extend(node_bridges)
|
|
197
|
+
summary_lines.append("")
|
|
198
|
+
|
|
199
|
+
summary_lines.extend(
|
|
200
|
+
[
|
|
201
|
+
" 🔄 Message Flow: Bridges ↔ Bus ↔ All Bridges",
|
|
202
|
+
"=" * 60,
|
|
203
|
+
"🎯 Starting message router...",
|
|
204
|
+
]
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Log as single message
|
|
208
|
+
logger.info("\n" + "\n".join(summary_lines))
|
|
209
|
+
|
|
210
|
+
# ================================================================
|
|
211
|
+
# Registration & Configuration
|
|
212
|
+
# ================================================================
|
|
213
|
+
|
|
214
|
+
def register_bridge(self, node_id: str, bridge: "Bridge") -> None:
|
|
215
|
+
"""
|
|
216
|
+
Register event bridge.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
node_id: Node identifier.
|
|
220
|
+
bridge: Bridge for event routing.
|
|
221
|
+
"""
|
|
222
|
+
self.bridges[node_id] = bridge
|
|
223
|
+
bridge.set_bus(self)
|
|
224
|
+
|
|
225
|
+
# ================================================================
|
|
226
|
+
# Public Messaging API
|
|
227
|
+
# ================================================================
|
|
228
|
+
|
|
229
|
+
async def broadcast(self, message: Message) -> None:
|
|
230
|
+
"""
|
|
231
|
+
Send message to all matching bridges.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
message: Message to broadcast.
|
|
235
|
+
|
|
236
|
+
Examples:
|
|
237
|
+
Basic message broadcast::
|
|
238
|
+
|
|
239
|
+
await bus.broadcast(Message(
|
|
240
|
+
source="intake",
|
|
241
|
+
event=ConversationTurn(role="user", content="Hello"),
|
|
242
|
+
))
|
|
243
|
+
|
|
244
|
+
Form completion broadcast::
|
|
245
|
+
|
|
246
|
+
await bus.broadcast(Message(
|
|
247
|
+
source="intake",
|
|
248
|
+
event=FormCompleted(status="done", fields={"name": "John"}),
|
|
249
|
+
))
|
|
250
|
+
|
|
251
|
+
See Also:
|
|
252
|
+
:meth:`call` - For responses
|
|
253
|
+
:meth:`request` - For specific agents
|
|
254
|
+
"""
|
|
255
|
+
logger.debug(f"Bus: Broadcasting message: {message}")
|
|
256
|
+
await self._queue_message(message)
|
|
257
|
+
|
|
258
|
+
# ================================================================
|
|
259
|
+
# Message Routing
|
|
260
|
+
# ================================================================
|
|
261
|
+
|
|
262
|
+
async def _queue_message(self, message: Message) -> None:
|
|
263
|
+
"""
|
|
264
|
+
Queue message for the router to process.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
message: Message to queue.
|
|
268
|
+
"""
|
|
269
|
+
try:
|
|
270
|
+
await self.message_queue.put(message)
|
|
271
|
+
logger.debug(f"Bus (_queue_message): Message queued: {message}")
|
|
272
|
+
except asyncio.QueueFull:
|
|
273
|
+
logger.error("Bus message queue is full, dropping message")
|
|
274
|
+
|
|
275
|
+
def _get_queue_info_synchronous(self) -> Dict[str, Any]:
|
|
276
|
+
"""Get information about what is on the queue.
|
|
277
|
+
|
|
278
|
+
Note:
|
|
279
|
+
This is incredibly expensive to do (in the land of low latency).
|
|
280
|
+
So call this method only for debugging purposes.
|
|
281
|
+
Never push code that calls this method to production.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
A dictionary of the `self.message_queue` information.
|
|
285
|
+
"""
|
|
286
|
+
return {
|
|
287
|
+
"queue_size": self.message_queue.qsize(),
|
|
288
|
+
"max_queue_size": self.max_queue_size,
|
|
289
|
+
"is_full": self.message_queue.full(),
|
|
290
|
+
"is_empty": self.message_queue.empty(),
|
|
291
|
+
"messages": self._peek_queue_contents(),
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
def _peek_queue_contents(self) -> List[Message]:
|
|
295
|
+
"""
|
|
296
|
+
Synchronously peek at all messages in the queue.
|
|
297
|
+
Note: This creates a copy of the queue contents and is not thread-safe.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
List of Message objects currently in the queue.
|
|
301
|
+
"""
|
|
302
|
+
messages = []
|
|
303
|
+
temp_queue = asyncio.Queue()
|
|
304
|
+
|
|
305
|
+
# Drain the original queue into a temporary queue
|
|
306
|
+
while not self.message_queue.empty():
|
|
307
|
+
try:
|
|
308
|
+
# Use get_nowait() to avoid blocking
|
|
309
|
+
message = self.message_queue.get_nowait()
|
|
310
|
+
messages.append(message)
|
|
311
|
+
temp_queue.put_nowait(message)
|
|
312
|
+
except asyncio.QueueEmpty:
|
|
313
|
+
break
|
|
314
|
+
|
|
315
|
+
# Restore the original queue
|
|
316
|
+
while not temp_queue.empty():
|
|
317
|
+
try:
|
|
318
|
+
message = temp_queue.get_nowait()
|
|
319
|
+
self.message_queue.put_nowait(message)
|
|
320
|
+
except asyncio.QueueFull:
|
|
321
|
+
break
|
|
322
|
+
|
|
323
|
+
return messages
|
|
324
|
+
|
|
325
|
+
async def _message_router(self) -> None:
|
|
326
|
+
"""Main message routing loop that processes queued messages."""
|
|
327
|
+
logger.info("Bus message router started")
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
while self.running:
|
|
331
|
+
try:
|
|
332
|
+
# Timeout allows checking shutdown flag periodically.
|
|
333
|
+
# logger.debug(f"Bus: Waiting for message in the _message_router")
|
|
334
|
+
message = await asyncio.wait_for(self.message_queue.get(), timeout=1.0)
|
|
335
|
+
logger.debug(f"Bus: Message received: {message}")
|
|
336
|
+
|
|
337
|
+
# Delegate to specific routing logic based on pattern.
|
|
338
|
+
await self._route_message(message)
|
|
339
|
+
|
|
340
|
+
except asyncio.TimeoutError:
|
|
341
|
+
# Prevents infinite blocking during shutdown.
|
|
342
|
+
continue
|
|
343
|
+
except Exception as e:
|
|
344
|
+
logger.exception(f"Error in message router: {e}")
|
|
345
|
+
|
|
346
|
+
except asyncio.CancelledError:
|
|
347
|
+
logger.info("Message router cancelled")
|
|
348
|
+
except Exception as e:
|
|
349
|
+
logger.exception(f"Unexpected error in message router: {e}")
|
|
350
|
+
|
|
351
|
+
async def _route_message(self, message: Message) -> None:
|
|
352
|
+
"""Route message to bridges based on direct or broadcast pattern."""
|
|
353
|
+
logger.debug(f"Bus: Routing message: {message}")
|
|
354
|
+
try:
|
|
355
|
+
# Handle agent handoff events or transfer_to_* tool calls directly.
|
|
356
|
+
# TODO: Consider adding certain types of events as being high priority to handle.
|
|
357
|
+
# For example, it would be reasonable to prioritize these events:
|
|
358
|
+
# - AgentResponse should be sent to the user bridge immediately.
|
|
359
|
+
# - Interruption events should be processed by the interruption routes first.
|
|
360
|
+
event = message.event
|
|
361
|
+
if isinstance(event, AgentHandoff):
|
|
362
|
+
logger.info(f"Bus: Handling handoff to {event.target_agent}")
|
|
363
|
+
await self._handle_handoff(message, event.target_agent)
|
|
364
|
+
return
|
|
365
|
+
elif isinstance(event, ToolCall) and event.tool_name.startswith("transfer_to_"):
|
|
366
|
+
target_agent = event.tool_name.replace("transfer_to_", "")
|
|
367
|
+
logger.info(f"Bus: Handling handoff to {target_agent}")
|
|
368
|
+
await self._handle_handoff(message, target_agent)
|
|
369
|
+
return
|
|
370
|
+
else:
|
|
371
|
+
# Broadcast to all bridges
|
|
372
|
+
tasks = []
|
|
373
|
+
for _, bridge in self.bridges.items():
|
|
374
|
+
# NOTE: Do not await this. We want to fire and forget.
|
|
375
|
+
# We want the bridges to process the tasks in the background.
|
|
376
|
+
# This is to ensure that future messages are not blocked by the task.
|
|
377
|
+
# NOTE: There is an implicit ordering here determined by the order of the bridges.
|
|
378
|
+
# TODO: Consider making this explicit.
|
|
379
|
+
tasks.append(asyncio.create_task(bridge.handle_event(message)))
|
|
380
|
+
|
|
381
|
+
except Exception as e:
|
|
382
|
+
logger.exception(f"Error routing message {message.id}: {e}")
|
|
383
|
+
|
|
384
|
+
async def _handle_handoff(self, message: Message, target_agent: str) -> None:
|
|
385
|
+
"""Handle handoff from transfer_to_* tool calls."""
|
|
386
|
+
from_agent = message.source
|
|
387
|
+
|
|
388
|
+
# Get reason from the event
|
|
389
|
+
reason = ""
|
|
390
|
+
if isinstance(message.event, AgentHandoff):
|
|
391
|
+
reason = message.event.reason
|
|
392
|
+
elif isinstance(message.event, ToolCall):
|
|
393
|
+
reason = message.event.tool_args.get("reason", "")
|
|
394
|
+
|
|
395
|
+
logger.info(f"Processing handoff: {from_agent} -> {target_agent} ({reason})")
|
|
396
|
+
|
|
397
|
+
# Tell user bridge to change authorization
|
|
398
|
+
user_auth_event = Authorize(node_id="system", agent=target_agent)
|
|
399
|
+
await self.broadcast(user_auth_event)
|
|
400
|
+
|
|
401
|
+
logger.info(f"Handoff completed: {from_agent} -> {target_agent}")
|
line/call_request.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PreCallResult(BaseModel):
|
|
7
|
+
"""Result from pre_call_handler containing metadata and config."""
|
|
8
|
+
|
|
9
|
+
metadata: Dict[str, Any] = Field(default_factory=dict, description="Metadata to include with the call")
|
|
10
|
+
config: Dict[str, Any] = Field(default_factory=dict, description="Configuration for the call")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CallRequest(BaseModel):
|
|
14
|
+
"""Request body for the /chats endpoint."""
|
|
15
|
+
|
|
16
|
+
call_id: str
|
|
17
|
+
from_: str = Field(alias="from") # Using from_ to avoid Python keyword conflict
|
|
18
|
+
to: str
|
|
19
|
+
agent_call_id: str # Agent call ID for logging and correlation
|
|
20
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
21
|
+
|
|
22
|
+
model_config = ConfigDict(
|
|
23
|
+
# Allow both field name (from_) and alias (from) for input
|
|
24
|
+
populate_by_name=True
|
|
25
|
+
)
|
line/events.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Typed event definitions for the agent bus system.
|
|
3
|
+
|
|
4
|
+
Each event inherits from EventMeta which provides automatic node identification
|
|
5
|
+
and instance tracking for distributed agent communication.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, Dict, Optional, Type, TypeVar, Union
|
|
10
|
+
import uuid
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
EventInstance = T
|
|
16
|
+
|
|
17
|
+
# Type[T] means EventType has to be a class (not an instance), and it allows us to refer to T in
|
|
18
|
+
# order to instantiate T later.
|
|
19
|
+
EventType = Type[T]
|
|
20
|
+
|
|
21
|
+
EventTypeOrAlias = Union[EventType, str]
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"AgentResponse",
|
|
25
|
+
"ToolResult",
|
|
26
|
+
"ToolCall",
|
|
27
|
+
"EndCall",
|
|
28
|
+
"AgentGenerationComplete",
|
|
29
|
+
"Authorize",
|
|
30
|
+
"AgentError",
|
|
31
|
+
"TransferCall",
|
|
32
|
+
"AgentHandoff",
|
|
33
|
+
"AgentStartedSpeaking",
|
|
34
|
+
"AgentStoppedSpeaking",
|
|
35
|
+
"UserStartedSpeaking",
|
|
36
|
+
"UserStoppedSpeaking",
|
|
37
|
+
"UserTranscriptionReceived",
|
|
38
|
+
"AgentSpeechSent",
|
|
39
|
+
"UserUnknownInputReceived",
|
|
40
|
+
"LogMetric",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AgentResponse(BaseModel):
|
|
45
|
+
"""Agent message to be sent to the user."""
|
|
46
|
+
|
|
47
|
+
content: str
|
|
48
|
+
chunk_type: str = "text"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ToolResult(BaseModel):
|
|
52
|
+
"""Tool execution result
|
|
53
|
+
- This will appear in the transcript in the Agent's current turn.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
- tool_name: Name of the tool that was called.
|
|
57
|
+
- tool_args: Arguments that were passed to the tool.
|
|
58
|
+
- result: Result returned by the tool.
|
|
59
|
+
- result_str: String representation of the result (computed).
|
|
60
|
+
- error: Error message if the tool call failed (None if successful).
|
|
61
|
+
- metadata: Additional metadata about the tool call.
|
|
62
|
+
- tool_call_id: Reference to the ToolCall instance that triggered this result (if applicable).
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
tool_name: str = ""
|
|
66
|
+
tool_args: dict = Field(default_factory=dict)
|
|
67
|
+
result: Optional[object] = None
|
|
68
|
+
error: Optional[str] = None
|
|
69
|
+
metadata: Optional[Dict] = None
|
|
70
|
+
tool_call_id: Optional[str] = None
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def result_str(self) -> Optional[str]:
|
|
74
|
+
"""String representation of the result, automatically computed from result."""
|
|
75
|
+
if self.result is not None:
|
|
76
|
+
try:
|
|
77
|
+
return json.dumps(self.result)
|
|
78
|
+
except Exception:
|
|
79
|
+
return str(self.result)
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def success(self) -> bool:
|
|
84
|
+
"""Returns True if there was no error, False otherwise."""
|
|
85
|
+
return self.error is None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ToolCall(BaseModel):
|
|
89
|
+
"""Tool execution request
|
|
90
|
+
- This will appear in the transcript in the Agent's current turn.
|
|
91
|
+
|
|
92
|
+
Attributes:
|
|
93
|
+
- tool_name: Name of the tool that was called
|
|
94
|
+
- tool_args: Arguments that were passed to the tool
|
|
95
|
+
- tool_call_id: Unique identifier for the tool call
|
|
96
|
+
- raw_response: Raw response from the tool call
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
tool_name: str
|
|
100
|
+
tool_args: Dict = Field(default_factory=dict)
|
|
101
|
+
tool_call_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
102
|
+
raw_response: Dict = Field(default_factory=dict)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class EndCall(BaseModel):
|
|
106
|
+
"""End the call."""
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def content(self) -> str:
|
|
110
|
+
"""Returns string representation of the end call event."""
|
|
111
|
+
return self.__repr__()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class AgentGenerationComplete(BaseModel):
|
|
115
|
+
"""Agent generation completion event."""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Authorize(BaseModel):
|
|
119
|
+
"""Change the authorized agent."""
|
|
120
|
+
|
|
121
|
+
agent: str
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AgentError(BaseModel):
|
|
125
|
+
"""Send error message to user."""
|
|
126
|
+
|
|
127
|
+
error: str
|
|
128
|
+
code: Optional[str] = None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TransferCall(BaseModel):
|
|
132
|
+
"""Transfer call to destination."""
|
|
133
|
+
|
|
134
|
+
destination: str
|
|
135
|
+
reason: Optional[str] = None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class AgentHandoff(BaseModel):
|
|
139
|
+
"""Agent handoff event for transfer_to_* patterns."""
|
|
140
|
+
|
|
141
|
+
target_agent: str
|
|
142
|
+
reason: str = ""
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class AgentStartedSpeaking(BaseModel):
|
|
146
|
+
"""Agent started speaking event."""
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class AgentStoppedSpeaking(BaseModel):
|
|
150
|
+
"""Agent stopped speaking event."""
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class UserStartedSpeaking(BaseModel):
|
|
154
|
+
"""User started speaking event."""
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class UserStoppedSpeaking(BaseModel):
|
|
158
|
+
"""User stopped speaking event."""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class UserTranscriptionReceived(BaseModel):
|
|
162
|
+
"""User transcription received event."""
|
|
163
|
+
|
|
164
|
+
content: str
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class AgentSpeechSent(BaseModel):
|
|
168
|
+
"""Agent speech content sent event."""
|
|
169
|
+
|
|
170
|
+
content: str
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class UserUnknownInputReceived(BaseModel):
|
|
174
|
+
"""User unknown input received event."""
|
|
175
|
+
|
|
176
|
+
input_data: str
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class LogMetric(BaseModel):
|
|
180
|
+
"""Log metric event for tracking usage metrics."""
|
|
181
|
+
|
|
182
|
+
name: str
|
|
183
|
+
value: Any
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class _EventsRegistry:
|
|
187
|
+
"""A singleton registry of all events.
|
|
188
|
+
|
|
189
|
+
Usage:
|
|
190
|
+
>>> registry = EventsRegistry()
|
|
191
|
+
>>> registry.register("system.eventA", SystemEventA)
|
|
192
|
+
>>> registry.register("system.eventB", SystemEventB)
|
|
193
|
+
>>> registry.get("system.eventA")
|
|
194
|
+
<class 'system.eventA'>
|
|
195
|
+
>>> registry.get("system.eventB")
|
|
196
|
+
<class 'system.eventB'>
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
_instance = None
|
|
200
|
+
|
|
201
|
+
def __new__(cls, *args, **kwargs):
|
|
202
|
+
if cls._instance is None:
|
|
203
|
+
cls._instance = super().__new__(cls)
|
|
204
|
+
cls._instance.events = {} # Dict[EventType, str]
|
|
205
|
+
return cls._instance
|
|
206
|
+
|
|
207
|
+
def register(self, alias: str, event_type: EventType):
|
|
208
|
+
if event_type in self.events:
|
|
209
|
+
raise ValueError(f"Event type {event_type} already registered with alias {alias}")
|
|
210
|
+
if not isinstance(alias, str):
|
|
211
|
+
raise TypeError(f"Alias {alias} is not a string")
|
|
212
|
+
self.events[event_type] = alias
|
|
213
|
+
|
|
214
|
+
def get(self, event_type: EventType) -> Optional[str]:
|
|
215
|
+
return self.events.get(event_type, None)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
EventsRegistry = _EventsRegistry()
|