Files
Javier Martinez 091d5f7020 fix: random bugs (#2301)
* fix: celery callbacks

* fix: s3 + skill creator

* fix: resumable when there's params

* fix: add distributed cache

* fix: do durable context stack

* fix: mcp tools

* fix: present server tool

* fix: add cache to the skills

* fix: mcp

* fix: mypy
2026-07-16 09:14:43 +02:00

134 lines
5.5 KiB
Python

from llama_index.core.base.llms.types import TextBlock
from pydantic import BaseModel, ConfigDict, Field
from private_gpt.components.chat.models.chat_config_models import ToolSpec
from private_gpt.components.context.errors import NonResumableToolError
from private_gpt.components.context.models.context_layer import (
AnyContextLayer,
ContentBundlesLayer,
DocumentLayer,
ToolDefinitionsLayer,
)
from private_gpt.components.context.models.layer_type import LayerType
from private_gpt.components.engines.citations.types import Document
from private_gpt.components.sandbox.content_bundle import ContentBundle
class ContextStack(BaseModel):
"""Ordered, immutable assembly of typed context layers for one request.
Interceptors grow the stack each iteration by calling ``append_layer()``
or ``append_layers()``, which return new instances (the stack is frozen).
The engine reads:
- ``stack.to_system_prompt()`` → system message text (prompt layers only)
- ``stack.all_tools()`` → flat deduplicated list of ToolSpec
"""
layers: list[AnyContextLayer] = Field(default_factory=list)
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
def checkpoint_dump(self) -> dict[str, object]:
"""Serialize the complete stack after validating server tool restoration."""
for layer in self.layers:
if not isinstance(layer, ToolDefinitionsLayer):
continue
for tool in layer.tools:
if tool.runtime == "server" and tool.execution_metadata is None:
raise NonResumableToolError(
f"Server tool {tool.name!r} from layer {layer.source!r} "
"does not define execution metadata."
)
return self.model_dump(mode="json")
# ------------------------------------------------------------------
# Accessors
# ------------------------------------------------------------------
def to_system_prompt(self) -> list[TextBlock]:
"""Render prompt layers by priority (then insertion order)."""
ordered_layers = sorted(
enumerate(self.layers),
key=lambda item: (item[1].priority, item[0]),
)
chunks = [
rendered
for _, layer in ordered_layers
for rendered in [layer.render()]
if rendered.strip()
]
return [TextBlock(text=chunk) for chunk in chunks]
def all_tools(self) -> list[ToolSpec]:
"""Return deduplicated ToolSpec list from all TOOL_DEFINITIONS layers."""
seen: set[str | None] = set()
result: list[ToolSpec] = []
for layer in self.layers:
if not isinstance(layer, ToolDefinitionsLayer):
continue
for tool in layer.tools:
if tool.name not in seen:
seen.add(tool.name)
result.append(tool)
return result
def all_documents(self) -> list[Document]:
"""Return documents from all DOCUMENT layers in insertion order."""
return [
layer.document for layer in self.layers if isinstance(layer, DocumentLayer)
]
def all_bundles(self) -> list[ContentBundle]:
"""Return bundles from all CONTENT_BUNDLES layers in insertion order."""
return [
bundle
for layer in self.layers
if isinstance(layer, ContentBundlesLayer)
for bundle in layer.bundles
]
def all_bundles_to_remove(self) -> list[str]:
"""Return canonical paths to remove from all CONTENT_BUNDLES layers."""
return [
path
for layer in self.layers
if isinstance(layer, ContentBundlesLayer)
for path in layer.to_remove
]
def layers_of_type(
self,
layer_types: LayerType | AnyContextLayer | list[LayerType | AnyContextLayer],
) -> list[AnyContextLayer]:
items = layer_types if isinstance(layer_types, list) else [layer_types]
normalized = {lt.type if isinstance(lt, BaseModel) else lt for lt in items}
return [layer for layer in self.layers if layer.type in normalized]
# ------------------------------------------------------------------
# Immutable mutation helpers
# ------------------------------------------------------------------
def insert_layer(self, layer: AnyContextLayer, index: int) -> "ContextStack":
"""Return a new stack with *layer* inserted at *index* (default 0)."""
return ContextStack(layers=[*self.layers[:index], layer, *self.layers[index:]])
def append_layer(self, layer: AnyContextLayer) -> "ContextStack":
"""Return a new stack with *layer* appended."""
return ContextStack(layers=[*self.layers, layer])
def append_layers(self, layers: list[AnyContextLayer]) -> "ContextStack":
"""Return a new stack with *layers* appended."""
return ContextStack(layers=[*self.layers, *layers])
def remove_layers_of_type(self, layer_type: LayerType) -> "ContextStack":
"""Return a new stack with all layers of *layer_type* removed."""
return ContextStack(
layers=[layer for layer in self.layers if layer.type is not layer_type]
)
def remove_layers_of_source(self, source: str) -> "ContextStack":
"""Return a new stack with all layers of *source* removed."""
return ContextStack(
layers=[layer for layer in self.layers if layer.source != source]
)