Platzhalter finalisiert - Option |d und Option |x implementiert #77
|
|
@ -258,6 +258,40 @@ class PlaceholderRegistry:
|
||||||
return metadata._resolver_func(profile_id)
|
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 instance
|
||||||
_global_registry = PlaceholderRegistry()
|
_global_registry = PlaceholderRegistry()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,20 @@ from data_layer.health_metrics import (
|
||||||
get_vo2_max_data
|
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 ──────────────────────────────────────────────────────────
|
# ── Helper Functions ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -1588,6 +1602,10 @@ def resolve_placeholders(template: str, profile_id: str) -> str:
|
||||||
"""
|
"""
|
||||||
Replace all {{placeholders}} in template with actual user data.
|
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:
|
Args:
|
||||||
template: Prompt template with placeholders
|
template: Prompt template with placeholders
|
||||||
profile_id: User profile ID
|
profile_id: User profile ID
|
||||||
|
|
@ -1595,18 +1613,26 @@ def resolve_placeholders(template: str, profile_id: str) -> str:
|
||||||
Returns:
|
Returns:
|
||||||
Resolved template with placeholders replaced by values
|
Resolved template with placeholders replaced by values
|
||||||
"""
|
"""
|
||||||
result = template
|
|
||||||
|
|
||||||
for placeholder, resolver in PLACEHOLDER_MAP.items():
|
def _repl(match: re.Match) -> str:
|
||||||
if placeholder in result:
|
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:
|
try:
|
||||||
value = resolver(profile_id)
|
value = str(resolver(profile_id))
|
||||||
result = result.replace(placeholder, str(value))
|
except Exception:
|
||||||
except Exception as e:
|
return f"[Fehler: {ph}]"
|
||||||
# On error, replace with error message
|
if "d" in mods:
|
||||||
result = result.replace(placeholder, f"[Fehler: {placeholder}]")
|
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]:
|
def get_unknown_placeholders(template: str) -> List[str]:
|
||||||
|
|
@ -1619,12 +1645,9 @@ def get_unknown_placeholders(template: str) -> List[str]:
|
||||||
Returns:
|
Returns:
|
||||||
List of unknown placeholder names (without {{}})
|
List of unknown placeholder names (without {{}})
|
||||||
"""
|
"""
|
||||||
# Find all {{...}} patterns
|
found = _PLACEHOLDER_TOKEN_RE.findall(template)
|
||||||
found = re.findall(r'\{\{(\w+)\}\}', template)
|
|
||||||
|
|
||||||
# Filter to only unknown ones
|
|
||||||
known_names = {p.strip('{}') for p in PLACEHOLDER_MAP.keys()}
|
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
|
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({
|
catalog[category].append({
|
||||||
'key': key,
|
'key': key,
|
||||||
'description': metadata.description,
|
'description': metadata.description,
|
||||||
'example': str(example)
|
'example': str(example),
|
||||||
|
'ai_caption': build_ai_placeholder_caption(metadata),
|
||||||
})
|
})
|
||||||
|
|
||||||
# Legacy placeholders (not in registry yet)
|
# 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({
|
catalog[category].append({
|
||||||
'key': key,
|
'key': key,
|
||||||
'description': description,
|
'description': description,
|
||||||
'example': str(example)
|
'example': str(example),
|
||||||
|
'ai_caption': description,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Add ALL remaining placeholders from PLACEHOLDER_MAP that aren't categorized yet
|
# 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({
|
catalog[sonstige_category].append({
|
||||||
'key': key,
|
'key': key,
|
||||||
'description': f'Platzhalter: {key}', # Generic description
|
'description': f'Platzhalter: {key}', # Generic description
|
||||||
'example': str(example)
|
'example': str(example),
|
||||||
|
'ai_caption': f'Platzhalter {key} (noch ohne erweiterte Registry-Beschreibung).',
|
||||||
})
|
})
|
||||||
|
|
||||||
return catalog
|
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.
|
Replace {{placeholder}} with values from variables dict.
|
||||||
|
|
||||||
Supports modifiers:
|
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:
|
Args:
|
||||||
template: String with {{key}} or {{key|modifiers}} placeholders
|
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
|
# Apply modifiers
|
||||||
if 'd' in modifiers:
|
if 'd' in modifiers:
|
||||||
if catalog:
|
if catalog:
|
||||||
# Add description from catalog
|
caption = None
|
||||||
description = None
|
|
||||||
for cat_items in catalog.values():
|
for cat_items in catalog.values():
|
||||||
matching = [item for item in cat_items if item['key'] == key]
|
matching = [item for item in cat_items if item['key'] == key]
|
||||||
if matching:
|
if matching:
|
||||||
description = matching[0].get('description', '')
|
row = matching[0]
|
||||||
|
caption = (row.get('ai_caption') or row.get('description') or '').strip()
|
||||||
break
|
break
|
||||||
|
|
||||||
if description:
|
if caption:
|
||||||
resolved_value = f"{resolved_value} ({description})"
|
resolved_value = f"{resolved_value} — {caption}"
|
||||||
else:
|
else:
|
||||||
# Catalog not available - log warning in debug
|
# Catalog not available - log warning in debug
|
||||||
if debug_info is not None:
|
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] = []
|
export_data['placeholders_by_category'][category] = []
|
||||||
for item in items:
|
for item in items:
|
||||||
key = item['key'].replace('{{', '').replace('}}', '')
|
key = item['key'].replace('{{', '').replace('}}', '')
|
||||||
export_data['placeholders_by_category'][category].append({
|
row = {
|
||||||
'key': item['key'],
|
'key': item['key'],
|
||||||
'description': item['description'],
|
'description': item['description'],
|
||||||
'value': cleaned_values.get(key, 'nicht verfügbar'),
|
'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
|
# Also include flat list for easy access
|
||||||
export_data['all_placeholders'] = cleaned_values
|
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