Se rendre au contenu


ODOO pour le RAG Observance ui crée automatiquement l’enregistrement après validation.

Parfait, on passe en v2 : le workflow n8n qui valide + écrit directement dans Odoo 🙌

Je te donne directement le JSON complet avec un node en plus :

  • HTTP Odoo Create Observation
    qui appelle /jsonrpc d’Odoo pour créer un enregistrement sur le modèle
    x_synergia_clinical_observation.

Tu auras à remplacer :

  • https://YOUR_ODOO_URL → ton URL Odoo Online
  • "DB_NAME" → le nom de ta base
  • 123456 → l’uid (ou laisser 2 si tu utilises un user générique)
  • "ODOO_API_KEY_OR_PASSWORD" → clé API ou mot de passe du user

📥 Workflow n8n v2 (avec push Odoo)

{ "name": "Synergia - Validation + Odoo Observation Clinique", "nodes": [ { "id": "Webhook_Observation_In", "name": "Webhook Observation In", "type": "n8n-nodes-base.webhook", "typeVersion": 1, "position": [ 260, 300 ], "parameters": { "httpMethod": "POST", "path": "synergia/observation/validate", "responseMode": "onReceived", "options": { "responseContentType": "application/json" } }, "webhookId": "synergia-observation-validate" }, { "id": "OpenAI_Embedding", "name": "OpenAI Embedding", "type": "n8n-nodes-base.openAi", "typeVersion": 2, "position": [ 560, 300 ], "credentials": { "openAiApi": { "id": "OpenAI_API_CREDENTIAL_ID", "name": "OpenAI API" } }, "parameters": { "resource": "embedding", "model": "text-embedding-3-small", "text": "={{$json[\"observation_text\"]}}" } }, { "id": "Function_Build_Supabase_Body", "name": "Build Supabase RPC Body", "type": "n8n-nodes-base.function", "typeVersion": 2, "position": [ 820, 300 ], "parameters": { "functionCode": "const embeddingNode = $json;\nlet embeddingVector;\nif (embeddingNode.data && embeddingNode.data[0] && embeddingNode.data[0].embedding) {\n embeddingVector = embeddingNode.data[0].embedding;\n} else if (embeddingNode.embedding) {\n embeddingVector = embeddingNode.embedding;\n} else {\n throw new Error('Embedding non trouvé dans la sortie du node OpenAI Embedding');\n}\n\nreturn [{\n query_embedding: embeddingVector,\n match_count: 10\n}];" } }, { "id": "HTTP_Supabase_RAG", "name": "Supabase match_observation", "type": "n8n-nodes-base.httpRequest", "typeVersion": 3, "position": [ 1080, 300 ], "parameters": { "method": "POST", "url": "https://YOUR_PROJECT.supabase.co/rest/v1/rpc/match_observation", "authentication": "none", "responseFormat": "json", "jsonParameters": true, "options": {}, "bodyParametersJson": "={{JSON.stringify($json)}}", "headerParametersJson": "{\n \"apikey\": \"SUPABASE_ANON_OR_SERVICE_KEY\",\n \"Authorization\": \"Bearer SUPABASE_ANON_OR_SERVICE_KEY\",\n \"Content-Type\": \"application/json\"\n}" } }, { "id": "Function_Build_LLM_Prompt", "name": "Build LLM Prompt", "type": "n8n-nodes-base.function", "typeVersion": 2, "position": [ 1340, 300 ], "parameters": { "functionCode": "const webhookItem = $items(\"Webhook Observation In\")[0].json;\nconst phrase = webhookItem.observation_text;\n\nconst matches = $json;\n\nconst exemples_proches = (Array.isArray(matches) ? matches : []).slice(0, 6).map((m) => ({\n content: m.content,\n polarity: m.polarity,\n category: m.category,\n tags: m.tags,\n distance: m.distance\n}));\n\nconst payloadLLM = {\n phrase_infirmiere: phrase,\n context: {\n exemples_proches,\n rappel_regles: [\n \"Les observations infirmières doivent être objectives, factuelles, non interprétatives.\",\n \"L'infirmier ne pose pas de diagnostic médical (CSP R.4311-3 et R.4311-5).\"\n ]\n }\n};\n\nreturn [{\n llm_payload: payloadLLM,\n phrase_infirmiere: phrase\n}];" } }, { "id": "OpenAI_Chat_Check_Rewrite", "name": "OpenAI Chat Check & Rewrite", "type": "n8n-nodes-base.openAi", "typeVersion": 2, "position": [ 1600, 300 ], "credentials": { "openAiApi": { "id": "OpenAI_API_CREDENTIAL_ID", "name": "OpenAI API" } }, "parameters": { "resource": "chat", "model": "gpt-4.1-mini", "mode": "assistant", "options": { "response_format": "json_object" }, "systemMessage": "Tu es un expert du droit de la santé en France et des pratiques infirmières.\\nTu connais le Code de la santé publique (R.4311-1 à R.4311-15) et les référentiels HAS.\\nTa mission :\\n1. Dire si une phrase d'observation clinique infirmière est CONFORME ou NON CONFORME.\\n2. Justifier en français simple.\\n3. Si NON CONFORME, proposer une réécriture FACTUELLE, sans diagnostic ni jugement, basée uniquement sur ce qui est observable, mesurable ou déclaré par le patient.\\n4. Répondre STRICTEMENT en JSON avec les champs : status, motif, proposition_factuelle, score_risque, type_non_conformite, references.", "messages": [ { "text": "={{JSON.stringify($json.llm_payload)}}", "type": "user" } ] } }, { "id": "Function_Prepare_Response_and_Odoo", "name": "Prepare HTTP Response + Odoo payload", "type": "n8n-nodes-base.function", "typeVersion": 2, "position": [ 1860, 300 ], "parameters": { "functionCode": "let raw;\nif ($json.choices && $json.choices[0] && $json.choices[0].message && $json.choices[0].message.content) {\n raw = $json.choices[0].message.content;\n} else if ($json.message && $json.message.content) {\n raw = $json.message.content;\n} else {\n throw new Error('Réponse LLM inattendue');\n}\n\nlet parsed;\ntry {\n parsed = JSON.parse(raw);\n} catch (e) {\n throw new Error('Impossible de parser la réponse JSON du LLM : ' + raw);\n}\n\nconst webhookItem = $items(\"Webhook Observation In\")[0].json;\nconst original = webhookItem.observation_text;\nconst visitId = webhookItem.visit_id || null; // à envoyer depuis le front\n\nconst status = parsed.status || \"inconnu\";\nconst suggestion = parsed.proposition_factuelle || original;\n\n// Construction du payload Odoo (modèle x_synergia_clinical_observation)\nconst odooPayload = {\n jsonrpc: \"2.0\",\n method: \"call\",\n params: {\n service: \"object\",\n method: \"execute_kw\",\n args: [\n \"DB_NAME\", // À remplacer\n 123456, // uid Odoo à remplacer\n \"ODOO_API_KEY_OR_PASSWORD\", // À remplacer\n \"x_synergia_clinical_observation\",\n \"create\",\n [\n {\n x_visit_id: visitId,\n x_observation_text: suggestion,\n x_original_text: original,\n x_validation_status: status,\n x_validation_score: parsed.score_risque ?? null,\n x_validation_motif: parsed.motif || null,\n x_validation_payload_json: JSON.stringify(parsed)\n }\n ]\n ]\n }\n};\n\nreturn [{\n validation: {\n status,\n motif: parsed.motif || null,\n suggestion,\n score_risque: parsed.score_risque ?? null,\n type_non_conformite: parsed.type_non_conformite || null,\n references: parsed.references || []\n },\n original_observation: original,\n proposed_observation: suggestion,\n llm_raw: parsed,\n odoo_payload: odooPayload\n}];" } }, { "id": "HTTP_Odoo_Create", "name": "Odoo Create Observation", "type": "n8n-nodes-base.httpRequest", "typeVersion": 3, "position": [ 2120, 300 ], "parameters": { "method": "POST", "url": "https://YOUR_ODOO_URL/jsonrpc", "authentication": "none", "responseFormat": "json", "jsonParameters": true, "options": {}, "bodyParametersJson": "={{JSON.stringify($json.odoo_payload)}}", "headerParametersJson": "{\n \"Content-Type\": \"application/json\"\n}" } }, { "id": "Function_Merge_Odoo_Result", "name": "Merge Odoo Result", "type": "n8n-nodes-base.function", "typeVersion": 2, "position": [ 2380, 300 ], "parameters": { "functionCode": "const odooRes = $json; // réponse Odoo JSON-RPC\nconst prev = $items(\"Prepare HTTP Response + Odoo payload\")[0].json;\n\nlet odooId = null;\nif (odooRes.result) {\n odooId = odooRes.result; // en général, ID créé\n}\n\nreturn [{\n validation: prev.validation,\n original_observation: prev.original_observation,\n proposed_observation: prev.proposed_observation,\n llm_raw: prev.llm_raw,\n odoo: {\n created_id: odooId,\n raw_response: odooRes\n }\n}];" } }, { "id": "Respond_to_Webhook", "name": "Respond to Webhook", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1, "position": [ 2640, 300 ], "parameters": { "webhookId": "synergia-observation-validate", "options": { "responseContentType": "application/json" }, "responseBody": "={{$json}}", "responseCode": 200 } } ], "connections": { "Webhook Observation In": { "main": [ [ { "node": "OpenAI Embedding", "type": "main", "index": 0 } ] ] }, "OpenAI Embedding": { "main": [ [ { "node": "Build Supabase RPC Body", "type": "main", "index": 0 } ] ] }, "Build Supabase RPC Body": { "main": [ [ { "node": "Supabase match_observation", "type": "main", "index": 0 } ] ] }, "Supabase match_observation": { "main": [ [ { "node": "Build LLM Prompt", "type": "main", "index": 0 } ] ] }, "Build LLM Prompt": { "main": [ [ { "node": "OpenAI Chat Check & Rewrite", "type": "main", "index": 0 } ] ] }, "OpenAI Chat Check & Rewrite": { "main": [ [ { "node": "Prepare HTTP Response + Odoo payload", "type": "main", "index": 0 } ] ] }, "Prepare HTTP Response + Odoo payload": { "main": [ [ { "node": "Odoo Create Observation", "type": "main", "index": 0 } ] ] }, "Odoo Create Observation": { "main": [ [ { "node": "Merge Odoo Result", "type": "main", "index": 0 } ] ] }, "Merge Odoo Result": { "main": [ [ { "node": "Respond to Webhook", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1" }, "versionId": "synergia-observation-clinique-rag-odoo-v2", "meta": { "templateCredsSetupCompleted": true }, "tags": [ { "name": "Synergia" }, { "name": "Observation clinique" } ] }

Concrètement côté front Synergia, tu postes sur :

POST /webhook/synergia/observation/validate Content-Type: application/json { "visit_id": 12345, "observation_text": "Le patient est probablement déshydraté." }

Et tu reçois :

{ "validation": { "status": "non_conforme", "motif": "…", "suggestion": "Lèvres sèches observées, patient dit : « Je n’ai presque pas bu aujourd’hui. »", "score_risque": 0.9, "type_non_conformite": "diagnostic_medical", "references": ["CSP R.4311-3", "CSP R.4311-5"] }, "original_observation": "Le patient est probablement déshydraté.", "proposed_observation": "Lèvres sèches observées, patient dit : « Je n’ai presque pas bu aujourd’hui. »", "odoo": { "created_id": 4567, "raw_response": { "...": "..." } } }

Si tu veux, je peux maintenant te faire :

  • soit le modèle Odoo complet + vues pour x_synergia_clinical_observation,
  • soit la spec front React pour afficher ce retour (bandeau vert/rouge + bouton “Accepter la réécriture”).
Vous avez dit :

la définition Odoo (x_synergia_clinical_observation : modèle + vues form/tree)

ChatGPT a dit :


ChatGPT peut commettre des erreurs. Il est recommandé de vérifier les informations importantes. Voir les préférences en matière de cookies.

Les grandes histoires ont une personnalité. Envisagez de raconter une belle histoire qui donne de la personnalité. Écrire une histoire avec de la personnalité pour des clients potentiels aidera à établir un lien relationnel. Cela se traduit par de petites spécificités comme le choix des mots ou des phrases. Écrivez de votre point de vue, pas de l'expérience de quelqu'un d'autre.

Les grandes histoires sont pour tout le monde, même lorsqu'elles ne sont écrites que pour une seule personne. Si vous essayez d'écrire en pensant à un public large et général, votre histoire sonnera fausse et manquera d'émotion. Personne ne sera intéressé. Ecrire pour une personne en particulier signifie que si c'est authentique pour l'un, c'est authentique pour le reste.