mirror of
https://github.com/imartinez/privateGPT.git
synced 2026-07-17 11:29:56 +00:00
* 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
134 lines
5.5 KiB
Python
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]
|
|
)
|