from __future__ import annotations import logging from datetime import datetime from typing import Any from zoneinfo import ZoneInfo import aiosqlite import httpx from server.db import utc_now_iso from server.messages import pick from server.settings import settings from server.stats import compute_stats logger = logging.getLogger(__name__) MOTIVATION_SYSTEM = ( "Du schreibst Tages-Motivationssprüche für 'ADHD live, laugh, toaster bath motivational'. " "Stil: dark humor, Sarkasmus, passiv-aggressiv, kreativ-motivierend — nie gemein, nie medizinisch. " "Genau ein Satz auf Deutsch, max 25 Wörter. Keine Anführungszeichen um den Spruch." ) ORACLE_SYSTEM = ( "Du bist das wöchentliche KI-Orakel einer ADHS-Medikamenten-App. " "Passiv-aggressiv, trocken, max 4 Sätze auf Deutsch. Keine medizinischen Ratschläge." ) 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() await cur.close() return row[0] if row else None async def _set_cache(db: aiosqlite.Connection, key: str, content: str) -> None: await db.execute( "INSERT INTO ai_cache (key, content, created_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET content=excluded.content, created_at=excluded.created_at", (key, content, utc_now_iso()), ) await db.commit() async def _call_openrouter(prompt: str, *, system: str) -> str | None: if not settings.openrouter_api_key: return None try: async with httpx.AsyncClient(timeout=30.0) as client: resp = await client.post( "https://openrouter.ai/api/v1/chat/completions", headers={ "Authorization": f"Bearer {settings.openrouter_api_key}", "HTTP-Referer": "https://medis.schwenk.online", "X-Title": "TakeYourMeds", }, json={ "model": settings.openrouter_model, "messages": [ {"role": "system", "content": system}, {"role": "user", "content": prompt}, ], "max_tokens": 120, }, ) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"].strip() except Exception: logger.exception("OpenRouter request failed") return None def _motivation_key(day: str) -> str: return f"motivation:{day}" async def _cached_motivation(db: aiosqlite.Connection, day: str) -> str | None: cached = await _get_cache(db, _motivation_key(day)) if cached: return cached return await _get_cache(db, f"roast:{day}") async def generate_daily_motivation( db: aiosqlite.Connection, tz: ZoneInfo, day: str | None = None, ) -> dict[str, Any]: day = day or datetime.now(tz).strftime("%Y-%m-%d") key = _motivation_key(day) cached = await _cached_motivation(db, day) if cached: return {"text": cached, "source": "cache", "day": day} stats = await compute_stats(db) prompt = ( "Schreibe den Motivationsspruch für heute. " f"Streak: {stats['streak']} Tage. " f"Compliance über {stats['compliance_window_days']} aktive Tage: {stats['compliance_percent']}%. " "Beispiel-Stil: Dein Gehirn ist nicht kaputt. Es läuft nur auf einem anderen Betriebssystem — ohne Support." ) text = await _call_openrouter(prompt, system=MOTIVATION_SYSTEM) if not text: text = pick("motivation_fallback") source = "fallback" else: source = "ai" await _set_cache(db, key, text) logger.info("Generated daily motivation for %s (%s)", day, source) return {"text": text, "source": source, "day": day} async def get_daily_motivation(db: aiosqlite.Connection, tz: ZoneInfo) -> dict[str, Any]: day = datetime.now(tz).strftime("%Y-%m-%d") cached = await _cached_motivation(db, day) if cached: return {"text": cached, "source": "cache", "day": day} return {"text": pick("motivation_fallback"), "source": "fallback", "day": day} async def get_roast_of_the_day(db: aiosqlite.Connection, tz: ZoneInfo) -> dict[str, Any]: return await get_daily_motivation(db, tz) async def get_oracle(db: aiosqlite.Connection, tz: ZoneInfo) -> dict[str, Any]: week = datetime.now(tz).strftime("%Y-W%W") key = f"oracle:{week}" cached = await _get_cache(db, key) if cached: return {"text": cached, "source": "cache", "week": week} stats = await compute_stats(db) days_active = stats["days_active"] window = stats["compliance_window_days"] young_app = days_active < 90 context = ( f"App aktiv seit {days_active} Tag(en), erste Einnahme am {stats['first_day']}. " if young_app else "" ) caveat = ( "Die App ist noch jung — lange Streaks oder 90-Tage-Compliance sind noch nicht realistisch erreichbar. " "Bewerte nur die verfügbaren Daten, sei fair aber sarkastisch. " if young_app else "" ) prompt = ( f"Wöchentlicher Orakel-Report. {context}{caveat}" f"Streak: {stats['streak']} Tage. " f"Compliance über {window} Tag(e): {stats['compliance_percent']}%. " f"Genommen gesamt: {stats['total_taken']}." ) text = await _call_openrouter(prompt, system=ORACLE_SYSTEM) if not text: if young_app: text = ( f"Tag {days_active} deiner Medis-Karriere. " f"Streak: {stats['streak']}. Compliance ({window} Tage): {stats['compliance_percent']}%. " "Das Orakel ist beeindruckt — oder gelangweilt. Grenzwertig." ) else: text = pick("streak", streak=stats["streak"]) + f" Compliance: {stats['compliance_percent']}%." source = "fallback" else: source = "ai" await _set_cache(db, key, text) return {"text": text, "source": source, "week": week}