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." ) 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: logger.warning("OPENROUTER_API_KEY not set") 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": 512, }, ) resp.raise_for_status() data = resp.json() if data.get("error"): logger.error("OpenRouter API error (model=%s): %s", settings.openrouter_model, data["error"]) return None choices = data.get("choices") or [] if not choices: logger.error("OpenRouter returned no choices (model=%s): %s", settings.openrouter_model, data) return None message = choices[0].get("message") or {} content = message.get("content") if not content or not str(content).strip(): logger.error( "OpenRouter empty content (model=%s, finish_reason=%s): %s", settings.openrouter_model, choices[0].get("finish_reason"), data, ) return None return str(content).strip() except Exception: logger.exception("OpenRouter request failed (model=%s)", settings.openrouter_model) return None def _motivation_key(day: str) -> str: return f"motivation:v2:{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, *, force: bool = False, ) -> dict[str, Any]: day = day or datetime.now(tz).strftime("%Y-%m-%d") key = _motivation_key(day) if not force: 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)