Files
Stirling-PDF/engine/src/stirling/contracts/progress.py
T
James BruntonandGitHub 672e81d286 Add ability for Stirling engine to reason across large documents (#6314)
# Description of Changes
Adds storage in the database for full document content alongside the RAG
content (and changes the service to `DocumentService` instead of
`RagService`). Then adds a generic capability that should be usable by
any agent (currently just used by the Question Agent) which allows the
agent to pull out the full contents of the doc, chunks it into various
sections that will fit in the context window, and then processes them in
parallel to create an intermediate result, and then processes the
intermediate result into a final answer. It will re-chunk as many times
as necessary to get the content small enough for the actual answer to be
analysed (I've tested on PDFs ~3500 pages long, which is well above the
context limit and requires maybe 3 rounds of compression to get an
answer).

The new full doc analysis stuff is heavier than the RAG lookup so both
remain. The agents should use RAG for targeted info and the chunked
reasoner for info that requires reading the full doc.
2026-05-14 13:19:38 +00:00

71 lines
2.2 KiB
Python

"""Progress events emitted by deep callees during a streaming orchestrator run.
Each subclass models one engine-side phase. The Java side forwards the JSON
verbatim into ``AiWorkflowProgressEvent.engineDetail``; the frontend switches
on ``phase`` and renders the typed fields.
"""
from __future__ import annotations
from typing import Annotated, Literal
from pydantic import Field
from stirling.models import ApiModel
class WholeDocReadStarted(ApiModel):
phase: Literal["whole_doc_read_started"] = "whole_doc_read_started"
question: str
pages: int
slices: int
class WholeDocSliceDone(ApiModel):
"""Emitted as each chunked-reasoner worker completes.
``completed`` is a monotonically increasing counter (1..total) reflecting
the order in which workers finished, NOT the slice's position in the
document. Callers showing "Read X of Y" should use this directly so X
increments by one with each event.
"""
phase: Literal["whole_doc_slice_done"] = "whole_doc_slice_done"
completed: int
total: int
pages: str
duration_ms: int
excerpts: int
facts: int
class WholeDocCompressionRound(ApiModel):
"""Emitted when the gathered slice notes exceed the synthesis context
budget and the reasoner consolidates them with a fast-model fold pass.
Long documents (a 3000-page novel produces ~900k chars of raw notes)
would otherwise overflow the smart-model's prompt. ``notes_in`` is the
count entering the round; ``groups`` is the number of fold calls fired
(each producing one consolidated note). One or two rounds usually fit;
the event fires per round so callers can render "Consolidating notes
(round N)..." rather than going silent through the fold.
"""
phase: Literal["whole_doc_compression_round"] = "whole_doc_compression_round"
round_number: int
notes_in: int
groups: int
class WholeDocReadDone(ApiModel):
phase: Literal["whole_doc_read_done"] = "whole_doc_read_done"
completed: int
slices: int
duration_seconds: float
type ProgressEvent = Annotated[
WholeDocReadStarted | WholeDocSliceDone | WholeDocCompressionRound | WholeDocReadDone,
Field(discriminator="phase"),
]