daita-agents 0.1.0__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 daita-agents might be problematic. Click here for more details.

Files changed (69) hide show
  1. daita/__init__.py +208 -0
  2. daita/agents/__init__.py +33 -0
  3. daita/agents/base.py +722 -0
  4. daita/agents/substrate.py +895 -0
  5. daita/cli/__init__.py +145 -0
  6. daita/cli/__main__.py +7 -0
  7. daita/cli/ascii_art.py +44 -0
  8. daita/cli/core/__init__.py +0 -0
  9. daita/cli/core/create.py +254 -0
  10. daita/cli/core/deploy.py +473 -0
  11. daita/cli/core/deployments.py +309 -0
  12. daita/cli/core/import_detector.py +219 -0
  13. daita/cli/core/init.py +382 -0
  14. daita/cli/core/logs.py +239 -0
  15. daita/cli/core/managed_deploy.py +709 -0
  16. daita/cli/core/run.py +648 -0
  17. daita/cli/core/status.py +421 -0
  18. daita/cli/core/test.py +239 -0
  19. daita/cli/core/webhooks.py +172 -0
  20. daita/cli/main.py +588 -0
  21. daita/cli/utils.py +541 -0
  22. daita/config/__init__.py +62 -0
  23. daita/config/base.py +159 -0
  24. daita/config/settings.py +184 -0
  25. daita/core/__init__.py +262 -0
  26. daita/core/decision_tracing.py +701 -0
  27. daita/core/exceptions.py +480 -0
  28. daita/core/focus.py +251 -0
  29. daita/core/interfaces.py +76 -0
  30. daita/core/plugin_tracing.py +550 -0
  31. daita/core/relay.py +695 -0
  32. daita/core/reliability.py +381 -0
  33. daita/core/scaling.py +444 -0
  34. daita/core/tools.py +402 -0
  35. daita/core/tracing.py +770 -0
  36. daita/core/workflow.py +1084 -0
  37. daita/display/__init__.py +1 -0
  38. daita/display/console.py +160 -0
  39. daita/execution/__init__.py +58 -0
  40. daita/execution/client.py +856 -0
  41. daita/execution/exceptions.py +92 -0
  42. daita/execution/models.py +317 -0
  43. daita/llm/__init__.py +60 -0
  44. daita/llm/anthropic.py +166 -0
  45. daita/llm/base.py +373 -0
  46. daita/llm/factory.py +101 -0
  47. daita/llm/gemini.py +152 -0
  48. daita/llm/grok.py +114 -0
  49. daita/llm/mock.py +135 -0
  50. daita/llm/openai.py +109 -0
  51. daita/plugins/__init__.py +141 -0
  52. daita/plugins/base.py +37 -0
  53. daita/plugins/base_db.py +167 -0
  54. daita/plugins/elasticsearch.py +844 -0
  55. daita/plugins/mcp.py +481 -0
  56. daita/plugins/mongodb.py +510 -0
  57. daita/plugins/mysql.py +351 -0
  58. daita/plugins/postgresql.py +331 -0
  59. daita/plugins/redis_messaging.py +500 -0
  60. daita/plugins/rest.py +529 -0
  61. daita/plugins/s3.py +761 -0
  62. daita/plugins/slack.py +729 -0
  63. daita/utils/__init__.py +18 -0
  64. daita_agents-0.1.0.dist-info/METADATA +350 -0
  65. daita_agents-0.1.0.dist-info/RECORD +69 -0
  66. daita_agents-0.1.0.dist-info/WHEEL +5 -0
  67. daita_agents-0.1.0.dist-info/entry_points.txt +2 -0
  68. daita_agents-0.1.0.dist-info/licenses/LICENSE +56 -0
  69. daita_agents-0.1.0.dist-info/top_level.txt +1 -0
daita/plugins/mcp.py ADDED
@@ -0,0 +1,481 @@
1
+ """
2
+ MCP (Model Context Protocol) plugin for Daita Agents.
3
+
4
+ This plugin enables agents to connect to any MCP server and autonomously use
5
+ their tools via LLM function calling. Agents discover available tools and
6
+ decide which ones to use based on the task.
7
+
8
+ Usage:
9
+ ```python
10
+ from daita import SubstrateAgent
11
+ from daita.plugins import mcp
12
+
13
+ # Agent with MCP tools
14
+ agent = SubstrateAgent(
15
+ name="file_analyzer",
16
+ mcp_servers=[
17
+ mcp.server(command="uvx", args=["mcp-server-filesystem", "/data"])
18
+ ]
19
+ )
20
+
21
+ # Agent autonomously discovers and uses MCP tools
22
+ result = await agent.process("Read report.csv and calculate average revenue")
23
+ ```
24
+
25
+ MCP Protocol:
26
+ The Model Context Protocol (MCP) is Anthropic's open standard for connecting
27
+ AI systems to external data sources and tools. This plugin provides native
28
+ MCP client support for Daita agents.
29
+ """
30
+
31
+ import asyncio
32
+ import logging
33
+ import json
34
+ from typing import Any, Dict, List, Optional, Callable
35
+ from dataclasses import dataclass
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ @dataclass
41
+ class MCPTool:
42
+ """Represents a tool exposed by an MCP server"""
43
+
44
+ name: str
45
+ description: str
46
+ input_schema: Dict[str, Any]
47
+
48
+ def to_llm_function(self) -> Dict[str, Any]:
49
+ """
50
+ Convert MCP tool schema to LLM function calling format.
51
+
52
+ Returns OpenAI/Anthropic compatible function definition.
53
+ """
54
+ return {
55
+ "name": self.name,
56
+ "description": self.description,
57
+ "parameters": self.input_schema
58
+ }
59
+
60
+
61
+ class MCPServer:
62
+ """
63
+ MCP Server connection manager.
64
+
65
+ Manages connection to a single MCP server via stdio transport,
66
+ discovers available tools, and executes tool calls.
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ command: str,
72
+ args: Optional[List[str]] = None,
73
+ env: Optional[Dict[str, str]] = None,
74
+ server_name: Optional[str] = None
75
+ ):
76
+ """
77
+ Initialize MCP server configuration.
78
+
79
+ Args:
80
+ command: Command to run MCP server (e.g., "uvx", "python", "npx")
81
+ args: Arguments for the command (e.g., ["mcp-server-filesystem", "/data"])
82
+ env: Environment variables for the server process
83
+ server_name: Optional name for this server (for logging/debugging)
84
+ """
85
+ self.command = command
86
+ self.args = args or []
87
+ self.env = env or {}
88
+ self.server_name = server_name or f"mcp_{command}"
89
+
90
+ # Connection state
91
+ self._session = None
92
+ self._read = None
93
+ self._write = None
94
+ self._tools: List[MCPTool] = []
95
+ self._connected = False
96
+ self._stdio_context_task = None # Background task keeping context alive
97
+ self._session_lock = asyncio.Lock() # Protects session access from concurrent calls
98
+
99
+ async def _maintain_connection(self, server_params):
100
+ """
101
+ Background task that maintains the stdio connection context.
102
+
103
+ This keeps the MCP SDK's anyio task group alive for the duration
104
+ of the connection. We use an event to signal when to disconnect.
105
+ """
106
+ from mcp import ClientSession
107
+ from mcp.client.stdio import stdio_client
108
+
109
+ try:
110
+ async with stdio_client(server_params) as (read_stream, write_stream):
111
+ # Create session as context manager (required by MCP SDK)
112
+ async with ClientSession(read_stream, write_stream) as session:
113
+ self._session = session
114
+
115
+ # Initialize session
116
+ await session.initialize()
117
+
118
+ # Discover tools
119
+ await self._discover_tools()
120
+
121
+ # Mark as connected
122
+ self._connected = True
123
+ logger.info(f"Connected to MCP server {self.server_name}: {len(self._tools)} tools available")
124
+
125
+ # Keep connection alive until disconnect is called
126
+ while self._connected:
127
+ await asyncio.sleep(0.1)
128
+
129
+ except Exception as e:
130
+ self._connected = False
131
+ logger.error(f"MCP connection error for {self.server_name}: {str(e)}")
132
+ raise
133
+ finally:
134
+ # Cleanup
135
+ self._session = None
136
+ self._read = None
137
+ self._write = None
138
+
139
+ async def connect(self) -> None:
140
+ """
141
+ Connect to the MCP server and discover available tools.
142
+
143
+ Raises:
144
+ ImportError: If MCP SDK is not installed
145
+ ConnectionError: If connection to server fails
146
+ """
147
+ if self._connected:
148
+ return
149
+
150
+ try:
151
+ # Import MCP SDK
152
+ from mcp import StdioServerParameters
153
+
154
+ # Create server parameters
155
+ server_params = StdioServerParameters(
156
+ command=self.command,
157
+ args=self.args,
158
+ env=self.env if self.env else None
159
+ )
160
+
161
+ logger.info(f"Connecting to MCP server: {self.server_name}")
162
+
163
+ # Start background task that maintains the connection
164
+ self._stdio_context_task = asyncio.create_task(
165
+ self._maintain_connection(server_params)
166
+ )
167
+
168
+ # Wait for connection to be established
169
+ max_wait = 5.0 # seconds
170
+ start_time = asyncio.get_event_loop().time()
171
+ while not self._connected:
172
+ if asyncio.get_event_loop().time() - start_time > max_wait:
173
+ raise ConnectionError(f"Connection timeout after {max_wait}s")
174
+ if self._stdio_context_task.done():
175
+ # Task failed
176
+ try:
177
+ self._stdio_context_task.result()
178
+ except Exception as e:
179
+ raise ConnectionError(f"Connection task failed: {str(e)}")
180
+ await asyncio.sleep(0.05)
181
+
182
+ except ImportError as e:
183
+ error_msg = (
184
+ "MCP SDK not installed. Install with: pip install mcp\n"
185
+ "Official SDK: https://github.com/modelcontextprotocol/python-sdk"
186
+ )
187
+ logger.error(error_msg)
188
+ raise ImportError(error_msg) from e
189
+
190
+ except Exception as e:
191
+ error_msg = f"Failed to connect to MCP server {self.server_name}: {str(e)}"
192
+ logger.error(error_msg)
193
+ raise ConnectionError(error_msg) from e
194
+
195
+ async def _discover_tools(self) -> None:
196
+ """Discover available tools from the MCP server"""
197
+ try:
198
+ # List available tools from server
199
+ tools_response = await self._session.list_tools()
200
+
201
+ # Convert to MCPTool objects
202
+ self._tools = []
203
+ for tool in tools_response.tools:
204
+ mcp_tool = MCPTool(
205
+ name=tool.name,
206
+ description=tool.description or f"Tool: {tool.name}",
207
+ input_schema=tool.inputSchema if hasattr(tool, 'inputSchema') else {}
208
+ )
209
+ self._tools.append(mcp_tool)
210
+ pass
211
+
212
+ logger.info(f"Discovered {len(self._tools)} tools from {self.server_name}")
213
+
214
+ except Exception as e:
215
+ logger.error(f"Failed to discover tools from {self.server_name}: {str(e)}")
216
+ raise
217
+
218
+ async def disconnect(self) -> None:
219
+ """Disconnect from the MCP server and clean up resources"""
220
+ # Thread-safe disconnect
221
+ async with self._session_lock:
222
+ if not self._connected:
223
+ return
224
+
225
+ try:
226
+ # Signal the connection task to stop
227
+ self._connected = False
228
+
229
+ # Wait for background task to finish (with timeout)
230
+ if self._stdio_context_task and not self._stdio_context_task.done():
231
+ try:
232
+ await asyncio.wait_for(self._stdio_context_task, timeout=2.0)
233
+ except asyncio.TimeoutError:
234
+ logger.warning(f"MCP disconnect timeout for {self.server_name}, cancelling task")
235
+ self._stdio_context_task.cancel()
236
+ try:
237
+ await self._stdio_context_task
238
+ except asyncio.CancelledError:
239
+ pass
240
+
241
+ self._stdio_context_task = None
242
+ logger.info(f"Disconnected from MCP server: {self.server_name}")
243
+
244
+ except Exception as e:
245
+ logger.warning(f"Error during MCP server disconnect: {str(e)}")
246
+
247
+ def list_tools(self) -> List[MCPTool]:
248
+ """
249
+ Get list of available tools from this MCP server.
250
+
251
+ Returns:
252
+ List of MCPTool objects
253
+
254
+ Raises:
255
+ RuntimeError: If not connected to server
256
+ """
257
+ if not self._connected:
258
+ raise RuntimeError(f"MCP server {self.server_name} not connected. Call connect() first.")
259
+
260
+ return self._tools.copy()
261
+
262
+ async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
263
+ """
264
+ Execute a tool on the MCP server with thread-safe session access.
265
+
266
+ Args:
267
+ tool_name: Name of the tool to call
268
+ arguments: Arguments to pass to the tool
269
+
270
+ Returns:
271
+ Tool execution result
272
+
273
+ Raises:
274
+ RuntimeError: If not connected or tool not found
275
+ Exception: If tool execution fails
276
+ """
277
+ # Thread-safe session access
278
+ async with self._session_lock:
279
+ if not self._connected:
280
+ raise RuntimeError(f"MCP server {self.server_name} not connected")
281
+
282
+ # Verify tool exists
283
+ tool = next((t for t in self._tools if t.name == tool_name), None)
284
+ if not tool:
285
+ available_tools = [t.name for t in self._tools]
286
+ raise RuntimeError(
287
+ f"Tool '{tool_name}' not found on server {self.server_name}. "
288
+ f"Available tools: {', '.join(available_tools)}"
289
+ )
290
+
291
+ try:
292
+ # Call the tool via MCP session (protected by lock)
293
+ result = await self._session.call_tool(tool_name, arguments=arguments)
294
+
295
+ # Extract content from result
296
+ if hasattr(result, 'content'):
297
+ # MCP returns content as a list of content items
298
+ if isinstance(result.content, list) and len(result.content) > 0:
299
+ first_content = result.content[0]
300
+ # Text content
301
+ if hasattr(first_content, 'text'):
302
+ return first_content.text
303
+ # Other content types
304
+ return str(first_content)
305
+ return result.content
306
+
307
+ return result
308
+
309
+ except Exception as e:
310
+ error_msg = f"MCP tool call failed: {tool_name} on {self.server_name}: {str(e)}"
311
+ logger.error(error_msg)
312
+ raise Exception(error_msg) from e
313
+
314
+ @property
315
+ def is_connected(self) -> bool:
316
+ """Check if server is connected"""
317
+ return self._connected
318
+
319
+ @property
320
+ def tool_names(self) -> List[str]:
321
+ """Get list of tool names"""
322
+ return [t.name for t in self._tools]
323
+
324
+ def __repr__(self) -> str:
325
+ status = "connected" if self._connected else "disconnected"
326
+ tool_count = len(self._tools) if self._tools else 0
327
+ return f"MCPServer({self.server_name}, {status}, {tool_count} tools)"
328
+
329
+ async def __aenter__(self):
330
+ """Async context manager entry - automatically connect"""
331
+ await self.connect()
332
+ return self
333
+
334
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
335
+ """Async context manager exit - automatically disconnect"""
336
+ await self.disconnect()
337
+
338
+
339
+ class MCPToolRegistry:
340
+ """
341
+ Registry for managing multiple MCP servers and their tools.
342
+
343
+ Used internally by SubstrateAgent to aggregate tools from multiple
344
+ MCP servers and route tool calls to the appropriate server.
345
+ """
346
+
347
+ def __init__(self):
348
+ """Initialize empty registry"""
349
+ self.servers: List[MCPServer] = []
350
+ self._tool_server_map: Dict[str, MCPServer] = {}
351
+
352
+ async def add_server(self, server: MCPServer) -> None:
353
+ """
354
+ Add an MCP server to the registry.
355
+
356
+ Args:
357
+ server: MCPServer instance to add
358
+ """
359
+ # Connect if not already connected
360
+ if not server.is_connected:
361
+ await server.connect()
362
+
363
+ # Add to registry
364
+ self.servers.append(server)
365
+
366
+ # Map tools to servers
367
+ for tool in server.list_tools():
368
+ if tool.name in self._tool_server_map:
369
+ logger.warning(
370
+ f"Tool name collision: {tool.name} exists in multiple servers. "
371
+ f"Using tool from {server.server_name}"
372
+ )
373
+ self._tool_server_map[tool.name] = server
374
+
375
+ logger.info(f"Added MCP server {server.server_name} to registry")
376
+
377
+ def get_all_tools(self) -> List[MCPTool]:
378
+ """Get aggregated list of all tools from all servers"""
379
+ all_tools = []
380
+ for server in self.servers:
381
+ all_tools.extend(server.list_tools())
382
+ return all_tools
383
+
384
+ async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
385
+ """
386
+ Call a tool by routing to the appropriate server.
387
+
388
+ Args:
389
+ tool_name: Name of the tool to call
390
+ arguments: Arguments for the tool
391
+
392
+ Returns:
393
+ Tool execution result
394
+ """
395
+ server = self._tool_server_map.get(tool_name)
396
+ if not server:
397
+ available_tools = list(self._tool_server_map.keys())
398
+ raise RuntimeError(
399
+ f"Tool '{tool_name}' not found in any MCP server. "
400
+ f"Available tools: {', '.join(available_tools)}"
401
+ )
402
+
403
+ return await server.call_tool(tool_name, arguments)
404
+
405
+ async def disconnect_all(self) -> None:
406
+ """Disconnect from all MCP servers"""
407
+ for server in self.servers:
408
+ try:
409
+ await server.disconnect()
410
+ except Exception as e:
411
+ logger.warning(f"Error disconnecting from {server.server_name}: {str(e)}")
412
+
413
+ self.servers.clear()
414
+ self._tool_server_map.clear()
415
+
416
+ @property
417
+ def tool_count(self) -> int:
418
+ """Total number of tools across all servers"""
419
+ return len(self._tool_server_map)
420
+
421
+ @property
422
+ def server_count(self) -> int:
423
+ """Number of connected servers"""
424
+ return len(self.servers)
425
+
426
+
427
+ # Factory function for clean server configuration
428
+ def server(
429
+ command: str,
430
+ args: Optional[List[str]] = None,
431
+ env: Optional[Dict[str, str]] = None,
432
+ name: Optional[str] = None
433
+ ) -> Dict[str, Any]:
434
+ """
435
+ Create MCP server configuration.
436
+
437
+ This factory function provides a clean API for configuring MCP servers
438
+ to be used with SubstrateAgent.
439
+
440
+ Args:
441
+ command: Command to run MCP server (e.g., "uvx", "python", "npx")
442
+ args: Arguments for the command
443
+ env: Environment variables for the server process
444
+ name: Optional name for the server
445
+
446
+ Returns:
447
+ Server configuration dict
448
+
449
+ Example:
450
+ ```python
451
+ from daita.plugins import mcp
452
+
453
+ # Filesystem server
454
+ fs_server = mcp.server(
455
+ command="uvx",
456
+ args=["mcp-server-filesystem", "/data"]
457
+ )
458
+
459
+ # GitHub server with environment
460
+ gh_server = mcp.server(
461
+ command="npx",
462
+ args=["-y", "@modelcontextprotocol/server-github"],
463
+ env={"GITHUB_TOKEN": os.getenv("GITHUB_TOKEN")}
464
+ )
465
+ ```
466
+ """
467
+ return {
468
+ "command": command,
469
+ "args": args or [],
470
+ "env": env or {},
471
+ "name": name
472
+ }
473
+
474
+
475
+ # Export public API
476
+ __all__ = [
477
+ "MCPServer",
478
+ "MCPTool",
479
+ "MCPToolRegistry",
480
+ "server"
481
+ ]