mirror of
http://100.103.83.12:3003/fegger/pv-agent.git
synced 2026-09-17 19:56:24 +00:00
feat(agent): support cost questions and thinking fallback
This commit is contained in:
@@ -185,6 +185,16 @@ questions:
|
|||||||
expected_ids: [wk-akt-04]
|
expected_ids: [wk-akt-04]
|
||||||
note: "Prämisse korrigieren: keine voraussetzungslose Auszahlung; lohngestaltende Grundlage, Zeitraum, Zusätzlichkeit und Abgaben außerhalb der Lohnsteuer anhand der Quelle erklären."
|
note: "Prämisse korrigieren: keine voraussetzungslose Auszahlung; lohngestaltende Grundlage, Zeitraum, Zusätzlichkeit und Abgaben außerhalb der Lohnsteuer anhand der Quelle erklären."
|
||||||
tags: [decision-support, premise-correction]
|
tags: [decision-support, premise-correction]
|
||||||
|
- id: q-128
|
||||||
|
question: "Mein Mitarbeiter verdient EUR 3000 brutto. Ich möchte ihm einmalig 500,- bar auszahlen. Was ist hier die günstigste Lösung?"
|
||||||
|
expected_ids: [wk-akt-04, lb-son-04, lb-sva-06]
|
||||||
|
note: "Gestaltungsfrage mit konkretem Einzelfall und Umlaut-freier Schreibweise (guenstigste loesung): Deterministischer Plan muss Kostendimensionen beisteuern — Lohnsteuer-Behandlung der einmaligen Barzahlung (lb-son-04) und Arbeitgeberbelastung (lb-sva-06). Keine pauschale Empfehlung ohne Abgabenvergleich; Antwort soll mit belegten Sätzen rechnen."
|
||||||
|
tags: [decision-support, cost-calculation, umlaut-free]
|
||||||
|
- id: q-129
|
||||||
|
question: "Mein Mitarbeiter verdient EUR 3000 brutto. Ich möchte ihm einmalig 500,- bar auszahlen. Wieviel kostet mich das?"
|
||||||
|
expected_ids: [lb-son-04, lb-sva-06]
|
||||||
|
note: "Reine Kostenaufstellung: Arbeitgeberkosten der Barzahlung Schritt für Schritt aus belegten Sätzen (Lohnsteuer, AG-SV inkl. DB/DZ) rechnen; Annahmen aus dem Kontext als solche nennen."
|
||||||
|
tags: [decision-support, cost-calculation, umlaut-free]
|
||||||
|
|
||||||
# --- Komplexe Fragen (Stufe 1, M6: Multi-Query + Temporal-Intent) ---
|
# --- Komplexe Fragen (Stufe 1, M6: Multi-Query + Temporal-Intent) ---
|
||||||
- id: q-110
|
- id: q-110
|
||||||
|
|||||||
+38
-12
@@ -12,7 +12,7 @@ import time
|
|||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .normalize import normalize_text
|
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 .query_planner import SubQuery, is_decision_support, plan_queries
|
||||||
from .retrieve import ChunkResult, Retriever
|
from .retrieve import ChunkResult, Retriever
|
||||||
|
|
||||||
@@ -71,9 +71,18 @@ Verbindliche Regeln:
|
|||||||
von zweckgebundenen Sach- oder Kostenleistungen. Vergleiche belegte
|
von zweckgebundenen Sach- oder Kostenleistungen. Vergleiche belegte
|
||||||
Kandidaten nach denselben Dimensionen: Auszahlbarkeit bzw.
|
Kandidaten nach denselben Dimensionen: Auszahlbarkeit bzw.
|
||||||
Verwendungsbindung, Lohnsteuer, Sozialversicherung einschließlich BV und
|
Verwendungsbindung, Lohnsteuer, Sozialversicherung einschließlich BV und
|
||||||
Lohnnebenkosten, Voraussetzungen sowie Stand. Nenne Quellenwidersprüche
|
Lohnnebenkosten, Voraussetzungen sowie Stand. Nennt die Frage konkrete
|
||||||
gemäß Regel 5 und stelle bei fehlendem Kontext am Ende genau EINE gezielte
|
Eckdaten (z. B. Bruttolohn, Betrag, Einmaligkeit, Barzahlung), wende die
|
||||||
Rückfrage. Sind [wk-akt-04] und [lb-sva-03] beide als Block-Köpfe im
|
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:
|
Kontext vorhanden, stelle den Konflikt ausdrücklich mit ⚠ dar:
|
||||||
[wk-akt-04] nennt die Mitarbeiterprämie 2026 SV-/BV-pflichtig;
|
[wk-akt-04] nennt die Mitarbeiterprämie 2026 SV-/BV-pflichtig;
|
||||||
[lb-sva-03] ordnet sie systematisch in den taxativen Katalog
|
[lb-sva-03] ordnet sie systematisch in den taxativen Katalog
|
||||||
@@ -268,6 +277,10 @@ def answer_question(
|
|||||||
if cfg.planner_enabled:
|
if cfg.planner_enabled:
|
||||||
try:
|
try:
|
||||||
sub_queries, planned, qtype = plan_queries(question, client, cfg)
|
sub_queries, planned, qtype = plan_queries(question, client, cfg)
|
||||||
|
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]
|
sub_queries = sub_queries[: cfg.planner_max_queries] or sub_queries[:1]
|
||||||
except Exception:
|
except Exception:
|
||||||
sub_queries, planned, qtype = [SubQuery(text=question)], False, "specific"
|
sub_queries, planned, qtype = [SubQuery(text=question)], False, "specific"
|
||||||
@@ -332,28 +345,41 @@ def answer_question(
|
|||||||
def chat(msgs, num_predict: int | None = None):
|
def chat(msgs, num_predict: int | None = None):
|
||||||
"""Ein Chat-Zug; liefert (gestrippter Content, done_reason).
|
"""Ein Chat-Zug; liefert (gestrippter Content, done_reason).
|
||||||
Bei done_reason='length' (Antwort bei num_predict abgeschnitten)
|
Bei done_reason='length' (Antwort bei num_predict abgeschnitten)
|
||||||
sind Zitationen ggf. unvollstaendig — der Aufrufer ruft einmal mit
|
sind Zitationen ggf. unvollständig — der Aufrufer ruft einmal mit
|
||||||
doppeltem Budget neu (technischer Retry, kein Regel-Regeneration)."""
|
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
|
budget = num_predict or cfg.num_predict
|
||||||
|
|
||||||
|
def call(use_think: bool):
|
||||||
if hasattr(client, "chat_full"):
|
if hasattr(client, "chat_full"):
|
||||||
raw, done_reason = client.chat_full(
|
return client.chat_full(
|
||||||
cfg.answer_model,
|
cfg.answer_model,
|
||||||
msgs,
|
msgs,
|
||||||
temperature=cfg.temperature,
|
temperature=cfg.temperature,
|
||||||
num_ctx=cfg.num_ctx,
|
num_ctx=cfg.num_ctx,
|
||||||
num_predict=budget,
|
num_predict=budget,
|
||||||
think=cfg.think,
|
think=use_think,
|
||||||
)
|
)
|
||||||
else:
|
return (
|
||||||
raw = client.chat(
|
client.chat(
|
||||||
cfg.answer_model,
|
cfg.answer_model,
|
||||||
msgs,
|
msgs,
|
||||||
temperature=cfg.temperature,
|
temperature=cfg.temperature,
|
||||||
num_ctx=cfg.num_ctx,
|
num_ctx=cfg.num_ctx,
|
||||||
num_predict=budget,
|
num_predict=budget,
|
||||||
think=cfg.think,
|
think=use_think,
|
||||||
|
),
|
||||||
|
"stop",
|
||||||
)
|
)
|
||||||
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
|
return strip_think(raw), done_reason
|
||||||
|
|
||||||
def chat_with_length_retry(msgs):
|
def chat_with_length_retry(msgs):
|
||||||
|
|||||||
+64
-8
@@ -13,6 +13,8 @@ import re
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
|
from .normalize import normalize_text
|
||||||
|
|
||||||
YEAR_RE = re.compile(r"\b(?:19|20)\d{2}\b")
|
YEAR_RE = re.compile(r"\b(?:19|20)\d{2}\b")
|
||||||
JSON_RE = re.compile(r"\{.*\}", re.DOTALL)
|
JSON_RE = re.compile(r"\{.*\}", re.DOTALL)
|
||||||
|
|
||||||
@@ -20,9 +22,30 @@ AGGREGATION_RE = re.compile(
|
|||||||
r"neuerungen|übersicht|zusammenfassung|alle\s|übersicht", re.I
|
r"neuerungen|übersicht|zusammenfassung|alle\s|übersicht", re.I
|
||||||
)
|
)
|
||||||
COMPARISON_RE = re.compile(r"unterschied|vergleic|\bbzw\.|\bsowie\b", 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(
|
DECISION_SUPPORT_RE = re.compile(
|
||||||
r"günstig(?:ste|er|e)?\s+lösung|abgabenbegünstigt|steuerfrei.*(?:auszahl|präm)|"
|
r"gu(e)?nstig(e|er|ste)?\s+lo(e)?sung"
|
||||||
r"(?:zusätzlich|extra).*(?:auszahl|präm)",
|
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,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -59,8 +82,12 @@ class SubQuery:
|
|||||||
|
|
||||||
|
|
||||||
def is_decision_support(question: str) -> bool:
|
def is_decision_support(question: str) -> bool:
|
||||||
"""Erkennt Gestaltungsfragen, die den deterministischen Plan benötigen."""
|
"""Erkennt Gestaltungsfragen, die den deterministischen Plan benötigen.
|
||||||
return bool(DECISION_SUPPORT_RE.search(question))
|
|
||||||
|
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:
|
def should_plan(question: str) -> bool:
|
||||||
@@ -97,6 +124,7 @@ def decision_support_plan(
|
|||||||
"""
|
"""
|
||||||
if not is_decision_support(question):
|
if not is_decision_support(question):
|
||||||
return None
|
return None
|
||||||
|
folded = normalize_text(question)
|
||||||
explicit_year = YEAR_RE.search(question)
|
explicit_year = YEAR_RE.search(question)
|
||||||
year = explicit_year.group(0) if explicit_year else default_year
|
year = explicit_year.group(0) if explicit_year else default_year
|
||||||
year_text = f" {year}" if year else ""
|
year_text = f" {year}" if year else ""
|
||||||
@@ -126,13 +154,41 @@ def decision_support_plan(
|
|||||||
stand_year=year,
|
stand_year=year,
|
||||||
scope="gesetz",
|
scope="gesetz",
|
||||||
)
|
)
|
||||||
broad = bool(
|
# Kostenaufstellung: einmalige Barzahlung -> Lohnsteuer-Behandlung der
|
||||||
re.search(r"günstig|möglichkeiten|statt|alternative", question, re.IGNORECASE)
|
# 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
|
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
|
# Queries passen in den normalen Antwortpfad und vermeiden unnötiges
|
||||||
# Map-Reduce samt zusätzlicher Latenz/Fehlverweigerungsrisiko.
|
# Map-Reduce samt zusätzlicher Latenz/Fehlverweigerungsrisiko.
|
||||||
return ([direct, cash_and_benefits, purpose_bound] if broad else [direct]), "specific"
|
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]:
|
def parse_plan(raw: str, original: str) -> tuple[list[SubQuery], str]:
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ def test_system_prompt_requires_contextual_decision_support():
|
|||||||
assert "genau EINE gezielte" in SYSTEM_PROMPT
|
assert "genau EINE gezielte" in SYSTEM_PROMPT
|
||||||
assert "Regelnummern niemals" in SYSTEM_PROMPT
|
assert "Regelnummern niemals" in SYSTEM_PROMPT
|
||||||
assert "spekuliere nicht mit Trainingswissen" 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:
|
class TestDecisionSupportValidation:
|
||||||
@@ -244,6 +248,43 @@ def test_trim_results_drops_tail_under_budget():
|
|||||||
assert trim_results(blocks, None) is blocks
|
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):
|
def test_length_retry_doubles_budget(mini_index):
|
||||||
"""done_reason='length' (abgeschnittene Antwort) -> ein technischer
|
"""done_reason='length' (abgeschnittene Antwort) -> ein technischer
|
||||||
Retry mit doppeltem num_predict; zaehlt nicht als Regel-Regenerierung."""
|
Retry mit doppeltem num_predict; zaehlt nicht als Regel-Regenerierung."""
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from agent.generate import answer_question
|
|||||||
from agent.query_planner import (
|
from agent.query_planner import (
|
||||||
SubQuery,
|
SubQuery,
|
||||||
decision_support_plan,
|
decision_support_plan,
|
||||||
|
is_decision_support,
|
||||||
parse_plan,
|
parse_plan,
|
||||||
plan_queries,
|
plan_queries,
|
||||||
should_plan,
|
should_plan,
|
||||||
@@ -37,6 +38,37 @@ def test_should_plan_gate():
|
|||||||
assert not should_plan("Wie hoch ist der KV-Mindestlohn im Friseurgewerbe?")
|
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():
|
def test_decision_support_plan_splits_direct_payment_and_alternatives():
|
||||||
planned = decision_support_plan(
|
planned = decision_support_plan(
|
||||||
"Ich will meinem Mitarbeiter 500 Euro zusätzlich auszahlen. "
|
"Ich will meinem Mitarbeiter 500 Euro zusätzlich auszahlen. "
|
||||||
|
|||||||
Reference in New Issue
Block a user