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.
Files changed (66) hide show
  1. a2a_server/__init__.py +29 -0
  2. a2a_server/a2a_agent_card.py +365 -0
  3. a2a_server/a2a_errors.py +1133 -0
  4. a2a_server/a2a_executor.py +926 -0
  5. a2a_server/a2a_router.py +1033 -0
  6. a2a_server/a2a_types.py +344 -0
  7. a2a_server/agent_card.py +408 -0
  8. a2a_server/agents_server.py +271 -0
  9. a2a_server/auth_api.py +349 -0
  10. a2a_server/billing_api.py +638 -0
  11. a2a_server/billing_service.py +712 -0
  12. a2a_server/billing_webhooks.py +501 -0
  13. a2a_server/config.py +96 -0
  14. a2a_server/database.py +2165 -0
  15. a2a_server/email_inbound.py +398 -0
  16. a2a_server/email_notifications.py +486 -0
  17. a2a_server/enhanced_agents.py +919 -0
  18. a2a_server/enhanced_server.py +160 -0
  19. a2a_server/hosted_worker.py +1049 -0
  20. a2a_server/integrated_agents_server.py +347 -0
  21. a2a_server/keycloak_auth.py +750 -0
  22. a2a_server/livekit_bridge.py +439 -0
  23. a2a_server/marketing_tools.py +1364 -0
  24. a2a_server/mcp_client.py +196 -0
  25. a2a_server/mcp_http_server.py +2256 -0
  26. a2a_server/mcp_server.py +191 -0
  27. a2a_server/message_broker.py +725 -0
  28. a2a_server/mock_mcp.py +273 -0
  29. a2a_server/models.py +494 -0
  30. a2a_server/monitor_api.py +5904 -0
  31. a2a_server/opencode_bridge.py +1594 -0
  32. a2a_server/redis_task_manager.py +518 -0
  33. a2a_server/server.py +726 -0
  34. a2a_server/task_manager.py +668 -0
  35. a2a_server/task_queue.py +742 -0
  36. a2a_server/tenant_api.py +333 -0
  37. a2a_server/tenant_middleware.py +219 -0
  38. a2a_server/tenant_service.py +760 -0
  39. a2a_server/user_auth.py +721 -0
  40. a2a_server/vault_client.py +576 -0
  41. a2a_server/worker_sse.py +873 -0
  42. agent_worker/__init__.py +8 -0
  43. agent_worker/worker.py +4877 -0
  44. codetether/__init__.py +10 -0
  45. codetether/__main__.py +4 -0
  46. codetether/cli.py +112 -0
  47. codetether/worker_cli.py +57 -0
  48. codetether-1.2.2.dist-info/METADATA +570 -0
  49. codetether-1.2.2.dist-info/RECORD +66 -0
  50. codetether-1.2.2.dist-info/WHEEL +5 -0
  51. codetether-1.2.2.dist-info/entry_points.txt +4 -0
  52. codetether-1.2.2.dist-info/licenses/LICENSE +202 -0
  53. codetether-1.2.2.dist-info/top_level.txt +5 -0
  54. codetether_voice_agent/__init__.py +6 -0
  55. codetether_voice_agent/agent.py +445 -0
  56. codetether_voice_agent/codetether_mcp.py +345 -0
  57. codetether_voice_agent/config.py +16 -0
  58. codetether_voice_agent/functiongemma_caller.py +380 -0
  59. codetether_voice_agent/session_playback.py +247 -0
  60. codetether_voice_agent/tools/__init__.py +21 -0
  61. codetether_voice_agent/tools/definitions.py +135 -0
  62. codetether_voice_agent/tools/handlers.py +380 -0
  63. run_server.py +314 -0
  64. ui/monitor-tailwind.html +1790 -0
  65. ui/monitor.html +1775 -0
  66. 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