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:
2026-04-23 07:54:01 -06:00
parent 1232490c3b
commit 916f86725d
70 changed files with 10945 additions and 187 deletions

View File

@@ -53,6 +53,7 @@ class GmailClient:
body: str,
cc: Optional[List[str]] = None,
reply_to_message_id: Optional[str] = None,
attachments: Optional[List[str]] = None,
) -> Dict:
"""Send an email.
@@ -80,6 +81,7 @@ class GmailClient:
body=body,
cc=cc,
reply_to_message_id=reply_to_message_id,
attachments=attachments,
)
result = (

View File

@@ -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}