Project Files
src / utils / pythonRunner.ts
/**
* Python Subprocess Utility
* Spawns Python scripts and handles JSON communication
*/
import { spawn } from 'child_process';
import path from 'path';
import { ragDebug } from './ragLogger.js';
export interface PythonResult<T = any> {
success: boolean;
data?: T;
error?: string;
}
/**
* Resolve the plugin root (where process.cwd() points when running under LM Studio)
*/
function pluginRoot(): string {
return process.cwd();
}
/**
* Path to the plugin-local venv for RAG dependencies.
* Always stays inside the plugin directory — never system-wide.
*/
export function ragVenvPython(): string {
return path.join(pluginRoot(), '.rag-venv', 'bin', 'python3');
}
/**
* Execute a Python script and return parsed JSON output
* Tries multiple Python paths to handle different environments.
* The plugin-local venv (.rag-venv) is always tried first.
*/
export async function executePythonScript<T = any>(
scriptPath: string,
args: string[] = [],
options: {
pythonPath?: string;
timeout?: number;
cwd?: string;
} = {}
): Promise<PythonResult<T>> {
// Plugin-local venv is always first — never fall through to a system Python
// that could install packages outside this plugin directory.
const pythonCandidates = [
options.pythonPath,
ragVenvPython(),
'/usr/local/bin/python3',
'/opt/homebrew/bin/python3',
'/usr/bin/python3',
'python3',
'python',
].filter(Boolean) as string[];
const timeout = options.timeout || 60000;
ragDebug('Python', `Attempting to run: ${scriptPath}`);
ragDebug('Python', `Args: ${JSON.stringify(args)}`);
for (const pythonPath of pythonCandidates) {
const result = await tryExecutePython<T>(pythonPath, scriptPath, args, timeout, options.cwd);
// If it succeeded or failed with actual output (not spawn error), return it
if (result.success || !result.error?.includes('spawn')) {
return result;
}
ragDebug('Python', `${pythonPath} not available, trying next...`);
}
return {
success: false,
error: `Could not find working Python installation. Tried: ${pythonCandidates.join(', ')}`,
};
}
/**
* Try to execute Python with a specific path
*/
async function tryExecutePython<T>(
pythonPath: string,
scriptPath: string,
args: string[],
timeout: number,
cwd?: string
): Promise<PythonResult<T>> {
return new Promise((resolve) => {
ragDebug('Python', `Trying: ${pythonPath} ${scriptPath}`);
let pythonProcess;
try {
pythonProcess = spawn(pythonPath, [scriptPath, ...args], {
cwd: cwd || path.dirname(scriptPath),
env: { ...process.env, PYTHONUNBUFFERED: '1' },
});
} catch (err) {
resolve({
success: false,
error: `Failed to spawn ${pythonPath}: ${err}`,
});
return;
}
let stdout = '';
let stderr = '';
let timedOut = false;
// Timeout handler
const timer = setTimeout(() => {
timedOut = true;
pythonProcess.kill();
resolve({
success: false,
error: `Python script timed out after ${timeout}ms`,
});
}, timeout);
// Collect stdout
pythonProcess.stdout.on('data', (data) => {
stdout += data.toString();
});
// Collect stderr
pythonProcess.stderr.on('data', (data) => {
stderr += data.toString();
});
// Handle process exit
pythonProcess.on('close', (code) => {
clearTimeout(timer);
if (timedOut) return;
ragDebug('Python', `Process exited with code ${code}`);
ragDebug('Python', `stdout length: ${stdout.length}`);
if (stderr) {
ragDebug('Python', `stderr: ${stderr}`);
}
if (code !== 0) {
ragDebug('Python', `Script failed with code ${code}`);
if (stderr) ragDebug('Python', `stderr: ${stderr.slice(0, 2000)}`);
if (stdout) ragDebug('Python', `stdout: ${stdout.slice(0, 2000)}`);
resolve({
success: false,
error: stderr || stdout || `Process exited with code ${code}`,
});
return;
}
// Try to parse JSON output
try {
const data = JSON.parse(stdout);
ragDebug('Python', 'Successfully parsed JSON response');
resolve({
success: true,
data,
});
} catch (error) {
ragDebug('Python', `Failed to parse JSON output: ${stdout.substring(0, 500)}`);
resolve({
success: false,
error: `Invalid JSON output: ${error}`,
});
}
});
// Handle spawn errors
pythonProcess.on('error', (error) => {
clearTimeout(timer);
ragDebug('Python', `Spawn error for ${pythonPath}: ${error.message}`);
resolve({
success: false,
error: `Failed to spawn Python process (${pythonPath}): ${error.message}`,
});
});
});
}
/**
* Find Python executable — prefers plugin-local venv (.rag-venv) over system Python.
*/
export async function findPython(): Promise<string> {
const candidates = [
ragVenvPython(),
'/usr/local/bin/python3',
'/opt/homebrew/bin/python3',
'/usr/bin/python3',
'python3',
'python',
];
for (const candidate of candidates) {
try {
const result = await tryExecutePython<any>(candidate, '-c', ['print("ok")'], 5000);
if (result.success) {
return candidate;
}
} catch {
continue;
}
}
return '/usr/local/bin/python3'; // Default fallback
}