dao-ai 0.0.25__py3-none-any.whl → 0.1.2__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.
- dao_ai/__init__.py +29 -0
- dao_ai/agent_as_code.py +5 -5
- dao_ai/cli.py +245 -40
- dao_ai/config.py +1863 -338
- dao_ai/genie/__init__.py +38 -0
- dao_ai/genie/cache/__init__.py +43 -0
- dao_ai/genie/cache/base.py +72 -0
- dao_ai/genie/cache/core.py +79 -0
- dao_ai/genie/cache/lru.py +347 -0
- dao_ai/genie/cache/semantic.py +970 -0
- dao_ai/genie/core.py +35 -0
- dao_ai/graph.py +27 -228
- dao_ai/hooks/__init__.py +9 -6
- dao_ai/hooks/core.py +27 -195
- dao_ai/logging.py +56 -0
- dao_ai/memory/__init__.py +10 -0
- dao_ai/memory/core.py +65 -30
- dao_ai/memory/databricks.py +402 -0
- dao_ai/memory/postgres.py +79 -38
- dao_ai/messages.py +6 -4
- dao_ai/middleware/__init__.py +125 -0
- dao_ai/middleware/assertions.py +806 -0
- dao_ai/middleware/base.py +50 -0
- dao_ai/middleware/core.py +67 -0
- dao_ai/middleware/guardrails.py +420 -0
- dao_ai/middleware/human_in_the_loop.py +232 -0
- dao_ai/middleware/message_validation.py +586 -0
- dao_ai/middleware/summarization.py +197 -0
- dao_ai/models.py +1306 -114
- dao_ai/nodes.py +261 -166
- dao_ai/optimization.py +674 -0
- dao_ai/orchestration/__init__.py +52 -0
- dao_ai/orchestration/core.py +294 -0
- dao_ai/orchestration/supervisor.py +278 -0
- dao_ai/orchestration/swarm.py +271 -0
- dao_ai/prompts.py +128 -31
- dao_ai/providers/databricks.py +645 -172
- dao_ai/state.py +157 -21
- dao_ai/tools/__init__.py +13 -5
- dao_ai/tools/agent.py +1 -3
- dao_ai/tools/core.py +64 -11
- dao_ai/tools/email.py +232 -0
- dao_ai/tools/genie.py +144 -295
- dao_ai/tools/mcp.py +220 -133
- dao_ai/tools/memory.py +50 -0
- dao_ai/tools/python.py +9 -14
- dao_ai/tools/search.py +14 -0
- dao_ai/tools/slack.py +22 -10
- dao_ai/tools/sql.py +202 -0
- dao_ai/tools/time.py +30 -7
- dao_ai/tools/unity_catalog.py +165 -88
- dao_ai/tools/vector_search.py +360 -40
- dao_ai/utils.py +218 -16
- dao_ai-0.1.2.dist-info/METADATA +455 -0
- dao_ai-0.1.2.dist-info/RECORD +64 -0
- {dao_ai-0.0.25.dist-info → dao_ai-0.1.2.dist-info}/WHEEL +1 -1
- dao_ai/chat_models.py +0 -204
- dao_ai/guardrails.py +0 -112
- dao_ai/tools/human_in_the_loop.py +0 -100
- dao_ai-0.0.25.dist-info/METADATA +0 -1165
- dao_ai-0.0.25.dist-info/RECORD +0 -41
- {dao_ai-0.0.25.dist-info → dao_ai-0.1.2.dist-info}/entry_points.txt +0 -0
- {dao_ai-0.0.25.dist-info → dao_ai-0.1.2.dist-info}/licenses/LICENSE +0 -0
dao_ai/state.py
CHANGED
|
@@ -1,41 +1,177 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
State definitions for DAO AI agents.
|
|
3
|
+
|
|
4
|
+
This module defines the state schemas used by DAO AI agents,
|
|
5
|
+
compatible with both LangChain v1's create_agent and LangGraph's StateGraph.
|
|
6
|
+
|
|
7
|
+
State Schema:
|
|
8
|
+
- AgentState: Primary state schema for all agent operations
|
|
9
|
+
- Context: Runtime context passed via ToolRuntime[Context] or Runtime[Context]
|
|
10
|
+
- GenieSpaceState: Per-space state for Genie conversations
|
|
11
|
+
- SessionState: Accumulated state that flows between requests
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
2
17
|
from langgraph.graph import MessagesState
|
|
3
|
-
from
|
|
4
|
-
from
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
19
|
+
from typing_extensions import NotRequired
|
|
5
20
|
|
|
6
21
|
|
|
7
|
-
class
|
|
22
|
+
class GenieSpaceState(BaseModel):
|
|
23
|
+
"""State for a single Genie space/conversation.
|
|
8
24
|
|
|
25
|
+
This tracks the conversation state and metadata for a Genie space,
|
|
26
|
+
allowing multi-turn conversations and caching information to be preserved.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
conversation_id: str = Field(description="Genie conversation ID for this space")
|
|
30
|
+
cache_hit: bool = Field(
|
|
31
|
+
default=False, description="Whether the last query was a cache hit"
|
|
32
|
+
)
|
|
33
|
+
cache_key: Optional[str] = Field(default=None, description="Cache key if cached")
|
|
34
|
+
follow_up_questions: list[str] = Field(
|
|
35
|
+
default_factory=list, description="Suggested follow-up questions from Genie"
|
|
36
|
+
)
|
|
37
|
+
last_query: Optional[str] = Field(
|
|
38
|
+
default=None, description="The last query sent to Genie"
|
|
39
|
+
)
|
|
40
|
+
last_query_time: Optional[datetime] = Field(
|
|
41
|
+
default=None, description="When the last query was made"
|
|
42
|
+
)
|
|
9
43
|
|
|
10
|
-
class OutgoingState(MessagesState):
|
|
11
|
-
is_valid: bool
|
|
12
|
-
message_error: str
|
|
13
44
|
|
|
45
|
+
class GenieState(BaseModel):
|
|
46
|
+
"""State for all Genie spaces.
|
|
14
47
|
|
|
15
|
-
|
|
48
|
+
Maps space_id to GenieSpaceState for each Genie space the user has interacted with.
|
|
16
49
|
"""
|
|
17
|
-
State representation for the DAO AI agent conversation workflow.
|
|
18
50
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
51
|
+
spaces: dict[str, GenieSpaceState] = Field(
|
|
52
|
+
default_factory=dict, description="Map of space_id to space state"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def get_conversation_id(self, space_id: str) -> Optional[str]:
|
|
56
|
+
"""Get conversation ID for a space, if it exists."""
|
|
57
|
+
if space_id in self.spaces:
|
|
58
|
+
return self.spaces[space_id].conversation_id
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def update_space(
|
|
62
|
+
self,
|
|
63
|
+
space_id: str,
|
|
64
|
+
conversation_id: str,
|
|
65
|
+
cache_hit: bool = False,
|
|
66
|
+
cache_key: Optional[str] = None,
|
|
67
|
+
follow_up_questions: Optional[list[str]] = None,
|
|
68
|
+
last_query: Optional[str] = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Update or create state for a Genie space."""
|
|
71
|
+
self.spaces[space_id] = GenieSpaceState(
|
|
72
|
+
conversation_id=conversation_id,
|
|
73
|
+
cache_hit=cache_hit,
|
|
74
|
+
cache_key=cache_key,
|
|
75
|
+
follow_up_questions=follow_up_questions or [],
|
|
76
|
+
last_query=last_query,
|
|
77
|
+
last_query_time=datetime.now() if last_query else None,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class SessionState(BaseModel):
|
|
82
|
+
"""Accumulated state that flows between requests.
|
|
83
|
+
|
|
84
|
+
This is the "paste from previous output" portion of the request.
|
|
85
|
+
Users can copy the session from custom_outputs and paste it back
|
|
86
|
+
as custom_inputs.session to restore state.
|
|
22
87
|
"""
|
|
23
88
|
|
|
24
|
-
|
|
89
|
+
genie: GenieState = Field(
|
|
90
|
+
default_factory=GenieState, description="Genie conversation state per space"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Future: Add other stateful tool state here
|
|
94
|
+
# other_tool_state: OtherToolState = Field(default_factory=OtherToolState)
|
|
95
|
+
|
|
25
96
|
|
|
26
|
-
|
|
27
|
-
|
|
97
|
+
class AgentState(MessagesState, total=False):
|
|
98
|
+
"""
|
|
99
|
+
Primary state schema for DAO AI agents.
|
|
100
|
+
|
|
101
|
+
Extends MessagesState to include the messages channel with proper
|
|
102
|
+
add_messages reducer, plus additional fields for DAO AI functionality.
|
|
28
103
|
|
|
29
|
-
|
|
104
|
+
Used for:
|
|
105
|
+
- state_schema in create_agent calls
|
|
106
|
+
- state_schema in StateGraph for orchestration
|
|
107
|
+
- Type parameter in ToolRuntime[Context, AgentState]
|
|
108
|
+
- Type parameter in AgentMiddleware[AgentState, Context]
|
|
109
|
+
- API input/output contracts
|
|
30
110
|
|
|
31
|
-
|
|
32
|
-
|
|
111
|
+
Fields:
|
|
112
|
+
messages: Conversation history with add_messages reducer (from MessagesState)
|
|
113
|
+
context: Short/long term memory context
|
|
114
|
+
active_agent: Name of currently active agent in multi-agent workflows
|
|
115
|
+
is_valid: Message validation status
|
|
116
|
+
message_error: Error message if validation failed
|
|
117
|
+
session: Accumulated session state (genie conversations, etc.)
|
|
118
|
+
structured_response: Structured output from response_format (populated by LangChain)
|
|
119
|
+
"""
|
|
33
120
|
|
|
34
|
-
|
|
35
|
-
|
|
121
|
+
context: NotRequired[str]
|
|
122
|
+
active_agent: NotRequired[str]
|
|
123
|
+
is_valid: NotRequired[bool]
|
|
124
|
+
message_error: NotRequired[str]
|
|
125
|
+
session: NotRequired[SessionState]
|
|
126
|
+
structured_response: NotRequired[Any]
|
|
36
127
|
|
|
37
128
|
|
|
38
129
|
class Context(BaseModel):
|
|
130
|
+
"""
|
|
131
|
+
Runtime context for DAO AI agents.
|
|
132
|
+
|
|
133
|
+
This is passed to tools and middleware via the runtime parameter.
|
|
134
|
+
Access via ToolRuntime[Context] in tools or Runtime[Context] in middleware.
|
|
135
|
+
|
|
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)
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
@tool
|
|
145
|
+
def my_tool(runtime: ToolRuntime[Context]) -> str:
|
|
146
|
+
user_id = runtime.context.user_id
|
|
147
|
+
store_num = runtime.context.store_num # Direct attribute access
|
|
148
|
+
return f"Hello, {user_id} at store {store_num}!"
|
|
149
|
+
|
|
150
|
+
class MyMiddleware(AgentMiddleware[AgentState, Context]):
|
|
151
|
+
def before_model(
|
|
152
|
+
self,
|
|
153
|
+
state: AgentState,
|
|
154
|
+
runtime: Runtime[Context]
|
|
155
|
+
) -> dict[str, Any] | None:
|
|
156
|
+
user_id = runtime.context.user_id
|
|
157
|
+
store_num = getattr(runtime.context, "store_num", None)
|
|
158
|
+
return None
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
model_config = ConfigDict(
|
|
162
|
+
extra="allow"
|
|
163
|
+
) # Allow extra fields as top-level attributes
|
|
164
|
+
|
|
39
165
|
user_id: str | None = None
|
|
40
166
|
thread_id: str | None = None
|
|
41
|
-
|
|
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
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
from dao_ai.genie.cache import LRUCacheService, SemanticCacheService
|
|
1
2
|
from dao_ai.hooks.core import create_hooks
|
|
2
3
|
from dao_ai.tools.agent import create_agent_endpoint_tool
|
|
3
|
-
from dao_ai.tools.core import
|
|
4
|
-
|
|
5
|
-
search_tool,
|
|
6
|
-
)
|
|
4
|
+
from dao_ai.tools.core import create_tools, say_hello_tool
|
|
5
|
+
from dao_ai.tools.email import create_send_email_tool
|
|
7
6
|
from dao_ai.tools.genie import create_genie_tool
|
|
8
7
|
from dao_ai.tools.mcp import create_mcp_tools
|
|
8
|
+
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
|
+
from dao_ai.tools.search import create_search_tool
|
|
10
11
|
from dao_ai.tools.slack import create_send_slack_message_tool
|
|
12
|
+
from dao_ai.tools.sql import create_execute_statement_tool
|
|
11
13
|
from dao_ai.tools.time import (
|
|
12
14
|
add_time_tool,
|
|
13
15
|
current_time_tool,
|
|
@@ -23,11 +25,15 @@ from dao_ai.tools.vector_search import create_vector_search_tool
|
|
|
23
25
|
__all__ = [
|
|
24
26
|
"add_time_tool",
|
|
25
27
|
"create_agent_endpoint_tool",
|
|
28
|
+
"create_execute_statement_tool",
|
|
26
29
|
"create_factory_tool",
|
|
27
30
|
"create_genie_tool",
|
|
28
31
|
"create_hooks",
|
|
29
32
|
"create_mcp_tools",
|
|
30
33
|
"create_python_tool",
|
|
34
|
+
"create_search_memory_tool",
|
|
35
|
+
"create_search_tool",
|
|
36
|
+
"create_send_email_tool",
|
|
31
37
|
"create_send_slack_message_tool",
|
|
32
38
|
"create_tools",
|
|
33
39
|
"create_uc_tools",
|
|
@@ -35,7 +41,9 @@ __all__ = [
|
|
|
35
41
|
"current_time_tool",
|
|
36
42
|
"format_time_tool",
|
|
37
43
|
"is_business_hours_tool",
|
|
38
|
-
"
|
|
44
|
+
"LRUCacheService",
|
|
45
|
+
"say_hello_tool",
|
|
46
|
+
"SemanticCacheService",
|
|
39
47
|
"time_difference_tool",
|
|
40
48
|
"time_in_timezone_tool",
|
|
41
49
|
"time_until_tool",
|
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
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core tool creation infrastructure for DAO AI.
|
|
3
|
+
|
|
4
|
+
This module provides the foundational tool creation and registration system:
|
|
5
|
+
- Tool registry for caching created tools
|
|
6
|
+
- Factory function for creating tools from configuration
|
|
7
|
+
- Example tools demonstrating runtime context usage
|
|
8
|
+
|
|
9
|
+
This is "core" because it contains the essential infrastructure that all
|
|
10
|
+
tool creation flows through, not because it contains all tools.
|
|
11
|
+
"""
|
|
12
|
+
|
|
1
13
|
from collections import OrderedDict
|
|
2
14
|
from typing import Sequence
|
|
3
15
|
|
|
4
|
-
from
|
|
16
|
+
from langchain.tools import ToolRuntime, tool
|
|
5
17
|
from langchain_core.runnables.base import RunnableLike
|
|
6
18
|
from loguru import logger
|
|
7
19
|
|
|
@@ -10,7 +22,9 @@ from dao_ai.config import (
|
|
|
10
22
|
ToolModel,
|
|
11
23
|
)
|
|
12
24
|
from dao_ai.hooks.core import create_hooks
|
|
25
|
+
from dao_ai.state import Context
|
|
13
26
|
|
|
27
|
+
# Module-level tool registry for caching created tools
|
|
14
28
|
tool_registry: dict[str, Sequence[RunnableLike]] = {}
|
|
15
29
|
|
|
16
30
|
|
|
@@ -22,7 +36,7 @@ def create_tools(tool_models: Sequence[ToolModel]) -> Sequence[RunnableLike]:
|
|
|
22
36
|
Each tool is created according to its type and parameters defined in the configuration.
|
|
23
37
|
|
|
24
38
|
Args:
|
|
25
|
-
|
|
39
|
+
tool_models: A sequence of ToolModel configurations
|
|
26
40
|
|
|
27
41
|
Returns:
|
|
28
42
|
A sequence of BaseTool objects created from the provided configurations
|
|
@@ -33,27 +47,66 @@ def create_tools(tool_models: Sequence[ToolModel]) -> Sequence[RunnableLike]:
|
|
|
33
47
|
for tool_config in tool_models:
|
|
34
48
|
name: str = tool_config.name
|
|
35
49
|
if name in tools:
|
|
36
|
-
logger.warning(
|
|
50
|
+
logger.warning("Tools already registered, skipping", tool_name=name)
|
|
37
51
|
continue
|
|
38
|
-
registered_tools: Sequence[RunnableLike] = tool_registry.get(name)
|
|
52
|
+
registered_tools: Sequence[RunnableLike] | None = tool_registry.get(name)
|
|
39
53
|
if registered_tools is None:
|
|
40
|
-
logger.
|
|
54
|
+
logger.trace("Creating tools", tool_name=name)
|
|
41
55
|
function: AnyTool = tool_config.function
|
|
42
56
|
registered_tools = create_hooks(function)
|
|
43
|
-
logger.
|
|
57
|
+
logger.trace("Registering tools", tool_name=name)
|
|
44
58
|
tool_registry[name] = registered_tools
|
|
45
59
|
else:
|
|
46
|
-
logger.
|
|
60
|
+
logger.trace("Tools already registered", tool_name=name)
|
|
47
61
|
|
|
48
62
|
tools[name] = registered_tools
|
|
49
63
|
|
|
50
64
|
all_tools: Sequence[RunnableLike] = [
|
|
51
65
|
t for tool_list in tools.values() for t in tool_list
|
|
52
66
|
]
|
|
53
|
-
logger.debug(
|
|
67
|
+
logger.debug("Tools created", tools_count=len(all_tools))
|
|
54
68
|
return all_tools
|
|
55
69
|
|
|
56
70
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
# =============================================================================
|
|
72
|
+
# Example Tools
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# The following tools serve as examples and are included here because they
|
|
75
|
+
# demonstrate core patterns (like ToolRuntime usage) rather than because they
|
|
76
|
+
# are fundamental infrastructure. They're simple enough to colocate with the
|
|
77
|
+
# core tool creation logic.
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@tool
|
|
81
|
+
def say_hello_tool(
|
|
82
|
+
name: str | None = None,
|
|
83
|
+
runtime: ToolRuntime[Context] = None,
|
|
84
|
+
) -> str:
|
|
85
|
+
"""
|
|
86
|
+
Say hello to someone by name.
|
|
87
|
+
|
|
88
|
+
This is an example tool demonstrating how to use ToolRuntime to access
|
|
89
|
+
runtime context (like user_id) within a tool.
|
|
90
|
+
|
|
91
|
+
If no name is provided, uses the user_id from the runtime context.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
name: Optional name of the person to greet. If not provided,
|
|
95
|
+
uses user_id from context.
|
|
96
|
+
runtime: Runtime context (automatically injected, not provided by user)
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
A greeting string
|
|
100
|
+
"""
|
|
101
|
+
# Use provided name, or fall back to user_id from context
|
|
102
|
+
if name is None:
|
|
103
|
+
if runtime and runtime.context:
|
|
104
|
+
user_id: str | None = runtime.context.user_id
|
|
105
|
+
if user_id:
|
|
106
|
+
name = user_id
|
|
107
|
+
else:
|
|
108
|
+
name = "there" # Default fallback
|
|
109
|
+
else:
|
|
110
|
+
name = "there" # Default fallback
|
|
111
|
+
|
|
112
|
+
return f"Hello, {name}!"
|
dao_ai/tools/email.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Email tool for sending emails via SMTP."""
|
|
2
|
+
|
|
3
|
+
import smtplib
|
|
4
|
+
from email.mime.multipart import MIMEMultipart
|
|
5
|
+
from email.mime.text import MIMEText
|
|
6
|
+
from typing import Any, Callable, Optional
|
|
7
|
+
|
|
8
|
+
from langchain_core.tools import tool
|
|
9
|
+
from loguru import logger
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
11
|
+
|
|
12
|
+
from dao_ai.config import AnyVariable, value_of
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SMTPConfigModel(BaseModel):
|
|
16
|
+
"""Configuration model for SMTP email settings."""
|
|
17
|
+
|
|
18
|
+
model_config = ConfigDict(use_enum_values=True, extra="forbid")
|
|
19
|
+
|
|
20
|
+
host: AnyVariable = Field(
|
|
21
|
+
default="smtp.gmail.com",
|
|
22
|
+
description="SMTP server hostname",
|
|
23
|
+
)
|
|
24
|
+
port: AnyVariable = Field(
|
|
25
|
+
default=587,
|
|
26
|
+
description="SMTP server port",
|
|
27
|
+
)
|
|
28
|
+
username: AnyVariable = Field(
|
|
29
|
+
description="SMTP username for authentication",
|
|
30
|
+
)
|
|
31
|
+
password: AnyVariable = Field(
|
|
32
|
+
description="SMTP password for authentication",
|
|
33
|
+
)
|
|
34
|
+
sender_email: Optional[AnyVariable] = Field(
|
|
35
|
+
default=None,
|
|
36
|
+
description="Email address to use as sender (defaults to username)",
|
|
37
|
+
)
|
|
38
|
+
use_tls: bool = Field(
|
|
39
|
+
default=True,
|
|
40
|
+
description="Whether to use TLS encryption",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def create_send_email_tool(
|
|
45
|
+
smtp_config: SMTPConfigModel | dict[str, Any],
|
|
46
|
+
name: Optional[str] = None,
|
|
47
|
+
description: Optional[str] = None,
|
|
48
|
+
) -> Callable[[str, str, str, Optional[str]], str]:
|
|
49
|
+
"""
|
|
50
|
+
Create a tool that sends emails via SMTP.
|
|
51
|
+
|
|
52
|
+
This factory function creates a tool for sending emails with configurable SMTP settings.
|
|
53
|
+
All configuration values support AnyVariable types, allowing use of environment variables,
|
|
54
|
+
secrets, and composite variables.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
smtp_config: SMTP configuration (SMTPConfigModel or dict). Supports:
|
|
58
|
+
- host: SMTP server hostname (supports variables/secrets)
|
|
59
|
+
- port: SMTP server port (supports variables/secrets)
|
|
60
|
+
- username: SMTP username (supports variables/secrets)
|
|
61
|
+
- password: SMTP password (supports variables/secrets)
|
|
62
|
+
- sender_email: Sender email address, defaults to username (supports variables/secrets)
|
|
63
|
+
- use_tls: Whether to use TLS encryption (default: True)
|
|
64
|
+
name: Custom tool name (default: 'send_email')
|
|
65
|
+
description: Custom tool description
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
A tool function that sends emails via SMTP
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
Basic usage with environment variables:
|
|
72
|
+
```yaml
|
|
73
|
+
tools:
|
|
74
|
+
send_email:
|
|
75
|
+
name: send_email
|
|
76
|
+
function:
|
|
77
|
+
type: factory
|
|
78
|
+
name: dao_ai.tools.email.create_send_email_tool
|
|
79
|
+
args:
|
|
80
|
+
smtp_config:
|
|
81
|
+
host: smtp.gmail.com
|
|
82
|
+
port: 587
|
|
83
|
+
username: ${SMTP_USER}
|
|
84
|
+
password: ${SMTP_PASSWORD}
|
|
85
|
+
sender_email: bot@example.com
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
With secrets:
|
|
89
|
+
```yaml
|
|
90
|
+
tools:
|
|
91
|
+
send_email:
|
|
92
|
+
name: send_email
|
|
93
|
+
function:
|
|
94
|
+
type: factory
|
|
95
|
+
name: dao_ai.tools.email.create_send_email_tool
|
|
96
|
+
args:
|
|
97
|
+
smtp_config:
|
|
98
|
+
host: smtp.gmail.com
|
|
99
|
+
port: 587
|
|
100
|
+
username:
|
|
101
|
+
type: secret
|
|
102
|
+
scope: email
|
|
103
|
+
key: smtp_user
|
|
104
|
+
password:
|
|
105
|
+
type: secret
|
|
106
|
+
scope: email
|
|
107
|
+
key: smtp_password
|
|
108
|
+
```
|
|
109
|
+
"""
|
|
110
|
+
logger.debug(
|
|
111
|
+
"Creating send_email_tool",
|
|
112
|
+
config_type=type(smtp_config).__name__,
|
|
113
|
+
tool_name=name,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Convert dict to SMTPConfigModel if needed
|
|
117
|
+
if isinstance(smtp_config, dict):
|
|
118
|
+
smtp_config = SMTPConfigModel(**smtp_config)
|
|
119
|
+
|
|
120
|
+
# Resolve all variable values
|
|
121
|
+
host: str = value_of(smtp_config.host)
|
|
122
|
+
port: int = int(value_of(smtp_config.port))
|
|
123
|
+
username: str = value_of(smtp_config.username)
|
|
124
|
+
password: str = value_of(smtp_config.password)
|
|
125
|
+
sender_email: str = (
|
|
126
|
+
value_of(smtp_config.sender_email) if smtp_config.sender_email else username
|
|
127
|
+
)
|
|
128
|
+
use_tls: bool = smtp_config.use_tls
|
|
129
|
+
|
|
130
|
+
logger.info(
|
|
131
|
+
"SMTP configuration resolved",
|
|
132
|
+
host=host,
|
|
133
|
+
port=port,
|
|
134
|
+
sender=sender_email,
|
|
135
|
+
use_tls=use_tls,
|
|
136
|
+
password_set=bool(password),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if name is None:
|
|
140
|
+
name = "send_email"
|
|
141
|
+
if description is None:
|
|
142
|
+
description = "Send an email to a recipient with subject and body content"
|
|
143
|
+
|
|
144
|
+
logger.debug("Creating email tool with decorator", tool_name=name)
|
|
145
|
+
|
|
146
|
+
@tool(
|
|
147
|
+
name_or_callable=name,
|
|
148
|
+
description=description,
|
|
149
|
+
)
|
|
150
|
+
def send_email(
|
|
151
|
+
to: str,
|
|
152
|
+
subject: str,
|
|
153
|
+
body: str,
|
|
154
|
+
cc: Optional[str] = None,
|
|
155
|
+
) -> str:
|
|
156
|
+
"""
|
|
157
|
+
Send an email via SMTP.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
to: Recipient email address
|
|
161
|
+
subject: Email subject line
|
|
162
|
+
body: Email body content (plain text)
|
|
163
|
+
cc: Optional CC recipients (comma-separated email addresses)
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
str: Success or error message
|
|
167
|
+
"""
|
|
168
|
+
logger.info(
|
|
169
|
+
"Sending email", to=to, subject=subject, body_length=len(body), cc=cc
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
# Create message
|
|
174
|
+
msg = MIMEMultipart()
|
|
175
|
+
msg["From"] = sender_email
|
|
176
|
+
msg["To"] = to
|
|
177
|
+
msg["Subject"] = subject
|
|
178
|
+
|
|
179
|
+
if cc:
|
|
180
|
+
msg["Cc"] = cc
|
|
181
|
+
|
|
182
|
+
# Attach body as plain text
|
|
183
|
+
msg.attach(MIMEText(body, "plain"))
|
|
184
|
+
|
|
185
|
+
# Send email
|
|
186
|
+
logger.debug("Connecting to SMTP server", host=host, port=port)
|
|
187
|
+
with smtplib.SMTP(host, port) as server:
|
|
188
|
+
if use_tls:
|
|
189
|
+
logger.trace("Upgrading to TLS")
|
|
190
|
+
server.starttls()
|
|
191
|
+
|
|
192
|
+
logger.trace("Authenticating", username=username)
|
|
193
|
+
server.login(username, password)
|
|
194
|
+
|
|
195
|
+
# Build recipient list
|
|
196
|
+
recipients = [to]
|
|
197
|
+
if cc:
|
|
198
|
+
cc_addresses = [addr.strip() for addr in cc.split(",")]
|
|
199
|
+
recipients.extend(cc_addresses)
|
|
200
|
+
|
|
201
|
+
logger.debug("Sending message", recipients_count=len(recipients))
|
|
202
|
+
server.send_message(msg)
|
|
203
|
+
|
|
204
|
+
success_msg = f"✓ Email sent successfully to {to}"
|
|
205
|
+
if cc:
|
|
206
|
+
success_msg += f" (cc: {cc})"
|
|
207
|
+
|
|
208
|
+
logger.success("Email sent successfully", to=to, cc=cc)
|
|
209
|
+
return success_msg
|
|
210
|
+
|
|
211
|
+
except smtplib.SMTPAuthenticationError as e:
|
|
212
|
+
error_msg = f"✗ SMTP authentication failed: {str(e)}"
|
|
213
|
+
logger.error(
|
|
214
|
+
"SMTP authentication failed",
|
|
215
|
+
server=f"{host}:{port}",
|
|
216
|
+
username=username,
|
|
217
|
+
error=str(e),
|
|
218
|
+
)
|
|
219
|
+
return error_msg
|
|
220
|
+
except smtplib.SMTPException as e:
|
|
221
|
+
error_msg = f"✗ SMTP error: {str(e)}"
|
|
222
|
+
logger.error("SMTP error", server=f"{host}:{port}", error=str(e))
|
|
223
|
+
return error_msg
|
|
224
|
+
except Exception as e:
|
|
225
|
+
error_msg = f"✗ Failed to send email: {str(e)}"
|
|
226
|
+
logger.error(
|
|
227
|
+
"Failed to send email", error_type=type(e).__name__, error=str(e)
|
|
228
|
+
)
|
|
229
|
+
return error_msg
|
|
230
|
+
|
|
231
|
+
logger.success("Email tool created", tool_name=name)
|
|
232
|
+
return send_email
|