daita-agents 0.2.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.
Files changed (69) hide show
  1. daita/__init__.py +216 -0
  2. daita/agents/__init__.py +33 -0
  3. daita/agents/base.py +743 -0
  4. daita/agents/substrate.py +1141 -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 +481 -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 +779 -0
  32. daita/core/reliability.py +381 -0
  33. daita/core/scaling.py +459 -0
  34. daita/core/tools.py +554 -0
  35. daita/core/tracing.py +770 -0
  36. daita/core/workflow.py +1144 -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 +291 -0
  45. daita/llm/base.py +530 -0
  46. daita/llm/factory.py +101 -0
  47. daita/llm/gemini.py +355 -0
  48. daita/llm/grok.py +219 -0
  49. daita/llm/mock.py +172 -0
  50. daita/llm/openai.py +220 -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 +849 -0
  55. daita/plugins/mcp.py +481 -0
  56. daita/plugins/mongodb.py +520 -0
  57. daita/plugins/mysql.py +362 -0
  58. daita/plugins/postgresql.py +342 -0
  59. daita/plugins/redis_messaging.py +500 -0
  60. daita/plugins/rest.py +537 -0
  61. daita/plugins/s3.py +770 -0
  62. daita/plugins/slack.py +729 -0
  63. daita/utils/__init__.py +18 -0
  64. daita_agents-0.2.0.dist-info/METADATA +409 -0
  65. daita_agents-0.2.0.dist-info/RECORD +69 -0
  66. daita_agents-0.2.0.dist-info/WHEEL +5 -0
  67. daita_agents-0.2.0.dist-info/entry_points.txt +2 -0
  68. daita_agents-0.2.0.dist-info/licenses/LICENSE +56 -0
  69. daita_agents-0.2.0.dist-info/top_level.txt +1 -0
daita/core/tools.py ADDED
@@ -0,0 +1,554 @@
1
+ """
2
+ Universal tool abstraction for Daita agents.
3
+
4
+ Tools are LLM-callable functions that can come from:
5
+ - Plugins (database queries, S3 operations, API calls)
6
+ - MCP servers (external tools via Model Context Protocol)
7
+ - Custom functions (user-defined Python functions)
8
+
9
+ This abstraction is provider-agnostic and supports both prompt-based
10
+ and native function calling (OpenAI, Anthropic, etc).
11
+ """
12
+
13
+ from dataclasses import dataclass
14
+ from typing import Callable, Dict, Any, List, Optional, Awaitable, Union, Literal, get_origin, get_args, get_type_hints
15
+ import asyncio
16
+ import inspect
17
+ import logging
18
+ import re
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def _parse_docstring_params(func: Callable) -> Dict[str, str]:
24
+ """
25
+ Extract parameter descriptions from Google/NumPy style docstrings.
26
+
27
+ Supports formats:
28
+ param: description
29
+ param (type): description
30
+ param : description
31
+
32
+ Args:
33
+ func: Function to extract docstring from
34
+
35
+ Returns:
36
+ Dict mapping parameter names to their descriptions
37
+ """
38
+ docstring = func.__doc__ or ""
39
+
40
+ # Find Args: section
41
+ args_match = re.search(
42
+ r'Args?:(.*?)(?=Returns?:|Raises?:|Example:|Notes?:|$)',
43
+ docstring,
44
+ re.DOTALL | re.IGNORECASE
45
+ )
46
+
47
+ if not args_match:
48
+ return {}
49
+
50
+ descriptions = {}
51
+ args_section = args_match.group(1)
52
+
53
+ # Match parameter entries: " param_name: description" or " param_name (type): description"
54
+ for match in re.finditer(
55
+ r'^\s+(\w+)(?:\s*\([^)]+\))?\s*:\s*(.+?)(?=^\s+\w+\s*(?:\([^)]+\))?\s*:|$)',
56
+ args_section,
57
+ re.MULTILINE | re.DOTALL
58
+ ):
59
+ param_name = match.group(1)
60
+ description = ' '.join(match.group(2).split()) # Clean up whitespace
61
+ descriptions[param_name] = description
62
+
63
+ return descriptions
64
+
65
+
66
+ def _type_hint_to_json_schema(hint: Any) -> Dict[str, Any]:
67
+ """
68
+ Convert Python type hints to JSON Schema.
69
+
70
+ Supports:
71
+ - Basic types: int, float, str, bool
72
+ - Optional[T]: Makes field not required, uses T's schema
73
+ - Literal["a", "b"]: Enum constraint
74
+ - List[T]: Array with item type
75
+ - Dict[K, V]: Object type
76
+ - Union[A, B]: anyOf schema
77
+
78
+ Args:
79
+ hint: Type hint to convert
80
+
81
+ Returns:
82
+ JSON Schema dict for the type
83
+ """
84
+ origin = get_origin(hint)
85
+ args = get_args(hint)
86
+
87
+ # Handle Union types (includes Optional)
88
+ if origin is Union:
89
+ # Check if it's Optional (Union with None)
90
+ non_none_args = [arg for arg in args if arg is not type(None)]
91
+
92
+ if len(non_none_args) == 1:
93
+ # Optional[T] - just return T's schema, optionality handled by 'required'
94
+ return _type_hint_to_json_schema(non_none_args[0])
95
+ else:
96
+ # Union[A, B, ...] - use anyOf
97
+ return {
98
+ "anyOf": [_type_hint_to_json_schema(arg) for arg in non_none_args]
99
+ }
100
+
101
+ # Handle Literal types
102
+ if origin is Literal:
103
+ # Determine type from first value
104
+ first_val = args[0] if args else ""
105
+ val_type = type(first_val).__name__
106
+ schema_type = {
107
+ "int": "integer",
108
+ "float": "number",
109
+ "str": "string",
110
+ "bool": "boolean"
111
+ }.get(val_type, "string")
112
+
113
+ return {
114
+ "type": schema_type,
115
+ "enum": list(args)
116
+ }
117
+
118
+ # Handle List types
119
+ if origin is list or origin is List:
120
+ schema = {"type": "array"}
121
+ if args:
122
+ schema["items"] = _type_hint_to_json_schema(args[0])
123
+ return schema
124
+
125
+ # Handle Dict types
126
+ if origin is dict or origin is Dict:
127
+ return {"type": "object"}
128
+
129
+ # Handle basic Python types
130
+ basic_type_map = {
131
+ int: "integer",
132
+ float: "number",
133
+ str: "string",
134
+ bool: "boolean",
135
+ list: "array",
136
+ dict: "object"
137
+ }
138
+
139
+ if hint in basic_type_map:
140
+ return {"type": basic_type_map[hint]}
141
+
142
+ # Default fallback
143
+ return {"type": "string"}
144
+
145
+
146
+ def _extract_parameters_from_function(func: Callable) -> Dict[str, Any]:
147
+ """
148
+ Extract JSON Schema from function signature with type hints and docstring.
149
+
150
+ Supports:
151
+ - Type hints: int, str, float, bool, Optional, Literal, List, Dict, Union
152
+ - Docstring parameter descriptions (Google/NumPy style)
153
+ - Default values (automatically marks as optional)
154
+
155
+ Args:
156
+ func: Python function to analyze
157
+
158
+ Returns:
159
+ JSON Schema dict with properties and required fields
160
+ """
161
+ sig = inspect.signature(func)
162
+ properties = {}
163
+ required = []
164
+
165
+ # Get type hints (resolves string annotations)
166
+ try:
167
+ type_hints = get_type_hints(func)
168
+ except Exception:
169
+ # Fallback if get_type_hints fails
170
+ type_hints = {}
171
+
172
+ # Get parameter descriptions from docstring
173
+ param_descriptions = _parse_docstring_params(func)
174
+
175
+ for param_name, param in sig.parameters.items():
176
+ if param_name in ('self', 'cls'):
177
+ continue
178
+
179
+ # Get type hint for this parameter
180
+ type_hint = type_hints.get(param_name, param.annotation)
181
+
182
+ # Check if parameter is Optional (has None in Union)
183
+ is_optional = False
184
+ if type_hint != inspect.Parameter.empty:
185
+ origin = get_origin(type_hint)
186
+ args = get_args(type_hint)
187
+ if origin is Union and type(None) in args:
188
+ is_optional = True
189
+
190
+ # Convert type hint to JSON Schema
191
+ if type_hint != inspect.Parameter.empty:
192
+ schema = _type_hint_to_json_schema(type_hint)
193
+ else:
194
+ schema = {"type": "string"}
195
+
196
+ # Get description from docstring or use generic
197
+ description = param_descriptions.get(param_name, f"Parameter: {param_name}")
198
+ schema["description"] = description
199
+
200
+ properties[param_name] = schema
201
+
202
+ # Mark as required if no default value and not Optional
203
+ has_default = param.default != inspect.Parameter.empty
204
+ if not has_default and not is_optional:
205
+ required.append(param_name)
206
+
207
+ return {
208
+ "type": "object",
209
+ "properties": properties,
210
+ "required": required
211
+ }
212
+
213
+
214
+ def _make_async_handler(func: Callable) -> Callable[[Dict[str, Any]], Awaitable[Any]]:
215
+ """
216
+ Convert any function to async handler format.
217
+
218
+ Args:
219
+ func: Sync or async function to convert
220
+
221
+ Returns:
222
+ Async handler that accepts Dict[str, Any] and returns result
223
+ """
224
+ if asyncio.iscoroutinefunction(func):
225
+ async def handler(args: Dict[str, Any]) -> Any:
226
+ return await func(**args)
227
+ else:
228
+ async def handler(args: Dict[str, Any]) -> Any:
229
+ return func(**args)
230
+ return handler
231
+
232
+
233
+ def tool(
234
+ func: Optional[Callable] = None,
235
+ *,
236
+ name: Optional[str] = None,
237
+ description: Optional[str] = None,
238
+ timeout_seconds: Optional[int] = None,
239
+ category: Optional[str] = None,
240
+ **kwargs
241
+ ) -> Union['AgentTool', Callable]:
242
+ """
243
+ Convert a function into an AgentTool.
244
+ Works as both decorator and function call.
245
+
246
+ Automatically extracts parameter schema from type hints and docstring.
247
+
248
+ Usage as decorator:
249
+ @tool
250
+ async def calculator(a: int, b: int) -> int:
251
+ '''Add two numbers'''
252
+ return a + b
253
+
254
+ Usage with options:
255
+ @tool(timeout_seconds=30, category="math")
256
+ async def calculator(a: int, b: int) -> int:
257
+ return a + b
258
+
259
+ Usage as function:
260
+ calc_tool = tool(calculator)
261
+ calc_tool = tool(calculator, timeout_seconds=30)
262
+
263
+ Args:
264
+ func: Function to convert (when used as direct call)
265
+ name: Tool name (defaults to function name)
266
+ description: Tool description (defaults to docstring first line)
267
+ timeout_seconds: Execution timeout in seconds
268
+ category: Tool category for organization
269
+ **kwargs: Additional AgentTool fields
270
+
271
+ Returns:
272
+ AgentTool instance or decorator function
273
+ """
274
+ def create_tool(f: Callable) -> 'AgentTool':
275
+ tool_name = name or f.__name__
276
+ tool_description = description or (f.__doc__ or f"Execute {tool_name}").strip().split('\n')[0]
277
+ tool_parameters = _extract_parameters_from_function(f)
278
+ handler = _make_async_handler(f)
279
+
280
+ return AgentTool(
281
+ name=tool_name,
282
+ description=tool_description,
283
+ parameters=tool_parameters,
284
+ handler=handler,
285
+ timeout_seconds=timeout_seconds,
286
+ category=category,
287
+ source="custom",
288
+ **kwargs
289
+ )
290
+
291
+ # Used as @tool (no parentheses)
292
+ if func is not None:
293
+ return create_tool(func)
294
+
295
+ # Used as @tool(...) (with arguments)
296
+ return create_tool
297
+
298
+
299
+ @dataclass
300
+ class AgentTool:
301
+ """
302
+ Universal tool definition for agent-LLM integration.
303
+
304
+ Represents any callable function that an agent can use, regardless of source.
305
+ Designed to be compatible with industry standards (LangChain, AutoGen, etc).
306
+
307
+ Example:
308
+ ```python
309
+ tool = AgentTool(
310
+ name="search_database",
311
+ description="Search for records in the database",
312
+ parameters={
313
+ "query": {
314
+ "type": "string",
315
+ "description": "SQL query to execute",
316
+ "required": True
317
+ }
318
+ },
319
+ handler=async_search_function
320
+ )
321
+ ```
322
+ """
323
+
324
+ # Core fields (required)
325
+ name: str
326
+ description: str
327
+ parameters: Dict[str, Any] # JSON Schema format
328
+ handler: Callable[[Dict[str, Any]], Awaitable[Any]] # async function
329
+
330
+ # Optional metadata
331
+ category: Optional[str] = None # "database", "storage", "api", etc
332
+ source: str = "custom" # "plugin", "mcp", "custom"
333
+ plugin_name: Optional[str] = None # Which plugin provides this tool
334
+
335
+ # Safety features
336
+ timeout_seconds: Optional[int] = None # Execution timeout
337
+
338
+ def to_openai_function(self) -> Dict[str, Any]:
339
+ """
340
+ Convert to OpenAI function calling format.
341
+
342
+ Returns:
343
+ OpenAI function definition dict
344
+
345
+ Reference:
346
+ https://platform.openai.com/docs/guides/function-calling
347
+ """
348
+ return {
349
+ "type": "function",
350
+ "function": {
351
+ "name": self.name,
352
+ "description": self.description,
353
+ "parameters": self.parameters # Already in correct JSON Schema format
354
+ }
355
+ }
356
+
357
+ def to_anthropic_tool(self) -> Dict[str, Any]:
358
+ """
359
+ Convert to Anthropic tool format.
360
+
361
+ Returns:
362
+ Anthropic tool definition dict
363
+
364
+ Reference:
365
+ https://docs.anthropic.com/claude/docs/tool-use
366
+ """
367
+ return {
368
+ "name": self.name,
369
+ "description": self.description,
370
+ "input_schema": self.parameters # Already in correct JSON Schema format
371
+ }
372
+
373
+ def to_llm_function(self) -> Dict[str, Any]:
374
+ """
375
+ Generic LLM function format (works for most providers).
376
+
377
+ For provider-specific formats, use to_openai_function() or to_anthropic_tool().
378
+ This format is used for prompt-based tool calling.
379
+ """
380
+ return {
381
+ "name": self.name,
382
+ "description": self.description,
383
+ "parameters": self.parameters # Already in correct JSON Schema format
384
+ }
385
+
386
+ def to_prompt_description(self) -> str:
387
+ """
388
+ Generate human-readable tool description for prompt injection.
389
+
390
+ Used in prompt-based tool calling (non-native function calling).
391
+
392
+ Returns:
393
+ Formatted tool description string
394
+ """
395
+ params_desc = []
396
+ properties = self.parameters.get("properties", {})
397
+ required_params = self.parameters.get("required", [])
398
+
399
+ for param_name, param_info in properties.items():
400
+ required = " (required)" if param_name in required_params else " (optional)"
401
+ param_type = param_info.get("type", "any")
402
+ param_desc = param_info.get("description", "")
403
+ params_desc.append(
404
+ f" - {param_name} ({param_type}){required}: {param_desc}"
405
+ )
406
+
407
+ params_str = "\n".join(params_desc) if params_desc else " (no parameters)"
408
+
409
+ return f"{self.name}: {self.description}\nParameters:\n{params_str}"
410
+
411
+ async def execute(self, arguments: Dict[str, Any]) -> Any:
412
+ """
413
+ Execute the tool with given arguments.
414
+
415
+ Args:
416
+ arguments: Tool arguments matching the parameter schema
417
+
418
+ Returns:
419
+ Tool execution result
420
+
421
+ Raises:
422
+ RuntimeError: If tool execution fails or times out
423
+ """
424
+ if not callable(self.handler):
425
+ raise RuntimeError(f"Tool '{self.name}' has non-callable handler")
426
+
427
+ # Execute with timeout if specified
428
+ if self.timeout_seconds:
429
+ try:
430
+ result = await asyncio.wait_for(
431
+ self.handler(arguments),
432
+ timeout=self.timeout_seconds
433
+ )
434
+ return result
435
+ except asyncio.TimeoutError:
436
+ raise RuntimeError(
437
+ f"Tool '{self.name}' execution timed out after {self.timeout_seconds}s"
438
+ )
439
+ else:
440
+ # No timeout
441
+ return await self.handler(arguments)
442
+
443
+ @classmethod
444
+ def from_mcp_tool(cls, mcp_tool, mcp_registry) -> 'AgentTool':
445
+ """
446
+ Create AgentTool from an MCP tool.
447
+
448
+ Args:
449
+ mcp_tool: MCPTool instance from MCP plugin
450
+ mcp_registry: MCPToolRegistry for routing calls
451
+
452
+ Returns:
453
+ AgentTool instance that wraps the MCP tool
454
+ """
455
+ # Create handler that routes to MCP registry
456
+ async def mcp_handler(arguments: Dict[str, Any]) -> Any:
457
+ return await mcp_registry.call_tool(mcp_tool.name, arguments)
458
+
459
+ return cls(
460
+ name=mcp_tool.name,
461
+ description=mcp_tool.description,
462
+ parameters=mcp_tool.input_schema.get("properties", {}),
463
+ handler=mcp_handler,
464
+ source="mcp",
465
+ category="mcp"
466
+ )
467
+
468
+
469
+ class ToolRegistry:
470
+ """
471
+ Registry for managing tools from multiple sources.
472
+
473
+ Used internally by agents to aggregate tools from plugins, MCP servers,
474
+ and custom functions.
475
+ """
476
+
477
+ def __init__(self):
478
+ """Initialize empty tool registry"""
479
+ self.tools: List[AgentTool] = []
480
+ self._tool_map: Dict[str, AgentTool] = {}
481
+
482
+ def register(self, tool: AgentTool) -> None:
483
+ """
484
+ Register a tool.
485
+
486
+ Args:
487
+ tool: AgentTool to register
488
+ """
489
+ if tool.name in self._tool_map:
490
+ logger.warning(
491
+ f"Tool '{tool.name}' already registered. "
492
+ f"Overwriting (old source: {self._tool_map[tool.name].source}, "
493
+ f"new source: {tool.source})"
494
+ )
495
+
496
+ self.tools.append(tool)
497
+ self._tool_map[tool.name] = tool
498
+
499
+ logger.debug(f"Registered tool: {tool.name} (source: {tool.source})")
500
+
501
+ def register_many(self, tools: List[AgentTool]) -> None:
502
+ """
503
+ Register multiple tools at once.
504
+
505
+ Args:
506
+ tools: List of AgentTool instances
507
+ """
508
+ for tool in tools:
509
+ self.register(tool)
510
+
511
+ def get(self, name: str) -> Optional[AgentTool]:
512
+ """
513
+ Get tool by name.
514
+
515
+ Args:
516
+ name: Tool name
517
+
518
+ Returns:
519
+ AgentTool instance or None if not found
520
+ """
521
+ return self._tool_map.get(name)
522
+
523
+ async def execute(self, name: str, arguments: Dict[str, Any]) -> Any:
524
+ """
525
+ Execute a tool by name.
526
+
527
+ Args:
528
+ name: Tool name
529
+ arguments: Tool arguments
530
+
531
+ Returns:
532
+ Tool execution result
533
+
534
+ Raises:
535
+ RuntimeError: If tool not found or execution fails
536
+ """
537
+ tool = self.get(name)
538
+ if not tool:
539
+ available = [t.name for t in self.tools]
540
+ raise RuntimeError(
541
+ f"Tool '{name}' not found. Available tools: {', '.join(available)}"
542
+ )
543
+
544
+ return await tool.execute(arguments)
545
+
546
+ @property
547
+ def tool_count(self) -> int:
548
+ """Total number of registered tools"""
549
+ return len(self.tools)
550
+
551
+ @property
552
+ def tool_names(self) -> List[str]:
553
+ """List of all tool names"""
554
+ return list(self._tool_map.keys())