Platzhalter finalisiert - Option |d und Option |x implementiert #77
|
|
@ -260,21 +260,34 @@ class PlaceholderRegistry:
|
|||
|
||||
def build_ai_placeholder_caption(metadata: PlaceholderMetadata, max_len: int = 400) -> str:
|
||||
"""
|
||||
Kurztext für KI-Kontext (z. B. Modifier |d): Bedeutung/Skala, ohne die Rohausgabe zu ersetzen.
|
||||
Nutzt business_meaning / semantic_contract; bei Scores explizite 0–100-Erläuterung.
|
||||
Text für |d und Exportfeld ai_caption: zuerst **was** der Platzhalter misst (description),
|
||||
dann **Einordnung** (business_meaning oder gekürzter semantic_contract).
|
||||
So ist klar, worauf sich der konkrete Wert bezieht — nicht nur eine „Meta-Bedeutung“.
|
||||
"""
|
||||
chunks: List[str] = []
|
||||
desc = (metadata.description or "").strip()
|
||||
bm = (metadata.business_meaning or "").strip()
|
||||
sc = (metadata.semantic_contract or "").strip()
|
||||
desc = (metadata.description or "").strip()
|
||||
|
||||
if bm:
|
||||
chunks.append(bm)
|
||||
elif sc:
|
||||
chunks.append(sc if len(sc) <= max_len else sc[: max_len - 1] + "…")
|
||||
elif desc:
|
||||
chunks: List[str] = []
|
||||
if desc:
|
||||
chunks.append(desc)
|
||||
|
||||
interpret = bm
|
||||
if not interpret and sc:
|
||||
interpret = sc if len(sc) <= max_len else sc[: max_len - 1] + "…"
|
||||
|
||||
if interpret:
|
||||
blob = " ".join(chunks).lower()
|
||||
il = interpret.lower()
|
||||
# Keine Dublette: gleicher Text oder lange Description bereits in der Interpretation
|
||||
redundant = il in blob or (
|
||||
desc
|
||||
and len(desc) >= 10
|
||||
and desc.lower() in il
|
||||
)
|
||||
if not redundant:
|
||||
chunks.append(interpret)
|
||||
|
||||
if metadata.placeholder_type == PlaceholderType.SCORE:
|
||||
chunks.append("Skala 0–100: höher = im Modell günstiger / besser abgestimmt.")
|
||||
|
||||
|
|
@ -282,9 +295,18 @@ def build_ai_placeholder_caption(metadata: PlaceholderMetadata, max_len: int = 4
|
|||
if unit and metadata.placeholder_type != PlaceholderType.SCORE:
|
||||
blob = " ".join(chunks).lower()
|
||||
u_low = unit.lower()
|
||||
if u_low not in blob and u_low.replace(" ", "") not in blob.replace(" ", ""):
|
||||
if u_low not in ("score (0-100)", "0-100", "0–100", "dimensionless"):
|
||||
chunks.append(f"Technischer Bezug: {unit}.")
|
||||
# Einheit oft schon in description („… in g (30d)“, „Kalorien“) — nicht doppeln
|
||||
compact_blob = blob.replace(" ", "").replace("/", "")
|
||||
compact_u = u_low.replace(" ", "").replace("/", "")
|
||||
unit_redundant = compact_u in compact_blob or (
|
||||
"g/day" in u_low and ("g/" in blob or "gramm" in blob or " protein" in blob or " fett" in blob or " kh" in blob)
|
||||
) or ("kcal" in u_low and ("kcal" in blob or "kalorien" in blob))
|
||||
|
||||
if (
|
||||
not unit_redundant
|
||||
and u_low not in ("score (0-100)", "0-100", "0–100", "dimensionless")
|
||||
):
|
||||
chunks.append(f"Technischer Bezug: {unit}.")
|
||||
|
||||
out = " ".join(c for c in chunks if c).strip()
|
||||
if len(out) > max_len + 120:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ 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
|
||||
from typing import Any, Dict, List, Optional, Callable, Tuple
|
||||
from db import get_db, get_cursor, r2d
|
||||
|
||||
# Phase 0c: Import data layer
|
||||
|
|
@ -62,6 +62,17 @@ def _ai_caption_for_placeholder_key(key: str) -> Optional[str]:
|
|||
return None
|
||||
|
||||
|
||||
def format_value_with_d_modifier(value: str, catalog_row: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Entspricht der Prompt-Ersetzung bei {{key|d}}: „Wert — Kontext“.
|
||||
Kontext: ai_caption aus dem Katalog, sonst description (wie prompt_executor).
|
||||
"""
|
||||
cap = (catalog_row.get("ai_caption") or catalog_row.get("description") or "").strip()
|
||||
if cap:
|
||||
return f"{value} — {cap}"
|
||||
return str(value)
|
||||
|
||||
|
||||
# ── Helper Functions ──────────────────────────────────────────────────────────
|
||||
|
||||
def get_profile_data(profile_id: str) -> Dict:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from placeholder_resolver import (
|
|||
resolve_placeholders,
|
||||
get_unknown_placeholders,
|
||||
get_placeholder_example_values,
|
||||
format_value_with_d_modifier,
|
||||
get_available_placeholders,
|
||||
get_placeholder_catalog
|
||||
)
|
||||
|
|
@ -457,8 +458,8 @@ def export_placeholder_values(session: dict = Depends(require_auth)):
|
|||
"""
|
||||
Export all available placeholders with their current resolved values.
|
||||
|
||||
Returns JSON export suitable for download with all placeholders
|
||||
resolved for the current user's profile.
|
||||
Pro Zeile: value = Rohwert wie bei {{key}}, example = Vorschau wie bei {{key|d}}
|
||||
(Wert — ai_caption bzw. description). JSON-Download für das aktive Profil.
|
||||
"""
|
||||
from datetime import datetime
|
||||
profile_id = session['profile_id']
|
||||
|
|
@ -486,11 +487,12 @@ def export_placeholder_values(session: dict = Depends(require_auth)):
|
|||
export_data['placeholders_by_category'][category] = []
|
||||
for item in items:
|
||||
key = item['key'].replace('{{', '').replace('}}', '')
|
||||
raw_val = cleaned_values.get(key, 'nicht verfügbar')
|
||||
row = {
|
||||
'key': item['key'],
|
||||
'description': item['description'],
|
||||
'value': cleaned_values.get(key, 'nicht verfügbar'),
|
||||
'example': item.get('example'),
|
||||
'value': raw_val,
|
||||
'example': format_value_with_d_modifier(str(raw_val), item),
|
||||
}
|
||||
if item.get('ai_caption'):
|
||||
row['ai_caption'] = item['ai_caption']
|
||||
|
|
@ -662,11 +664,12 @@ def export_placeholder_values_extended(
|
|||
export_data['legacy']['placeholders_by_category'][category] = []
|
||||
for item in items:
|
||||
key = item['key'].replace('{{', '').replace('}}', '')
|
||||
raw_val = cleaned_values.get(key, 'nicht verfügbar')
|
||||
export_data['legacy']['placeholders_by_category'][category].append({
|
||||
'key': item['key'],
|
||||
'description': item['description'],
|
||||
'value': cleaned_values.get(key, 'nicht verfügbar'),
|
||||
'example': item.get('example')
|
||||
'value': raw_val,
|
||||
'example': format_value_with_d_modifier(str(raw_val), item),
|
||||
})
|
||||
|
||||
# Fill metadata flat
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from placeholder_registry import (
|
|||
build_ai_placeholder_caption,
|
||||
)
|
||||
import placeholder_resolver as pr
|
||||
from placeholder_resolver import format_value_with_d_modifier
|
||||
|
||||
|
||||
def test_build_ai_caption_prefers_business_meaning():
|
||||
|
|
@ -22,9 +23,28 @@ def test_build_ai_caption_prefers_business_meaning():
|
|||
output_type=OutputType.NUMERIC,
|
||||
)
|
||||
cap = build_ai_placeholder_caption(m)
|
||||
assert cap.startswith("Kurzbeschreibung")
|
||||
assert "Kernbedeutung" in cap
|
||||
|
||||
|
||||
def test_build_ai_caption_description_then_meaning_like_protein_avg():
|
||||
m = PlaceholderMetadata(
|
||||
key="protein_avg",
|
||||
category="Ernährung",
|
||||
description="Durchschn. Protein in g (30d)",
|
||||
resolver_module="m",
|
||||
resolver_function="f",
|
||||
business_meaning="Zentraler Placeholder für Muskelerhalt.",
|
||||
unit="g/day",
|
||||
placeholder_type=PlaceholderType.INTERPRETED,
|
||||
output_type=OutputType.NUMERIC,
|
||||
)
|
||||
cap = build_ai_placeholder_caption(m)
|
||||
assert cap.startswith("Durchschn. Protein in g (30d)")
|
||||
assert "Muskelerhalt" in cap
|
||||
assert "Technischer Bezug" not in cap
|
||||
|
||||
|
||||
def test_build_ai_caption_score_adds_scale():
|
||||
m = PlaceholderMetadata(
|
||||
key="test_score",
|
||||
|
|
@ -54,3 +74,19 @@ def test_placeholder_token_regex_optional_modifier():
|
|||
def test_get_unknown_placeholders_strips_modifier():
|
||||
unk = pr.get_unknown_placeholders("{{not_a_real_key|d}}")
|
||||
assert set(unk) == {"not_a_real_key"}
|
||||
|
||||
|
||||
def test_format_value_with_d_modifier_matches_prompt_executor():
|
||||
row = {
|
||||
"key": "protein_avg",
|
||||
"description": "Durchschn. Protein in g (30d)",
|
||||
"example": "119g/Tag",
|
||||
"ai_caption": "Durchschn. Protein in g (30d). Zentral für Muskelerhalt.",
|
||||
}
|
||||
out = format_value_with_d_modifier("119g/Tag", row)
|
||||
assert out == "119g/Tag — Durchschn. Protein in g (30d). Zentral für Muskelerhalt."
|
||||
|
||||
|
||||
def test_format_value_with_d_modifier_falls_back_to_description():
|
||||
row = {"description": "Nur Beschreibung", "key": "x"}
|
||||
assert format_value_with_d_modifier("42", row) == "42 — Nur Beschreibung"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user