feat: scheduled reminders + less aggressive article summary

Add scheduled messages/reminders system:
- New scheduled_messages table in memory-service with CRUD endpoints
- schedule_message, list_reminders, cancel_reminder tools for the bot
- Background scheduler loop (30s) sends due reminders automatically
- Supports one-time, daily, weekly, weekdays, monthly repeat patterns

Make article URL handling non-blocking:
- Show 3 options (discuss, text summary, audio) instead of forcing audio wizard
- Default to passing article context to AI if user just keeps chatting
- New AWAITING_LANGUAGE state for cleaner audio flow FSM

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-03-09 06:55:14 +02:00
parent 19abea01ca
commit 964a3f6075
4 changed files with 558 additions and 8 deletions

View File

@@ -84,7 +84,11 @@ class ArticleSummaryHandler:
return await self._check_for_url(room_id, sender, body)
elif session.state == ArticleState.URL_DETECTED:
# Waiting for language selection
# Waiting for user to pick action (discuss, text summary, audio)
return await self._on_action_choice(room_id, sender, body, body_lower)
elif session.state == ArticleState.AWAITING_LANGUAGE:
# Audio flow: waiting for language selection
return self._on_language(room_id, sender, body_lower)
elif session.state == ArticleState.LANGUAGE:
@@ -143,10 +147,11 @@ class ArticleSummaryHandler:
return (
f"**Found:** {session.title} (~{read_time} min read){topics_hint}\n\n"
f"Want an audio summary? What language?\n"
f"1️⃣ English\n"
f"2️⃣ German\n\n"
f"_(or say \"cancel\" to skip)_"
f"What would you like to do?\n"
f"1\ufe0f\u20e3 **Discuss** \u2014 I'll read the article and we can talk about it\n"
f"2\ufe0f\u20e3 **Text summary** \u2014 Quick written summary\n"
f"3\ufe0f\u20e3 **Audio summary** \u2014 Blinkist-style MP3\n\n"
f"_(or just keep chatting \u2014 I won't interrupt)_"
)
def _on_language(
@@ -224,6 +229,79 @@ class ArticleSummaryHandler:
self.sessions.touch(sender, room_id)
return "__GENERATE__"
async def _on_action_choice(
self, room_id: str, sender: str, body: str, body_lower: str
) -> str | None:
"""Handle user's choice after URL detection: discuss, text summary, or audio."""
session = self.sessions.get(sender, room_id)
# Option 1: Discuss — reset FSM, return article context for AI handler
if body_lower in ("1", "discuss", "diskutieren", "besprechen"):
article_context = session.content[:8000]
title = session.title
self.sessions.reset(sender, room_id)
return f"__DISCUSS__{title}\n{article_context}"
# Option 2: Text summary — generate and return text, no TTS
if body_lower in ("2", "text", "text summary", "zusammenfassung"):
return await self._generate_text_summary(room_id, sender)
# Option 3: Audio summary — enter language selection (existing flow)
if body_lower in ("3", "audio", "audio summary"):
return self._prompt_language(room_id, sender)
# Anything else — user is just chatting, reset and pass through with article context
article_context = session.content[:8000]
title = session.title
self.sessions.reset(sender, room_id)
return f"__DISCUSS__{title}\n{article_context}"
def _prompt_language(self, room_id: str, sender: str) -> str:
"""Present language selection for audio summary."""
session = self.sessions.get(sender, room_id)
session.state = ArticleState.AWAITING_LANGUAGE
self.sessions.touch(sender, room_id)
return (
"What language for the audio summary?\n"
"1\ufe0f\u20e3 English\n"
"2\ufe0f\u20e3 German"
)
async def _generate_text_summary(self, room_id: str, sender: str) -> str | None:
"""Generate a text-only summary of the article."""
session = self.sessions.get(sender, room_id)
try:
resp = await self.llm.chat.completions.create(
model=self.model,
messages=[
{
"role": "system",
"content": (
"Summarize this article concisely in 3-5 paragraphs. "
"Respond in the same language as the article."
),
},
{
"role": "user",
"content": f"Article: {session.title}\n\n{session.content[:12000]}",
},
],
max_tokens=1000,
temperature=0.3,
)
summary = resp.choices[0].message.content.strip()
session.summary_text = summary
session.state = ArticleState.COMPLETE
self.sessions.touch(sender, room_id)
return (
f"**Summary: {session.title}**\n\n{summary}\n\n"
f"_Ask follow-up questions or share a new link._"
)
except Exception:
logger.warning("Text summary failed", exc_info=True)
self.sessions.reset(sender, room_id)
return None
async def generate_and_post(self, bot, room_id: str, sender: str) -> None:
"""Run the full pipeline: summarize → TTS → upload MP3."""
session = self.sessions.get(sender, room_id)

View File

@@ -10,6 +10,7 @@ from enum import Enum, auto
class ArticleState(Enum):
IDLE = auto()
URL_DETECTED = auto()
AWAITING_LANGUAGE = auto() # Audio flow: waiting for language selection
LANGUAGE = auto()
DURATION = auto()
TOPICS = auto()