feat: Enhance placeholder resolution and error handling
- Updated `extract_value_raw` to improve JSON parsing and handle unavailable data more effectively. - Introduced new functions in `placeholder_resolver.py` for standardized responses when data is unavailable, enhancing clarity for users and AI. - Modified various data retrieval functions to utilize the new response format, providing detailed reasons for unavailability. - Improved availability checks in `export_placeholder_values_extended` to account for new response formats. These changes enhance the robustness of the placeholder system and improve user experience by providing clearer error messages and data handling.
This commit is contained in:
parent
052ba195cc
commit
04e23d8115
|
|
@ -29,14 +29,22 @@ def extract_value_raw(value_display: str, output_type: OutputType, placeholder_t
|
|||
|
||||
Returns: (raw_value, success)
|
||||
"""
|
||||
if not value_display or value_display in ['nicht verfügbar', 'nicht genug Daten']:
|
||||
s = (value_display or "").strip()
|
||||
if (
|
||||
not s
|
||||
or s in ['nicht verfügbar', 'nicht genug Daten']
|
||||
or s.startswith('nicht verfügbar —')
|
||||
):
|
||||
# V2 strict mode: missing/unavailable value is not a successful extraction
|
||||
return None, False
|
||||
|
||||
# JSON output type
|
||||
if output_type == OutputType.JSON:
|
||||
try:
|
||||
return json.loads(value_display), True
|
||||
parsed = json.loads(value_display)
|
||||
if isinstance(parsed, dict) and parsed.get('_available') is False:
|
||||
return None, False
|
||||
return parsed, True
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
# Try to find JSON in string
|
||||
json_match = re.search(r'(\{.*\}|\[.*\])', value_display, re.DOTALL)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ Used for prompt templates and preview functionality.
|
|||
Phase 0c: Refactored to use data_layer for structured data.
|
||||
This module now focuses on FORMATTING for AI consumption.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Callable, Tuple
|
||||
|
|
@ -57,6 +58,32 @@ def get_profile_data(profile_id: str) -> Dict:
|
|||
return r2d(cur.fetchone()) if cur.rowcount > 0 else {}
|
||||
|
||||
|
||||
def pv_unavailable(reason: str, detail: Optional[str] = None) -> str:
|
||||
"""
|
||||
Standard-Antwort wenn kein Platzhalter-Wert lieferbar ist.
|
||||
Grund ist für Nutzer und KI lesbar (ggf. Alternativen im Text).
|
||||
"""
|
||||
r = (reason or "Keine auswertbaren Daten").strip()
|
||||
if detail:
|
||||
d = str(detail).strip()
|
||||
if d:
|
||||
return f"nicht verfügbar — {r} ({d})"
|
||||
return f"nicht verfügbar — {r}"
|
||||
|
||||
|
||||
def pv_unavailable_json(reason: str, detail: Optional[str] = None) -> str:
|
||||
"""Strukturierte JSON-Antwort statt leeres {} (für KI / Clients)."""
|
||||
payload: Dict[str, object] = {
|
||||
"_available": False,
|
||||
"_reason": (reason or "Keine auswertbaren Daten").strip(),
|
||||
}
|
||||
if detail:
|
||||
d = str(detail).strip()
|
||||
if d:
|
||||
payload["_detail"] = d
|
||||
return json.dumps(payload, ensure_ascii=False)
|
||||
|
||||
|
||||
def get_latest_weight(profile_id: str) -> Optional[str]:
|
||||
"""
|
||||
Get latest weight entry.
|
||||
|
|
@ -67,7 +94,10 @@ def get_latest_weight(profile_id: str) -> Optional[str]:
|
|||
data = get_latest_weight_data(profile_id)
|
||||
|
||||
if data['confidence'] == 'insufficient':
|
||||
return "nicht verfügbar"
|
||||
return pv_unavailable(
|
||||
"Kein aktuelles Gewicht",
|
||||
f"confidence={data.get('confidence')}, data_points={data.get('data_points',0)}",
|
||||
)
|
||||
|
||||
return f"{data['weight']:.1f} kg"
|
||||
|
||||
|
|
@ -82,7 +112,10 @@ def get_weight_trend(profile_id: str, days: int = 28) -> str:
|
|||
data = get_weight_trend_data(profile_id, days)
|
||||
|
||||
if data['confidence'] == 'insufficient':
|
||||
return "nicht genug Daten"
|
||||
return pv_unavailable(
|
||||
"Gewichtstrend nicht ermittelbar",
|
||||
f"confidence={data.get('confidence')}, Fenster={days} Tage",
|
||||
)
|
||||
|
||||
direction = data['direction']
|
||||
delta = data['delta']
|
||||
|
|
@ -105,7 +138,10 @@ def get_latest_bf(profile_id: str) -> Optional[str]:
|
|||
data = get_body_composition_data(profile_id)
|
||||
|
||||
if data['confidence'] == 'insufficient':
|
||||
return "nicht verfügbar"
|
||||
return pv_unavailable(
|
||||
"Körperfett nicht ermittelbar",
|
||||
f"confidence={data.get('confidence')} (keine ausreichenden Caliper-/Kompositionsdaten)",
|
||||
)
|
||||
|
||||
return f"{data['body_fat_pct']:.1f}%"
|
||||
|
||||
|
|
@ -120,7 +156,10 @@ def get_nutrition_avg(profile_id: str, field: str, days: int = 30) -> str:
|
|||
data = get_nutrition_average_data(profile_id, days)
|
||||
|
||||
if data['confidence'] == 'insufficient':
|
||||
return "nicht verfügbar"
|
||||
return pv_unavailable(
|
||||
"Ernährungsmittelwert nicht ermittelbar",
|
||||
f"confidence={data.get('confidence')}, Feld={field}, Fenster={days} Tage",
|
||||
)
|
||||
|
||||
# Map field names to data keys
|
||||
field_map = {
|
||||
|
|
@ -224,7 +263,10 @@ def get_protein_ziel_low(profile_id: str) -> str:
|
|||
data = get_protein_targets_data(profile_id)
|
||||
|
||||
if data['confidence'] == 'insufficient':
|
||||
return "nicht verfügbar"
|
||||
return pv_unavailable(
|
||||
"Proteinziel unten nicht ermittelbar",
|
||||
f"confidence={data.get('confidence')} (Gewicht/Profil für g/kg)",
|
||||
)
|
||||
|
||||
return f"{int(data['protein_target_low'])}"
|
||||
|
||||
|
|
@ -239,7 +281,10 @@ def get_protein_ziel_high(profile_id: str) -> str:
|
|||
data = get_protein_targets_data(profile_id)
|
||||
|
||||
if data['confidence'] == 'insufficient':
|
||||
return "nicht verfügbar"
|
||||
return pv_unavailable(
|
||||
"Proteinziel oben nicht ermittelbar",
|
||||
f"confidence={data.get('confidence')} (Gewicht/Profil für g/kg)",
|
||||
)
|
||||
|
||||
return f"{int(data['protein_target_high'])}"
|
||||
|
||||
|
|
@ -423,7 +468,12 @@ def get_sleep_avg_duration(profile_id: str, days: int = 7) -> str:
|
|||
data = get_sleep_duration_data(profile_id, days)
|
||||
|
||||
if data['confidence'] == 'insufficient':
|
||||
return "nicht verfügbar"
|
||||
return pv_unavailable(
|
||||
"Schlafdauer (formatiert) nicht aus sleep_segments ableitbar",
|
||||
f"confidence={data.get('confidence')}, "
|
||||
f"nächte_mit_segmenten={data.get('nights_with_data', 0)}/{data.get('days_analyzed', days)}; "
|
||||
f"Hinweis: {{sleep_avg_duration_7d}} nutzt duration_minutes und kann trotzdem Werte liefern",
|
||||
)
|
||||
|
||||
return f"{data['avg_duration_hours']:.1f}h"
|
||||
|
||||
|
|
@ -438,7 +488,11 @@ def get_sleep_avg_quality(profile_id: str, days: int = 7) -> str:
|
|||
data = get_sleep_quality_data(profile_id, days)
|
||||
|
||||
if data['confidence'] == 'insufficient':
|
||||
return "nicht verfügbar"
|
||||
return pv_unavailable(
|
||||
"Schlafqualität (Deep+REM) nicht aus sleep_segments ableitbar",
|
||||
f"confidence={data.get('confidence')}, "
|
||||
f"nächte_analysiert={data.get('nights_analyzed', 0)}/{data.get('days_analyzed', days)}",
|
||||
)
|
||||
|
||||
return f"{data['quality_score']:.0f}% (Deep+REM)"
|
||||
|
||||
|
|
@ -464,7 +518,10 @@ def get_vitals_avg_hr(profile_id: str, days: int = 7) -> str:
|
|||
data = get_resting_heart_rate_data(profile_id, days)
|
||||
|
||||
if data['confidence'] == 'insufficient':
|
||||
return "nicht verfügbar"
|
||||
return pv_unavailable(
|
||||
"Ruhepuls-Schnitt nicht ermittelbar",
|
||||
f"confidence={data.get('confidence')}, Fenster={days} Tage (vitals_baseline)",
|
||||
)
|
||||
|
||||
return f"{data['avg_rhr']} bpm"
|
||||
|
||||
|
|
@ -479,7 +536,10 @@ def get_vitals_avg_hrv(profile_id: str, days: int = 7) -> str:
|
|||
data = get_heart_rate_variability_data(profile_id, days)
|
||||
|
||||
if data['confidence'] == 'insufficient':
|
||||
return "nicht verfügbar"
|
||||
return pv_unavailable(
|
||||
"HRV-Schnitt nicht ermittelbar",
|
||||
f"confidence={data.get('confidence')}, Fenster={days} Tage (vitals_baseline)",
|
||||
)
|
||||
|
||||
return f"{data['avg_hrv']} ms"
|
||||
|
||||
|
|
@ -494,11 +554,156 @@ def get_vitals_vo2_max(profile_id: str) -> str:
|
|||
data = get_vo2_max_data(profile_id)
|
||||
|
||||
if data['confidence'] == 'insufficient':
|
||||
return "nicht verfügbar"
|
||||
return pv_unavailable(
|
||||
"VO2max nicht ermittelbar",
|
||||
f"confidence={data.get('confidence')} (vitals_baseline)",
|
||||
)
|
||||
|
||||
return f"{data['vo2_max']:.1f} ml/kg/min"
|
||||
|
||||
|
||||
# Begründungen wenn Layer-2-Berechnung None liefert (für KI / Export)
|
||||
_DEFAULT_NUMERIC_UNAVAILABLE = (
|
||||
"Numerische Berechnung liefert keinen Wert (Daten unzureichend oder Schwellen nicht erreicht)"
|
||||
)
|
||||
_DEFAULT_STR_UNAVAILABLE = (
|
||||
"Kein Wert ermittelbar (Daten unzureichend oder Schwellen nicht erreicht)"
|
||||
)
|
||||
_DEFAULT_JSON_UNAVAILABLE = (
|
||||
"Keine strukturierten JSON-Daten ermittelbar (Berechnung liefert None)"
|
||||
)
|
||||
|
||||
_SAFE_INT_NONE_REASON: Dict[str, str] = {
|
||||
"goal_progress_score": (
|
||||
"Aggregierter Ziel-Fortschritt nicht berechenbar; Alternativen: {{active_goals_md}}, "
|
||||
"{{data_quality_score}}"
|
||||
),
|
||||
"body_progress_score": "Körper-Fortschritts-Score nicht berechenbar",
|
||||
"nutrition_score": (
|
||||
"Ernährungs-Score nicht berechenbar (z. B. keine gewichteten Ernährungs-Fokusbereiche oder zu wenig Log-Daten)"
|
||||
),
|
||||
"activity_score": (
|
||||
"Aktivitäts-Score nicht berechenbar (z. B. Score-Schwellen oder fehlende abilities-Zuordnung in activity_log)"
|
||||
),
|
||||
"recovery_score_v2": "Recovery-Score v2 nicht berechenbar (Schlaf/Vitals/Last)",
|
||||
"data_quality_score": "Datenqualitäts-Score nicht berechenbar",
|
||||
"top_goal_progress_pct": (
|
||||
"Fortschritt % des Top-Ziels nicht ermittelbar (progress_pct fehlt oder Ziel nicht quantifizierbar); "
|
||||
"Alternative: {{active_goals_json}}"
|
||||
),
|
||||
"top_focus_area_progress": "Fortschritt % des Top-Fokusbereichs nicht ermittelbar",
|
||||
"focus_cat_körper_progress": "Kategorie-Fortschritt „Körper“ nicht berechenbar",
|
||||
"focus_cat_ernährung_progress": "Kategorie-Fortschritt „Ernährung“ nicht berechenbar",
|
||||
"focus_cat_aktivität_progress": "Kategorie-Fortschritt „Aktivität“ nicht berechenbar",
|
||||
"focus_cat_recovery_progress": "Kategorie-Fortschritt „Recovery“ nicht berechenbar",
|
||||
"focus_cat_vitalwerte_progress": "Kategorie-Fortschritt „Vitalwerte“ nicht berechenbar",
|
||||
"focus_cat_mental_progress": "Kategorie-Fortschritt „Mental“ nicht berechenbar",
|
||||
"focus_cat_lebensstil_progress": "Kategorie-Fortschritt „Lebensstil“ nicht berechenbar",
|
||||
"training_minutes_week": "Trainingsminuten/Woche nicht berechenbar",
|
||||
"training_frequency_7d": "Trainingseinheiten (7 Tage) nicht berechenbar",
|
||||
"quality_sessions_pct": "Anteil Qualitätssessions nicht berechenbar",
|
||||
"ability_balance_strength": (
|
||||
"Fähigkeiten-Balance Kraft nicht berechenbar (zu wenig abilities-Daten in activity_log)"
|
||||
),
|
||||
"ability_balance_endurance": (
|
||||
"Fähigkeiten-Balance Ausdauer nicht berechenbar (abilities in activity_log)"
|
||||
),
|
||||
"ability_balance_mental": (
|
||||
"Fähigkeiten-Balance Mental nicht berechenbar (abilities in activity_log)"
|
||||
),
|
||||
"ability_balance_coordination": (
|
||||
"Fähigkeiten-Balance Koordination nicht berechenbar (abilities in activity_log)"
|
||||
),
|
||||
"ability_balance_mobility": (
|
||||
"Fähigkeiten-Balance Mobilität nicht berechenbar (abilities in activity_log)"
|
||||
),
|
||||
"proxy_internal_load_7d": "Interne Last (7 Tage) nicht berechenbar",
|
||||
"strain_score": "Strain-Score nicht berechenbar",
|
||||
"rest_day_compliance": "Ruhetag-Compliance nicht berechenbar",
|
||||
"protein_adequacy_28d": "Protein-Adequacy (28 Tage) nicht berechenbar",
|
||||
"macro_consistency_score": "Makro-Konsistenz-Score nicht berechenbar",
|
||||
"recent_load_balance_3d": "Load-Balance (3 Tage) nicht berechenbar",
|
||||
"sleep_quality_7d": "Schlafqualität 7 Tage nicht berechenbar",
|
||||
}
|
||||
|
||||
_SAFE_FLOAT_NONE_REASON: Dict[str, str] = {
|
||||
"weight_7d_median": (
|
||||
"Gewichts-Median 7 Tage: mindestens 4 Messungen im Fenster erforderlich"
|
||||
),
|
||||
"weight_28d_slope": (
|
||||
"Gewichts-Trend 28 Tage: zu wenige Messpunkte (ca. 60 % Tagesabdeckung im Fenster)"
|
||||
),
|
||||
"weight_90d_slope": (
|
||||
"Gewichts-Trend 90 Tage: zu wenige Messpunkte (ca. 60 % Tagesabdeckung im Fenster)"
|
||||
),
|
||||
"fm_28d_change": "Fettmasse-Änderung 28 Tage nicht berechenbar (Serie Caliper/Gewicht)",
|
||||
"lbm_28d_change": "Magermasse-Änderung 28 Tage nicht berechenbar (Serie Caliper/Gewicht)",
|
||||
"waist_28d_delta": "Taillen-Delta 28 Tage nicht berechenbar (zwei auswertbare Messungen nötig)",
|
||||
"hip_28d_delta": "Hüft-Delta 28 Tage nicht berechenbar",
|
||||
"chest_28d_delta": "Brust-Delta 28 Tage nicht berechenbar",
|
||||
"arm_28d_delta": "Arm-Delta 28 Tage nicht berechenbar",
|
||||
"thigh_28d_delta": "Oberschenkel-Delta 28 Tage nicht berechenbar",
|
||||
"waist_hip_ratio": "Taille-Hüfte-Verhältnis nicht berechenbar",
|
||||
"energy_balance_7d": (
|
||||
"Energiebilanz 7 Tage nicht berechenbar (Intake oder TDEE/Gewicht fehlt)"
|
||||
),
|
||||
"protein_g_per_kg": "Protein g/kg nicht berechenbar",
|
||||
"monotony_score": "Monotonie-Score nicht berechenbar",
|
||||
"vo2max_trend_28d": "VO2max-Trend 28 Tage nicht berechenbar",
|
||||
"hrv_vs_baseline_pct": "HRV vs. Baseline nicht berechenbar (Baseline/Historie)",
|
||||
"rhr_vs_baseline_pct": "Ruhepuls vs. Baseline nicht berechenbar",
|
||||
"sleep_avg_duration_7d": "Schlafdauer 7 Tage nicht berechenbar (duration_minutes in sleep_log)",
|
||||
"sleep_debt_hours": "Schlafschuld nicht berechenbar (mindestens ~10 Nächte mit Dauer)",
|
||||
"sleep_regularity_proxy": "Schlaf-Regularität nicht berechenbar",
|
||||
"focus_cat_körper_weight": "Kategorie-Gewichtung „Körper“ nicht berechenbar",
|
||||
"focus_cat_ernährung_weight": "Kategorie-Gewichtung „Ernährung“ nicht berechenbar",
|
||||
"focus_cat_aktivität_weight": "Kategorie-Gewichtung „Aktivität“ nicht berechenbar",
|
||||
"focus_cat_recovery_weight": "Kategorie-Gewichtung „Recovery“ nicht berechenbar",
|
||||
"focus_cat_vitalwerte_weight": "Kategorie-Gewichtung „Vitalwerte“ nicht berechenbar",
|
||||
"focus_cat_mental_weight": "Kategorie-Gewichtung „Mental“ nicht berechenbar",
|
||||
"focus_cat_lebensstil_weight": "Kategorie-Gewichtung „Lebensstil“ nicht berechenbar",
|
||||
}
|
||||
|
||||
_SAFE_STR_NONE_REASON: Dict[str, str] = {
|
||||
"top_goal_name": "Kein priorisiertes Ziel ermittelbar",
|
||||
"top_goal_status": "Status des Top-Ziels nicht ermittelbar",
|
||||
"top_focus_area_name": "Kein Top-Fokusbereich ermittelbar",
|
||||
"recomposition_quadrant": "Rekompositions-Quadrant nicht berechenbar (FM/LBM-Serie)",
|
||||
"energy_deficit_surplus": "Defizit/Überschuss-Status nicht berechenbar",
|
||||
"protein_days_in_target": "Protein-Tage im Ziel nicht berechenbar",
|
||||
"intake_volatility": "Intake-Volatilität nicht berechenbar",
|
||||
"active_goals_md": "Aktive Ziele (Markdown) nicht darstellbar",
|
||||
"focus_areas_weighted_md": "Fokusbereiche (Markdown) nicht darstellbar",
|
||||
"top_3_focus_areas": "Top-3-Fokusbereiche nicht darstellbar",
|
||||
"top_3_goals_behind_schedule": "Ziele „hinter Zeitplan“ nicht darstellbar",
|
||||
"top_3_goals_on_track": "Ziele „im Plan“ nicht darstellbar",
|
||||
}
|
||||
|
||||
_SAFE_JSON_NONE_REASON: Dict[str, str] = {
|
||||
"training_sessions_recent_json": "Keine Session-Daten für JSON-Zeitfenster",
|
||||
"correlation_energy_weight_lag": (
|
||||
"Korrelation Energiebilanz zu Gewicht: zu wenige gekoppelte Datenpunkte"
|
||||
),
|
||||
"correlation_protein_lbm": (
|
||||
"Korrelation Protein zu Magermasse: zu wenige gekoppelte Datenpunkte"
|
||||
),
|
||||
"correlation_load_hrv": (
|
||||
"Korrelation Trainingslast zu HRV: zu wenige gekoppelte Datenpunkte"
|
||||
),
|
||||
"correlation_load_rhr": (
|
||||
"Korrelation Trainingslast zu Ruhepuls: zu wenige gekoppelte Datenpunkte"
|
||||
),
|
||||
"correlation_sleep_recovery": (
|
||||
"Korrelation Schlaf zu Recovery: zu wenige gekoppelte Datenpunkte"
|
||||
),
|
||||
"plateau_detected": "Plateau-Erkennung: keine auswertbare Serie",
|
||||
"top_drivers": "Top-Treiber: keine auswertbare Korrelationsbasis",
|
||||
"active_goals_json": "Aktive Ziele als JSON nicht ermittelbar",
|
||||
"focus_areas_weighted_json": "Gewichtete Fokusbereiche JSON nicht ermittelbar",
|
||||
"focus_area_weights_json": "Fokus-Gewichtungen JSON nicht ermittelbar",
|
||||
}
|
||||
|
||||
|
||||
# ── Phase 0b Calculation Engine Integration ──────────────────────────────────
|
||||
|
||||
def _safe_int(func_name: str, profile_id: str) -> str:
|
||||
|
|
@ -510,7 +715,7 @@ def _safe_int(func_name: str, profile_id: str) -> str:
|
|||
profile_id: Profile ID
|
||||
|
||||
Returns:
|
||||
String representation of integer value or 'nicht verfügbar'
|
||||
String representation of integer value oder pv_unavailable-Text mit Grund
|
||||
"""
|
||||
import traceback
|
||||
try:
|
||||
|
|
@ -554,14 +759,24 @@ def _safe_int(func_name: str, profile_id: str) -> str:
|
|||
|
||||
func = func_map.get(func_name)
|
||||
if not func:
|
||||
return 'nicht verfügbar'
|
||||
return pv_unavailable(
|
||||
"Ungültiger Platzhalter (keine numerische Berechnung registriert)",
|
||||
func_name,
|
||||
)
|
||||
|
||||
result = func(profile_id)
|
||||
return str(int(result)) if result is not None else 'nicht verfügbar'
|
||||
if result is None:
|
||||
return pv_unavailable(
|
||||
_SAFE_INT_NONE_REASON.get(func_name, _DEFAULT_NUMERIC_UNAVAILABLE),
|
||||
)
|
||||
return str(int(result))
|
||||
except Exception as e:
|
||||
print(f"[ERROR] _safe_int({func_name}, {profile_id}): {type(e).__name__}: {e}")
|
||||
traceback.print_exc()
|
||||
return 'nicht verfügbar'
|
||||
return pv_unavailable(
|
||||
"Berechnungsfehler bei numerischem Platzhalter",
|
||||
f"{type(e).__name__}: {e}",
|
||||
)
|
||||
|
||||
|
||||
def _safe_float(func_name: str, profile_id: str, decimals: int = 1) -> str:
|
||||
|
|
@ -574,7 +789,7 @@ def _safe_float(func_name: str, profile_id: str, decimals: int = 1) -> str:
|
|||
decimals: Number of decimal places
|
||||
|
||||
Returns:
|
||||
String representation of float value or 'nicht verfügbar'
|
||||
String representation of float value oder pv_unavailable-Text mit Grund
|
||||
"""
|
||||
import traceback
|
||||
try:
|
||||
|
|
@ -612,14 +827,24 @@ def _safe_float(func_name: str, profile_id: str, decimals: int = 1) -> str:
|
|||
|
||||
func = func_map.get(func_name)
|
||||
if not func:
|
||||
return 'nicht verfügbar'
|
||||
return pv_unavailable(
|
||||
"Ungültiger Platzhalter (keine Float-Berechnung registriert)",
|
||||
func_name,
|
||||
)
|
||||
|
||||
result = func(profile_id)
|
||||
return f"{result:.{decimals}f}" if result is not None else 'nicht verfügbar'
|
||||
if result is None:
|
||||
return pv_unavailable(
|
||||
_SAFE_FLOAT_NONE_REASON.get(func_name, _DEFAULT_NUMERIC_UNAVAILABLE),
|
||||
)
|
||||
return f"{result:.{decimals}f}"
|
||||
except Exception as e:
|
||||
print(f"[ERROR] _safe_float({func_name}, {profile_id}): {type(e).__name__}: {e}")
|
||||
traceback.print_exc()
|
||||
return 'nicht verfügbar'
|
||||
return pv_unavailable(
|
||||
"Berechnungsfehler bei Float-Platzhalter",
|
||||
f"{type(e).__name__}: {e}",
|
||||
)
|
||||
|
||||
|
||||
def _safe_str(func_name: str, profile_id: str) -> str:
|
||||
|
|
@ -648,14 +873,24 @@ def _safe_str(func_name: str, profile_id: str) -> str:
|
|||
|
||||
func = func_map.get(func_name)
|
||||
if not func:
|
||||
return 'nicht verfügbar'
|
||||
return pv_unavailable(
|
||||
"Ungültiger Platzhalter (keine String-Berechnung registriert)",
|
||||
func_name,
|
||||
)
|
||||
|
||||
result = func(profile_id)
|
||||
return str(result) if result is not None else 'nicht verfügbar'
|
||||
if result is None:
|
||||
return pv_unavailable(
|
||||
_SAFE_STR_NONE_REASON.get(func_name, _DEFAULT_STR_UNAVAILABLE),
|
||||
)
|
||||
return str(result)
|
||||
except Exception as e:
|
||||
print(f"[ERROR] _safe_str({func_name}, {profile_id}): {type(e).__name__}: {e}")
|
||||
traceback.print_exc()
|
||||
return 'nicht verfügbar'
|
||||
return pv_unavailable(
|
||||
"Berechnungsfehler bei Text-Platzhalter",
|
||||
f"{type(e).__name__}: {e}",
|
||||
)
|
||||
|
||||
|
||||
def _safe_json(func_name: str, profile_id: str) -> str:
|
||||
|
|
@ -684,11 +919,16 @@ def _safe_json(func_name: str, profile_id: str) -> str:
|
|||
|
||||
func = func_map.get(func_name)
|
||||
if not func:
|
||||
return '{}'
|
||||
return pv_unavailable_json(
|
||||
"Ungültiger Platzhalter (keine JSON-Berechnung registriert)",
|
||||
func_name,
|
||||
)
|
||||
|
||||
result = func(profile_id)
|
||||
if result is None:
|
||||
return '{}'
|
||||
return pv_unavailable_json(
|
||||
_SAFE_JSON_NONE_REASON.get(func_name, _DEFAULT_JSON_UNAVAILABLE),
|
||||
)
|
||||
|
||||
# If already string, return it; otherwise convert to JSON
|
||||
if isinstance(result, str):
|
||||
|
|
@ -698,7 +938,10 @@ def _safe_json(func_name: str, profile_id: str) -> str:
|
|||
except Exception as e:
|
||||
print(f"[ERROR] _safe_json({func_name}, {profile_id}): {type(e).__name__}: {e}")
|
||||
traceback.print_exc()
|
||||
return '{}'
|
||||
return pv_unavailable_json(
|
||||
"Berechnungsfehler bei JSON-Platzhalter",
|
||||
f"{type(e).__name__}: {e}",
|
||||
)
|
||||
|
||||
|
||||
def _get_active_goals_json(profile_id: str) -> str:
|
||||
|
|
@ -840,8 +1083,11 @@ def _format_top_focus_areas(profile_id: str, n: int = 3) -> str:
|
|||
lines.append(f"{i}. {name} ({weight}%)")
|
||||
|
||||
return ', '.join(lines)
|
||||
except Exception:
|
||||
return 'nicht verfügbar'
|
||||
except Exception as e:
|
||||
return pv_unavailable(
|
||||
"Top-Fokusbereiche nicht darstellbar",
|
||||
f"{type(e).__name__}: {e}",
|
||||
)
|
||||
|
||||
|
||||
def _format_goals_behind(profile_id: str, n: int = 3) -> str:
|
||||
|
|
@ -1005,7 +1251,10 @@ def _format_goals_behind(profile_id: str, n: int = 3) -> str:
|
|||
print(f"[ERROR] _format_goals_behind: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 'nicht verfügbar'
|
||||
return pv_unavailable(
|
||||
"Ziele „hinter Zeitplan“ nicht darstellbar",
|
||||
f"{type(e).__name__}: {e}",
|
||||
)
|
||||
|
||||
|
||||
def _format_goals_on_track(profile_id: str, n: int = 3) -> str:
|
||||
|
|
@ -1166,7 +1415,10 @@ def _format_goals_on_track(profile_id: str, n: int = 3) -> str:
|
|||
print(f"[ERROR] _format_goals_on_track: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 'nicht verfügbar'
|
||||
return pv_unavailable(
|
||||
"Ziele „im Plan“ nicht darstellbar",
|
||||
f"{type(e).__name__}: {e}",
|
||||
)
|
||||
|
||||
|
||||
# ── Placeholder Registry ──────────────────────────────────────────────────────
|
||||
|
|
@ -1321,7 +1573,11 @@ def calculate_bmi(profile_id: str) -> str:
|
|||
data = get_bmi_data(profile_id)
|
||||
bmi = data.get("bmi")
|
||||
if bmi is None:
|
||||
return "nicht verfügbar"
|
||||
return pv_unavailable(
|
||||
"BMI nicht berechenbar",
|
||||
f"confidence={data.get('confidence')}; benötigt Profil-Größe (cm) und letztes Gewicht "
|
||||
f"(weight_log); height_cm={data.get('height_cm')}, weight_kg={data.get('weight_kg')}",
|
||||
)
|
||||
return f"{bmi:.1f}"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -583,8 +583,14 @@ def export_placeholder_values_extended(
|
|||
if 'value_raw' not in metadata.unresolved_fields:
|
||||
metadata.unresolved_fields.append('value_raw')
|
||||
|
||||
# Check availability
|
||||
if value in ['nicht verfügbar', 'nicht genug Daten', '[Fehler:', '[Nicht']:
|
||||
# Check availability (Resolver liefert oft „nicht verfügbar — <Grund>“)
|
||||
sv = str(value)
|
||||
if (
|
||||
sv in ['nicht verfügbar', 'nicht genug Daten']
|
||||
or sv.startswith('nicht verfügbar —')
|
||||
or sv.startswith('[Fehler:')
|
||||
or sv.startswith('[Nicht')
|
||||
):
|
||||
metadata.available = False
|
||||
metadata.missing_reason = value
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -44,6 +44,15 @@ def test_value_raw_json():
|
|||
assert not success
|
||||
assert val is None
|
||||
|
||||
# Resolver-Fehlerhülle (kein verwertbares JSON für Charts)
|
||||
val, success = extract_value_raw(
|
||||
'{"_available": false, "_reason": "test"}',
|
||||
OutputType.JSON,
|
||||
PlaceholderType.RAW_DATA,
|
||||
)
|
||||
assert not success
|
||||
assert val is None
|
||||
|
||||
|
||||
def test_value_raw_number():
|
||||
"""Numeric outputs must extract numbers without units."""
|
||||
|
|
@ -66,6 +75,13 @@ def test_value_raw_number():
|
|||
val, success = extract_value_raw('nicht verfügbar', OutputType.NUMBER, PlaceholderType.ATOMIC)
|
||||
assert not success
|
||||
|
||||
val, success = extract_value_raw(
|
||||
'nicht verfügbar — Keine Messungen (detail)',
|
||||
OutputType.NUMBER,
|
||||
PlaceholderType.ATOMIC,
|
||||
)
|
||||
assert not success
|
||||
|
||||
|
||||
def test_value_raw_markdown():
|
||||
"""Markdown outputs keep as string."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user