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,345 @@
|
|
|
1
|
+
"""CodeTether MCP Client Module.
|
|
2
|
+
|
|
3
|
+
This module provides an async client to interact with CodeTether's MCP (Model Context Protocol)
|
|
4
|
+
tools and APIs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import uuid
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
import aiohttp
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MCPError(Exception):
|
|
19
|
+
"""Exception raised for MCP-specific errors."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
message: str,
|
|
24
|
+
code: Optional[int] = None,
|
|
25
|
+
data: Optional[Any] = None,
|
|
26
|
+
):
|
|
27
|
+
self.message = message
|
|
28
|
+
self.code = code
|
|
29
|
+
self.data = data
|
|
30
|
+
super().__init__(self.message)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Task:
|
|
35
|
+
"""Represents a CodeTether task."""
|
|
36
|
+
|
|
37
|
+
id: str
|
|
38
|
+
title: str
|
|
39
|
+
description: str = ''
|
|
40
|
+
status: str = 'pending'
|
|
41
|
+
codebase_id: str = 'global'
|
|
42
|
+
agent_type: str = 'build'
|
|
43
|
+
priority: int = 0
|
|
44
|
+
created_at: Optional[str] = None
|
|
45
|
+
result: Optional[Dict[str, Any]] = None
|
|
46
|
+
error: Optional[str] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Agent:
|
|
51
|
+
"""Represents a CodeTether agent."""
|
|
52
|
+
|
|
53
|
+
name: str
|
|
54
|
+
description: str = ''
|
|
55
|
+
url: str = ''
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class Message:
|
|
60
|
+
"""Represents a message in a session."""
|
|
61
|
+
|
|
62
|
+
role: str
|
|
63
|
+
content: str
|
|
64
|
+
timestamp: Optional[str] = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CodeTetherMCP:
|
|
68
|
+
"""Async client for CodeTether MCP tools and APIs.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
api_url: The base URL for the CodeTether API.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, api_url: str):
|
|
75
|
+
self.api_url = api_url.rstrip('/')
|
|
76
|
+
self.mcp_url = f'{self.api_url}/mcp'
|
|
77
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
78
|
+
|
|
79
|
+
async def _get_session(self) -> aiohttp.ClientSession:
|
|
80
|
+
"""Get or create an aiohttp client session.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The aiohttp ClientSession instance.
|
|
84
|
+
"""
|
|
85
|
+
if self._session is None or self._session.closed:
|
|
86
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
87
|
+
self._session = aiohttp.ClientSession(timeout=timeout)
|
|
88
|
+
return self._session
|
|
89
|
+
|
|
90
|
+
async def close(self):
|
|
91
|
+
"""Close the client session."""
|
|
92
|
+
if self._session and not self._session.closed:
|
|
93
|
+
await self._session.close()
|
|
94
|
+
self._session = None
|
|
95
|
+
|
|
96
|
+
async def __aenter__(self):
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
100
|
+
await self.close()
|
|
101
|
+
|
|
102
|
+
async def call_tool(
|
|
103
|
+
self, tool_name: str, arguments: Dict[str, Any]
|
|
104
|
+
) -> Dict[str, Any]:
|
|
105
|
+
"""Call an MCP tool with the given name and arguments.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
tool_name: The name of the tool to call.
|
|
109
|
+
arguments: The arguments to pass to the tool.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
The result from the tool call.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
MCPError: If the tool call fails.
|
|
116
|
+
"""
|
|
117
|
+
session = await self._get_session()
|
|
118
|
+
request_id = str(uuid.uuid4())
|
|
119
|
+
|
|
120
|
+
payload = {
|
|
121
|
+
'jsonrpc': '2.0',
|
|
122
|
+
'id': request_id,
|
|
123
|
+
'method': 'tools/call',
|
|
124
|
+
'params': {'name': tool_name, 'arguments': arguments},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
logger.info(
|
|
128
|
+
f'Calling MCP tool: {tool_name} with request_id: {request_id}'
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
async with session.post(
|
|
133
|
+
self.mcp_url,
|
|
134
|
+
json=payload,
|
|
135
|
+
headers={'Content-Type': 'application/json'},
|
|
136
|
+
) as response:
|
|
137
|
+
if response.status != 200:
|
|
138
|
+
error_text = await response.text()
|
|
139
|
+
logger.error(
|
|
140
|
+
f'MCP tool call failed with status {response.status}: {error_text}'
|
|
141
|
+
)
|
|
142
|
+
raise MCPError(f'HTTP {response.status}: {error_text}')
|
|
143
|
+
|
|
144
|
+
result = await response.json()
|
|
145
|
+
|
|
146
|
+
if 'error' in result and result['error'] is not None:
|
|
147
|
+
error = result['error']
|
|
148
|
+
logger.error(f'MCP tool call error: {error}')
|
|
149
|
+
raise MCPError(
|
|
150
|
+
message=error.get('message', 'Unknown error'),
|
|
151
|
+
code=error.get('code'),
|
|
152
|
+
data=error.get('data'),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if 'result' not in result:
|
|
156
|
+
logger.error(f'MCP tool call returned no result: {result}')
|
|
157
|
+
raise MCPError('MCP tool call returned no result')
|
|
158
|
+
|
|
159
|
+
logger.info(f'MCP tool {tool_name} completed successfully')
|
|
160
|
+
return result['result']
|
|
161
|
+
|
|
162
|
+
except aiohttp.ClientError as e:
|
|
163
|
+
logger.error(f'Network error calling MCP tool {tool_name}: {e}')
|
|
164
|
+
raise MCPError(f'Network error: {str(e)}')
|
|
165
|
+
|
|
166
|
+
async def create_task(
|
|
167
|
+
self,
|
|
168
|
+
title: str,
|
|
169
|
+
description: str = '',
|
|
170
|
+
codebase_id: str = 'global',
|
|
171
|
+
agent_type: str = 'build',
|
|
172
|
+
priority: int = 0,
|
|
173
|
+
) -> Task:
|
|
174
|
+
"""Create a new task.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
title: The title of the task.
|
|
178
|
+
description: The description of the task.
|
|
179
|
+
codebase_id: The codebase ID to associate with the task.
|
|
180
|
+
agent_type: The type of agent to use.
|
|
181
|
+
priority: The priority of the task (0-10).
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
The created Task object.
|
|
185
|
+
"""
|
|
186
|
+
arguments = {
|
|
187
|
+
'title': title,
|
|
188
|
+
'description': description,
|
|
189
|
+
'codebase_id': codebase_id,
|
|
190
|
+
'agent_type': agent_type,
|
|
191
|
+
'priority': priority,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
result = await self.call_tool('create_task', arguments)
|
|
195
|
+
|
|
196
|
+
return Task(
|
|
197
|
+
id=result.get('id', ''),
|
|
198
|
+
title=result.get('title', title),
|
|
199
|
+
description=result.get('description', description),
|
|
200
|
+
status=result.get('status', 'pending'),
|
|
201
|
+
codebase_id=result.get('codebase_id', codebase_id),
|
|
202
|
+
agent_type=result.get('agent_type', agent_type),
|
|
203
|
+
priority=result.get('priority', priority),
|
|
204
|
+
created_at=result.get('created_at'),
|
|
205
|
+
result=result.get('result'),
|
|
206
|
+
error=result.get('error'),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
async def list_tasks(
|
|
210
|
+
self, status: Optional[str] = None, codebase_id: Optional[str] = None
|
|
211
|
+
) -> List[Task]:
|
|
212
|
+
"""List tasks with optional filtering.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
status: Filter tasks by status.
|
|
216
|
+
codebase_id: Filter tasks by codebase ID.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
A list of Task objects.
|
|
220
|
+
"""
|
|
221
|
+
arguments: Dict[str, Any] = {}
|
|
222
|
+
if status is not None:
|
|
223
|
+
arguments['status'] = status
|
|
224
|
+
if codebase_id is not None:
|
|
225
|
+
arguments['codebase_id'] = codebase_id
|
|
226
|
+
|
|
227
|
+
result = await self.call_tool('list_tasks', arguments)
|
|
228
|
+
|
|
229
|
+
tasks = []
|
|
230
|
+
for task_data in result.get('tasks', []):
|
|
231
|
+
tasks.append(
|
|
232
|
+
Task(
|
|
233
|
+
id=task_data.get('id', ''),
|
|
234
|
+
title=task_data.get('title', ''),
|
|
235
|
+
description=task_data.get('description', ''),
|
|
236
|
+
status=task_data.get('status', 'pending'),
|
|
237
|
+
codebase_id=task_data.get('codebase_id', 'global'),
|
|
238
|
+
agent_type=task_data.get('agent_type', 'build'),
|
|
239
|
+
priority=task_data.get('priority', 0),
|
|
240
|
+
created_at=task_data.get('created_at'),
|
|
241
|
+
result=task_data.get('result'),
|
|
242
|
+
error=task_data.get('error'),
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return tasks
|
|
247
|
+
|
|
248
|
+
async def get_task(self, task_id: str) -> Optional[Task]:
|
|
249
|
+
"""Get a task by ID.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
task_id: The ID of the task to retrieve.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
The Task object if found, None otherwise.
|
|
256
|
+
"""
|
|
257
|
+
result = await self.call_tool('get_task', {'task_id': task_id})
|
|
258
|
+
|
|
259
|
+
if not result:
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
return Task(
|
|
263
|
+
id=result.get('id', ''),
|
|
264
|
+
title=result.get('title', ''),
|
|
265
|
+
description=result.get('description', ''),
|
|
266
|
+
status=result.get('status', 'pending'),
|
|
267
|
+
codebase_id=result.get('codebase_id', 'global'),
|
|
268
|
+
agent_type=result.get('agent_type', 'build'),
|
|
269
|
+
priority=result.get('priority', 0),
|
|
270
|
+
created_at=result.get('created_at'),
|
|
271
|
+
result=result.get('result'),
|
|
272
|
+
error=result.get('error'),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
async def cancel_task(self, task_id: str) -> bool:
|
|
276
|
+
"""Cancel a task.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
task_id: The ID of the task to cancel.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
True if the task was cancelled, False otherwise.
|
|
283
|
+
"""
|
|
284
|
+
result = await self.call_tool('cancel_task', {'task_id': task_id})
|
|
285
|
+
return result.get('success', False)
|
|
286
|
+
|
|
287
|
+
async def get_session_messages(self, session_id: str) -> List[Message]:
|
|
288
|
+
"""Get messages from a session.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
session_id: The ID of the session.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
A list of Message objects.
|
|
295
|
+
"""
|
|
296
|
+
result = await self.call_tool('get_session', {'session_id': session_id})
|
|
297
|
+
|
|
298
|
+
messages = []
|
|
299
|
+
for msg_data in result.get('messages', []):
|
|
300
|
+
messages.append(
|
|
301
|
+
Message(
|
|
302
|
+
role=msg_data.get('role', ''),
|
|
303
|
+
content=msg_data.get('content', ''),
|
|
304
|
+
timestamp=msg_data.get('timestamp'),
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return messages
|
|
309
|
+
|
|
310
|
+
async def discover_agents(self) -> List[Agent]:
|
|
311
|
+
"""Discover available agents.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
A list of Agent objects.
|
|
315
|
+
"""
|
|
316
|
+
result = await self.call_tool('discover_agents', {})
|
|
317
|
+
|
|
318
|
+
agents = []
|
|
319
|
+
for agent_data in result.get('agents', []):
|
|
320
|
+
agents.append(
|
|
321
|
+
Agent(
|
|
322
|
+
name=agent_data.get('name', ''),
|
|
323
|
+
description=agent_data.get('description', ''),
|
|
324
|
+
url=agent_data.get('url', ''),
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
return agents
|
|
329
|
+
|
|
330
|
+
async def send_message(
|
|
331
|
+
self, agent_name: str, message: str
|
|
332
|
+
) -> Dict[str, Any]:
|
|
333
|
+
"""Send a message to an agent.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
agent_name: The name of the agent to send the message to.
|
|
337
|
+
message: The message content.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
The response from the agent.
|
|
341
|
+
"""
|
|
342
|
+
result = await self.call_tool(
|
|
343
|
+
'send_message', {'agent_name': agent_name, 'message': message}
|
|
344
|
+
)
|
|
345
|
+
return result
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pydantic import BaseSettings
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Settings(BaseSettings):
|
|
6
|
+
LIVEKIT_URL: str = 'wss://your-livekit-url.livekit.cloud'
|
|
7
|
+
CODETETHER_API_URL: str = 'http://localhost:8000'
|
|
8
|
+
FUNCTIONGEMMA_MODEL_PATH: str = 'google/functiongemma-2b'
|
|
9
|
+
GOOGLE_API_KEY: str = ''
|
|
10
|
+
LOG_LEVEL: str = 'info'
|
|
11
|
+
|
|
12
|
+
class Config:
|
|
13
|
+
env_prefix = 'CODETETHER_VOICE_AGENT_'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
settings = Settings()
|