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
+41
View File
@@ -53,6 +53,10 @@ def test_system_prompt_requires_contextual_decision_support():
assert "genau EINE gezielte" in SYSTEM_PROMPT
assert "Regelnummern niemals" in SYSTEM_PROMPT
assert "spekuliere nicht mit Trainingswissen" in SYSTEM_PROMPT
# Anwendung auf den konkreten Einzelfall (Kostenaufstellung)
assert "Arbeitgeberkosten Schritt für Schritt" in SYSTEM_PROMPT
assert "nenne diese Annahme explizit" in SYSTEM_PROMPT
assert "nicht zusätzlich, wenn die Rechnung" in SYSTEM_PROMPT
class TestDecisionSupportValidation:
@@ -244,6 +248,43 @@ def test_trim_results_drops_tail_under_budget():
assert trim_results(blocks, None) is blocks
def test_thinking_only_empty_content_retries_without_think(mini_index):
"""think=true: qwen3.8 lieferte fallweise leeren content (Antwort nur im
thinking-Feld). Ein OllamaError auf dem think-Zug -> einmaliger Retry ohne
thinking; die Anfrage scheitert nicht mehr mit 503."""
from agent.ollama_client import OllamaError
class ThinkingClient:
def __init__(self):
self.think_flags = []
def chat_full(self, model, messages, temperature=0.1, num_ctx=16384,
num_predict=1024, think=False):
self.think_flags.append(think)
if think:
raise OllamaError(
"empty response from qwen3.8:27b (keys: ['message'])"
)
return "Barauszahlung ist lohnsteuerpflichtig [lb-min-01].", "stop"
def chat(self, *a, **k):
raise AssertionError("chat() sollte via chat_full laufen")
def close(self):
pass
import dataclasses
cfg = dataclasses.replace(mini_index, think=True)
client = ThinkingClient()
result = answer_question(
"Was passiert bei einer Barauszahlung?", cfg, client=client
)
assert result["verified"] is True
assert result["citations"] == ["lb-min-01"]
assert client.think_flags == [True, False]
def test_length_retry_doubles_budget(mini_index):
"""done_reason='length' (abgeschnittene Antwort) -> ein technischer
Retry mit doppeltem num_predict; zaehlt nicht als Regel-Regenerierung."""
+32
View File
@@ -9,6 +9,7 @@ from agent.generate import answer_question
from agent.query_planner import (
SubQuery,
decision_support_plan,
is_decision_support,
parse_plan,
plan_queries,
should_plan,
@@ -37,6 +38,37 @@ def test_should_plan_gate():
assert not should_plan("Wie hoch ist der KV-Mindestlohn im Friseurgewerbe?")
def test_decision_support_matches_umlaut_free_phrasings():
"""Nutzer geben Umlaute oft als ae/oe/ue ein; der Trigger muss beide
Schreibweisen erkennen und für Kostenaufstellungen eigene Queries liefern."""
q1 = (
"mein mitarbeiter verdient EUR 3000 brutto. ich moechte ihm einmalig "
"500,- bar auszahlen, was ist hier die guenstigste loesung?"
)
q2 = (
"mein mitarbeiter verdient EUR 3000 brutto. ich moechte ihm einmalig "
"500,- bar auszahlen, wieviel kostet mich das?"
)
assert is_decision_support(q1)
assert is_decision_support(q2)
subs1, qtype1 = decision_support_plan(q1, default_year="2026")
assert qtype1 == "specific"
assert len(subs1) == 4 # Vergleichs- + Kostendimension, Zukunftssicherung fällt weg
assert all(s.scope == "gesetz" for s in subs1)
texts1 = [s.text for s in subs1]
assert any("einmalige Bezüge" in t for t in texts1)
assert any("Dienstgeberbeitrag" in t for t in texts1)
subs2, qtype2 = decision_support_plan(q2, default_year="2026")
assert qtype2 == "specific"
assert len(subs2) == 3
texts = [s.text for s in subs2]
assert any("einmalige Bezüge" in t for t in texts)
assert any("Dienstgeberbeitrag" in t for t in texts)
assert all(s.scope == "gesetz" for s in subs2)
def test_decision_support_plan_splits_direct_payment_and_alternatives():
planned = decision_support_plan(
"Ich will meinem Mitarbeiter 500 Euro zusätzlich auszahlen. "