KI-Tracking vollständig, Cloud-Limit 20/Woche, Statusmail täglich 06:00 — SW by-v434, APP_VER 413
- ki.complete() zählt sich selbst (user_id-Parameter, _track_usage) - CLOUD_WEEKLY_LIMIT=20, geprüft vor jedem Cloud-Call - user_id durchgereicht in health, diary, knigge, notes, ki-route - Admin-Panel: 7-Tage-Ansicht, Limit-Info, Top-Cloud-User-Tabelle - Statusmail täglich 06:00 CEST statt alle 2h
This commit is contained in:
parent
85836e4e6e
commit
570dcd4e93
11 changed files with 135 additions and 78 deletions
103
backend/ki.py
103
backend/ki.py
|
|
@ -22,11 +22,12 @@ from functools import wraps
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
KI_MODE = os.getenv("KI_MODE", "local") # off | local | cloud
|
||||
LOCAL_BASE_URL = os.getenv("KI_LOCAL_URL", "http://10.47.11.70:11435/v1")
|
||||
LOCAL_MODEL = os.getenv("KI_LOCAL_MODEL", "gemma-4-31b-it")
|
||||
CLOUD_MODEL = os.getenv("KI_CLOUD_MODEL", "claude-sonnet-4-6")
|
||||
ANTHROPIC_KEY = os.getenv("ANTHROPIC_API_KEY", "")
|
||||
KI_MODE = os.getenv("KI_MODE", "local") # off | local | cloud
|
||||
LOCAL_BASE_URL = os.getenv("KI_LOCAL_URL", "http://10.47.11.70:11435/v1")
|
||||
LOCAL_MODEL = os.getenv("KI_LOCAL_MODEL", "gemma-4-31b-it")
|
||||
CLOUD_MODEL = os.getenv("KI_CLOUD_MODEL", "claude-sonnet-4-6")
|
||||
ANTHROPIC_KEY = os.getenv("ANTHROPIC_API_KEY", "")
|
||||
CLOUD_WEEKLY_LIMIT = int(os.getenv("KI_CLOUD_WEEKLY_LIMIT", "20"))
|
||||
|
||||
# Lazy Imports — nur laden wenn wirklich benötigt
|
||||
_openai_client = None
|
||||
|
|
@ -62,6 +63,50 @@ class KIPremiumRequired(Exception):
|
|||
# ------------------------------------------------------------------
|
||||
# Haupt-Aufruf-Funktion — zentraler Eingang für alle KI-Requests
|
||||
# ------------------------------------------------------------------
|
||||
def _track_usage(user_id: int | None, source: str) -> None:
|
||||
"""Schreibt einen KI-Aufruf in ki_daily_calls (fire-and-forget, swallows errors)."""
|
||||
if user_id is None:
|
||||
return
|
||||
try:
|
||||
from database import db
|
||||
from datetime import date
|
||||
today = date.today().isoformat()
|
||||
with db() as conn:
|
||||
conn.execute(
|
||||
"""INSERT INTO ki_daily_calls (user_id, date, count, source)
|
||||
VALUES (?, ?, 1, ?)
|
||||
ON CONFLICT(user_id, date, source)
|
||||
DO UPDATE SET count = count + 1""",
|
||||
(user_id, today, source)
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(f"KI-Tracking fehlgeschlagen: {exc}")
|
||||
|
||||
|
||||
def _check_weekly_cloud_limit(user_id: int | None) -> None:
|
||||
"""Wirft KIPremiumRequired wenn user_id das wöchentliche Cloud-Limit erreicht hat."""
|
||||
if user_id is None or CLOUD_WEEKLY_LIMIT <= 0:
|
||||
return
|
||||
try:
|
||||
from database import db
|
||||
with db() as conn:
|
||||
used = conn.execute(
|
||||
"""SELECT COALESCE(SUM(count), 0) FROM ki_daily_calls
|
||||
WHERE user_id=? AND source='cloud'
|
||||
AND date >= DATE('now', '-6 days')""",
|
||||
(user_id,)
|
||||
).fetchone()[0]
|
||||
if used >= CLOUD_WEEKLY_LIMIT:
|
||||
raise KIPremiumRequired(
|
||||
f"Wöchentliches KI-Limit erreicht ({CLOUD_WEEKLY_LIMIT} Cloud-Anfragen/Woche). "
|
||||
"Morgen wieder verfügbar."
|
||||
)
|
||||
except KIPremiumRequired:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.warning(f"KI-Limit-Check fehlgeschlagen: {exc}")
|
||||
|
||||
|
||||
async def complete(
|
||||
prompt: str,
|
||||
system: str = "Du bist ein hilfreicher Assistent für Hundebesitzer.",
|
||||
|
|
@ -71,55 +116,43 @@ async def complete(
|
|||
json_mode: bool = False,
|
||||
return_model: bool = False,
|
||||
return_source: bool = False,
|
||||
user_id: int | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
KI-Completion. Wählt automatisch den richtigen Backend.
|
||||
|
||||
Args:
|
||||
prompt: User-Nachricht
|
||||
system: System-Prompt
|
||||
max_tokens: Maximale Antwortlänge
|
||||
requires_premium: True = nur für Premium-User (nutzt Cloud)
|
||||
user_is_premium: Ob der anfragende User Premium hat
|
||||
json_mode: Antwort als JSON anfordern
|
||||
return_source: Falls True: gibt (text, source) zurück, source = 'cloud'|'local'
|
||||
|
||||
Returns:
|
||||
KI-Antwort als String, oder (str, str) wenn return_source=True
|
||||
|
||||
Raises:
|
||||
KIPremiumRequired: Cloud-Feature ohne Premium
|
||||
KIUnavailableError: KI komplett deaktiviert
|
||||
Zählt jeden Aufruf in ki_daily_calls wenn user_id übergeben wird.
|
||||
Prüft wöchentliches Cloud-Limit (CLOUD_WEEKLY_LIMIT) für Cloud-Aufrufe.
|
||||
"""
|
||||
if KI_MODE == "off":
|
||||
raise KIUnavailableError("KI ist deaktiviert.")
|
||||
|
||||
# Premium-Check vor Cloud-Aufrufen
|
||||
if requires_premium and not user_is_premium:
|
||||
raise KIPremiumRequired(
|
||||
"Dieses Feature ist Teil von Ban Yaro Premium."
|
||||
)
|
||||
raise KIPremiumRequired("Dieses Feature ist Teil von Ban Yaro Premium.")
|
||||
|
||||
# Cloud-Aufruf: nur wenn Premium UND cloud-Modus
|
||||
# Cloud-Aufruf: Premium UND cloud-Modus
|
||||
if requires_premium and user_is_premium and KI_MODE == "cloud":
|
||||
_check_weekly_cloud_limit(user_id)
|
||||
text = await _cloud_complete(prompt, system, max_tokens, json_mode)
|
||||
_track_usage(user_id, "cloud")
|
||||
if return_model:
|
||||
return (text, CLOUD_MODEL)
|
||||
return (text, "cloud") if return_source else text
|
||||
|
||||
# Lokaler Aufruf: Entwicklung + Free-User
|
||||
# Lokaler Aufruf + Cloud-Fallback
|
||||
if KI_MODE in ("local", "cloud"):
|
||||
try:
|
||||
text = await _local_complete(prompt, system, max_tokens, json_mode)
|
||||
_track_usage(user_id, "local")
|
||||
if return_model:
|
||||
return (text, LOCAL_MODEL)
|
||||
return (text, "local") if return_source else text
|
||||
except Exception as e:
|
||||
logger.warning(f"Lokales KI-Modell nicht erreichbar: {e}")
|
||||
# Cloud-Fallback: im cloud-Modus immer, sonst nur für Premium-User
|
||||
if ANTHROPIC_KEY and (KI_MODE == "cloud" or (requires_premium and user_is_premium)):
|
||||
logger.info("Fallback auf Cloud-KI.")
|
||||
_check_weekly_cloud_limit(user_id)
|
||||
text = await _cloud_complete(prompt, system, max_tokens, json_mode)
|
||||
_track_usage(user_id, "cloud")
|
||||
if return_model:
|
||||
return (text, CLOUD_MODEL)
|
||||
return (text, "cloud") if return_source else text
|
||||
|
|
@ -188,6 +221,7 @@ async def _cloud_complete(
|
|||
# ------------------------------------------------------------------
|
||||
|
||||
async def symptom_check(symptoms: str, dog_info: dict,
|
||||
user_id: int | None = None,
|
||||
user_is_premium: bool = False) -> dict:
|
||||
"""
|
||||
Symptom-Triage für Hunde.
|
||||
|
|
@ -216,9 +250,10 @@ Antworte NUR als JSON:
|
|||
result = await complete(
|
||||
prompt, system,
|
||||
max_tokens=400,
|
||||
requires_premium=False, # Basis-Triage ist kostenlos
|
||||
requires_premium=False,
|
||||
user_is_premium=user_is_premium,
|
||||
json_mode=True,
|
||||
user_id=user_id,
|
||||
)
|
||||
import json, re
|
||||
# Cloud-Modelle wrappen JSON manchmal in ```json … ``` — stripppen
|
||||
|
|
@ -258,7 +293,7 @@ Strukturiert, konkret, motivierend.
|
|||
)
|
||||
|
||||
|
||||
async def diary_tags(entry_text: str) -> list[str]:
|
||||
async def diary_tags(entry_text: str, user_id: int | None = None) -> list[str]:
|
||||
"""
|
||||
Automatische Tags für Tagebucheinträge — läuft lokal, kostenlos.
|
||||
"""
|
||||
|
|
@ -274,7 +309,7 @@ reise, meilenstein, lustig, traurig, wetter, sonstige
|
|||
Antworte NUR mit den Tags, kommagetrennt, keine Erklärung.
|
||||
"""
|
||||
try:
|
||||
result = await complete(prompt, max_tokens=50, requires_premium=False)
|
||||
result = await complete(prompt, max_tokens=50, requires_premium=False, user_id=user_id)
|
||||
return [t.strip().lower() for t in result.split(",") if t.strip()]
|
||||
except Exception:
|
||||
return []
|
||||
|
|
@ -305,7 +340,8 @@ Bewerte als JSON:
|
|||
|
||||
|
||||
async def health_summary(health_data: list, dog_info: dict,
|
||||
user_is_premium: bool = False) -> str:
|
||||
user_is_premium: bool = False,
|
||||
user_id: int | None = None) -> str:
|
||||
"""
|
||||
Tierärztliche Zusammenfassung aller Gesundheitsdaten — lokal für alle.
|
||||
Fasst Impfungen, Besuche, Medikamente etc. in einem lesbaren Bericht zusammen.
|
||||
|
|
@ -364,8 +400,9 @@ Schreibe klar und verständlich für den Hundebesitzer.
|
|||
return await complete(
|
||||
prompt, system,
|
||||
max_tokens=700,
|
||||
requires_premium=False, # Kostenlos für alle
|
||||
requires_premium=False,
|
||||
user_is_premium=user_is_premium,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue