dao-ai 0.1.5__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 (57) 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 +446 -16
  7. dao_ai/config.py +1034 -103
  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 +5 -0
  23. dao_ai/middleware/tool_selector.py +129 -0
  24. dao_ai/models.py +327 -370
  25. dao_ai/nodes.py +4 -4
  26. dao_ai/orchestration/core.py +33 -9
  27. dao_ai/orchestration/supervisor.py +23 -8
  28. dao_ai/orchestration/swarm.py +6 -1
  29. dao_ai/{prompts.py → prompts/__init__.py} +12 -61
  30. dao_ai/prompts/instructed_retriever_decomposition.yaml +58 -0
  31. dao_ai/prompts/instruction_reranker.yaml +14 -0
  32. dao_ai/prompts/router.yaml +37 -0
  33. dao_ai/prompts/verifier.yaml +46 -0
  34. dao_ai/providers/base.py +28 -2
  35. dao_ai/providers/databricks.py +352 -33
  36. dao_ai/state.py +1 -0
  37. dao_ai/tools/__init__.py +5 -3
  38. dao_ai/tools/genie.py +103 -26
  39. dao_ai/tools/instructed_retriever.py +366 -0
  40. dao_ai/tools/instruction_reranker.py +202 -0
  41. dao_ai/tools/mcp.py +539 -97
  42. dao_ai/tools/router.py +89 -0
  43. dao_ai/tools/slack.py +13 -2
  44. dao_ai/tools/sql.py +7 -3
  45. dao_ai/tools/unity_catalog.py +32 -10
  46. dao_ai/tools/vector_search.py +493 -160
  47. dao_ai/tools/verifier.py +159 -0
  48. dao_ai/utils.py +182 -2
  49. dao_ai/vector_search.py +9 -1
  50. {dao_ai-0.1.5.dist-info → dao_ai-0.1.20.dist-info}/METADATA +10 -8
  51. dao_ai-0.1.20.dist-info/RECORD +89 -0
  52. dao_ai/agent_as_code.py +0 -22
  53. dao_ai/genie/cache/semantic.py +0 -970
  54. dao_ai-0.1.5.dist-info/RECORD +0 -70
  55. {dao_ai-0.1.5.dist-info → dao_ai-0.1.20.dist-info}/WHEEL +0 -0
  56. {dao_ai-0.1.5.dist-info → dao_ai-0.1.20.dist-info}/entry_points.txt +0 -0
  57. {dao_ai-0.1.5.dist-info → dao_ai-0.1.20.dist-info}/licenses/LICENSE +0 -0
dao_ai/nodes.py CHANGED
@@ -259,8 +259,6 @@ def create_agent_node(
259
259
  else:
260
260
  logger.debug("No custom prompt configured", agent=agent.name)
261
261
 
262
- checkpointer: bool = memory is not None and memory.checkpointer is not None
263
-
264
262
  # Get the prompt as middleware (always returns AgentMiddleware or None)
265
263
  prompt_middleware: AgentMiddleware | None = make_prompt(agent.prompt)
266
264
 
@@ -291,12 +289,14 @@ def create_agent_node(
291
289
  # Use LangChain v1's create_agent with middleware
292
290
  # AgentState extends MessagesState with additional DAO AI fields
293
291
  # System prompt is provided via middleware (dynamic_prompt)
292
+ # NOTE: checkpointer=False because these agents are used as subgraphs
293
+ # within the parent orchestration graph (swarm/supervisor) which handles
294
+ # checkpointing at the root level. Subgraphs cannot have checkpointer=True.
294
295
  logger.info(
295
296
  "Creating LangChain agent",
296
297
  agent=agent.name,
297
298
  tools_count=len(tools),
298
299
  middleware_count=len(middleware_list),
299
- has_checkpointer=checkpointer,
300
300
  )
301
301
 
302
302
  compiled_agent: CompiledStateGraph = create_agent(
@@ -304,7 +304,7 @@ def create_agent_node(
304
304
  model=llm,
305
305
  tools=tools,
306
306
  middleware=middleware_list,
307
- checkpointer=checkpointer,
307
+ checkpointer=False,
308
308
  state_schema=AgentState,
309
309
  context_schema=Context,
310
310
  response_format=response_format, # Add structured output support
@@ -9,7 +9,7 @@ This module provides the foundational utilities for multi-agent orchestration:
9
9
  - Main orchestration graph factory
10
10
  """
11
11
 
12
- from typing import Awaitable, Callable, Literal
12
+ from typing import Any, Awaitable, Callable, Literal
13
13
 
14
14
  from langchain.tools import ToolRuntime, tool
15
15
  from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, ToolMessage
@@ -179,8 +179,16 @@ def create_agent_node_handler(
179
179
  "messages": filtered_messages,
180
180
  }
181
181
 
182
- # Invoke the agent
183
- result: AgentState = await agent.ainvoke(agent_state, context=runtime.context)
182
+ # Build config with configurable from context for langmem compatibility
183
+ # langmem tools expect user_id to be in config.configurable
184
+ config: dict[str, Any] = {}
185
+ if runtime.context:
186
+ config = {"configurable": runtime.context.model_dump()}
187
+
188
+ # Invoke the agent with both context and config
189
+ result: AgentState = await agent.ainvoke(
190
+ agent_state, context=runtime.context, config=config
191
+ )
184
192
 
185
193
  # Extract agent response based on output mode
186
194
  result_messages = result.get("messages", [])
@@ -227,15 +235,31 @@ def create_handoff_tool(
227
235
  tool_call_id: str = runtime.tool_call_id
228
236
  logger.debug("Handoff to agent", target_agent=target_agent_name)
229
237
 
238
+ # Get the AIMessage that triggered this handoff (required for tool_use/tool_result pairing)
239
+ # LLMs expect tool calls to be paired with their responses, so we must include both
240
+ # the AIMessage containing the tool call and the ToolMessage acknowledging it.
241
+ messages: list[BaseMessage] = runtime.state.get("messages", [])
242
+ last_ai_message: AIMessage | None = None
243
+ for msg in reversed(messages):
244
+ if isinstance(msg, AIMessage) and msg.tool_calls:
245
+ last_ai_message = msg
246
+ break
247
+
248
+ # Build message list with proper pairing
249
+ update_messages: list[BaseMessage] = []
250
+ if last_ai_message:
251
+ update_messages.append(last_ai_message)
252
+ update_messages.append(
253
+ ToolMessage(
254
+ content=f"Transferred to {target_agent_name}",
255
+ tool_call_id=tool_call_id,
256
+ )
257
+ )
258
+
230
259
  return Command(
231
260
  update={
232
261
  "active_agent": target_agent_name,
233
- "messages": [
234
- ToolMessage(
235
- content=f"Transferred to {target_agent_name}",
236
- tool_call_id=tool_call_id,
237
- )
238
- ],
262
+ "messages": update_messages,
239
263
  },
240
264
  goto=target_agent_name,
241
265
  graph=Command.PARENT,
@@ -13,7 +13,7 @@ from langchain.agents import create_agent
13
13
  from langchain.agents.middleware import AgentMiddleware as LangchainAgentMiddleware
14
14
  from langchain.tools import ToolRuntime, tool
15
15
  from langchain_core.language_models import LanguageModelLike
16
- from langchain_core.messages import ToolMessage
16
+ from langchain_core.messages import AIMessage, BaseMessage, ToolMessage
17
17
  from langchain_core.tools import BaseTool
18
18
  from langgraph.checkpoint.base import BaseCheckpointSaver
19
19
  from langgraph.graph import StateGraph
@@ -75,15 +75,30 @@ def _create_handoff_back_to_supervisor_tool() -> BaseTool:
75
75
  tool_call_id: str = runtime.tool_call_id
76
76
  logger.debug("Agent handing back to supervisor", summary_preview=summary[:100])
77
77
 
78
+ # Get the AIMessage that triggered this handoff (required for tool_use/tool_result pairing)
79
+ # LLMs expect tool calls to be paired with their responses, so we must include both
80
+ # the AIMessage containing the tool call and the ToolMessage acknowledging it.
81
+ messages: list[BaseMessage] = runtime.state.get("messages", [])
82
+ last_ai_message: AIMessage | None = None
83
+ for msg in reversed(messages):
84
+ if isinstance(msg, AIMessage) and msg.tool_calls:
85
+ last_ai_message = msg
86
+ break
87
+
88
+ # Build message list with proper pairing
89
+ update_messages: list[BaseMessage] = []
90
+ if last_ai_message:
91
+ update_messages.append(last_ai_message)
92
+ update_messages.append(
93
+ ToolMessage(
94
+ content=f"Task completed: {summary}",
95
+ tool_call_id=tool_call_id,
96
+ )
97
+ )
98
+
78
99
  return Command(
79
100
  update={
80
- "active_agent": None,
81
- "messages": [
82
- ToolMessage(
83
- content=f"Task completed: {summary}",
84
- tool_call_id=tool_call_id,
85
- )
86
- ],
101
+ "messages": update_messages,
87
102
  },
88
103
  goto=SUPERVISOR_NODE,
89
104
  graph=Command.PARENT,
@@ -167,8 +167,13 @@ def create_swarm_graph(config: AppConfig) -> CompiledStateGraph:
167
167
  default_agent: str
168
168
  if isinstance(swarm.default_agent, AgentModel):
169
169
  default_agent = swarm.default_agent.name
170
- else:
170
+ elif swarm.default_agent is not None:
171
171
  default_agent = swarm.default_agent
172
+ elif len(config.app.agents) > 0:
173
+ # Fallback to first agent if no default specified
174
+ default_agent = config.app.agents[0].name
175
+ else:
176
+ raise ValueError("Swarm requires at least one agent and a default_agent")
172
177
 
173
178
  logger.info(
174
179
  "Creating swarm graph",
@@ -2,9 +2,11 @@
2
2
  Prompt utilities for DAO AI agents.
3
3
 
4
4
  This module provides utilities for creating dynamic prompts using
5
- LangChain v1's @dynamic_prompt middleware decorator pattern.
5
+ LangChain v1's @dynamic_prompt middleware decorator pattern, as well as
6
+ paths to prompt template files.
6
7
  """
7
8
 
9
+ from pathlib import Path
8
10
  from typing import Any, Optional
9
11
 
10
12
  from langchain.agents.middleware import (
@@ -18,6 +20,13 @@ from loguru import logger
18
20
  from dao_ai.config import PromptModel
19
21
  from dao_ai.state import Context
20
22
 
23
+ PROMPTS_DIR = Path(__file__).parent
24
+
25
+
26
+ def get_prompt_path(name: str) -> Path:
27
+ """Get the path to a prompt template file."""
28
+ return PROMPTS_DIR / name
29
+
21
30
 
22
31
  def make_prompt(
23
32
  base_system_prompt: Optional[str | PromptModel],
@@ -61,19 +70,14 @@ def make_prompt(
61
70
  @dynamic_prompt
62
71
  def dynamic_system_prompt(request: ModelRequest) -> str:
63
72
  """Generate dynamic system prompt based on runtime context."""
64
- # Get parameters from runtime context
73
+ # Initialize parameters for template variables
65
74
  params: dict[str, Any] = {
66
75
  input_variable: "" for input_variable in prompt_template.input_variables
67
76
  }
68
77
 
69
- # Access context from runtime
78
+ # Apply context fields as template parameters
70
79
  context: Context = request.runtime.context
71
80
  if context:
72
- if context.user_id and "user_id" in params:
73
- params["user_id"] = context.user_id
74
- if context.thread_id and "thread_id" in params:
75
- params["thread_id"] = context.thread_id
76
- # Apply all context fields as template parameters
77
81
  context_dict = context.model_dump()
78
82
  for key, value in context_dict.items():
79
83
  if key in params and value is not None:
@@ -89,56 +93,3 @@ def make_prompt(
89
93
  return formatted_prompt
90
94
 
91
95
  return dynamic_system_prompt
92
-
93
-
94
- def create_prompt_middleware(
95
- base_system_prompt: Optional[str | PromptModel],
96
- ) -> AgentMiddleware | None:
97
- """
98
- Create a dynamic prompt middleware from configuration.
99
-
100
- This always returns an AgentMiddleware suitable for use with
101
- LangChain v1's middleware system.
102
-
103
- Args:
104
- base_system_prompt: The system prompt string or PromptModel
105
-
106
- Returns:
107
- An AgentMiddleware created by @dynamic_prompt, or None if no prompt
108
- """
109
- if not base_system_prompt:
110
- return None
111
-
112
- # Extract template string from PromptModel or use string directly
113
- template_str: str
114
- if isinstance(base_system_prompt, PromptModel):
115
- template_str = base_system_prompt.template
116
- else:
117
- template_str = base_system_prompt
118
-
119
- prompt_template: PromptTemplate = PromptTemplate.from_template(template_str)
120
-
121
- @dynamic_prompt
122
- def prompt_middleware(request: ModelRequest) -> str:
123
- """Generate system prompt based on runtime context."""
124
- # Get parameters from runtime context
125
- params: dict[str, Any] = {
126
- input_variable: "" for input_variable in prompt_template.input_variables
127
- }
128
-
129
- # Access context from runtime
130
- context: Context = request.runtime.context
131
- if context:
132
- # Apply all context fields as template parameters
133
- context_dict = context.model_dump()
134
- for key, value in context_dict.items():
135
- if key in params and value is not None:
136
- params[key] = value
137
-
138
- # Format the prompt
139
- formatted_prompt: str = prompt_template.format(**params)
140
- logger.trace("Formatted dynamic prompt with context")
141
-
142
- return formatted_prompt
143
-
144
- return prompt_middleware
@@ -0,0 +1,58 @@
1
+ name: instructed_retriever_decomposition
2
+ description: Decomposes user queries into multiple search queries with metadata filters
3
+
4
+ template: |
5
+ You are a search query decomposition expert. Your task is to break down a user query into one or more focused search queries with appropriate metadata filters. Respond with a JSON object.
6
+
7
+ ## Current Time
8
+ {current_time}
9
+
10
+ ## Database Schema
11
+ {schema_description}
12
+
13
+ ## Constraints
14
+ {constraints}
15
+
16
+ ## Few-Shot Examples
17
+ {examples}
18
+
19
+ ## Instructions
20
+ 1. Analyze the user query and identify distinct search intents
21
+ 2. For each intent, create a focused search query text
22
+ 3. Extract metadata filters from the query using the exact filter syntax above
23
+ 4. Resolve relative time references (e.g., "last month", "past year") using the current time
24
+ 5. Generate at most {max_subqueries} search queries
25
+ 6. If no filters apply, set filters to null
26
+
27
+ ## User Query
28
+ {query}
29
+
30
+ Generate search queries that together capture all aspects of the user's information need.
31
+
32
+ variables:
33
+ - current_time
34
+ - schema_description
35
+ - constraints
36
+ - examples
37
+ - max_subqueries
38
+ - query
39
+
40
+ output_format: |
41
+ The output must be a JSON object with a "queries" field containing an array of search query objects.
42
+ Each search query object has:
43
+ - "text": The search query string
44
+ - "filters": An array of filter objects, each with "key" (column + optional operator) and "value", or null if no filters
45
+
46
+ Supported filter operators (append to column name):
47
+ - Equality: {"key": "column", "value": "val"} or {"key": "column", "value": ["val1", "val2"]}
48
+ - Exclusion: {"key": "column NOT", "value": "val"}
49
+ - Comparison: {"key": "column <", "value": 100}, also <=, >, >=
50
+ - Token match: {"key": "column LIKE", "value": "word"}
51
+ - Exclude token: {"key": "column NOT LIKE", "value": "word"}
52
+
53
+ Examples:
54
+ - [{"key": "brand_name", "value": "MILWAUKEE"}]
55
+ - [{"key": "price <", "value": 100}]
56
+ - [{"key": "brand_name NOT", "value": "DEWALT"}]
57
+ - [{"key": "brand_name", "value": ["MILWAUKEE", "DEWALT"]}]
58
+ - [{"key": "description LIKE", "value": "cordless"}]
@@ -0,0 +1,14 @@
1
+ name: instruction_aware_reranking
2
+ version: "1.1"
3
+ description: Rerank documents based on user instructions and constraints
4
+
5
+ template: |
6
+ Rerank these search results for the query "{query}".
7
+
8
+ {instructions}
9
+
10
+ ## Documents
11
+
12
+ {documents}
13
+
14
+ Score each document 0.0-1.0 based on relevance to the query and instructions. Return results sorted by score (highest first). Only include documents scoring > 0.1.
@@ -0,0 +1,37 @@
1
+ name: router_query_classification
2
+ version: "1.0"
3
+ description: Classify query to determine execution mode (standard vs instructed)
4
+
5
+ template: |
6
+ You are a query classification system. Your task is to determine the best execution mode for a search query.
7
+
8
+ ## Execution Modes
9
+
10
+ **standard**: Use for simple keyword or product searches without specific constraints.
11
+ - General questions about products
12
+ - Simple keyword searches
13
+ - Broad category browsing
14
+
15
+ **instructed**: Use for queries with explicit constraints that require metadata filtering.
16
+ - Price constraints ("under $100", "between $50 and $200")
17
+ - Brand preferences ("Milwaukee", "not DeWalt", "excluding Makita")
18
+ - Category filters ("power tools", "paint supplies")
19
+ - Time/recency constraints ("recent", "from last month", "updated this year")
20
+ - Comparison queries ("compare X and Y")
21
+ - Multiple combined constraints
22
+
23
+ ## Available Schema for Filtering
24
+
25
+ {schema_description}
26
+
27
+ ## Query to Classify
28
+
29
+ "{query}"
30
+
31
+ ## Instructions
32
+
33
+ Analyze the query and determine:
34
+ 1. Does it contain explicit constraints that can be translated to metadata filters?
35
+ 2. Would the query benefit from being decomposed into subqueries?
36
+
37
+ Return your classification as a JSON object with a single field "mode" set to either "standard" or "instructed".
@@ -0,0 +1,46 @@
1
+ name: result_verification
2
+ version: "1.0"
3
+ description: Verify search results satisfy user constraints
4
+
5
+ template: |
6
+ You are a result verification system. Your task is to determine whether search results satisfy the user's query constraints.
7
+
8
+ ## User Query
9
+
10
+ "{query}"
11
+
12
+ ## Schema Information
13
+
14
+ {schema_description}
15
+
16
+ ## Constraints to Verify
17
+
18
+ {constraints}
19
+
20
+ ## Retrieved Results (Top {num_results})
21
+
22
+ {results_summary}
23
+
24
+ ## Previous Attempt Feedback (if retry)
25
+
26
+ {previous_feedback}
27
+
28
+ ## Instructions
29
+
30
+ Analyze whether the results satisfy the user's explicit and implicit constraints:
31
+
32
+ 1. **Intent Match**: Do the results address what the user is looking for?
33
+ 2. **Explicit Constraints**: Are price, brand, category, date constraints met?
34
+ 3. **Relevance**: Are the results actually useful for the user's needs?
35
+
36
+ If results do NOT satisfy constraints, suggest specific filter relaxations:
37
+ - Use "REMOVE" to drop a filter entirely
38
+ - Use "BROADEN" to widen a range (e.g., price < 100 -> price < 150)
39
+ - Use specific values to change a filter
40
+
41
+ Return a JSON object with:
42
+ - passed: boolean (true if results are satisfactory)
43
+ - confidence: float (0.0-1.0, your confidence in the assessment)
44
+ - feedback: string (brief explanation of issues, if any)
45
+ - suggested_filter_relaxation: object (filter changes for retry, e.g., {{"brand_name": "REMOVE"}})
46
+ - unmet_constraints: array of strings (list of constraints not satisfied)
dao_ai/providers/base.py CHANGED
@@ -1,15 +1,19 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Any, Sequence
2
+ from typing import TYPE_CHECKING, Any, Sequence
3
3
 
4
4
  from dao_ai.config import (
5
5
  AppModel,
6
6
  DatasetModel,
7
+ DeploymentTarget,
7
8
  SchemaModel,
8
9
  UnityCatalogFunctionSqlModel,
9
10
  VectorStoreModel,
10
11
  VolumeModel,
11
12
  )
12
13
 
14
+ if TYPE_CHECKING:
15
+ from dao_ai.config import AppConfig
16
+
13
17
 
14
18
  class ServiceProvider(ABC):
15
19
  @abstractmethod
@@ -52,4 +56,26 @@ class ServiceProvider(ABC):
52
56
  ) -> Any: ...
53
57
 
54
58
  @abstractmethod
55
- def deploy_agent(self, config: AppModel) -> Any: ...
59
+ def deploy_model_serving_agent(self, config: "AppConfig") -> Any:
60
+ """Deploy agent to Databricks Model Serving endpoint."""
61
+ ...
62
+
63
+ @abstractmethod
64
+ def deploy_apps_agent(self, config: "AppConfig") -> Any:
65
+ """Deploy agent as a Databricks App."""
66
+ ...
67
+
68
+ @abstractmethod
69
+ def deploy_agent(
70
+ self,
71
+ config: "AppConfig",
72
+ target: DeploymentTarget = DeploymentTarget.MODEL_SERVING,
73
+ ) -> Any:
74
+ """
75
+ Deploy agent to the specified target.
76
+
77
+ Args:
78
+ config: The AppConfig containing deployment configuration
79
+ target: The deployment target (MODEL_SERVING or APPS)
80
+ """
81
+ ...