Set up document management for Stirling Engine (#6476)

# Description of Changes
Change Stirling Engine to support deleting documents automatically. This
happens both on user logout and after an amount of time specified by the
Java when ingesting a document (allowing for personal documents to have
short lifetimes but org documents to be left in the db with no expiry
date). Also sets up an [ACL
policy](https://en.wikipedia.org/wiki/Access-control_list) for the
documents so the database knows which users have access to which
documents. This is not fully implemented in the Java, so currently all
docs are treated as having a single owner, the uploader, but
theoretically when we need to support org storage, we shouldn't need to
change the db schema.
This commit is contained in:
James Brunton
2026-06-03 11:52:11 +00:00
committed by GitHub
parent 71633861d0
commit 1264f4cfed
48 changed files with 2039 additions and 411 deletions
@@ -21,6 +21,7 @@ from pydantic_ai.toolsets import AbstractToolset
from stirling.agents.contradiction.detector import ContradictionDetector
from stirling.contracts import AiFile
from stirling.contracts.contradiction import Claim, ContradictionReport
from stirling.models import PrincipalId
logger = logging.getLogger(__name__)
@@ -52,6 +53,7 @@ class ContradictionCapability:
self,
detector: ContradictionDetector,
files: list[AiFile],
principals: list[PrincipalId],
*,
max_audits: int = DEFAULT_MAX_AUDITS,
) -> None:
@@ -59,6 +61,7 @@ class ContradictionCapability:
raise ValueError("max_audits must be >= 1")
self._detector = detector
self._files = files
self._principals = principals
self._max_audits = max_audits
self._audit_count = 0
toolset: FunctionToolset[None] = FunctionToolset()
@@ -121,7 +124,7 @@ class ContradictionCapability:
if not self._files:
return "No documents attached to audit."
report = await self._detector.detect(self._files, query=query)
report = await self._detector.detect(self._files, principals=self._principals, query=query)
formatted = self.format_report(report)
logger.info(
"[contradiction-capability] audit query=%r files=%d -> %d findings, %d chars",
@@ -41,6 +41,7 @@ from stirling.contracts.contradiction import (
ContradictionSeverity,
)
from stirling.contracts.documents import Page
from stirling.models import PrincipalId
from stirling.services import AppRuntime
logger = logging.getLogger(__name__)
@@ -205,8 +206,13 @@ class ContradictionDetector:
# Public entry point
# ------------------------------------------------------------------
async def detect(self, files: list[AiFile], query: str | None = None) -> ContradictionReport:
"""Run the full pipeline over the supplied files.
async def detect(
self,
files: list[AiFile],
principals: list[PrincipalId],
query: str | None = None,
) -> ContradictionReport:
"""Run the full pipeline over the supplied files for ``principals``.
``files`` must have already been ingested (the caller is
responsible for the ``has_collection`` precheck — the question
@@ -234,7 +240,7 @@ class ContradictionDetector:
effective_query = query or "extract claims"
per_file_results = await asyncio.gather(
*(self._extract_claims_for_file(file, effective_query) for file in files),
*(self._extract_claims_for_file(file, effective_query, principals) for file in files),
return_exceptions=True,
)
@@ -312,6 +318,7 @@ class ContradictionDetector:
self,
file: AiFile,
query: str,
principals: list[PrincipalId],
) -> _FileExtractionResult:
"""Run the per-chunk extractor over one file's pages.
@@ -324,7 +331,7 @@ class ContradictionDetector:
``asyncio.gather`` and the mapper's internal semaphore — this
helper itself awaits each step sequentially within one file.
"""
file_pages = await self._runtime.documents.read_pages(file.id)
file_pages = await self._runtime.documents.read_pages(file.id, principals=principals)
if not file_pages:
logger.info(
"[contradiction] no stored pages for %s (id=%s); skipping",