Name: [ADD] viin_ai_approval(+account,+sale,+ops_brain): P-OPS-2 approval governance + AI advisory on human approvals

State: Failed finished in 194m

PR State: merged

PR Author: David Tran

PR Author Email:

PR: #47

Committer: David Tran

Committer Email: davidtran.hp@gmail.com

Commit: cfe6ff11c56dbf3e9cebc03a232d5bab51c2e534

Description:

                                            [ADD] viin_ai_approval,viin_ai_ops_brain,test_viin_ai: P-OPS-2 approval governance + no-key AI test framework

Phase 3.8 P-OPS-2 on top of viin_ai_ops, wired to the existing OmniApproval engine
(viin_approval, tvtmaaddons17), workflow-independent (behaviour + tests identical with
or without viin_workflow_automation), plus a cross-cutting no-key AI/LLM test framework.
Status docs (roadmap + AGENTS) reconciled. SSOT: docs/ai/operating-model.md, ADR-015/016.

== P-OPS-2 approval governance ==

Concern A - "Approval OF AI" (viin_ai_approval + viin_ai_ops_brain): a side-effecting
viin.ai.action.proposal (medium/high risk, or a runs_as_sudo tool) is routed to a human
viin.approval.request; on the human decision the proposal state syncs (approved/rejected/
draft) and the approved action is handed to execution. State sync is DATA-DRIVEN via the
approval type's code_* hooks (code_validate_post / code_refuse_post / code_cancel_post)
calling the proposal's public callbacks - NOT action_* method overrides (the idiomatic
OmniApproval way, which is why post-approval execution is environment-independent). Captures
trace_id / prompt_snapshot / iteration_index into approval evidence. viin_ai_ops_brain writes
an attributed "Approval Rationale" Brain page (T5: is_ai_authored + agent + trace_id +
confidence; html_escape on untrusted text).

Concern B - "AI IN approval" (viin_ai_approval infra + per-app pilots): posts an AI risk-
advisory card (risk score + summary + recommendation + signals) to a human approval request's
chatter, for the approver - ADVISORY-ONLY (no auto-approve). Data-driven trigger:
code_confirm_post enqueues (none -> pending) and wakes a cron; the cron runs the LLM
out-of-band, using cr.savepoint() per record + the ir.cron row-lock for non-concurrency
(no manual cr.commit). Graceful 'skipped' when no BYOK key is configured; silent 'error' on
failure. Per-app pilots (ADR-015 convention: auto_install bridge, data-XML agent/topic seed,
non-clobbering post_init_hook, PII-minimised signal builder): viin_ai_approval_account
(CUSTOMER_INVOICE / VENDOR_PAYMENTS / CUSTOMER_REFUNDS) + viin_ai_approval_sale (QUOTE).

== No-key AI/LLM test framework ==

L1 - shared unit-test helpers in viin_ai_base/tests/ (not on the runtime path): canonical
FakeResponse, result builders (make_llm_call_result / make_completion_result /
make_stream_chunks / make_fake_embed_fn), and AIStubMixin (_stub_llm / _stub_stream /
_stub_provider patching the registry-class agent seam) + seed_pgvector_rows. 10 legacy test
files converge onto these; 5 disconnected stub patterns (3 _FakeResponse copies, 2 identical
fake_embed, 3 raw-SQL pgvector blocks) are deduplicated. The provider-inheritance test is
hardened to assert the resolved MRO instead of the _inherit attribute.

L2 - test_viin_ai: a separate TEST-ONLY module (Odoo test_* convention) the customer NEVER
installs (installable, no auto_install, in no product dependency closure; dev/demo/CI only ->
customer prod carries zero fabrication code). Two roles: a gated mock LLM provider (overrides
one seam, make_request, for api_protocol='mock'; every protocol-keyed helper falls through to
the openai_compat shape so cost-cap / PII redaction / normalize / usage.log / trace all run
real; a fail-closed gate in models/gate.py - @api.constrains on the vendor + a refusal in
make_request - so no fabrication is reachable when the gate is closed, preserving the BYOK
invariant) AND the home for test cases that cannot live in a production module (cross-module /
full-stack / mock-dependent).

== Docs ==

ADR-015 (Concern B per-app advisory pattern), ADR-016 (shared AI test framework + gated
test_viin_ai placement + L3 rejection), docs/ai/testing.md (no-key testing SSOT: decision
tree, helper API, coverage fence, @tagged rule, Runbot lint/test commands). Pointers from
AGENTS.md §6, docs/ai/README.md, docs/decisions/README.md. roadmap.md + AGENTS.md §3
reconciled: P-OPS-2 shipped (flagging that the original spec assumed action_* overrides while
the shipped path is data-driven code_* hooks); gate M3 still needs P-OPS-3 (viin_ai_workflow)
+ P-OPS-4 (ops cockpit) + heartbeat routines.

== Verification ==

No-key: Community 376/376 + EE 304/304 (with viin_workflow_automation present), 0 failed /
0 error. Code-quality (test_lint + test_pylint, exact Runbot config): flake8 + pylint_odoo
clean, no lint suppressions; manifest/RST clean. Live demo: a real sale-quotation approval
confirm -> cron -> agent.run -> mock provider yields a genuine advisory card (no network,
no key, no canned message_post).
                                            

Branch: 17.0

Instance ID: 0

Age:

Up-time: