Files
DocsGPT/docsgpt/sandbox/base.py
T
Alex 574f96341e refactor: rename the application package to docsgpt
The backend import package is now docsgpt, the name it will carry on PyPI;
application was far too generic to install into anyone's site-packages.
git mv plus a mechanical rewrite of every import, dotted string and path
reference: 734 Python files, the compose files, Dockerfile, workflows, docs,
setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage
config, .gitignore. Behaviour is unchanged.

Kept for one release:
- A top-level application package whose meta-path finder resolves
  application.x.y to the already-imported docsgpt.x.y object, so old imports
  and entry points (celery -A application.app.celery,
  uvicorn application.asgi:asgi_app) keep working with a FutureWarning.
- Celery registers every application.* task name as an alias of its
  docsgpt.* task on start-up, so messages queued by the previous release still
  run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries
  the previous release wrote are left unread instead of firing twice.

The backend image builds from the repository root (docker build -f
docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore
allow-lists docsgpt/ and application/ and keeps caches, local data, .env
files, the sample index files and the Dockerfile out. Compose and the image
workflows point at the new context.
2026-09-07 10:20:43 +01:00

91 lines
3.3 KiB
Python

"""Backend-agnostic code-execution sandbox interface and result types."""
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
@dataclass
class DisplayData:
"""A single rich (non-stream) kernel output keyed by MIME bundle."""
data: Dict[str, Any] = field(default_factory=dict)
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass
class Plot:
"""An image/plot output captured from execution, base64-encoded by format."""
format: str
content_base64: str
@dataclass
class ExecResult:
"""Outcome of one ``exec`` call against a sandbox session."""
status: str = "ok" # "ok" | "error"
stdout: str = ""
stderr: str = ""
exit_code: int = 0
execution_count: Optional[int] = None
error_name: Optional[str] = None
error_value: Optional[str] = None
traceback: List[str] = field(default_factory=list)
results: List[DisplayData] = field(default_factory=list)
display_data: List[DisplayData] = field(default_factory=list)
plots: List[Plot] = field(default_factory=list)
truncated: bool = False # output exceeded the budget and was cut; status stays "ok"
# The backend invalidated the runtime while producing this result. Managers
# must discard their cached handle so the next open performs a cold start.
runtime_invalidated: bool = False
@property
def ok(self) -> bool:
"""True when the execution completed without raising."""
return self.status == "ok"
class SandboxGoneError(IOError):
"""The cloud runtime behind a session no longer exists (deleted upstream).
File-operation counterpart of ``ExecResult.runtime_invalidated``: backends
raise it (instead of a plain ``IOError``) when a put/get/list failed because
the runtime is gone, so managers can drop their cached session and the next
``open`` performs a cold start instead of replaying the dead handle.
Subclasses ``IOError`` so callers that only handle ``IOError`` keep working.
"""
class CodeSandbox(ABC):
"""Common interface every sandbox backend (Jupyter, Daytona, ...) implements."""
@abstractmethod
def open(self, session_id: str) -> str:
"""Create the underlying runtime for ``session_id`` and return its handle id."""
@abstractmethod
def attach(self, session_id: str) -> str:
"""Reattach to ``session_id``'s runtime; MAY return a cold kernel (state not guaranteed)."""
@abstractmethod
def close(self, session_id: str) -> None:
"""Tear down the runtime bound to ``session_id``, freeing all resources."""
@abstractmethod
def exec(self, session_id: str, code: str, timeout: Optional[float] = None) -> ExecResult:
"""Execute ``code`` in ``session_id``'s stateful runtime and return its result."""
@abstractmethod
def put_file(self, session_id: str, dest_path: str, data: bytes) -> None:
"""Write ``data`` to ``dest_path``; precondition: a relative path contained in the workspace."""
@abstractmethod
def get_file(self, session_id: str, path: str) -> bytes:
"""Read ``path`` from the workspace; precondition: a relative path contained in the workspace."""
@abstractmethod
def list_files(self, session_id: str) -> List[str]:
"""List workspace-relative file paths for ``session_id`` (never escapes the workspace)."""