Files
Javier Martinez 21d42fd97a feat: code execution v4 (#2295)
* feat: add bundle to remove

* fix: spaces

* feat: add xml render as skill spec

* feat: add skill volume root to cache

* fix: deduplicate values

* fix: change the mount path to skill_id

* fix: use different paths

* feat: add principal

(cherry picked from commit 5db64fe721d5706440ce9f342b1388ffe742bc16)

# Conflicts:
#	private_gpt/components/code_execution/base.py
#	private_gpt/components/code_execution/code_execution_component.py
#	private_gpt/components/code_execution/local.py
#	private_gpt/components/environment/manager.py
#	private_gpt/components/tools/builders/bash_tool_builder.py
#	private_gpt/components/tools/builders/text_editor_tool_builder.py
#	private_gpt/components/tools/processors/bash_processor.py

* fix: mypy

...

* fix: config

* fix: sandbox config

* feat: add forward cookies

* fix: loop

* feat: add present server

* feat: add feature flag for tools

* refactor: move principal to another better place

* feat: add api key principal

* docs: fix docs

* docs: add present server

* fix: principal

* fix: mypy

* fix: avoid to block the loop

* fix: blocks in expansion

* fix: remove maximum concurrent users

...

* fix: multiplexer

* fix: readers

* fix: more fixes

...

* fix: impl

* feat: tool scheduler

* feat: add adaptative

* feat: add chat worker

* fix: config

* fix: max

* feat: add chat/tools workers

* fix: mypy

* feat: add generic scheduler

* fix: get result

* feat: do serializable the tool executor

* fix: tools

* fix: config

* fix: config

* fix: args

* fix: config

* fix: serializer

* Revert "fix: blocks in expansion"

This reverts commit a2110f94a8.

* fix: unify all logic

* feat: add ingestion scheduler

* fix: settings

* fix: config

* feat: add arq worker to chat

* fix: arq worker

* fix: add nest

* fix: mypy

* fix: await

* fix: script stress

* fix: tokenizer

* fix: chat scheduler

* fix: mypy

* fix: add async tokenizer

* fix: improve condense

* fix: tool scheduler

* feat: add initial real async chat worker

* fix: mypy

* fix: do resumable local executor

...

...

...

fix: revert usleess changes

fix: remove parent chat job

fix: refactor

fix: loop

ref: rename models

fix: chat engine

fix: mypy

...

...

...

fix: fix deps

* fix: tests

* fix: tests

* ...

* fix: stream

* fix: config

* fix: scheduler

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Handle PGPT_WORKER_MODE=celery in health check worker status

* fix: cancel

* fix: arch

* fix: test ingestion

* fix: deserialization of chat messages

* fix: broken results

* fix: mypy

* fix: test

* fix: config

* fix: remove arq tool worker

* fix: output cls

* fix: preserve early resumable tool callbacks

* fix: preserve async tool result order

* refactor: address worker PR review comments

* fix: mypy

* test: colocate ARQ chat enqueue coverage

* fix: remove redis from tests

* fix: mypy

* fix: tests

* test: isolate chat mocks and cancellation timing

* fix: tests

* fix: tests

* fix: test

(cherry picked from commit f8ee460af2)

* fix: worker config

* test: remove flaky chat cancellation assertion

(cherry picked from commit 1115ff2349)

# Conflicts:
#	tests/server/chat/test_chat_routes.py

* fix: emit chat pings from stream listeners

* fix: don't duplciate the output

* fix: rss memory

...

* fix: pass the args

* fix: threads

* fix: websearch

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-15 13:05:23 +02:00

100 lines
3.5 KiB
Python

from __future__ import annotations
import logging
import time
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from private_gpt.components.environment.content_mounter import ContentMounter
from private_gpt.components.sandbox.base import (
SandboxCodeOptions,
SandboxExecOptions,
SandboxExecutionResult,
SandboxSession,
)
from private_gpt.components.sandbox.content_bundle import ContentBundle
logger = logging.getLogger(__name__)
@dataclass
class Environment:
"""A live, mounted sandbox bound to a session id.
Tools (code execution, bash, ...) share one Environment per session.
Delegated calls refresh the idle clock the manager's reaper watches, so
any tool activity keeps the environment alive.
ContentBundles (skills, tools, ...) are registered via add_pending() and
materialized by _flush_pending(). When the sandbox is being created for
the first time, bundles that couldn't be volume-mounted are deferred and
flushed before the first exec(). When the sandbox is already running,
the manager flushes immediately so bundles are available right away.
The _stale flag is set on any flush failure so the EnvironmentManager
can evict and recreate on the next acquire().
"""
id: str
sandbox: SandboxSession
workspace: str
content_mounters: list[ContentMounter]
last_accessed: float = field(default_factory=time.monotonic)
def __post_init__(self) -> None:
self._mounted: set[str] = set()
self._pending: list[ContentBundle] = []
self._stale: bool = False
def touch(self) -> None:
self.last_accessed = time.monotonic()
def idle_seconds(self, now: float) -> float:
return now - self.last_accessed
def add_pending(self, bundles: list[ContentBundle]) -> None:
"""Stage bundles for materialization, skipping already-mounted paths.
When the container is already running, the caller is responsible for
calling _flush_pending() immediately after. When the container is being
created, deferred bundles are flushed before the first exec().
"""
for bundle in bundles:
if bundle.canonical_path not in self._mounted:
self._pending.append(bundle)
async def _flush_pending(self) -> None:
if not self._pending:
return
pending, self._pending = self._pending, []
try:
for bundle in pending:
mounter = next(
(m for m in self.content_mounters if m.can_handle(bundle)), None
)
if mounter:
await mounter.materialize(bundle, self.sandbox)
self._mounted.add(bundle.canonical_path)
except Exception:
self._stale = True
raise
async def remove_bundles(self, canonical_paths: list[str]) -> None:
for path in canonical_paths:
await self.sandbox.remove_mount(path)
self._mounted.discard(path)
async def exec(
self, command: str, opts: SandboxExecOptions | None = None
) -> SandboxExecutionResult:
self.touch()
await self._flush_pending()
return await self.sandbox.exec(command, opts)
async def run_code(
self, code: str, opts: SandboxCodeOptions | None = None
) -> SandboxExecutionResult:
self.touch()
await self._flush_pending()
return await self.sandbox.run_code(code, opts)