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
JavaScript
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';
}
}