mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Feature/pdf to markdown agent (#6271)
Co-authored-by: James Brunton <[email protected]>
This commit is contained in:
co-authored by
James Brunton
parent
5b9ef852ab
commit
ece1bb6865
@@ -14,6 +14,7 @@ from .common import (
|
||||
ArtifactKind,
|
||||
ConversationMessage,
|
||||
ExtractedFileText,
|
||||
GenerateFileResponse,
|
||||
MathAuditorToolReportArtifact,
|
||||
NeedContentFileRequest,
|
||||
NeedContentResponse,
|
||||
@@ -87,6 +88,17 @@ from .pdf_questions import (
|
||||
PdfQuestionResponse,
|
||||
PdfQuestionTerminalResponse,
|
||||
)
|
||||
from .pdf_to_markdown import (
|
||||
LayoutFragment,
|
||||
LayoutLine,
|
||||
PageLayout,
|
||||
PageLayoutArtifact,
|
||||
PageLayoutFileEntry,
|
||||
PdfToMarkdownCannotDoResponse,
|
||||
PdfToMarkdownOrchestrateResponse,
|
||||
PdfToMarkdownRequest,
|
||||
PdfToMarkdownResponse,
|
||||
)
|
||||
from .progress import (
|
||||
ProgressEvent,
|
||||
WholeDocCompressionRound,
|
||||
@@ -114,6 +126,10 @@ __all__ = [
|
||||
"CompletedExecutionAction",
|
||||
"ConversationMessage",
|
||||
"DeleteDocumentResponse",
|
||||
"PdfToMarkdownCannotDoResponse",
|
||||
"PdfToMarkdownOrchestrateResponse",
|
||||
"PdfToMarkdownRequest",
|
||||
"PdfToMarkdownResponse",
|
||||
"Discrepancy",
|
||||
"DiscrepancyKind",
|
||||
"EditCannotDoResponse",
|
||||
@@ -129,6 +145,7 @@ __all__ = [
|
||||
"FolioType",
|
||||
"format_conversation_history",
|
||||
"format_file_names",
|
||||
"GenerateFileResponse",
|
||||
"HealthResponse",
|
||||
"IngestDocumentRequest",
|
||||
"IngestDocumentResponse",
|
||||
@@ -139,7 +156,12 @@ __all__ = [
|
||||
"NextExecutionAction",
|
||||
"OrchestratorRequest",
|
||||
"OrchestratorResponse",
|
||||
"LayoutFragment",
|
||||
"LayoutLine",
|
||||
"Page",
|
||||
"PageLayout",
|
||||
"PageLayoutArtifact",
|
||||
"PageLayoutFileEntry",
|
||||
"PageRange",
|
||||
"PageText",
|
||||
"PdfCommentInstruction",
|
||||
|
||||
@@ -61,6 +61,7 @@ class WorkflowOutcome(StrEnum):
|
||||
COMPLETED = "completed"
|
||||
CANNOT_CONTINUE = "cannot_continue"
|
||||
UNSUPPORTED_CAPABILITY = "unsupported_capability"
|
||||
GENERATE_FILE = "generate_file"
|
||||
|
||||
|
||||
class ArtifactKind(StrEnum):
|
||||
@@ -70,6 +71,7 @@ class ArtifactKind(StrEnum):
|
||||
"""
|
||||
|
||||
EXTRACTED_TEXT = "extracted_text"
|
||||
PAGE_LAYOUT = "page_layout"
|
||||
TOOL_REPORT = "tool_report"
|
||||
|
||||
|
||||
@@ -89,6 +91,7 @@ class SupportedCapability(StrEnum):
|
||||
AGENT_REVISE = "agent_revise"
|
||||
AGENT_NEXT_ACTION = "agent_next_action"
|
||||
MATH_AUDITOR_AGENT = "math_auditor_agent"
|
||||
PDF_TO_MARKDOWN = "pdf_to_markdown"
|
||||
|
||||
|
||||
class ConversationMessage(ApiModel):
|
||||
@@ -200,6 +203,19 @@ class ToolOperationStep(ApiModel):
|
||||
return self
|
||||
|
||||
|
||||
class GenerateFileResponse(ApiModel):
|
||||
"""Return generated text content directly to Java for file packaging.
|
||||
|
||||
Java converts the content string to bytes and stores it as a result file,
|
||||
avoiding a round-trip through a write-file tool endpoint.
|
||||
"""
|
||||
|
||||
outcome: Literal[WorkflowOutcome.GENERATE_FILE] = WorkflowOutcome.GENERATE_FILE
|
||||
content: str
|
||||
filename: str = Field(pattern=r"^[^/\\]+$", description="Output filename; no path separators.")
|
||||
summary: str | None = None
|
||||
|
||||
|
||||
def drop_unknown_tool_endpoints(value: Iterable[str | ToolEndpoint]) -> list[ToolEndpoint]:
|
||||
"""Coerce inbound endpoint identifiers into `ToolEndpoint` members, dropping unknowns.
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from .common import (
|
||||
ArtifactKind,
|
||||
ConversationMessage,
|
||||
ExtractedFileText,
|
||||
GenerateFileResponse,
|
||||
NeedContentResponse,
|
||||
NeedIngestResponse,
|
||||
SupportedCapability,
|
||||
@@ -22,6 +23,7 @@ from .common import (
|
||||
from .execution import NextExecutionAction
|
||||
from .pdf_edit import PdfEditTerminalResponse
|
||||
from .pdf_questions import PdfQuestionTerminalResponse
|
||||
from .pdf_to_markdown import PageLayoutArtifact
|
||||
|
||||
|
||||
class ExtractedTextArtifact(ApiModel):
|
||||
@@ -29,7 +31,10 @@ class ExtractedTextArtifact(ApiModel):
|
||||
files: list[ExtractedFileText] = Field(default_factory=list)
|
||||
|
||||
|
||||
WorkflowArtifact = Annotated[ExtractedTextArtifact | ToolReportArtifact, Field(discriminator="kind")]
|
||||
WorkflowArtifact = Annotated[
|
||||
ExtractedTextArtifact | PageLayoutArtifact | ToolReportArtifact,
|
||||
Field(discriminator="kind"),
|
||||
]
|
||||
|
||||
|
||||
class OrchestratorRequest(ApiModel):
|
||||
@@ -53,6 +58,7 @@ class UnsupportedCapabilityResponse(ApiModel):
|
||||
type OrchestratorResponse = Annotated[
|
||||
PdfEditTerminalResponse
|
||||
| PdfQuestionTerminalResponse
|
||||
| GenerateFileResponse
|
||||
| NeedContentResponse
|
||||
| NeedIngestResponse
|
||||
| AgentDraftResponse
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Contracts for the PDF to Markdown Agent.
|
||||
|
||||
The agent accepts a parsed document and returns a single Markdown document that
|
||||
faithfully reconstructs the PDF content — headings, paragraphs, and tables in
|
||||
reading order, using page_layout as the primary source of truth for structure.
|
||||
|
||||
Java extracts page layout via PdfIngester and returns it as a PageLayoutArtifact
|
||||
through the orchestrator resume_with pattern.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from stirling.models import ApiModel
|
||||
|
||||
from .common import ArtifactKind, ConversationMessage, GenerateFileResponse, NeedContentResponse
|
||||
from .pdf_edit import EditCannotDoResponse
|
||||
|
||||
# ── Input: layout models (mirror Java's RawLine / TextFragment geometry) ────────────────────────
|
||||
|
||||
|
||||
class LayoutFragment(ApiModel):
|
||||
"""One text fragment with its bounding-box geometry and font properties."""
|
||||
|
||||
text: str
|
||||
x: float
|
||||
y: float
|
||||
width: float
|
||||
font_size: float
|
||||
bold: bool
|
||||
|
||||
|
||||
class LayoutLine(ApiModel):
|
||||
"""A visual line on the page: one y-coordinate and all fragments on that line."""
|
||||
|
||||
y: float
|
||||
fragments: list[LayoutFragment]
|
||||
|
||||
|
||||
class PageLayout(ApiModel):
|
||||
"""All layout lines for a single page, in top-to-bottom order."""
|
||||
|
||||
page_number: int
|
||||
lines: list[LayoutLine]
|
||||
|
||||
|
||||
# ── Artifact: page layout (produced by Java, consumed by orchestrate()) ──────────────────────────
|
||||
|
||||
|
||||
class PageLayoutFileEntry(ApiModel):
|
||||
"""Page layout data for one file, as extracted by Java's PdfIngester."""
|
||||
|
||||
file_name: str
|
||||
pages: list[PageLayout] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PageLayoutArtifact(ApiModel):
|
||||
"""Artifact carrying full spatial page layout for all input files."""
|
||||
|
||||
kind: Literal[ArtifactKind.PAGE_LAYOUT] = ArtifactKind.PAGE_LAYOUT
|
||||
files: list[PageLayoutFileEntry] = Field(default_factory=list)
|
||||
|
||||
|
||||
# ── Input: full request ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class PdfToMarkdownRequest(ApiModel):
|
||||
"""Request sent by Java after PdfIngester has parsed the document.
|
||||
|
||||
page_layout: per-fragment positional data from the original (y-sorted) line order.
|
||||
Each fragment carries its x/y position, width, font size, and bold flag.
|
||||
This is the primary source of truth for column detection and heading hierarchy.
|
||||
"""
|
||||
|
||||
user_message: str
|
||||
file_names: list[str] = Field(default_factory=list)
|
||||
conversation_history: list[ConversationMessage] = Field(default_factory=list)
|
||||
page_layout: list[PageLayout] = Field(default_factory=list)
|
||||
|
||||
|
||||
# ── Output: response variants ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class PdfToMarkdownSuccessResponse(ApiModel):
|
||||
outcome: Literal["document_reconstructed"] = "document_reconstructed"
|
||||
markdown: str
|
||||
|
||||
|
||||
class PdfToMarkdownCannotDoResponse(ApiModel):
|
||||
outcome: Literal["cannot_do"] = "cannot_do"
|
||||
reason: str
|
||||
|
||||
|
||||
type PdfToMarkdownResponse = Annotated[
|
||||
PdfToMarkdownSuccessResponse | PdfToMarkdownCannotDoResponse,
|
||||
Field(discriminator="outcome"),
|
||||
]
|
||||
|
||||
type PdfToMarkdownOrchestrateResponse = Annotated[
|
||||
GenerateFileResponse | EditCannotDoResponse | NeedContentResponse,
|
||||
Field(discriminator="outcome"),
|
||||
]
|
||||
Reference in New Issue
Block a user