feat: enhance formatting and normalization of activity metrics
- Introduced `format_scalar_for_prompt_text` function to standardize the representation of scalar values in activity summaries and details. - Updated `get_activity_summary` and `get_activity_detail` functions to utilize the new formatting for improved readability. - Added normalization for float values in session metrics to prevent excessively long representations. - Enhanced unit tests to verify the new formatting and normalization behavior.
This commit is contained in:
parent
6756dc60f3
commit
178534e9eb
|
|
@ -13,9 +13,31 @@ from data_layer.activity_data_canon import (
|
||||||
ACTIVITY_LOG_LEGACY_COLUMN_FOR_EAV_PRIMARY_PARAM,
|
ACTIVITY_LOG_LEGACY_COLUMN_FOR_EAV_PRIMARY_PARAM,
|
||||||
ACTIVITY_MODULE_REGISTRY_FIELD_KEYS,
|
ACTIVITY_MODULE_REGISTRY_FIELD_KEYS,
|
||||||
)
|
)
|
||||||
|
from data_layer.prompt_output_compact import normalize_prompt_number
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_metric_value_for_read(data_type: str, val: Any) -> Any:
|
||||||
|
"""Lesepfad (Layer 1): keine unnötig langen Float-Strings für KI/UI (Issue 53 / Platzhalter)."""
|
||||||
|
if val is None:
|
||||||
|
return None
|
||||||
|
dt = (data_type or "").strip().lower()
|
||||||
|
if dt == "string":
|
||||||
|
return val
|
||||||
|
if dt == "boolean":
|
||||||
|
return bool(val)
|
||||||
|
if dt == "integer":
|
||||||
|
try:
|
||||||
|
if isinstance(val, bool):
|
||||||
|
return int(val)
|
||||||
|
return int(val)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return normalize_prompt_number(val)
|
||||||
|
if dt == "float":
|
||||||
|
return normalize_prompt_number(val)
|
||||||
|
return normalize_prompt_number(val)
|
||||||
|
|
||||||
# Diese Spalten nicht aus CSV-Parameter-Zuordnung überschreiben (kommen aus Typ-Mapping / System).
|
# Diese Spalten nicht aus CSV-Parameter-Zuordnung überschreiben (kommen aus Typ-Mapping / System).
|
||||||
ACTIVITY_LOG_PATCH_FORBIDDEN = frozenset(
|
ACTIVITY_LOG_PATCH_FORBIDDEN = frozenset(
|
||||||
{
|
{
|
||||||
|
|
@ -430,6 +452,8 @@ def merge_column_backed_and_eav_metrics(
|
||||||
keys_handled.add(k)
|
keys_handled.add(k)
|
||||||
|
|
||||||
merged.sort(key=lambda x: x["key"])
|
merged.sort(key=lambda x: x["key"])
|
||||||
|
for m in merged:
|
||||||
|
m["value"] = _normalize_metric_value_for_read(m.get("data_type") or "", m.get("value"))
|
||||||
return merged
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,31 @@ def compact_json_payload_for_prompts(obj: Any) -> Any:
|
||||||
return normalize_prompt_number(obj)
|
return normalize_prompt_number(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def format_scalar_for_prompt_text(x: Any) -> str:
|
||||||
|
"""
|
||||||
|
Kurzdarstellung für Text-Platzhalter (activity_detail, Tabellen, …).
|
||||||
|
Nutzt dieselbe Komprimierung wie JSON (normalize_prompt_number).
|
||||||
|
"""
|
||||||
|
if x is None:
|
||||||
|
return "—"
|
||||||
|
if isinstance(x, bool):
|
||||||
|
return "ja" if x else "nein"
|
||||||
|
if isinstance(x, str):
|
||||||
|
return x
|
||||||
|
n = normalize_prompt_number(x)
|
||||||
|
if isinstance(n, bool):
|
||||||
|
return "ja" if n else "nein"
|
||||||
|
if isinstance(n, int) and not isinstance(n, bool):
|
||||||
|
return str(n)
|
||||||
|
if isinstance(n, float):
|
||||||
|
if not math.isfinite(n):
|
||||||
|
return str(n)
|
||||||
|
if abs(n - round(n)) < 1e-9:
|
||||||
|
return str(int(round(n)))
|
||||||
|
return str(n)
|
||||||
|
return str(n)
|
||||||
|
|
||||||
|
|
||||||
def session_metrics_list_to_key_value_compact(metrics: list[Any] | None) -> dict[str, Any]:
|
def session_metrics_list_to_key_value_compact(metrics: list[Any] | None) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Session-Metriken für KI-JSON: nur key → Wert (keine wiederholten Namen/Beschreibungen).
|
Session-Metriken für KI-JSON: nur key → Wert (keine wiederholten Namen/Beschreibungen).
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ from data_layer.nutrition_metrics import (
|
||||||
get_nutrition_days_data,
|
get_nutrition_days_data,
|
||||||
get_protein_targets_data
|
get_protein_targets_data
|
||||||
)
|
)
|
||||||
|
from data_layer.prompt_output_compact import format_scalar_for_prompt_text
|
||||||
|
|
||||||
from data_layer.activity_metrics import (
|
from data_layer.activity_metrics import (
|
||||||
get_activity_summary_data,
|
get_activity_summary_data,
|
||||||
get_activity_detail_data,
|
get_activity_detail_data,
|
||||||
|
|
@ -350,7 +352,11 @@ def get_activity_summary(profile_id: str, days: int = 14) -> str:
|
||||||
if data['confidence'] == 'insufficient':
|
if data['confidence'] == 'insufficient':
|
||||||
return f"Keine Aktivitäten in den letzten {days} Tagen"
|
return f"Keine Aktivitäten in den letzten {days} Tagen"
|
||||||
|
|
||||||
return f"{data['activity_count']} Einheiten in {days} Tagen (Ø {data['avg_duration_min']} min/Einheit, {data['total_kcal']} kcal gesamt)"
|
return (
|
||||||
|
f"{data['activity_count']} Einheiten in {days} Tagen (Ø "
|
||||||
|
f"{format_scalar_for_prompt_text(data['avg_duration_min'])} min/Einheit, "
|
||||||
|
f"{format_scalar_for_prompt_text(data['total_kcal'])} kcal gesamt)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def calculate_age(dob) -> str:
|
def calculate_age(dob) -> str:
|
||||||
|
|
@ -423,18 +429,23 @@ def get_activity_detail(profile_id: str, days: int = 14) -> str:
|
||||||
# Format as readable list (max 20 entries to avoid token bloat)
|
# Format as readable list (max 20 entries to avoid token bloat)
|
||||||
lines = []
|
lines = []
|
||||||
for activity in data["activities"][:20]:
|
for activity in data["activities"][:20]:
|
||||||
hr_str = f", HF={activity['hr_avg']}" if activity.get("hr_avg") else ""
|
hr_str = (
|
||||||
|
f", HF={format_scalar_for_prompt_text(activity['hr_avg'])}"
|
||||||
|
if activity.get("hr_avg") is not None
|
||||||
|
else ""
|
||||||
|
)
|
||||||
eav_parts = []
|
eav_parts = []
|
||||||
for m in activity.get("session_metrics") or []:
|
for m in activity.get("session_metrics") or []:
|
||||||
k, v = m.get("key"), m.get("value")
|
k, v = m.get("key"), m.get("value")
|
||||||
if k is None or v is None:
|
if k is None or v is None:
|
||||||
continue
|
continue
|
||||||
label = m.get("name_de") or m.get("name_en") or k
|
label = m.get("name_de") or m.get("name_en") or k
|
||||||
eav_parts.append(f"{label} ({k})={v}")
|
eav_parts.append(f"{label} ({k})={format_scalar_for_prompt_text(v)}")
|
||||||
eav_str = f" | EAV: {'; '.join(eav_parts)}" if eav_parts else ""
|
eav_str = f" | EAV: {'; '.join(eav_parts)}" if eav_parts else ""
|
||||||
lines.append(
|
lines.append(
|
||||||
f"{activity['date']}: {activity['activity_type']} "
|
f"{activity['date']}: {activity['activity_type']} "
|
||||||
f"({activity['duration_min']}min, {activity['kcal_active']}kcal{hr_str}{eav_str})"
|
f"({format_scalar_for_prompt_text(activity['duration_min'])}min, "
|
||||||
|
f"{format_scalar_for_prompt_text(activity['kcal_active'])}kcal{hr_str}{eav_str})"
|
||||||
)
|
)
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,38 @@ def test_merge_parameter_schema_includes_descriptions():
|
||||||
assert merged[0]["description_en"] == "5 min average power"
|
assert merged[0]["description_en"] == "5 min average power"
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_eav_float_value_normalized_no_long_tail():
|
||||||
|
"""Layer 1: lange Floats (z. B. kcal_per_km) für Lesepfad kompakt."""
|
||||||
|
schema = [
|
||||||
|
{
|
||||||
|
"training_parameter_id": 1,
|
||||||
|
"key": "kcal_per_km",
|
||||||
|
"data_type": "float",
|
||||||
|
"unit": "kcal/km",
|
||||||
|
"validation_rules": {},
|
||||||
|
"source_field": None,
|
||||||
|
"name_de": "Kcal/km",
|
||||||
|
"name_en": "kcal/km",
|
||||||
|
"description_de": None,
|
||||||
|
"description_en": None,
|
||||||
|
"param_category": "performance",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
eav = [
|
||||||
|
{
|
||||||
|
"training_parameter_id": 1,
|
||||||
|
"key": "kcal_per_km",
|
||||||
|
"data_type": "float",
|
||||||
|
"unit": "kcal/km",
|
||||||
|
"value": 51.5818181818181818,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
out = merge_column_backed_and_eav_metrics({}, schema, eav)
|
||||||
|
assert len(out) == 1
|
||||||
|
v = out[0]["value"]
|
||||||
|
assert "581818" not in repr(v)
|
||||||
|
|
||||||
|
|
||||||
def test_merge_column_backed_includes_human_labels_from_schema():
|
def test_merge_column_backed_includes_human_labels_from_schema():
|
||||||
schema = [
|
schema = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import pytest
|
||||||
from data_layer.prompt_output_compact import (
|
from data_layer.prompt_output_compact import (
|
||||||
compact_float_for_prompt,
|
compact_float_for_prompt,
|
||||||
compact_json_payload_for_prompts,
|
compact_json_payload_for_prompts,
|
||||||
|
format_scalar_for_prompt_text,
|
||||||
normalize_prompt_number,
|
normalize_prompt_number,
|
||||||
session_metrics_list_to_key_value_compact,
|
session_metrics_list_to_key_value_compact,
|
||||||
)
|
)
|
||||||
|
|
@ -38,6 +39,12 @@ def test_compact_json_nested():
|
||||||
assert out["d"][0] == 1.11
|
assert out["d"][0] == 1.11
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_scalar_no_long_float_tail():
|
||||||
|
s = format_scalar_for_prompt_text(51.5818181818181818)
|
||||||
|
assert "181818" not in s
|
||||||
|
assert len(s) <= 8
|
||||||
|
|
||||||
|
|
||||||
def test_session_metrics_key_value_only():
|
def test_session_metrics_key_value_only():
|
||||||
sm = [
|
sm = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user