feat: restore roast-of-the-day with 14-day prompt history

Separate roast generation from motivation again and pass cached roasts
from the last 14 days into the OpenRouter prompt to reduce repetition.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Frank Schwenk
2026-06-17 08:45:09 +02:00
parent c59c3069c4
commit 24ac7f2f48
4 changed files with 104 additions and 11 deletions
+74 -2
View File
@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from datetime import datetime from datetime import datetime, timedelta
from typing import Any from typing import Any
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
@@ -21,6 +21,14 @@ MOTIVATION_SYSTEM = (
"Genau ein Satz auf Deutsch, max 25 Wörter. Keine Anführungszeichen um den Spruch." "Genau ein Satz auf Deutsch, max 25 Wörter. Keine Anführungszeichen um den Spruch."
) )
ROAST_SYSTEM = (
"Du bist ein sarkastischer, dark-humor Medikamenten-Coach für jemanden mit ADHS. "
"Kurz (max 2 Sätze), deutsch, witzig aber nicht gemein. Keine medizinischen Ratschläge. "
"Keine Anführungszeichen um den Spruch. Wiederhole keine Formulierungen oder Ideen aus der Historie."
)
ROAST_HISTORY_DAYS = 14
async def _get_cache(db: aiosqlite.Connection, key: str) -> str | None: async def _get_cache(db: aiosqlite.Connection, key: str) -> str | None:
cur = await db.execute("SELECT content FROM ai_cache WHERE key = ?", (key,)) cur = await db.execute("SELECT content FROM ai_cache WHERE key = ?", (key,))
row = await cur.fetchone() row = await cur.fetchone()
@@ -87,6 +95,26 @@ def _motivation_key(day: str) -> str:
return f"motivation:v2:{day}" return f"motivation:v2:{day}"
def _roast_key(day: str) -> str:
return f"roast:{day}"
async def _recent_roasts(
db: aiosqlite.Connection,
*,
before_day: str,
days: int = ROAST_HISTORY_DAYS,
) -> list[str]:
end = datetime.strptime(before_day, "%Y-%m-%d").date()
roasts: list[str] = []
for offset in range(1, days + 1):
day = (end - timedelta(days=offset)).isoformat()
text = await _get_cache(db, _roast_key(day))
if text:
roasts.append(text)
return roasts
async def _cached_motivation(db: aiosqlite.Connection, day: str) -> str | None: async def _cached_motivation(db: aiosqlite.Connection, day: str) -> str | None:
cached = await _get_cache(db, _motivation_key(day)) cached = await _get_cache(db, _motivation_key(day))
if cached: if cached:
@@ -134,5 +162,49 @@ async def get_daily_motivation(db: aiosqlite.Connection, tz: ZoneInfo) -> dict[s
return {"text": pick("motivation_fallback"), "source": "fallback", "day": day} return {"text": pick("motivation_fallback"), "source": "fallback", "day": day}
async def generate_roast_of_the_day(
db: aiosqlite.Connection,
tz: ZoneInfo,
day: str | None = None,
*,
force: bool = False,
) -> dict[str, Any]:
day = day or datetime.now(tz).strftime("%Y-%m-%d")
key = _roast_key(day)
if not force:
cached = await _get_cache(db, key)
if cached:
return {"text": cached, "source": "cache", "day": day}
stats = await compute_stats(db)
recent = await _recent_roasts(db, before_day=day)
history = ""
if recent:
lines = "\n".join(f"- {text}" for text in recent)
history = (
f"\n\nDiese Roasts der letzten {ROAST_HISTORY_DAYS} Tage wurden bereits verwendet "
f"(nicht wiederholen, neue Idee):\n{lines}"
)
prompt = (
"Schreibe den Roast-of-the-Day. "
f"Streak: {stats['streak']} Tage. "
f"Compliance über {stats['compliance_window_days']} aktive Tage: {stats['compliance_percent']}%. "
f"Ein sarkastischer Spruch im dark-humor Medikamenten-Coach Stil.{history}"
)
text = await _call_openrouter(prompt, system=ROAST_SYSTEM)
if not text:
text = pick("roast_fallback")
source = "fallback"
else:
source = "ai"
await _set_cache(db, key, text)
logger.info("Generated roast of the day for %s (%s)", day, source)
return {"text": text, "source": source, "day": day}
async def get_roast_of_the_day(db: aiosqlite.Connection, tz: ZoneInfo) -> dict[str, Any]: async def get_roast_of_the_day(db: aiosqlite.Connection, tz: ZoneInfo) -> dict[str, Any]:
return await get_daily_motivation(db, tz) day = datetime.now(tz).strftime("%Y-%m-%d")
cached = await _get_cache(db, _roast_key(day))
if cached:
return {"text": cached, "source": "cache", "day": day}
return {"text": pick("roast_fallback"), "source": "fallback", "day": day}
+5 -4
View File
@@ -11,7 +11,7 @@ from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.responses import FileResponse, JSONResponse from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from server.ai import generate_daily_motivation, get_roast_of_the_day from server.ai import generate_daily_motivation, generate_roast_of_the_day, get_roast_of_the_day
from server.auth import create_token, require_auth, verify_pin from server.auth import create_token, require_auth, verify_pin
from server.config_loader import load_meds_config, slot_to_dict, today_str from server.config_loader import load_meds_config, slot_to_dict, today_str
from server.db import ensure_schema, get_db, new_id, utc_now_iso from server.db import ensure_schema, get_db, new_id, utc_now_iso
@@ -40,13 +40,14 @@ _configure_logging()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def _ensure_today_motivation() -> None: async def _ensure_today_ai_texts() -> None:
config = load_meds_config() config = load_meds_config()
db = await get_db() db = await get_db()
try: try:
await generate_daily_motivation(db, config.timezone) await generate_daily_motivation(db, config.timezone)
await generate_roast_of_the_day(db, config.timezone)
except Exception: except Exception:
logger.exception("Startup motivation generation failed") logger.exception("Startup AI text generation failed")
finally: finally:
await db.close() await db.close()
@@ -57,7 +58,7 @@ async def lifespan(app: FastAPI):
await ensure_schema(db) await ensure_schema(db)
await db.close() await db.close()
start_scheduler() start_scheduler()
asyncio.create_task(_ensure_today_motivation()) asyncio.create_task(_ensure_today_ai_texts())
yield yield
+19 -1
View File
@@ -9,7 +9,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger from apscheduler.triggers.date import DateTrigger
from server.ai import generate_daily_motivation from server.ai import generate_daily_motivation, generate_roast_of_the_day
from server.config_loader import load_meds_config, slot_to_dict, today_str from server.config_loader import load_meds_config, slot_to_dict, today_str
from server.db import get_db, utc_now_iso from server.db import get_db, utc_now_iso
from server.messages import pick from server.messages import pick
@@ -54,6 +54,17 @@ async def _generate_daily_motivation() -> None:
await db.close() await db.close()
async def _generate_daily_roast() -> None:
config = load_meds_config()
db = await get_db()
try:
await generate_roast_of_the_day(db, config.timezone)
except Exception:
logger.exception("Daily roast generation failed")
finally:
await db.close()
async def _evening_check() -> None: async def _evening_check() -> None:
logger.info("Evening check triggered") logger.info("Evening check triggered")
db = await get_db() db = await get_db()
@@ -147,6 +158,13 @@ def start_scheduler() -> None:
replace_existing=True, replace_existing=True,
) )
scheduler.add_job(
_generate_daily_roast,
trigger=CronTrigger(hour=3, minute=5, timezone=tz),
id="daily-roast",
replace_existing=True,
)
if not scheduler.running: if not scheduler.running:
scheduler.start() scheduler.start()
logger.info("Scheduler started for timezone %s", tz) logger.info("Scheduler started for timezone %s", tz)
+6 -4
View File
@@ -1,12 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Regenerate cached AI texts (motivation).""" """Regenerate cached AI texts (motivation, roast)."""
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import asyncio import asyncio
from server.ai import generate_daily_motivation from server.ai import generate_daily_motivation, generate_roast_of_the_day
from server.config_loader import load_meds_config from server.config_loader import load_meds_config
from server.db import get_db from server.db import get_db
@@ -19,8 +19,10 @@ async def main() -> None:
config = load_meds_config() config = load_meds_config()
db = await get_db() db = await get_db()
try: try:
result = await generate_daily_motivation(db, config.timezone, force=args.force) motivation = await generate_daily_motivation(db, config.timezone, force=args.force)
print("motivation:", result) roast = await generate_roast_of_the_day(db, config.timezone, force=args.force)
print("motivation:", motivation)
print("roast:", roast)
finally: finally:
await db.close() await db.close()