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_MODULE_REGISTRY_FIELD_KEYS,
|
||||
)
|
||||
from data_layer.prompt_output_compact import normalize_prompt_number
|
||||
|
||||
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).
|
||||
ACTIVITY_LOG_PATCH_FORBIDDEN = frozenset(
|
||||
{
|
||||
|
|
@ -430,6 +452,8 @@ def merge_column_backed_and_eav_metrics(
|
|||
keys_handled.add(k)
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,31 @@ def compact_json_payload_for_prompts(obj: Any) -> Any:
|
|||
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]:
|
||||
"""
|
||||
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_protein_targets_data
|
||||
)
|
||||
from data_layer.prompt_output_compact import format_scalar_for_prompt_text
|
||||
|
||||
from data_layer.activity_metrics import (
|
||||
get_activity_summary_data,
|
||||
get_activity_detail_data,
|
||||
|
|
@ -350,7 +352,11 @@ def get_activity_summary(profile_id: str, days: int = 14) -> str:
|
|||
if data['confidence'] == 'insufficient':
|
||||
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:
|
||||
|
|
@ -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)
|
||||
lines = []
|
||||
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 = []
|
||||
for m in activity.get("session_metrics") or []:
|
||||
k, v = m.get("key"), m.get("value")
|
||||
if k is None or v is None:
|
||||
continue
|
||||
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 ""
|
||||
lines.append(
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -121,6 +121,38 @@ def test_merge_parameter_schema_includes_descriptions():
|
|||
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():
|
||||
schema = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import pytest
|
|||
from data_layer.prompt_output_compact import (
|
||||
compact_float_for_prompt,
|
||||
compact_json_payload_for_prompts,
|
||||
format_scalar_for_prompt_text,
|
||||
normalize_prompt_number,
|
||||
session_metrics_list_to_key_value_compact,
|
||||
)
|
||||
|
|
@ -38,6 +39,12 @@ def test_compact_json_nested():
|
|||
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():
|
||||
sm = [
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user