feat(coding-agent/eval): added local python-runner subprocess execution
- Replaced Python execution with a local `python -u runner.py` subprocess and NDJSON stdin/stdout framing. - Removed shared-gateway architecture, including coordinator lifecycle APIs, `useSharedGateway` wiring, and `jupyter` CLI/actions. - Simplified setup checks to a plain Python 3 availability probe and removed automatic dependency-install fallbacks. - Updated kernel cancellation and display processing to use status frames, SIGINT/SIGTERM escalation, and normalized output coercion. - Added `python-runner` integration and display tests while deleting legacy websocket and kernel lifecycle test suites.
This commit is contained in:
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* Jupyter CLI command handlers.
|
||||
*
|
||||
* Handles `omp jupyter` subcommand for managing the shared Python gateway.
|
||||
*/
|
||||
|
||||
import { APP_NAME } from "@oh-my-pi/pi-utils";
|
||||
import chalk from "chalk";
|
||||
import { getGatewayStatus, shutdownSharedGateway } from "../eval/py/gateway-coordinator";
|
||||
|
||||
export type JupyterAction = "kill" | "status";
|
||||
|
||||
export interface JupyterCommandArgs {
|
||||
action: JupyterAction;
|
||||
}
|
||||
|
||||
export function parseJupyterArgs(args: string[]): JupyterCommandArgs | undefined {
|
||||
if (args.length === 0 || args[0] !== "jupyter") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const action = args[1] as JupyterAction | undefined;
|
||||
if (!action || !["kill", "status"].includes(action)) {
|
||||
return { action: "status" };
|
||||
}
|
||||
|
||||
return { action };
|
||||
}
|
||||
|
||||
export async function runJupyterCommand(cmd: JupyterCommandArgs): Promise<void> {
|
||||
switch (cmd.action) {
|
||||
case "kill":
|
||||
await runKill();
|
||||
break;
|
||||
case "status":
|
||||
await runStatus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function runKill(): Promise<void> {
|
||||
const status = await getGatewayStatus();
|
||||
|
||||
if (!status.active) {
|
||||
console.log(chalk.dim("No Jupyter gateway is running"));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Killing Jupyter gateway (PID ${status.pid})...`);
|
||||
await shutdownSharedGateway();
|
||||
console.log(chalk.green("Jupyter gateway stopped"));
|
||||
}
|
||||
|
||||
async function runStatus(): Promise<void> {
|
||||
const status = await getGatewayStatus();
|
||||
|
||||
if (!status.active) {
|
||||
console.log(chalk.dim("No Jupyter gateway is running"));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold("Jupyter Gateway Status\n"));
|
||||
console.log(` ${chalk.green("●")} Running`);
|
||||
console.log(` PID: ${status.pid}`);
|
||||
console.log(` URL: ${status.url}`);
|
||||
if (status.uptime !== null) {
|
||||
console.log(` Uptime: ${formatUptime(status.uptime)}`);
|
||||
}
|
||||
if (status.pythonPath) {
|
||||
console.log(` Python: ${status.pythonPath}`);
|
||||
}
|
||||
if (status.venvPath) {
|
||||
console.log(` Venv: ${status.venvPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
function formatUptime(ms: number): string {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m ${seconds % 60}s`;
|
||||
}
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
export function printJupyterHelp(): void {
|
||||
console.log(`${chalk.bold(`${APP_NAME} jupyter`)} - Manage the shared Jupyter gateway
|
||||
|
||||
${chalk.bold("Usage:")}
|
||||
${APP_NAME} jupyter <command>
|
||||
|
||||
${chalk.bold("Commands:")}
|
||||
status Show gateway status (default)
|
||||
kill Stop the running gateway
|
||||
|
||||
${chalk.bold("Examples:")}
|
||||
${APP_NAME} jupyter # Show status
|
||||
${APP_NAME} jupyter status # Show status
|
||||
${APP_NAME} jupyter kill # Stop the gateway
|
||||
`);
|
||||
}
|
||||
@@ -21,7 +21,6 @@ export interface SetupCommandArgs {
|
||||
|
||||
const VALID_COMPONENTS: SetupComponent[] = ["python", "stt"];
|
||||
|
||||
const PYTHON_PACKAGES = ["jupyter_kernel_gateway", "ipykernel"];
|
||||
const MANAGED_PYTHON_ENV = getPythonEnvDir();
|
||||
|
||||
/**
|
||||
@@ -65,10 +64,6 @@ export function parseSetupArgs(args: string[]): SetupCommandArgs | undefined {
|
||||
interface PythonCheckResult {
|
||||
available: boolean;
|
||||
pythonPath?: string;
|
||||
uvPath?: string;
|
||||
pipPath?: string;
|
||||
missingPackages: string[];
|
||||
installedPackages: string[];
|
||||
usingManagedEnv?: boolean;
|
||||
managedEnvPath?: string;
|
||||
}
|
||||
@@ -85,8 +80,6 @@ function managedPythonPath(): string {
|
||||
async function checkPythonSetup(): Promise<PythonCheckResult> {
|
||||
const result: PythonCheckResult = {
|
||||
available: false,
|
||||
missingPackages: [],
|
||||
installedPackages: [],
|
||||
managedEnvPath: MANAGED_PYTHON_ENV,
|
||||
};
|
||||
|
||||
@@ -94,109 +87,24 @@ async function checkPythonSetup(): Promise<PythonCheckResult> {
|
||||
const managedPath = managedPythonPath();
|
||||
const hasManagedEnv = await Bun.file(managedPath).exists();
|
||||
|
||||
result.uvPath = $which("uv") ?? undefined;
|
||||
result.pipPath = $which("pip3") ?? $which("pip") ?? undefined;
|
||||
|
||||
const candidates = [systemPythonPath, hasManagedEnv ? managedPath : undefined].filter(
|
||||
(candidate): candidate is string => !!candidate,
|
||||
);
|
||||
if (candidates.length === 0) {
|
||||
const pythonPath = systemPythonPath ?? (hasManagedEnv ? managedPath : undefined);
|
||||
if (!pythonPath) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result.pythonPath = systemPythonPath ?? managedPath;
|
||||
let bestMatch = {
|
||||
pythonPath: candidates[0],
|
||||
missingPackages: [...PYTHON_PACKAGES],
|
||||
installedPackages: [] as string[],
|
||||
usingManagedEnv: candidates[0] === managedPath,
|
||||
};
|
||||
|
||||
for (const pythonPath of candidates) {
|
||||
const installedPackages: string[] = [];
|
||||
const missingPackages: string[] = [];
|
||||
for (const pkg of PYTHON_PACKAGES) {
|
||||
const moduleName = pkg === "jupyter_kernel_gateway" ? "kernel_gateway" : pkg;
|
||||
const script = `import importlib.util; raise SystemExit(0 if importlib.util.find_spec('${moduleName}') else 1)`;
|
||||
const check = await $`${pythonPath} -c ${script}`.quiet().nothrow();
|
||||
if (check.exitCode === 0) {
|
||||
installedPackages.push(pkg);
|
||||
} else {
|
||||
missingPackages.push(pkg);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingPackages.length < bestMatch.missingPackages.length) {
|
||||
bestMatch = {
|
||||
pythonPath,
|
||||
missingPackages,
|
||||
installedPackages,
|
||||
usingManagedEnv: pythonPath === managedPath,
|
||||
};
|
||||
}
|
||||
|
||||
if (missingPackages.length === 0) {
|
||||
result.available = true;
|
||||
result.pythonPath = pythonPath;
|
||||
result.missingPackages = missingPackages;
|
||||
result.installedPackages = installedPackages;
|
||||
result.usingManagedEnv = pythonPath === managedPath;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
result.pythonPath = bestMatch.pythonPath;
|
||||
result.missingPackages = bestMatch.missingPackages;
|
||||
result.installedPackages = bestMatch.installedPackages;
|
||||
result.usingManagedEnv = bestMatch.usingManagedEnv;
|
||||
const probe = await $`${pythonPath} -c "import sys;sys.exit(0)"`.quiet().nothrow();
|
||||
result.pythonPath = pythonPath;
|
||||
result.available = probe.exitCode === 0;
|
||||
result.usingManagedEnv = pythonPath === managedPath;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install Python packages using uv (preferred) or pip.
|
||||
*/
|
||||
async function installPythonPackages(
|
||||
packages: string[],
|
||||
pythonPath: string,
|
||||
uvPath?: string,
|
||||
pipPath?: string,
|
||||
): Promise<{ success: boolean; usedManagedEnv: boolean }> {
|
||||
if (uvPath) {
|
||||
console.log(chalk.dim(`Installing via uv: ${packages.join(" ")}`));
|
||||
const result = await $`${uvPath} pip install ${packages}`.nothrow();
|
||||
if (result.exitCode === 0) {
|
||||
return { success: true, usedManagedEnv: false };
|
||||
}
|
||||
}
|
||||
|
||||
if (pipPath) {
|
||||
console.log(chalk.dim(`Installing via pip: ${packages.join(" ")}`));
|
||||
const result = await $`${pipPath} install ${packages}`.nothrow();
|
||||
if (result.exitCode === 0) {
|
||||
return { success: true, usedManagedEnv: false };
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.dim(`Falling back to managed virtual environment: ${MANAGED_PYTHON_ENV}`));
|
||||
|
||||
if (uvPath) {
|
||||
const createEnv = await $`${uvPath} venv ${MANAGED_PYTHON_ENV}`.quiet().nothrow();
|
||||
if (createEnv.exitCode !== 0) {
|
||||
return { success: false, usedManagedEnv: true };
|
||||
}
|
||||
const installInManagedEnv = await $`${uvPath} pip install --python ${MANAGED_PYTHON_ENV} ${packages}`.nothrow();
|
||||
return { success: installInManagedEnv.exitCode === 0, usedManagedEnv: true };
|
||||
}
|
||||
|
||||
const createEnv = await $`${pythonPath} -m venv ${MANAGED_PYTHON_ENV}`.quiet().nothrow();
|
||||
if (createEnv.exitCode !== 0) {
|
||||
return { success: false, usedManagedEnv: true };
|
||||
}
|
||||
|
||||
const managedPython = managedPythonPath();
|
||||
const installInManagedEnv = await $`${managedPython} -m pip install ${packages}`.nothrow();
|
||||
return { success: installInManagedEnv.exitCode === 0, usedManagedEnv: true };
|
||||
}
|
||||
// Python installation helper removed: the subprocess runner has no Python
|
||||
// package dependencies beyond a working interpreter. `omp setup python --check`
|
||||
// remains as a probe; users install optional libs (pandas, matplotlib, ...)
|
||||
// directly via pip or the in-process `%pip` magic.
|
||||
|
||||
/**
|
||||
* Run the setup command.
|
||||
@@ -232,67 +140,13 @@ async function handlePythonSetup(flags: { json?: boolean; check?: boolean }): Pr
|
||||
console.log(chalk.dim(`Using managed environment: ${check.managedEnvPath}`));
|
||||
}
|
||||
|
||||
if (check.uvPath) {
|
||||
console.log(chalk.dim(`uv: ${check.uvPath}`));
|
||||
} else if (check.pipPath) {
|
||||
console.log(chalk.dim(`pip: ${check.pipPath}`));
|
||||
}
|
||||
|
||||
if (check.installedPackages.length > 0) {
|
||||
console.log(chalk.green(`${theme.status.success} Installed: ${check.installedPackages.join(", ")}`));
|
||||
}
|
||||
|
||||
if (check.missingPackages.length === 0) {
|
||||
if (check.available) {
|
||||
console.log(chalk.green(`\n${theme.status.success} Python execution is ready`));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.yellow(`${theme.status.warning} Missing: ${check.missingPackages.join(", ")}`));
|
||||
|
||||
if (flags.check) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!check.uvPath && !check.pipPath) {
|
||||
console.error(chalk.red(`\n${theme.status.error} No package manager found`));
|
||||
console.error(chalk.dim("Install uv (recommended) or pip:"));
|
||||
console.error(chalk.dim(" curl -LsSf https://astral.sh/uv/install.sh | sh"));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("");
|
||||
const install = await installPythonPackages(check.missingPackages, check.pythonPath, check.uvPath, check.pipPath);
|
||||
|
||||
if (!install.success) {
|
||||
console.error(chalk.red(`\n${theme.status.error} Installation failed`));
|
||||
console.error(chalk.dim("Try installing manually:"));
|
||||
if (install.usedManagedEnv) {
|
||||
if (check.uvPath) {
|
||||
console.error(chalk.dim(` uv venv ${MANAGED_PYTHON_ENV}`));
|
||||
console.error(
|
||||
chalk.dim(` uv pip install --python ${MANAGED_PYTHON_ENV} ${check.missingPackages.join(" ")}`),
|
||||
);
|
||||
} else {
|
||||
console.error(chalk.dim(` ${check.pythonPath} -m venv ${MANAGED_PYTHON_ENV}`));
|
||||
console.error(chalk.dim(` ${managedPythonPath()} -m pip install ${check.missingPackages.join(" ")}`));
|
||||
}
|
||||
} else {
|
||||
console.error(chalk.dim(` ${check.uvPath ? "uv pip" : "pip"} install ${check.missingPackages.join(" ")}`));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const recheck = await checkPythonSetup();
|
||||
if (recheck.available) {
|
||||
console.log(chalk.green(`\n${theme.status.success} Python execution is ready`));
|
||||
if (recheck.usingManagedEnv) {
|
||||
console.log(chalk.dim(`Managed Python environment: ${recheck.managedEnvPath}`));
|
||||
}
|
||||
} else {
|
||||
console.error(chalk.red(`\n${theme.status.error} Setup incomplete`));
|
||||
console.error(chalk.dim(`Still missing: ${recheck.missingPackages.join(", ")}`));
|
||||
process.exit(1);
|
||||
}
|
||||
console.error(chalk.red(`\n${theme.status.error} Python interpreter reported failure`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function handleSttSetup(flags: { json?: boolean; check?: boolean }): Promise<void> {
|
||||
@@ -359,9 +213,8 @@ ${chalk.bold("Usage:")}
|
||||
${APP_NAME} setup <component> [options]
|
||||
|
||||
${chalk.bold("Components:")}
|
||||
python Install Jupyter kernel dependencies for Python code execution
|
||||
python Verify a Python 3 interpreter is reachable for code execution
|
||||
stt Install speech-to-text dependencies (openai-whisper, recording tools)
|
||||
Packages: ${PYTHON_PACKAGES.join(", ")}
|
||||
|
||||
${chalk.bold("Options:")}
|
||||
-c, --check Check if dependencies are installed without installing
|
||||
|
||||
Reference in New Issue
Block a user