feat: Enhance sleep module with CSV import functionality and date parsing improvements
This commit is contained in:
parent
24f60c0a6d
commit
00437a92ab
|
|
@ -9,7 +9,8 @@ Endpoints:
|
|||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||||
from pydantic import BaseModel
|
||||
from typing import Literal
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal, InvalidOperation
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
|
|
@ -19,6 +20,174 @@ from db import get_db, get_cursor
|
|||
|
||||
router = APIRouter(prefix="/api/sleep", tags=["sleep"])
|
||||
|
||||
|
||||
def _strip_row_keys(row: dict) -> dict:
|
||||
return {(k or "").strip(): (v.strip() if isinstance(v, str) else v) for k, v in row.items()}
|
||||
|
||||
|
||||
def _safe_float(value) -> float | None:
|
||||
if value is None or value == "":
|
||||
return None
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
if isinstance(value, Decimal):
|
||||
return float(value)
|
||||
try:
|
||||
s = str(value).strip().replace(",", ".")
|
||||
return float(s)
|
||||
except (ValueError, InvalidOperation):
|
||||
return None
|
||||
|
||||
|
||||
def _parse_apple_sleep_datetime(value: str) -> datetime:
|
||||
raw = (value or "").strip()
|
||||
if not raw:
|
||||
raise ValueError("empty datetime")
|
||||
fmts = (
|
||||
"%Y-%m-%d %H:%M:%S %z",
|
||||
"%d.%m.%y %H:%M:%S",
|
||||
"%d.%m.%Y %H:%M:%S",
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
last_err = None
|
||||
for fmt in fmts:
|
||||
try:
|
||||
return datetime.strptime(raw, fmt)
|
||||
except ValueError as e:
|
||||
last_err = e
|
||||
raise ValueError(f"Unbekanntes Datumsformat: {raw!r}") from last_err
|
||||
|
||||
|
||||
def _hr_to_minutes(hours: float | None) -> int:
|
||||
if hours is None:
|
||||
return 0
|
||||
return int(round(float(hours) * 60))
|
||||
|
||||
|
||||
def _detect_apple_sleep_csv_format(fieldnames: list[str] | None) -> Literal["segments", "summary"]:
|
||||
if not fieldnames:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="CSV enthält keine Spaltenüberschriften.",
|
||||
)
|
||||
fn = {(f or "").strip() for f in fieldnames}
|
||||
if {"Start", "End", "Duration (hr)", "Value"}.issubset(fn):
|
||||
return "segments"
|
||||
if "Start" in fn and "End" in fn and "Total Sleep (hr)" in fn:
|
||||
return "summary"
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=(
|
||||
"Unbekanntes Apple-Health-Schlaf-CSV. "
|
||||
"Erwartet wird entweder der Segment-Export (Spalten Start, End, Duration (hr), Value) "
|
||||
"oder die Schlafanalyse mit Nacht-Zusammenfassung (u. a. Total Sleep (hr), Core/Light, Deep, REM)."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _build_nights_from_apple_summary(reader: csv.DictReader) -> dict[date, dict]:
|
||||
nights_dict: dict[date, dict] = {}
|
||||
for raw in reader:
|
||||
row = _strip_row_keys(raw)
|
||||
start_s = row.get("Start") or ""
|
||||
end_s = row.get("End") or ""
|
||||
if not start_s or not end_s:
|
||||
continue
|
||||
try:
|
||||
start_dt = _parse_apple_sleep_datetime(start_s)
|
||||
end_dt = _parse_apple_sleep_datetime(end_s)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
dt_key = (row.get("Date/Time") or row.get("Datum/Uhrzeit") or "").strip()
|
||||
if dt_key:
|
||||
try:
|
||||
wake_d = datetime.strptime(dt_key[:10], "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
wake_d = end_dt.date()
|
||||
else:
|
||||
wake_d = end_dt.date()
|
||||
|
||||
core_hr = _safe_float(row.get("Core (hr)"))
|
||||
if core_hr is None:
|
||||
core_hr = _safe_float(row.get("Light (hr)")) or 0.0
|
||||
deep_min = _hr_to_minutes(_safe_float(row.get("Deep (hr)")))
|
||||
rem_min = _hr_to_minutes(_safe_float(row.get("REM (hr)")))
|
||||
light_min = _hr_to_minutes(core_hr)
|
||||
awake_min = _hr_to_minutes(_safe_float(row.get("Awake (hr)")))
|
||||
nights_dict[wake_d] = {
|
||||
"bedtime": start_dt,
|
||||
"wake_time": end_dt,
|
||||
"segments": [],
|
||||
"deep_minutes": deep_min,
|
||||
"rem_minutes": rem_min,
|
||||
"light_minutes": light_min,
|
||||
"awake_minutes": awake_min,
|
||||
}
|
||||
return nights_dict
|
||||
|
||||
|
||||
def _build_nights_from_apple_segments(reader: csv.DictReader, phase_map: dict) -> dict[date, dict]:
|
||||
segments = []
|
||||
for raw in reader:
|
||||
row = _strip_row_keys(raw)
|
||||
phase_key = (row.get("Value") or "").strip()
|
||||
phase_en = phase_map.get(phase_key)
|
||||
if phase_en is None:
|
||||
continue
|
||||
try:
|
||||
start_dt = _parse_apple_sleep_datetime(row.get("Start") or "")
|
||||
end_dt = _parse_apple_sleep_datetime(row.get("End") or "")
|
||||
duration_hr = _safe_float(row.get("Duration (hr)"))
|
||||
if duration_hr is None:
|
||||
continue
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
duration_min = int(duration_hr * 60)
|
||||
segments.append({
|
||||
"start": start_dt,
|
||||
"end": end_dt,
|
||||
"duration_min": duration_min,
|
||||
"phase": phase_en,
|
||||
})
|
||||
|
||||
segments.sort(key=lambda s: s["start"])
|
||||
nights = []
|
||||
current_night = None
|
||||
|
||||
for seg in segments:
|
||||
if current_night is None or (seg["start"] - current_night["wake_time"]).total_seconds() > 7200:
|
||||
current_night = {
|
||||
"bedtime": seg["start"],
|
||||
"wake_time": seg["end"],
|
||||
"segments": [],
|
||||
"deep_minutes": 0,
|
||||
"rem_minutes": 0,
|
||||
"light_minutes": 0,
|
||||
"awake_minutes": 0,
|
||||
}
|
||||
nights.append(current_night)
|
||||
|
||||
current_night["segments"].append(seg)
|
||||
current_night["wake_time"] = max(current_night["wake_time"], seg["end"])
|
||||
current_night["bedtime"] = min(current_night["bedtime"], seg["start"])
|
||||
|
||||
if seg["phase"] == "deep":
|
||||
current_night["deep_minutes"] += seg["duration_min"]
|
||||
elif seg["phase"] == "rem":
|
||||
current_night["rem_minutes"] += seg["duration_min"]
|
||||
elif seg["phase"] == "light":
|
||||
current_night["light_minutes"] += seg["duration_min"]
|
||||
elif seg["phase"] == "awake":
|
||||
current_night["awake_minutes"] += seg["duration_min"]
|
||||
|
||||
nights_dict: dict[date, dict] = {}
|
||||
for night in nights:
|
||||
wake_date = night["wake_time"].date()
|
||||
nights_dict[wake_date] = night
|
||||
return nights_dict
|
||||
|
||||
|
||||
# ── Models ────────────────────────────────────────────────────────────────────
|
||||
|
||||
class SleepCreate(BaseModel):
|
||||
|
|
@ -460,96 +629,46 @@ async def import_apple_health_sleep(
|
|||
"""
|
||||
Import sleep data from Apple Health CSV export.
|
||||
|
||||
Expected CSV format:
|
||||
Start,End,Duration (hr),Value,Source
|
||||
2026-03-14 22:44:23,2026-03-14 23:00:19,0.266,Kern,Apple Watch
|
||||
Supports:
|
||||
- Segment-Export: Start, End, Duration (hr), Value (Kern/Tief/REM/Wach oder Core/Deep/…)
|
||||
- Schlafanalyse (Nacht-Zusammenfassung): Date/Time, Start, End, Total Sleep (hr),
|
||||
Core/Light (hr), Deep (hr), REM (hr), Awake (hr), …
|
||||
|
||||
- Aggregates segments by night (wake date)
|
||||
- Maps German phase names: Kern→light, REM→rem, Tief→deep, Wach→awake
|
||||
- Stores raw segments in JSONB
|
||||
- Does NOT overwrite manual entries (source='manual')
|
||||
- Aggregates segments by night (wake date) where applicable
|
||||
- Stores Roh-Segmente in JSONB (bei Zusammenfassung oft leer)
|
||||
- Überschreibt keine manuellen Einträge (source='manual')
|
||||
"""
|
||||
pid = session['profile_id']
|
||||
|
||||
# Read CSV
|
||||
content = await file.read()
|
||||
csv_text = content.decode('utf-8-sig') # Handle BOM
|
||||
csv_text = content.decode('utf-8-sig')
|
||||
reader = csv.DictReader(io.StringIO(csv_text))
|
||||
fmt = _detect_apple_sleep_csv_format(reader.fieldnames)
|
||||
|
||||
# Phase mapping (German → English)
|
||||
phase_map = {
|
||||
'Kern': 'light',
|
||||
'REM': 'rem',
|
||||
'Tief': 'deep',
|
||||
'Wach': 'awake',
|
||||
'Schlafend': None # Ignore initial sleep entry
|
||||
"Kern": "light",
|
||||
"Core": "light",
|
||||
"Light": "light",
|
||||
"REM": "rem",
|
||||
"Tief": "deep",
|
||||
"Deep": "deep",
|
||||
"Wach": "awake",
|
||||
"Awake": "awake",
|
||||
"Schlafend": None,
|
||||
"Asleep": None,
|
||||
"In Bed": None,
|
||||
}
|
||||
|
||||
# Parse segments
|
||||
segments = []
|
||||
for row in reader:
|
||||
phase_de = row['Value'].strip()
|
||||
phase_en = phase_map.get(phase_de)
|
||||
if fmt == "summary":
|
||||
nights_dict = _build_nights_from_apple_summary(reader)
|
||||
else:
|
||||
nights_dict = _build_nights_from_apple_segments(reader, phase_map)
|
||||
|
||||
if phase_en is None: # Skip "Schlafend"
|
||||
continue
|
||||
|
||||
start_dt = datetime.strptime(row['Start'], '%Y-%m-%d %H:%M:%S')
|
||||
end_dt = datetime.strptime(row['End'], '%Y-%m-%d %H:%M:%S')
|
||||
duration_hr = float(row['Duration (hr)'])
|
||||
duration_min = int(duration_hr * 60)
|
||||
|
||||
segments.append({
|
||||
'start': start_dt,
|
||||
'end': end_dt,
|
||||
'duration_min': duration_min,
|
||||
'phase': phase_en
|
||||
})
|
||||
|
||||
# Sort segments chronologically
|
||||
segments.sort(key=lambda s: s['start'])
|
||||
|
||||
# Group segments into nights (gap-based)
|
||||
# If gap between segments > 2 hours → new night
|
||||
nights = []
|
||||
current_night = None
|
||||
|
||||
for seg in segments:
|
||||
# Start new night if:
|
||||
# 1. First segment
|
||||
# 2. Gap > 2 hours since last segment
|
||||
if current_night is None or (seg['start'] - current_night['wake_time']).total_seconds() > 7200:
|
||||
current_night = {
|
||||
'bedtime': seg['start'],
|
||||
'wake_time': seg['end'],
|
||||
'segments': [],
|
||||
'deep_minutes': 0,
|
||||
'rem_minutes': 0,
|
||||
'light_minutes': 0,
|
||||
'awake_minutes': 0
|
||||
}
|
||||
nights.append(current_night)
|
||||
|
||||
# Add segment to current night
|
||||
current_night['segments'].append(seg)
|
||||
current_night['wake_time'] = max(current_night['wake_time'], seg['end'])
|
||||
current_night['bedtime'] = min(current_night['bedtime'], seg['start'])
|
||||
|
||||
# Sum phases
|
||||
if seg['phase'] == 'deep':
|
||||
current_night['deep_minutes'] += seg['duration_min']
|
||||
elif seg['phase'] == 'rem':
|
||||
current_night['rem_minutes'] += seg['duration_min']
|
||||
elif seg['phase'] == 'light':
|
||||
current_night['light_minutes'] += seg['duration_min']
|
||||
elif seg['phase'] == 'awake':
|
||||
current_night['awake_minutes'] += seg['duration_min']
|
||||
|
||||
# Convert nights list to dict with wake_date as key
|
||||
nights_dict = {}
|
||||
for night in nights:
|
||||
wake_date = night['wake_time'].date() # Date when you woke up
|
||||
nights_dict[wake_date] = night
|
||||
if not nights_dict:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Keine importierbaren Schlafzeilen gefunden (prüfe Start/Ende und Format).",
|
||||
)
|
||||
|
||||
# Insert nights
|
||||
imported = 0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
# Umsetzungsplan: Sportspezifisch auswertbare Trainingsprofile
|
||||
|
||||
**Status:** Konzept für fachliche Abstimmung / Konzeptagent
|
||||
**Datum:** 2026-04-05
|
||||
**Bezug:** Fachliches Teilkonzept „Sportspezifisch auswertbare Trainingsprofile“ (User-Entwurf)
|
||||
**Hinweis:** Keine Implementierung ohne Freigabe der unter [Offene Fragen](#offene-fragen-für-den-fachlichen-konzeptagenten) gekläerten Punkte.
|
||||
|
||||
---
|
||||
|
||||
## 1. Kurzfazit: Machbarkeit
|
||||
|
||||
**Fachlich und technisch grundsätzlich machbar** als gestaffelte Erweiterung auf dem bestehenden System:
|
||||
|
||||
- `training_types.profile` (JSONB)
|
||||
- `TrainingProfileEvaluator` (`backend/profile_evaluator.py`)
|
||||
- Persistenz: `activity_log.evaluation`, `quality_label`, `overall_score`
|
||||
|
||||
**Engpass:** Viele im Teilkonzept genannten Kriterien (z. B. Muskelgruppen, Nähe zum muskulären Versagen, detaillierte Zonenzeiten) **lassen sich aus der aktuellen `activity_log`-Datenbasis nicht zuverlässig ableiten**. Dafür braucht es zusätzliche Erfassung, Importfelder oder explizite „niedrige Evidenz / unbekannt“-Markierung in Metriken und KI-Kontext (Placeholder-Registry, Erklärbarkeit).
|
||||
|
||||
---
|
||||
|
||||
## 2. Ist-Analyse (Abhängigkeiten)
|
||||
|
||||
### 2.1 Daten & Konfiguration
|
||||
|
||||
| Baustein | Rolle |
|
||||
|---------|--------|
|
||||
| `training_types` | `category`, `subcategory`, `name_*`, optional `abilities` (JSONB), `profile` (JSONB) |
|
||||
| `activity_type_mappings` | Import/manuelles Mapping `activity_type` → `training_type_id` (unverändert lassen) |
|
||||
| `training_parameters` | Registry messbarer Parameter für Regeln (`source_field` → `activity_log`) |
|
||||
| `activity_log` | u. a. `training_type_id`, `evaluation`, `quality_label`, `overall_score` |
|
||||
|
||||
### 2.2 Bewertung heute
|
||||
|
||||
- Der Evaluator läuft über mehrere **rule_sets** (Minimumanforderungen, Zonen, Effekte, Periodisierung, KPI, Safety).
|
||||
- **Ein** zusammengefasster `overall_score` und **ein** `quality_label` (`excellent` | `good` | `acceptable` | `poor`).
|
||||
- **Relevanz, intrinsische Qualität, Zielbezug, Platzierung** sind im Ergebnis **nicht formal getrennt**; Periodisierung/Performance sind teils **MVP/vereinfacht**.
|
||||
|
||||
### 2.3 Downstream (nicht brechen)
|
||||
|
||||
- **Issue #31 / Quality Filter:** `profiles.quality_filter_level`, `quality_filter.py` (SQL-Fragmente nach `quality_label`)
|
||||
- **Frontend:** Badges in Aktivitäts-UI, History-Hinweise
|
||||
- **Data Layer / Charts:** u. a. `calculate_quality_sessions_pct`, `activity_score`, Korrelationen
|
||||
- **Placeholder Registry:** u. a. `quality_sessions_pct` (bekannte Semantik-/Label-Diskrepanz, siehe unten)
|
||||
|
||||
### 2.4 Bekannte technische Inkonsistenz (Bestand)
|
||||
|
||||
`calculate_quality_sessions_pct` filtert u. a. auf `quality_label IN (..., 'very_good', ...)`, der `TrainingProfileEvaluator` setzt **`very_good` nicht**. Das sollte **vor oder zusammen mit** größeren Profil-Erweiterungen bereinigt werden, damit neue Dimensionen nicht auf wackeliger Basis validiert werden.
|
||||
|
||||
---
|
||||
|
||||
## 3. Umsetzungsprinzipien (Kompatibilität)
|
||||
|
||||
1. **Kein Big-Bang:** Consumer erwarten weiterhin sinnvolle `quality_label` / `overall_score`, es sei denn, es gibt eine **versionierte Cutover-Strategie**.
|
||||
2. **Additive JSON-Struktur** in `evaluation` (z. B. mehrdimensionale Teilergebnisse), ohne bestehende Schlüssel willkürlich zu entfernen.
|
||||
3. **Legacy-Vertrag festlegen:** z. B. `overall_score` = **primär intrinsische** Qualität (oder explizit „Legacy-Komposit“ mit `schema_version`).
|
||||
4. **`training_types` bleibt Single Source** für Typ + Profil; Mapping-Import-Logik nicht „mitziehen“, außer es ist fachlich nötig.
|
||||
5. **Neue Coach-Metriken:** über Placeholder Registry, mit Evidence-Tags; keine doppelte Wahrheit neben der Registry.
|
||||
6. **KI:** `ai_context` / `interpretation_boundary` im Profil ausbaubar machen (was erlaubt/verboten ist, bei Datenlücken).
|
||||
|
||||
---
|
||||
|
||||
## 4. Phasenplan
|
||||
|
||||
### Phase 0 – Begriffe, Verträge, Bestandsaufnahme
|
||||
|
||||
- Glossar: Relevanz, intrinsische Qualität, Zielbeitrag, Platzierung, Belastung, sportspezifische Qualität → **Mapping auf bestehende `rule_sets`**.
|
||||
- Vollständige Liste: wer liest `quality_label` / `evaluation` / `overall_score` (APIs, SQL, Frontend).
|
||||
- Align: `very_good` vs. Evaluator-Labels; optional: Rolle von `acceptable` in „Qualitätssessions“ vs. globalem Filter **fachlich** klären.
|
||||
|
||||
### Phase 1 – Metamodell im Profil-JSON
|
||||
|
||||
- Erweiterung `training_types.profile`: z. B. `sport_logic`, `dimensions_config`, `goal_linkage`, `load_character`, `recovery_character`, `interference`, `ai_interpretation`.
|
||||
- Evaluator liefert **Teilscores + Begründungen** pro Dimension; **Legacy-Felder** ableitbar und dokumentiert.
|
||||
|
||||
### Phase 2 – Zielbezug
|
||||
|
||||
- Input: aktive Ziele, `goal_mode`, Focus-Gewichte (`user_focus_area_weights`).
|
||||
- Ausgabe: eigene Dimension **`goal_fit`**, ohne intrinsische Bewertung zu überschreiben.
|
||||
|
||||
### Phase 3 – Wochenkontext / Platzierung / Interferenz
|
||||
|
||||
- Kontext in `load_evaluation_context` erweitern (Reihenfolge, Vortag, Ruhetage, optional Vitals/Schlaf).
|
||||
- Regeln schrittweise; False-Positive-Risiko beachten.
|
||||
|
||||
### Phase 4 – Sportspezifische Templates
|
||||
|
||||
- Profilvorlagen pro `sport_logic` (analog `profile_templates.py`), unterschiedliche Parameter und Schwellen.
|
||||
- `confidence` / `data_sufficiency` pro Dimension.
|
||||
|
||||
### Phase 5 – Coach-Metriken & Placeholder
|
||||
|
||||
- Neue Platzhalter nur bei klarer Semantik; ggf. `quality_sessions_pct` **umschreiben** oder durch neuen Key ersetzen + Deprecation-Plan.
|
||||
|
||||
### Phase 6 – Frontend / Admin
|
||||
|
||||
- Dimensionen sichtbar machen; Badges können vorerst Legacy-Score anzeigen.
|
||||
|
||||
---
|
||||
|
||||
## 5. Risiken (Kurz)
|
||||
|
||||
| Risiko | Maßnahme |
|
||||
|--------|----------|
|
||||
| Bedeutung von `quality_label` wechselt still | `evaluation.schema_version`, dokumentierter Vertrag, ggf. separates `intrinsic_quality_label` nach Cutover |
|
||||
| Charts/Scores springen | Shadow-Vergleich, gestuftes Rollout |
|
||||
| KI übertreibt bei Datenlücken | `ai_interpretation` + Metadata-Evidence |
|
||||
| Registry-Drift | Alle neuen Kennzahlen registrieren |
|
||||
|
||||
---
|
||||
|
||||
## Offene Fragen für den fachlichen Konzeptagenten
|
||||
|
||||
Die folgenden Blöcke sind bewusst als Markdown-Codeblöcke formatiert, damit sie **unverändert** in Prompts oder Tickets übernommen werden können.
|
||||
|
||||
### Block A – Priorität & Konflikte zwischen Dimensionen
|
||||
|
||||
```markdown
|
||||
Wenn sich Dimensionen widersprechen (z. B. intrinsische Qualität „hoch“, Zielpassung „niedrig“, Platzierung „problematisch“):
|
||||
|
||||
1. Welche Dimension soll für die **primäre Nutzer-/Coach-Aussage** in der UI führend sein?
|
||||
2. Gibt es eine **feste Prioritätsreihenfolge** (z. B. Sicherheit > Platzierung > Zielpassung > intrinsische Qualität) oder ist sie kontextabhängig?
|
||||
3. Soll es eine **explizite „Gesamt-Coach-Empfehlung“** geben, die aus den Dimensionen kombiniert wird, oder nur parallele Teilurteile ohne Gesamtnote?
|
||||
```
|
||||
|
||||
### Block B – Minimaldatensatz & Sportlogiken
|
||||
|
||||
```markdown
|
||||
Pro Trainingsfamilie / „sport_logic“ (Kraft, Cardio, Schnellkraft, Kampfsport, Mobility, Erholung aktiv, Geist & Meditation):
|
||||
|
||||
1. Welche **messbaren Felder** sind minimal nötig, damit eine sportspezifische Bewertung **fachlich vertretbar** ist (nicht nur „nice to have“)?
|
||||
2. Welche Aussagen dürfen **ohne** diese Felder **nicht** getroffen werden (nur „unbekannt“ / Confidence niedrig)?
|
||||
3. Sollen fehlende Daten die **intrinsische** Bewertung hart begrenzen (Cap), oder nur **Zusatzdimensionen** betreffen?
|
||||
```
|
||||
|
||||
### Block C – Einzeltraining vs. Wochenkontext
|
||||
|
||||
```markdown
|
||||
Platzierung / Kontextqualität:
|
||||
|
||||
1. Darf schlechte Platzierung die **sichtbare Einheit-Qualität** numerisch senken, oder nur **Flags/Hinweise** erzeugen?
|
||||
2. Welche zeitliche Auflösung ist Pflicht: nur **Datum** oder auch **start_time** / Reihenfolge am Tag?
|
||||
3. Wie gehen wir mit **unvollständiger Historie** um (Importlücken, manuell gelöschte Einträge)?
|
||||
```
|
||||
|
||||
### Block D – Ziele & Multi-Ziel
|
||||
|
||||
```markdown
|
||||
Zielbezug (goal_fit):
|
||||
|
||||
1. Bewertung strikt gegen **ein Primärziel**, oder gewichtet gegen **alle aktiven Ziele**?
|
||||
2. Wie werden **Focus Areas** vs. **konkrete Goals** priorisiert, wenn sie sich widersprechen?
|
||||
3. Soll „behindert das Ziel“ (Teilkonzept) ein **eigener Score** oder nur eine **kategorische** Aussage sein?
|
||||
```
|
||||
|
||||
### Block E – KI & Haftung / Grenzen
|
||||
|
||||
```markdown
|
||||
KI-Interpretationsrahmen (pro Profil oder global):
|
||||
|
||||
1. Welche Formulierungen sind **verboten** (medizinische Diagnosen, absolute Leistungsversprechen, …)?
|
||||
2. Muss jede KI-gestützte Aussage einen **Evidenz-Hinweis** tragen („basierend auf Dauer/HF“, „Muskelgruppen nicht erfasst“)?
|
||||
3. Soll der Nutzer **opt-in** für interpretative Texte jenseits der Regeln haben?
|
||||
```
|
||||
|
||||
### Block F – Legacy-Metriken & Filter
|
||||
|
||||
```markdown
|
||||
Bestehende Kennzahlen:
|
||||
|
||||
1. Soll `quality_label` / `overall_score` dauerhaft **nur intrinsisch** bedeuten, oder ein **Komposit** aus mehreren Dimensionen?
|
||||
2. Wie sollen `quality_sessions_pct` und der globale **Quality Filter** künftig mit mehrdimensionalen Urteilen umgehen (nur intrinsisch filtern vs. Kombination)?
|
||||
3. Ist `acceptable` fachlich eine „Qualitätssession“ oder nur „formal ok“?
|
||||
```
|
||||
|
||||
### Block G – Abnahme / Akzeptanz
|
||||
|
||||
```markdown
|
||||
Abnahme:
|
||||
|
||||
1. Welche **messbaren** Abnahmekriterien gelten pro Phase (z. B. % konsistent bewerteter Sessions, keine Regression bei Filter-Charts)?
|
||||
2. Wer **fachlich** sign-off: Rolle „Domain Owner“ vs. Entwicklung?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anhang: Referenz im Repository
|
||||
|
||||
| Thema | Pfad (orientierend) |
|
||||
|-------|---------------------|
|
||||
| Evaluator | `backend/profile_evaluator.py` |
|
||||
| Regeln | `backend/rule_engine.py` |
|
||||
| Auswertung + Kontext | `backend/evaluation_helper.py` |
|
||||
| Profile JSON / Migration | `backend/migrations/014_training_profiles.sql`, Templates `backend/profile_templates.py` |
|
||||
| Quality Filter | `backend/quality_filter.py` |
|
||||
| Aktivitäts-API inkl. Eval | `backend/routers/activity.py` |
|
||||
| Quality-Sessions-Metrik | `backend/data_layer/activity_metrics.py` (`calculate_quality_sessions_pct`) |
|
||||
| Placeholder Registry | `backend/placeholder_registrations/activity_metrics.py` |
|
||||
|
||||
---
|
||||
|
||||
*Ende des Dokuments.*
|
||||
Loading…
Reference in New Issue
Block a user