UNPKG

agile-planner-mcp-server

Version:

Serveur MCP pour la génération d'artefacts agiles (backlogs, features, user stories) avec IA - compatible Windsurf, Claude et Cursor

439 lines (403 loc) 16.3 kB
const OpenAI = require('openai'); const Groq = require('groq-sdk'); const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); // Ces importations ont été supprimées car non utilisées dans ce module // const validatorsFactory = require('./utils/validators/validators-factory'); // const { parseJsonResponse } = require('./utils/json-parser'); const LOG_PATH = path.join(process.cwd(), '.agile-planner-backlog', 'debug-cli.log'); function debugLog(msg) { try { fs.appendFileSync(LOG_PATH, `[${new Date().toISOString()}] ${msg}\n`); } catch (e) { // If logging to file fails, at least log the original message and error to console console.error(`Failed to write to debug log file (${LOG_PATH}): ${e.message}`); console.error(`Original debug message: ${msg}`); } } function initializeClient(openaiKey, groqKey) { debugLog(`initializeClient: openaiKey=${openaiKey ? '[OK]' : '[ABSENT]'}, groqKey=${groqKey ? '[OK]' : '[ABSENT]'}`); if (openaiKey) { return new OpenAI({ apiKey: openaiKey }); } else if (groqKey) { return new Groq({ apiKey: groqKey }); } else { throw new Error('No API key provided for OpenAI or GROQ'); } } /** * Crée le schéma de validation pour une user story * @param {string} idPattern - Pattern de l'ID (US\d{3} ou OS\d{3}) * @returns {Object} Schéma de validation pour une user story * @private */ function _createUserStorySchema(idPattern = "^US\\d{3}$") { return { type: "object", required: ["id", "title", "description", "acceptance_criteria", "tasks", "priority", "slug"], properties: { id: { type: "string", pattern: idPattern }, title: { type: "string" }, description: { type: "string" }, // Format: En tant que [rôle], je veux [action] afin de [bénéfice] acceptance_criteria: { type: "array", minItems: 1, items: { type: "string" } // Format: Étant donné [contexte], quand [action], alors [résultat] }, tasks: { type: "array", minItems: 1, items: { type: "string" } }, priority: { enum: ["HIGH", "MEDIUM", "LOW"] }, slug: { type: "string", pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" } } }; } /** * Crée le schéma de validation pour une feature * @returns {Object} Schéma de validation pour une feature * @private */ function _createFeatureSchema() { return { type: "object", required: ["id", "title", "description", "slug", "stories"], properties: { id: { type: "string", pattern: "^FEAT\\d{3}$" }, title: { type: "string" }, description: { type: "string" }, slug: { type: "string", pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, stories: { type: "array", minItems: 1, // Au moins une story par feature items: _createUserStorySchema() } } }; } /** * Crée le schéma de validation pour un epic * @returns {Object} Schéma de validation pour un epic * @private */ function _createEpicSchema() { return { type: "object", required: ["id", "title", "description", "slug", "features"], properties: { id: { type: "string", pattern: "^EPIC\\d{3}$" }, title: { type: "string" }, description: { type: "string" }, slug: { type: "string", pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" }, features: { type: "array", minItems: 1, // Au moins une feature par epic items: _createFeatureSchema() } } }; } /** * Crée le schéma de validation pour le backlog * @returns {Object} Schéma de validation JSON complet pour le backlog */ function createBacklogSchema() { return { type: "object", required: ["projectName", "projectDescription", "epics", "orphan_stories"], properties: { projectName: { type: "string" }, projectDescription: { type: "string" }, epics: { type: "array", minItems: 1, // Au moins un epic items: _createEpicSchema() }, orphan_stories: { type: "array", items: _createUserStorySchema("^OS\\d{3}$") // OS pour Orphan Story } } }; } /** * Détermine le modèle à utiliser en fonction du client API * @param {Object} client - Le client API (OpenAI ou GROQ) * @param {string} taskComplexity - Complexité de la tâche ('standard', 'high', 'low') * @returns {string} Le nom du modèle à utiliser */ function determineModel(client, taskComplexity = 'standard') { if (client.baseURL === undefined || client.baseURL.includes('openai.com')) { // OpenAI: only allow gpt-4.1 or o4-mini switch (taskComplexity) { case 'high': case 'standard': return "gpt-4.1"; // Only allow gpt-4.1 for high/standard case 'low': return "o4-mini"; // Only allow o4-mini for low default: return "gpt-4.1"; } } else { // Non-OpenAI models (e.g., GROQ) return "llama3-70b-8192"; } } /** * Crée le message d'instruction système pour l'API LLM * @param {string} projectName - Nom du projet * @param {string} projectDescription - Description du projet * @returns {Object} Message système formaté * @private */ function _createSystemPrompt(projectName, projectDescription) { return { role: "system", content: `Tu es un expert Product Owner agile. Génère un backlog agile détaillé pour un projet informatique en suivant strictement le schéma JSON demandé. IMPORTANT : Le JSON généré doit OBLIGATOIREMENT avoir à la racine : { "projectName": "${projectName}", "projectDescription": "${projectDescription}", "epics": [ ... ], "orphan_stories": [ ... ] } N'ajoute AUCUN texte avant ou après le JSON. Respecte strictement ces propriétés à la racine. Structure requise: { "projectName": "${projectName}", // Obligatoire, utilise ce nom exact "epics": [ // Crée exactement 2 epics significatifs pour ce projet { "id": "EPIC001", "title": "Titre de l'epic 1", "description": "Description détaillée", "slug": "titre-de-lepic-1", // slug généré à partir du titre "features": [ ... ] } ], "orphan_stories": [ ... ] } ` }; } /** * Crée le message utilisateur pour l'API LLM * @param {string} projectName - Nom du projet * @param {string} projectDescription - Description du projet * @returns {Object} Message utilisateur formaté * @private */ function _createUserPrompt(projectName, projectDescription) { return { role: "user", content: `Génère un backlog agile détaillé pour le projet suivant: ${projectName} Description: ${projectDescription} Le backlog doit contenir au minimum: - 2 epics significatifs - 1 feature par epic - 1 story par feature - 1 story orpheline Tout le contenu doit être pertinent pour ${projectName} et basé sur la description fournie.` }; } /** * Crée les messages pour l'API * @param {string} project - Description complète du projet * @returns {Array} Messages formatés pour l'API */ function createApiMessages(project) { const splitProject = project.split(': '); const projectName = splitProject[0] || 'Projet sans nom'; const projectDescription = splitProject[1] || 'Pas de description'; return [ _createSystemPrompt(projectName, projectDescription), _createUserPrompt(projectName, projectDescription) ]; } /** * Effectue un appel API pour générer le backlog * @param {Object} client - Le client API * @param {string} model - Le modèle à utiliser * @param {Array} messages - Les messages pour l'API * @param {Object} backlogSchema - Le schéma de validation * @returns {Promise<Object>} Résultat de l'appel API */ async function callApiForBacklog(client, model, messages, backlogSchema) { // Si un constructeur est passé, instancier pour obtenir .chat if (typeof client === 'function') { client = new client(); } try { console.error(chalk.blue('✨ Appel API en cours (stderr)... Modèle:'), model); // --- BEGIN DEBUG LOGS --- console.error('[DEBUG callApiForBacklog] Client object:', client); if (client) { console.error('[DEBUG callApiForBacklog] client.chat exists:', !!client.chat); if (client.chat) { console.error('[DEBUG callApiForBacklog] client.chat.completions exists:', !!client.chat.completions); } } else { console.error('[DEBUG callApiForBacklog] Client is null or undefined'); } // Appel à l'API OpenAI/GROQ avec paramètres optimisés debugLog('generateBacklog: appel client.chat.completions.create...'); const completion = await client.chat.completions.create({ model, messages, temperature: 0.7, functions: backlogSchema.functions || undefined }); // Vérifie que la réponse contient bien un choix if (!completion.choices?.length) { throw new Error("Réponse API invalide: aucun choix retourné"); } // Récupération de la réponse debugLog('generateBacklog: réponse API reçue'); const content = completion.choices[0].message.content; debugLog(`generateBacklog: réponse API (début): ${content ? content.slice(0, 120) : '[vide]'}`); return JSON.parse(content); } catch (error) { debugLog('callApiForBacklog: ERREUR ' + (error && error.message ? error.message : error)); throw error; } } async function generateBacklog(projectName, projectDescription, client, provider = 'openai') { console.error('DEBUG_BACKLOG_GENERATOR: Entrée dans generateBacklog. Client reçu:', client); console.error('DEBUG_BACKLOG_GENERATOR: Provider reçu:', provider); debugLog(`generateBacklog: appelé avec client=${client ? '[OK]' : '[UNDEFINED]'}`); // Note: Bien que l'opérateur de chaînage optionnel (?.) soit recommandé par SonarQube, // dans ce cas précis, l'expression ternaire est plus claire car nous vérifions // l'existence de l'objet client lui-même, pas d'une propriété sur client. if (!client) { debugLog('generateBacklog: ERREUR client API non initialisé'); throw new Error('Client API non initialisé'); } debugLog('generateBacklog: début appel API'); console.error(chalk.blue('🧠 Génération du backlog à partir de la description (stderr)...')); console.error(chalk.yellow(`Client API disponible (stderr): ${!!client}`)); const fullProject = `${projectName}: ${projectDescription}`; const model = determineModel(client); const messages = createApiMessages(fullProject); const schema = createBacklogSchema(); try { const rawResultFromApi = await callApiForBacklog(client, model, messages, schema); console.error(chalk.magentaBright('DEBUG_BACKLOG_GENERATOR: Raw result from callApiForBacklog:'), JSON.stringify(rawResultFromApi, null, 2)); // Basic validation of rawResultFromApi if (!rawResultFromApi || typeof rawResultFromApi !== 'object' || Object.keys(rawResultFromApi).length === 0) { console.error(chalk.red('DEBUG_BACKLOG_GENERATOR: callApiForBacklog returned invalid or empty result.'), rawResultFromApi); throw new Error('Failed to get valid data from LLM API. Result was empty or not an object.'); } // Example check: A successful backlog result should ideally have epics or a project name. // This check might need refinement based on the actual minimal valid structure from the LLM. if (typeof rawResultFromApi.projectName === 'undefined' && !Array.isArray(rawResultFromApi.epics)) { console.error(chalk.red('DEBUG_BACKLOG_GENERATOR: callApiForBacklog result missing expected fields (projectName or epics).'), rawResultFromApi); throw new Error('LLM API result is missing expected fields like projectName or epics.'); } const outputDir = path.join(process.cwd(), '.agile-planner-backlog'); await fs.ensureDir(outputDir); const jsonPath = path.join(outputDir, 'backlog.json'); console.error(chalk.blueBright(`DEBUG_BACKLOG_GENERATOR: Attempting to write the following to ${jsonPath}:`), JSON.stringify(rawResultFromApi, null, 2)); await fs.writeFile(jsonPath, JSON.stringify(rawResultFromApi, null, 2)); console.error(chalk.green(`✅ Backlog généré avec succès : ${jsonPath}`)); return rawResultFromApi; } catch (error) { debugLog(`generateBacklog: ERREUR ${error?.message ?? error}`); throw error; } } /** * Sauvegarde le backlog brut généré au format JSON * @param {Object} result - Le résultat de la génération * @param {string} outputDir - Le répertoire de sortie * @param {Object} options - Options de sauvegarde * @param {boolean} options.auditMode - Sauvegarde en mode audit (backlog-last-dump.json) * @returns {Promise<string>} - Chemin du fichier généré */ async function saveRawBacklog(result, outputDir = './output', options = {}) { try { const fs = require('fs-extra'); const path = require('path'); await fs.ensureDir(outputDir); // Déterminer le nom du fichier selon le mode // Mode audit : backlog-last-dump.json (utilisé par les tests et l'audit CLI) // Mode normal : backlog.json (utilisation standard) const fileName = options.auditMode ? 'backlog-last-dump.json' : 'backlog.json'; const jsonPath = path.join(outputDir, fileName); await fs.writeFile(jsonPath, JSON.stringify(result, null, 2)); debugLog(`Backlog sauvegardé avec succès dans : ${jsonPath}`); console.error(chalk.green(`✅ Backlog sauvegardé avec succès : ${jsonPath}`)); return jsonPath; } catch (error) { debugLog(`saveRawBacklog: ERREUR ${error?.message ?? error}`); console.error('Erreur lors de la sauvegarde du backlog au format JSON:', error); throw error; } } /** * Valide les paramètres d'entrée pour la génération du backlog * @param {string} projectName * @param {string} projectDescription * @param {Object} client * @returns {{valid: boolean, error?: Error, project?: Object, client?: Object}} */ function processBacklogParams(projectName, projectDescription, client) { if (!projectName || typeof projectName !== 'string') { return { valid: false, error: new Error('Nom de projet invalide') }; } if (!projectDescription || typeof projectDescription !== 'string') { return { valid: false, error: new Error('Description de projet invalide') }; } if (!client) { return { valid: false, error: new Error('Client API non fourni') }; } return { valid: true, project: { name: projectName, description: projectDescription }, client }; } /** * Harmonise les stories dans toutes les features de chaque epic * @param {Object} backlogResult */ function harmonizeStories(backlogResult) { if (!Array.isArray(backlogResult.epics)) return; for (const epic of backlogResult.epics) { if (Array.isArray(epic.features)) { for (const feature of epic.features) { if (!Array.isArray(feature.stories)) { feature.stories = []; } if (feature.stories.length === 0) { console.error(chalk.yellow(`⚠️ Feature "${feature.title}" sans user stories : un dossier user-stories vide sera généré.`)); } } } } } /** * Formate une erreur pour la génération du backlog * @param {Error} error * @returns {{success: false, error: {message: string}}} */ function handleBacklogError(error) { const errorMessage = error?.message ?? 'Une erreur est survenue lors de la génération du backlog'; return { success: false, error: { message: errorMessage } }; } module.exports = { // Fonctions principales initializeClient, generateBacklog, saveRawBacklog, // Fonctions utilitaires processBacklogParams, createApiMessages, determineModel, createBacklogSchema, harmonizeStories, handleBacklogError, // Pour les tests unitaires attemptBacklogGeneration: callApiForBacklog // aliased for backward compatibility };