Oliver-ai-bot_2.0/backend/app/tools/code_interpreter.py
Vadym Samoilenko f2b5dce63a feat: code interpreter, agent analytics/execute APIs, usage sync, RAG scoping fixes
**Phase 1 — Agent Usage Sync to AgentHub Collector**
- Add agent_usage service: per-agent stats (messages, tokens, conversations, unique users, first/last used)
- Collector sync now includes usage data in payload; sync_agent accepts optional db session
- Celery beat task runs every 6h to sync all active agents with fresh usage stats

**Phase 2 — LibreCodeInterpreter Integration**
- Add code-interpreter, redis, minio services to docker-compose.prod.yml
- CodeInterpreterTool (BaseTool): sandboxed execution via /exec, 13 languages, Python session persistence via conversation_id
- ToolContext extended with conversation_id and agent_slug
- enable_code_interpreter boolean on Agent model (migration 027), tool seeded in tool_definitions (migration 026)
- Code interpreter auto-injected into agent tools when enabled
- Frontend: CodeExecutionResult component with terminal-style stdout/stderr/files rendering

**Phase 3 — Agent API Endpoints**
- GET /api/v1/agents/{slug}/analytics — per-agent usage stats + daily time series
- POST /api/v1/agents/{slug}/execute — synchronous programmatic agent execution (non-SSE)
- Sub-routes registered before /{slug} to avoid FastAPI route conflict

**Phase 4 — Fix Department & Region RAG Scoping**
- Department filter now OR-includes global (null department) docs, matching region filter behaviour
- retriever.search_documents/retrieve_and_prepare/query accept department_ids/region_codes lists
- MatchAny used for multi-value Qdrant filters; chat.py passes full arrays from knowledge_scope
- Admin PATCH /users/{id} now validates region_code against the regions table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 20:13:27 +01:00

160 lines
5.4 KiB
Python

"""
Code Interpreter Tool
Executes code in a sandboxed environment via the LibreCodeInterpreter microservice.
Supports 13 languages. Python sessions are persistent within a conversation.
"""
import logging
from typing import Any, Dict
import httpx
from app.config import settings
from app.tools.base import BaseTool, ToolContext, ToolResult
logger = logging.getLogger(__name__)
# Map friendly language names to LibreCodeInterpreter language codes
_LANG_CODES: Dict[str, str] = {
"python": "py",
"javascript": "js",
"typescript": "ts",
"go": "go",
"java": "java",
"c": "c",
"cpp": "cpp",
"rust": "rs",
"php": "php",
"r": "r",
"bash": "bash",
"fortran": "f90",
}
class CodeInterpreterTool(BaseTool):
"""Execute code in a secure sandboxed environment."""
@property
def name(self) -> str:
return "code_interpreter"
@property
def display_name(self) -> str:
return "Code Interpreter"
@property
def description(self) -> str:
return (
"Execute code in a secure sandboxed environment. "
"Supports Python, JavaScript, TypeScript, Go, Java, C, C++, Rust, PHP, R, Bash, and Fortran. "
"Use for calculations, data analysis, generating files, visualizations, or any task "
"that benefits from running code. Returns stdout, stderr, and any generated files. "
"Python sessions maintain state between calls within the same conversation."
)
@property
def parameters_schema(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "The code to execute.",
},
"language": {
"type": "string",
"enum": list(_LANG_CODES.keys()),
"description": "Programming language to use. Defaults to python.",
"default": "python",
},
},
"required": ["code"],
}
@property
def category(self) -> str:
return "code_execution"
@property
def requires_graph_consent(self) -> bool:
return False
async def execute(self, arguments: Dict[str, Any], context: ToolContext) -> ToolResult:
if not settings.CODE_INTERPRETER_URL:
return ToolResult(
success=False,
error="Code interpreter is not configured. Set CODE_INTERPRETER_URL.",
)
code = arguments.get("code", "").strip()
if not code:
return ToolResult(success=False, error="No code provided.")
language = arguments.get("language", "python").lower()
lang_code = _LANG_CODES.get(language, "py")
# Use conversation_id as session_id for state persistence (Python REPL)
session_id = str(context.conversation_id) if context.conversation_id else str(context.user_id)
payload = {
"code": code,
"lang": lang_code,
"session_id": session_id,
"user_id": str(context.user_id),
}
if context.agent_slug:
payload["entity_id"] = context.agent_slug[:40]
try:
async with httpx.AsyncClient(timeout=60.0) as client:
resp = await client.post(
f"{settings.CODE_INTERPRETER_URL.rstrip('/')}/exec",
json=payload,
headers={
"X-API-Key": settings.CODE_INTERPRETER_API_KEY,
"Content-Type": "application/json",
},
)
except httpx.TimeoutException:
return ToolResult(success=False, error="Code execution timed out after 60 seconds.")
except Exception as exc: # noqa: BLE001
logger.error("code_interpreter: request failed: %s", exc)
return ToolResult(success=False, error=f"Failed to reach code interpreter: {exc}")
if resp.status_code != 200:
logger.warning("code_interpreter: non-200 response %s: %s", resp.status_code, resp.text[:200])
return ToolResult(success=False, error=f"Code interpreter returned HTTP {resp.status_code}.")
try:
result = resp.json()
except Exception:
return ToolResult(success=False, error="Invalid response from code interpreter.")
stdout = result.get("stdout", "")
stderr = result.get("stderr", "")
files = result.get("files", [])
# Build human-readable display string
parts = []
if stdout:
parts.append(f"Output:\n{stdout.rstrip()}")
if stderr:
parts.append(f"Errors:\n{stderr.rstrip()}")
if files:
names = [f.get("name", f.get("id", "file")) for f in files]
parts.append(f"Generated files: {', '.join(names)}")
if not parts:
parts.append("Code executed successfully (no output).")
return ToolResult(
success=True,
data={
"language": language,
"code": code,
"stdout": stdout,
"stderr": stderr,
"files": files,
"session_id": result.get("session_id", session_id),
},
display="\n".join(parts),
)