dao-ai 0.1.1__py3-none-any.whl → 0.1.3__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 (47) hide show
  1. dao_ai/agent_as_code.py +2 -5
  2. dao_ai/cli.py +65 -15
  3. dao_ai/config.py +672 -218
  4. dao_ai/genie/cache/core.py +6 -2
  5. dao_ai/genie/cache/lru.py +29 -11
  6. dao_ai/genie/cache/semantic.py +95 -44
  7. dao_ai/hooks/core.py +5 -5
  8. dao_ai/logging.py +56 -0
  9. dao_ai/memory/core.py +61 -44
  10. dao_ai/memory/databricks.py +54 -41
  11. dao_ai/memory/postgres.py +77 -36
  12. dao_ai/middleware/assertions.py +45 -17
  13. dao_ai/middleware/core.py +13 -7
  14. dao_ai/middleware/guardrails.py +30 -25
  15. dao_ai/middleware/human_in_the_loop.py +9 -5
  16. dao_ai/middleware/message_validation.py +61 -29
  17. dao_ai/middleware/summarization.py +16 -11
  18. dao_ai/models.py +172 -69
  19. dao_ai/nodes.py +148 -19
  20. dao_ai/optimization.py +26 -16
  21. dao_ai/orchestration/core.py +15 -8
  22. dao_ai/orchestration/supervisor.py +22 -8
  23. dao_ai/orchestration/swarm.py +57 -12
  24. dao_ai/prompts.py +17 -17
  25. dao_ai/providers/databricks.py +365 -155
  26. dao_ai/state.py +24 -6
  27. dao_ai/tools/__init__.py +2 -0
  28. dao_ai/tools/agent.py +1 -3
  29. dao_ai/tools/core.py +7 -7
  30. dao_ai/tools/email.py +29 -77
  31. dao_ai/tools/genie.py +18 -13
  32. dao_ai/tools/mcp.py +223 -156
  33. dao_ai/tools/python.py +5 -2
  34. dao_ai/tools/search.py +1 -1
  35. dao_ai/tools/slack.py +21 -9
  36. dao_ai/tools/sql.py +202 -0
  37. dao_ai/tools/time.py +30 -7
  38. dao_ai/tools/unity_catalog.py +129 -86
  39. dao_ai/tools/vector_search.py +318 -244
  40. dao_ai/utils.py +15 -10
  41. dao_ai-0.1.3.dist-info/METADATA +455 -0
  42. dao_ai-0.1.3.dist-info/RECORD +64 -0
  43. dao_ai-0.1.1.dist-info/METADATA +0 -1878
  44. dao_ai-0.1.1.dist-info/RECORD +0 -62
  45. {dao_ai-0.1.1.dist-info → dao_ai-0.1.3.dist-info}/WHEEL +0 -0
  46. {dao_ai-0.1.1.dist-info → dao_ai-0.1.3.dist-info}/entry_points.txt +0 -0
  47. {dao_ai-0.1.1.dist-info → dao_ai-0.1.3.dist-info}/licenses/LICENSE +0 -0
dao_ai/state.py CHANGED
@@ -15,7 +15,7 @@ from datetime import datetime
15
15
  from typing import Any, Optional
16
16
 
17
17
  from langgraph.graph import MessagesState
18
- from pydantic import BaseModel, Field
18
+ from pydantic import BaseModel, ConfigDict, Field
19
19
  from typing_extensions import NotRequired
20
20
 
21
21
 
@@ -133,15 +133,18 @@ class Context(BaseModel):
133
133
  This is passed to tools and middleware via the runtime parameter.
134
134
  Access via ToolRuntime[Context] in tools or Runtime[Context] in middleware.
135
135
 
136
- The `custom` dict allows application-specific context values that can be:
137
- - Used as template parameters in prompts (all keys are applied)
138
- - Validated by middleware (check for specific keys like "store_num")
136
+ Additional fields beyond user_id and thread_id can be added dynamically
137
+ and will be available as top-level attributes on the context object.
138
+ These fields are:
139
+ - Used as template parameters in prompts (all fields are applied)
140
+ - Validated by middleware (check for specific fields like "store_num")
141
+ - Accessible as direct attributes (e.g., context.store_num)
139
142
 
140
143
  Example:
141
144
  @tool
142
145
  def my_tool(runtime: ToolRuntime[Context]) -> str:
143
146
  user_id = runtime.context.user_id
144
- store_num = runtime.context.custom.get("store_num")
147
+ store_num = runtime.context.store_num # Direct attribute access
145
148
  return f"Hello, {user_id} at store {store_num}!"
146
149
 
147
150
  class MyMiddleware(AgentMiddleware[AgentState, Context]):
@@ -151,9 +154,24 @@ class Context(BaseModel):
151
154
  runtime: Runtime[Context]
152
155
  ) -> dict[str, Any] | None:
153
156
  user_id = runtime.context.user_id
157
+ store_num = getattr(runtime.context, "store_num", None)
154
158
  return None
155
159
  """
156
160
 
161
+ model_config = ConfigDict(
162
+ extra="allow"
163
+ ) # Allow extra fields as top-level attributes
164
+
157
165
  user_id: str | None = None
158
166
  thread_id: str | None = None
159
- custom: dict[str, Any] = Field(default_factory=dict)
167
+
168
+ @classmethod
169
+ def from_runnable_config(cls, config: dict[str, Any]) -> "Context":
170
+ """
171
+ Create Context from LangChain RunnableConfig.
172
+
173
+ This method is called by LangChain when context_schema is provided to create_agent.
174
+ It extracts the 'configurable' dict from the config and uses it to instantiate Context.
175
+ """
176
+ configurable = config.get("configurable", {})
177
+ return cls(**configurable)
dao_ai/tools/__init__.py CHANGED
@@ -9,6 +9,7 @@ from dao_ai.tools.memory import create_search_memory_tool
9
9
  from dao_ai.tools.python import create_factory_tool, create_python_tool
10
10
  from dao_ai.tools.search import create_search_tool
11
11
  from dao_ai.tools.slack import create_send_slack_message_tool
12
+ from dao_ai.tools.sql import create_execute_statement_tool
12
13
  from dao_ai.tools.time import (
13
14
  add_time_tool,
14
15
  current_time_tool,
@@ -24,6 +25,7 @@ from dao_ai.tools.vector_search import create_vector_search_tool
24
25
  __all__ = [
25
26
  "add_time_tool",
26
27
  "create_agent_endpoint_tool",
28
+ "create_execute_statement_tool",
27
29
  "create_factory_tool",
28
30
  "create_genie_tool",
29
31
  "create_hooks",
dao_ai/tools/agent.py CHANGED
@@ -14,9 +14,7 @@ def create_agent_endpoint_tool(
14
14
  name: Optional[str] = None,
15
15
  description: Optional[str] = None,
16
16
  ) -> Callable[..., Any]:
17
- logger.debug(
18
- f"Creating agent endpoint tool with name: {name} and description: {description}"
19
- )
17
+ logger.debug("Creating agent endpoint tool", name=name, description=description)
20
18
 
21
19
  default_description: str = dedent("""
22
20
  This tool allows you to interact with a language model endpoint to answer questions.
dao_ai/tools/core.py CHANGED
@@ -36,7 +36,7 @@ def create_tools(tool_models: Sequence[ToolModel]) -> Sequence[RunnableLike]:
36
36
  Each tool is created according to its type and parameters defined in the configuration.
37
37
 
38
38
  Args:
39
- tool_configs: A sequence of dictionaries containing tool configurations
39
+ tool_models: A sequence of ToolModel configurations
40
40
 
41
41
  Returns:
42
42
  A sequence of BaseTool objects created from the provided configurations
@@ -47,24 +47,24 @@ def create_tools(tool_models: Sequence[ToolModel]) -> Sequence[RunnableLike]:
47
47
  for tool_config in tool_models:
48
48
  name: str = tool_config.name
49
49
  if name in tools:
50
- logger.warning(f"Tools already registered for: {name}, skipping creation.")
50
+ logger.warning("Tools already registered, skipping", tool_name=name)
51
51
  continue
52
52
  registered_tools: Sequence[RunnableLike] | None = tool_registry.get(name)
53
53
  if registered_tools is None:
54
- logger.debug(f"Creating tools for: {name}...")
54
+ logger.trace("Creating tools", tool_name=name)
55
55
  function: AnyTool = tool_config.function
56
56
  registered_tools = create_hooks(function)
57
- logger.debug(f"Registering tools for: {tool_config}")
57
+ logger.trace("Registering tools", tool_name=name)
58
58
  tool_registry[name] = registered_tools
59
59
  else:
60
- logger.debug(f"Tools already registered for: {name}")
60
+ logger.trace("Tools already registered", tool_name=name)
61
61
 
62
62
  tools[name] = registered_tools
63
63
 
64
64
  all_tools: Sequence[RunnableLike] = [
65
65
  t for tool_list in tools.values() for t in tool_list
66
66
  ]
67
- logger.debug(f"Created tools: {all_tools}")
67
+ logger.debug("Tools created", tools_count=len(all_tools))
68
68
  return all_tools
69
69
 
70
70
 
@@ -101,7 +101,7 @@ def say_hello_tool(
101
101
  # Use provided name, or fall back to user_id from context
102
102
  if name is None:
103
103
  if runtime and runtime.context:
104
- user_id = runtime.context.user_id
104
+ user_id: str | None = runtime.context.user_id
105
105
  if user_id:
106
106
  name = user_id
107
107
  else:
dao_ai/tools/email.py CHANGED
@@ -107,70 +107,41 @@ def create_send_email_tool(
107
107
  key: smtp_password
108
108
  ```
109
109
  """
110
- logger.info("=== Creating send_email_tool ===")
111
110
  logger.debug(
112
- f"Factory called with config type: {type(smtp_config).__name__}, "
113
- f"name={name}, description={description}"
111
+ "Creating send_email_tool",
112
+ config_type=type(smtp_config).__name__,
113
+ tool_name=name,
114
114
  )
115
115
 
116
116
  # Convert dict to SMTPConfigModel if needed
117
117
  if isinstance(smtp_config, dict):
118
- logger.debug("Converting dict config to SMTPConfigModel")
119
118
  smtp_config = SMTPConfigModel(**smtp_config)
120
- else:
121
- logger.debug("Config already is SMTPConfigModel")
122
119
 
123
120
  # Resolve all variable values
124
- logger.debug("Resolving SMTP configuration variables...")
125
-
126
- logger.debug(" - Resolving host")
127
121
  host: str = value_of(smtp_config.host)
128
- logger.debug(f" Host resolved: {host}")
129
-
130
- logger.debug(" - Resolving port")
131
122
  port: int = int(value_of(smtp_config.port))
132
- logger.debug(f" Port resolved: {port}")
133
-
134
- logger.debug(" - Resolving username")
135
123
  username: str = value_of(smtp_config.username)
136
- logger.debug(f" Username resolved: {username}")
137
-
138
- logger.debug(" - Resolving password")
139
124
  password: str = value_of(smtp_config.password)
140
- logger.debug(
141
- f" Password resolved: {'*' * len(password) if password else 'None'}"
142
- )
143
-
144
- logger.debug(" - Resolving sender_email")
145
125
  sender_email: str = (
146
126
  value_of(smtp_config.sender_email) if smtp_config.sender_email else username
147
127
  )
148
- logger.debug(
149
- f" Sender email resolved: {sender_email} "
150
- f"({'from sender_email' if smtp_config.sender_email else 'defaulted to username'})"
151
- )
152
-
153
128
  use_tls: bool = smtp_config.use_tls
154
- logger.debug(f" - TLS enabled: {use_tls}")
155
129
 
156
130
  logger.info(
157
- f"SMTP configuration resolved - host={host}, port={port}, "
158
- f"sender={sender_email}, use_tls={use_tls}"
131
+ "SMTP configuration resolved",
132
+ host=host,
133
+ port=port,
134
+ sender=sender_email,
135
+ use_tls=use_tls,
136
+ password_set=bool(password),
159
137
  )
160
138
 
161
139
  if name is None:
162
140
  name = "send_email"
163
- logger.debug(f"Tool name defaulted to: {name}")
164
- else:
165
- logger.debug(f"Tool name set to: {name}")
166
-
167
141
  if description is None:
168
142
  description = "Send an email to a recipient with subject and body content"
169
- logger.debug("Tool description using default")
170
- else:
171
- logger.debug(f"Tool description set to: {description}")
172
143
 
173
- logger.info(f"Creating tool '{name}' with @tool decorator")
144
+ logger.debug("Creating email tool with decorator", tool_name=name)
174
145
 
175
146
  @tool(
176
147
  name_or_callable=name,
@@ -194,87 +165,68 @@ def create_send_email_tool(
194
165
  Returns:
195
166
  str: Success or error message
196
167
  """
197
- logger.info("=== send_email tool invoked ===")
198
- logger.info(f" To: {to}")
199
- logger.info(f" Subject: {subject}")
200
- logger.info(f" Body length: {len(body)} characters")
201
- logger.info(f" CC: {cc if cc else 'None'}")
168
+ logger.info(
169
+ "Sending email", to=to, subject=subject, body_length=len(body), cc=cc
170
+ )
202
171
 
203
172
  try:
204
- logger.debug("Constructing email message...")
205
-
206
173
  # Create message
207
174
  msg = MIMEMultipart()
208
175
  msg["From"] = sender_email
209
176
  msg["To"] = to
210
177
  msg["Subject"] = subject
211
- logger.debug(f" From: {sender_email}")
212
- logger.debug(f" To: {to}")
213
- logger.debug(f" Subject: {subject}")
214
178
 
215
179
  if cc:
216
180
  msg["Cc"] = cc
217
- logger.debug(f" CC: {cc}")
218
181
 
219
182
  # Attach body as plain text
220
183
  msg.attach(MIMEText(body, "plain"))
221
- logger.debug(f" Body attached ({len(body)} chars)")
222
184
 
223
185
  # Send email
224
- logger.info(f"Connecting to SMTP server {host}:{port}...")
186
+ logger.debug("Connecting to SMTP server", host=host, port=port)
225
187
  with smtplib.SMTP(host, port) as server:
226
- logger.debug("SMTP connection established")
227
-
228
188
  if use_tls:
229
- logger.debug("Upgrading connection to TLS...")
189
+ logger.trace("Upgrading to TLS")
230
190
  server.starttls()
231
- logger.debug("TLS upgrade successful")
232
191
 
233
- logger.debug(f"Authenticating with username: {username}")
192
+ logger.trace("Authenticating", username=username)
234
193
  server.login(username, password)
235
- logger.info("SMTP authentication successful")
236
194
 
237
195
  # Build recipient list
238
196
  recipients = [to]
239
197
  if cc:
240
198
  cc_addresses = [addr.strip() for addr in cc.split(",")]
241
199
  recipients.extend(cc_addresses)
242
- logger.debug(f"Total recipients: {len(recipients)} ({recipients})")
243
- else:
244
- logger.debug(f"Single recipient: {to}")
245
200
 
246
- logger.info(f"Sending message to {len(recipients)} recipient(s)...")
201
+ logger.debug("Sending message", recipients_count=len(recipients))
247
202
  server.send_message(msg)
248
- logger.info("Message sent successfully via SMTP")
249
203
 
250
204
  success_msg = f"✓ Email sent successfully to {to}"
251
205
  if cc:
252
206
  success_msg += f" (cc: {cc})"
253
207
 
254
- logger.info(success_msg)
255
- logger.info("=== send_email completed successfully ===")
208
+ logger.success("Email sent successfully", to=to, cc=cc)
256
209
  return success_msg
257
210
 
258
211
  except smtplib.SMTPAuthenticationError as e:
259
212
  error_msg = f"✗ SMTP authentication failed: {str(e)}"
260
- logger.error(error_msg)
261
- logger.error(f" Server: {host}:{port}")
262
- logger.error(f" Username: {username}")
263
- logger.error("=== send_email failed (authentication) ===")
213
+ logger.error(
214
+ "SMTP authentication failed",
215
+ server=f"{host}:{port}",
216
+ username=username,
217
+ error=str(e),
218
+ )
264
219
  return error_msg
265
220
  except smtplib.SMTPException as e:
266
221
  error_msg = f"✗ SMTP error: {str(e)}"
267
- logger.error(error_msg)
268
- logger.error(f" Server: {host}:{port}")
269
- logger.error("=== send_email failed (SMTP error) ===")
222
+ logger.error("SMTP error", server=f"{host}:{port}", error=str(e))
270
223
  return error_msg
271
224
  except Exception as e:
272
225
  error_msg = f"✗ Failed to send email: {str(e)}"
273
- logger.error(error_msg)
274
- logger.error(f" Error type: {type(e).__name__}")
275
- logger.error("=== send_email failed (unexpected error) ===")
226
+ logger.error(
227
+ "Failed to send email", error_type=type(e).__name__, error=str(e)
228
+ )
276
229
  return error_msg
277
230
 
278
- logger.info(f"Tool '{name}' created successfully")
279
- logger.info("=== send_email_tool creation complete ===")
231
+ logger.success("Email tool created", tool_name=name)
280
232
  return send_email
dao_ai/tools/genie.py CHANGED
@@ -89,15 +89,15 @@ def create_genie_tool(
89
89
  Returns:
90
90
  A LangGraph tool that processes natural language queries through Genie
91
91
  """
92
- logger.debug("create_genie_tool")
93
- logger.debug(f"genie_room type: {type(genie_room)}")
94
- logger.debug(f"genie_room: {genie_room}")
95
- logger.debug(f"persist_conversation: {persist_conversation}")
96
- logger.debug(f"truncate_results: {truncate_results}")
97
- logger.debug(f"name: {name}")
98
- logger.debug(f"description: {description}")
99
- logger.debug(f"lru_cache_parameters: {lru_cache_parameters}")
100
- logger.debug(f"semantic_cache_parameters: {semantic_cache_parameters}")
92
+ logger.debug(
93
+ "Creating Genie tool",
94
+ genie_room_type=type(genie_room).__name__,
95
+ persist_conversation=persist_conversation,
96
+ truncate_results=truncate_results,
97
+ name=name,
98
+ has_lru_cache=lru_cache_parameters is not None,
99
+ has_semantic_cache=semantic_cache_parameters is not None,
100
+ )
101
101
 
102
102
  if isinstance(genie_room, dict):
103
103
  genie_room = GenieRoomModel(**genie_room)
@@ -188,8 +188,10 @@ GenieResponse: A response object containing the conversation ID and result from
188
188
  existing_conversation_id: str | None = session.genie.get_conversation_id(
189
189
  space_id_str
190
190
  )
191
- logger.debug(
192
- f"Existing conversation ID for space {space_id_str}: {existing_conversation_id}"
191
+ logger.trace(
192
+ "Using existing conversation ID",
193
+ space_id=space_id_str,
194
+ conversation_id=existing_conversation_id,
193
195
  )
194
196
 
195
197
  # Call ask_question which always returns CacheResult with cache metadata
@@ -202,8 +204,11 @@ GenieResponse: A response object containing the conversation ID and result from
202
204
 
203
205
  current_conversation_id: str = genie_response.conversation_id
204
206
  logger.debug(
205
- f"Current conversation ID for space {space_id_str}: {current_conversation_id}, "
206
- f"cache_hit: {cache_hit}, cache_key: {cache_key}"
207
+ "Genie question answered",
208
+ space_id=space_id_str,
209
+ conversation_id=current_conversation_id,
210
+ cache_hit=cache_hit,
211
+ cache_key=cache_key,
207
212
  )
208
213
 
209
214
  # Update session state with cache information