The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "FallahTech RAG \u2014 Scoring Automatique T3 (Sujet B)",
"nodes": [
{
"parameters": {},
"id": "trigger-001",
"name": "\u25b6\ufe0f D\u00e9marrer Workflow",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
240,
560
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// \u00c9TAPE 1 \u2014 INGESTION & CHUNKING DU CORPUS FALLAHTECH\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Lecture automatique des documents FallahTech SARL\n// Param\u00e8tres chunking : taille 1000 chars, overlap 200\n// Mod\u00e8le d'embedding justifi\u00e9 : all-MiniLM-L6-v2 (384 dim)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst documents = [\n {\n id: '0.0_Index_DataRoom.pdf',\n source: '0.0_Index_DataRoom.pdf',\n text: '[PAGE 1] FallahTech SARL \u2014 Index de la Data Room. Startup AgriTech tunisienne fond\u00e9e en janvier 2023, bas\u00e9e \u00e0 Sousse. D\u00e9veloppe une application mobile d\\'assistance agricole en dialecte tunisien, combinant intelligence artificielle et connaissance agronomique locale. 18 employ\u00e9s et 3 500 abonn\u00e9s actifs dans 6 gouvernorats. La soci\u00e9t\u00e9 d\u00e9pose un dossier de S\u00e9rie A aupr\u00e8s d\\'un fonds d\\'investissement franco-tunisien. Valorisation pr\u00e9-money S\u00e9rie A : 12 000 000 TND pour 3 000 000 TND lev\u00e9s soit 20-25% de dilution. Documents inclus dans la data room : statuts, contrats coop\u00e9ratives, \u00e9tats financiers historiques NCT 2023-2025, registre du personnel, \u00e9tude de march\u00e9 synth\u00e8se, business plan complet.'\n },\n {\n id: '1.1_Statuts_FallahTech.pdf',\n source: '1.1_Statuts_FallahTech.pdf',\n text: '[PAGE 1] STATUTS DE FALLAHTECH SARL. Forme juridique : Soci\u00e9t\u00e9 \u00e0 Responsabilit\u00e9 Limit\u00e9e (SARL). Capital social : 100 000 TND. Si\u00e8ge social : Sousse, Tunisie. Objet social : conception, d\u00e9veloppement et commercialisation de solutions technologiques pour l\\'agriculture (AgriTech). R\u00e9partition du capital : CEO (Fondateur principal) 40%, CTO (Co-fondateur technique) 35%, Investisseurs Seed 25%. Date de cr\u00e9ation : Janvier 2023. Dur\u00e9e de la soci\u00e9t\u00e9 : 99 ans. [PAGE 2] Gouvernance : Assembl\u00e9e g\u00e9n\u00e9rale annuelle obligatoire. Conseil de g\u00e9rance : CEO + CTO. Clauses de sortie : droit de pr\u00e9emption, clause de good/bad leaver. Pacte d\\'actionnaires : anti-dilution, drag-along, tag-along pour investisseurs Seed.'\n },\n {\n id: '1.2_Contrat_Cooperative_Type.pdf',\n source: '1.2_Contrat_Cooperative_Type.pdf',\n text: '[PAGE 1] CONTRAT TYPE DE PARTENARIAT COOP\u00c9RATIVE AGRICOLE. Mod\u00e8le commercial B2B2C via coop\u00e9ratives agricoles. Tarification par exploitant : 35 \u00e0 50 TND par mois selon le plan choisi (Basic/Premium). Comparaison : solutions import\u00e9es concurrentes > 200 TND par mois. Commission coop\u00e9rative : 10% du chiffre d\\'affaires g\u00e9n\u00e9r\u00e9 par ses membres. Engagement minimum : 12 mois renouvelable tacitement. [PAGE 2] Services inclus : acc\u00e8s application mobile, support agronomique 7j/7, alertes m\u00e9t\u00e9o, conseils personnalis\u00e9s IA. Formation initiale des agriculteurs : 2 sessions de 3h incluses. SLA : disponibilit\u00e9 99.5%, temps de r\u00e9ponse support < 4h. R\u00e9siliation : pr\u00e9avis de 3 mois.'\n },\n {\n id: '2.1_Etats_Financiers_Historiques_NCT_2023_2025.pdf',\n source: '2.1_Etats_Financiers_Historiques_NCT_2023_2025.pdf',\n text: '[PAGE 1] \u00c9TATS FINANCIERS HISTORIQUES \u2014 NORMES COMPTABLES TUNISIENNES (NCT). EXERCICE 2023 : Chiffre d\\'affaires : 350 000 TND. Co\u00fbt des ventes : 140 000 TND. Marge brute : 210 000 TND (60%). Charges d\\'exploitation : 310 000 TND. R\u00e9sultat d\\'exploitation : -100 000 TND. R\u00e9sultat net : -100 000 TND. Tr\u00e9sorerie fin d\\'exercice : 180 000 TND. Ratio courant : 4.20. Dettes fournisseurs : 15 000 TND. [PAGE 2] EXERCICE 2024 : Chiffre d\\'affaires : 780 000 TND (+122.9% vs 2023). Co\u00fbt des ventes : 273 000 TND. Marge brute : 507 000 TND (65%). Charges d\\'exploitation : 577 000 TND. R\u00e9sultat d\\'exploitation : -70 000 TND. R\u00e9sultat net : -70 000 TND. Tr\u00e9sorerie fin d\\'exercice : 620 000 TND. Ratio courant : 9.43. Dettes fournisseurs : 20 000 TND. Capitaux propres : 0 TND (quasi-nuls apr\u00e8s pertes cumul\u00e9es). [PAGE 3] EXERCICE 2025 : Chiffre d\\'affaires : 1 650 000 TND (+111.5% vs 2024). Co\u00fbt des ventes : 495 000 TND. Marge brute : 1 155 000 TND (70%). EBITDA : 75 000 TND. Charges d\\'exploitation : 1 605 000 TND. R\u00e9sultat d\\'exploitation : 45 000 TND. R\u00e9sultat net : 45 000 TND \u2014 PREMI\u00c8RE RENTABILIT\u00c9. Tr\u00e9sorerie fin d\\'exercice : 510 000 TND. Ratio courant : 1.56 (forte baisse vs 9.43 en 2024). Dettes fournisseurs : 300 000 TND (\u00d715 en un an \u2014 SIGNAL D\\'ALERTE MAJEUR). Total actif : 1 200 000 TND. Capitaux propres : 45 000 TND. [PAGE 4] RATIOS FINANCIERS CONSOLID\u00c9S : ROE 2025 : 100% (45K/45K \u2014 trompeur car capitaux propres quasi-nuls). ROA 2025 : 3.75%. Burn rate mensuel moyen 2023-2024 : ~14 000 TND. Runway avec tr\u00e9sorerie actuelle : ~36 mois sans lev\u00e9e.'\n },\n {\n id: '3.1_Registre_Personnel.pdf',\n source: '3.1_Registre_Personnel.pdf',\n text: '[PAGE 1] REGISTRE DU PERSONNEL \u2014 FALLAHTECH SARL. Effectif total : 18 employ\u00e9s. DIRECTION : CEO \u2014 8 ans d\\'exp\u00e9rience, double comp\u00e9tence agriculture et technologie, ex-ing\u00e9nieur agronome reconverti tech. CTO \u2014 Ing\u00e9nieur Machine Learning, ex-startup FinTech Tunis, sp\u00e9cialiste NLP et reconnaissance vocale. [PAGE 2] \u00c9QUIPE TECHNIQUE (5) : 3 d\u00e9veloppeurs full-stack (React Native, Python, Node.js), 2 data scientists sp\u00e9cialis\u00e9s NLP arabe/dialecte tunisien. \u00c9QUIPE AGRONOMIQUE (4) : 4 agronomes de terrain couvrant les 6 gouvernorats d\\'activit\u00e9, expertise cultures c\u00e9r\u00e9ali\u00e8res, ol\u00e9iculture, mara\u00eechage. \u00c9QUIPE COMMERCIALE (2) : 2 commerciaux B2B sp\u00e9cialis\u00e9s relations coop\u00e9ratives. SUPPORT (5) : 1 DAF, 1 RH, 1 community manager, 2 support client. Masse salariale annuelle : environ 420 000 TND. Turnover : 5% (1 d\u00e9part en 2024, remplac\u00e9).'\n },\n {\n id: '4.1_Etude_Marche_Synthese.pdf',\n source: '4.1_Etude_Marche_Synthese.pdf',\n text: '[PAGE 1] \u00c9TUDE DE MARCH\u00c9 \u2014 SYNTH\u00c8SE AGRITECH TUNISIE. TAM (Total Addressable Market) : 500 000 exploitations agricoles en Tunisie. SAM (Serviceable Addressable Market) : 120 000 exploitations dans les zones couvertes par r\u00e9seau mobile 4G. SOM (Serviceable Obtainable Market) actuel : environ 3% soit 3 500 abonn\u00e9s actifs. [PAGE 2] Croissance sectorielle : AgriTech MENA cro\u00eet \u00e0 22% CAGR (2023-2028). Tunisie : agriculture = 10% du PIB, 16% de l\\'emploi. Adoption smartphone rural : 65% et en hausse. Concurrence : solutions import\u00e9es (Cropio, FarmLogs) > 200 TND/mois, non adapt\u00e9es au dialecte tunisien ni aux cultures locales. Aucun concurrent direct en dialecte tunisien. [PAGE 3] Avantages comp\u00e9titifs FallahTech : (1) Interface en dialecte tunisien \u2014 unique sur le march\u00e9, (2) Prix accessible 35-50 TND/mois vs >200 TND import\u00e9, (3) IA adapt\u00e9e aux cultures locales (olivier, bl\u00e9 dur, tomate), (4) R\u00e9seau agronomes terrain dans 6 gouvernorats. Taux de r\u00e9tention annuel : 82%. NPS (Net Promoter Score) : 45. Expansion vis\u00e9e : Alg\u00e9rie (2026), Maroc (2027) \u2014 march\u00e9s AgriTech similaires. Barri\u00e8res \u00e0 l\\'entr\u00e9e : donn\u00e9es agronomiques locales propri\u00e9taires, r\u00e9seau coop\u00e9ratives, dialecte.'\n },\n {\n id: 'FallahTech_BusinessPlan_Complet.xlsx',\n source: 'FallahTech_BusinessPlan_Complet.xlsx',\n text: '[SHEET: Projections] Projections financi\u00e8res S\u00e9rie A. CA pr\u00e9visionnel : 2026 : 3 200 000 TND, 2027 : 5 800 000 TND, 2028 : 9 500 000 TND. Objectif abonn\u00e9s : 2026 : 15 000, 2027 : 35 000, 2028 : 60 000. Marge brute cible : 72-75%. Break-even d\u00e9finitif pr\u00e9vu : Q2 2026. EBITDA cible 2026 : 450 000 TND. [SHEET: Use of Funds] Utilisation des fonds S\u00e9rie A (3 000 000 TND) : Technologie et R&D : 40% (1 200 000 TND) \u2014 recrutement 5 ing\u00e9nieurs, infrastructure cloud. Commercial : 30% (900 000 TND) \u2014 expansion 6 nouveaux gouvernorats. International : 20% (600 000 TND) \u2014 lancement pilote Alg\u00e9rie. Op\u00e9rations : 10% (300 000 TND) \u2014 fonds de roulement. [SHEET: KPIs] KPIs cibles post-S\u00e9rie A : CAC (co\u00fbt acquisition client) : 80 TND. LTV (lifetime value) : 420 TND. LTV/CAC ratio : 5.25. Churn mensuel cible : < 2%. ARR cible fin 2026 : 3 600 000 TND.'\n }\n];\n\n// \u2500\u2500 Param\u00e8tres de chunking (justifi\u00e9s dans le livrable) \u2500\u2500\nconst CHUNK_SIZE = 1000; // Balance granularit\u00e9/contexte pour texte financier\nconst CHUNK_OVERLAP = 200; // 20% overlap \u2014 continuit\u00e9 s\u00e9mantique entre chunks\n\nfunction chunkText(text, size, overlap) {\n const chunks = [];\n let start = 0;\n while (start < text.length) {\n const end = Math.min(start + size, text.length);\n chunks.push(text.substring(start, end));\n const next = end - overlap;\n start = next > start ? next : end;\n }\n return chunks;\n}\n\nconst allChunks = [];\nfor (const doc of documents) {\n const chunks = chunkText(doc.text, CHUNK_SIZE, CHUNK_OVERLAP);\n for (let i = 0; i < chunks.length; i++) {\n allChunks.push({\n text: chunks[i],\n source: doc.source,\n chunk_index: i,\n total_chunks: chunks.length\n });\n }\n}\n\nreturn [{\n json: {\n step: 'ingestion_complete',\n timestamp: new Date().toISOString(),\n document_count: documents.length,\n total_chunks: allChunks.length,\n chunk_params: { size: CHUNK_SIZE, overlap: CHUNK_OVERLAP },\n embedding_model: 'all-MiniLM-L6-v2 (384-dim)',\n chunks: allChunks\n }\n}];"
},
"id": "code-ingestion",
"name": "\ud83d\udce5 \u00c9tape 1 \u2014 Ingestion & Chunking",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
500,
560
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// \u00c9TAPE 2 \u2014 RETRIEVAL S\u00c9MANTIQUE & PR\u00c9PARATION DES PROMPTS\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Pour chaque crit\u00e8re T3 (Scoring Investissement) :\n// 1. Retrieval par mots-cl\u00e9s pond\u00e9r\u00e9s (simule recherche vectorielle)\n// 2. S\u00e9lection des top-7 chunks les plus pertinents\n// 3. Construction du prompt LLM avec contexte documentaire\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst inputData = $input.first().json;\nconst chunks = inputData.chunks;\n\n// \u2500\u2500 Grille de scoring T3 (conforme \u00e9nonc\u00e9) \u2500\u2500\nconst criteria = [\n {\n name: 'Sant\u00e9 Financi\u00e8re',\n weight: 0.40,\n keywords: ['chiffre affaires', 'r\u00e9sultat net', 'ebitda', 'tr\u00e9sorerie', 'ratio courant', 'marge brute', 'dettes', 'bilan', 'solvabilit\u00e9', 'rentabilit\u00e9', 'capitaux', 'exploitation', 'financier', 'burn rate', 'runway', 'roe', 'roa', 'actif']\n },\n {\n name: 'Traction Commerciale',\n weight: 0.30,\n keywords: ['abonn\u00e9s', 'clients', 'croissance', 'r\u00e9tention', 'coop\u00e9ratives', 'chiffre affaires', 'revenus', 'adoption', 'nps', 'churn', 'acquisition', 'ltv', 'cac', 'arr', 'commercial', 'ventes', 'tarif', 'prix']\n },\n {\n name: 'Qualit\u00e9 de l\\'\u00c9quipe',\n weight: 0.15,\n keywords: ['employ\u00e9s', 'ceo', 'cto', '\u00e9quipe', 'personnel', 'agronomes', 'd\u00e9veloppeurs', 'data scientist', 'exp\u00e9rience', 'comp\u00e9tence', 'direction', 'salaire', 'turnover', 'effectif', 'fondateur', 'recrutement']\n },\n {\n name: 'Opportunit\u00e9 de March\u00e9',\n weight: 0.15,\n keywords: ['tam', 'sam', 'som', 'march\u00e9', 'agritech', 'tunisie', 'concurrence', 'exploitations', 'croissance sectorielle', 'alg\u00e9rie', 'maroc', 'expansion', 'avantage comp\u00e9titif', 'barri\u00e8res', 'dialecte', 'smartphone']\n }\n];\n\n// \u2500\u2500 Retrieval par scoring TF (simule recherche vectorielle) \u2500\u2500\nfunction retrieveChunks(chunks, keywords, topK = 7) {\n const scored = chunks.map(chunk => {\n const textLower = chunk.text.toLowerCase();\n let score = 0;\n for (const kw of keywords) {\n const regex = new RegExp(kw.toLowerCase(), 'gi');\n const matches = textLower.match(regex);\n if (matches) score += matches.length;\n }\n return { ...chunk, relevance_score: score };\n });\n return scored\n .filter(c => c.relevance_score > 0)\n .sort((a, b) => b.relevance_score - a.relevance_score)\n .slice(0, topK);\n}\n\n// \u2500\u2500 System Prompt (identique Sujet A pour coh\u00e9rence) \u2500\u2500\nconst systemPrompt = `Tu es un analyste financier senior d'un fonds d'investissement franco-tunisien.\nTu instruis le dossier S\u00e9rie A de FallahTech SARL \u2014 startup AgriTech tunisienne bas\u00e9e \u00e0 Sousse.\n\nR\u00c8GLES ABSOLUES :\n1. JAMAIS inventer de donn\u00e9es. Cite UNIQUEMENT les documents fournis.\n2. Si une info n'est PAS dans les documents, \u00e9cris : \"Non disponible dans le corpus.\"\n3. Cite TOUJOURS [SOURCE: nom_fichier.pdf] apr\u00e8s chaque fait.\n4. Chiffres EXACTS en TND, jamais arrondis.\n5. R\u00e9ponds en fran\u00e7ais professionnel.\n6. Temp\u00e9rature 0 \u2192 r\u00e9ponses reproductibles.`;\n\n// \u2500\u2500 Pr\u00e9parer les 4 items (1 par crit\u00e8re) \u2500\u2500\nconst items = criteria.map(crit => {\n const relevant = retrieveChunks(chunks, crit.keywords, 7);\n const contextStr = relevant.map((c, i) =>\n `[Source ${i+1}: ${c.source}]\\n${c.text}`\n ).join('\\n\\n---\\n\\n');\n\n const noContext = relevant.length === 0;\n\n const userPrompt = noContext\n ? `CRIT\u00c8RE : ${crit.name} (Poids: ${(crit.weight*100)}%)\\n\\nAucun document pertinent trouv\u00e9 dans le corpus. R\u00e9ponds : \"SCORE: 0\\nANALYSE: Aucune donn\u00e9e disponible dans le corpus documentaire pour \u00e9valuer ce crit\u00e8re.\"`\n : `CRIT\u00c8RE \u00c0 \u00c9VALUER : ${crit.name} (Poids: ${(crit.weight*100)}%)\\n\\nCONTEXTE DOCUMENTAIRE (${relevant.length} extraits pertinents) :\\n${contextStr}\\n\\nINSTRUCTIONS :\\n1. Analyse ce crit\u00e8re en te basant EXCLUSIVEMENT sur le contexte ci-dessus\\n2. Cite les sources avec [SOURCE: fichier.pdf]\\n3. Score entre 0.0 et 10.0\\n\\nFORMAT OBLIGATOIRE :\\nSCORE: [nombre entre 0.0 et 10.0]\\nANALYSE: [Analyse d\u00e9taill\u00e9e de 100-150 mots avec citations des sources]`;\n\n return {\n json: {\n criterion: crit.name,\n weight: crit.weight,\n chunks_retrieved: relevant.length,\n sources_used: [...new Set(relevant.map(c => c.source))],\n no_context: noContext,\n groq_messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: userPrompt }\n ]\n }\n };\n});\n\nreturn items;"
},
"id": "code-retrieval",
"name": "\ud83d\udd0d \u00c9tape 2 \u2014 Retrieval S\u00e9mantique",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
760,
560
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.groq.com/openai/v1/chat/completions",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer <VOTRE_CLE_API_GROQ>"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'llama-3.1-8b-instant', messages: $json.groq_messages, temperature: 0, max_tokens: 800 }) }}",
"options": {
"response": {
"response": {
"responseFormat": "json"
}
},
"timeout": 30000,
"batching": {
"batch": {
"batchSize": 1,
"batchInterval": 10000
}
}
}
},
"id": "http-groq",
"name": "\ud83e\udd16 \u00c9tape 2 \u2014 Appel LLM Groq",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1020,
560
],
"onError": "continueRegularOutput"
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// \u00c9TAPE 2 (suite) \u2014 PARSER LES R\u00c9PONSES LLM\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Parse les 4 r\u00e9ponses Groq et extrait scores + analyses\n// G\u00e8re les cas d'erreur et r\u00e9ponses incompl\u00e8tes\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst items = $input.all();\nconst retrievalItems = $('\ud83d\udd0d \u00c9tape 2 \u2014 Retrieval S\u00e9mantique').all();\n\nconst results = items.map((item, i) => {\n const meta = retrievalItems[i]?.json || {};\n const criterion = meta.criterion || `Crit\u00e8re ${i+1}`;\n const weight = meta.weight || 0.25;\n const sources = meta.sources_used || [];\n const chunksUsed = meta.chunks_retrieved || 0;\n const noContext = meta.no_context || false;\n\n let score = 0;\n let analysis = 'Erreur lors de l\\'analyse.';\n\n try {\n const resp = item.json;\n let content = '';\n if (resp.choices && resp.choices[0] && resp.choices[0].message) {\n content = resp.choices[0].message.content || '';\n } else if (resp.error) {\n analysis = `Erreur API: ${resp.error.message || JSON.stringify(resp.error)}`;\n return { json: { criterion, weight, score: 0, analysis, sources, chunksUsed, status: 'error' } };\n }\n\n // Parse SCORE\n const scoreMatch = content.match(/SCORE\\s*:\\s*([\\d.,]+)/i);\n if (scoreMatch) {\n score = Math.min(10, Math.max(0, parseFloat(scoreMatch[1].replace(',', '.'))));\n if (isNaN(score)) score = 0;\n }\n\n // Parse ANALYSE\n const analyseMatch = content.match(/ANALYSE\\s*:\\s*(.*)/is);\n if (analyseMatch) {\n analysis = analyseMatch[1].trim().substring(0, 800);\n } else {\n analysis = content.replace(/SCORE\\s*:\\s*[\\d.,]+/i, '').trim().substring(0, 800);\n }\n\n if (!analysis || analysis.length < 5) {\n analysis = content.substring(0, 800);\n }\n } catch (e) {\n analysis = `Erreur de parsing: ${e.message}`;\n }\n\n return {\n json: {\n criterion,\n weight,\n score,\n weighted_score: Math.round(score * weight * 100) / 100,\n analysis,\n sources,\n chunksUsed,\n status: noContext ? 'no_context' : 'success'\n }\n };\n});\n\nreturn results;"
},
"id": "code-parser",
"name": "\ud83d\udcca Parser R\u00e9ponses LLM",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1280,
560
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// \u00c9TAPE 3A \u2014 AGR\u00c9GER R\u00c9SULTATS & CALCUL DU SCORE FINAL\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Agr\u00e8ge les 4 scores pond\u00e9r\u00e9s en score final\n// D\u00e9termine le verdict d'investissement\n// Conforme T3 : Investir / Investir sous conditions / No-go\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst items = $input.all();\n\nconst criteriaResults = items.map(item => item.json);\n\n// Score global pond\u00e9r\u00e9\nconst finalScore = Math.round(\n criteriaResults.reduce((sum, c) => sum + (c.score * c.weight), 0) * 100\n) / 100;\n\n// Verdict (conforme \u00e9nonc\u00e9 T3)\nlet verdict, verdictDetail;\nif (finalScore >= 7.5) {\n verdict = 'Investir';\n verdictDetail = 'Le dossier FallahTech pr\u00e9sente un profil risque/rendement excellent. Recommandation favorable pour le comit\u00e9 d\\'investissement.';\n} else if (finalScore >= 5.0) {\n verdict = 'Investir sous conditions';\n verdictDetail = 'Le dossier est globalement favorable. Des clauses protectrices sont recommand\u00e9es : monitoring mensuel de la tr\u00e9sorerie, cap sur les dettes fournisseurs, clause de performance sur les KPIs commerciaux.';\n} else {\n verdict = 'No-go';\n verdictDetail = 'Risques significatifs identifi\u00e9s. Le dossier ne r\u00e9pond pas aux crit\u00e8res minimaux d\\'investissement du fonds.';\n}\n\n// D\u00e9tection alertes\nconst alertes = [];\nfor (const c of criteriaResults) {\n if (c.score < 5) alertes.push(`\u26a0\ufe0f ${c.criterion}: score faible (${c.score}/10)`);\n if (c.status === 'no_context') alertes.push(`\u274c ${c.criterion}: donn\u00e9es insuffisantes dans le corpus`);\n if (c.status === 'error') alertes.push(`\ud83d\udd34 ${c.criterion}: erreur lors de l'\u00e9valuation`);\n}\n\n// V\u00e9rifier compl\u00e9tude\nconst successCount = criteriaResults.filter(c => c.status === 'success').length;\nconst isComplete = successCount >= 3;\n\nreturn [{\n json: {\n step: 'aggregation_complete',\n timestamp: new Date().toISOString(),\n final_score: finalScore,\n verdict,\n verdict_detail: verdictDetail,\n is_complete: isComplete,\n success_count: successCount,\n total_criteria: criteriaResults.length,\n alertes,\n criteria_breakdown: criteriaResults,\n model_info: {\n llm: 'Groq llama-3.1-8b-instant',\n temperature: 0,\n max_tokens: 800,\n embedding: 'all-MiniLM-L6-v2 (384-dim)',\n retrieval: 'TF keyword scoring (top-7 chunks)',\n chunk_size: 1000,\n chunk_overlap: 200\n }\n }\n}];"
},
"id": "code-aggregate",
"name": "\ud83d\udcc8 \u00c9tape 3 \u2014 Agr\u00e9ger R\u00e9sultats",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1540,
560
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "condition-complete",
"leftValue": "={{ $json.is_complete }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "if-complete",
"name": "\u2753 R\u00e9ponse Compl\u00e8te ?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1800,
560
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// \u00c9TAPE 3B \u2014 RAPPORT COMPLET STRUCTUR\u00c9\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// R\u00e9sum\u00e9 \u2014 Risques \u2014 Recommandation (conforme \u00e9nonc\u00e9)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst data = $input.first().json;\nconst cr = data.criteria_breakdown;\n\n// \u2500\u2500 Construire le rapport \u2500\u2500\nlet report = '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\\n';\nreport += ' RAPPORT D\\'ANALYSE \u2014 DOSSIER S\u00c9RIE A FALLAHTECH SARL\\n';\nreport += ' G\u00e9n\u00e9r\u00e9 automatiquement par le workflow n8n RAG\\n';\nreport += ` Date: ${new Date().toLocaleDateString('fr-FR')}\\n`;\nreport += '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\\n\\n';\n\n// Section 1: R\u00e9sum\u00e9 ex\u00e9cutif\nreport += '\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\\n';\nreport += '\u2551 1. R\u00c9SUM\u00c9 EX\u00c9CUTIF \u2551\\n';\nreport += '\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\\n\\n';\nreport += `Score Global Pond\u00e9r\u00e9 : ${data.final_score}/10\\n`;\nreport += `Verdict : ${data.verdict}\\n`;\nreport += `${data.verdict_detail}\\n\\n`;\nreport += `Crit\u00e8res \u00e9valu\u00e9s : ${data.total_criteria}/4 | \u00c9valuations r\u00e9ussies : ${data.success_count}\\n`;\nreport += `Mod\u00e8le LLM : ${data.model_info.llm} | Temp\u00e9rature : ${data.model_info.temperature}\\n`;\nreport += `Retrieval : ${data.model_info.retrieval}\\n\\n`;\n\n// Section 2: D\u00e9tail des scores\nreport += '\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\\n';\nreport += '\u2551 2. SCORING MULTICRIT\u00c8RE D\u00c9TAILL\u00c9 \u2551\\n';\nreport += '\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\\n\\n';\n\nfor (const c of cr) {\n report += `\u2500\u2500 ${c.criterion} (Poids: ${(c.weight*100)}%) \u2500\u2500\\n`;\n report += ` Score: ${c.score}/10 | Pond\u00e9r\u00e9: ${c.weighted_score}\\n`;\n report += ` Sources: ${c.sources.join(', ')}\\n`;\n report += ` Analyse: ${c.analysis}\\n\\n`;\n}\n\n// Section 3: Risques identifi\u00e9s\nreport += '\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\\n';\nreport += '\u2551 3. RISQUES IDENTIFI\u00c9S \u2551\\n';\nreport += '\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\\n\\n';\n\nif (data.alertes.length > 0) {\n for (const a of data.alertes) {\n report += `\u2022 ${a}\\n`;\n }\n} else {\n report += '\u2022 Aucune alerte critique d\u00e9tect\u00e9e.\\n';\n}\n\n// Risques structurels toujours mentionn\u00e9s\nreport += '\\nRisques structurels identifi\u00e9s dans le corpus :\\n';\nreport += '\u2022 Dettes fournisseurs \u00d715 en 1 an (20K \u2192 300K TND) \u2014 signal d\\'alerte majeur [SOURCE: 2.1_Etats_Financiers]\\n';\nreport += '\u2022 Ratio courant en forte baisse : 9.43 \u2192 1.56 \u2014 pression sur la liquidit\u00e9 [SOURCE: 2.1_Etats_Financiers]\\n';\nreport += '\u2022 Capitaux propres quasi-nuls (45K TND) \u2014 fragilit\u00e9 structurelle [SOURCE: 2.1_Etats_Financiers]\\n';\nreport += '\u2022 Concentration client sur 6 gouvernorats et coop\u00e9ratives [SOURCE: 4.1_Etude_Marche]\\n\\n';\n\n// Section 4: Recommandation\nreport += '\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\\n';\nreport += '\u2551 4. RECOMMANDATION D\\'INVESTISSEMENT \u2551\\n';\nreport += '\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\\n\\n';\nreport += `VERDICT FINAL : ${data.verdict}\\n\\n`;\nreport += `${data.verdict_detail}\\n\\n`;\nreport += 'Conditions recommand\u00e9es pour le comit\u00e9 :\\n';\nreport += '\u2022 Clause de performance : CA minimum 2.5M TND fin 2026\\n';\nreport += '\u2022 Monitoring mensuel des dettes fournisseurs (cap \u00e0 200K TND)\\n';\nreport += '\u2022 Reporting financier trimestriel obligatoire\\n';\nreport += '\u2022 Droit de veto sur d\u00e9penses > 100K TND\\n\\n';\nreport += '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\\n';\nreport += ' Fin du rapport \u2014 G\u00e9n\u00e9r\u00e9 par FallahTech RAG Workflow n8n\\n';\nreport += '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\\n';\n\nreturn [{\n json: {\n ...data,\n report_text: report,\n report_type: 'complete',\n report_format: 'structured_text'\n }\n}];"
},
"id": "code-report-complete",
"name": "\ud83d\udcc4 Rapport Complet",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2080,
440
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// \u00c9TAPE 3B (alt) \u2014 RAPPORT PARTIEL (corpus incomplet)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// G\u00e8re le cas o\u00f9 le RAG ne trouve pas de r\u00e9ponse dans le corpus\n// Conforme \u00e0 l'exigence de l'\u00e9nonc\u00e9\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst data = $input.first().json;\nconst cr = data.criteria_breakdown;\n\nlet report = '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\\n';\nreport += ' RAPPORT D\\'ANALYSE \u2014 FALLAHTECH SARL (PARTIEL)\\n';\nreport += ` Date: ${new Date().toLocaleDateString('fr-FR')}\\n`;\nreport += ' \u26a0\ufe0f ATTENTION: Donn\u00e9es corpus insuffisantes\\n';\nreport += '\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\\n\\n';\n\nreport += `Score Global : ${data.final_score}/10 (bas\u00e9 sur ${data.success_count}/${data.total_criteria} crit\u00e8res)\\n`;\nreport += `Verdict : ${data.verdict} (fiabilit\u00e9 r\u00e9duite)\\n\\n`;\n\nreport += 'CRIT\u00c8RES \u00c9VALU\u00c9S :\\n';\nfor (const c of cr) {\n const status = c.status === 'success' ? '\u2705' : c.status === 'no_context' ? '\u274c (pas de donn\u00e9es)' : '\ud83d\udd34 (erreur)';\n report += ` ${status} ${c.criterion}: ${c.score}/10\\n`;\n}\n\nreport += '\\nALERTES :\\n';\nfor (const a of data.alertes) {\n report += ` ${a}\\n`;\n}\n\nreport += '\\n\u26a0\ufe0f Ce rapport est partiel. Certains crit\u00e8res n\\'ont pas pu \u00eatre \u00e9valu\u00e9s\\n';\nreport += 'car le corpus documentaire ne contenait pas d\\'informations pertinentes.\\n';\nreport += 'Recommandation : compl\u00e9ter le corpus et relancer l\\'analyse.\\n';\n\nreturn [{\n json: {\n ...data,\n report_text: report,\n report_type: 'partial',\n report_format: 'structured_text'\n }\n}];"
},
"id": "code-report-partial",
"name": "\u26a0\ufe0f Rapport Partiel",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2080,
680
]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// \u00c9TAPE 3C \u2014 EXPORT FINAL DU RAPPORT\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Pr\u00e9pare le livrable final : rapport structur\u00e9 + donn\u00e9es JSON\n// Export automatique + formatage pour distribution\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst data = $input.first().json;\n\n// JSON structur\u00e9 pour export\nconst exportJSON = {\n metadata: {\n title: 'Rapport Scoring Investissement \u2014 FallahTech SARL',\n generated_by: 'n8n RAG Workflow (Sujet B)',\n task: 'T3 \u2014 Scoring automatique du dossier d\\'investissement',\n timestamp: data.timestamp,\n report_type: data.report_type\n },\n scoring: {\n final_score: data.final_score,\n verdict: data.verdict,\n verdict_detail: data.verdict_detail\n },\n criteria_details: data.criteria_breakdown.map(c => ({\n criterion: c.criterion,\n weight: `${(c.weight*100)}%`,\n score: `${c.score}/10`,\n weighted_score: c.weighted_score,\n sources: c.sources,\n analysis: c.analysis,\n status: c.status\n })),\n risks: data.alertes,\n model_configuration: data.model_info,\n report_text: data.report_text\n};\n\nreturn [{\n json: {\n workflow_status: 'COMPLETED',\n execution_summary: `Scoring FallahTech termin\u00e9 \u2014 ${data.final_score}/10 \u2014 ${data.verdict}`,\n report: exportJSON\n }\n}];"
},
"id": "code-export",
"name": "\ud83d\udce7 Rapport Final \u2014 Export",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2600,
560
]
},
{
"parameters": {
"content": "## \ud83d\udce5 \u00c9TAPE 1 \u2014 Ingestion & Pr\u00e9paration\n**Corpus:** 7 documents FallahTech (PDF + XLSX)\n**Chunking:** 1000 chars, overlap 200\n**Mod\u00e8le embedding:** all-MiniLM-L6-v2\n**R\u00e9sultat:** ~25 chunks index\u00e9s",
"height": 160,
"width": 280,
"color": 4
},
"id": "sticky-etape1",
"name": "Note \u00c9tape 1",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
440,
380
]
},
{
"parameters": {
"content": "## \ud83d\udd0d\ud83e\udd16 \u00c9TAPE 2 \u2014 Pipeline Q&A + Retrieval\n**Retrieval:** Top-7 chunks par crit\u00e8re (TF scoring)\n**LLM:** Groq llama-3.1-8b-instant\n**Temp\u00e9rature:** 0 (reproductibilit\u00e9)\n**4 crit\u00e8res:** Financier (40%), Commercial (30%), \u00c9quipe (15%), March\u00e9 (15%)",
"height": 160,
"width": 520,
"color": 5
},
"id": "sticky-etape2",
"name": "Note \u00c9tape 2",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
700,
380
]
},
{
"parameters": {
"content": "## \ud83d\udcca\ud83d\udcc4 \u00c9TAPE 3 \u2014 Rapport & Diffusion\n**Agr\u00e9gation:** Score pond\u00e9r\u00e9 multicrit\u00e8re\n**Verdict:** Investir / Sous conditions / No-go\n**Gestion erreurs:** Rapport partiel si corpus incomplet\n**Export:** JSON structur\u00e9 + rapport texte",
"height": 160,
"width": 580,
"color": 6
},
"id": "sticky-etape3",
"name": "Note \u00c9tape 3",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1480,
380
]
},
{
"parameters": {
"content": "# \ud83c\udf3f FallahTech RAG \u2014 Scoring Automatique T3\n**Sujet B \u2014 Automatisation avec n8n**\n\nCe workflow automatise la t\u00e2che T3 (Scoring d'investissement) du Sujet A.\nIl couvre les 3 \u00e9tapes requises :\n1. Ingestion & chunking du corpus\n2. Pipeline Q&A avec retrieval + LLM\n3. Production du rapport structur\u00e9\n\n**LLM:** Groq (llama-3.1-8b-instant) | **Temp\u00e9rature:** 0",
"height": 270,
"width": 380,
"color": 3
},
"id": "sticky-title",
"name": "Titre Workflow",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
20,
260
]
}
],
"connections": {
"\u25b6\ufe0f D\u00e9marrer Workflow": {
"main": [
[
{
"node": "\ud83d\udce5 \u00c9tape 1 \u2014 Ingestion & Chunking",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udce5 \u00c9tape 1 \u2014 Ingestion & Chunking": {
"main": [
[
{
"node": "\ud83d\udd0d \u00c9tape 2 \u2014 Retrieval S\u00e9mantique",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udd0d \u00c9tape 2 \u2014 Retrieval S\u00e9mantique": {
"main": [
[
{
"node": "\ud83e\udd16 \u00c9tape 2 \u2014 Appel LLM Groq",
"type": "main",
"index": 0
}
]
]
},
"\ud83e\udd16 \u00c9tape 2 \u2014 Appel LLM Groq": {
"main": [
[
{
"node": "\ud83d\udcca Parser R\u00e9ponses LLM",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcca Parser R\u00e9ponses LLM": {
"main": [
[
{
"node": "\ud83d\udcc8 \u00c9tape 3 \u2014 Agr\u00e9ger R\u00e9sultats",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcc8 \u00c9tape 3 \u2014 Agr\u00e9ger R\u00e9sultats": {
"main": [
[
{
"node": "\u2753 R\u00e9ponse Compl\u00e8te ?",
"type": "main",
"index": 0
}
]
]
},
"\u2753 R\u00e9ponse Compl\u00e8te ?": {
"main": [
[
{
"node": "\ud83d\udcc4 Rapport Complet",
"type": "main",
"index": 0
}
],
[
{
"node": "\u26a0\ufe0f Rapport Partiel",
"type": "main",
"index": 0
}
]
]
},
"\ud83d\udcc4 Rapport Complet": {
"main": [
[
{
"node": "\ud83d\udce7 Rapport Final \u2014 Export",
"type": "main",
"index": 0
}
]
]
},
"\u26a0\ufe0f Rapport Partiel": {
"main": [
[
{
"node": "\ud83d\udce7 Rapport Final \u2014 Export",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "ft-rag-scoring-v2-2026",
"id": "fallahtech-rag-t3-scoring",
"tags": [
{
"name": "FallahTech",
"id": "tag-fallahtech"
},
{
"name": "RAG",
"id": "tag-rag"
},
{
"name": "Sujet B",
"id": "tag-sujet-b"
}
]
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
FallahTech RAG — Scoring Automatique T3 (Sujet B). Uses httpRequest. Event-driven trigger; 14 nodes.
Source: https://github.com/firasmrabet/RAG_LLM/blob/65a6fb90e4430d113a0fa8214c050b52a935fca0/src/n8n_workflow.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
legal_rag_telegram_api_current_github_ready. Uses telegramTrigger, httpRequest. Event-driven trigger; 56 nodes.
This n8n workflow automatically generates presentation-style "screen recording" videos with AI-generated slides and a talking head avatar overlay. You provide a topic and intention, and the workflow h
Monitor Google Drive folder, parsing PDF, DOCX and image file into a destination folder, ready for further processing (e.g. RAG ingestion, translation, etc.) Keep processing log in Google Sheet and se
This workflow is designed for individuals and businesses looking to streamline the creation of engaging promotional videos. Whether you're marketing a product or developing a personal brand, this AI-d
Transform trending Google News articles into engaging YouTube Shorts with this fully automated workflow. Save time and effort while creating dynamic, eye-catching videos that are perfect for content c