Files
zooqueenandHanzo Dev b144973bfc fix(lint): resolve all ruff + black errors across all packages
Fix 35 ruff errors (E722, F841, B904, B905, B007, E741, S202, S310,
S306, S110, TID251, UP042, F401) and run black on 526 files.

Co-authored-by: Hanzo Dev <[email protected]>
2026-02-21 23:32:32 -08:00

491 lines
15 KiB
Python

"""
Unified REPL Tool with Multi-Language Jupyter Kernel Support.
Provides persistent interactive sessions for code evaluation across languages.
Uses Jupyter kernels as backend for consistent cross-language experience.
"""
from __future__ import annotations
import uuid
import asyncio
from typing import Any
from datetime import datetime
from dataclasses import field, dataclass
# Jupyter client imports
try:
from jupyter_client import KernelManager as JupyterKernelManager
from jupyter_client.asynchronous import AsyncKernelClient
JUPYTER_AVAILABLE = True
except ImportError:
JUPYTER_AVAILABLE = False
JupyterKernelManager = None
AsyncKernelClient = None
from hanzo_tools.core import BaseTool
# Language to kernel mapping
LANGUAGE_KERNELS: dict[str, str] = {
# Python variants
"python": "python3",
"python3": "python3",
"py": "python3",
"ipython": "python3",
# JavaScript/TypeScript
"javascript": "tslab",
"js": "tslab",
"typescript": "tslab",
"ts": "tslab",
"node": "tslab",
# Shell
"bash": "bash",
"sh": "bash",
"shell": "bash",
"zsh": "bash",
# Other languages (require kernel installation)
"ruby": "ruby",
"go": "gophernotes",
"rust": "evcxr",
"julia": "julia-1.10",
"r": "ir",
}
@dataclass
class ExecutionResult:
"""Result of code execution."""
success: bool
output: str
error: str | None = None
execution_count: int = 0
data: dict[str, Any] = field(default_factory=dict)
duration_ms: float = 0
@dataclass
class KernelSession:
"""Represents an active kernel session."""
id: str
language: str
kernel_name: str
manager: Any # JupyterKernelManager
client: Any # AsyncKernelClient
created_at: datetime = field(default_factory=datetime.now)
execution_count: int = 0
history: list[tuple[str, ExecutionResult]] = field(default_factory=list)
class KernelManager:
"""Manages multiple Jupyter kernel sessions."""
_instance: KernelManager | None = None
def __init__(self) -> None:
self.sessions: dict[str, KernelSession] = {}
self.default_session: str | None = None
self._lock = asyncio.Lock()
@classmethod
def get_instance(cls) -> KernelManager:
"""Get singleton instance."""
if cls._instance is None:
cls._instance = cls()
return cls._instance
async def start_kernel(
self,
language: str = "python",
session_id: str | None = None,
) -> KernelSession:
"""Start a new kernel session."""
if not JUPYTER_AVAILABLE:
raise RuntimeError(
"jupyter-client not installed. Install with: pip install hanzo-tools-repl[full]"
)
kernel_name = LANGUAGE_KERNELS.get(language.lower(), language)
session_id = session_id or f"{language}_{uuid.uuid4().hex[:8]}"
async with self._lock:
if session_id in self.sessions:
return self.sessions[session_id]
# Create kernel manager
km = JupyterKernelManager(kernel_name=kernel_name)
km.start_kernel()
# Get async client
client = km.client()
client.start_channels()
# Wait for kernel ready
await asyncio.sleep(0.5)
session = KernelSession(
id=session_id,
language=language,
kernel_name=kernel_name,
manager=km,
client=client,
)
self.sessions[session_id] = session
if self.default_session is None:
self.default_session = session_id
return session
async def execute(
self,
code: str,
session_id: str | None = None,
timeout: float = 30.0,
) -> ExecutionResult:
"""Execute code in a kernel session."""
session_id = session_id or self.default_session
if not session_id or session_id not in self.sessions:
# Auto-start default Python kernel
session = await self.start_kernel("python")
session_id = session.id
session = self.sessions[session_id]
start_time = datetime.now()
try:
# Execute code
msg_id = session.client.execute(code)
output_parts: list[str] = []
error_output: str | None = None
data: dict[str, Any] = {}
# Collect results
while True:
try:
msg = await asyncio.wait_for(
asyncio.to_thread(
session.client.get_iopub_msg, timeout=timeout
),
timeout=timeout,
)
except asyncio.TimeoutError:
break
msg_type = msg.get("msg_type", "")
content = msg.get("content", {})
if msg_type == "stream":
output_parts.append(content.get("text", ""))
elif msg_type == "execute_result":
data = content.get("data", {})
if "text/plain" in data:
output_parts.append(data["text/plain"])
elif msg_type == "display_data":
data.update(content.get("data", {}))
if "text/plain" in data:
output_parts.append(data["text/plain"])
elif msg_type == "error":
error_output = "\n".join(content.get("traceback", []))
elif msg_type == "status":
if content.get("execution_state") == "idle":
break
duration = (datetime.now() - start_time).total_seconds() * 1000
session.execution_count += 1
result = ExecutionResult(
success=error_output is None,
output="".join(output_parts),
error=error_output,
execution_count=session.execution_count,
data=data,
duration_ms=duration,
)
# Add to history
session.history.append((code, result))
return result
except Exception as e:
return ExecutionResult(
success=False,
output="",
error=str(e),
)
async def stop_kernel(self, session_id: str | None = None) -> bool:
"""Stop a kernel session."""
session_id = session_id or self.default_session
if not session_id or session_id not in self.sessions:
return False
async with self._lock:
session = self.sessions.pop(session_id)
session.client.stop_channels()
session.manager.shutdown_kernel()
if self.default_session == session_id:
self.default_session = next(iter(self.sessions), None)
return True
async def stop_all(self) -> int:
"""Stop all kernel sessions."""
count = len(self.sessions)
for session_id in list(self.sessions.keys()):
await self.stop_kernel(session_id)
return count
def list_sessions(self) -> list[dict[str, Any]]:
"""List all active sessions."""
return [
{
"id": s.id,
"language": s.language,
"kernel": s.kernel_name,
"created": s.created_at.isoformat(),
"executions": s.execution_count,
"is_default": s.id == self.default_session,
}
for s in self.sessions.values()
]
def get_history(
self,
session_id: str | None = None,
limit: int = 20,
) -> list[dict[str, Any]]:
"""Get execution history for a session."""
session_id = session_id or self.default_session
if not session_id or session_id not in self.sessions:
return []
session = self.sessions[session_id]
history = session.history[-limit:]
return [
{
"index": i + 1,
"code": code,
"success": result.success,
"output": result.output[:500] if result.output else None,
"error": result.error[:200] if result.error else None,
}
for i, (code, result) in enumerate(history)
]
@staticmethod
def list_available_kernels() -> list[dict[str, str]]:
"""List available Jupyter kernels."""
if not JUPYTER_AVAILABLE:
return []
try:
from jupyter_client.kernelspec import find_kernel_specs
specs = find_kernel_specs()
return [{"name": name, "path": path} for name, path in specs.items()]
except Exception:
return []
class ReplTool(BaseTool):
"""
Multi-language REPL with Jupyter kernel backend.
Provides interactive code evaluation for agents across languages:
- Python, Node.js/TypeScript, Bash, and any Jupyter kernel
Actions:
- start: Start a new kernel session
- eval: Execute code in a session
- stop: Stop a kernel session
- list: List active sessions
- history: Get execution history
- kernels: List available kernels
Examples:
repl(action="start", language="python")
repl(action="eval", code="x = 1 + 1; print(x)")
repl(action="eval", code="console.log('hello')", language="node")
repl(action="history", limit=10)
repl(action="stop")
"""
name = "repl"
@property
def description(self) -> str:
return """Multi-language REPL with persistent Jupyter kernel sessions.
ACTIONS:
- start: Start kernel (language: python|node|typescript|bash|ruby|go|rust)
- eval: Execute code (code: string, language?: string, session?: string)
- stop: Stop kernel (session?: string)
- list: List active sessions
- history: Get execution history (session?: string, limit?: int)
- kernels: List available Jupyter kernels
LANGUAGES: python, node/javascript/typescript, bash/shell, ruby, go, rust
EXAMPLES:
repl(action="start", language="python")
repl(action="eval", code="print('hello')")
repl(action="eval", code="const x = [1,2,3].map(n => n*2)", language="node")
repl(action="history")
repl(action="stop")"""
def __init__(self) -> None:
super().__init__()
self.manager = KernelManager.get_instance()
async def call(
self,
action: str = "eval",
code: str | None = None,
language: str = "python",
session: str | None = None,
limit: int = 20,
timeout: float = 30.0,
**kwargs: Any,
) -> str:
"""Execute REPL action."""
action = action.lower()
if action == "start":
return await self._start(language, session)
elif action in ("eval", "execute", "run"):
if not code:
return "Error: code parameter required for eval action"
return await self._eval(code, language, session, timeout)
elif action == "stop":
return await self._stop(session)
elif action == "list":
return self._list()
elif action == "history":
return self._history(session, limit)
elif action == "kernels":
return self._kernels()
else:
return f"Unknown action: {action}. Use: start, eval, stop, list, history, kernels"
async def _start(self, language: str, session_id: str | None) -> str:
"""Start a new kernel session."""
try:
session = await self.manager.start_kernel(language, session_id)
return f"""Kernel started:
Session: {session.id}
Language: {session.language}
Kernel: {session.kernel_name}
Use repl(action="eval", code="...") to execute code."""
except Exception as e:
return f"Error starting kernel: {e}"
async def _eval(
self,
code: str,
language: str,
session_id: str | None,
timeout: float,
) -> str:
"""Execute code in a session."""
# Auto-start kernel if needed
if not self.manager.sessions:
await self.manager.start_kernel(language)
elif session_id is None and language != "python":
# Start language-specific kernel if not exists
lang_sessions = [
s
for s in self.manager.sessions.values()
if s.language.lower() == language.lower()
]
if not lang_sessions:
await self.manager.start_kernel(language)
result = await self.manager.execute(code, session_id, timeout)
if result.success:
output = result.output or "(no output)"
return f"""[{result.execution_count}] {output}"""
else:
return f"""[{result.execution_count}] Error:
{result.error}"""
async def _stop(self, session_id: str | None) -> str:
"""Stop a kernel session."""
if session_id == "all":
count = await self.manager.stop_all()
return f"Stopped {count} kernel(s)"
success = await self.manager.stop_kernel(session_id)
if success:
return f"Kernel stopped: {session_id or 'default'}"
return "No active kernel to stop"
def _list(self) -> str:
"""List active sessions."""
sessions = self.manager.list_sessions()
if not sessions:
return "No active REPL sessions"
lines = ["Active REPL sessions:"]
for s in sessions:
default = " (default)" if s["is_default"] else ""
lines.append(
f" {s['id']}: {s['language']} ({s['kernel']}) - {s['executions']} executions{default}"
)
return "\n".join(lines)
def _history(self, session_id: str | None, limit: int) -> str:
"""Get execution history."""
history = self.manager.get_history(session_id, limit)
if not history:
return "No execution history"
lines = ["Execution history:"]
for h in history:
status = "OK" if h["success"] else "ERR"
code_preview = h["code"][:50].replace("\n", "\\n")
if len(h["code"]) > 50:
code_preview += "..."
lines.append(f" [{h['index']}] {status}: {code_preview}")
return "\n".join(lines)
def _kernels(self) -> str:
"""List available kernels."""
kernels = self.manager.list_available_kernels()
if not kernels:
return "No Jupyter kernels found. Install with: pip install ipykernel"
lines = ["Available Jupyter kernels:"]
for k in kernels:
lines.append(f" {k['name']}")
lines.append("\nSupported language aliases:")
for lang, kernel in sorted(set((v, k) for k, v in LANGUAGE_KERNELS.items())):
aliases = [k for k, v in LANGUAGE_KERNELS.items() if v == lang]
lines.append(f" {lang}: {', '.join(aliases)}")
return "\n".join(lines)