feat(agent): tune retrieval for decision support

This commit is contained in:
2026-09-16 09:34:29 +02:00
parent f525fb4862
commit 50ba153904
7 changed files with 290 additions and 12 deletions
+68
View File
@@ -11,6 +11,7 @@ from __future__ import annotations
import json
import re
from dataclasses import dataclass
from datetime import date
YEAR_RE = re.compile(r"\b(?:19|20)\d{2}\b")
JSON_RE = re.compile(r"\{.*\}", re.DOTALL)
@@ -19,6 +20,12 @@ AGGREGATION_RE = re.compile(
r"neuerungen|übersicht|zusammenfassung|alle\s|übersicht", re.I
)
COMPARISON_RE = re.compile(r"unterschied|vergleic|\bbzw\.|\bsowie\b", re.I)
DECISION_SUPPORT_RE = re.compile(
r"günstig(?:ste|er|e)?\s+lösung|abgabenbegünstigt|steuerfrei.*(?:auszahl|präm)|"
r"(?:zusätzlich|extra).*(?:auszahl|präm)",
re.IGNORECASE,
)
PLANNER_PROMPT = """Du planst Suchanfragen für eine Wissensbasis zur österreichischen
Personalverrechnung (kuratierte Briefings, Kollektivverträge je Branche und
@@ -51,6 +58,11 @@ class SubQuery:
scope: str | None = None # "gesetz" (ohne Branchen-KV) | "kv" | None
def is_decision_support(question: str) -> bool:
"""Erkennt Gestaltungsfragen, die den deterministischen Plan benötigen."""
return bool(DECISION_SUPPORT_RE.search(question))
def should_plan(question: str) -> bool:
"""Heuristik-Gate: nur komplexe Fragen bekommen einen Planer-Call.
@@ -64,6 +76,8 @@ def should_plan(question: str) -> bool:
return True
if COMPARISON_RE.search(q):
return True
if is_decision_support(q):
return True
if AGGREGATION_RE.search(q):
return True
if " und " in q.casefold() and len(words) >= 10:
@@ -71,6 +85,56 @@ def should_plan(question: str) -> bool:
return False
def decision_support_plan(
question: str, default_year: str | None = None
) -> tuple[list[SubQuery], str] | None:
"""Deterministische Zerlegung für Gestaltungsfragen zu zusätzlichen
Arbeitnehmerleistungen. Sie verhindert, dass die vielen Branchen-KV-
Treffer allgemeine Abgabenregeln verdrängen.
Ohne genanntes Jahr wird der neueste Indexstand verwendet; die Antwort
muss fehlenden Einzelfallkontext weiterhin über die Prompt-Regel erfragen.
"""
if not is_decision_support(question):
return None
explicit_year = YEAR_RE.search(question)
year = explicit_year.group(0) if explicit_year else default_year
year_text = f" {year}" if year else ""
direct = SubQuery(
text=(
f"Mitarbeiterprämie{year_text} WIKU Personal aktuell Nr 12 FAQ "
"Höchstbetrag 500 Lohnsteuer SV BV Beitragsfreie Bezugsbestandteile "
"§ 49 Abs 3 Z 30"
),
stand_year=year,
scope="gesetz",
)
cash_and_benefits = SubQuery(
text=(
"Zukunftssicherungsmaßnahmen 300 Kalenderjahr Risikoversicherung "
"Gruppenerfordernis Bezugsumwandlung Lohnsteuer SV BV DB DZ KommSt"
),
stand_year=year,
scope="gesetz",
)
purpose_bound = SubQuery(
text=(
"Sachzuwendungen Geschenke Barzahlungen stets steuerpflichtig Freibetrag "
"Beitragsfreie Bezugsbestandteile Z 1 bis Z 16a Mahlzeiten "
"Kinderbetreuungszuschuss"
),
stand_year=year,
scope="gesetz",
)
broad = bool(
re.search(r"günstig|möglichkeiten|statt|alternative", question, re.IGNORECASE)
)
# Ein Gestaltungsvergleich ist kein Korpus-Survey: Die drei gezielten
# Queries passen in den normalen Antwortpfad und vermeiden unnötiges
# Map-Reduce samt zusätzlicher Latenz/Fehlverweigerungsrisiko.
return ([direct, cash_and_benefits, purpose_bound] if broad else [direct]), "specific"
def parse_plan(raw: str, original: str) -> tuple[list[SubQuery], str]:
"""Robustes JSON-Parsing; jeder Fehler → [Originalfrage]. Liefert
(Sub-Queries, Fragetyp 'survey' | 'specific')."""
@@ -107,6 +171,10 @@ def plan_queries(
"""Liefert (Sub-Queries, geplant?, Fragetyp) — Call-/Parse-Fehler →
Original als Einzel-Query, Typ 'specific'. Der Planer-Call ist klein
(Frage ohne Kontext, kurzes num_predict); Temperature 0."""
deterministic = decision_support_plan(question, default_year=str(date.today().year))
if deterministic:
subs, qtype = deterministic
return subs, True, qtype
if not should_plan(question):
return [SubQuery(text=question)], False, "specific"
prompt = PLANNER_PROMPT.format(question=question.strip())