Platzhalter finalisiert - Option |d und Option |x implementiert #77
|
|
@ -258,6 +258,40 @@ class PlaceholderRegistry:
|
|||
return metadata._resolver_func(profile_id)
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
chunks: List[str] = []
|
||||
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.append(desc)
|
||||
|
||||
if metadata.placeholder_type == PlaceholderType.SCORE:
|
||||
chunks.append("Skala 0–100: höher = im Modell günstiger / besser abgestimmt.")
|
||||
|
||||
unit = (metadata.unit or "").strip()
|
||||
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}.")
|
||||
|
||||
out = " ".join(c for c in chunks if c).strip()
|
||||
if len(out) > max_len + 120:
|
||||
out = out[: max_len + 60] + "…"
|
||||
return out or desc or metadata.key
|
||||
|
||||
|
||||
# Global registry instance
|
||||
_global_registry = PlaceholderRegistry()
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,20 @@ from data_layer.health_metrics import (
|
|||
get_vo2_max_data
|
||||
)
|
||||
|
||||
from placeholder_registry import build_ai_placeholder_caption, get_registry
|
||||
|
||||
# {{key}} oder {{key|d}} — Modifier d hängt KI-Kontext (ai_caption) an
|
||||
_PLACEHOLDER_TOKEN_RE = re.compile(
|
||||
r"\{\{\s*([a-zA-Z0-9_]+)(?:\s*\|\s*([a-zA-Z0-9_,\s]+))?\s*\}\}"
|
||||
)
|
||||
|
||||
|
||||
def _ai_caption_for_placeholder_key(key: str) -> Optional[str]:
|
||||
meta = get_registry().get(key)
|
||||
if meta:
|
||||
return build_ai_placeholder_caption(meta)
|
||||
return None
|
||||
|
||||
|
||||
# ── Helper Functions ──────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -1588,6 +1602,10 @@ def resolve_placeholders(template: str, profile_id: str) -> str:
|
|||
"""
|
||||
Replace all {{placeholders}} in template with actual user data.
|
||||
|
||||
Unterstützt Modifier wie bei der Prompt-Pipeline:
|
||||
- {{fat_avg}} — nur Wert
|
||||
- {{fat_avg|d}} — Wert plus KI-Kontext (business_meaning / semantic_contract aus Registry)
|
||||
|
||||
Args:
|
||||
template: Prompt template with placeholders
|
||||
profile_id: User profile ID
|
||||
|
|
@ -1595,18 +1613,26 @@ def resolve_placeholders(template: str, profile_id: str) -> str:
|
|||
Returns:
|
||||
Resolved template with placeholders replaced by values
|
||||
"""
|
||||
result = template
|
||||
|
||||
for placeholder, resolver in PLACEHOLDER_MAP.items():
|
||||
if placeholder in result:
|
||||
try:
|
||||
value = resolver(profile_id)
|
||||
result = result.replace(placeholder, str(value))
|
||||
except Exception as e:
|
||||
# On error, replace with error message
|
||||
result = result.replace(placeholder, f"[Fehler: {placeholder}]")
|
||||
def _repl(match: re.Match) -> str:
|
||||
key = match.group(1)
|
||||
modifiers_raw = (match.group(2) or "").strip()
|
||||
mods = {x.strip().lower() for x in modifiers_raw.split(",") if x.strip()}
|
||||
ph = f"{{{{{key}}}}}"
|
||||
resolver = PLACEHOLDER_MAP.get(ph)
|
||||
if not resolver:
|
||||
return match.group(0)
|
||||
try:
|
||||
value = str(resolver(profile_id))
|
||||
except Exception:
|
||||
return f"[Fehler: {ph}]"
|
||||
if "d" in mods:
|
||||
cap = _ai_caption_for_placeholder_key(key)
|
||||
if cap:
|
||||
value = f"{value} — {cap}"
|
||||
return value
|
||||
|
||||
return result
|
||||
return _PLACEHOLDER_TOKEN_RE.sub(_repl, template)
|
||||
|
||||
|
||||
def get_unknown_placeholders(template: str) -> List[str]:
|
||||
|
|
@ -1619,12 +1645,9 @@ def get_unknown_placeholders(template: str) -> List[str]:
|
|||
Returns:
|
||||
List of unknown placeholder names (without {{}})
|
||||
"""
|
||||
# Find all {{...}} patterns
|
||||
found = re.findall(r'\{\{(\w+)\}\}', template)
|
||||
|
||||
# Filter to only unknown ones
|
||||
found = _PLACEHOLDER_TOKEN_RE.findall(template)
|
||||
known_names = {p.strip('{}') for p in PLACEHOLDER_MAP.keys()}
|
||||
unknown = [p for p in found if p not in known_names]
|
||||
unknown = [key for key, _ in found if key not in known_names]
|
||||
|
||||
return list(set(unknown)) # Remove duplicates
|
||||
|
||||
|
|
@ -1781,7 +1804,8 @@ def get_placeholder_catalog(profile_id: str) -> Dict[str, List[Dict[str, str]]]:
|
|||
catalog[category].append({
|
||||
'key': key,
|
||||
'description': metadata.description,
|
||||
'example': str(example)
|
||||
'example': str(example),
|
||||
'ai_caption': build_ai_placeholder_caption(metadata),
|
||||
})
|
||||
|
||||
# Legacy placeholders (not in registry yet)
|
||||
|
|
@ -1810,7 +1834,8 @@ def get_placeholder_catalog(profile_id: str) -> Dict[str, List[Dict[str, str]]]:
|
|||
catalog[category].append({
|
||||
'key': key,
|
||||
'description': description,
|
||||
'example': str(example)
|
||||
'example': str(example),
|
||||
'ai_caption': description,
|
||||
})
|
||||
|
||||
# Add ALL remaining placeholders from PLACEHOLDER_MAP that aren't categorized yet
|
||||
|
|
@ -1839,7 +1864,8 @@ def get_placeholder_catalog(profile_id: str) -> Dict[str, List[Dict[str, str]]]:
|
|||
catalog[sonstige_category].append({
|
||||
'key': key,
|
||||
'description': f'Platzhalter: {key}', # Generic description
|
||||
'example': str(example)
|
||||
'example': str(example),
|
||||
'ai_caption': f'Platzhalter {key} (noch ohne erweiterte Registry-Beschreibung).',
|
||||
})
|
||||
|
||||
return catalog
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ def resolve_placeholders(template: str, variables: Dict[str, Any], debug_info: O
|
|||
Replace {{placeholder}} with values from variables dict.
|
||||
|
||||
Supports modifiers:
|
||||
- {{key|d}} - Include description in parentheses (requires catalog)
|
||||
- {{key|d}} — angehängter KI-Kontext (ai_caption aus Katalog, sonst description; Katalog nötig)
|
||||
|
||||
Args:
|
||||
template: String with {{key}} or {{key|modifiers}} placeholders
|
||||
|
|
@ -52,16 +52,16 @@ def resolve_placeholders(template: str, variables: Dict[str, Any], debug_info: O
|
|||
# Apply modifiers
|
||||
if 'd' in modifiers:
|
||||
if catalog:
|
||||
# Add description from catalog
|
||||
description = None
|
||||
caption = None
|
||||
for cat_items in catalog.values():
|
||||
matching = [item for item in cat_items if item['key'] == key]
|
||||
if matching:
|
||||
description = matching[0].get('description', '')
|
||||
row = matching[0]
|
||||
caption = (row.get('ai_caption') or row.get('description') or '').strip()
|
||||
break
|
||||
|
||||
if description:
|
||||
resolved_value = f"{resolved_value} ({description})"
|
||||
if caption:
|
||||
resolved_value = f"{resolved_value} — {caption}"
|
||||
else:
|
||||
# Catalog not available - log warning in debug
|
||||
if debug_info is not None:
|
||||
|
|
|
|||
|
|
@ -486,12 +486,15 @@ def export_placeholder_values(session: dict = Depends(require_auth)):
|
|||
export_data['placeholders_by_category'][category] = []
|
||||
for item in items:
|
||||
key = item['key'].replace('{{', '').replace('}}', '')
|
||||
export_data['placeholders_by_category'][category].append({
|
||||
row = {
|
||||
'key': item['key'],
|
||||
'description': item['description'],
|
||||
'value': cleaned_values.get(key, 'nicht verfügbar'),
|
||||
'example': item.get('example')
|
||||
})
|
||||
'example': item.get('example'),
|
||||
}
|
||||
if item.get('ai_caption'):
|
||||
row['ai_caption'] = item['ai_caption']
|
||||
export_data['placeholders_by_category'][category].append(row)
|
||||
|
||||
# Also include flat list for easy access
|
||||
export_data['all_placeholders'] = cleaned_values
|
||||
|
|
|
|||
56
backend/tests/test_placeholder_modifier_d.py
Normal file
56
backend/tests/test_placeholder_modifier_d.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"""Tests für {{key|d}}, ai_caption und Unbekannt-Erkennung."""
|
||||
from placeholder_registry import (
|
||||
PlaceholderMetadata,
|
||||
PlaceholderType,
|
||||
OutputType,
|
||||
build_ai_placeholder_caption,
|
||||
)
|
||||
import placeholder_resolver as pr
|
||||
|
||||
|
||||
def test_build_ai_caption_prefers_business_meaning():
|
||||
m = PlaceholderMetadata(
|
||||
key="test_x",
|
||||
category="Test",
|
||||
description="Kurzbeschreibung",
|
||||
resolver_module="m",
|
||||
resolver_function="f",
|
||||
semantic_contract="Lang Vertrag " * 50,
|
||||
business_meaning="Kernbedeutung für die KI.",
|
||||
unit="g/day",
|
||||
placeholder_type=PlaceholderType.INTERPRETED,
|
||||
output_type=OutputType.NUMERIC,
|
||||
)
|
||||
cap = build_ai_placeholder_caption(m)
|
||||
assert "Kernbedeutung" in cap
|
||||
|
||||
|
||||
def test_build_ai_caption_score_adds_scale():
|
||||
m = PlaceholderMetadata(
|
||||
key="test_score",
|
||||
category="Test",
|
||||
description="Score",
|
||||
resolver_module="m",
|
||||
resolver_function="f",
|
||||
business_meaning="Gewichteter Gesamtscore.",
|
||||
unit="Score (0-100)",
|
||||
placeholder_type=PlaceholderType.SCORE,
|
||||
output_type=OutputType.NUMERIC,
|
||||
)
|
||||
cap = build_ai_placeholder_caption(m)
|
||||
assert "0–100" in cap or "0-100" in cap
|
||||
assert "Gewichteter" in cap
|
||||
|
||||
|
||||
def test_placeholder_token_regex_optional_modifier():
|
||||
m0 = pr._PLACEHOLDER_TOKEN_RE.search("{{fat_avg}}")
|
||||
assert m0 and m0.group(1) == "fat_avg" and m0.group(2) is None
|
||||
m1 = pr._PLACEHOLDER_TOKEN_RE.search("{{fat_avg|d}}")
|
||||
assert m1 and m1.group(1) == "fat_avg" and m1.group(2).strip() == "d"
|
||||
m2 = pr._PLACEHOLDER_TOKEN_RE.search("{{ protein_avg | d }}")
|
||||
assert m2 and m2.group(1) == "protein_avg" and m2.group(2).strip() == "d"
|
||||
|
||||
|
||||
def test_get_unknown_placeholders_strips_modifier():
|
||||
unk = pr.get_unknown_placeholders("{{not_a_real_key|d}}")
|
||||
assert set(unk) == {"not_a_real_key"}
|
||||
Loading…
Reference in New Issue
Block a user