dao-ai 0.0.35__py3-none-any.whl → 0.1.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.
- dao_ai/__init__.py +29 -0
- dao_ai/cli.py +195 -30
- dao_ai/config.py +797 -242
- 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 +75 -0
- dao_ai/genie/cache/lru.py +329 -0
- dao_ai/genie/cache/semantic.py +919 -0
- dao_ai/genie/core.py +35 -0
- dao_ai/graph.py +27 -253
- dao_ai/hooks/__init__.py +9 -6
- dao_ai/hooks/core.py +22 -190
- dao_ai/memory/__init__.py +10 -0
- dao_ai/memory/core.py +23 -5
- dao_ai/memory/databricks.py +389 -0
- dao_ai/memory/postgres.py +2 -2
- dao_ai/messages.py +6 -4
- dao_ai/middleware/__init__.py +125 -0
- dao_ai/middleware/assertions.py +778 -0
- dao_ai/middleware/base.py +50 -0
- dao_ai/middleware/core.py +61 -0
- dao_ai/middleware/guardrails.py +415 -0
- dao_ai/middleware/human_in_the_loop.py +228 -0
- dao_ai/middleware/message_validation.py +554 -0
- dao_ai/middleware/summarization.py +192 -0
- dao_ai/models.py +1177 -108
- dao_ai/nodes.py +118 -161
- dao_ai/optimization.py +664 -0
- dao_ai/orchestration/__init__.py +52 -0
- dao_ai/orchestration/core.py +287 -0
- dao_ai/orchestration/supervisor.py +264 -0
- dao_ai/orchestration/swarm.py +226 -0
- dao_ai/prompts.py +126 -29
- dao_ai/providers/databricks.py +126 -381
- dao_ai/state.py +139 -21
- dao_ai/tools/__init__.py +11 -5
- dao_ai/tools/core.py +57 -4
- dao_ai/tools/email.py +280 -0
- dao_ai/tools/genie.py +108 -35
- dao_ai/tools/mcp.py +4 -3
- dao_ai/tools/memory.py +50 -0
- dao_ai/tools/python.py +4 -12
- dao_ai/tools/search.py +14 -0
- dao_ai/tools/slack.py +1 -1
- dao_ai/tools/unity_catalog.py +8 -6
- dao_ai/tools/vector_search.py +16 -9
- dao_ai/utils.py +72 -8
- dao_ai-0.1.0.dist-info/METADATA +1878 -0
- dao_ai-0.1.0.dist-info/RECORD +62 -0
- 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.35.dist-info/METADATA +0 -1169
- dao_ai-0.0.35.dist-info/RECORD +0 -41
- {dao_ai-0.0.35.dist-info → dao_ai-0.1.0.dist-info}/WHEEL +0 -0
- {dao_ai-0.0.35.dist-info → dao_ai-0.1.0.dist-info}/entry_points.txt +0 -0
- {dao_ai-0.0.35.dist-info → dao_ai-0.1.0.dist-info}/licenses/LICENSE +0 -0
dao_ai/state.py
CHANGED
|
@@ -1,41 +1,159 @@
|
|
|
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, Field
|
|
19
|
+
from typing_extensions import NotRequired
|
|
20
|
+
|
|
5
21
|
|
|
22
|
+
class GenieSpaceState(BaseModel):
|
|
23
|
+
"""State for a single Genie space/conversation.
|
|
6
24
|
|
|
7
|
-
|
|
25
|
+
This tracks the conversation state and metadata for a Genie space,
|
|
26
|
+
allowing multi-turn conversations and caching information to be preserved.
|
|
27
|
+
"""
|
|
8
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
|
+
)
|
|
25
92
|
|
|
26
|
-
|
|
27
|
-
|
|
93
|
+
# Future: Add other stateful tool state here
|
|
94
|
+
# other_tool_state: OtherToolState = Field(default_factory=OtherToolState)
|
|
28
95
|
|
|
29
|
-
summarized_messages: list[AnyMessage]
|
|
30
96
|
|
|
31
|
-
|
|
32
|
-
|
|
97
|
+
class AgentState(MessagesState, total=False):
|
|
98
|
+
"""
|
|
99
|
+
Primary state schema for DAO AI agents.
|
|
33
100
|
|
|
34
|
-
|
|
35
|
-
|
|
101
|
+
Extends MessagesState to include the messages channel with proper
|
|
102
|
+
add_messages reducer, plus additional fields for DAO AI functionality.
|
|
103
|
+
|
|
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
|
|
110
|
+
|
|
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
|
+
"""
|
|
120
|
+
|
|
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
|
+
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")
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
@tool
|
|
142
|
+
def my_tool(runtime: ToolRuntime[Context]) -> str:
|
|
143
|
+
user_id = runtime.context.user_id
|
|
144
|
+
store_num = runtime.context.custom.get("store_num")
|
|
145
|
+
return f"Hello, {user_id} at store {store_num}!"
|
|
146
|
+
|
|
147
|
+
class MyMiddleware(AgentMiddleware[AgentState, Context]):
|
|
148
|
+
def before_model(
|
|
149
|
+
self,
|
|
150
|
+
state: AgentState,
|
|
151
|
+
runtime: Runtime[Context]
|
|
152
|
+
) -> dict[str, Any] | None:
|
|
153
|
+
user_id = runtime.context.user_id
|
|
154
|
+
return None
|
|
155
|
+
"""
|
|
156
|
+
|
|
39
157
|
user_id: str | None = None
|
|
40
158
|
thread_id: str | None = None
|
|
41
|
-
|
|
159
|
+
custom: dict[str, Any] = Field(default_factory=dict)
|
dao_ai/tools/__init__.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
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
|
|
11
12
|
from dao_ai.tools.time import (
|
|
12
13
|
add_time_tool,
|
|
@@ -28,6 +29,9 @@ __all__ = [
|
|
|
28
29
|
"create_hooks",
|
|
29
30
|
"create_mcp_tools",
|
|
30
31
|
"create_python_tool",
|
|
32
|
+
"create_search_memory_tool",
|
|
33
|
+
"create_search_tool",
|
|
34
|
+
"create_send_email_tool",
|
|
31
35
|
"create_send_slack_message_tool",
|
|
32
36
|
"create_tools",
|
|
33
37
|
"create_uc_tools",
|
|
@@ -35,7 +39,9 @@ __all__ = [
|
|
|
35
39
|
"current_time_tool",
|
|
36
40
|
"format_time_tool",
|
|
37
41
|
"is_business_hours_tool",
|
|
38
|
-
"
|
|
42
|
+
"LRUCacheService",
|
|
43
|
+
"say_hello_tool",
|
|
44
|
+
"SemanticCacheService",
|
|
39
45
|
"time_difference_tool",
|
|
40
46
|
"time_in_timezone_tool",
|
|
41
47
|
"time_until_tool",
|
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
|
|
|
@@ -54,6 +68,45 @@ def create_tools(tool_models: Sequence[ToolModel]) -> Sequence[RunnableLike]:
|
|
|
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 = 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,280 @@
|
|
|
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.info("=== Creating send_email_tool ===")
|
|
111
|
+
logger.debug(
|
|
112
|
+
f"Factory called with config type: {type(smtp_config).__name__}, "
|
|
113
|
+
f"name={name}, description={description}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Convert dict to SMTPConfigModel if needed
|
|
117
|
+
if isinstance(smtp_config, dict):
|
|
118
|
+
logger.debug("Converting dict config to SMTPConfigModel")
|
|
119
|
+
smtp_config = SMTPConfigModel(**smtp_config)
|
|
120
|
+
else:
|
|
121
|
+
logger.debug("Config already is SMTPConfigModel")
|
|
122
|
+
|
|
123
|
+
# Resolve all variable values
|
|
124
|
+
logger.debug("Resolving SMTP configuration variables...")
|
|
125
|
+
|
|
126
|
+
logger.debug(" - Resolving host")
|
|
127
|
+
host: str = value_of(smtp_config.host)
|
|
128
|
+
logger.debug(f" Host resolved: {host}")
|
|
129
|
+
|
|
130
|
+
logger.debug(" - Resolving port")
|
|
131
|
+
port: int = int(value_of(smtp_config.port))
|
|
132
|
+
logger.debug(f" Port resolved: {port}")
|
|
133
|
+
|
|
134
|
+
logger.debug(" - Resolving username")
|
|
135
|
+
username: str = value_of(smtp_config.username)
|
|
136
|
+
logger.debug(f" Username resolved: {username}")
|
|
137
|
+
|
|
138
|
+
logger.debug(" - Resolving password")
|
|
139
|
+
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
|
+
sender_email: str = (
|
|
146
|
+
value_of(smtp_config.sender_email) if smtp_config.sender_email else username
|
|
147
|
+
)
|
|
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
|
+
use_tls: bool = smtp_config.use_tls
|
|
154
|
+
logger.debug(f" - TLS enabled: {use_tls}")
|
|
155
|
+
|
|
156
|
+
logger.info(
|
|
157
|
+
f"SMTP configuration resolved - host={host}, port={port}, "
|
|
158
|
+
f"sender={sender_email}, use_tls={use_tls}"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if name is None:
|
|
162
|
+
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
|
+
if description is None:
|
|
168
|
+
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
|
+
|
|
173
|
+
logger.info(f"Creating tool '{name}' with @tool decorator")
|
|
174
|
+
|
|
175
|
+
@tool(
|
|
176
|
+
name_or_callable=name,
|
|
177
|
+
description=description,
|
|
178
|
+
)
|
|
179
|
+
def send_email(
|
|
180
|
+
to: str,
|
|
181
|
+
subject: str,
|
|
182
|
+
body: str,
|
|
183
|
+
cc: Optional[str] = None,
|
|
184
|
+
) -> str:
|
|
185
|
+
"""
|
|
186
|
+
Send an email via SMTP.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
to: Recipient email address
|
|
190
|
+
subject: Email subject line
|
|
191
|
+
body: Email body content (plain text)
|
|
192
|
+
cc: Optional CC recipients (comma-separated email addresses)
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
str: Success or error message
|
|
196
|
+
"""
|
|
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'}")
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
logger.debug("Constructing email message...")
|
|
205
|
+
|
|
206
|
+
# Create message
|
|
207
|
+
msg = MIMEMultipart()
|
|
208
|
+
msg["From"] = sender_email
|
|
209
|
+
msg["To"] = to
|
|
210
|
+
msg["Subject"] = subject
|
|
211
|
+
logger.debug(f" From: {sender_email}")
|
|
212
|
+
logger.debug(f" To: {to}")
|
|
213
|
+
logger.debug(f" Subject: {subject}")
|
|
214
|
+
|
|
215
|
+
if cc:
|
|
216
|
+
msg["Cc"] = cc
|
|
217
|
+
logger.debug(f" CC: {cc}")
|
|
218
|
+
|
|
219
|
+
# Attach body as plain text
|
|
220
|
+
msg.attach(MIMEText(body, "plain"))
|
|
221
|
+
logger.debug(f" Body attached ({len(body)} chars)")
|
|
222
|
+
|
|
223
|
+
# Send email
|
|
224
|
+
logger.info(f"Connecting to SMTP server {host}:{port}...")
|
|
225
|
+
with smtplib.SMTP(host, port) as server:
|
|
226
|
+
logger.debug("SMTP connection established")
|
|
227
|
+
|
|
228
|
+
if use_tls:
|
|
229
|
+
logger.debug("Upgrading connection to TLS...")
|
|
230
|
+
server.starttls()
|
|
231
|
+
logger.debug("TLS upgrade successful")
|
|
232
|
+
|
|
233
|
+
logger.debug(f"Authenticating with username: {username}")
|
|
234
|
+
server.login(username, password)
|
|
235
|
+
logger.info("SMTP authentication successful")
|
|
236
|
+
|
|
237
|
+
# Build recipient list
|
|
238
|
+
recipients = [to]
|
|
239
|
+
if cc:
|
|
240
|
+
cc_addresses = [addr.strip() for addr in cc.split(",")]
|
|
241
|
+
recipients.extend(cc_addresses)
|
|
242
|
+
logger.debug(f"Total recipients: {len(recipients)} ({recipients})")
|
|
243
|
+
else:
|
|
244
|
+
logger.debug(f"Single recipient: {to}")
|
|
245
|
+
|
|
246
|
+
logger.info(f"Sending message to {len(recipients)} recipient(s)...")
|
|
247
|
+
server.send_message(msg)
|
|
248
|
+
logger.info("Message sent successfully via SMTP")
|
|
249
|
+
|
|
250
|
+
success_msg = f"✓ Email sent successfully to {to}"
|
|
251
|
+
if cc:
|
|
252
|
+
success_msg += f" (cc: {cc})"
|
|
253
|
+
|
|
254
|
+
logger.info(success_msg)
|
|
255
|
+
logger.info("=== send_email completed successfully ===")
|
|
256
|
+
return success_msg
|
|
257
|
+
|
|
258
|
+
except smtplib.SMTPAuthenticationError as e:
|
|
259
|
+
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) ===")
|
|
264
|
+
return error_msg
|
|
265
|
+
except smtplib.SMTPException as e:
|
|
266
|
+
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) ===")
|
|
270
|
+
return error_msg
|
|
271
|
+
except Exception as e:
|
|
272
|
+
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) ===")
|
|
276
|
+
return error_msg
|
|
277
|
+
|
|
278
|
+
logger.info(f"Tool '{name}' created successfully")
|
|
279
|
+
logger.info("=== send_email_tool creation complete ===")
|
|
280
|
+
return send_email
|