SPB Git forge

spb/uqo-chat

Public
14commits 1branches 0releases
1.4 MBsize
maindefault branch
17 days agolast push
Python 64.6% TypeScript 33.7% CSS 0.8%
3.5 KB · 82 lines python
Raw Blame History
1"""execute_python — run Python in the sandbox-runner, collect outputs and figures."""23from __future__ import annotations45import base646import re7from typing import Any89from pydantic import BaseModel, Field1011from app.core.ratelimit import MSG_SANDBOX, limiter12from app.llm.schemas import Artifact, ToolResult13from app.sandbox.client import SandboxClient14from app.services import files as file_service15from app.tools.registry import ToolContext, registry161718class ExecArgs(BaseModel):19    code: str = Field(..., description="Python source code")20    description: str = Field("", max_length=300)21    heavy: bool = False222324IMAGE_EXT = {".png", ".jpg", ".jpeg", ".svg"}252627async def run(args: dict[str, Any], ctx: ToolContext) -> ToolResult:28    if ctx.role == "student":29        limiter.check(f"sandbox:{ctx.user_id}", ctx.settings.RATE_SANDBOX_PER_HOUR, 3600,30                      MSG_SANDBOX)31    await ctx.report("running", "Exécution du code Python…")32    code = args["code"].strip()33    m = re.match(r"^```(?:python|py)?\s*\n(.*?)\n```\s*$", code, re.S)34    if m:35        code = m.group(1)36    args["code"] = code37    client = SandboxClient(ctx.settings)38    files_in = await file_service.sandbox_inputs(ctx.user_id, ctx.file_ids)39    timeout = ctx.settings.SANDBOX_HEAVY_TIMEOUT_S if args.get("heavy") else None40    res = await client.run(args["code"], files_in, timeout)41    if res.error and res.exit_code == 127:42        return ToolResult(content=f"Le sandbox n'a pas pu exécuter le code : {res.error}. "43                          "Explique le calcul à la main ou réessaie plus tard.", error=True,44                          payload={"code": args["code"], "stderr": res.error})4546    artifacts: list[Artifact] = []47    file_lines: list[str] = []48    for f in res.files_out:49        name = f.get("name", "sortie")50        data = base64.b64decode(f.get("content_b64", "")) if f.get("content_b64") else b""51        if not data:52            continue53        ext = "." + name.rsplit(".", 1)[-1].lower() if "." in name else ""54        ftype = "image" if ext in IMAGE_EXT else ("xlsx" if ext == ".xlsx" else55                                                  "csv" if ext == ".csv" else "file")56        stored = await file_service.store_artifact(57            user_id=ctx.user_id, conversation_id=ctx.conversation_id, filename=name,58            data=data, ftype=ftype)59        artifacts.append(Artifact(type=ftype, file_id=stored.id, filename=name,60                                  url=f"/api/v1/files/{stored.id}"))61        file_lines.append(f"- {name} ({len(data)} octets)")6263    status = "ok" if res.exit_code == 0 else f"exit {res.exit_code}"64    content = (f"Exécution terminée ({status}, {res.duration_ms} ms).\n"65               f"STDOUT:\n{res.stdout or '(vide)'}\n")66    if res.stderr:67        content += f"STDERR:\n{res.stderr}\n"68    if file_lines:69        content += "Fichiers produits (déjà affichés à l'étudiant) :\n" + "\n".join(file_lines)70    if res.truncated:71        content += "\n(sortie tronquée)"72    return ToolResult(73        content=content, artifacts=artifacts, error=res.exit_code != 0,74        payload={"code": args["code"], "description": args.get("description", ""),75                 "stdout": res.stdout, "stderr": res.stderr, "exit_code": res.exit_code,76                 "duration_ms": res.duration_ms, "artifacts": [a.to_dict() for a in artifacts]},77        meta={"summary": args.get("description") or "Exécution Python"},78    )798081registry.register("execute_python", run, ExecArgs, heavy=True)82