UNPKG

ollama-code-qwen

Version:

Un assistant IA en ligne de commande utilisant Ollama et le modèle qwen2.5-coder pour aider au développement, avec des capacités MCP améliorées et détection d'intentions en français et anglais

372 lines (330 loc) 10 kB
import { spawn } from 'child_process'; import fs from 'fs/promises'; import path from 'path'; import os from 'os'; import crypto from 'crypto'; /** * Service pour l'exécution de code dans divers langages */ export class CodeExecutor { /** * Initialise l'exécuteur de code * @param {Object} options - Options supplémentaires */ constructor(options = {}) { this.tempDir = options.tempDir || path.join(os.tmpdir(), 'ollama-code'); this.timeoutMs = options.timeout || 10000; // 10 secondes par défaut this.maxOutput = options.maxOutput || 1000000; // 1 MB max output // Extension de fichier par langage this.fileExtensions = { javascript: '.js', js: '.js', typescript: '.ts', ts: '.ts', python: '.py', py: '.py', ruby: '.rb', go: '.go', rust: '.rs', java: '.java', csharp: '.cs', cs: '.cs', php: '.php', shell: '.sh', bash: '.sh' }; // Commandes d'exécution par langage this.executionCommands = { javascript: ['node'], js: ['node'], typescript: ['ts-node'], ts: ['ts-node'], python: ['python3'], py: ['python3'], ruby: ['ruby'], go: ['go', 'run'], rust: ['rustc', '-o'], java: ['javac'], csharp: ['dotnet', 'run'], cs: ['dotnet', 'run'], php: ['php'], shell: ['bash'], bash: ['bash'] }; } /** * Extrait les blocs de code à partir d'une réponse markdown * @param {string} response - Réponse en markdown * @returns {Array<Array<string>>} - Liste de paires [langage, code] */ extractCodeBlocks(response) { const codeBlocks = []; const regex = /```([\w-]*)\n([\s\S]*?)```/g; let match; while ((match = regex.exec(response)) !== null) { const language = match[1] ? match[1].trim().toLowerCase() : ''; const code = match[2]; codeBlocks.push([language, code]); } return codeBlocks; } /** * Exécute du code dans un environnement sécurisé * @param {string} language - Langage de programmation * @param {string} code - Code à exécuter * @returns {Promise<Object>} - Résultat de l'exécution */ async executeCode(language, code) { try { // Créer un répertoire temporaire si nécessaire await fs.mkdir(this.tempDir, { recursive: true }); // Générer un nom de fichier unique const fileId = crypto.randomBytes(8).toString('hex'); const ext = this.fileExtensions[language.toLowerCase()] || '.txt'; const tempFilePath = path.join(this.tempDir, `code_${fileId}${ext}`); // Écrire le code dans un fichier temporaire await fs.writeFile(tempFilePath, code, 'utf-8'); // Obtenir la commande pour exécuter le code const execCmd = await this._getExecutionCommand(language, tempFilePath, fileId); if (!execCmd) { return { success: false, error: `Unsupported language: ${language}` }; } // Exécuter le code const result = await this._executeCommand( execCmd.command, execCmd.args, execCmd.cwd || this.tempDir ); // Nettoyer les fichiers temporaires try { if (execCmd.outputFiles) { for (const file of execCmd.outputFiles) { await fs.unlink(file); } } await fs.unlink(tempFilePath); } catch (error) { // Ignorer les erreurs de nettoyage } return { success: result.exitCode === 0, stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode }; } catch (error) { return { success: false, error: error.message }; } } /** * Obtient la commande d'exécution pour un langage donné * @param {string} language - Langage de programmation * @param {string} filePath - Chemin du fichier temporaire * @param {string} fileId - Identifiant unique du fichier * @returns {Promise<Object|null>} - Commande d'exécution ou null */ async _getExecutionCommand(language, filePath, fileId) { const lang = language.toLowerCase(); const commands = this.executionCommands[lang]; if (!commands) { return null; } switch (lang) { case 'javascript': case 'js': return { command: 'node', args: [filePath], cwd: this.tempDir }; case 'typescript': case 'ts': // Vérifier si ts-node est disponible try { await this._executeCommand('which', ['ts-node']); return { command: 'ts-node', args: [filePath], cwd: this.tempDir }; } catch (error) { return { command: 'npx', args: ['ts-node', filePath], cwd: this.tempDir }; } case 'python': case 'py': return { command: 'python3', args: [filePath], cwd: this.tempDir }; case 'ruby': return { command: 'ruby', args: [filePath], cwd: this.tempDir }; case 'go': return { command: 'go', args: ['run', filePath], cwd: this.tempDir }; case 'rust': const outputFile = path.join(this.tempDir, `rust_${fileId}`); return { command: 'rustc', args: [filePath, '-o', outputFile], execNext: { command: outputFile, args: [] }, outputFiles: [outputFile], cwd: this.tempDir }; case 'java': // Extraire le nom de la classe const javaContent = await fs.readFile(filePath, 'utf-8'); const classMatch = javaContent.match(/public\s+class\s+(\w+)/); const className = classMatch ? classMatch[1] : `Main${fileId}`; // Renommer le fichier pour correspondre au nom de la classe const javaFilePath = path.join(this.tempDir, `${className}.java`); await fs.rename(filePath, javaFilePath); return { command: 'javac', args: [javaFilePath], execNext: { command: 'java', args: ['-cp', this.tempDir, className] }, outputFiles: [path.join(this.tempDir, `${className}.class`)], cwd: this.tempDir }; case 'shell': case 'bash': // Rendre exécutable await fs.chmod(filePath, 0o755); return { command: 'bash', args: [filePath], cwd: this.tempDir }; case 'php': return { command: 'php', args: [filePath], cwd: this.tempDir }; default: return null; } } /** * Exécute une commande système * @param {string} command - Commande à exécuter * @param {Array<string>} args - Arguments de la commande * @param {string} cwd - Répertoire de travail * @returns {Promise<Object>} - Résultat de l'exécution */ async _executeCommand(command, args = [], cwd = null) { return new Promise((resolve) => { const proc = spawn(command, args, { cwd, env: { ...process.env, PYTHONUNBUFFERED: '1' }, timeout: this.timeoutMs }); let stdout = ''; let stderr = ''; let killed = false; // Limiter la taille de la sortie const checkOutputSize = () => { if (stdout.length + stderr.length > this.maxOutput) { proc.kill(); killed = true; stderr += '\nOutput limit exceeded. Process terminated.'; } }; proc.stdout.on('data', (data) => { stdout += data.toString(); checkOutputSize(); }); proc.stderr.on('data', (data) => { stderr += data.toString(); checkOutputSize(); }); const timeout = setTimeout(() => { proc.kill(); killed = true; stderr += '\nExecution time limit exceeded. Process terminated.'; }, this.timeoutMs); proc.on('close', async (exitCode) => { clearTimeout(timeout); // Si c'est une commande avec une suite (comme rustc -> ./executable) const execCmd = args[0] && args[0].execNext; if (exitCode === 0 && execCmd) { const nextResult = await this._executeCommand( execCmd.command, execCmd.args, execCmd.cwd || cwd ); resolve({ exitCode: nextResult.exitCode, stdout: nextResult.stdout, stderr: nextResult.stderr }); } else { resolve({ exitCode: killed ? 1 : exitCode, stdout, stderr }); } }); proc.on('error', (error) => { clearTimeout(timeout); stderr += `\nError: ${error.message}`; resolve({ exitCode: 1, stdout, stderr }); }); }); } /** * Détermine le langage de programmation à partir du chemin du fichier * @param {string} filePath - Chemin du fichier * @returns {string} - Langage détecté */ getLanguageFromFilePath(filePath) { const ext = path.extname(filePath).toLowerCase(); const langMap = { '.js': 'javascript', '.jsx': 'javascript', '.ts': 'typescript', '.tsx': 'typescript', '.py': 'python', '.rb': 'ruby', '.go': 'go', '.rs': 'rust', '.java': 'java', '.cs': 'csharp', '.php': 'php', '.sh': 'bash', '.html': 'html', '.css': 'css', '.json': 'json', '.md': 'markdown' }; return langMap[ext] || 'text'; } }