feat(r5.2): assistant au contrat + suggestion de codes de bilan

apps/ai : /internal/ask — seuil de pertinence, extraits sourcés ou
refus honnête portant la taille du corpus cherché (D2), rédaction via
le Generateur opt-in ; /internal/suggest — similarité sémantique entre
la description libre et les libellés ACTIFS des référentiels, un code
par champ, confiance FORTE/MOYENNE, « N bilans similaires sur ce
parc ». Sans LLM : déterministe, explicable. 23 pytest.

Contrat (74 opérations) : POST /assistant/ask → AssistantAnswer
(EXTRACTIVE/GENERATED/REFUSAL, extraits cités, corpus cherché) et
POST /assistant/suggest-bilan (codes existants seulement) ; clients
web/mobile régénérés.

API NestJS : module assistant — proxy vers siop2-ai (AI_SERVICE_URL/
AI_SERVICE_TOKEN, ADR-004 §4), permissions matrice (ask=view,
suggest=edit), traduction interne→contrat, 503 propre si service
éteint. 6 e2e sur stub HTTP (76 tests API).

Bug débusqué par la vraie chaîne : fastembed ne norme pas ses
vecteurs — la similarité des suggestions dépassait 1 (pgvector
normalisait dans son opérateur, masquant l'écart). Normalisation à
l'encodage + réindexation : bilans en tête (0.41), refus hors corpus,
scores cosinus ≤ 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-17 15:05:08 +01:00
parent 76c2ccdfb1
commit 45ae491827
17 changed files with 1215 additions and 2 deletions

View File

@@ -4,6 +4,23 @@ Trace chronologique des sessions (la plus récente en premier). Le **playbook**
---
## 2026-07-17 — Pr. Daaif (+ Claude) — R5.2 : l'assistant au contrat + suggestion de codes de bilan
**Actions**
- **`apps/ai`** : `/internal/ask` — recherche → **seuil de pertinence** → extraits sourcés, ou **refus honnête** portant la taille du corpus cherché (« 1 document, 3 bilans » — l'écran 2 des maquettes aura ses chiffres) ; la rédaction passe par le `Generateur` (opt-in — mode extractif par défaut). `/internal/suggest` — similarité sémantique entre la description libre et les libellés **actifs** des référentiels (contextualisés « anomalie constatée : … »), un seul code par champ, au-dessus du seuil, confiance FORTE/MOYENNE + « N bilans similaires sur ce parc » (comptés dans les chunks d'historique). Sans LLM : déterministe, explicable. 23 pytest.
- **Contrat** (74 opérations) : `POST /assistant/ask``AssistantAnswer` (mode EXTRACTIVE/GENERATED/REFUSAL, extraits cités document/page ou bilan daté, corpus cherché) ; `POST /assistant/suggest-bilan` → codes existants seulement. Clients web/mobile régénérés.
- **API NestJS** : module `assistant` — proxy vers `siop2-ai` (`AI_SERVICE_URL`/`AI_SERVICE_TOKEN` à l'env, ADR-004 §4), permissions par la matrice (`ask` = WORK_ORDERS.view ; `suggest` = WORK_ORDERS.edit — qui remplit des bilans), traduction du dialecte interne vers le contrat, **503 propre** si le service IA est éteint (jamais un 500). 6 tests e2e sur un **stub HTTP** (76 tests API, 14 suites).
- **Vérifié sur la vraie chaîne** (NestJS → ai → pgvector, vrai modèle) — et elle a débusqué un bug : fastembed ne renvoie pas des vecteurs normés, la similarité des suggestions dépassait 1 (pgvector normalisait dans son opérateur, ce qui masquait l'écart). **Normalisation à l'encodage** + réindexation : bilans du parc trouvés en tête (0.41), refus honnête hors corpus, suggestions cosinus ≤ 1 à confiances étagées.
**Décisions**
- Les seuils (pertinence 0.30, suggestion 0.35, confiance forte 0.55) sont des constantes de départ — à calibrer en recette sur le corpus réel du client.
**Prochaine étape** : R5.3 — les écrans validés (Assistant sidebar, corpus dans la Bibliothèque, suggestions dans les fiches OT web et clôture mobile). Restes : recette R4 sur appareil, redéploiement Dokploy de `release/r3`.
---
## 2026-07-17 — Pr. Daaif (+ Claude) — R5.1+ : clé API de génération configurable (demande du référent)
**Actions**

View File

@@ -1165,6 +1165,84 @@
}
}
},
"/assistant/ask": {
"post": {
"operationId": "askAssistant",
"summary": "Assistant R5 — sourcé ou silencieux : extraits cités ou refus honnête (D2)",
"tags": [
"assistant"
],
"security": [
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AssistantAsk"
}
}
}
},
"responses": {
"200": {
"description": "Réponse sourcée ou refus",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AssistantAnswer"
}
}
}
},
"503": {
"description": "Service IA indisponible"
}
}
}
},
"/assistant/suggest-bilan": {
"post": {
"operationId": "suggestBilan",
"summary": "Suggérer des codes de bilan depuis une description libre (D1 — lhumain valide)",
"tags": [
"assistant"
],
"security": [
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SuggestBilan"
}
}
}
},
"responses": {
"200": {
"description": "Suggestions (codes existants seulement)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BilanSuggestions"
}
}
}
},
"503": {
"description": "Service IA indisponible"
}
}
}
},
"/search": {
"get": {
"operationId": "globalSearch",
@@ -4975,6 +5053,209 @@
],
"additionalProperties": false
},
"AssistantAnswer": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": [
"EXTRACTIVE",
"GENERATED",
"REFUSAL"
]
},
"answer": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"excerpts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"sourceType": {
"type": "string",
"enum": [
"DOCUMENT",
"WORK_ORDER"
]
},
"documentId": {
"anyOf": [
{
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
{
"type": "null"
}
]
},
"workOrderId": {
"anyOf": [
{
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
{
"type": "null"
}
]
},
"title": {
"type": "string"
},
"locator": {
"type": "string"
},
"content": {
"type": "string"
},
"score": {
"type": "number"
}
},
"required": [
"sourceType",
"documentId",
"workOrderId",
"title",
"locator",
"content",
"score"
],
"additionalProperties": false
}
},
"corpus": {
"type": "object",
"properties": {
"documents": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"reports": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
}
},
"required": [
"documents",
"reports"
],
"additionalProperties": false
}
},
"required": [
"mode",
"answer",
"excerpts",
"corpus"
],
"additionalProperties": false
},
"AssistantAsk": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"question": {
"type": "string",
"minLength": 3,
"maxLength": 500
}
},
"required": [
"question"
],
"additionalProperties": false
},
"BilanSuggestions": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"suggestions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"field": {
"type": "string",
"enum": [
"DOOR_STATE",
"CABIN_POSITION",
"ANOMALY",
"EXTERNAL_CAUSE",
"ACTION_TAKEN",
"COMPONENT_CONCERNED"
]
},
"valueId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
},
"label": {
"type": "string"
},
"confidence": {
"type": "string",
"enum": [
"HIGH",
"MEDIUM"
]
},
"similarReports": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"score": {
"type": "number"
}
},
"required": [
"field",
"valueId",
"label",
"confidence",
"similarReports",
"score"
],
"additionalProperties": false
}
}
},
"required": [
"suggestions"
],
"additionalProperties": false
},
"SuggestBilan": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"description": {
"type": "string",
"minLength": 10,
"maxLength": 2000
}
},
"required": [
"description"
],
"additionalProperties": false
},
"SearchResponse": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",