* WIP AI design
- Chat Interface
- French Translation
* AI Home Screen
Added a home page for the AI assistant, with pre-recorded prompts
* Fix missing user
* Add copyright
* Fix working tools and alignment
The AI module currently has no declared rights ($this->rights stays
empty), so per-user/group attribution of "who is allowed to talk to
the LLM" is not possible. Assistant access is gated only by
isModEnabled('ai'), meaning any authenticated user on an AI-enabled
install can send organisational data to the configured LLM provider
-- which is a GDPR / EU AI Act concern (see issue #38331).
This patch introduces ONE proper Dolibarr right that admins can
attribute per user/group via the standard permission UI
(Users -> Permissions):
ai/assistant/use -> right to use the AI Assistant page
NOT granted by default; admins must
explicitly assign it to authorized users
Per the feedback from @sonikf and @eldy on PR #38312:
- Setup access intentionally stays a hard-coded $user->admin check.
The technical setup of the AI module (API keys, provider URLs) is
an admin task in line with how every other Dolibarr module is
configured -- a dedicated 'ai/setup/write' right would only
duplicate responsibility that admins already hold.
- ai/assistant/use defaults to OFF (not granted) so that, on a
fresh install or after enabling the module, no user can talk to
the LLM until the admin explicitly authorises them. This matches
the EU AI Act expectation that AI usage is an opt-in per-user
decision owned by the GDPR DPO / AI module DPA.
Access checks added:
- htdocs/ai/assistant/index.php
+ if (!$user->hasRight('ai','assistant','use')) accessforbidden();
Added after the existing isModEnabled('ai') / AI_MCP_ENABLED gate.
- htdocs/ai/assistant/parse_intent.php
+ if (!$user->hasRight('ai','assistant','use')) accessforbidden();
Defence in depth: the AJAX endpoint that actually talks to the LLM
cannot be reached by a user who lacks the right, even via direct
call bypassing the Assistant page.
- htdocs/ai/admin/setup.php
Unchanged behaviour: $user->admin check stays (just a comment
clarifying the reason).
The right is declared between the existing MODULEBUILDER PERMISSIONS
placeholders in modAi.class.php so the standard right-installation
flow applies: after applying this change, administrators must
disable/re-enable the AI module once to register the new right in
llx_rights_def (existing Dolibarr mechanism, no migration script
needed).
Tested:
- Fresh non-admin user (no extra right granted) -> Assistant page
shows accessforbidden; parse_intent.php also blocks direct call.
- Same user with 'ai/assistant/use' granted by an admin ->
Assistant accessible, LLM round-trip works.
- Admin user -> Assistant accessible transparently (admins hold
every declared right by definition); Setup accessible via the
unchanged $user->admin check.
- Module disabled -> both still blocked by the existing
isModEnabled gate.
Refs: #38331 (EU AI Act / GDPR for the AI module).
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
* Add new language entries for AI tool management
* Update file reference in assistant.php documentation
* Implement isSystem method in conversation class
* Add Tool Access Control tab
* Initial commit AI general css
* Initial commit AI general js file
* Implement isSystem method in mcptool class
Add isSystem method to determine tool visibility.
* Enhance MCPServer to enforce tool allow-list
* Enhance McpHandler with tool context handling
Added support for tool context in McpHandler class.
* Remove empty line at the start of mcp.class.php
* Refactor tool schema handling for LLM integration
* Modify McpHandler initialization in execute_tool.php
Updated McpHandler initialization to include configuration and context for assistant.
* New AI tool access control
* Fix pre-commit
* fix pre-commit
* fix pre-commit
* fix pre-commit
* fix pre-commit
* fix phan php-stan
Two bugs combined to make UniversalLLMAdapter::__construct() crash with:
TypeError: Argument #4 ($model) must be of type string, array given
at parse_intent.php line 301 when the user-configured model wasn't
properly read.
(1) Wrong shape assumption -- getListOfAIServices() declares model
defaults as a NESTED array of the form:
$servicesList[$key]['textgeneration'] = ['default' => 'model-name']
The previous code read $servicesList[$key]['textgeneration'] (without
['default']) and got the inner array back. That array was then passed
as the $defModel fallback to a (string)-typed constructor argument.
(2) Wrong constant -- the admin UI ("Prompt and custom AI models" tab
in setup.php) stores the per-function model under
AI_API_<SERVICE>_MODEL_TEXT, matching the convention already used by
Ai::generateContent() for the same data. The previous code read
AI_API_<SERVICE>_MODEL (no _TEXT suffix), which is never written by
that form. So the user-configured model was silently ignored and we
fell back to the (array) default from bug #1.
Together these two bugs reliably reproduced the TypeError on any
default install of the AI module where the user had set a custom
text model via the admin UI.
This patch:
- Walks $servicesList[$key]['textgeneration'] correctly (extracting
['default'] when it's an array, accepting strings for backward
compat).
- Tries AI_API_<SERVICE>_MODEL_TEXT first (the constant the admin
UI writes), and falls back to the legacy AI_API_<SERVICE>_MODEL
for compatibility with anyone who might have set it manually.
- Defensively coerces the final value to a string and falls back to
the default if for any reason it's still not a string.
Tested with google (gemini-2.5-flash) and chatgpt configured via
the admin UI -> UniversalLLMAdapter receives a string and the
AI request goes through.
cleanToolSchemaForLLM() truncates each tool description to 3 words
whenever the schema has more than 20 tools. This heuristic likely
made sense for GPT-3.5 (4K context window) but is harmful with the
current crop of LLMs:
- Gemini 2.5 Flash: 1M token context
- GPT-4o: 128K
- Claude Sonnet: 1M
Three words per description is far too aggressive and actively
breaks tool selection. For example:
- create_other_document -> "Create documents other"
- create_sales_order -> "Create a CUSTOMER"
- add_line_item -> "Add a single"
The LLM then has no way to know that create_other_document is what
should be used for supplier_invoice, or that add_line_item modifies
an EXISTING draft. We've observed Gemini concluding that
"supplier_invoice creation is not available" because the truncated
description didn't mention it.
Two complementary changes:
1. Raise the threshold from 20 to 100 -- this effectively disables
compression for normal installs (~30 tools) while leaving a
safety net for very large custom installs that register dozens
of additional tools via the addMcpTools hook.
2. Even when compression IS active, never touch the tool-level
description. Parameter-level compression (stripping defaults,
descriptions of optional fields, collapsing complex objects)
still applies and continues to save tokens. The right answer
for truly oversized schemas is to filter the toolset upstream
(which filterToolsProfessional() already does), not to mutilate
each tool's description.
Tested with: a 30-tool schema sent in full now allows the LLM to
correctly select create_other_document with object_type
'supplier_invoice', 'proposal' and 'supplier_proposal' --
previously it returned a "feature not available" message.