feat: add email_send orchestrator tool

Wraps the existing email_utils.send_email helper as an admin-only tool.
Accepts to, subject, body (plain text); newlines converted to <br> for HTML part.
Registered in _CALLABLES, _ALL_DECLARATIONS, and TOOL_ROLES (admin).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-29 21:35:29 -04:00
parent a5658eb3c4
commit fd0fb76c08
2 changed files with 40 additions and 2 deletions

View File

@@ -48,7 +48,7 @@ from tools.scratch import (
from tools.system import cortex_restart as _cortex_restart, cortex_logs as _cortex_logs from tools.system import cortex_restart as _cortex_restart, cortex_logs as _cortex_logs
from tools.web import http_fetch as _http_fetch from tools.web import http_fetch as _http_fetch
from tools.files import file_list as _file_list, file_write as _file_write from tools.files import file_list as _file_list, file_write as _file_write
from tools.notify import nc_talk_send as _nc_talk_send from tools.notify import nc_talk_send as _nc_talk_send, email_send as _email_send
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -296,6 +296,7 @@ _CALLABLES: dict[str, callable] = {
"cortex_restart": _cortex_restart, "cortex_restart": _cortex_restart,
"cortex_logs": _cortex_logs, "cortex_logs": _cortex_logs,
"http_fetch": _http_fetch, "http_fetch": _http_fetch,
"email_send": _email_send,
"nc_talk_send": _nc_talk_send, "nc_talk_send": _nc_talk_send,
"task_list": _task_list, "task_list": _task_list,
"task_create": _task_create, "task_create": _task_create,
@@ -755,6 +756,24 @@ _file_write_declaration = types.FunctionDeclaration(
), ),
) )
_email_send_declaration = types.FunctionDeclaration(
name="email_send",
description=(
"Send an email from the server's configured SMTP account. Use for delivering "
"summaries, reports, reminders, or any content the user wants emailed. "
"body is plain text; newlines are preserved."
),
parameters=types.Schema(
type=types.Type.OBJECT,
properties={
"to": types.Schema(type=types.Type.STRING, description="Recipient email address"),
"subject": types.Schema(type=types.Type.STRING, description="Email subject line"),
"body": types.Schema(type=types.Type.STRING, description="Plain-text email body"),
},
required=["to", "subject", "body"],
),
)
_nc_talk_send_declaration = types.FunctionDeclaration( _nc_talk_send_declaration = types.FunctionDeclaration(
name="nc_talk_send", name="nc_talk_send",
description=( description=(
@@ -791,6 +810,8 @@ TOOL_ROLES: dict[str, str] = {
"file_list": "admin", "file_list": "admin",
"file_write": "admin", "file_write": "admin",
"ae_task_list": "admin", # reads agents_sync kanban "ae_task_list": "admin", # reads agents_sync kanban
"email_send": "admin", # sends from server SMTP account
"nc_talk_send": "admin",
} }
# Tools that require explicit user confirmation before executing. # Tools that require explicit user confirmation before executing.
@@ -831,6 +852,7 @@ _ALL_DECLARATIONS: list[types.FunctionDeclaration] = [
_cortex_restart_declaration, _cortex_restart_declaration,
_cortex_logs_declaration, _cortex_logs_declaration,
_http_fetch_declaration, _http_fetch_declaration,
_email_send_declaration,
_nc_talk_send_declaration, _nc_talk_send_declaration,
_task_list_declaration, _task_list_declaration,
_task_create_declaration, _task_create_declaration,

View File

@@ -2,9 +2,10 @@
Notification tools — proactively send messages to user channels. Notification tools — proactively send messages to user channels.
nc_talk_send routes through notification.py → channels.json. nc_talk_send routes through notification.py → channels.json.
Requires notification_channel and notification_room set in the user's channels.json. email_send uses the server SMTP config from .env (smtp_server, smtp_from_*).
""" """
import asyncio
import logging import logging
from persona import get_user from persona import get_user
@@ -12,6 +13,21 @@ from persona import get_user
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def email_send(to: str, subject: str, body: str) -> str:
"""Send an email via the server's configured SMTP account."""
from email_utils import send_email
ok = await asyncio.to_thread(
send_email,
to_email=to,
subject=subject,
body_text=body,
body_html=body.replace("\n", "<br>"),
)
if ok:
return f"Email sent to {to}."
return "Failed to send email — check SMTP configuration in .env."
async def nc_talk_send(message: str) -> str: async def nc_talk_send(message: str) -> str:
"""Send a message to the user via their configured notification channel. """Send a message to the user via their configured notification channel.