Merge pull request #145 from valueonag/feat/demo-system-readieness
trustee agent fix
This commit is contained in:
commit
4d7ccb0418
3 changed files with 211 additions and 0 deletions
|
|
@ -633,6 +633,107 @@ def getDataObjects() -> List[Dict[str, Any]]:
|
|||
return DATA_OBJECTS
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Feature Data Sub-Agent — domain hints
|
||||
# ---------------------------------------------------------------------------
|
||||
# Appended to the sub-agent's system prompt so it understands the Swiss
|
||||
# KMU chart-of-accounts conventions, the period-month semantics of
|
||||
# TrusteeDataAccountBalance, and the canonical query patterns. Without this
|
||||
# the agent invents wrong patterns: e.g. SUMming closingBalance across
|
||||
# years/months (closingBalance is already a balance) or guessing that 5400
|
||||
# (Materialaufwand) is a bank account.
|
||||
|
||||
_AGENT_DOMAIN_HINTS = """\
|
||||
TRUSTEE DOMAIN HINTS (Swiss KMU accounting):
|
||||
|
||||
CHART OF ACCOUNTS — account number prefixes (KMU-Kontenrahmen):
|
||||
1xxx = Aktiven (assets)
|
||||
100x Kasse / cash on hand (e.g. 1000 Hauptkasse)
|
||||
102x Bank / Post (e.g. 1020 ZKB, 1021 PostFinance, 1025 UBS)
|
||||
105x Wertschriften
|
||||
11xx Forderungen aus Lieferungen und Leistungen (receivables)
|
||||
14xx Anlagevermögen (fixed assets)
|
||||
2xxx = Passiven (liabilities + equity)
|
||||
20xx Verbindlichkeiten (payables)
|
||||
28xx Eigenkapital (equity)
|
||||
3xxx = Ertrag (revenue)
|
||||
4xxx = Material-/Warenaufwand
|
||||
5xxx = Personalaufwand
|
||||
6xxx = übriger betrieblicher Aufwand
|
||||
8xxx = ausserordentliches / betriebsfremdes Ergebnis
|
||||
9xxx = Abschluss
|
||||
|
||||
TrusteeDataAccount.accountType is one of: asset / liability / revenue /
|
||||
expense / closing (derived from the first digit when the source system
|
||||
doesn't provide it).
|
||||
→ "Bankkonten" = accountNumber LIKE '102%'.
|
||||
→ "Kassenkonten" = accountNumber LIKE '100%'.
|
||||
→ "Liquide Mittel / Cash" = accountNumber LIKE '10%'.
|
||||
|
||||
PERIOD CONVENTION on TrusteeDataAccountBalance:
|
||||
periodMonth = 0 → annual total for periodYear (use this for
|
||||
"Saldo per 31.12.YYYY" / "Stand Jahresende")
|
||||
periodMonth = 1..12 → monthly snapshot (use the month the question
|
||||
refers to, e.g. "per Ende März" → periodMonth=3)
|
||||
closingBalance is the balance AT THE END of the period; openingBalance
|
||||
is the balance AT THE START.
|
||||
|
||||
CANONICAL QUERY PATTERNS:
|
||||
|
||||
1) "Banksaldo per 31.12.2025" (single tool call, no aggregate):
|
||||
queryTable(
|
||||
tableName="TrusteeDataAccountBalance",
|
||||
filters=[
|
||||
{"field": "periodYear", "op": "=", "value": 2025},
|
||||
{"field": "periodMonth", "op": "=", "value": 0},
|
||||
{"field": "accountNumber", "op": "LIKE", "value": "102%"}
|
||||
],
|
||||
fields=["accountNumber", "closingBalance", "currency"]
|
||||
)
|
||||
→ return the rows as-is. Sum them ONLY in the final answer if the user
|
||||
asked for a total across banks; otherwise list per account.
|
||||
|
||||
2) "Saldo Konto 1020 per Ende 2024":
|
||||
queryTable(
|
||||
tableName="TrusteeDataAccountBalance",
|
||||
filters=[
|
||||
{"field": "accountNumber", "op": "=", "value": "1020"},
|
||||
{"field": "periodYear", "op": "=", "value": 2024},
|
||||
{"field": "periodMonth", "op": "=", "value": 0}
|
||||
],
|
||||
fields=["closingBalance", "currency"]
|
||||
)
|
||||
|
||||
3) "Welche Konten gehören zu welchem Typ?" — query TrusteeDataAccount.
|
||||
|
||||
4) "Buchungen im März 2025" — bookingDate is a UTC unix-seconds float on
|
||||
TrusteeDataJournalEntry. Convert: 2025-03-01 → 1740787200.0,
|
||||
2025-04-01 → 1743465600.0, then filter '>= 1740787200.0' AND
|
||||
'< 1743465600.0'. NEVER compare bookingDate against ISO strings.
|
||||
|
||||
ANTI-PATTERNS (do NOT do this):
|
||||
- aggregateTable(SUM, closingBalance, GROUP BY accountNumber) without
|
||||
period filters — closingBalance is already a balance per period; summing
|
||||
multiple periods produces nonsense (e.g. 7 years × 13 periods = ~90×
|
||||
the real saldo).
|
||||
- Using debitTotal/creditTotal as a balance — those are turnovers, not
|
||||
balances. The balance is closingBalance.
|
||||
- Picking the top-N accounts by SUM and assuming they are "the most
|
||||
important". Account 5400 (Materialaufwand) and 3434 (Erlöskorrekturen)
|
||||
are NOT bank accounts.
|
||||
"""
|
||||
|
||||
|
||||
def getAgentDomainHints() -> str:
|
||||
"""Return Trustee-specific guidance for the Feature Data Sub-Agent.
|
||||
|
||||
The text is appended verbatim to the sub-agent's system prompt by
|
||||
``featureDataAgent._buildSchemaContext``. Keep it concise and
|
||||
pattern-driven — every line costs tokens on every sub-agent call.
|
||||
"""
|
||||
return _AGENT_DOMAIN_HINTS
|
||||
|
||||
|
||||
def registerFeature(catalogService) -> bool:
|
||||
"""
|
||||
Register this feature's RBAC objects in the catalog.
|
||||
|
|
|
|||
|
|
@ -339,6 +339,12 @@ def _buildSchemaContext(
|
|||
JournalLine for "Saldo per <date>"),
|
||||
* format date filters as UNIX timestamps when the type is float,
|
||||
* recognise FK relations even though the tools cannot JOIN.
|
||||
|
||||
On top of the generic schema block we append feature-specific domain
|
||||
hints (e.g. Swiss KMU chart-of-accounts conventions for trustee) when
|
||||
the feature module exports a ``getAgentDomainHints()`` function. This
|
||||
lets each feature teach the sub-agent its own jargon without polluting
|
||||
the generic agent code.
|
||||
"""
|
||||
tableNames: List[str] = []
|
||||
tableBlocks: List[str] = []
|
||||
|
|
@ -393,14 +399,69 @@ def _buildSchemaContext(
|
|||
"about the link in your answer.",
|
||||
"- When a table has period-bucketed aggregates (opening/closing balances or totals per "
|
||||
"period), prefer it over recomputing the same aggregate from raw transactional rows.",
|
||||
"- NEVER apply SUM/AVG to columns that already represent a balance, closing/opening "
|
||||
"total or aggregate (e.g. closingBalance, openingBalance, debitTotal, creditTotal, "
|
||||
"*Balance, *Total). These are already aggregated per period — summing them across "
|
||||
"periods produces meaningless numbers. Use queryTable with explicit period filters "
|
||||
"instead, then pick the single matching row.",
|
||||
"- CRITICAL: Return data as compact JSON, NOT as markdown tables or prose.",
|
||||
"- Do NOT reformat, rewrite, or narrate the tool results. Return the raw data directly.",
|
||||
"- If the question asks for rows, return them as a JSON array. Do NOT generate a markdown table.",
|
||||
"- Keep your answer SHORT. The caller is a machine, not a human.",
|
||||
]
|
||||
|
||||
domainHints = _loadFeatureDomainHints(featureCode)
|
||||
if domainHints:
|
||||
parts.extend(["", domainHints.strip()])
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _loadFeatureDomainHints(featureCode: str) -> str:
|
||||
"""Pull optional domain-specific hints from the feature's main module.
|
||||
|
||||
Each feature can expose ``getAgentDomainHints() -> str`` in its
|
||||
``mainXxx.py``. The returned text is appended verbatim to the
|
||||
sub-agent's system prompt, so features can teach the agent their own
|
||||
domain jargon (chart-of-accounts conventions, period semantics, query
|
||||
patterns) without coupling the generic agent code to any one feature.
|
||||
|
||||
Failures (missing module, missing hook, exception inside the hook) are
|
||||
swallowed and just yield an empty hints block — domain hints are a
|
||||
best-effort enhancement, not a hard dependency.
|
||||
"""
|
||||
if not featureCode:
|
||||
return ""
|
||||
try:
|
||||
from modules.system.registry import loadFeatureMainModules
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
try:
|
||||
mainModules = loadFeatureMainModules() or {}
|
||||
except Exception as exc:
|
||||
logger.debug("Domain-hints lookup: cannot load main modules (%s)", exc)
|
||||
return ""
|
||||
|
||||
module = mainModules.get(featureCode) or mainModules.get(featureCode.lower())
|
||||
if module is None:
|
||||
return ""
|
||||
|
||||
hook = getattr(module, "getAgentDomainHints", None)
|
||||
if not callable(hook):
|
||||
return ""
|
||||
|
||||
try:
|
||||
hints = hook()
|
||||
except Exception as exc:
|
||||
logger.warning("Feature '%s' getAgentDomainHints() raised: %s", featureCode, exc)
|
||||
return ""
|
||||
|
||||
if not isinstance(hints, str):
|
||||
return ""
|
||||
return hints
|
||||
|
||||
|
||||
def _buildTableSchemaBlock(tableName: str, tableLabel: str, fields: List[str]) -> str:
|
||||
"""Render a single table's schema block, enriched from its Pydantic model.
|
||||
|
||||
|
|
|
|||
|
|
@ -134,3 +134,52 @@ def test_buildTableSchemaBlock_journalLineHasNoBookingDate():
|
|||
)
|
||||
assert "Table: TrusteeDataJournalLine" in block
|
||||
assert "bookingDate" not in block
|
||||
|
||||
|
||||
def test_buildSchemaContext_forbidsSummingAggregateFields():
|
||||
"""The most damaging anti-pattern in trustee queries: SUMming closingBalance
|
||||
across periods. Without this rule the agent reports 11 mio for an account
|
||||
whose real closing balance is 48k. The generic prompt must call this out so
|
||||
every feature benefits, not just trustee."""
|
||||
selected = [_trusteeAccountBalanceObj()]
|
||||
prompt = _buildSchemaContext(
|
||||
featureCode="trustee",
|
||||
instanceLabel="Demo AG",
|
||||
selectedTables=selected,
|
||||
requestLang="de",
|
||||
)
|
||||
assert "NEVER apply SUM/AVG to columns that already represent a balance" in prompt
|
||||
assert "closingBalance" in prompt
|
||||
|
||||
|
||||
def test_buildSchemaContext_appendsTrusteeDomainHints():
|
||||
"""When the feature module exposes getAgentDomainHints(), the schema prompt
|
||||
must include those hints so the sub-agent knows e.g. that 102x are bank
|
||||
accounts and periodMonth=0 is the annual total."""
|
||||
selected = [_trusteeAccountBalanceObj()]
|
||||
prompt = _buildSchemaContext(
|
||||
featureCode="trustee",
|
||||
instanceLabel="Demo AG",
|
||||
selectedTables=selected,
|
||||
requestLang="de",
|
||||
)
|
||||
assert "TRUSTEE DOMAIN HINTS" in prompt
|
||||
assert "102x Bank / Post" in prompt
|
||||
assert "periodMonth = 0" in prompt
|
||||
assert "ANTI-PATTERNS" in prompt
|
||||
assert 'LIKE \'102%\'' in prompt or "LIKE '102%'" in prompt
|
||||
|
||||
|
||||
def test_buildSchemaContext_skipsHintsForFeaturesWithoutHook():
|
||||
"""Features that don't export getAgentDomainHints() should produce a prompt
|
||||
without the trailing hints block. Verified by using a feature code that
|
||||
cannot resolve to a main module (registry returns None)."""
|
||||
selected = [_trusteeAccountBalanceObj()]
|
||||
prompt = _buildSchemaContext(
|
||||
featureCode="nosuchfeature",
|
||||
instanceLabel="",
|
||||
selectedTables=selected,
|
||||
requestLang="de",
|
||||
)
|
||||
assert "TRUSTEE DOMAIN HINTS" not in prompt
|
||||
assert "Keep your answer SHORT" in prompt
|
||||
|
|
|
|||
Loading…
Reference in a new issue