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.
Files changed (58) hide show
  1. dao_ai/__init__.py +29 -0
  2. dao_ai/cli.py +195 -30
  3. dao_ai/config.py +797 -242
  4. dao_ai/genie/__init__.py +38 -0
  5. dao_ai/genie/cache/__init__.py +43 -0
  6. dao_ai/genie/cache/base.py +72 -0
  7. dao_ai/genie/cache/core.py +75 -0
  8. dao_ai/genie/cache/lru.py +329 -0
  9. dao_ai/genie/cache/semantic.py +919 -0
  10. dao_ai/genie/core.py +35 -0
  11. dao_ai/graph.py +27 -253
  12. dao_ai/hooks/__init__.py +9 -6
  13. dao_ai/hooks/core.py +22 -190
  14. dao_ai/memory/__init__.py +10 -0
  15. dao_ai/memory/core.py +23 -5
  16. dao_ai/memory/databricks.py +389 -0
  17. dao_ai/memory/postgres.py +2 -2
  18. dao_ai/messages.py +6 -4
  19. dao_ai/middleware/__init__.py +125 -0
  20. dao_ai/middleware/assertions.py +778 -0
  21. dao_ai/middleware/base.py +50 -0
  22. dao_ai/middleware/core.py +61 -0
  23. dao_ai/middleware/guardrails.py +415 -0
  24. dao_ai/middleware/human_in_the_loop.py +228 -0
  25. dao_ai/middleware/message_validation.py +554 -0
  26. dao_ai/middleware/summarization.py +192 -0
  27. dao_ai/models.py +1177 -108
  28. dao_ai/nodes.py +118 -161
  29. dao_ai/optimization.py +664 -0
  30. dao_ai/orchestration/__init__.py +52 -0
  31. dao_ai/orchestration/core.py +287 -0
  32. dao_ai/orchestration/supervisor.py +264 -0
  33. dao_ai/orchestration/swarm.py +226 -0
  34. dao_ai/prompts.py +126 -29
  35. dao_ai/providers/databricks.py +126 -381
  36. dao_ai/state.py +139 -21
  37. dao_ai/tools/__init__.py +11 -5
  38. dao_ai/tools/core.py +57 -4
  39. dao_ai/tools/email.py +280 -0
  40. dao_ai/tools/genie.py +108 -35
  41. dao_ai/tools/mcp.py +4 -3
  42. dao_ai/tools/memory.py +50 -0
  43. dao_ai/tools/python.py +4 -12
  44. dao_ai/tools/search.py +14 -0
  45. dao_ai/tools/slack.py +1 -1
  46. dao_ai/tools/unity_catalog.py +8 -6
  47. dao_ai/tools/vector_search.py +16 -9
  48. dao_ai/utils.py +72 -8
  49. dao_ai-0.1.0.dist-info/METADATA +1878 -0
  50. dao_ai-0.1.0.dist-info/RECORD +62 -0
  51. dao_ai/chat_models.py +0 -204
  52. dao_ai/guardrails.py +0 -112
  53. dao_ai/tools/human_in_the_loop.py +0 -100
  54. dao_ai-0.0.35.dist-info/METADATA +0 -1169
  55. dao_ai-0.0.35.dist-info/RECORD +0 -41
  56. {dao_ai-0.0.35.dist-info → dao_ai-0.1.0.dist-info}/WHEEL +0 -0
  57. {dao_ai-0.0.35.dist-info → dao_ai-0.1.0.dist-info}/entry_points.txt +0 -0
  58. {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
- from langchain_core.messages import AnyMessage
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 langgraph.managed import RemainingSteps
4
- from pydantic import BaseModel
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
- class IncomingState(MessagesState): ...
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
- class SharedState(MessagesState):
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
- Extends LangGraph's MessagesState to maintain the conversation history while
20
- adding additional state fields specific to the DAO domain. This state is
21
- passed between nodes in the agent graph and modified during execution.
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
- context: str # short term/long term memory
89
+ genie: GenieState = Field(
90
+ default_factory=GenieState, description="Genie conversation state per space"
91
+ )
25
92
 
26
- active_agent: str # langgraph-swarm
27
- remaining_steps: RemainingSteps # langgraph-supervisor
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
- is_valid: bool # message validation node
32
- message_error: str
97
+ class AgentState(MessagesState, total=False):
98
+ """
99
+ Primary state schema for DAO AI agents.
33
100
 
34
- # A mapping of genie space_id to conversation_id
35
- genie_conversation_ids: dict[str, str] # Genie
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
- store_num: int | None = None
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
- create_tools,
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
- "search_tool",
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 langchain_community.tools import DuckDuckGoSearchRun
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
- def search_tool() -> RunnableLike:
58
- logger.debug("search_tool")
59
- return DuckDuckGoSearchRun(output_format="list")
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