feat: RSO observation system, child safety, Discord adapter, Telegram watchdog, email attachments
Core agent improvements: - RSO (Relevance Scoring & Observation) system: interaction_logger, memory_scorer, signal_detector - Memory access logging (memory_access_log table) for relevance scoring; high-signal turn detection - Rich conversation storage for notable turns; compact_conversation truncates long user messages - Task-type classifier (query/action/analysis/creative) for observation tagging - Nested sub-agent visibility: deep delegations now register against the main agent's manager Child safety (Gabriel profile): - child_safety.py: filtering, audit logging, prompt constants for restricted sessions - .kiro/specs/child-safety-profile: requirements, design, tasks specs - GABRIEL_BOT_PROPOSAL.md: initial proposal doc - Reduced context window (10 msgs) and tutor-mode identity for restricted users Telegram adapter: - Polling watchdog: auto-restarts updater if polling drops unexpectedly - get_me() with exponential-backoff retry on NetworkError at startup - Correct stop() ordering: signal watchdog before cancelling tasks Email / Gmail: - send_email: supports file attachments (attachments list param) - get_email: surfaces attachment metadata in response Scheduled tasks / weather: - Remove OpenWeatherMap API calls from morning-weather task; use wttr.in exclusively - New scheduled tasks and scheduler state persistence Discord: - adapters/discord/__init__.py scaffold - discord-plugin: MCP plugin for Claude Code Discord integration (server.ts, skills, config) Infrastructure: - n8n workflow exports (garvis_webhook, content_pipeline variants) - memory_workspace: context, homelab-repo-updates, weekly observation summaries, error logs - UCS C240 migration plan doc - requirements.txt: new deps - .claude/settings.json, fix_hooks.py: hook/permission tuning
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
"""Utility functions for Gmail/Calendar tools."""
|
||||
|
||||
import base64
|
||||
import mimetypes
|
||||
import re
|
||||
from email.mime.base import MIMEBase
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email import encoders
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
@@ -59,6 +63,7 @@ def create_mime_message(
|
||||
from_email: str = "me",
|
||||
cc: Optional[List[str]] = None,
|
||||
reply_to_message_id: Optional[str] = None,
|
||||
attachments: Optional[List[str]] = None,
|
||||
) -> Dict:
|
||||
"""Create a MIME message for Gmail API.
|
||||
|
||||
@@ -69,11 +74,22 @@ def create_mime_message(
|
||||
from_email: Sender email (default: "me")
|
||||
cc: Optional list of CC recipients
|
||||
reply_to_message_id: Optional message ID to reply to
|
||||
attachments: Optional list of file paths to attach
|
||||
|
||||
Returns:
|
||||
Dict with 'raw' key containing base64url-encoded message
|
||||
"""
|
||||
message = MIMEMultipart("alternative")
|
||||
is_html = bool(re.search(r"<[a-z][\s\S]*>", body, re.IGNORECASE))
|
||||
|
||||
# Build the body part first
|
||||
if attachments:
|
||||
# mixed wraps body alternative + file parts
|
||||
message = MIMEMultipart("mixed")
|
||||
body_part = MIMEMultipart("alternative")
|
||||
else:
|
||||
message = MIMEMultipart("alternative")
|
||||
body_part = message
|
||||
|
||||
message["To"] = to
|
||||
message["From"] = from_email
|
||||
message["Subject"] = subject
|
||||
@@ -85,21 +101,25 @@ def create_mime_message(
|
||||
message["In-Reply-To"] = reply_to_message_id
|
||||
message["References"] = reply_to_message_id
|
||||
|
||||
# Try to detect if body is HTML
|
||||
is_html = bool(re.search(r"<[a-z][\s\S]*>", body, re.IGNORECASE))
|
||||
|
||||
if is_html:
|
||||
# Add both plain text and HTML versions
|
||||
text_part = MIMEText(html_to_text(body), "plain")
|
||||
html_part = MIMEText(body, "html")
|
||||
message.attach(text_part)
|
||||
message.attach(html_part)
|
||||
body_part.attach(MIMEText(html_to_text(body), "plain"))
|
||||
body_part.attach(MIMEText(body, "html"))
|
||||
else:
|
||||
# Plain text only
|
||||
text_part = MIMEText(body, "plain")
|
||||
message.attach(text_part)
|
||||
body_part.attach(MIMEText(body, "plain"))
|
||||
|
||||
if attachments:
|
||||
message.attach(body_part)
|
||||
for file_path in attachments:
|
||||
path = Path(file_path)
|
||||
mime_type, _ = mimetypes.guess_type(str(path))
|
||||
main_type, sub_type = (mime_type or "application/octet-stream").split("/", 1)
|
||||
with open(path, "rb") as f:
|
||||
part = MIMEBase(main_type, sub_type)
|
||||
part.set_payload(f.read())
|
||||
encoders.encode_base64(part)
|
||||
part.add_header("Content-Disposition", "attachment", filename=path.name)
|
||||
message.attach(part)
|
||||
|
||||
# Encode as base64url
|
||||
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
|
||||
return {"raw": raw_message}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user