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:
|
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.
|
Text für |d und Exportfeld ai_caption: zuerst **was** der Platzhalter misst (description),
|
||||||
Nutzt business_meaning / semantic_contract; bei Scores explizite 0–100-Erläuterung.
|
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()
|
bm = (metadata.business_meaning or "").strip()
|
||||||
sc = (metadata.semantic_contract or "").strip()
|
sc = (metadata.semantic_contract or "").strip()
|
||||||
desc = (metadata.description or "").strip()
|
|
||||||
|
|
||||||
if bm:
|
chunks: List[str] = []
|
||||||
chunks.append(bm)
|
if desc:
|
||||||
elif sc:
|
|
||||||
chunks.append(sc if len(sc) <= max_len else sc[: max_len - 1] + "…")
|
|
||||||
elif desc:
|
|
||||||
chunks.append(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:
|
if metadata.placeholder_type == PlaceholderType.SCORE:
|
||||||
chunks.append("Skala 0–100: höher = im Modell günstiger / besser abgestimmt.")
|
chunks.append("Skala 0–100: höher = im Modell günstiger / besser abgestimmt.")
|
||||||
|
|
||||||
|
|
@ -282,8 +295,17 @@ def build_ai_placeholder_caption(metadata: PlaceholderMetadata, max_len: int = 4
|
||||||
if unit and metadata.placeholder_type != PlaceholderType.SCORE:
|
if unit and metadata.placeholder_type != PlaceholderType.SCORE:
|
||||||
blob = " ".join(chunks).lower()
|
blob = " ".join(chunks).lower()
|
||||||
u_low = unit.lower()
|
u_low = unit.lower()
|
||||||
if u_low not in blob and u_low.replace(" ", "") not in blob.replace(" ", ""):
|
# Einheit oft schon in description („… in g (30d)“, „Kalorien“) — nicht doppeln
|
||||||
if u_low not in ("score (0-100)", "0-100", "0–100", "dimensionless"):
|
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}.")
|
chunks.append(f"Technischer Bezug: {unit}.")
|
||||||
|
|
||||||
out = " ".join(c for c in chunks if c).strip()
|
out = " ".join(c for c in chunks if c).strip()
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ This module now focuses on FORMATTING for AI consumption.
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta
|
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
|
from db import get_db, get_cursor, r2d
|
||||||
|
|
||||||
# Phase 0c: Import data layer
|
# Phase 0c: Import data layer
|
||||||
|
|
@ -62,6 +62,17 @@ def _ai_caption_for_placeholder_key(key: str) -> Optional[str]:
|
||||||
return None
|
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 ──────────────────────────────────────────────────────────
|
# ── Helper Functions ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def get_profile_data(profile_id: str) -> Dict:
|
def get_profile_data(profile_id: str) -> Dict:
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ from placeholder_resolver import (
|
||||||
resolve_placeholders,
|
resolve_placeholders,
|
||||||
get_unknown_placeholders,
|
get_unknown_placeholders,
|
||||||
get_placeholder_example_values,
|
get_placeholder_example_values,
|
||||||
|
format_value_with_d_modifier,
|
||||||
get_available_placeholders,
|
get_available_placeholders,
|
||||||
get_placeholder_catalog
|
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.
|
Export all available placeholders with their current resolved values.
|
||||||
|
|
||||||
Returns JSON export suitable for download with all placeholders
|
Pro Zeile: value = Rohwert wie bei {{key}}, example = Vorschau wie bei {{key|d}}
|
||||||
resolved for the current user's profile.
|
(Wert — ai_caption bzw. description). JSON-Download für das aktive Profil.
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
profile_id = session['profile_id']
|
profile_id = session['profile_id']
|
||||||
|
|
@ -486,11 +487,12 @@ 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('}}', '')
|
||||||
|
raw_val = cleaned_values.get(key, 'nicht verfügbar')
|
||||||
row = {
|
row = {
|
||||||
'key': item['key'],
|
'key': item['key'],
|
||||||
'description': item['description'],
|
'description': item['description'],
|
||||||
'value': cleaned_values.get(key, 'nicht verfügbar'),
|
'value': raw_val,
|
||||||
'example': item.get('example'),
|
'example': format_value_with_d_modifier(str(raw_val), item),
|
||||||
}
|
}
|
||||||
if item.get('ai_caption'):
|
if item.get('ai_caption'):
|
||||||
row['ai_caption'] = item['ai_caption']
|
row['ai_caption'] = item['ai_caption']
|
||||||
|
|
@ -662,11 +664,12 @@ def export_placeholder_values_extended(
|
||||||
export_data['legacy']['placeholders_by_category'][category] = []
|
export_data['legacy']['placeholders_by_category'][category] = []
|
||||||
for item in items:
|
for item in items:
|
||||||
key = item['key'].replace('{{', '').replace('}}', '')
|
key = item['key'].replace('{{', '').replace('}}', '')
|
||||||
|
raw_val = cleaned_values.get(key, 'nicht verfügbar')
|
||||||
export_data['legacy']['placeholders_by_category'][category].append({
|
export_data['legacy']['placeholders_by_category'][category].append({
|
||||||
'key': item['key'],
|
'key': item['key'],
|
||||||
'description': item['description'],
|
'description': item['description'],
|
||||||
'value': cleaned_values.get(key, 'nicht verfügbar'),
|
'value': raw_val,
|
||||||
'example': item.get('example')
|
'example': format_value_with_d_modifier(str(raw_val), item),
|
||||||
})
|
})
|
||||||
|
|
||||||
# Fill metadata flat
|
# Fill metadata flat
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from placeholder_registry import (
|
||||||
build_ai_placeholder_caption,
|
build_ai_placeholder_caption,
|
||||||
)
|
)
|
||||||
import placeholder_resolver as pr
|
import placeholder_resolver as pr
|
||||||
|
from placeholder_resolver import format_value_with_d_modifier
|
||||||
|
|
||||||
|
|
||||||
def test_build_ai_caption_prefers_business_meaning():
|
def test_build_ai_caption_prefers_business_meaning():
|
||||||
|
|
@ -22,9 +23,28 @@ def test_build_ai_caption_prefers_business_meaning():
|
||||||
output_type=OutputType.NUMERIC,
|
output_type=OutputType.NUMERIC,
|
||||||
)
|
)
|
||||||
cap = build_ai_placeholder_caption(m)
|
cap = build_ai_placeholder_caption(m)
|
||||||
|
assert cap.startswith("Kurzbeschreibung")
|
||||||
assert "Kernbedeutung" in cap
|
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():
|
def test_build_ai_caption_score_adds_scale():
|
||||||
m = PlaceholderMetadata(
|
m = PlaceholderMetadata(
|
||||||
key="test_score",
|
key="test_score",
|
||||||
|
|
@ -54,3 +74,19 @@ def test_placeholder_token_regex_optional_modifier():
|
||||||
def test_get_unknown_placeholders_strips_modifier():
|
def test_get_unknown_placeholders_strips_modifier():
|
||||||
unk = pr.get_unknown_placeholders("{{not_a_real_key|d}}")
|
unk = pr.get_unknown_placeholders("{{not_a_real_key|d}}")
|
||||||
assert set(unk) == {"not_a_real_key"}
|
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