dodil/erp-brick-projects

src/brick_projects/infrastructure/ai_steps.py

"""AI steps (the M0 AI-step contract): an AI step is a PROVIDER of a capability, with its fallback as the
manual path. Every step here is suggest-only — it never changes an item, never writes to a client.

erp-platform's capabilities.yaml names no AI capability yet, so no provider can be wired: each step answers
with its fallback, saying so. Proposed capability names, for when it does: `ai.suggest_plan`,
`ai.draft_message`, `ai.flag_risk`. Wiring one turns the same use case into a suggestion, with no new step.
"""

AI_STEPS = {
    "ai_draft_plan": {
        "capability": None,
        "safe_for_auto": False,             # a date promised to a client is a commitment
        "fallback": "items are dated at the project's end and owned by the lead",
        "benefit": "a plan of twenty items gets dates and owners in seconds, from how similar work actually went",
        "eval": "finished projects replayed: suggested durations against actual, owner match rate",
    },
    "ai_chase_brief": {
        "capability": None,
        "safe_for_auto": False,             # it speaks to the client
        "fallback": "the board shows the item as blocked past the chase days",
        "benefit": "a brief that is late gets a polite, specific reminder drafted the day it is due",
        "eval": "drafts rated by leads for accuracy (names the right asset and date) and tone",
    },
    "ai_flag_shortfall": {
        "capability": None,
        "safe_for_auto": True,              # it only warns
        "fallback": "reconciliation reports the shortfall when the window closes",
        "benefit": "a shortfall is seen while the window is open and the work can still be done",
        "eval": "closed projects replayed two weeks before their end: precision and recall against the actual shortfall",
    },
}


def suggest(step: str, mode: str, context: dict) -> dict:
    """The step's answer. Off: it does not run. Suggest with no provider: the fallback, in words."""
    spec = AI_STEPS[step]
    if mode == "off":
        return {"step": step, "mode": "off", "suggestion": None, "applied": False,
                "fallback": spec["fallback"], "reason": "this step is switched off in the settings"}
    return {"step": step, "mode": mode, "suggestion": None, "applied": False, "fallback": spec["fallback"],
            "reason": "no AI provider is connected for this step yet", "context": context}