From bc8e9fb7fa838539495db0dbb72540b4a71830b1 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 17 Apr 2026 20:42:11 +0200 Subject: [PATCH] feat: Enhance training parameters handling and documentation - Introduced new fields for descriptions in training parameters, improving clarity for AI context in `training_sessions_recent_json`. - Added a glossary placeholder `{{training_parameters_glossary_md}}` to provide a Markdown table of active training parameters, including names and descriptions. - Updated the `placeholder_resolver.py` and `activity_metrics.py` to support the new glossary functionality. - Enhanced the `AdminActivityAttributeProfilesPage` to allow input for descriptions in both German and English, ensuring better context for metrics. - Revised tests to validate the inclusion of description fields in parameter schema merges and metrics handling. --- ...CTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md | 3 + CLAUDE.md | 2 +- backend/data_layer/activity_metrics.py | 30 ++++++++++ .../data_layer/activity_session_metrics.py | 40 +++++++++++-- .../activity_session_insights.py | 60 ++++++++++++++++++- backend/placeholder_resolver.py | 45 +++++++++++++- .../tests/test_activity_session_metrics.py | 46 ++++++++++++++ .../AdminActivityAttributeProfilesPage.jsx | 43 +++++++++++++ 8 files changed, 262 insertions(+), 7 deletions(-) diff --git a/.claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md b/.claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md index 4aacfbd..a516e2b 100644 --- a/.claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md +++ b/.claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md @@ -57,6 +57,8 @@ - Platzhalter / Charts, die Session-Details brauchen: **nur** diese Layer-1-Helfer erweitern oder aufrufen (z. B. `activity_metrics.get_training_sessions_recent_weeks_data` nutzt `enrich_sessions_with_metrics`). - Router: `get_db`, `get_cursor`, Auth; Business-Validierung delegieren an `activity_session_metrics`. +**KI-Kontext:** In `training_sessions_recent_json` enthält jedes Element von `session_metrics` neben `key`/`value` die Felder `name_de`, `name_en`, `description_de`, `description_en` (aus dem effektiven Schema). Für nicht selbsterklärende Keys soll im Katalog `training_parameters.description_*` gepflegt werden (Admin). Ergänzend liefert der Platzhalter `{{training_parameters_glossary_md}}` die gesamte aktive Parameter-Legende als Markdown-Tabelle (`get_training_parameters_ki_glossary_data` → `get_training_parameters_glossary_md`). + --- ## 4. API (Ist / geplant) @@ -137,6 +139,7 @@ Abdeckung: reine Merge-Logik (`merge_parameter_schema_rows`), Validierung (`_val - Migration 004/014: `training_types`, `activity_log`-Erweiterungen - Pattern Admin-Katalog: `routers/admin_reference_value_types.py` - Platzhalter Session-JSON: `data_layer/activity_metrics.py` → `get_training_sessions_recent_weeks_data` +- KI-Legende: `get_training_parameters_ki_glossary_data`, Platzhalter `{{training_parameters_glossary_md}}` --- diff --git a/CLAUDE.md b/CLAUDE.md index be58c60..7b3291c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -120,7 +120,7 @@ frontend/src/ - **Agent-Guide:** `.claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md` (Prod: nur additive Migration **054**; Layer1 `data_layer/activity_session_metrics.py`). - **DB:** `training_category_parameter`, `training_type_parameter`, `activity_session_metrics`; `activity_log.started_at` / `ended_at` (nullable). -- **API:** Admin `/api/admin/training-parameters`, `/api/admin/training-category-parameters`, `/api/admin/training-type-parameters`; Nutzer `GET /api/activity/{id}`, `PUT /api/activity/{id}/metrics`; Platzhalter-Pfad `training_sessions_recent_json` liefert pro Session `session_metrics` (wenn befüllt). +- **API:** Admin `/api/admin/training-parameters`, `/api/admin/training-category-parameters`, `/api/admin/training-type-parameters`; Nutzer `GET /api/activity/{id}`, `PUT /api/activity/{id}/metrics`; Platzhalter-Pfad `training_sessions_recent_json` liefert pro Session `session_metrics` inkl. `name_*` / `description_*`; **`{{training_parameters_glossary_md}}`** = Markdown-Legende aller aktiven Parameter (KI). - **Frontend:** Admin `/admin/activity-attribute-profiles`; Aktivität → Verlauf → Bearbeiten: Profil-Kennwerte; `api.js` ergänzt. ### Updates (16.04.2026 - Aktivität Phase A abgeschlossen, Phase B gestartet) diff --git a/backend/data_layer/activity_metrics.py b/backend/data_layer/activity_metrics.py index cfdd940..9c27451 100644 --- a/backend/data_layer/activity_metrics.py +++ b/backend/data_layer/activity_metrics.py @@ -10,6 +10,7 @@ Functions: - get_training_frequency_by_type_data(): Häufigkeit & Intensität pro activity_type - get_training_inter_session_gap_data(): Pausen zwischen Einheiten (Stunden) - get_training_sessions_recent_weeks_data(): Wochen-JSON für KI-Kontext + - get_training_parameters_ki_glossary_data(): Parameter-Katalog (Feld, Namen, Beschreibungen) für KI All functions return structured data (dict) without formatting. Use placeholder_resolver.py for formatted strings for AI. @@ -1179,3 +1180,32 @@ def get_training_sessions_recent_weeks_data( }, } ) + + +def get_training_parameters_ki_glossary_data(profile_id: str) -> Dict[str, Any]: + """ + Alle aktiven ``training_parameters`` für KI-Kontext (z. B. neben ``training_sessions_recent_json``). + + Enthält technischen key, name_de/name_en, description_de/description_en, data_type, unit, category. + + Args: + profile_id: Reserviert für spätere Einschränkung (z. B. nur im Profil vorkommende Keys); + aktuell ungenutzt, Signatur bleibt für Platzhalter-Resolver. + """ + _ = profile_id + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + """ + SELECT key, name_de, name_en, description_de, description_en, + data_type, unit, category + FROM training_parameters + WHERE is_active = true + ORDER BY category, key + """ + ) + rows = [r2d(r) for r in cur.fetchall()] + return { + "parameters": rows, + "meta": {"count": len(rows), "scope": "global_active_catalog"}, + } diff --git a/backend/data_layer/activity_session_metrics.py b/backend/data_layer/activity_session_metrics.py index fcd0c68..e354d89 100644 --- a/backend/data_layer/activity_session_metrics.py +++ b/backend/data_layer/activity_session_metrics.py @@ -71,6 +71,8 @@ def merge_parameter_schema_rows( "key": r["key"], "name_de": r["name_de"], "name_en": r["name_en"], + "description_de": r.get("description_de"), + "description_en": r.get("description_en"), "param_category": r["param_category"], "data_type": r["data_type"], "unit": r["unit"], @@ -90,6 +92,8 @@ def merge_parameter_schema_rows( "key": r["key"], "name_de": r["name_de"], "name_en": r["name_en"], + "description_de": r.get("description_de"), + "description_en": r.get("description_en"), "param_category": r["param_category"], "data_type": r["data_type"], "unit": r["unit"], @@ -133,7 +137,9 @@ def resolve_activity_attribute_schema( tcp.sort_order AS cat_sort, tcp.required AS cat_required, tcp.ui_group AS cat_ui_group, - tp.key, tp.name_de, tp.name_en, tp.category AS param_category, + tp.key, tp.name_de, tp.name_en, + tp.description_de, tp.description_en, + tp.category AS param_category, tp.data_type, tp.unit, tp.validation_rules, tp.source_field FROM training_category_parameter tcp JOIN training_parameters tp ON tp.id = tcp.training_parameter_id @@ -151,7 +157,9 @@ def resolve_activity_attribute_schema( ttp.sort_order AS typ_sort, ttp.required AS typ_required, ttp.ui_group AS typ_ui_group, - tp.key, tp.name_de, tp.name_en, tp.category AS param_category, + tp.key, tp.name_de, tp.name_en, + tp.description_de, tp.description_en, + tp.category AS param_category, tp.data_type, tp.unit, tp.validation_rules, tp.source_field FROM training_type_parameter ttp JOIN training_parameters tp ON tp.id = ttp.training_parameter_id @@ -164,6 +172,16 @@ def resolve_activity_attribute_schema( return merge_parameter_schema_rows(category_rows, type_rows) +def _metric_human_labels(schema_row: Mapping[str, Any]) -> Dict[str, Any]: + """Bezeichnung + Kurzbeschreibung aus training_parameters (KI / Export).""" + return { + "name_de": schema_row.get("name_de"), + "name_en": schema_row.get("name_en"), + "description_de": schema_row.get("description_de"), + "description_en": schema_row.get("description_en"), + } + + def _validation_rules_dict(raw: Any) -> Dict[str, Any]: if isinstance(raw, dict): return raw @@ -357,6 +375,7 @@ def merge_column_backed_and_eav_metrics( "data_type": dt, "unit": unit, "value": val, + **_metric_human_labels(s), } ) used_column = True @@ -377,6 +396,7 @@ def merge_column_backed_and_eav_metrics( "data_type": dt, "unit": unit, "value": val, + **_metric_human_labels(s), } ) keys_handled.add(k) @@ -395,6 +415,7 @@ def merge_column_backed_and_eav_metrics( "data_type": dt, "unit": unit, "value": val, + **_metric_human_labels(s), } ) keys_handled.add(k) @@ -403,7 +424,9 @@ def merge_column_backed_and_eav_metrics( pass if k in eav_by_key: - merged.append(dict(eav_by_key[k])) + row = dict(eav_by_key[k]) + row.update(_metric_human_labels(s)) + merged.append(row) keys_handled.add(k) merged.sort(key=lambda x: x["key"]) @@ -699,6 +722,15 @@ def enrich_sessions_with_metrics(cur, sessions: List[Dict[str, Any]]) -> None: eav_list = by_act.get(aid, []) merged = merge_column_backed_and_eav_metrics(header, schema, eav_list) s["session_metrics"] = [ - {"key": m["key"], "data_type": m["data_type"], "unit": m["unit"], "value": m["value"]} + { + "key": m["key"], + "data_type": m["data_type"], + "unit": m["unit"], + "value": m["value"], + "name_de": m.get("name_de"), + "name_en": m.get("name_en"), + "description_de": m.get("description_de"), + "description_en": m.get("description_en"), + } for m in merged ] diff --git a/backend/placeholder_registrations/activity_session_insights.py b/backend/placeholder_registrations/activity_session_insights.py index 38fcd63..0e49eb9 100644 --- a/backend/placeholder_registrations/activity_session_insights.py +++ b/backend/placeholder_registrations/activity_session_insights.py @@ -143,7 +143,8 @@ def register_activity_session_insights(): "duration_min, kcal_active, hr_avg, hr_max, rpe, training_category, training_type_name, " "session_metrics[]. " "session_metrics: effektive Liste nach merge_column_backed_and_eav_metrics — Einträge mit " - "training_parameter_id, key, data_type, unit, value; nur Parameter aus Attributschema " + "training_parameter_id, key, data_type, unit, value, name_de/name_en, description_de/description_en; " + "nur Parameter aus Attributschema " "(training_category_parameter + training_type_parameter Overrides), keys sortiert. " "Kanon Lesen: activity_log-Spalte vor EAV bei Konflikt. " "meta: weeks_requested, days_loaded, session_count, confidence. " @@ -170,6 +171,7 @@ def register_activity_session_insights(): "session_metrics oft [] (kein Typ, kein Profil, keine gespeicherten Werte). " "Anzahl und Namen der Metrik-Keys sind instanz-/adminabhängig — JSON nicht als festes Schema " "für Downstream-Parsing harter Logik verwenden. " + "Für KI-Semantik zusätzlich {{training_parameters_glossary_md}} (gesamter aktiver Katalog) in den Prompt legen. " "Composite-Parameter (JSON in EAV) noch nicht im MVP expandiert; ggf. Roh-value_text in späterer Phase." ), layer_1_decision="activity_metrics.get_training_sessions_recent_weeks_data", @@ -192,5 +194,61 @@ def register_activity_session_insights(): _ev(pj, "known_limitations", EvidenceType.MIXED) register_placeholder(pj) + md_gloss = PlaceholderMetadata( + key="training_parameters_glossary_md", + category="Aktivität", + description=( + "Markdown-Tabelle: alle aktiven training_parameters (key, DE/EN, Beschreibungen, Typ, Einheit, Kategorie). " + "Ergänzung zu training_sessions_recent_json für KI (Bedeutung dynamischer Metrik-Keys)." + ), + resolver_module="backend/placeholder_resolver.py", + resolver_function="get_training_parameters_glossary_md", + data_layer_module="backend/data_layer/activity_metrics.py", + data_layer_function="get_training_parameters_ki_glossary_data", + source_tables=["training_parameters"], + semantic_contract=( + "SELECT auf training_parameters WHERE is_active; sortiert category, key. " + "profile_id-Parameter im Resolver reserviert, aktuell globaler Katalog." + ), + business_meaning="KI: Legende zu session_metrics-Keys und Custom-Parametern", + unit="Markdown", + time_window="n/a (Katalog-Snapshot)", + output_type=OutputType.TEXT_SUMMARY, + placeholder_type=PlaceholderType.INTERPRETED, + format_hint="GitHub-Flavored Markdown-Tabelle", + example_output="| Feld (key) | DE | EN | Beschreibung DE | … |", + minimum_data_requirements="Optional leer → Kurztext statt Tabelle", + quality_filter_policy=None, + confidence_logic="Immer verfügbar wenn DB erreichbar", + missing_value_policy=MissingValuePolicy( + available=False, + value_raw=None, + missing_reason="no_data", + legacy_display="Keine aktiven Trainingsparameter im Katalog.", + ), + known_limitations=( + "Keine profil-spezifische Einschränkung auf tatsächlich genutzte Keys (V2). " + "Tabellen können bei großem Katalog lang werden." + ), + layer_1_decision="activity_metrics.get_training_parameters_ki_glossary_data", + layer_2a_decision="get_training_parameters_glossary_md", + layer_2b_reuse_possible=True, + architecture_alignment="Phase 0c", + issue_53_alignment="Layer 2a", + evidence={}, + ) + for f in ( + "key", "category", "description", "resolver_module", "resolver_function", + "data_layer_module", "data_layer_function", "source_tables", "semantic_contract", + "unit", "time_window", "output_type", "placeholder_type", "format_hint", + "example_output", "minimum_data_requirements", "confidence_logic", + "missing_value_policy", "layer_1_decision", "layer_2a_decision", + "layer_2b_reuse_possible", "architecture_alignment", "issue_53_alignment", + ): + _ev(md_gloss, f) + _ev(md_gloss, "business_meaning", EvidenceType.DRAFT_DERIVED) + _ev(md_gloss, "known_limitations", EvidenceType.MIXED) + register_placeholder(md_gloss) + register_activity_session_insights() diff --git a/backend/placeholder_resolver.py b/backend/placeholder_resolver.py index 7e35202..6f635c2 100644 --- a/backend/placeholder_resolver.py +++ b/backend/placeholder_resolver.py @@ -35,6 +35,7 @@ from data_layer.activity_metrics import ( get_training_frequency_by_type_data, get_training_inter_session_gap_data, get_training_sessions_recent_weeks_data, + get_training_parameters_ki_glossary_data, ) from data_layer.recovery_metrics import ( get_sleep_duration_data, @@ -426,7 +427,8 @@ def get_activity_detail(profile_id: str, days: int = 14) -> str: k, v = m.get("key"), m.get("value") if k is None or v is None: continue - eav_parts.append(f"{k}={v}") + label = m.get("name_de") or m.get("name_en") or k + eav_parts.append(f"{label} ({k})={v}") eav_str = f" | EAV: {'; '.join(eav_parts)}" if eav_parts else "" lines.append( f"{activity['date']}: {activity['activity_type']} " @@ -456,6 +458,45 @@ def get_trainingstyp_verteilung(profile_id: str, days: int = 14) -> str: return ", ".join(parts) +def get_training_parameters_glossary_md(profile_id: str) -> str: + """ + Markdown-Tabelle: alle aktiven training_parameters (key, Namen, Beschreibungen, Typ, Einheit). + Für KI neben session_metrics / training_sessions_recent_json. + """ + data = get_training_parameters_ki_glossary_data(profile_id) + params = data.get("parameters") or [] + if not params: + return "Keine aktiven Trainingsparameter im Katalog." + + def cell(x: object) -> str: + if x is None: + return "—" + return str(x).replace("|", "·").replace("\n", " ").strip()[:400] + + lines = [ + "| Feld (key) | DE | EN | Beschreibung DE | Beschreibung EN | Typ | Einheit | Kategorie |", + "|---|---|---|---|---|---|---|---|", + ] + for p in params: + lines.append( + "| " + + " | ".join( + [ + cell(p.get("key")), + cell(p.get("name_de")), + cell(p.get("name_en")), + cell(p.get("description_de")), + cell(p.get("description_en")), + cell(p.get("data_type")), + cell(p.get("unit")), + cell(p.get("category")), + ] + ) + + " |" + ) + return "\n".join(lines) + + def get_training_frequency_by_type_md(profile_id: str, days: int = 28) -> str: """ Markdown-Tabelle: pro Trainingsart (Roh-Label) Ø Sessions/Woche, Dauer, kcal, HF, RPE, kcal/min. @@ -1545,6 +1586,7 @@ PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = { '{{training_frequency_by_type_md}}': get_training_frequency_by_type_md, '{{training_inter_session_gap_md}}': get_training_inter_session_gap_md, '{{training_sessions_recent_json}}': lambda pid: _safe_json('training_sessions_recent_json', pid), + '{{training_parameters_glossary_md}}': get_training_parameters_glossary_md, # Schlaf & Erholung (10 Registry-Keys; recovery_score hier, nicht unter Meta Scores) '{{sleep_avg_duration}}': lambda pid: get_sleep_avg_duration(pid, 7), @@ -1749,6 +1791,7 @@ def get_available_placeholders(categories: Optional[List[str]] = None) -> Dict[s '{{proxy_internal_load_7d}}', '{{monotony_score}}', '{{strain_score}}', '{{rest_day_compliance}}', '{{vo2max_trend_28d}}', '{{activity_score}}', '{{training_frequency_by_type_md}}', '{{training_inter_session_gap_md}}', '{{training_sessions_recent_json}}', + '{{training_parameters_glossary_md}}', ], 'schlaf': [ '{{sleep_avg_duration}}', '{{sleep_avg_quality}}', '{{rest_days_count}}', diff --git a/backend/tests/test_activity_session_metrics.py b/backend/tests/test_activity_session_metrics.py index 2178b0f..a2bc11a 100644 --- a/backend/tests/test_activity_session_metrics.py +++ b/backend/tests/test_activity_session_metrics.py @@ -97,6 +97,52 @@ def test_merge_type_overrides_required_and_sort(): assert merged[0]["required"] is True +def test_merge_parameter_schema_includes_descriptions(): + cat = [ + { + "training_parameter_id": 1, + "cat_sort": 0, + "cat_required": False, + "cat_ui_group": None, + "key": "custom_w", + "name_de": "Leistung", + "name_en": "Watts", + "description_de": "Mittlere 5-Min-Leistung", + "description_en": "5 min average power", + "param_category": "performance", + "data_type": "integer", + "unit": "W", + "validation_rules": {}, + "source_field": None, + } + ] + merged = merge_parameter_schema_rows(cat, []) + assert merged[0]["description_de"] == "Mittlere 5-Min-Leistung" + assert merged[0]["description_en"] == "5 min average power" + + +def test_merge_column_backed_includes_human_labels_from_schema(): + schema = [ + { + "training_parameter_id": 1, + "key": "watts", + "data_type": "integer", + "unit": "W", + "validation_rules": {}, + "source_field": "avg_power", + "name_de": "Leistung", + "name_en": "Power", + "description_de": "Gerätewert", + "description_en": "Device reading", + } + ] + out = merge_column_backed_and_eav_metrics({"avg_power": 200}, schema, []) + assert len(out) == 1 + assert out[0]["value"] == 200 + assert out[0]["name_de"] == "Leistung" + assert out[0]["description_en"] == "Device reading" + + def test_merge_type_adds_parameter_not_in_category(): typ = [_ttp_row(7, "cadence", typ_sort=1, typ_required=True, data_type="integer")] merged = merge_parameter_schema_rows([], typ) diff --git a/frontend/src/pages/AdminActivityAttributeProfilesPage.jsx b/frontend/src/pages/AdminActivityAttributeProfilesPage.jsx index f0e21cd..89b0af2 100644 --- a/frontend/src/pages/AdminActivityAttributeProfilesPage.jsx +++ b/frontend/src/pages/AdminActivityAttributeProfilesPage.jsx @@ -10,6 +10,8 @@ const emptyParamForm = () => ({ key: '', name_de: '', name_en: '', + description_de: '', + description_en: '', category: 'physical', data_type: 'float', unit: '', @@ -130,6 +132,8 @@ export default function AdminActivityAttributeProfilesPage() { key: paramForm.key.trim().toLowerCase(), name_de: paramForm.name_de.trim(), name_en: paramForm.name_en.trim(), + description_de: paramForm.description_de.trim() || null, + description_en: paramForm.description_en.trim() || null, category: paramForm.category, data_type: paramForm.data_type, unit: paramForm.unit.trim() || null, @@ -153,6 +157,8 @@ export default function AdminActivityAttributeProfilesPage() { await api.adminUpdateTrainingParameter(editParam.id, { name_de: editParam.name_de.trim(), name_en: editParam.name_en.trim(), + description_de: editParam.description_de?.trim() || null, + description_en: editParam.description_en?.trim() || null, category: editParam.category, data_type: editParam.data_type, unit: editParam.unit?.trim() || null, @@ -302,6 +308,11 @@ export default function AdminActivityAttributeProfilesPage() { Nach Migration 055 werden Standard-Parameter allen Kategorien zugeordnet und vorhandene{' '} activity_log-Spalten idempotent nach EAV gespiegelt (sofern noch keine EAV-Zeile existiert). +
  • + KI: Bei eigenen / unklaren Metriken kurze Beschreibung DE/EN im Katalog + pflegen — sie erscheinen in Export/Platzhalter-Kontext (training_sessions_recent_json,{' '} + {'{{training_parameters_glossary_md}}'}). +
  • @@ -394,6 +405,23 @@ export default function AdminActivityAttributeProfilesPage() { onChange={(e) => setParamForm((f) => ({ ...f, name_en: e.target.value }))} /> +
    + +