From ad3faa194b75295988857fc123ddcd24df412d57 Mon Sep 17 00:00:00 2001 From: alma Date: Fri, 9 Jan 2026 12:50:54 +0100 Subject: [PATCH] Mission Refactor Big --- NeahMissionGeneratePlan.json | 139 ++++++++++++++++++ .../[missionId]/generate-plan/route.ts | 43 ++++-- 2 files changed, 166 insertions(+), 16 deletions(-) create mode 100644 NeahMissionGeneratePlan.json diff --git a/NeahMissionGeneratePlan.json b/NeahMissionGeneratePlan.json new file mode 100644 index 00000000..1099ac12 --- /dev/null +++ b/NeahMissionGeneratePlan.json @@ -0,0 +1,139 @@ +{ + "name": "NeahMissionGeneratePlan", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "GeneratePlan", + "responseMode": "lastNode", + "responseData": "allEntries", + "options": {} + }, + "name": "Webhook GeneratePlan", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1, + "position": [ + -1040, + -32 + ], + "id": "28206383-afc0-472a-81f2-c99dc1e14f24", + "webhookId": "633b32e3-07c3-4e82-8e27-9ea4d6ec28e9" + }, + { + "parameters": { + "jsCode": "// Build Project Action Plan Prompt (Senior Project Manager)\n\n// 1. Read input safely\nconst query = $input.item.json.query || \"\";\nconst mission = $input.item.json.mission;\nconst model = $input.item.json.model || \"qwen3:8b\";\n\n// 2. Handle case: no mission provided\nif (!mission) {\n return {\n json: {\n response_prompt: `No mission details were provided.\nPolitely ask the user to supply the mission context required to build a project action plan.`,\n num_predict: 300,\n model: model,\n query: query\n }\n };\n}\n\n// 3. Normalize mission fields (safe defaults)\nconst {\n name = \"Unnamed Mission\",\n oddScope = [],\n niveau = \"B\",\n intention = \"\",\n missionType = \"\",\n donneurDOrdre = \"\",\n projection = \"\",\n services = [],\n profils = []\n} = mission;\n\n// 4. Construct the Senior Project Manager Prompt\nconst prompt = `\nYou are a Senior Project Manager with extensive experience leading large-scale, complex and cross-functional projects for organizations, NGOs and startups.\n\nMISSION CONTEXT:\n- Mission name: ${name}\n- Mission scope (UN SDGs): ${oddScope.length ? oddScope.join(\", \") : \"Not specified\"}\n- Mission complexity level: ${niveau}\n- Mission intention: ${intention}\n- Mission type: ${missionType}\n- Ordering organization: ${donneurDOrdre}\n- Time projection: ${projection}\n- Services involved: ${services.length ? services.join(\", \") : \"None specified\"}\n- Required profiles: ${profils.length ? profils.join(\", \") : \"Not specified\"}\n\nTASK:\nProduce a clear, structured, and actionable ACTION PLAN as a senior project manager would do at the start of a major project.\n\nCRITICAL INSTRUCTIONS:\n- Respond ONLY in English.\n- Write as a senior project manager, not as a consultant or academic.\n- Focus on execution, structure, governance, and delivery.\n- Do NOT restate the mission description.\n- Do NOT use motivational or generic language.\n- Assume a complex, long-term mission with multiple stakeholders.\n- Be concise, concrete, and pragmatic.\n\nSTRUCTURE YOUR RESPONSE USING THE FOLLOWING SECTIONS:\n\n1. Mission Framing & Strategic Intent \nClarify the real objective of the mission, key constraints, and strategic priorities.\n\n2. Success Criteria & KPIs \nDefine how success will be measured (operational, impact, and sustainability metrics).\n\n3. Execution Roadmap \nBreak the mission into clear phases (e.g. initiation, build, deployment, scaling) with concrete outcomes per phase.\n\n4. Team Structure & Governance \nExplain how the different profiles will collaborate, decision-making model, and coordination mechanisms.\n\n5. Key Risks & Mitigation Plan \nIdentify major delivery, technical, organizational, and stakeholder risks and how to mitigate them.\n\n6. Delivery & Impact Measurement \nExplain how results will be delivered, validated, and aligned with the relevant UN SDGs over time.\n\nWrite in a professional, structured format, using short paragraphs or bullet points where relevant.\n`;\n\n// 5. Return LLM payload\nreturn {\n json: {\n response_prompt: prompt,\n num_predict: 2500,\n model: model,\n query: query\n }\n};\n" + }, + "name": "Process Prompt for Ollama", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -736, + -32 + ], + "id": "6f0cdeb3-b5b5-4d2c-b13c-468eb92f0a52" + }, + { + "parameters": { + "method": "POST", + "url": "http://172.16.0.117:11434/api/generate", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ {\n \"model\": $json.model,\n \"prompt\": $json.response_prompt,\n \"stream\": false,\n \"options\": {\n \"temperature\": 0.3,\n \"top_p\": 0.9,\n \"num_predict\": $json.num_predict\n }\n} }}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + -528, + -32 + ], + "id": "5918cf24-0473-44f7-9d36-c05d9b73039b", + "name": "HTTP Request" + }, + { + "parameters": { + "jsCode": "// Clean Theme Response\nconst response = $input.item.json.response || $input.item.json.body?.response || '';\nlet cleanedResponse = response;\nif (cleanedResponse.includes('')) {\n cleanedResponse = cleanedResponse.split('')[1] || cleanedResponse;\n}\nreturn { json: { response: cleanedResponse.trim(), query: $input.item.json.query } };" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -320, + -32 + ], + "id": "5b029d07-b152-49b8-a269-8331e57b898f", + "name": "Clean Response" + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "{{ { \"response\": $json.response } }}", + "options": {} + }, + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.4, + "position": [ + -112, + -32 + ], + "id": "38099abd-8ac8-42e6-a400-dbbffbf14f04", + "name": "Respond to Webhook" + } + ], + "pinData": {}, + "connections": { + "Webhook GeneratePlan": { + "main": [ + [ + { + "node": "Process Prompt for Ollama", + "type": "main", + "index": 0 + } + ] + ] + }, + "Process Prompt for Ollama": { + "main": [ + [ + { + "node": "HTTP Request", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP Request": { + "main": [ + [ + { + "node": "Clean Response", + "type": "main", + "index": 0 + } + ] + ] + }, + "Clean Response": { + "main": [ + [ + { + "node": "Respond to Webhook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1" + }, + "versionId": "6ce946c6-c278-43d0-acfe-1fe814c4f963", + "meta": { + "instanceId": "21947434c58170635d41cc9137ebeab13a628beaa4cf8318a6d7c90f9b354219" + }, + "id": "k34Oeva3jxsmDg9M", + "tags": [] +} \ No newline at end of file diff --git a/app/api/missions/[missionId]/generate-plan/route.ts b/app/api/missions/[missionId]/generate-plan/route.ts index 6954f310..ffce0de7 100644 --- a/app/api/missions/[missionId]/generate-plan/route.ts +++ b/app/api/missions/[missionId]/generate-plan/route.ts @@ -45,23 +45,26 @@ export async function POST( return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); } - // Prepare data for N8N webhook + // Prepare data for N8N webhook - must be nested under "mission" key const webhookData = { - name: mission.name, - oddScope: mission.oddScope, - niveau: mission.niveau, - intention: mission.intention, - missionType: mission.missionType, - donneurDOrdre: mission.donneurDOrdre, - projection: mission.projection, - services: mission.services, - participation: mission.participation, - profils: mission.profils, + mission: { + name: mission.name, + oddScope: mission.oddScope, + niveau: mission.niveau, + intention: mission.intention, + missionType: mission.missionType, + donneurDOrdre: mission.donneurDOrdre, + projection: mission.projection, + services: mission.services, + participation: mission.participation, + profils: mission.profils, + } }; logger.debug('Calling N8N GeneratePlan webhook', { missionId, - missionName: mission.name + missionName: mission.name, + webhookData }); // Call N8N webhook @@ -92,11 +95,17 @@ export async function POST( try { const result = JSON.parse(responseText); - // The LLM response might be in different formats - actionPlan = result.plan || result.actionPlan || result.content || result.text || responseText; + // N8N returns { "response": "..." } based on the workflow + actionPlan = result.response || result.plan || result.actionPlan || result.content || result.text || responseText; + + logger.debug('Parsed N8N response', { + hasResponse: !!result.response, + responseLength: actionPlan?.length || 0 + }); } catch { // If not JSON, use the raw text actionPlan = responseText; + logger.debug('Using raw response text', { length: actionPlan.length }); } logger.debug('Received action plan from N8N', { @@ -105,7 +114,8 @@ export async function POST( }); // Save the action plan to the mission - const updatedMission = await prisma.mission.update({ + // Note: Using 'as any' until prisma generate is run to update types + const updatedMission = await (prisma.mission as any).update({ where: { id: missionId }, data: { actionPlan: actionPlan, @@ -184,7 +194,8 @@ export async function PUT( } // Update the action plan - const updatedMission = await prisma.mission.update({ + // Note: Using 'as any' until prisma generate is run to update types + const updatedMission = await (prisma.mission as any).update({ where: { id: missionId }, data: { actionPlan: actionPlan