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
+48
View File
@@ -16,6 +16,7 @@ from agent.generate import (
strip_think,
trim_results,
validate_answer,
validate_decision_support_answer,
)
@@ -42,6 +43,53 @@ def test_system_prompt_requires_nonofficial_source_disclosure():
assert "nicht amtlich" in SYSTEM_PROMPT
def test_system_prompt_requires_contextual_decision_support():
assert "keine Option pauschal zum Sieger" in SYSTEM_PROMPT
assert "frei verfügbare Barzahlung" in SYSTEM_PROMPT
assert "systematisch in einen Katalog abgabenfreier Bezüge" in SYSTEM_PROMPT
assert "[wk-akt-04] und [lb-sva-03]" in SYSTEM_PROMPT
assert "Bezeichne keine der widersprechenden Quellen als" in SYSTEM_PROMPT
assert "genau EINE gezielte" in SYSTEM_PROMPT
assert "Regelnummern niemals" in SYSTEM_PROMPT
assert "spekuliere nicht mit Trainingswissen" in SYSTEM_PROMPT
class TestDecisionSupportValidation:
question = (
"Ich will meinem Mitarbeiter 500 Euro zusätzlich auszahlen. "
"Was ist die günstigste Lösung?"
)
allowed = ["wk-akt-04", "lb-sva-03"]
def test_requires_conflict_when_both_sources_are_in_context(self):
violations = validate_decision_support_answer(
self.question, "Nur lohnsteuerfrei [wk-akt-04].", self.allowed
)
assert violations and "Quellenkonflikt" in violations[0]
def test_accepts_explicit_conflict(self):
answer = (
"⚠ [lb-sva-03] ordnet die Prämie in den Katalog beitragsfreier "
"Bezüge ein; [wk-akt-04] nennt sie SV- und BV-pflichtig."
)
assert validate_decision_support_answer(self.question, answer, self.allowed) == []
def test_rejects_reversed_source_roles_and_prompt_leakage(self):
answer = (
"⚠ [wk-akt-04] und [lb-sva-03] ordnen die Prämie als beitragsfrei "
"ein; gemäß Regel 11 ist die aktuellere News-Quelle maßgeblich."
)
violations = validate_decision_support_answer(self.question, answer, self.allowed)
assert any("Rollen" in item for item in violations)
assert any("internen Regeln" in item for item in violations)
assert any("priorisiere" in item for item in violations)
def test_does_not_require_missing_source(self):
assert validate_decision_support_answer(
self.question, "Lohnsteuerfrei [wk-akt-04].", ["wk-akt-04"]
) == []
class TestRefusalDetection:
def test_refusal_phrase(self):
assert looks_like_refusal("Dazu enthält die Wissensbasis keine Aussage.")
+40 -1
View File
@@ -6,7 +6,13 @@ import pytest
from agent.config import Config
from agent.generate import answer_question
from agent.query_planner import SubQuery, parse_plan, plan_queries, should_plan
from agent.query_planner import (
SubQuery,
decision_support_plan,
parse_plan,
plan_queries,
should_plan,
)
from tests.conftest import FakeOllama
@@ -20,12 +26,45 @@ def test_should_plan_gate():
"Wie wird die Überstundenpauschale behandelt und wie wirkt sie sich "
"auf die Sozialversicherung und die Lohnsteuer aus?"
)
# Gestaltungsfrage -> planen, auch wenn sie unter der Längenschwelle bleibt
assert should_plan(
"Ich will meinem Mitarbeiter 500 Euro zusätzlich auszahlen. "
"Was ist die günstigste Lösung?"
)
# einfach -> Single-Shot
assert not should_plan("Wie viele Werktage Urlaub stehen Arbeitnehmern zu?")
assert not should_plan("Was ist Altersteilzeit?")
assert not should_plan("Wie hoch ist der KV-Mindestlohn im Friseurgewerbe?")
def test_decision_support_plan_splits_direct_payment_and_alternatives():
planned = decision_support_plan(
"Ich will meinem Mitarbeiter 500 Euro zusätzlich auszahlen. "
"Was ist die günstigste Lösung?",
default_year="2026",
)
assert planned is not None
subs, qtype = planned
assert qtype == "specific"
assert len(subs) == 3
assert all(s.scope == "gesetz" for s in subs)
assert all(s.stand_year == "2026" for s in subs)
assert "Mitarbeiterprämie 2026" in subs[0].text
assert "Zukunftssicherung" in subs[1].text
assert "Mahlzeiten" in subs[2].text
def test_decision_support_plan_keeps_explicit_year_and_specific_intent():
subs, qtype = decision_support_plan(
"Kann ich 2025 jedem Mitarbeiter einfach 500 Euro steuerfrei auszahlen?",
default_year="2026",
)
assert qtype == "specific"
assert len(subs) == 1
assert subs[0].stand_year == "2025"
assert "Mitarbeiterprämie 2025" in subs[0].text
def test_parse_plan_valid_and_fallback():
subs, qtype = parse_plan(
'Vorab: {"type": "specific", "queries": [{"text": "mindestlohn friseur", "stand_year": "2024"}, '