Branch
[REM] docs, viin_ai_account, viin_ai_skill, viin_brain_account_reports: drop superseded TT200 accounting refs TT200 (Thong tu 200/2014/TT-BTC) is superseded by TT99 (Thong tu 99/2025). TT133 (Thong tu 133/2016, for SMEs) and TT99 both remain in effect and are kept. - Replace every TT200 reference with its successor TT99: docs (roadmap, architecture diagram, data-models), README, the account connector manifest + seed comment, the accounting skill pack, and the brain account-reports bridge. The architecture box-drawing diagram keeps its column alignment. - Keep TT133 and TT99 (both current): the accounting skill pack teaches TT99 (enterprises) / TT133 (SMEs). - Attribute receivable-provision rates to the current doubtful-debt provisioning regulation generically, not to the accounting regime. The "200h" effort figure in adr-008 is hours, not a circular, and is left untouched (ADRs are append-only).
[IMP] viin_ai_agent, viin_ai_crm, viin_ai_helpdesk: strip HTML from AI tool payloads Connector tools fed raw Html-field values (lead/ticket description, activity note, message body) straight into the payload the LLM reads - HTML tags are token noise and can confuse the model. - viin_ai_agent: add _tool_plain_text on ir.actions.server, a sibling of _tool_result that runs a value through html2plaintext ('' for falsy), callable from a server-action code body (safe_eval). Mirrors the existing viin_ai_chat plain-text helper; no viin_brain coupling. - viin_ai_crm: strip crm.lead.description and mail.activity.note. - viin_ai_helpdesk: strip viin.helpdesk.ticket.description and mail.message.body (strip first, then truncate so the cap applies to plain text). - Surveyed the other connectors (account/sale/sale_crm/stock) and the AI/Brain tool surface: no other tool returns a raw Html field (Brain already returns the content_plain T1 representation). - Tests: red-green behaviour tests assert the payload value carries no '<' and preserves the readable text, driven through a real ir.actions.server.run() as the tool's low-privilege group. - docs: note the _tool_plain_text sibling next to _tool_result in ai/architecture.md so future connectors strip Html-field values.
[IMP] viin_ai_agent, viin_ai_crm, viin_ai_helpdesk: strip HTML from AI tool payloads Connector tools fed raw Html-field values (lead/ticket description, activity note, message body) straight into the payload the LLM reads - HTML tags are token noise and can confuse the model. - viin_ai_agent: add _tool_plain_text on ir.actions.server, a sibling of _tool_result that runs a value through html2plaintext ('' for falsy), callable from a server-action code body (safe_eval). Mirrors the existing viin_ai_chat plain-text helper; no viin_brain coupling. - viin_ai_crm: strip crm.lead.description and mail.activity.note. - viin_ai_helpdesk: strip viin.helpdesk.ticket.description and mail.message.body (strip first, then truncate so the cap applies to plain text). - Surveyed the other connectors (account/sale/sale_crm/stock) and the AI/Brain tool surface: no other tool returns a raw Html field (Brain already returns the content_plain T1 representation). - Tests: red-green behaviour tests assert the payload value carries no '<' and preserves the readable text, driven through a real ir.actions.server.run() as the tool's low-privilege group.
[ADD] viin_ai_helpdesk, viin_ai_crm: Phase 4 AI connectors (helpdesk seed + CRM advisory tools) Phase 4 (Track C) per-app AI connectors, unlocked by M3: - viin_ai_helpdesk (NEW): AI connector for viin_helpdesk. Seeds 1 agent (Helpdesk Assistant) + 1 topic (Helpdesk Triage) + 3 READ-only tools on viin.helpdesk.ticket: triage_ticket, suggest_canned_response, generate_faq_from_thread. Data-XML, zero Python (Layer-3 convention). - viin_ai_crm (IMP): adds 3 advisory READ tools (qualify_lead, next_action_suggest, email_draft_for_lead) wired into the Lead Qualification topic. - All tools: requires_confirmation=False (read-only), runs_as_sudo=False, group_ids ACL-gated; model_id write-gate satisfied (group has perm_write on the target model). run()-driven boundary tests prove the safe_eval + write-gate path. - docs: AGENTS.md + docs/roadmap.md reflect Phase 4 progress; reconcile a pre-existing roadmap drift (helpdesk dependency named the Odoo-EE 'helpdesk' module instead of Viindoo 'viin_helpdesk').
[IMP] viin_ai_base, viin_ai_agent: bidirectional data-driven message adapter (close #64 #62 #63) Symmetric follow-up to the outgoing adapter (#59): make the INCOMING response normalizer data-driven via viin.ai.message.adapter, add a per-vendor empty-content knob, and route the streaming path through the adapter with a contract guard. - #64: viin.ai.message.adapter gains a `direction` discriminator (outgoing|incoming) + incoming response-map knobs; viin.ai.provider._normalize_completion_response is rewired to dispatch data-driven incoming primitives (output byte-stable for the 3 seeded protocols). An admin can hotfix a vendor response-key rename with no release. - #62: per-vendor `empty_content_repr` knob (empty_string|null|omit), default empty_string (byte-identical to prior behaviour) for strict gateways (Azure OpenAI). - #63: viin_ai_agent._do_llm_stream redacts-then-adapts before transport + a contract guard test that fails if a future streaming multi-turn path bypasses the adapter (also patches a latent streaming PII-redaction gap). - Hardening (post code-review): incoming rows must declare all leaf-key paths (finish/usage/text/tool); incoming rows for an unsupported protocol are rejected at write time (no silent mis-parse); clearer parse-error diagnostics; tightened guard tests (per-location PII, value-switch data-driven proof, assertRaises). - ADR-018 (bidirectional data-driven message adapter); docs/roadmap/module-map reconcile (32 modules, PR #57/#59/#60 history, ADR ledger, adr-015/016 status sync). Tests: Odoo native, no-API-key; 302 non-tour tests green (normalize/wire byte-stable non-regression + incoming data-driven proof + #62 matrix + #63 guard + multiturn loop + new validation guards). flake8 clean on the Runbot lint gate. Pre-existing HttpCase browser tours need a live http server and fail identically on base 17.0 under --no-http. Claude-Session: https://claude.ai/code/session_01MpghN1mdjfEzHd1yuSqdut
[IMP] viin_ai_base, viin_ai_agent: bidirectional data-driven message adapter (close #64 #62 #63) Symmetric follow-up to the outgoing adapter (#59): make the INCOMING response normalizer data-driven via viin.ai.message.adapter, add a per-vendor empty-content knob, and route the streaming path through the adapter with a contract guard. - #64: viin.ai.message.adapter gains a `direction` discriminator (outgoing|incoming) + incoming response-map knobs; viin.ai.provider._normalize_completion_response is rewired to dispatch data-driven incoming primitives (output byte-stable for the 3 seeded protocols). An admin can hotfix a vendor response-key rename with no release. - #62: per-vendor `empty_content_repr` knob (empty_string|null|omit), default empty_string (byte-identical to prior behaviour) for strict gateways (Azure OpenAI). - #63: viin_ai_agent._do_llm_stream redacts-then-adapts before transport + a contract guard test that fails if a future streaming multi-turn path bypasses the adapter (also patches a latent streaming PII-redaction gap). - Hardening (post code-review): incoming rows must declare all leaf-key paths (finish/usage/text/tool); incoming rows for an unsupported protocol are rejected at write time (no silent mis-parse); clearer parse-error diagnostics; tightened guard tests (per-location PII, value-switch data-driven proof, assertRaises). - ADR-018 (bidirectional data-driven message adapter); docs/roadmap/module-map reconcile (32 modules, PR #57/#59/#60 history, ADR ledger, adr-015/016 status sync). Tests: Odoo native, no-API-key; 302 non-tour tests green (normalize/wire byte-stable non-regression + incoming data-driven proof + #62 matrix + #63 guard + multiturn loop + new validation guards). Pre-existing HttpCase browser tours need a live http server and fail identically on base 17.0 under --no-http (not part of this change). Claude-Session: https://claude.ai/code/session_01MpghN1mdjfEzHd1yuSqdut
[IMP] viin_ai_base, viin_ai_agent: bidirectional data-driven message adapter (close #64 #62 #63) Symmetric follow-up to the outgoing adapter (#59): make the INCOMING response normalizer data-driven via viin.ai.message.adapter, add a per-vendor empty-content knob, and route the streaming path through the adapter with a contract guard. - #64: viin.ai.message.adapter gains a `direction` discriminator (outgoing|incoming) + incoming response-map knobs; viin.ai.provider._normalize_completion_response is rewired to dispatch data-driven incoming primitives (output byte-stable for the 3 seeded protocols). An admin can hotfix a vendor response-key rename with no release. - #62: per-vendor `empty_content_repr` knob (empty_string|null|omit), default empty_string (byte-identical to prior behaviour) for strict gateways (Azure OpenAI). - #63: viin_ai_agent._do_llm_stream redacts-then-adapts before transport + a contract guard test that fails if a future streaming multi-turn path bypasses the adapter (also patches a latent streaming PII-redaction gap). - ADR-018 (bidirectional data-driven message adapter); docs/roadmap/module-map reconcile (32 modules, PR #57/#59/#60 history, ADR ledger, adr-015/016 status sync). Tests: Odoo native, no-API-key; 74-class net green (normalize/wire byte-stable non-regression + incoming data-driven proof + #62 matrix + #63 guard + multiturn loop). Claude-Session: https://claude.ai/code/session_01MpghN1mdjfEzHd1yuSqdut
[FIX] viin_ai_brain: revert record_historian as-of boundary to inclusive-at-ts + lock-in test verify2 (review iter-2) flagged a silent, undocumented boundary flip in record_historian._as_of_record Step-4: the tracking-message revert filter had been changed from ('date','>',ts) to '>=', making a change stamped exactly at ts get reverted (exclusive-at-ts) - the opposite of the whole-codebase convention (viin_brain._find_active_at inclusive-at-ts; memory recall <= as_of; the Step-2 create_date boundary in the same method). "State as of ts" must reflect a change effective at ts, so revert only strictly-after-ts changes. - record_historian.py: ('date','>=',ts) -> ('date','>',ts) + convention comment. - test_record_historian.py: add boundary-exact lock-in test H7 (RED under '>=' AssertionError partner_before reverted; GREEN under '>'). Claude-Session: https://claude.ai/code/session_01EVjERtZ9dnk5fhZHns8vLv
[FIX] viin_ai: PR #60 review-2 - W8150 lint blocker + record_state_as_of ACL guard + temporal test hardening - viin_ai_brain: fix Runbot test_pylint W8150 (module-level relative import in test_record_historian.py); add caller-env ACL gate (check_access_rights/rule) before record_state_as_of reconstruction, return {} on AccessError; RED-GREEN test as low_priv; perm_write=1 lock-in test + rationale. - viin_ai_memory: neutralize-proof UUID tokens in as-of recall tests; defensive str as_of normalize via fields.Datetime.to_datetime + cross-module contract comment. - viin_brain: boundary-exact active-at fixtures (valid_to==ts / valid_from==ts) guarding the inclusive operator; expression.AND for temporal domain combine. - viin_ai_memory_brain: explicit required=False/default=False on vault_id. - docs: propagate _search_active_at -> _find_active_at; correct stale 2-condition active-at domain to the shipped 3-condition predicate (ADR-006 append-only Wave-2 note). Claude-Session: https://claude.ai/code/session_01EVjERtZ9dnk5fhZHns8vLv
[FIX] viin_ai: PR #60 review-2 - W8150 lint blocker + record_state_as_of ACL guard + temporal test hardening - viin_ai_brain: fix Runbot test_pylint W8150 (module-level relative import in test_record_historian.py); add caller-env ACL gate (check_access_rights/rule) before record_state_as_of reconstruction, return {} on AccessError; RED-GREEN test as low_priv; perm_write=1 lock-in test + rationale. - viin_ai_memory: neutralize-proof UUID tokens in as-of recall tests; defensive str as_of normalize via fields.Datetime.to_datetime + cross-module contract comment. - viin_brain: boundary-exact active-at fixtures (valid_to==ts / valid_from==ts) guarding the inclusive operator; expression.AND for temporal domain combine. - viin_ai_memory_brain: explicit required=False/default=False on vault_id. - docs: propagate _search_active_at -> _find_active_at; correct stale 2-condition active-at domain to the shipped 3-condition predicate (ADR-006 append-only Wave-2 note). Claude-Session: https://claude.ai/code/session_01EVjERtZ9dnk5fhZHns8vLv
[FIX] viin_ai_base: provider-native outgoing message adapter (#59) The agentic tool loop shipped turn-2 canonical history (assistant tool_calls, tool role tool_results) to the wire verbatim - no outgoing adapter existed, only an incoming normalizer. A real tool-using turn returned HTTP 400 on anthropic_native/openai_compat and silently dropped the tool result on google_native. The same payload builder dropped the system prompt on all 3 protocols, and the PII redactor skipped tool-turn free-text (tool_calls arguments, tool_results result/error). Add a data-driven, UI-editable viin.ai.message.adapter (protocol-keyed with an optional vendor override, noupdate seed) that reshapes the canonical history into each provider's native wire format via vetted code primitives - zero eval/template on the BYOK egress path. A write-time coherence constraint rejects incoherent (protocol, shape/strategy) admin edits. System-prompt placement is handled per protocol (anthropic top-level system, google systemInstruction, openai role=system) with the system kwarg as the single source of truth, which also fixes viin_ai_search Path B on anthropic/google. PII redaction is extended to tool-turn free-text and runs before the adapt step (redact-then-adapt). Add honest tests that stub only the transport seam and assert the built outgoing payload per protocol (the seam #59 slipped through), plus data-driven, coherence-gate, and PII-redaction tests. Claude-Session: https://claude.ai/code/session_0128GTfwboHEZqJZ5xgfkxoL
[FIX] viin_ai: address PR #60 review (temporal_diff ACL, BFS N+1, as-of test gaps, conventions) H-A: add ACL post-filter in tool_brain_temporal_diff - batch-search readable page ids after edge set-diff, drop edges where either endpoint is unreadable (same D6 fix class applied to graph_traverse in Wave-1). H-B: batch the fallback BFS _acl_filter call - collect all candidate to_page ids per BFS step BEFORE the ACL check (single Page.search per step, not one per link, mirroring the primary delegate path). H-C: add test_observation_recall_as_of_filters - protects the third temporal leg (created_at <= as_of on observations); RED-then-GREEN verified. M1: rename _search_active_at -> _find_active_at in viin.brain.link and all call sites in brain_tools.py and tests (avoids ORM _search_<field> prefix collision, no active_at field exists). M2: fix assertIsNotNone(link_a.superseded_at) -> assertTrue (Odoo Datetime null is False, not None; assertIsNotNone(False) passes silently). M3: replace (6, 0, [...]) legacy tuples with Command.set([...]) in viin_ai_memory_brain/tests/test_vault_id_isolation.py (AGENTS.md #7). M4: move imports (datetime, fields.Datetime/Date) from inside function body to module top-level in brain_tools.py (python.md imports rule). Claude-Session: https://claude.ai/code/session_01JTYAzu8ndxtrQGJeTNa3pf
[ADD] viin_ai_memory_brain: AI memory <-> Brain vault bridge (KG temporal Wave-2) New auto-install bridge (depends viin_ai_memory + viin_brain). Adds an optional vault_id Many2one('viin.brain.vault') to viin.ai.memory so a memory can be scoped to a Brain vault, plus a GLOBAL ir.rule that restricts memory visibility to vaults the user can access (member or access-group), while vault-less memories remain governed by the existing per-owner rule. The vault rule is GLOBAL (AND-combines = restricts), not non-global: a non-global rule with an OR(vault_id=False, ...) disjunct would OR-union with the owner rule and leak every vault-less memory across owners. Tests cover cross-vault, cross-company, owner-only vault-less, and the cross-owner vault-less regression. Part of ADR-006 KG temporal Wave-2. Claude-Session: https://claude.ai/code/session_01JTYAzu8ndxtrQGJeTNa3pf
[REF] viin_ai_stock: return AI tool results via the standard envelope helper The 3 stock tools (get_stock_level, get_low_stock_products, get_product_moves) now use env['ir.actions.server']._tool_result(payload); tests assert the ir.actions.client envelope (tag viin_ai_tool_result) under params['data']. Claude-Session: https://claude.ai/code/session_0128GTfwboHEZqJZ5xgfkxoL
[IMP] viin_brain: reconcile linkable docstring with ADR-012 carrier reality The viin_brain_linkable docstring still claimed the per-app bridges "explicitly inherit this mixin" (their concrete models were deleted) and that "WI-7 will set installable:False" (contradicted by the ADR-012 revive). Correct both claims; the _brain_form_sidebar flag and all logic are untouched. Claude-Session: https://claude.ai/code/session_01JTYAzu8ndxtrQGJeTNa3pf
[REF] viin_ai_search: tidy stale arguments wording in web_query tool comment/docstring Replace residual 'tool_web_query(arguments)' prose (which implied 'arguments' is a safe_eval dispatch-scope variable) with 'tool_web_query(...)'. The bare 'arguments' is the method parameter name, not a server-action context variable. Comment/docstring only - no behavior change. Claude-Session: https://claude.ai/code/session_0128GTfwboHEZqJZ5xgfkxoL