dao-ai 0.1.2__py3-none-any.whl → 0.1.20__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. dao_ai/apps/__init__.py +24 -0
  2. dao_ai/apps/handlers.py +105 -0
  3. dao_ai/apps/model_serving.py +29 -0
  4. dao_ai/apps/resources.py +1122 -0
  5. dao_ai/apps/server.py +39 -0
  6. dao_ai/cli.py +546 -37
  7. dao_ai/config.py +1179 -139
  8. dao_ai/evaluation.py +543 -0
  9. dao_ai/genie/__init__.py +55 -7
  10. dao_ai/genie/cache/__init__.py +34 -7
  11. dao_ai/genie/cache/base.py +143 -2
  12. dao_ai/genie/cache/context_aware/__init__.py +31 -0
  13. dao_ai/genie/cache/context_aware/base.py +1151 -0
  14. dao_ai/genie/cache/context_aware/in_memory.py +609 -0
  15. dao_ai/genie/cache/context_aware/persistent.py +802 -0
  16. dao_ai/genie/cache/context_aware/postgres.py +1166 -0
  17. dao_ai/genie/cache/core.py +1 -1
  18. dao_ai/genie/cache/lru.py +257 -75
  19. dao_ai/genie/cache/optimization.py +890 -0
  20. dao_ai/genie/core.py +235 -11
  21. dao_ai/memory/postgres.py +175 -39
  22. dao_ai/middleware/__init__.py +38 -0
  23. dao_ai/middleware/assertions.py +3 -3
  24. dao_ai/middleware/context_editing.py +230 -0
  25. dao_ai/middleware/core.py +4 -4
  26. dao_ai/middleware/guardrails.py +3 -3
  27. dao_ai/middleware/human_in_the_loop.py +3 -2
  28. dao_ai/middleware/message_validation.py +4 -4
  29. dao_ai/middleware/model_call_limit.py +77 -0
  30. dao_ai/middleware/model_retry.py +121 -0
  31. dao_ai/middleware/pii.py +157 -0
  32. dao_ai/middleware/summarization.py +1 -1
  33. dao_ai/middleware/tool_call_limit.py +210 -0
  34. dao_ai/middleware/tool_retry.py +174 -0
  35. dao_ai/middleware/tool_selector.py +129 -0
  36. dao_ai/models.py +327 -370
  37. dao_ai/nodes.py +9 -16
  38. dao_ai/orchestration/core.py +33 -9
  39. dao_ai/orchestration/supervisor.py +29 -13
  40. dao_ai/orchestration/swarm.py +6 -1
  41. dao_ai/{prompts.py → prompts/__init__.py} +12 -61
  42. dao_ai/prompts/instructed_retriever_decomposition.yaml +58 -0
  43. dao_ai/prompts/instruction_reranker.yaml +14 -0
  44. dao_ai/prompts/router.yaml +37 -0
  45. dao_ai/prompts/verifier.yaml +46 -0
  46. dao_ai/providers/base.py +28 -2
  47. dao_ai/providers/databricks.py +363 -33
  48. dao_ai/state.py +1 -0
  49. dao_ai/tools/__init__.py +5 -3
  50. dao_ai/tools/genie.py +103 -26
  51. dao_ai/tools/instructed_retriever.py +366 -0
  52. dao_ai/tools/instruction_reranker.py +202 -0
  53. dao_ai/tools/mcp.py +539 -97
  54. dao_ai/tools/router.py +89 -0
  55. dao_ai/tools/slack.py +13 -2
  56. dao_ai/tools/sql.py +7 -3
  57. dao_ai/tools/unity_catalog.py +32 -10
  58. dao_ai/tools/vector_search.py +493 -160
  59. dao_ai/tools/verifier.py +159 -0
  60. dao_ai/utils.py +182 -2
  61. dao_ai/vector_search.py +46 -1
  62. {dao_ai-0.1.2.dist-info → dao_ai-0.1.20.dist-info}/METADATA +45 -9
  63. dao_ai-0.1.20.dist-info/RECORD +89 -0
  64. dao_ai/agent_as_code.py +0 -22
  65. dao_ai/genie/cache/semantic.py +0 -970
  66. dao_ai-0.1.2.dist-info/RECORD +0 -64
  67. {dao_ai-0.1.2.dist-info → dao_ai-0.1.20.dist-info}/WHEEL +0 -0
  68. {dao_ai-0.1.2.dist-info → dao_ai-0.1.20.dist-info}/entry_points.txt +0 -0
  69. {dao_ai-0.1.2.dist-info → dao_ai-0.1.20.dist-info}/licenses/LICENSE +0 -0
@@ -150,7 +150,7 @@ def create_summarization_middleware(
150
150
  chat_history: ChatHistoryModel configuration for summarization
151
151
 
152
152
  Returns:
153
- LoggingSummarizationMiddleware configured with the specified parameters
153
+ List containing LoggingSummarizationMiddleware configured with the specified parameters
154
154
 
155
155
  Example:
156
156
  from dao_ai.config import ChatHistoryModel, LLMModel
@@ -0,0 +1,210 @@
1
+ """
2
+ Tool call limit middleware for DAO AI agents.
3
+
4
+ This module provides a factory for creating LangChain's ToolCallLimitMiddleware
5
+ from DAO AI configuration.
6
+
7
+ Example:
8
+ from dao_ai.middleware import create_tool_call_limit_middleware
9
+
10
+ # Global limit across all tools
11
+ middleware = create_tool_call_limit_middleware(
12
+ thread_limit=20,
13
+ run_limit=10,
14
+ )
15
+
16
+ # Limit specific tool by name
17
+ search_limiter = create_tool_call_limit_middleware(
18
+ tool="search_web",
19
+ run_limit=3,
20
+ exit_behavior="continue",
21
+ )
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from typing import Any, Literal
27
+
28
+ from langchain.agents.middleware import ToolCallLimitMiddleware
29
+ from langchain_core.tools import BaseTool
30
+ from loguru import logger
31
+
32
+ from dao_ai.config import BaseFunctionModel, ToolModel
33
+
34
+ __all__ = [
35
+ "ToolCallLimitMiddleware",
36
+ "create_tool_call_limit_middleware",
37
+ ]
38
+
39
+
40
+ def _resolve_tool(tool: str | ToolModel | dict[str, Any]) -> list[str]:
41
+ """
42
+ Resolve tool argument to a list of actual tool names.
43
+
44
+ Args:
45
+ tool: String name, ToolModel, or dict to resolve
46
+
47
+ Returns:
48
+ List of tool name strings
49
+
50
+ Raises:
51
+ ValueError: If dict cannot be converted to ToolModel
52
+ TypeError: If tool is not a supported type
53
+ """
54
+ # String: return as single-item list
55
+ if isinstance(tool, str):
56
+ return [tool]
57
+
58
+ # Dict: convert to ToolModel first
59
+ if isinstance(tool, dict):
60
+ try:
61
+ tool_model = ToolModel(**tool)
62
+ except Exception as e:
63
+ raise ValueError(
64
+ f"Failed to construct ToolModel from dict: {e}\n"
65
+ f"Dict must have 'name' and 'function' keys."
66
+ ) from e
67
+ elif isinstance(tool, ToolModel):
68
+ tool_model = tool
69
+ else:
70
+ raise TypeError(
71
+ f"tool must be str, ToolModel, or dict, got {type(tool).__name__}"
72
+ )
73
+
74
+ # Extract tool names from ToolModel
75
+ return _extract_tool_names(tool_model)
76
+
77
+
78
+ def _extract_tool_names(tool_model: ToolModel) -> list[str]:
79
+ """
80
+ Extract actual tool names from a ToolModel.
81
+
82
+ A single ToolModel can produce multiple tools (e.g., UC functions).
83
+ Falls back to ToolModel.name if extraction fails.
84
+ """
85
+ function = tool_model.function
86
+
87
+ # String function references can't be introspected
88
+ if not isinstance(function, BaseFunctionModel):
89
+ logger.debug(
90
+ "Cannot extract names from string function, using ToolModel.name",
91
+ tool_model_name=tool_model.name,
92
+ )
93
+ return [tool_model.name]
94
+
95
+ # Try to extract names from created tools
96
+ try:
97
+ tool_names = [
98
+ tool.name
99
+ for tool in function.as_tools()
100
+ if isinstance(tool, BaseTool) and tool.name
101
+ ]
102
+ if tool_names:
103
+ logger.trace(
104
+ "Extracted tool names",
105
+ tool_model_name=tool_model.name,
106
+ tool_names=tool_names,
107
+ )
108
+ return tool_names
109
+ except Exception as e:
110
+ logger.warning(
111
+ "Error extracting tool names from ToolModel",
112
+ tool_model_name=tool_model.name,
113
+ error=str(e),
114
+ )
115
+
116
+ # Fallback to ToolModel.name
117
+ logger.debug(
118
+ "Falling back to ToolModel.name",
119
+ tool_model_name=tool_model.name,
120
+ )
121
+ return [tool_model.name]
122
+
123
+
124
+ def create_tool_call_limit_middleware(
125
+ tool: str | ToolModel | dict[str, Any] | None = None,
126
+ thread_limit: int | None = None,
127
+ run_limit: int | None = None,
128
+ exit_behavior: Literal["continue", "error", "end"] = "continue",
129
+ ) -> ToolCallLimitMiddleware:
130
+ """
131
+ Create a ToolCallLimitMiddleware with graceful termination support.
132
+
133
+ Factory for LangChain's ToolCallLimitMiddleware that supports DAO AI
134
+ configuration types.
135
+
136
+ Args:
137
+ tool: Tool to limit. Can be:
138
+ - None: Global limit on all tools
139
+ - str: Limit specific tool by name
140
+ - ToolModel: Limit tool(s) from DAO AI config
141
+ - dict: Tool config dict (converted to ToolModel)
142
+ thread_limit: Max calls per thread (conversation). Requires checkpointer.
143
+ run_limit: Max calls per run (single invocation).
144
+ exit_behavior: What to do when limit hit:
145
+ - "continue": Block tool with error message, let agent continue
146
+ - "error": Raise ToolCallLimitExceededError immediately
147
+ - "end": Stop execution gracefully (single-tool only)
148
+
149
+ Returns:
150
+ A ToolCallLimitMiddleware instance. If ToolModel produces multiple tools,
151
+ only the first tool is used (with a warning logged).
152
+
153
+ Raises:
154
+ ValueError: If no limits specified, or invalid dict
155
+ TypeError: If tool is unsupported type
156
+
157
+ Example:
158
+ # Global limit
159
+ limiter = create_tool_call_limit_middleware(run_limit=10)
160
+
161
+ # Tool-specific limit
162
+ limiter = create_tool_call_limit_middleware(
163
+ tool="search_web",
164
+ run_limit=3,
165
+ exit_behavior="continue",
166
+ )
167
+ """
168
+ if thread_limit is None and run_limit is None:
169
+ raise ValueError("At least one of thread_limit or run_limit must be specified.")
170
+
171
+ # Global limit: no tool parameter
172
+ if tool is None:
173
+ logger.debug(
174
+ "Creating global tool call limit",
175
+ thread_limit=thread_limit,
176
+ run_limit=run_limit,
177
+ exit_behavior=exit_behavior,
178
+ )
179
+ return ToolCallLimitMiddleware(
180
+ thread_limit=thread_limit,
181
+ run_limit=run_limit,
182
+ exit_behavior=exit_behavior,
183
+ )
184
+
185
+ # Resolve to list of tool names
186
+ names = _resolve_tool(tool)
187
+
188
+ # Use first tool name (warn if multiple)
189
+ tool_name = names[0]
190
+ if len(names) > 1:
191
+ logger.warning(
192
+ "ToolModel resolved to multiple tool names, using first only",
193
+ tool_names=names,
194
+ using=tool_name,
195
+ )
196
+
197
+ logger.debug(
198
+ "Creating tool call limit middleware",
199
+ tool_name=tool_name,
200
+ thread_limit=thread_limit,
201
+ run_limit=run_limit,
202
+ exit_behavior=exit_behavior,
203
+ )
204
+
205
+ return ToolCallLimitMiddleware(
206
+ tool_name=tool_name,
207
+ thread_limit=thread_limit,
208
+ run_limit=run_limit,
209
+ exit_behavior=exit_behavior,
210
+ )
@@ -0,0 +1,174 @@
1
+ """
2
+ Tool retry middleware for DAO AI agents.
3
+
4
+ Automatically retries failed tool calls with configurable exponential backoff.
5
+
6
+ Example:
7
+ from dao_ai.middleware import create_tool_retry_middleware
8
+
9
+ # Retry failed tool calls with exponential backoff
10
+ middleware = create_tool_retry_middleware(
11
+ max_retries=3,
12
+ backoff_factor=2.0,
13
+ initial_delay=1.0,
14
+ )
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Any, Callable, Literal
20
+
21
+ from langchain.agents.middleware import ToolRetryMiddleware
22
+ from langchain_core.tools import BaseTool
23
+ from loguru import logger
24
+
25
+ from dao_ai.config import BaseFunctionModel, ToolModel
26
+
27
+ __all__ = [
28
+ "ToolRetryMiddleware",
29
+ "create_tool_retry_middleware",
30
+ ]
31
+
32
+
33
+ def _resolve_tools(
34
+ tools: list[str | ToolModel | dict[str, Any]] | None,
35
+ ) -> list[str] | None:
36
+ """
37
+ Resolve tool specs to a list of tool name strings.
38
+
39
+ Returns None if tools is None (apply to all tools).
40
+ """
41
+ if tools is None:
42
+ return None
43
+
44
+ result: list[str] = []
45
+ for tool in tools:
46
+ if isinstance(tool, str):
47
+ result.append(tool)
48
+ elif isinstance(tool, dict):
49
+ try:
50
+ tool_model = ToolModel(**tool)
51
+ result.extend(_extract_tool_names(tool_model))
52
+ except Exception as e:
53
+ raise ValueError(f"Failed to construct ToolModel from dict: {e}") from e
54
+ elif isinstance(tool, ToolModel):
55
+ result.extend(_extract_tool_names(tool))
56
+ else:
57
+ raise TypeError(
58
+ f"Tool must be str, ToolModel, or dict, got {type(tool).__name__}"
59
+ )
60
+
61
+ return result if result else None
62
+
63
+
64
+ def _extract_tool_names(tool_model: ToolModel) -> list[str]:
65
+ """Extract tool names from ToolModel, falling back to ToolModel.name."""
66
+ function = tool_model.function
67
+
68
+ if not isinstance(function, BaseFunctionModel):
69
+ return [tool_model.name]
70
+
71
+ try:
72
+ tool_names = [
73
+ tool.name
74
+ for tool in function.as_tools()
75
+ if isinstance(tool, BaseTool) and tool.name
76
+ ]
77
+ return tool_names if tool_names else [tool_model.name]
78
+ except Exception:
79
+ return [tool_model.name]
80
+
81
+
82
+ def create_tool_retry_middleware(
83
+ max_retries: int = 3,
84
+ backoff_factor: float = 2.0,
85
+ initial_delay: float = 1.0,
86
+ max_delay: float | None = None,
87
+ jitter: bool = False,
88
+ tools: list[str | ToolModel | dict[str, Any]] | None = None,
89
+ retry_on: tuple[type[Exception], ...] | Callable[[Exception], bool] | None = None,
90
+ on_failure: Literal["continue", "error"] | Callable[[Exception], str] = "continue",
91
+ ) -> ToolRetryMiddleware:
92
+ """
93
+ Create a ToolRetryMiddleware for automatic tool call retries.
94
+
95
+ Handles transient failures in external API calls with exponential backoff.
96
+
97
+ Args:
98
+ max_retries: Max retry attempts after initial call. Default 3.
99
+ backoff_factor: Multiplier for exponential backoff. Default 2.0.
100
+ Delay = initial_delay * (backoff_factor ** retry_number)
101
+ Set to 0.0 for constant delay.
102
+ initial_delay: Initial delay in seconds before first retry. Default 1.0.
103
+ max_delay: Max delay in seconds (caps exponential growth). None = no cap.
104
+ jitter: Add ±25% random jitter to avoid thundering herd. Default False.
105
+ tools: List of tools to apply retry to. Can be:
106
+ - None: Apply to all tools (default)
107
+ - list of str: Tool names
108
+ - list of ToolModel: DAO AI tool models
109
+ - list of dict: Tool config dicts
110
+ retry_on: When to retry:
111
+ - None: Retry on all errors (default)
112
+ - tuple of Exception types: Retry only on these
113
+ - callable: Function(exception) -> bool
114
+ on_failure: Behavior when all retries exhausted:
115
+ - "continue": Return error message, let agent continue (default)
116
+ - "error": Re-raise exception, stop execution
117
+ - callable: Function(exception) -> str for custom message
118
+
119
+ Returns:
120
+ List containing ToolRetryMiddleware instance
121
+
122
+ Example:
123
+ # Basic retry with defaults
124
+ retry = create_tool_retry_middleware()
125
+
126
+ # Retry specific tools with custom backoff
127
+ retry = create_tool_retry_middleware(
128
+ max_retries=5,
129
+ backoff_factor=1.5,
130
+ initial_delay=0.5,
131
+ tools=["search_web", "query_database"],
132
+ )
133
+
134
+ # Retry only on specific exceptions
135
+ retry = create_tool_retry_middleware(
136
+ max_retries=3,
137
+ retry_on=(TimeoutError, ConnectionError),
138
+ on_failure="error",
139
+ )
140
+ """
141
+ tool_names = _resolve_tools(tools)
142
+
143
+ logger.debug(
144
+ "Creating tool retry middleware",
145
+ max_retries=max_retries,
146
+ backoff_factor=backoff_factor,
147
+ initial_delay=initial_delay,
148
+ max_delay=max_delay,
149
+ jitter=jitter,
150
+ tools=tool_names or "all",
151
+ on_failure=on_failure if isinstance(on_failure, str) else "custom",
152
+ )
153
+
154
+ # Build kwargs
155
+ kwargs: dict[str, Any] = {
156
+ "max_retries": max_retries,
157
+ "backoff_factor": backoff_factor,
158
+ "initial_delay": initial_delay,
159
+ "on_failure": on_failure,
160
+ }
161
+
162
+ if tool_names is not None:
163
+ kwargs["tools"] = tool_names
164
+
165
+ if max_delay is not None:
166
+ kwargs["max_delay"] = max_delay
167
+
168
+ if jitter:
169
+ kwargs["jitter"] = jitter
170
+
171
+ if retry_on is not None:
172
+ kwargs["retry_on"] = retry_on
173
+
174
+ return ToolRetryMiddleware(**kwargs)
@@ -0,0 +1,129 @@
1
+ """
2
+ Tool selector middleware for intelligently filtering tools before LLM calls.
3
+
4
+ This middleware uses an LLM to select relevant tools from a large set, improving
5
+ performance and accuracy by reducing context size and improving focus.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from langchain.agents.middleware import LLMToolSelectorMiddleware
13
+ from langchain_core.language_models import LanguageModelLike
14
+ from loguru import logger
15
+
16
+ from dao_ai.config import ToolModel
17
+
18
+
19
+ def create_llm_tool_selector_middleware(
20
+ model: LanguageModelLike,
21
+ max_tools: int = 3,
22
+ always_include: list[str | ToolModel | dict[str, Any]] | None = None,
23
+ ) -> LLMToolSelectorMiddleware:
24
+ """
25
+ Create an LLMToolSelectorMiddleware for intelligent tool selection.
26
+
27
+ Uses an LLM to analyze the current query and select the most relevant tools
28
+ before calling the main model. This is particularly useful for agents with
29
+ many tools (10+) where most aren't relevant for any given query.
30
+
31
+ Benefits:
32
+ - Reduces token usage by filtering irrelevant tools
33
+ - Improves model focus and accuracy
34
+ - Optimizes cost for agents with large tool sets
35
+ - Maintains context window efficiency
36
+
37
+ Args:
38
+ model: The LLM to use for tool selection. Typically a smaller, faster
39
+ model like "gpt-4o-mini" or similar.
40
+ max_tools: Maximum number of tools to select for each query.
41
+ Default 3. Adjust based on your use case - higher values
42
+ increase context but improve tool coverage.
43
+ always_include: List of tools that should always be included regardless
44
+ of the LLM's selection. Can be:
45
+ - str: Tool name
46
+ - ToolModel: Full tool configuration
47
+ - dict: Tool configuration dictionary
48
+ Use this for critical tools that should always be available.
49
+
50
+ Returns:
51
+ LLMToolSelectorMiddleware configured with the specified parameters
52
+
53
+ Example:
54
+ from dao_ai.middleware import create_llm_tool_selector_middleware
55
+ from dao_ai.llms import create_llm
56
+
57
+ # Use a fast, cheap model for tool selection
58
+ selector_llm = create_llm("databricks-gpt-4o-mini")
59
+
60
+ middleware = create_llm_tool_selector_middleware(
61
+ model=selector_llm,
62
+ max_tools=3,
63
+ always_include=["search_web"], # Always include search
64
+ )
65
+
66
+ Use Cases:
67
+ - Large tool sets (10+ tools) where most are specialized
68
+ - Cost optimization by reducing tokens in main model calls
69
+ - Improved accuracy by reducing tool confusion
70
+ - Dynamic tool filtering based on query relevance
71
+
72
+ Note:
73
+ The selector model makes an additional LLM call for each agent turn.
74
+ Choose a fast, inexpensive model to minimize latency and cost overhead.
75
+ """
76
+ # Extract tool names from always_include
77
+ always_include_names: list[str] = []
78
+ if always_include:
79
+ always_include_names = _resolve_tool_names(always_include)
80
+
81
+ logger.debug(
82
+ "Creating LLM tool selector middleware",
83
+ max_tools=max_tools,
84
+ always_include_count=len(always_include_names),
85
+ always_include=always_include_names,
86
+ )
87
+
88
+ return LLMToolSelectorMiddleware(
89
+ model=model,
90
+ max_tools=max_tools,
91
+ always_include=always_include_names if always_include_names else None,
92
+ )
93
+
94
+
95
+ def _resolve_tool_names(tools: list[str | ToolModel | dict[str, Any]]) -> list[str]:
96
+ """
97
+ Extract tool names from a list of tool specifications.
98
+
99
+ Args:
100
+ tools: List of tool specifications (strings, ToolModels, or dicts)
101
+
102
+ Returns:
103
+ List of tool names as strings
104
+ """
105
+ names: list[str] = []
106
+
107
+ for tool_spec in tools:
108
+ if isinstance(tool_spec, str):
109
+ # Simple string tool name
110
+ names.append(tool_spec)
111
+ elif isinstance(tool_spec, ToolModel):
112
+ # ToolModel - use its name
113
+ names.append(tool_spec.name)
114
+ elif isinstance(tool_spec, dict):
115
+ # Dictionary - try to extract name
116
+ if "name" in tool_spec:
117
+ names.append(tool_spec["name"])
118
+ else:
119
+ logger.warning(
120
+ "Tool dict missing 'name' field, skipping",
121
+ tool_spec=tool_spec,
122
+ )
123
+ else:
124
+ logger.warning(
125
+ "Unknown tool specification type, skipping",
126
+ tool_spec_type=type(tool_spec).__name__,
127
+ )
128
+
129
+ return names