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:
+74
-2
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user