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
+51 -25
View File
@@ -12,7 +12,7 @@ import time
from .config import Config
from .normalize import normalize_text
from .ollama_client import OllamaClient
from .ollama_client import OllamaClient, OllamaError
from .query_planner import SubQuery, is_decision_support, plan_queries
from .retrieve import ChunkResult, Retriever
@@ -71,9 +71,18 @@ Verbindliche Regeln:
von zweckgebundenen Sach- oder Kostenleistungen. Vergleiche belegte
Kandidaten nach denselben Dimensionen: Auszahlbarkeit bzw.
Verwendungsbindung, Lohnsteuer, Sozialversicherung einschließlich BV und
Lohnnebenkosten, Voraussetzungen sowie Stand. Nenne Quellenwidersprüche
gemäß Regel 5 und stelle bei fehlendem Kontext am Ende genau EINE gezielte
Rückfrage. Sind [wk-akt-04] und [lb-sva-03] beide als Block-Köpfe im
Lohnnebenkosten, Voraussetzungen sowie Stand. Nennt die Frage konkrete
Eckdaten (z. B. Bruttolohn, Betrag, Einmaligkeit, Barzahlung), wende die
belegten Sätze unmittelbar auf diesen Fall an: rechne mit den im Kontext
genannten Sätzen die Arbeitgeberkosten Schritt für Schritt (Lohnsteuer,
SV einschließlich DB und DZ, allfällige KommSt) und gib die Gesamtbelastung
an. Belege jede in die Rechnung eingehende Zahl mit [ID] und Stand; steht
ein Rechenwert nur allgemein im Kontext (z. B. Jahressechstel,
Bemessungsgrundlage), nenne diese Annahme explizit. Fehlt hingegen ein für
die Berechnung wesentlicher Parameter, stelle am Ende genau EINE gezielte
Rückfrage — nicht zusätzlich, wenn die Rechnung mit den Kontextwerten
möglich ist. Nenne Quellenwidersprüche
gemäß Regel 5. Sind [wk-akt-04] und [lb-sva-03] beide als Block-Köpfe im
Kontext vorhanden, stelle den Konflikt ausdrücklich mit ⚠ dar:
[wk-akt-04] nennt die Mitarbeiterprämie 2026 SV-/BV-pflichtig;
[lb-sva-03] ordnet sie systematisch in den taxativen Katalog
@@ -268,7 +277,11 @@ def answer_question(
if cfg.planner_enabled:
try:
sub_queries, planned, qtype = plan_queries(question, client, cfg)
sub_queries = sub_queries[: cfg.planner_max_queries] or sub_queries[:1]
if not is_decision_support(question):
# LLM-Pläne bleiben auf die Maximalzahl begrenzt; der
# deterministische Abgaben-Vergleichsplan liefert nur gezielte
# Queries (max. 5) und darf sie vollständig behalten.
sub_queries = sub_queries[: cfg.planner_max_queries] or sub_queries[:1]
except Exception:
sub_queries, planned, qtype = [SubQuery(text=question)], False, "specific"
@@ -332,28 +345,41 @@ def answer_question(
def chat(msgs, num_predict: int | None = None):
"""Ein Chat-Zug; liefert (gestrippter Content, done_reason).
Bei done_reason='length' (Antwort bei num_predict abgeschnitten)
sind Zitationen ggf. unvollstaendig — der Aufrufer ruft einmal mit
doppeltem Budget neu (technischer Retry, kein Regel-Regeneration)."""
sind Zitationen ggf. unvollständig — der Aufrufer ruft einmal mit
doppeltem Budget neu (technischer Retry, kein Regel-Regeneration).
Liefert das Modell bei aktivem Thinking leeren Content (Antwort nur
im thinking-Feld bzw. Budget im Thinking aufgebraucht), wird einmal
ohne Thinking wiederholt."""
budget = num_predict or cfg.num_predict
if hasattr(client, "chat_full"):
raw, done_reason = client.chat_full(
cfg.answer_model,
msgs,
temperature=cfg.temperature,
num_ctx=cfg.num_ctx,
num_predict=budget,
think=cfg.think,
def call(use_think: bool):
if hasattr(client, "chat_full"):
return client.chat_full(
cfg.answer_model,
msgs,
temperature=cfg.temperature,
num_ctx=cfg.num_ctx,
num_predict=budget,
think=use_think,
)
return (
client.chat(
cfg.answer_model,
msgs,
temperature=cfg.temperature,
num_ctx=cfg.num_ctx,
num_predict=budget,
think=use_think,
),
"stop",
)
else:
raw = client.chat(
cfg.answer_model,
msgs,
temperature=cfg.temperature,
num_ctx=cfg.num_ctx,
num_predict=budget,
think=cfg.think,
)
done_reason = "stop"
try:
raw, done_reason = call(cfg.think)
except OllamaError:
if not cfg.think:
raise
raw, done_reason = call(False)
return strip_think(raw), done_reason
def chat_with_length_retry(msgs):