feat: Add new profile and time period placeholders in placeholder_resolver.py
- Introduced functions to retrieve profile name, age, height, and gender for better placeholder resolution. - Added functions for displaying current date and time period labels (last 7, 30, and 90 days). - Updated PLACEHOLDER_MAP to utilize new functions for improved readability and maintainability. - Enhanced placeholder registrations in __init__.py to include new modules for sleep, vital metrics, and profile time periods. These changes enhance the flexibility and functionality of the placeholder system, allowing for more dynamic content generation.
This commit is contained in:
parent
e9e094c6a4
commit
2ea5f905c4
|
|
@ -13,6 +13,12 @@ from . import body_metrics
|
|||
from . import body_extras
|
||||
from . import activity_metrics
|
||||
from . import activity_session_insights
|
||||
from . import schlaf_erholung
|
||||
from . import vitalwerte
|
||||
from . import profil_zeitraum
|
||||
from . import phase_0b_meta_scores
|
||||
from . import phase_0b_ziele_fokus
|
||||
from . import korrelationen
|
||||
|
||||
__all__ = [
|
||||
'nutrition_part_a',
|
||||
|
|
@ -23,4 +29,10 @@ __all__ = [
|
|||
'body_extras',
|
||||
'activity_metrics',
|
||||
'activity_session_insights',
|
||||
'schlaf_erholung',
|
||||
'vitalwerte',
|
||||
'profil_zeitraum',
|
||||
'phase_0b_meta_scores',
|
||||
'phase_0b_ziele_fokus',
|
||||
'korrelationen',
|
||||
]
|
||||
|
|
|
|||
19
backend/placeholder_registrations/_evidence.py
Normal file
19
backend/placeholder_registrations/_evidence.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
"""Gemeinsames Evidence-Tagging für Registry-Einträge."""
|
||||
|
||||
from placeholder_registry import EvidenceType, PlaceholderMetadata
|
||||
|
||||
STANDARD_FIELDS = (
|
||||
"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",
|
||||
)
|
||||
|
||||
|
||||
def tag_standard_evidence(meta: PlaceholderMetadata) -> None:
|
||||
for field in STANDARD_FIELDS:
|
||||
meta.set_evidence(field, EvidenceType.CODE_DERIVED)
|
||||
meta.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
||||
meta.set_evidence("known_limitations", EvidenceType.MIXED)
|
||||
96
backend/placeholder_registrations/korrelationen.py
Normal file
96
backend/placeholder_registrations/korrelationen.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
"""Registry: Korrelations- und Treiber-Metriken (Data Layer correlations)."""
|
||||
|
||||
from placeholder_registry import (
|
||||
PlaceholderMetadata,
|
||||
MissingValuePolicy,
|
||||
OutputType,
|
||||
PlaceholderType,
|
||||
register_placeholder,
|
||||
)
|
||||
from ._evidence import tag_standard_evidence
|
||||
|
||||
CAT = "Korrelationen"
|
||||
MVP = lambda reason, disp: MissingValuePolicy(
|
||||
available=False, value_raw=None, missing_reason=reason, legacy_display=disp
|
||||
)
|
||||
|
||||
|
||||
def register_korrelationen():
|
||||
for key, dl_fn, desc, tables, sem in [
|
||||
(
|
||||
"correlation_energy_weight_lag",
|
||||
"calculate_lag_correlation",
|
||||
"JSON: Lag-Korrelation Energiebilanz ↔ Gewicht",
|
||||
["nutrition_log", "weight_log"],
|
||||
"correlations.calculate_lag_correlation(pid, 'energy', 'weight')",
|
||||
),
|
||||
(
|
||||
"correlation_protein_lbm",
|
||||
"calculate_lag_correlation",
|
||||
"JSON: Lag-Korrelation Protein ↔ Magermasse",
|
||||
["nutrition_log", "weight_log", "caliper_log"],
|
||||
"correlations.calculate_lag_correlation(pid, 'protein', 'lbm')",
|
||||
),
|
||||
(
|
||||
"correlation_load_hrv",
|
||||
"calculate_lag_correlation",
|
||||
"JSON: Lag-Korrelation Trainingslast ↔ HRV",
|
||||
["activity_log", "vitals_baseline"],
|
||||
"correlations.calculate_lag_correlation(pid, 'training_load', 'hrv')",
|
||||
),
|
||||
(
|
||||
"correlation_load_rhr",
|
||||
"calculate_lag_correlation",
|
||||
"JSON: Lag-Korrelation Trainingslast ↔ Ruhepuls",
|
||||
["activity_log", "vitals_baseline"],
|
||||
"correlations.calculate_lag_correlation(pid, 'training_load', 'rhr')",
|
||||
),
|
||||
(
|
||||
"plateau_detected",
|
||||
"calculate_plateau_detected",
|
||||
"JSON: Platten-Erkennung (Gewicht/Körper)",
|
||||
["weight_log", "caliper_log"],
|
||||
"correlations.calculate_plateau_detected",
|
||||
),
|
||||
(
|
||||
"top_drivers",
|
||||
"calculate_top_drivers",
|
||||
"JSON: Top Treiber für Ziel-/Score-Variablen",
|
||||
["weight_log", "nutrition_log", "activity_log", "vitals_baseline", "sleep_log"],
|
||||
"correlations.calculate_top_drivers",
|
||||
),
|
||||
]:
|
||||
m = PlaceholderMetadata(
|
||||
key=key,
|
||||
category=CAT,
|
||||
description=desc,
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="_safe_json",
|
||||
data_layer_module="backend/data_layer/correlations.py",
|
||||
data_layer_function=dl_fn,
|
||||
source_tables=tables,
|
||||
semantic_contract=sem,
|
||||
business_meaning="Strukturierte Korrelationsausgabe für KI",
|
||||
unit="JSON",
|
||||
time_window="funktionsintern",
|
||||
output_type=OutputType.JSON,
|
||||
placeholder_type=PlaceholderType.RAW_DATA,
|
||||
format_hint="JSON-String",
|
||||
example_output="{}",
|
||||
minimum_data_requirements="Ausreichend gekoppelte Zeitreihen",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="Wie correlations.*",
|
||||
missing_value_policy=MVP("insufficient_data", "{}"),
|
||||
known_limitations="Bei wenigen Daten leer oder wenig robust",
|
||||
layer_1_decision=f"correlations.{dl_fn}",
|
||||
layer_2a_decision="_safe_json",
|
||||
layer_2b_reuse_possible=True,
|
||||
architecture_alignment="Phase 0c",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
tag_standard_evidence(m)
|
||||
register_placeholder(m)
|
||||
|
||||
|
||||
register_korrelationen()
|
||||
66
backend/placeholder_registrations/phase_0b_meta_scores.py
Normal file
66
backend/placeholder_registrations/phase_0b_meta_scores.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"""Registry: Meta-Scores (Phase 0b) — Ziel-Fortschritt und Datenqualität."""
|
||||
|
||||
from placeholder_registry import (
|
||||
PlaceholderMetadata,
|
||||
MissingValuePolicy,
|
||||
OutputType,
|
||||
PlaceholderType,
|
||||
register_placeholder,
|
||||
)
|
||||
from ._evidence import tag_standard_evidence
|
||||
|
||||
CAT = "Scores (Phase 0b)"
|
||||
MVP = lambda reason, disp: MissingValuePolicy(
|
||||
available=False, value_raw=None, missing_reason=reason, legacy_display=disp
|
||||
)
|
||||
|
||||
|
||||
def register_phase_0b_meta_scores():
|
||||
for key, dl_fn, desc, unit in [
|
||||
(
|
||||
"goal_progress_score",
|
||||
"calculate_goal_progress_score",
|
||||
"Aggregierter Ziel-Fortschritt 0–100",
|
||||
"0–100",
|
||||
),
|
||||
(
|
||||
"data_quality_score",
|
||||
"calculate_data_quality_score",
|
||||
"Datenqualitäts-Score 0–100",
|
||||
"0–100",
|
||||
),
|
||||
]:
|
||||
m = PlaceholderMetadata(
|
||||
key=key,
|
||||
category=CAT,
|
||||
description=desc,
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="_safe_int",
|
||||
data_layer_module="backend/data_layer/scores.py",
|
||||
data_layer_function=dl_fn,
|
||||
source_tables=["goals", "weight_log", "nutrition_log", "activity_log", "profiles"],
|
||||
semantic_contract=f"scores.{dl_fn} (siehe Data Layer).",
|
||||
business_meaning="Meta-KPI für Prompt-Gewichtung",
|
||||
unit=unit,
|
||||
time_window="composite",
|
||||
output_type=OutputType.NUMERIC,
|
||||
placeholder_type=PlaceholderType.SCORE,
|
||||
format_hint="Ganzzahl als String",
|
||||
example_output="72",
|
||||
minimum_data_requirements="Abhängig von Score-Implementierung",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="Wie calculate_* in scores.py",
|
||||
missing_value_policy=MVP("insufficient_data", "nicht verfügbar"),
|
||||
known_limitations="Bei dünnen Daten weniger aussagekräftig",
|
||||
layer_1_decision=f"scores.{dl_fn}",
|
||||
layer_2a_decision="_safe_int",
|
||||
layer_2b_reuse_possible=True,
|
||||
architecture_alignment="Phase 0b",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
tag_standard_evidence(m)
|
||||
register_placeholder(m)
|
||||
|
||||
|
||||
register_phase_0b_meta_scores()
|
||||
392
backend/placeholder_registrations/phase_0b_ziele_fokus.py
Normal file
392
backend/placeholder_registrations/phase_0b_ziele_fokus.py
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
"""Registry: Ziele, Fokusbereiche, Kategorie-Scores und formatierte Listen (Phase 0b)."""
|
||||
|
||||
from placeholder_registry import (
|
||||
PlaceholderMetadata,
|
||||
MissingValuePolicy,
|
||||
OutputType,
|
||||
PlaceholderType,
|
||||
register_placeholder,
|
||||
)
|
||||
from ._evidence import tag_standard_evidence
|
||||
|
||||
CAT = "Ziele & Fokus (Phase 0b)"
|
||||
MVP = lambda reason, disp: MissingValuePolicy(
|
||||
available=False, value_raw=None, missing_reason=reason, legacy_display=disp
|
||||
)
|
||||
|
||||
|
||||
def register_phase_0b_ziele_fokus():
|
||||
# Top-Ziel / Top-Fokusbereich
|
||||
m = PlaceholderMetadata(
|
||||
key="top_goal_name",
|
||||
category=CAT,
|
||||
description="Name/Typ des höchstpriorisierten Ziels",
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="_safe_str",
|
||||
data_layer_module="backend/data_layer/scores.py",
|
||||
data_layer_function="get_top_priority_goal",
|
||||
source_tables=["goals"],
|
||||
semantic_contract="Feld name oder goal_type aus get_top_priority_goal",
|
||||
business_meaning="Priorisierung für KI-Empfehlungen",
|
||||
unit="text",
|
||||
time_window="aktuell",
|
||||
output_type=OutputType.STRING,
|
||||
placeholder_type=PlaceholderType.INTERPRETED,
|
||||
format_hint="Kurztext",
|
||||
example_output="Gewicht 80kg",
|
||||
minimum_data_requirements="Mindestens ein aktives Ziel",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="scores.get_top_priority_goal",
|
||||
missing_value_policy=MVP("insufficient_data", "nicht verfügbar"),
|
||||
known_limitations=None,
|
||||
layer_1_decision="scores.get_top_priority_goal",
|
||||
layer_2a_decision="_safe_str('top_goal_name')",
|
||||
layer_2b_reuse_possible=False,
|
||||
architecture_alignment="Phase 0b",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
tag_standard_evidence(m)
|
||||
register_placeholder(m)
|
||||
|
||||
m = PlaceholderMetadata(
|
||||
key="top_goal_progress_pct",
|
||||
category=CAT,
|
||||
description="Fortschritt Top-Ziel (%)",
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="_safe_int",
|
||||
data_layer_module="backend/data_layer/scores.py",
|
||||
data_layer_function="get_top_priority_goal",
|
||||
source_tables=["goals"],
|
||||
semantic_contract="progress_pct aus get_top_priority_goal",
|
||||
business_meaning="Priorisierung für KI-Empfehlungen",
|
||||
unit="%",
|
||||
time_window="aktuell",
|
||||
output_type=OutputType.NUMERIC,
|
||||
placeholder_type=PlaceholderType.SCORE,
|
||||
format_hint="Ganzzahl",
|
||||
example_output="65",
|
||||
minimum_data_requirements="Mindestens ein aktives Ziel",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="scores.get_top_priority_goal",
|
||||
missing_value_policy=MVP("insufficient_data", "nicht verfügbar"),
|
||||
known_limitations=None,
|
||||
layer_1_decision="scores.get_top_priority_goal",
|
||||
layer_2a_decision="_safe_int('top_goal_progress_pct')",
|
||||
layer_2b_reuse_possible=False,
|
||||
architecture_alignment="Phase 0b",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
tag_standard_evidence(m)
|
||||
register_placeholder(m)
|
||||
|
||||
m = PlaceholderMetadata(
|
||||
key="top_goal_status",
|
||||
category=CAT,
|
||||
description="Status-Label Top-Ziel",
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="_safe_str",
|
||||
data_layer_module="backend/data_layer/scores.py",
|
||||
data_layer_function="get_top_priority_goal",
|
||||
source_tables=["goals"],
|
||||
semantic_contract="status aus get_top_priority_goal",
|
||||
business_meaning="Priorisierung für KI-Empfehlungen",
|
||||
unit="text",
|
||||
time_window="aktuell",
|
||||
output_type=OutputType.STRING,
|
||||
placeholder_type=PlaceholderType.INTERPRETED,
|
||||
format_hint="Kurztext",
|
||||
example_output="active",
|
||||
minimum_data_requirements="Mindestens ein aktives Ziel",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="scores.get_top_priority_goal",
|
||||
missing_value_policy=MVP("insufficient_data", "nicht verfügbar"),
|
||||
known_limitations=None,
|
||||
layer_1_decision="scores.get_top_priority_goal",
|
||||
layer_2a_decision="_safe_str('top_goal_status')",
|
||||
layer_2b_reuse_possible=False,
|
||||
architecture_alignment="Phase 0b",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
tag_standard_evidence(m)
|
||||
register_placeholder(m)
|
||||
|
||||
m = PlaceholderMetadata(
|
||||
key="top_focus_area_name",
|
||||
category=CAT,
|
||||
description="Bezeichnung des gewichtet stärksten Fokusbereichs",
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="_safe_str",
|
||||
data_layer_module="backend/data_layer/scores.py",
|
||||
data_layer_function="get_top_focus_area",
|
||||
source_tables=["user_focus_area_weights", "focus_area_definitions"],
|
||||
semantic_contract="label aus get_top_focus_area",
|
||||
business_meaning="Priorisierung für KI-Empfehlungen",
|
||||
unit="text",
|
||||
time_window="aktuell",
|
||||
output_type=OutputType.STRING,
|
||||
placeholder_type=PlaceholderType.INTERPRETED,
|
||||
format_hint="Kurztext",
|
||||
example_output="Kraft",
|
||||
minimum_data_requirements="Gewichtete Fokusbereiche",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="scores.get_top_focus_area",
|
||||
missing_value_policy=MVP("insufficient_data", "nicht verfügbar"),
|
||||
known_limitations=None,
|
||||
layer_1_decision="scores.get_top_focus_area",
|
||||
layer_2a_decision="_safe_str('top_focus_area_name')",
|
||||
layer_2b_reuse_possible=False,
|
||||
architecture_alignment="Phase 0b",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
tag_standard_evidence(m)
|
||||
register_placeholder(m)
|
||||
|
||||
m = PlaceholderMetadata(
|
||||
key="top_focus_area_progress",
|
||||
category=CAT,
|
||||
description="Fortschritt Top-Fokusbereich (%)",
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="_safe_int",
|
||||
data_layer_module="backend/data_layer/scores.py",
|
||||
data_layer_function="get_top_focus_area",
|
||||
source_tables=["user_focus_area_weights", "focus_area_definitions", "goals"],
|
||||
semantic_contract="progress aus get_top_focus_area",
|
||||
business_meaning="Priorisierung für KI-Empfehlungen",
|
||||
unit="%",
|
||||
time_window="aktuell",
|
||||
output_type=OutputType.NUMERIC,
|
||||
placeholder_type=PlaceholderType.SCORE,
|
||||
format_hint="Ganzzahl",
|
||||
example_output="58",
|
||||
minimum_data_requirements="Gewichtete Fokusbereiche",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="scores.get_top_focus_area",
|
||||
missing_value_policy=MVP("insufficient_data", "nicht verfügbar"),
|
||||
known_limitations=None,
|
||||
layer_1_decision="scores.get_top_focus_area",
|
||||
layer_2a_decision="_safe_int('top_focus_area_progress')",
|
||||
layer_2b_reuse_possible=False,
|
||||
architecture_alignment="Phase 0b",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
tag_standard_evidence(m)
|
||||
register_placeholder(m)
|
||||
|
||||
# Kategorie Progress / Weight (7 Kategorien)
|
||||
for slug in (
|
||||
"körper",
|
||||
"ernährung",
|
||||
"aktivität",
|
||||
"recovery",
|
||||
"vitalwerte",
|
||||
"mental",
|
||||
"lebensstil",
|
||||
):
|
||||
key_p = f"focus_cat_{slug}_progress"
|
||||
key_w = f"focus_cat_{slug}_weight"
|
||||
m_p = PlaceholderMetadata(
|
||||
key=key_p,
|
||||
category=CAT,
|
||||
description=f"Aggregierter Fortschritt Kategorie „{slug}“ (%)",
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="_safe_int",
|
||||
data_layer_module="backend/data_layer/scores.py",
|
||||
data_layer_function="calculate_category_progress",
|
||||
source_tables=["goals", "focus_area_definitions", "user_focus_area_weights"],
|
||||
semantic_contract=f"scores.calculate_category_progress(pid, '{slug}')",
|
||||
business_meaning="Focus-Area-Kategorie-Score",
|
||||
unit="%",
|
||||
time_window="aktuell",
|
||||
output_type=OutputType.NUMERIC,
|
||||
placeholder_type=PlaceholderType.SCORE,
|
||||
format_hint="Ganzzahl",
|
||||
example_output="55",
|
||||
minimum_data_requirements="Gewichtete Bereiche in Kategorie",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="scores.calculate_category_progress",
|
||||
missing_value_policy=MVP("insufficient_data", "nicht verfügbar"),
|
||||
known_limitations=None,
|
||||
layer_1_decision="scores.calculate_category_progress",
|
||||
layer_2a_decision="_safe_int",
|
||||
layer_2b_reuse_possible=True,
|
||||
architecture_alignment="Phase 0b",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
tag_standard_evidence(m_p)
|
||||
register_placeholder(m_p)
|
||||
|
||||
m_w = PlaceholderMetadata(
|
||||
key=key_w,
|
||||
category=CAT,
|
||||
description=f"Nutzer-Gewichtung Kategorie „{slug}“ (Anteil 0–1)",
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="_safe_float",
|
||||
data_layer_module="backend/data_layer/scores.py",
|
||||
data_layer_function="calculate_category_weight",
|
||||
source_tables=["user_focus_area_weights", "focus_area_definitions"],
|
||||
semantic_contract=f"scores.calculate_category_weight(pid, '{slug}')",
|
||||
business_meaning="Kategorie-Gewichtung im Fokusmodell",
|
||||
unit="0–1",
|
||||
time_window="aktuell",
|
||||
output_type=OutputType.NUMERIC,
|
||||
placeholder_type=PlaceholderType.INTERPRETED,
|
||||
format_hint="Dezimal",
|
||||
example_output="0.25",
|
||||
minimum_data_requirements="user_focus_area_weights",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="scores.calculate_category_weight",
|
||||
missing_value_policy=MVP("insufficient_data", "nicht verfügbar"),
|
||||
known_limitations=None,
|
||||
layer_1_decision="scores.calculate_category_weight",
|
||||
layer_2a_decision="_safe_float",
|
||||
layer_2b_reuse_possible=False,
|
||||
architecture_alignment="Phase 0b",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
tag_standard_evidence(m_w)
|
||||
register_placeholder(m_w)
|
||||
|
||||
# Strukturierte Ziele / Fokus
|
||||
for key, res_fn, dl_mod, dl_fn, desc, out, ptype in [
|
||||
(
|
||||
"active_goals_json",
|
||||
"_safe_json",
|
||||
"backend/goal_utils.py",
|
||||
"get_active_goals",
|
||||
"Aktive Ziele als JSON",
|
||||
OutputType.JSON,
|
||||
PlaceholderType.RAW_DATA,
|
||||
),
|
||||
(
|
||||
"active_goals_md",
|
||||
"_safe_str",
|
||||
"backend/placeholder_resolver.py",
|
||||
"_format_goals_as_markdown",
|
||||
"Aktive Ziele als Markdown-Tabelle",
|
||||
OutputType.TEXT_SUMMARY,
|
||||
PlaceholderType.INTERPRETED,
|
||||
),
|
||||
(
|
||||
"focus_areas_weighted_json",
|
||||
"_safe_json",
|
||||
"backend/placeholder_resolver.py",
|
||||
"_get_focus_areas_weighted_json",
|
||||
"Gewichtete Fokusbereiche mit Namen (JSON)",
|
||||
OutputType.JSON,
|
||||
PlaceholderType.RAW_DATA,
|
||||
),
|
||||
(
|
||||
"focus_areas_weighted_md",
|
||||
"_safe_str",
|
||||
"backend/placeholder_resolver.py",
|
||||
"_format_focus_areas_as_markdown",
|
||||
"Gewichtete Fokusbereiche als Markdown",
|
||||
OutputType.TEXT_SUMMARY,
|
||||
PlaceholderType.INTERPRETED,
|
||||
),
|
||||
(
|
||||
"focus_area_weights_json",
|
||||
"_safe_json",
|
||||
"backend/data_layer/scores.py",
|
||||
"get_user_focus_weights",
|
||||
"Rohe Gewichtungen key→Anteil (JSON)",
|
||||
OutputType.JSON,
|
||||
PlaceholderType.RAW_DATA,
|
||||
),
|
||||
]:
|
||||
m = PlaceholderMetadata(
|
||||
key=key,
|
||||
category=CAT,
|
||||
description=desc,
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function=res_fn,
|
||||
data_layer_module=dl_mod,
|
||||
data_layer_function=dl_fn,
|
||||
source_tables=["goals", "focus_area_definitions", "user_focus_area_weights"],
|
||||
semantic_contract=f"{dl_fn} (siehe Modul {dl_mod})",
|
||||
business_meaning="Strukturierte Übersicht für Prompts",
|
||||
unit="JSON" if out == OutputType.JSON else "markdown",
|
||||
time_window="aktuell",
|
||||
output_type=out,
|
||||
placeholder_type=ptype,
|
||||
format_hint="String aus Resolver",
|
||||
example_output="[]" if out == OutputType.JSON else "—",
|
||||
minimum_data_requirements="Ziele bzw. Fokusgewichte",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="Resolver + goal_utils / scores",
|
||||
missing_value_policy=MVP("insufficient_data", "[]" if out == OutputType.JSON else "nicht verfügbar"),
|
||||
known_limitations=None,
|
||||
layer_1_decision=dl_fn,
|
||||
layer_2a_decision=res_fn,
|
||||
layer_2b_reuse_possible=False,
|
||||
architecture_alignment="Phase 0b",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
tag_standard_evidence(m)
|
||||
register_placeholder(m)
|
||||
|
||||
for key, res_fn, dl_fn, desc, ex in [
|
||||
(
|
||||
"top_3_focus_areas",
|
||||
"_safe_str",
|
||||
"_format_top_focus_areas",
|
||||
"Top-3 Fokusbereiche als formatierter Text",
|
||||
"1. Kraft …",
|
||||
),
|
||||
(
|
||||
"top_3_goals_behind_schedule",
|
||||
"_safe_str",
|
||||
"_format_goals_behind",
|
||||
"Bis zu drei Ziele hinter Zeitplan",
|
||||
"—",
|
||||
),
|
||||
(
|
||||
"top_3_goals_on_track",
|
||||
"_safe_str",
|
||||
"_format_goals_on_track",
|
||||
"Bis zu drei Ziele im Plan",
|
||||
"—",
|
||||
),
|
||||
]:
|
||||
m = PlaceholderMetadata(
|
||||
key=key,
|
||||
category=CAT,
|
||||
description=desc,
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function=res_fn,
|
||||
data_layer_module="backend/goal_utils.py",
|
||||
data_layer_function="get_active_goals",
|
||||
source_tables=["goals", "focus_area_definitions"],
|
||||
semantic_contract=f"Resolver {dl_fn}",
|
||||
business_meaning="Kurzlisten für Coaching-Prompts",
|
||||
unit="text",
|
||||
time_window="aktuell",
|
||||
output_type=OutputType.TEXT_SUMMARY,
|
||||
placeholder_type=PlaceholderType.INTERPRETED,
|
||||
format_hint="Freitext / Aufzählung",
|
||||
example_output=ex,
|
||||
minimum_data_requirements="Ziele / Fokusdaten",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic=dl_fn,
|
||||
missing_value_policy=MVP("insufficient_data", "nicht verfügbar"),
|
||||
known_limitations=None,
|
||||
layer_1_decision="goals + focus aggregation",
|
||||
layer_2a_decision=dl_fn,
|
||||
layer_2b_reuse_possible=False,
|
||||
architecture_alignment="Phase 0b",
|
||||
issue_53_alignment="Layer 2a",
|
||||
evidence={},
|
||||
)
|
||||
tag_standard_evidence(m)
|
||||
register_placeholder(m)
|
||||
|
||||
|
||||
register_phase_0b_ziele_fokus()
|
||||
139
backend/placeholder_registrations/profil_zeitraum.py
Normal file
139
backend/placeholder_registrations/profil_zeitraum.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
"""
|
||||
Registry: Profil-Stammdaten und statische Zeitraum-Labels für Prompts.
|
||||
"""
|
||||
|
||||
from placeholder_registry import (
|
||||
PlaceholderMetadata,
|
||||
MissingValuePolicy,
|
||||
OutputType,
|
||||
PlaceholderType,
|
||||
register_placeholder,
|
||||
)
|
||||
from ._evidence import tag_standard_evidence
|
||||
|
||||
MVP = lambda reason, disp: MissingValuePolicy(
|
||||
available=False, value_raw=None, missing_reason=reason, legacy_display=disp
|
||||
)
|
||||
|
||||
|
||||
def register_profil_zeitraum():
|
||||
cat_profil = "Profil"
|
||||
for key, desc, res_fn, unit, ptype, out, hint, ex, sem in [
|
||||
(
|
||||
"name",
|
||||
"Anzeigename aus profiles.name",
|
||||
"get_profile_name",
|
||||
"text",
|
||||
PlaceholderType.ATOMIC,
|
||||
OutputType.STRING,
|
||||
"Kurzname",
|
||||
"Max",
|
||||
"profiles.name, Fallback „Nutzer“.",
|
||||
),
|
||||
(
|
||||
"age",
|
||||
"Alter in Jahren aus profiles.dob",
|
||||
"get_profile_age_display",
|
||||
"Jahre",
|
||||
PlaceholderType.ATOMIC,
|
||||
OutputType.STRING,
|
||||
"Ganzzahl oder unbekannt",
|
||||
"42",
|
||||
"Berechnung aus Geburtsdatum; PostgreSQL date oder ISO-String.",
|
||||
),
|
||||
(
|
||||
"height",
|
||||
"Körpergröße (cm) aus profiles.height",
|
||||
"get_profile_height_display",
|
||||
"cm",
|
||||
PlaceholderType.ATOMIC,
|
||||
OutputType.STRING,
|
||||
"Zahl oder unbekannt",
|
||||
"180",
|
||||
"profiles.height.",
|
||||
),
|
||||
(
|
||||
"geschlecht",
|
||||
"Geschlecht (männlich/weiblich) aus profiles.sex",
|
||||
"get_profile_geschlecht_display",
|
||||
"Kategorie",
|
||||
PlaceholderType.ATOMIC,
|
||||
OutputType.STRING,
|
||||
"m/w-Mapping",
|
||||
"männlich",
|
||||
"sex == 'm' → männlich, sonst weiblich.",
|
||||
),
|
||||
]:
|
||||
m = PlaceholderMetadata(
|
||||
key=key,
|
||||
category=cat_profil,
|
||||
description=desc,
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function=res_fn,
|
||||
data_layer_module=None,
|
||||
data_layer_function=None,
|
||||
source_tables=["profiles"],
|
||||
semantic_contract=sem,
|
||||
business_meaning="Profil-Kontext für KI-Prompts",
|
||||
unit=unit,
|
||||
time_window="latest profile row",
|
||||
output_type=out,
|
||||
placeholder_type=ptype,
|
||||
format_hint=hint,
|
||||
example_output=ex,
|
||||
minimum_data_requirements="Profilzeile",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="Row vorhanden",
|
||||
missing_value_policy=MVP("no_data", "unbekannt" if key != "name" else "Nutzer"),
|
||||
known_limitations="Keine diversen Geschlechtsoptionen im Platzhalter",
|
||||
layer_1_decision="profiles",
|
||||
layer_2a_decision=res_fn,
|
||||
layer_2b_reuse_possible=False,
|
||||
architecture_alignment="Phase 0b",
|
||||
issue_53_alignment="Resolver",
|
||||
evidence={},
|
||||
)
|
||||
tag_standard_evidence(m)
|
||||
register_placeholder(m)
|
||||
|
||||
cat_zeit = "Zeitraum"
|
||||
for key, desc, res_fn, sem, ex_out in [
|
||||
("datum_heute", "Heutiges Datum (lokal)", "get_datum_heute", "datetime.now, Format dd.mm.yyyy", "11.04.2026"),
|
||||
("zeitraum_7d", "Label „letzte 7 Tage“", "get_zeitraum_label_7d", "Statisches UI/Prompt-Label", "letzte 7 Tage"),
|
||||
("zeitraum_30d", "Label „letzte 30 Tage“", "get_zeitraum_label_30d", "Statisches UI/Prompt-Label", "letzte 30 Tage"),
|
||||
("zeitraum_90d", "Label „letzte 90 Tage“", "get_zeitraum_label_90d", "Statisches UI/Prompt-Label", "letzte 90 Tage"),
|
||||
]:
|
||||
m = PlaceholderMetadata(
|
||||
key=key,
|
||||
category=cat_zeit,
|
||||
description=desc,
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function=res_fn,
|
||||
data_layer_module=None,
|
||||
data_layer_function=None,
|
||||
source_tables=[],
|
||||
semantic_contract=sem,
|
||||
business_meaning="Zeitlicher Bezug im Prompt ohne Datenabfrage",
|
||||
unit="label",
|
||||
time_window="n/a",
|
||||
output_type=OutputType.STRING,
|
||||
placeholder_type=PlaceholderType.META,
|
||||
format_hint="Kurzdeutsch",
|
||||
example_output=ex_out,
|
||||
minimum_data_requirements=None,
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="Immer verfügbar",
|
||||
missing_value_policy=None,
|
||||
known_limitations="Kein kalender-basierter Datenfilter allein durch Platzhalter",
|
||||
layer_1_decision="n/a",
|
||||
layer_2a_decision=res_fn,
|
||||
layer_2b_reuse_possible=False,
|
||||
architecture_alignment="Phase 0b",
|
||||
issue_53_alignment="Resolver",
|
||||
evidence={},
|
||||
)
|
||||
tag_standard_evidence(m)
|
||||
register_placeholder(m)
|
||||
|
||||
|
||||
register_profil_zeitraum()
|
||||
236
backend/placeholder_registrations/schlaf_erholung.py
Normal file
236
backend/placeholder_registrations/schlaf_erholung.py
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
"""
|
||||
Registry: Schlaf, Ruhetage, Recovery-Score, Schlaf-Metriken, Schlaf-Erholungs-Korrelation.
|
||||
"""
|
||||
|
||||
from placeholder_registry import (
|
||||
PlaceholderMetadata,
|
||||
MissingValuePolicy,
|
||||
EvidenceType,
|
||||
OutputType,
|
||||
PlaceholderType,
|
||||
register_placeholder,
|
||||
)
|
||||
|
||||
CAT = "Schlaf & Erholung"
|
||||
MVP = lambda reason, disp: MissingValuePolicy(
|
||||
available=False, value_raw=None, missing_reason=reason, legacy_display=disp
|
||||
)
|
||||
|
||||
|
||||
def _tag(m: PlaceholderMetadata):
|
||||
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",
|
||||
):
|
||||
m.set_evidence(f, EvidenceType.CODE_DERIVED)
|
||||
m.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
||||
m.set_evidence("known_limitations", EvidenceType.MIXED)
|
||||
|
||||
|
||||
def register_schlaf_erholung():
|
||||
# ── formatierte Schlaf-/Ruhetage-Snapshots ───────────────────────────────
|
||||
m = PlaceholderMetadata(
|
||||
key="sleep_avg_duration",
|
||||
category=CAT,
|
||||
description="Durchschnittliche Schlafdauer (Stunden), formatiert",
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="get_sleep_avg_duration",
|
||||
data_layer_module="backend/data_layer/recovery_metrics.py",
|
||||
data_layer_function="get_sleep_duration_data",
|
||||
source_tables=["sleep_log"],
|
||||
semantic_contract="Mittel aus Schlafphasen im Fenster (siehe get_sleep_duration_data).",
|
||||
business_meaning="KI-Kontext Schlafdauer",
|
||||
unit="h (Anzeige mit Einheit)",
|
||||
time_window="7d default im Resolver",
|
||||
output_type=OutputType.STRING,
|
||||
placeholder_type=PlaceholderType.INTERPRETED,
|
||||
format_hint="z. B. 7.2h",
|
||||
example_output="7.2h",
|
||||
minimum_data_requirements="sleep_log im Fenster",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="data['confidence'] im Layer1",
|
||||
missing_value_policy=MVP("no_data", "nicht verfügbar"),
|
||||
known_limitations="Abhängig von Import/Qualität der Phasen",
|
||||
layer_1_decision="recovery_metrics.get_sleep_duration_data",
|
||||
layer_2a_decision="get_sleep_avg_duration",
|
||||
layer_2b_reuse_possible=True,
|
||||
architecture_alignment="Phase 0c",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
_tag(m)
|
||||
register_placeholder(m)
|
||||
|
||||
m = PlaceholderMetadata(
|
||||
key="sleep_avg_quality",
|
||||
category=CAT,
|
||||
description="Schlafqualität (Deep+REM %), formatiert",
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="get_sleep_avg_quality",
|
||||
data_layer_module="backend/data_layer/recovery_metrics.py",
|
||||
data_layer_function="get_sleep_quality_data",
|
||||
source_tables=["sleep_log"],
|
||||
semantic_contract="Anteil Deep+REM aus Segmenten (siehe get_sleep_quality_data).",
|
||||
business_meaning="KI-Kontext Schlafqualität",
|
||||
unit="%",
|
||||
time_window="7d default",
|
||||
output_type=OutputType.STRING,
|
||||
placeholder_type=PlaceholderType.INTERPRETED,
|
||||
format_hint="Prozent oder nicht verfügbar",
|
||||
example_output="24%",
|
||||
minimum_data_requirements="sleep_log mit Phasen",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="Layer-1-Confidence",
|
||||
missing_value_policy=MVP("no_data", "nicht verfügbar"),
|
||||
known_limitations="Segment-Schreibweise case-sensitiv normalisiert",
|
||||
layer_1_decision="recovery_metrics.get_sleep_quality_data",
|
||||
layer_2a_decision="get_sleep_avg_quality",
|
||||
layer_2b_reuse_possible=True,
|
||||
architecture_alignment="Phase 0c",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
_tag(m)
|
||||
register_placeholder(m)
|
||||
|
||||
m = PlaceholderMetadata(
|
||||
key="rest_days_count",
|
||||
category=CAT,
|
||||
description="Anzahl dokumentierter Ruhetage (30d default)",
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="get_rest_days_count",
|
||||
data_layer_module="backend/data_layer/recovery_metrics.py",
|
||||
data_layer_function="get_rest_days_data",
|
||||
source_tables=["rest_days"],
|
||||
semantic_contract="Count rest_days im Zeitraum",
|
||||
business_meaning="Aktive/passive Erholungstags-Übersicht",
|
||||
unit="count",
|
||||
time_window="30d default",
|
||||
output_type=OutputType.STRING,
|
||||
placeholder_type=PlaceholderType.RAW_DATA,
|
||||
format_hint="z. B. 2 Ruhetage",
|
||||
example_output="2 Ruhetage",
|
||||
minimum_data_requirements="rest_days",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="Immer Zählung, 0 möglich",
|
||||
missing_value_policy=MVP("no_data", "0 Ruhetage"),
|
||||
known_limitations="Nur explizit erfasste Ruhetage",
|
||||
layer_1_decision="recovery_metrics.get_rest_days_data",
|
||||
layer_2a_decision="get_rest_days_count",
|
||||
layer_2b_reuse_possible=True,
|
||||
architecture_alignment="Phase 0c",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
_tag(m)
|
||||
register_placeholder(m)
|
||||
|
||||
m = PlaceholderMetadata(
|
||||
key="recovery_score",
|
||||
category=CAT,
|
||||
description="Recovery-Score 0–100 (v2, komposit)",
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="_safe_int",
|
||||
data_layer_module="backend/data_layer/recovery_metrics.py",
|
||||
data_layer_function="calculate_recovery_score_v2",
|
||||
source_tables=["sleep_log", "vitals_baseline", "activity_log"],
|
||||
semantic_contract="Gewichteter Score aus Schlaf, Vitaltrends, optional Load (siehe Implementierung).",
|
||||
business_meaning="Gesamt-Recovery-KPI für Prompts",
|
||||
unit="0–100",
|
||||
time_window="composite",
|
||||
output_type=OutputType.NUMERIC,
|
||||
placeholder_type=PlaceholderType.SCORE,
|
||||
format_hint="Ganzzahl-String",
|
||||
example_output="72",
|
||||
minimum_data_requirements="Teilkomponenten je nach Gewichtung",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="Wie calculate_recovery_score_v2",
|
||||
missing_value_policy=MVP("insufficient_data", "nicht verfügbar"),
|
||||
known_limitations="Abhängig von Datenabdeckung HF/HRV/Schlaf",
|
||||
layer_1_decision="recovery_metrics.calculate_recovery_score_v2",
|
||||
layer_2a_decision="_safe_int('recovery_score_v2')",
|
||||
layer_2b_reuse_possible=True,
|
||||
architecture_alignment="Phase 0c",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
_tag(m)
|
||||
register_placeholder(m)
|
||||
|
||||
for key, dl_fn, desc, unit, tbls, res_fn in [
|
||||
("sleep_avg_duration_7d", "calculate_sleep_avg_duration_7d", "Durchschnittliche Schlafdauer 7d (h)", "h", ["sleep_log"], "_safe_float"),
|
||||
("sleep_debt_hours", "calculate_sleep_debt_hours", "Kumulative Schlafschuld (h)", "h", ["sleep_log"], "_safe_float"),
|
||||
("sleep_regularity_proxy", "calculate_sleep_regularity_proxy", "Schlaf-Regularität (Proxy)", "min", ["sleep_log"], "_safe_float"),
|
||||
("recent_load_balance_3d", "calculate_recent_load_balance_3d", "Load-Balance 3d (Score)", "score", ["activity_log"], "_safe_int"),
|
||||
("sleep_quality_7d", "calculate_sleep_quality_7d", "Schlafqualität 7d (0–100)", "0-100", ["sleep_log"], "_safe_int"),
|
||||
]:
|
||||
m = PlaceholderMetadata(
|
||||
key=key,
|
||||
category=CAT,
|
||||
description=desc,
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function=res_fn,
|
||||
data_layer_module="backend/data_layer/recovery_metrics.py",
|
||||
data_layer_function=dl_fn,
|
||||
source_tables=tbls,
|
||||
semantic_contract=f"Berechnung {dl_fn} in recovery_metrics.",
|
||||
business_meaning="Erholungs-Detailmetrik",
|
||||
unit=unit,
|
||||
time_window="siehe Funktion",
|
||||
output_type=OutputType.NUMERIC,
|
||||
placeholder_type=PlaceholderType.INTERPRETED,
|
||||
format_hint="numerischer String",
|
||||
example_output="1.0",
|
||||
minimum_data_requirements="wie Funktion",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="Funktionsintern",
|
||||
missing_value_policy=MVP("insufficient_data", "nicht verfügbar"),
|
||||
known_limitations=None,
|
||||
layer_1_decision=f"recovery_metrics.{dl_fn}",
|
||||
layer_2a_decision="Resolver _safe_float/_safe_int",
|
||||
layer_2b_reuse_possible=True,
|
||||
architecture_alignment="Phase 0c",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
_tag(m)
|
||||
register_placeholder(m)
|
||||
|
||||
m = PlaceholderMetadata(
|
||||
key="correlation_sleep_recovery",
|
||||
category=CAT,
|
||||
description="JSON: Korrelation Schlaf ↔ Recovery-Indikatoren",
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="_safe_json",
|
||||
data_layer_module="backend/data_layer/correlations.py",
|
||||
data_layer_function="calculate_correlation_sleep_recovery",
|
||||
source_tables=["sleep_log", "vitals_baseline", "activity_log"],
|
||||
semantic_contract="Strukturierte Korrelationsauswertung (siehe correlations).",
|
||||
business_meaning="KI: Zusammenhänge Schlaf und Erholung",
|
||||
unit="JSON",
|
||||
time_window="funktionsabhängig",
|
||||
output_type=OutputType.JSON,
|
||||
placeholder_type=PlaceholderType.RAW_DATA,
|
||||
format_hint="JSON-String",
|
||||
example_output="{}",
|
||||
minimum_data_requirements="Ausreichend gekoppelte Datenpunkte",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="Wie correlation_metrics",
|
||||
missing_value_policy=MVP("insufficient_data", "{}"),
|
||||
known_limitations="Bei wenig Daten leer oder schwach",
|
||||
layer_1_decision="correlations.calculate_correlation_sleep_recovery",
|
||||
layer_2a_decision="_safe_json",
|
||||
layer_2b_reuse_possible=True,
|
||||
architecture_alignment="Phase 0c",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
_tag(m)
|
||||
register_placeholder(m)
|
||||
|
||||
|
||||
register_schlaf_erholung()
|
||||
180
backend/placeholder_registrations/vitalwerte.py
Normal file
180
backend/placeholder_registrations/vitalwerte.py
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
"""
|
||||
Registry: Baseline-Vitals (Ruhepuls, HRV, VO2 Max) und Abweichung vs. persönlicher Baseline.
|
||||
"""
|
||||
|
||||
from placeholder_registry import (
|
||||
PlaceholderMetadata,
|
||||
MissingValuePolicy,
|
||||
EvidenceType,
|
||||
OutputType,
|
||||
PlaceholderType,
|
||||
register_placeholder,
|
||||
)
|
||||
|
||||
CAT = "Vitalwerte"
|
||||
MVP = lambda reason, disp: MissingValuePolicy(
|
||||
available=False, value_raw=None, missing_reason=reason, legacy_display=disp
|
||||
)
|
||||
|
||||
|
||||
def _tag(m: PlaceholderMetadata):
|
||||
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",
|
||||
):
|
||||
m.set_evidence(f, EvidenceType.CODE_DERIVED)
|
||||
m.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED)
|
||||
m.set_evidence("known_limitations", EvidenceType.MIXED)
|
||||
|
||||
|
||||
def register_vitalwerte():
|
||||
m = PlaceholderMetadata(
|
||||
key="vitals_avg_hr",
|
||||
category=CAT,
|
||||
description="Durchschnittlicher Ruhepuls (7d), formatiert",
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="get_vitals_avg_hr",
|
||||
data_layer_module="backend/data_layer/health_metrics.py",
|
||||
data_layer_function="get_resting_heart_rate_data",
|
||||
source_tables=["vitals_baseline"],
|
||||
semantic_contract="Mittel RHR aus vitals_baseline im Fenster (siehe health_metrics).",
|
||||
business_meaning="KI-Kontext kardiovaskuläre Ruhelage",
|
||||
unit="bpm (Anzeige mit Einheit)",
|
||||
time_window="7d default im Resolver",
|
||||
output_type=OutputType.STRING,
|
||||
placeholder_type=PlaceholderType.INTERPRETED,
|
||||
format_hint="z. B. 58 bpm",
|
||||
example_output="58 bpm",
|
||||
minimum_data_requirements="vitals_baseline im Fenster",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="data['confidence'] im Layer1",
|
||||
missing_value_policy=MVP("no_data", "nicht verfügbar"),
|
||||
known_limitations="Nur erfasste Morgen-Baseline-Messungen",
|
||||
layer_1_decision="health_metrics.get_resting_heart_rate_data",
|
||||
layer_2a_decision="get_vitals_avg_hr",
|
||||
layer_2b_reuse_possible=True,
|
||||
architecture_alignment="Phase 0c",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
_tag(m)
|
||||
register_placeholder(m)
|
||||
|
||||
m = PlaceholderMetadata(
|
||||
key="vitals_avg_hrv",
|
||||
category=CAT,
|
||||
description="Durchschnittliche HRV (7d), formatiert",
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="get_vitals_avg_hrv",
|
||||
data_layer_module="backend/data_layer/health_metrics.py",
|
||||
data_layer_function="get_heart_rate_variability_data",
|
||||
source_tables=["vitals_baseline"],
|
||||
semantic_contract="Mittel HRV aus vitals_baseline im Fenster.",
|
||||
business_meaning="KI-Kontext autonome Regulation / Erholung",
|
||||
unit="ms (Anzeige mit Einheit)",
|
||||
time_window="7d default",
|
||||
output_type=OutputType.STRING,
|
||||
placeholder_type=PlaceholderType.INTERPRETED,
|
||||
format_hint="z. B. 45 ms",
|
||||
example_output="45 ms",
|
||||
minimum_data_requirements="vitals_baseline mit HRV",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="data['confidence'] im Layer1",
|
||||
missing_value_policy=MVP("no_data", "nicht verfügbar"),
|
||||
known_limitations="Geräte-/Messprotokoll kann streuen",
|
||||
layer_1_decision="health_metrics.get_heart_rate_variability_data",
|
||||
layer_2a_decision="get_vitals_avg_hrv",
|
||||
layer_2b_reuse_possible=True,
|
||||
architecture_alignment="Phase 0c",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
_tag(m)
|
||||
register_placeholder(m)
|
||||
|
||||
m = PlaceholderMetadata(
|
||||
key="vitals_vo2_max",
|
||||
category=CAT,
|
||||
description="Aktueller VO2 Max (letzte Messung), formatiert",
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function="get_vitals_vo2_max",
|
||||
data_layer_module="backend/data_layer/health_metrics.py",
|
||||
data_layer_function="get_vo2_max_data",
|
||||
source_tables=["vitals_baseline"],
|
||||
semantic_contract="Jüngster vo2_max aus vitals_baseline.",
|
||||
business_meaning="Ausdauer-/Fitness-Kontext",
|
||||
unit="ml/kg/min",
|
||||
time_window="latest",
|
||||
output_type=OutputType.STRING,
|
||||
placeholder_type=PlaceholderType.INTERPRETED,
|
||||
format_hint="eine Dezimalstelle + Einheit",
|
||||
example_output="42.0 ml/kg/min",
|
||||
minimum_data_requirements="mindestens eine VO2-Messung",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="data['confidence'] im Layer1",
|
||||
missing_value_policy=MVP("no_data", "nicht verfügbar"),
|
||||
known_limitations="Schätzung vs. Labortest je nach Quelle",
|
||||
layer_1_decision="health_metrics.get_vo2_max_data",
|
||||
layer_2a_decision="get_vitals_vo2_max",
|
||||
layer_2b_reuse_possible=True,
|
||||
architecture_alignment="Phase 0c",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
_tag(m)
|
||||
register_placeholder(m)
|
||||
|
||||
for key, dl_fn, desc, unit, res_fn in [
|
||||
(
|
||||
"hrv_vs_baseline_pct",
|
||||
"calculate_hrv_vs_baseline_pct",
|
||||
"HRV vs. persönlicher Baseline (%)",
|
||||
"%",
|
||||
"_safe_float",
|
||||
),
|
||||
(
|
||||
"rhr_vs_baseline_pct",
|
||||
"calculate_rhr_vs_baseline_pct",
|
||||
"Ruhepuls vs. persönlicher Baseline (%)",
|
||||
"%",
|
||||
"_safe_float",
|
||||
),
|
||||
]:
|
||||
m = PlaceholderMetadata(
|
||||
key=key,
|
||||
category=CAT,
|
||||
description=desc,
|
||||
resolver_module="backend/placeholder_resolver.py",
|
||||
resolver_function=res_fn,
|
||||
data_layer_module="backend/data_layer/recovery_metrics.py",
|
||||
data_layer_function=dl_fn,
|
||||
source_tables=["vitals_baseline"],
|
||||
semantic_contract=f"Vergleich aktueller Wert zu Baseline (siehe {dl_fn}).",
|
||||
business_meaning="Erholungs- und Belastungsindikator relativ zur Norm des Nutzers",
|
||||
unit=unit,
|
||||
time_window="funktionsintern",
|
||||
output_type=OutputType.NUMERIC,
|
||||
placeholder_type=PlaceholderType.INTERPRETED,
|
||||
format_hint="numerischer Prozent-String",
|
||||
example_output="5.2",
|
||||
minimum_data_requirements="Ausreichend Baseline-Historie",
|
||||
quality_filter_policy=None,
|
||||
confidence_logic="Funktionsintern",
|
||||
missing_value_policy=MVP("insufficient_data", "nicht verfügbar"),
|
||||
known_limitations="Baseline braucht ausreichend Vorlauf",
|
||||
layer_1_decision=f"recovery_metrics.{dl_fn}",
|
||||
layer_2a_decision=f"Resolver {res_fn}",
|
||||
layer_2b_reuse_possible=True,
|
||||
architecture_alignment="Phase 0c",
|
||||
issue_53_alignment="Layer 1",
|
||||
evidence={},
|
||||
)
|
||||
_tag(m)
|
||||
register_placeholder(m)
|
||||
|
||||
|
||||
register_vitalwerte()
|
||||
|
|
@ -9,7 +9,7 @@ This module now focuses on FORMATTING for AI consumption.
|
|||
"""
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Callable
|
||||
from typing import Dict, List, Optional, Callable, Tuple
|
||||
from db import get_db, get_cursor, r2d
|
||||
|
||||
# Phase 0c: Import data layer
|
||||
|
|
@ -277,6 +277,43 @@ def calculate_age(dob) -> str:
|
|||
return "unbekannt"
|
||||
|
||||
|
||||
def get_profile_name(profile_id: str) -> str:
|
||||
"""Profil-Platzhalter: Anzeigename (profiles.name)."""
|
||||
return get_profile_data(profile_id).get('name', 'Nutzer')
|
||||
|
||||
|
||||
def get_profile_age_display(profile_id: str) -> str:
|
||||
"""Profil-Platzhalter: Alter aus Geburtsdatum."""
|
||||
return calculate_age(get_profile_data(profile_id).get('dob'))
|
||||
|
||||
|
||||
def get_profile_height_display(profile_id: str) -> str:
|
||||
"""Profil-Platzhalter: Körpergröße (cm) als String."""
|
||||
return str(get_profile_data(profile_id).get('height', 'unbekannt'))
|
||||
|
||||
|
||||
def get_profile_geschlecht_display(profile_id: str) -> str:
|
||||
"""Profil-Platzhalter: Geschlecht aus profiles.sex (m/w)."""
|
||||
return 'männlich' if get_profile_data(profile_id).get('sex') == 'm' else 'weiblich'
|
||||
|
||||
|
||||
def get_datum_heute(_profile_id: str) -> str:
|
||||
"""Zeitraum-Platzhalter: heutiges Datum (dd.mm.yyyy)."""
|
||||
return datetime.now().strftime('%d.%m.%Y')
|
||||
|
||||
|
||||
def get_zeitraum_label_7d(_profile_id: str) -> str:
|
||||
return 'letzte 7 Tage'
|
||||
|
||||
|
||||
def get_zeitraum_label_30d(_profile_id: str) -> str:
|
||||
return 'letzte 30 Tage'
|
||||
|
||||
|
||||
def get_zeitraum_label_90d(_profile_id: str) -> str:
|
||||
return 'letzte 90 Tage'
|
||||
|
||||
|
||||
def get_activity_detail(profile_id: str, days: int = 14) -> str:
|
||||
"""
|
||||
Get detailed activity log for analysis.
|
||||
|
|
@ -1136,10 +1173,10 @@ def _format_goals_on_track(profile_id: str, n: int = 3) -> str:
|
|||
|
||||
PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = {
|
||||
# Profil
|
||||
'{{name}}': lambda pid: get_profile_data(pid).get('name', 'Nutzer'),
|
||||
'{{age}}': lambda pid: calculate_age(get_profile_data(pid).get('dob')),
|
||||
'{{height}}': lambda pid: str(get_profile_data(pid).get('height', 'unbekannt')),
|
||||
'{{geschlecht}}': lambda pid: 'männlich' if get_profile_data(pid).get('sex') == 'm' else 'weiblich',
|
||||
'{{name}}': get_profile_name,
|
||||
'{{age}}': get_profile_age_display,
|
||||
'{{height}}': get_profile_height_display,
|
||||
'{{geschlecht}}': get_profile_geschlecht_display,
|
||||
|
||||
# Körper (21 Registry-Keys: body_metrics + body_extras — alles hier gebündelt)
|
||||
'{{weight_aktuell}}': get_latest_weight,
|
||||
|
|
@ -1203,29 +1240,37 @@ PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = {
|
|||
'{{training_inter_session_gap_md}}': get_training_inter_session_gap_md,
|
||||
'{{training_sessions_recent_json}}': lambda pid: _safe_json('training_sessions_recent_json', pid),
|
||||
|
||||
# Schlaf & Erholung
|
||||
# Schlaf & Erholung (10 Registry-Keys; recovery_score hier, nicht unter Meta Scores)
|
||||
'{{sleep_avg_duration}}': lambda pid: get_sleep_avg_duration(pid, 7),
|
||||
'{{sleep_avg_quality}}': lambda pid: get_sleep_avg_quality(pid, 7),
|
||||
'{{rest_days_count}}': lambda pid: get_rest_days_count(pid, 30),
|
||||
'{{recovery_score}}': lambda pid: _safe_int('recovery_score_v2', pid),
|
||||
'{{sleep_avg_duration_7d}}': lambda pid: _safe_float('sleep_avg_duration_7d', pid),
|
||||
'{{sleep_debt_hours}}': lambda pid: _safe_float('sleep_debt_hours', pid),
|
||||
'{{sleep_regularity_proxy}}': lambda pid: _safe_float('sleep_regularity_proxy', pid),
|
||||
'{{recent_load_balance_3d}}': lambda pid: _safe_int('recent_load_balance_3d', pid),
|
||||
'{{sleep_quality_7d}}': lambda pid: _safe_int('sleep_quality_7d', pid),
|
||||
'{{correlation_sleep_recovery}}': lambda pid: _safe_json('correlation_sleep_recovery', pid),
|
||||
|
||||
# Vitalwerte
|
||||
# Vitalwerte (5 Registry-Keys: Mittelwerte + vs. Baseline)
|
||||
'{{vitals_avg_hr}}': lambda pid: get_vitals_avg_hr(pid, 7),
|
||||
'{{vitals_avg_hrv}}': lambda pid: get_vitals_avg_hrv(pid, 7),
|
||||
'{{vitals_vo2_max}}': get_vitals_vo2_max,
|
||||
'{{hrv_vs_baseline_pct}}': lambda pid: _safe_float('hrv_vs_baseline_pct', pid),
|
||||
'{{rhr_vs_baseline_pct}}': lambda pid: _safe_float('rhr_vs_baseline_pct', pid),
|
||||
|
||||
# Zeitraum
|
||||
'{{datum_heute}}': lambda pid: datetime.now().strftime('%d.%m.%Y'),
|
||||
'{{zeitraum_7d}}': lambda pid: 'letzte 7 Tage',
|
||||
'{{zeitraum_30d}}': lambda pid: 'letzte 30 Tage',
|
||||
'{{zeitraum_90d}}': lambda pid: 'letzte 90 Tage',
|
||||
'{{datum_heute}}': get_datum_heute,
|
||||
'{{zeitraum_7d}}': get_zeitraum_label_7d,
|
||||
'{{zeitraum_30d}}': get_zeitraum_label_30d,
|
||||
'{{zeitraum_90d}}': get_zeitraum_label_90d,
|
||||
|
||||
# ========================================================================
|
||||
# PHASE 0b: Goal-Aware Placeholders (Dynamic Focus Areas v2.0)
|
||||
# ========================================================================
|
||||
|
||||
# --- Meta Scores (Ebene 1: Aggregierte Scores; body/nutrition/activity scores → jeweilige Kategorie) ---
|
||||
# --- Meta Scores (Ebene 1; recovery_score → Schlaf & Erholung) ---
|
||||
'{{goal_progress_score}}': lambda pid: _safe_int('goal_progress_score', pid),
|
||||
'{{recovery_score}}': lambda pid: _safe_int('recovery_score_v2', pid),
|
||||
'{{data_quality_score}}': lambda pid: _safe_int('data_quality_score', pid),
|
||||
|
||||
# --- Top-Weighted Goals/Focus Areas (Ebene 2: statt Primary) ---
|
||||
|
|
@ -1251,21 +1296,11 @@ PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = {
|
|||
'{{focus_cat_lebensstil_progress}}': lambda pid: _safe_int('focus_cat_lebensstil_progress', pid),
|
||||
'{{focus_cat_lebensstil_weight}}': lambda pid: _safe_float('focus_cat_lebensstil_weight', pid),
|
||||
|
||||
# --- Recovery Metrics (Recovery Score v2) ---
|
||||
'{{hrv_vs_baseline_pct}}': lambda pid: _safe_float('hrv_vs_baseline_pct', pid),
|
||||
'{{rhr_vs_baseline_pct}}': lambda pid: _safe_float('rhr_vs_baseline_pct', pid),
|
||||
'{{sleep_avg_duration_7d}}': lambda pid: _safe_float('sleep_avg_duration_7d', pid),
|
||||
'{{sleep_debt_hours}}': lambda pid: _safe_float('sleep_debt_hours', pid),
|
||||
'{{sleep_regularity_proxy}}': lambda pid: _safe_float('sleep_regularity_proxy', pid),
|
||||
'{{recent_load_balance_3d}}': lambda pid: _safe_int('recent_load_balance_3d', pid),
|
||||
'{{sleep_quality_7d}}': lambda pid: _safe_int('sleep_quality_7d', pid),
|
||||
|
||||
# --- Correlation Metrics (C1-C7) ---
|
||||
'{{correlation_energy_weight_lag}}': lambda pid: _safe_json('correlation_energy_weight_lag', pid),
|
||||
'{{correlation_protein_lbm}}': lambda pid: _safe_json('correlation_protein_lbm', pid),
|
||||
'{{correlation_load_hrv}}': lambda pid: _safe_json('correlation_load_hrv', pid),
|
||||
'{{correlation_load_rhr}}': lambda pid: _safe_json('correlation_load_rhr', pid),
|
||||
'{{correlation_sleep_recovery}}': lambda pid: _safe_json('correlation_sleep_recovery', pid),
|
||||
'{{plateau_detected}}': lambda pid: _safe_json('plateau_detected', pid),
|
||||
'{{top_drivers}}': lambda pid: _safe_json('top_drivers', pid),
|
||||
|
||||
|
|
@ -1378,9 +1413,42 @@ def get_available_placeholders(categories: Optional[List[str]] = None) -> Dict[s
|
|||
'{{rest_day_compliance}}', '{{vo2max_trend_28d}}', '{{activity_score}}',
|
||||
'{{training_frequency_by_type_md}}', '{{training_inter_session_gap_md}}', '{{training_sessions_recent_json}}',
|
||||
],
|
||||
'schlaf': [
|
||||
'{{sleep_avg_duration}}', '{{sleep_avg_quality}}', '{{rest_days_count}}',
|
||||
'{{recovery_score}}',
|
||||
'{{sleep_avg_duration_7d}}', '{{sleep_debt_hours}}', '{{sleep_regularity_proxy}}',
|
||||
'{{recent_load_balance_3d}}', '{{sleep_quality_7d}}',
|
||||
'{{correlation_sleep_recovery}}',
|
||||
],
|
||||
'vitalwerte': [
|
||||
'{{vitals_avg_hr}}', '{{vitals_avg_hrv}}', '{{vitals_vo2_max}}',
|
||||
'{{hrv_vs_baseline_pct}}', '{{rhr_vs_baseline_pct}}',
|
||||
],
|
||||
'zeitraum': [
|
||||
'{{datum_heute}}', '{{zeitraum_7d}}', '{{zeitraum_30d}}', '{{zeitraum_90d}}'
|
||||
]
|
||||
],
|
||||
'phase0b_meta': [
|
||||
'{{goal_progress_score}}', '{{data_quality_score}}',
|
||||
],
|
||||
'ziele_fokus': [
|
||||
'{{top_goal_name}}', '{{top_goal_progress_pct}}', '{{top_goal_status}}',
|
||||
'{{top_focus_area_name}}', '{{top_focus_area_progress}}',
|
||||
'{{focus_cat_körper_progress}}', '{{focus_cat_körper_weight}}',
|
||||
'{{focus_cat_ernährung_progress}}', '{{focus_cat_ernährung_weight}}',
|
||||
'{{focus_cat_aktivität_progress}}', '{{focus_cat_aktivität_weight}}',
|
||||
'{{focus_cat_recovery_progress}}', '{{focus_cat_recovery_weight}}',
|
||||
'{{focus_cat_vitalwerte_progress}}', '{{focus_cat_vitalwerte_weight}}',
|
||||
'{{focus_cat_mental_progress}}', '{{focus_cat_mental_weight}}',
|
||||
'{{focus_cat_lebensstil_progress}}', '{{focus_cat_lebensstil_weight}}',
|
||||
'{{active_goals_json}}', '{{active_goals_md}}',
|
||||
'{{focus_areas_weighted_json}}', '{{focus_areas_weighted_md}}', '{{focus_area_weights_json}}',
|
||||
'{{top_3_focus_areas}}', '{{top_3_goals_behind_schedule}}', '{{top_3_goals_on_track}}',
|
||||
],
|
||||
'korrelationen': [
|
||||
'{{correlation_energy_weight_lag}}', '{{correlation_protein_lbm}}',
|
||||
'{{correlation_load_hrv}}', '{{correlation_load_rhr}}',
|
||||
'{{plateau_detected}}', '{{top_drivers}}',
|
||||
],
|
||||
}
|
||||
|
||||
if not categories:
|
||||
|
|
@ -1460,50 +1528,7 @@ def get_placeholder_catalog(profile_id: str) -> Dict[str, List[Dict[str, str]]]:
|
|||
})
|
||||
|
||||
# Legacy placeholders (not in registry yet)
|
||||
legacy_placeholders = {
|
||||
'Profil': [
|
||||
('name', 'Name des Nutzers'),
|
||||
('age', 'Alter in Jahren'),
|
||||
('height', 'Körpergröße in cm'),
|
||||
('geschlecht', 'Geschlecht'),
|
||||
],
|
||||
'Schlaf & Erholung': [
|
||||
('sleep_avg_duration', 'Durchschn. Schlafdauer (7d)'),
|
||||
('sleep_avg_quality', 'Durchschn. Schlafqualität (7d)'),
|
||||
('rest_days_count', 'Anzahl Ruhetage (30d)'),
|
||||
('sleep_avg_duration_7d', 'Schlaf 7d (Stunden)'),
|
||||
('sleep_debt_hours', 'Schlafschuld (Stunden)'),
|
||||
('sleep_regularity_proxy', 'Schlaf-Regelmäßigkeit (Min Abweichung)'),
|
||||
('sleep_quality_7d', 'Schlafqualität 7d (0-100)'),
|
||||
],
|
||||
'Vitalwerte': [
|
||||
('vitals_avg_hr', 'Durchschn. Ruhepuls (7d)'),
|
||||
('vitals_avg_hrv', 'Durchschn. HRV (7d)'),
|
||||
('vitals_vo2_max', 'Aktueller VO2 Max'),
|
||||
('hrv_vs_baseline_pct', 'HRV vs. Baseline (%)'),
|
||||
('rhr_vs_baseline_pct', 'RHR vs. Baseline (%)'),
|
||||
],
|
||||
'Scores (Phase 0b)': [
|
||||
('goal_progress_score', 'Goal Progress Score (0-100)'),
|
||||
('recovery_score', 'Recovery Score (0-100)'),
|
||||
('data_quality_score', 'Data Quality Score (0-100)'),
|
||||
],
|
||||
'Focus Areas': [
|
||||
('top_focus_area_name', 'Top Focus Area Name'),
|
||||
('top_focus_area_progress', 'Top Focus Area Progress (%)'),
|
||||
('focus_cat_körper_progress', 'Kategorie Körper - Progress (%)'),
|
||||
('focus_cat_körper_weight', 'Kategorie Körper - Gewichtung (%)'),
|
||||
('focus_cat_ernährung_progress', 'Kategorie Ernährung - Progress (%)'),
|
||||
('focus_cat_ernährung_weight', 'Kategorie Ernährung - Gewichtung (%)'),
|
||||
('focus_cat_aktivität_progress', 'Kategorie Aktivität - Progress (%)'),
|
||||
('focus_cat_aktivität_weight', 'Kategorie Aktivität - Gewichtung (%)'),
|
||||
],
|
||||
'Zeitraum': [
|
||||
('datum_heute', 'Heutiges Datum'),
|
||||
('zeitraum_7d', '7-Tage-Zeitraum'),
|
||||
('zeitraum_30d', '30-Tage-Zeitraum'),
|
||||
],
|
||||
}
|
||||
legacy_placeholders: Dict[str, List[Tuple[str, str]]] = {}
|
||||
|
||||
# Add legacy placeholders (skip if already in registry)
|
||||
for category, items in legacy_placeholders.items():
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user