From 24ac7f2f48127042f98d4b7c7471987a9f9191fc Mon Sep 17 00:00:00 2001 From: Frank Schwenk Date: Wed, 17 Jun 2026 08:45:09 +0200 Subject: [PATCH] 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 --- server/ai.py | 76 ++++++++++++++++++++++++++++++++++++++++-- server/app.py | 9 ++--- server/scheduler.py | 20 ++++++++++- tools/regenerate_ai.py | 10 +++--- 4 files changed, 104 insertions(+), 11 deletions(-) diff --git a/server/ai.py b/server/ai.py index 6244c98..85c82d7 100644 --- a/server/ai.py +++ b/server/ai.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from datetime import datetime +from datetime import datetime, timedelta from typing import Any 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." ) +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: cur = await db.execute("SELECT content FROM ai_cache WHERE key = ?", (key,)) row = await cur.fetchone() @@ -87,6 +95,26 @@ def _motivation_key(day: str) -> str: 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: cached = await _get_cache(db, _motivation_key(day)) 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} +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]: - 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} diff --git a/server/app.py b/server/app.py index 739f1fd..032c652 100644 --- a/server/app.py +++ b/server/app.py @@ -11,7 +11,7 @@ from fastapi import Depends, FastAPI, HTTPException, Request from fastapi.responses import FileResponse, JSONResponse 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.config_loader import load_meds_config, slot_to_dict, today_str from server.db import ensure_schema, get_db, new_id, utc_now_iso @@ -40,13 +40,14 @@ _configure_logging() logger = logging.getLogger(__name__) -async def _ensure_today_motivation() -> None: +async def _ensure_today_ai_texts() -> None: config = load_meds_config() db = await get_db() try: await generate_daily_motivation(db, config.timezone) + await generate_roast_of_the_day(db, config.timezone) except Exception: - logger.exception("Startup motivation generation failed") + logger.exception("Startup AI text generation failed") finally: await db.close() @@ -57,7 +58,7 @@ async def lifespan(app: FastAPI): await ensure_schema(db) await db.close() start_scheduler() - asyncio.create_task(_ensure_today_motivation()) + asyncio.create_task(_ensure_today_ai_texts()) yield diff --git a/server/scheduler.py b/server/scheduler.py index 081254b..b8093a9 100644 --- a/server/scheduler.py +++ b/server/scheduler.py @@ -9,7 +9,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger 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.db import get_db, utc_now_iso from server.messages import pick @@ -54,6 +54,17 @@ async def _generate_daily_motivation() -> None: 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: logger.info("Evening check triggered") db = await get_db() @@ -147,6 +158,13 @@ def start_scheduler() -> None: 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: scheduler.start() logger.info("Scheduler started for timezone %s", tz) diff --git a/tools/regenerate_ai.py b/tools/regenerate_ai.py index 1204ca4..7f96344 100644 --- a/tools/regenerate_ai.py +++ b/tools/regenerate_ai.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 -"""Regenerate cached AI texts (motivation).""" +"""Regenerate cached AI texts (motivation, roast).""" from __future__ import annotations import argparse 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.db import get_db @@ -19,8 +19,10 @@ async def main() -> None: config = load_meds_config() db = await get_db() try: - result = await generate_daily_motivation(db, config.timezone, force=args.force) - print("motivation:", result) + motivation = await generate_daily_motivation(db, config.timezone, force=args.force) + roast = await generate_roast_of_the_day(db, config.timezone, force=args.force) + print("motivation:", motivation) + print("roast:", roast) finally: await db.close()