This page explains the intent assistant of Cluster ERP. The engine in src/cluster_erp/ai/ (intent_engine.py, tokenizer.py, vectorizer.py, and the corpora intent_corpus_en.json and intent_corpus_tr.json) matches a typed request to an API intent. The chat route src/cluster_erp/server/routes/chat.py wraps the engine for the web app: it handles small talk, checks permissions, calls the matched endpoint, and returns replies. The screens are described on assistant and chat screens and desktop chat widget.
In short¶
- The engine is a local text classifier: tokenisation with Turkish stemming, word and character features, TF-IDF vectors, and cosine similarity against example sentences (
src/cluster_erp/ai/tokenizer.py:400-438,src/cluster_erp/ai/vectorizer.py:7-52). - An intent names a module, an HTTP method, an API path, and a description (
src/cluster_erp/ai/intent_engine.py:14-24). - The route offers only intents whose endpoint the caller holds the permission for, within the modules the client declares installed (
src/cluster_erp/server/routes/chat.py:1158-1177,src/cluster_erp/server/routes/chat.py:1253-1265). - Reads run the matched list endpoint in-process with the caller's own token; create intents return a task card; update and delete intents point to the record instead of acting (
src/cluster_erp/server/routes/chat.py:1055-1066,src/cluster_erp/server/routes/chat.py:1436-1466).
How it works¶
Request path¶
flowchart TD
A["POST /api/v1/chat/message: text, action_context, allowed_modules"] --> P["require_permission dashboard.read"]
P --> AC{"action_context set"}
AC -->|yes| HA["_handle_action follow-up"]
AC -->|no| ST{"small talk"}
ST -->|yes| SR["fixed greeting or help reply"]
ST -->|no| SP["_try_split_intents"]
SP --> RE["IntentEngine.recognize top 5"]
RE --> F["_intent_allowed: module scope and permission code"]
F -->|none left| FB["_scope_fallback"]
F --> C{"confidence below 0.60"}
C -->|yes| SUG["did you mean, top 3"]
C -->|no| K{"intent kind"}
K -->|list or get| G["_api_get in-process with caller token"]
K -->|create| T["task card from metadata"]
K -->|update or delete| N["navigate to the record"]In prose:
- The route needs
dashboard.readand loads the caller's permission codes (src/cluster_erp/server/routes/chat.py:1191-1200). - The language is Turkish when the text has Turkish letters or common Turkish command words, otherwise English (
src/cluster_erp/server/routes/chat.py:999-1040). - A follow-up from an action button goes to
_handle_action. Greetings and help requests get a fixed reply (src/cluster_erp/server/routes/chat.py:1209-1218,src/cluster_erp/server/routes/chat.py:537-647). - A text with several requests is split and each part is processed in turn (
src/cluster_erp/server/routes/chat.py:1223-1239,src/cluster_erp/server/routes/chat.py:664-717). IntentEngine.recognizereturns up to five candidates. The engine is created and loaded once per process (src/cluster_erp/server/routes/chat.py:24-35,src/cluster_erp/server/routes/chat.py:1253).- Candidates whose endpoint the caller may not reach are removed before anything is shown. With no candidate left, the reply is the same as for an unknown request (
src/cluster_erp/server/routes/chat.py:1255-1265,src/cluster_erp/server/routes/chat.py:1179-1185). - Below a confidence of 0.60 the route lists the top three as suggestions (
src/cluster_erp/server/routes/chat.py:1270-1279). - For a list or get intent the route builds filters, temporal order, and a search term from the text, calls the endpoint in-process through an ASGI transport with the caller's
Authorizationheader, and formats the rows with the list columns from metadata (src/cluster_erp/server/routes/chat.py:1282-1433,src/cluster_erp/server/routes/chat.py:1055-1066). - For a create intent it builds a task card with fields from the entity's form metadata; for update and delete intents it asks the user to open the record (
src/cluster_erp/server/routes/chat.py:1436-1466,src/cluster_erp/server/routes/chat.py:1552).
Matching¶
IntentEngine.load reads each corpus, extracts features from every example, fits one TF-IDF vectorizer per language, and stores the example vectors, a description vector, and the normalised parts of the intent id (src/cluster_erp/ai/intent_engine.py:364-418).
| Step | What happens | Source |
|---|---|---|
| Tokenise | lower-case (Turkish-aware), fold accents to ASCII, strip punctuation, drop stop words and one-letter tokens; Turkish tokens are stemmed by suffix | src/cluster_erp/ai/tokenizer.py:250-258, src/cluster_erp/ai/tokenizer.py:391-422 |
| Features | unigrams, bigrams, and character trigrams of tokens of four letters or more | src/cluster_erp/ai/tokenizer.py:424-438 |
| Vectors | term frequency times smoothed inverse document frequency; unseen terms get the highest weight | src/cluster_erp/ai/vectorizer.py:17-38 |
| Similarity | cosine similarity to the best example and to the description | src/cluster_erp/ai/vectorizer.py:40-52, src/cluster_erp/ai/intent_engine.py:521-528 |
| Id overlap | action and entity words, also mapped from Turkish and corrected by edit distance, compared with the parts of the intent id | src/cluster_erp/ai/intent_engine.py:84-105, src/cluster_erp/ai/intent_engine.py:605-765 |
| Score | a weighted sum whose weights depend on whether an entity and a strong action word were found, plus small bonuses for list and get hints | src/cluster_erp/ai/intent_engine.py:530-560 |
| Ambiguity | when the top two candidates of different modules are within 0.04 and the top is below 0.72, the top confidence is capped at 0.59 | src/cluster_erp/ai/intent_engine.py:584-601 |
The default threshold for a candidate is 0.05 and the default number of results is 5 (src/cluster_erp/ai/intent_engine.py:420-426).
Corpora¶
Each corpus is a JSON file with version, description, total_intents, and intents. An intent has id, module, method, path, description, and examples (src/cluster_erp/ai/intent_corpus_en.json, src/cluster_erp/ai/intent_corpus_tr.json). Counted with a JSON load of both files: 347 intents in each language, 1553 English examples, and 1168 Turkish examples. The intents cover 12 targets: auth, crm, currencies, design_engineering, documents, owned_organisations, parties, products_and_services, projects, quotations, sales_orders, and server.
Why it is built this way¶
- The route comment states that the assistant only knows what the caller may reach, so that out-of-scope intents cannot leak descriptions, suggestions, or forms (
src/cluster_erp/server/routes/chat.py:1255-1258). ChatRequest.allowed_modulesonly narrows what the assistant offers; permissions are always enforced from the caller's token (src/cluster_erp/server/routes/chat.py:1069-1077).- The permission code of an intent is derived from metadata in the form
{entity_key}.{action}, not from a hand list (src/cluster_erp/server/routes/chat.py:1120-1156).
Rules and boundaries¶
- The text is at most 2000 characters;
allowed_modulesat most 200 entries (src/cluster_erp/server/routes/chat.py:1069-1077). - An intent whose endpoint cannot be resolved in metadata is denied (
src/cluster_erp/server/routes/chat.py:1158-1177). - The assistant does not run update or delete actions (
src/cluster_erp/server/routes/chat.py:1454-1466). - The desktop client builds its own
IntentEnginein the chat widget (src/cluster_erp/desktop/widgets/chat/chat_widget.py:47,src/cluster_erp/desktop/widgets/chat/chat_widget.py:818).
Interaction with other parts¶
| Part | Purpose | Source |
|---|---|---|
| Metadata of all modules | navigation targets, list columns, entity labels, permission codes, task card fields | src/cluster_erp/server/routes/chat.py:875-998, src/cluster_erp/server/routes/chat.py:1044-1053 |
UserService.get_user_permission_codes |
the caller's permission codes | src/cluster_erp/server/routes/chat.py:1200 |
| The module APIs | the actual data, behind their own permission and app checks | src/cluster_erp/server/routes/chat.py:1055-1066 |
The chat route is one of the server routes outside modules; see shared server routes.
Limits and open points¶
- The corpora cover 12 targets; intents for modules such as
fleet,intranet,inventory, oranalyticswere not found in either corpus. - Replies such as greetings, help text, suggestions, and fallbacks are built as Turkish or English strings in the route (
src/cluster_erp/server/routes/chat.py:537-647,src/cluster_erp/server/routes/chat.py:1179-1185,src/cluster_erp/server/routes/chat.py:1270-1276).
Discrepancies¶
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| intent | IntentMatch |
A recognised request mapped to a module, method, and API path, with a confidence. | Niyet | src/cluster_erp/ai/intent_engine.py:14-24 |
| intent corpus | intent_corpus_en.json |
The example sentences per intent that the engine learns from at start. | Niyet derlemi | src/cluster_erp/ai/intent_engine.py:365-371 |
| task card | TaskCardOut |
A step-by-step form in the conversation, built from metadata for a create intent, that creates one record. | görev kartı | src/cluster_erp/server/routes/chat.py:1099-1104 |
Related pages¶
- Intent assistant section
- Assistant and chat screens
- Desktop chat widget
- Roles and permissions
- Metadata model
Verified against commit 92f8051 on 2026-11-13.