codetether 1.2.2__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.
- a2a_server/__init__.py +29 -0
- a2a_server/a2a_agent_card.py +365 -0
- a2a_server/a2a_errors.py +1133 -0
- a2a_server/a2a_executor.py +926 -0
- a2a_server/a2a_router.py +1033 -0
- a2a_server/a2a_types.py +344 -0
- a2a_server/agent_card.py +408 -0
- a2a_server/agents_server.py +271 -0
- a2a_server/auth_api.py +349 -0
- a2a_server/billing_api.py +638 -0
- a2a_server/billing_service.py +712 -0
- a2a_server/billing_webhooks.py +501 -0
- a2a_server/config.py +96 -0
- a2a_server/database.py +2165 -0
- a2a_server/email_inbound.py +398 -0
- a2a_server/email_notifications.py +486 -0
- a2a_server/enhanced_agents.py +919 -0
- a2a_server/enhanced_server.py +160 -0
- a2a_server/hosted_worker.py +1049 -0
- a2a_server/integrated_agents_server.py +347 -0
- a2a_server/keycloak_auth.py +750 -0
- a2a_server/livekit_bridge.py +439 -0
- a2a_server/marketing_tools.py +1364 -0
- a2a_server/mcp_client.py +196 -0
- a2a_server/mcp_http_server.py +2256 -0
- a2a_server/mcp_server.py +191 -0
- a2a_server/message_broker.py +725 -0
- a2a_server/mock_mcp.py +273 -0
- a2a_server/models.py +494 -0
- a2a_server/monitor_api.py +5904 -0
- a2a_server/opencode_bridge.py +1594 -0
- a2a_server/redis_task_manager.py +518 -0
- a2a_server/server.py +726 -0
- a2a_server/task_manager.py +668 -0
- a2a_server/task_queue.py +742 -0
- a2a_server/tenant_api.py +333 -0
- a2a_server/tenant_middleware.py +219 -0
- a2a_server/tenant_service.py +760 -0
- a2a_server/user_auth.py +721 -0
- a2a_server/vault_client.py +576 -0
- a2a_server/worker_sse.py +873 -0
- agent_worker/__init__.py +8 -0
- agent_worker/worker.py +4877 -0
- codetether/__init__.py +10 -0
- codetether/__main__.py +4 -0
- codetether/cli.py +112 -0
- codetether/worker_cli.py +57 -0
- codetether-1.2.2.dist-info/METADATA +570 -0
- codetether-1.2.2.dist-info/RECORD +66 -0
- codetether-1.2.2.dist-info/WHEEL +5 -0
- codetether-1.2.2.dist-info/entry_points.txt +4 -0
- codetether-1.2.2.dist-info/licenses/LICENSE +202 -0
- codetether-1.2.2.dist-info/top_level.txt +5 -0
- codetether_voice_agent/__init__.py +6 -0
- codetether_voice_agent/agent.py +445 -0
- codetether_voice_agent/codetether_mcp.py +345 -0
- codetether_voice_agent/config.py +16 -0
- codetether_voice_agent/functiongemma_caller.py +380 -0
- codetether_voice_agent/session_playback.py +247 -0
- codetether_voice_agent/tools/__init__.py +21 -0
- codetether_voice_agent/tools/definitions.py +135 -0
- codetether_voice_agent/tools/handlers.py +380 -0
- run_server.py +314 -0
- ui/monitor-tailwind.html +1790 -0
- ui/monitor.html +1775 -0
- ui/monitor.js +2662 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LiveKit Bridge Module for A2A Server
|
|
3
|
+
|
|
4
|
+
This module handles all communication with LiveKit services, including:
|
|
5
|
+
- Creating rooms via LiveKit SDK
|
|
6
|
+
- Minting short-lived access tokens
|
|
7
|
+
- Mapping A2A roles to LiveKit grants
|
|
8
|
+
- Validating A2A authentication before minting tokens
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Optional, Dict, Any, List
|
|
15
|
+
from datetime import datetime, timedelta
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from livekit import api
|
|
19
|
+
|
|
20
|
+
LIVEKIT_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
LIVEKIT_AVAILABLE = False
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
logger.warning(
|
|
25
|
+
'LiveKit SDK not installed. LiveKit features will be disabled.'
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LiveKitBridge:
|
|
32
|
+
"""Bridge between A2A server and LiveKit services."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
api_key: Optional[str] = None,
|
|
37
|
+
api_secret: Optional[str] = None,
|
|
38
|
+
livekit_url: Optional[str] = None,
|
|
39
|
+
public_url: Optional[str] = None,
|
|
40
|
+
):
|
|
41
|
+
"""Initialize LiveKit bridge with API credentials.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
api_key: LiveKit API key (defaults to LIVEKIT_API_KEY env var)
|
|
45
|
+
api_secret: LiveKit API secret (defaults to LIVEKIT_API_SECRET env var)
|
|
46
|
+
livekit_url: LiveKit server URL for API calls (defaults to LIVEKIT_URL env var)
|
|
47
|
+
public_url: Public LiveKit URL for client WebSocket connections (defaults to LIVEKIT_PUBLIC_URL env var)
|
|
48
|
+
"""
|
|
49
|
+
if not LIVEKIT_AVAILABLE:
|
|
50
|
+
raise ImportError(
|
|
51
|
+
'LiveKit SDK is not installed. '
|
|
52
|
+
'Install it with: pip install livekit'
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
self.api_key = api_key or os.getenv('LIVEKIT_API_KEY')
|
|
56
|
+
self.api_secret = api_secret or os.getenv('LIVEKIT_API_SECRET')
|
|
57
|
+
self.livekit_url = livekit_url or os.getenv(
|
|
58
|
+
'LIVEKIT_URL', 'https://live.quantum-forge.net/'
|
|
59
|
+
)
|
|
60
|
+
# Public URL for client connections (defaults to wss:// version of livekit_url if not set)
|
|
61
|
+
self.public_url = public_url or os.getenv('LIVEKIT_PUBLIC_URL')
|
|
62
|
+
|
|
63
|
+
if not self.api_key or not self.api_secret:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
'LiveKit API key and secret must be provided either as parameters '
|
|
66
|
+
'or through LIVEKIT_API_KEY and LIVEKIT_API_SECRET environment variables'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Normalize URL - remove trailing slash and ensure http/https
|
|
70
|
+
self.livekit_url = self.livekit_url.rstrip('/')
|
|
71
|
+
if not self.livekit_url.startswith(('http://', 'https://')):
|
|
72
|
+
self.livekit_url = f'https://{self.livekit_url}'
|
|
73
|
+
|
|
74
|
+
# Normalize public URL - if not set, derive from livekit_url
|
|
75
|
+
if not self.public_url:
|
|
76
|
+
# Convert http(s):// to wss://
|
|
77
|
+
self.public_url = self.livekit_url.replace(
|
|
78
|
+
'http://', 'wss://'
|
|
79
|
+
).replace('https://', 'wss://')
|
|
80
|
+
self.public_url = self.public_url.rstrip('/')
|
|
81
|
+
|
|
82
|
+
# Initialize LiveKit API client lazily (on first use) to avoid event loop issues
|
|
83
|
+
self._livekit_api = None
|
|
84
|
+
|
|
85
|
+
logger.info(
|
|
86
|
+
f'LiveKit bridge initialized with URL: {self.livekit_url}, public URL: {self.public_url}'
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def livekit_api(self):
|
|
91
|
+
"""Lazy initialization of LiveKit API client."""
|
|
92
|
+
if self._livekit_api is None:
|
|
93
|
+
self._livekit_api = api.LiveKitAPI(
|
|
94
|
+
url=self.livekit_url,
|
|
95
|
+
api_key=self.api_key,
|
|
96
|
+
api_secret=self.api_secret,
|
|
97
|
+
)
|
|
98
|
+
return self._livekit_api
|
|
99
|
+
|
|
100
|
+
async def create_room(
|
|
101
|
+
self,
|
|
102
|
+
room_name: str,
|
|
103
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
104
|
+
max_participants: int = 50,
|
|
105
|
+
empty_timeout: int = 300, # 5 minutes
|
|
106
|
+
departure_timeout: int = 60, # 1 minute
|
|
107
|
+
) -> Dict[str, Any]:
|
|
108
|
+
"""Create a new LiveKit room using the SDK.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
room_name: Unique name for the room
|
|
112
|
+
metadata: Optional metadata to attach to the room
|
|
113
|
+
max_participants: Maximum number of participants allowed
|
|
114
|
+
empty_timeout: Timeout in seconds for empty room
|
|
115
|
+
departure_timeout: Timeout in seconds after last participant leaves
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Dictionary containing room information
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
# Use LiveKit SDK to create room
|
|
122
|
+
room = await self.livekit_api.room.create_room(
|
|
123
|
+
api.CreateRoomRequest(
|
|
124
|
+
name=room_name,
|
|
125
|
+
empty_timeout=empty_timeout,
|
|
126
|
+
departure_timeout=departure_timeout,
|
|
127
|
+
max_participants=max_participants,
|
|
128
|
+
metadata=str(metadata) if metadata else None,
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
logger.info(f'Created LiveKit room: {room_name}')
|
|
133
|
+
|
|
134
|
+
# Return room info as dictionary
|
|
135
|
+
return {
|
|
136
|
+
'name': room.name,
|
|
137
|
+
'sid': room.sid,
|
|
138
|
+
'empty_timeout': room.empty_timeout,
|
|
139
|
+
'departure_timeout': room.departure_timeout,
|
|
140
|
+
'max_participants': room.max_participants,
|
|
141
|
+
'creation_time': room.creation_time,
|
|
142
|
+
'num_participants': room.num_participants,
|
|
143
|
+
'metadata': room.metadata,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f'Failed to create LiveKit room {room_name}: {e}')
|
|
148
|
+
raise
|
|
149
|
+
|
|
150
|
+
async def get_room_info(self, room_name: str) -> Optional[Dict[str, Any]]:
|
|
151
|
+
"""Get information about an existing room using the SDK.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
room_name: Name of the room to get info for
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Room information dictionary or None if room doesn't exist
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
# Use LiveKit SDK to list rooms
|
|
161
|
+
rooms = await self.livekit_api.room.list_rooms(
|
|
162
|
+
api.ListRoomsRequest(names=[room_name])
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if rooms and len(rooms) > 0:
|
|
166
|
+
room = rooms[0]
|
|
167
|
+
return {
|
|
168
|
+
'name': room.name,
|
|
169
|
+
'sid': room.sid,
|
|
170
|
+
'empty_timeout': room.empty_timeout,
|
|
171
|
+
'departure_timeout': room.departure_timeout,
|
|
172
|
+
'max_participants': room.max_participants,
|
|
173
|
+
'creation_time': room.creation_time,
|
|
174
|
+
'num_participants': room.num_participants,
|
|
175
|
+
'metadata': room.metadata,
|
|
176
|
+
}
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.error(f'Failed to get room info for {room_name}: {e}')
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
def mint_access_token(
|
|
184
|
+
self,
|
|
185
|
+
identity: str,
|
|
186
|
+
room_name: str,
|
|
187
|
+
a2a_role: str = 'participant',
|
|
188
|
+
metadata: Optional[str] = None,
|
|
189
|
+
ttl_minutes: int = 60,
|
|
190
|
+
) -> str:
|
|
191
|
+
"""Mint a short-lived access token for LiveKit room access using SDK.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
identity: Unique identity for the participant
|
|
195
|
+
room_name: Name of the room to join
|
|
196
|
+
a2a_role: A2A role (mapped to LiveKit grants)
|
|
197
|
+
metadata: Optional metadata for the participant
|
|
198
|
+
ttl_minutes: Token time-to-live in minutes
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
JWT access token string
|
|
202
|
+
"""
|
|
203
|
+
try:
|
|
204
|
+
# Map A2A role to LiveKit video grants
|
|
205
|
+
grants = self._map_a2a_role_to_grants(a2a_role, room_name)
|
|
206
|
+
|
|
207
|
+
# Create access token using SDK
|
|
208
|
+
token = api.AccessToken(self.api_key, self.api_secret)
|
|
209
|
+
token.with_identity(identity)
|
|
210
|
+
token.with_name(identity)
|
|
211
|
+
token.with_ttl(timedelta(minutes=ttl_minutes))
|
|
212
|
+
|
|
213
|
+
# Set video grants
|
|
214
|
+
token.with_grants(
|
|
215
|
+
api.VideoGrants(
|
|
216
|
+
room_join=grants.get('roomJoin', True),
|
|
217
|
+
room=grants.get('room', room_name),
|
|
218
|
+
room_admin=grants.get('roomAdmin', False),
|
|
219
|
+
can_publish=grants.get('canPublish', True),
|
|
220
|
+
can_subscribe=grants.get('canSubscribe', True),
|
|
221
|
+
can_publish_data=grants.get('canPublishData', True),
|
|
222
|
+
can_update_own_metadata=grants.get(
|
|
223
|
+
'canUpdateOwnMetadata', True
|
|
224
|
+
),
|
|
225
|
+
hidden=grants.get('hidden', False),
|
|
226
|
+
recorder=grants.get('recorder', False),
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if metadata:
|
|
231
|
+
token.with_metadata(metadata)
|
|
232
|
+
|
|
233
|
+
access_token_str = token.to_jwt()
|
|
234
|
+
|
|
235
|
+
logger.info(
|
|
236
|
+
f'Minted access token for {identity} in room {room_name} with role {a2a_role}'
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return access_token_str
|
|
240
|
+
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.error(f'Failed to mint access token for {identity}: {e}')
|
|
243
|
+
raise
|
|
244
|
+
|
|
245
|
+
def _map_a2a_role_to_grants(
|
|
246
|
+
self, a2a_role: str, room_name: str
|
|
247
|
+
) -> Dict[str, Any]:
|
|
248
|
+
"""Map A2A roles to LiveKit video grants.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
a2a_role: A2A role string
|
|
252
|
+
room_name: Room name for scoped permissions
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Dictionary with video grants
|
|
256
|
+
"""
|
|
257
|
+
# Define role mappings
|
|
258
|
+
role_mappings = {
|
|
259
|
+
'admin': {
|
|
260
|
+
'roomJoin': True,
|
|
261
|
+
'room': room_name,
|
|
262
|
+
'roomAdmin': True,
|
|
263
|
+
'canPublish': True,
|
|
264
|
+
'canSubscribe': True,
|
|
265
|
+
'canPublishData': True,
|
|
266
|
+
'canUpdateOwnMetadata': True,
|
|
267
|
+
'recorder': True,
|
|
268
|
+
},
|
|
269
|
+
'moderator': {
|
|
270
|
+
'roomJoin': True,
|
|
271
|
+
'room': room_name,
|
|
272
|
+
'roomAdmin': False,
|
|
273
|
+
'canPublish': True,
|
|
274
|
+
'canSubscribe': True,
|
|
275
|
+
'canPublishData': True,
|
|
276
|
+
'canUpdateOwnMetadata': True,
|
|
277
|
+
'recorder': False,
|
|
278
|
+
},
|
|
279
|
+
'publisher': {
|
|
280
|
+
'roomJoin': True,
|
|
281
|
+
'room': room_name,
|
|
282
|
+
'roomAdmin': False,
|
|
283
|
+
'canPublish': True,
|
|
284
|
+
'canSubscribe': True,
|
|
285
|
+
'canPublishData': True,
|
|
286
|
+
'canUpdateOwnMetadata': False,
|
|
287
|
+
'recorder': False,
|
|
288
|
+
},
|
|
289
|
+
'participant': {
|
|
290
|
+
'roomJoin': True,
|
|
291
|
+
'room': room_name,
|
|
292
|
+
'roomAdmin': False,
|
|
293
|
+
'canPublish': True,
|
|
294
|
+
'canSubscribe': True,
|
|
295
|
+
'canPublishData': False,
|
|
296
|
+
'canUpdateOwnMetadata': False,
|
|
297
|
+
'recorder': False,
|
|
298
|
+
},
|
|
299
|
+
'viewer': {
|
|
300
|
+
'roomJoin': True,
|
|
301
|
+
'room': room_name,
|
|
302
|
+
'roomAdmin': False,
|
|
303
|
+
'canPublish': False,
|
|
304
|
+
'canSubscribe': True,
|
|
305
|
+
'canPublishData': False,
|
|
306
|
+
'canUpdateOwnMetadata': False,
|
|
307
|
+
'recorder': False,
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# Default to participant if role not found
|
|
312
|
+
grants = role_mappings.get(
|
|
313
|
+
a2a_role.lower(), role_mappings['participant']
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
logger.debug(
|
|
317
|
+
f"Mapped A2A role '{a2a_role}' to LiveKit grants for room {room_name}"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
return grants
|
|
321
|
+
|
|
322
|
+
async def delete_room(self, room_name: str) -> bool:
|
|
323
|
+
"""Delete a LiveKit room via HTTP API.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
room_name: Name of the room to delete
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
True if deletion was successful, False otherwise
|
|
330
|
+
"""
|
|
331
|
+
try:
|
|
332
|
+
# Generate auth token for API request
|
|
333
|
+
auth_token = self._generate_api_token()
|
|
334
|
+
|
|
335
|
+
response = await self.http_client.post(
|
|
336
|
+
f'{self.livekit_url}/twirp/livekit.RoomService/DeleteRoom',
|
|
337
|
+
json={'room': room_name},
|
|
338
|
+
headers={
|
|
339
|
+
'Authorization': f'Bearer {auth_token}',
|
|
340
|
+
'Content-Type': 'application/json',
|
|
341
|
+
},
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
if response.status_code == 200:
|
|
345
|
+
logger.info(f'Deleted LiveKit room: {room_name}')
|
|
346
|
+
return True
|
|
347
|
+
else:
|
|
348
|
+
logger.error(
|
|
349
|
+
f'Failed to delete room {room_name}: {response.status_code}'
|
|
350
|
+
)
|
|
351
|
+
return False
|
|
352
|
+
|
|
353
|
+
except Exception as e:
|
|
354
|
+
logger.error(f'Failed to delete room {room_name}: {e}')
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
async def list_participants(self, room_name: str) -> List[Dict[str, Any]]:
|
|
358
|
+
"""List participants in a room via HTTP API.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
room_name: Name of the room
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
List of participant information dictionaries
|
|
365
|
+
"""
|
|
366
|
+
try:
|
|
367
|
+
# Generate auth token for API request
|
|
368
|
+
auth_token = self._generate_api_token()
|
|
369
|
+
|
|
370
|
+
response = await self.http_client.post(
|
|
371
|
+
f'{self.livekit_url}/twirp/livekit.RoomService/ListParticipants',
|
|
372
|
+
json={'room': room_name},
|
|
373
|
+
headers={
|
|
374
|
+
'Authorization': f'Bearer {auth_token}',
|
|
375
|
+
'Content-Type': 'application/json',
|
|
376
|
+
},
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
if response.status_code == 200:
|
|
380
|
+
participants_data = response.json()
|
|
381
|
+
return participants_data.get('participants', [])
|
|
382
|
+
else:
|
|
383
|
+
logger.error(
|
|
384
|
+
f'Failed to list participants for room {room_name}: {response.status_code}'
|
|
385
|
+
)
|
|
386
|
+
return []
|
|
387
|
+
|
|
388
|
+
except Exception as e:
|
|
389
|
+
logger.error(
|
|
390
|
+
f'Failed to list participants for room {room_name}: {e}'
|
|
391
|
+
)
|
|
392
|
+
return []
|
|
393
|
+
|
|
394
|
+
def generate_join_url(
|
|
395
|
+
self, room_name: str, token: str, base_url: Optional[str] = None
|
|
396
|
+
) -> str:
|
|
397
|
+
"""Generate a join URL for a LiveKit room.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
room_name: Name of the room
|
|
401
|
+
token: Access token for the room
|
|
402
|
+
base_url: Base URL for the LiveKit frontend (optional)
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Complete join URL
|
|
406
|
+
"""
|
|
407
|
+
if base_url:
|
|
408
|
+
# Use provided base URL (e.g., custom frontend)
|
|
409
|
+
return f'{base_url}?room={room_name}&token={token}'
|
|
410
|
+
else:
|
|
411
|
+
# Use LiveKit server URL with default frontend
|
|
412
|
+
url = self.livekit_url
|
|
413
|
+
if url.startswith('https://'):
|
|
414
|
+
# Keep https for web frontend
|
|
415
|
+
pass
|
|
416
|
+
elif url.startswith('http://'):
|
|
417
|
+
# Keep http for local development
|
|
418
|
+
pass
|
|
419
|
+
else:
|
|
420
|
+
url = f'https://{url}'
|
|
421
|
+
|
|
422
|
+
return f'{url}?room={room_name}&token={token}'
|
|
423
|
+
|
|
424
|
+
async def close(self):
|
|
425
|
+
"""Close the HTTP client."""
|
|
426
|
+
await self.http_client.aclose()
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def create_livekit_bridge() -> Optional[LiveKitBridge]:
|
|
430
|
+
"""Create a LiveKit bridge instance if credentials are available.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
LiveKitBridge instance or None if credentials not configured
|
|
434
|
+
"""
|
|
435
|
+
try:
|
|
436
|
+
return LiveKitBridge()
|
|
437
|
+
except ValueError as e:
|
|
438
|
+
logger.warning(f'LiveKit bridge not initialized: {e}')
|
|
439
|
+
return None
|