feat(agent): support cost questions and thinking fallback

This commit is contained in:
2026-09-17 00:02:38 +02:00
parent d9c612e354
commit cfa2f37986
5 changed files with 200 additions and 35 deletions
+66 -10
View File
@@ -13,6 +13,8 @@ import re
from dataclasses import dataclass
from datetime import date
from .normalize import normalize_text
YEAR_RE = re.compile(r"\b(?:19|20)\d{2}\b")
JSON_RE = re.compile(r"\{.*\}", re.DOTALL)
@@ -20,9 +22,30 @@ 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)
# Muster gelten auf normalize_text(question) (NFKD-Folding). Neben dem
# gefoldeten "gunstig/losung" decken die Alternativen auch die ue-Schreibweise
# ab ("guenstigste loesung"), die Nutzer häufig ohne Umlaute eingeben.
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)",
r"gu(e)?nstig(e|er|ste)?\s+lo(e)?sung"
r"|abgabenbegu(e)?nstigt"
r"|steuerfrei.*(?:auszahl|pra(e)?m)"
r"|(?:zusa(e)?tzl?ich|extra).*(?:auszahl|pra(e)?m)"
r"|\beinmalig(e|n|r|s)?\b.{0,80}?\bauszah"
r"|\bbar\s+auszah|\bbarauszah"
r"|\b(?:wie\s*viel|wieviel)\b.{0,60}?\bkostet\b"
r"|\bkostet\s+mich\b"
r"|\bmitarbeiterpra(e)?m",
re.IGNORECASE,
)
BROAD_RE = re.compile(
r"gu(e)?nstig|m(?:o|e)?glichkeiten|\bstatt\b|alternativ", re.IGNORECASE
)
COST_INTENT_RE = re.compile(
r"\b(?:wie\s*viel|wieviel)\b.{0,60}?\bkostet\b"
r"|\bkostet\s+mich\b"
r"|\beinmalig(e|n|r|s)?\b.{0,80}?\bauszah"
r"|\bbar\s+auszah|\bbarauszah",
re.IGNORECASE,
)
@@ -59,8 +82,12 @@ class SubQuery:
def is_decision_support(question: str) -> bool:
"""Erkennt Gestaltungsfragen, die den deterministischen Plan benötigen."""
return bool(DECISION_SUPPORT_RE.search(question))
"""Erkennt Gestaltungsfragen, die den deterministischen Plan benötigen.
Die Frage wird vorher normalisiert, damit auch Schreibweisen ohne Umlaute
("guenstigste loesung") erkannt werden.
"""
return bool(DECISION_SUPPORT_RE.search(normalize_text(question)))
def should_plan(question: str) -> bool:
@@ -97,6 +124,7 @@ def decision_support_plan(
"""
if not is_decision_support(question):
return None
folded = normalize_text(question)
explicit_year = YEAR_RE.search(question)
year = explicit_year.group(0) if explicit_year else default_year
year_text = f" {year}" if year else ""
@@ -126,13 +154,41 @@ def decision_support_plan(
stand_year=year,
scope="gesetz",
)
broad = bool(
re.search(r"günstig|möglichkeiten|statt|alternative", question, re.IGNORECASE)
# Kostenaufstellung: einmalige Barzahlung -> Lohnsteuer-Behandlung der
# Bezüge plus Arbeitgeberbelastung (SV-Beitragssätze, DB/DZ, KommSt).
cost_tax = SubQuery(
text=(
"einmalige Bezüge Sonderzahlungen Lohnsteuer Jahressechstel "
"6 Prozent steuerfrei 620 Freibetrag Zuwendung EStG"
),
stand_year=year,
scope="gesetz",
)
# 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"
cost_lnk = SubQuery(
text=(
"Beitragssätze des jeweiligen Jahres für Dienstnehmer "
"Dienstgeberanteil Sozialversicherungsbeiträge DB DZ Zuschlag "
"zum Dienstgeberbeitrag Kommunalsteuer Lohnnebenkosten Zuwendung"
),
stand_year=year,
scope="gesetz",
)
broad = bool(BROAD_RE.search(folded))
cost_intent = bool(COST_INTENT_RE.search(folded))
queries = [direct]
if broad and not cost_intent:
# Ein Gestaltungsvergleich ist kein Korpus-Survey: Die gezielten
# Queries passen in den normalen Antwortpfad und vermeiden unnötiges
# Map-Reduce samt zusätzlicher Latenz/Fehlverweigerungsrisiko.
queries += [cash_and_benefits, purpose_bound]
elif broad:
# Explizite Kostenaufstellung: Die Zukunftssicherungs-Query verdrängt
# bei Slots für vier Queries die Beitrags-/Lohnsteuerquellen; die
# zweckgebundenen Alternativen bleiben über purpose_bound abgedeckt.
queries.append(purpose_bound)
if cost_intent:
queries += [cost_tax, cost_lnk]
return queries[:4], "specific"
def parse_plan(raw: str, original: str) -> tuple[list[SubQuery], str]: