dodil/erp-brick-consolidation
DODIL ERP brick: consolidation
src/brick_consolidation/infrastructure/ai_steps.py
"""AI steps (erp-roadmap #53, the AI-step contract): an AI step is a PROVIDER of a capability, with the
ManualTask as its fallback and its reviewer. The mode is part of the signed BPMN: off | suggest (the
default: AI drafts, a person approves) | auto-with-review | auto (only a step marked safe_for_auto; never
destructive or money-moving without approval).
TODO(ai): erp-platform has no step-mode API yet. Until it lands, `suggest` is this brick's job: the use
case stores the provider's draft as a ManualTask for a person to approve, and never acts on it alone.
Generated by `erp-compose scaffold` from module.yaml.
"""
from erp_platform.capabilities import CapabilitySeam
# What every AI action leaves behind (the AI-step contract): the provider's action log holds the model,
# the inputs, the output and the confidence; the brick records the approver — who accepted the ManualTask,
# or reviewed an auto-with-review result.
AUDIT = ("model", "inputs", "output", "confidence", "approver")
AI_STEPS = {
"explain_elimination_difference": {
"capability": "ai.suggest",
"mode": "suggest",
"modes_allowed": ["off", "suggest"],
"safe_for_auto": False,
"fallback": "the group accountant works the difference by hand",
"benefit": "a pair whose intercompany balances differ comes with a likely cause (an invoice in one month and the bill in the next, a different rate, a missing recharge)",
"inputs": ["the pair's reported data", "gl's intercompany match for the pair", "the two companies' rates"],
"eval": "seeded differences of each kind; accuracy of the cause",
},
}
SEAMS = {
"explain_elimination_difference": CapabilitySeam("ai.suggest"),
}
def propose(step: str, idempotency_key: str, input: dict, mode: str):
"""Ask the step's provider for a proposal. `off` → None (the process is the fallback). Otherwise the
provider's Result, or — no provider, or none wired — the ManualTask with the exact input (rule 8). What
happens next is the caller's, by the mode: suggest → a ManualTask a person accepts, edits or rejects;
auto-with-review → apply it and open a review task; auto → apply it (safe_for_auto steps only).
The provider never applies anything."""
spec = AI_STEPS[step]
if mode == "off":
return None
if mode not in spec["modes_allowed"]:
raise ValueError(f"{step}: mode {mode!r} is not allowed ({', '.join(spec['modes_allowed'])})")
if mode == "auto" and not spec["safe_for_auto"]:
raise ValueError(f"{step} is not safe for auto: a person approves it")
if step not in SEAMS:
return None # no capability yet: the fallback, a person (the note above)
return SEAMS[step].call(idempotency_key, input)