diff --git a/.taskfiles/engine.yml b/.taskfiles/engine.yml index 339ae1b29..5c9d6a8e6 100644 --- a/.taskfiles/engine.yml +++ b/.taskfiles/engine.yml @@ -86,8 +86,9 @@ tasks: fix: desc: "Auto-fix lint + format" cmds: + - task: format # Can auto-fix some things that `lint:fix` can't like line length violations - task: lint:fix - - task: format + - task: format # Ensure that after lint fixing that the code is still formatted correctly check: desc: "Full engine quality gate" diff --git a/app/common/src/main/java/stirling/software/common/model/api/PDFFile.java b/app/common/src/main/java/stirling/software/common/model/api/PDFFile.java index f999f7dc0..ce9eab10c 100644 --- a/app/common/src/main/java/stirling/software/common/model/api/PDFFile.java +++ b/app/common/src/main/java/stirling/software/common/model/api/PDFFile.java @@ -14,7 +14,6 @@ import lombok.NoArgsConstructor; @Data @NoArgsConstructor @EqualsAndHashCode -@Schema(description = "PDF file input - either upload a file or provide a server-side file ID") public class PDFFile { @Schema( diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/general/RotatePDFRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/general/RotatePDFRequest.java index 43695de3d..19f318982 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/general/RotatePDFRequest.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/general/RotatePDFRequest.java @@ -13,8 +13,7 @@ public class RotatePDFRequest extends PDFFile { @Schema( description = - "The clockwise angle by which to rotate the PDF file. Must be a multiple of" - + " 90.", + "The clockwise angle by which to rotate all pages in the PDF file. Must be a multiple of 90.", type = "integer", requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {"0", "90", "180", "270"}) diff --git a/engine/scripts/generate_tool_models.py b/engine/scripts/generate_tool_models.py index 5b98b5195..9bc1458fe 100644 --- a/engine/scripts/generate_tool_models.py +++ b/engine/scripts/generate_tool_models.py @@ -21,7 +21,7 @@ from datamodel_code_generator.format import Formatter from referencing import Registry, Resource from referencing.jsonschema import DRAFT202012 -# Fields inherited from PDFFile base class — not tool parameters. +# Fields inherited from PDFFile base class - not tool parameters. BASE_CLASS_FIELDS = frozenset({"fileInput", "fileId"}) _ENGINE_ROOT = Path(__file__).resolve().parents[1] @@ -73,8 +73,9 @@ class ToolDiscovery: for path, path_item in sorted(self.spec.get("paths", {}).items()): if "{" in path or not any(path.startswith(p) for p in self.ALLOWED_PATH_PREFIXES): continue - body_props = self._get_request_properties(path_item) or {} + body_schema = self._get_request_body_schema(path_item) or {} query_props = self._get_query_parameters(path_item) + body_props = body_schema.get("properties") or {} # Body properties win on name collision — body is the canonical param source # for the existing tools; query params are additive. properties = {**query_props, **body_props} @@ -87,7 +88,21 @@ class ToolDiscovery: enum_name = _deduplicate(_path_to_enum_name(path), used_enum) class_name = _deduplicate(_path_to_class_name(path), used_class) - defs[class_name] = {"type": "object", "properties": clean_props} + entry: dict[str, Any] = { + "type": "object", + "properties": clean_props, + "description": body_schema.get("description"), + } + # Calculate which fields are actually required (many are marked as required, + # but have a default set, so they're not really required) + required = [ + name + for name in body_schema.get("required") or [] + if name in clean_props and "default" not in (clean_props[name] or {}) + ] + if required: + entry["required"] = required + defs[class_name] = entry tools.append(ToolSpec(path, enum_name, class_name)) self._inline_component_refs(defs) @@ -119,7 +134,7 @@ class ToolDiscovery: return self.resolver.lookup(schema["$ref"]).contents return schema - def _get_request_properties(self, path_item: dict[str, Any]) -> dict[str, Any] | None: + def _get_request_body_schema(self, path_item: dict[str, Any]) -> dict[str, Any] | None: post = path_item.get("post") if not post: return None @@ -128,7 +143,7 @@ class ToolDiscovery: if media_type in content: schema = content[media_type].get("schema") if schema: - return self._resolve_ref(schema).get("properties") + return self._resolve_ref(schema) return None def _get_query_parameters(self, path_item: dict[str, Any]) -> dict[str, Any]: @@ -227,6 +242,8 @@ def generate_models_code(combined_schema: dict[str, Any]) -> str: field_constraints=True, no_alias=True, set_default_enum_member=True, + strict_nullable=True, + use_schema_description=True, additional_imports=["enum.StrEnum"], enable_version_header=False, custom_file_header=_FILE_HEADER, diff --git a/engine/src/stirling/agents/pdf_edit.py b/engine/src/stirling/agents/pdf_edit.py index 4dac512c1..99901a4b6 100644 --- a/engine/src/stirling/agents/pdf_edit.py +++ b/engine/src/stirling/agents/pdf_edit.py @@ -34,9 +34,9 @@ logger = logging.getLogger(__name__) class PdfEditPlanSelection(ApiModel): outcome: Literal["plan"] = "plan" + rationale: str operations: list[ToolEndpoint] = Field(min_length=1) summary: str - rationale: str | None = None type PdfEditPlanOutput = PdfEditPlanSelection | EditClarificationRequest | EditCannotDoResponse | NeedContentResponse @@ -254,10 +254,16 @@ class PdfEditAgent: "Plan PDF edit requests. " f"Supported operations are: {self._get_operations_prompt(supported_operations)}." f"{unavailable_clause} " + "Each operation in the user-facing prompt is listed with its full set of parameters. " + "Treat that list as authoritative: an operation can ONLY do what its listed parameters " + "and description allow. " "Return an ordered list of one or more supported operations for the plan. " + "Chain multiple operations together whenever the request needs effects that no single " + "supported operation provides on its own (for example, splitting then rotating then " + "merging, or extracting pages then re-inserting them). " + "Only return cannot_do when no sequence of the supported operations could achieve the request. " "Do not produce operation parameters in this stage. " "Return need_clarification when the request is genuinely ambiguous. " - "Return cannot_do when the request is outside the supported operations. " "Return plan when a reasonable multi-step plan can be created. " "Never return partial plans." ), @@ -280,7 +286,7 @@ class PdfEditAgent: f"Conversation history:\n{format_conversation_history(request.conversation_history)}\n" f"User request: {request.user_message}\n" f"Files: {format_file_names(request.files)}\n" - f"Supported operations: {self._get_operations_prompt(supported_operations)}\n" + f"Supported operations:\n{self._get_supported_operations_prompt(supported_operations)}\n" f"{unavailable_line}" f"Extracted page text:\n{format_page_text(request.page_text)}" ) @@ -297,6 +303,29 @@ class PdfEditAgent: def _get_operations_prompt(operations: Iterable[ToolEndpoint]) -> str: return ", ".join(f"{op.name} ({op.value})" for op in operations) + @staticmethod + def _get_supported_operations_prompt(operations: Iterable[ToolEndpoint]) -> str: + """Render each operation with its description and a flat list of param descriptions. + + The selection step decides which tool fits the user's request, so it just needs + what each tool does and what knobs it has, not the full schema. + """ + lines: list[str] = [] + for op in operations: + schema = OPERATIONS[op].model_json_schema() + head = f"- {op.name} ({op.value})" + description = (schema.get("description") or "").strip() + if description: + head += f": {description}" + lines.append(head) + for name, prop in (schema.get("properties") or {}).items(): + param_description = (prop.get("description") or "").strip() + if param_description: + lines.append(f" {name}: {param_description}") + else: + lines.append(f" {name}") + return "\n".join(lines) + def _fill_need_content_defaults( self, selection: NeedContentResponse, diff --git a/engine/src/stirling/models/tool_models.py b/engine/src/stirling/models/tool_models.py index 2e3caad86..742d1a796 100644 --- a/engine/src/stirling/models/tool_models.py +++ b/engine/src/stirling/models/tool_models.py @@ -12,15 +12,15 @@ from stirling.models.base import ApiModel class AddAttachmentsParams(ApiModel): - attachments: list[bytes] | None = Field(None, description="The image file to be overlaid onto the PDF.") - convert_to_pdf_a3b: bool | None = Field( + attachments: list[bytes] = Field(..., description="The image file to be overlaid onto the PDF.") + convert_to_pdf_a3b: bool = Field( False, description="Convert the resulting PDF to PDF/A-3b format after adding attachments" ) class AddCommentsParams(ApiModel): - comments: str | None = Field( - None, + comments: str = Field( + ..., description="JSON array of comment specs. Each element has: {pageIndex, x, y, width, height, text, author?, subject?}. Coordinates are PDF user-space with origin at the page's bottom-left.", examples=[ '[{"pageIndex":0,"x":72,"y":720,"width":20,"height":20,"text":"Check this paragraph","author":"Reviewer","subject":"Unclear wording"}]' @@ -29,12 +29,16 @@ class AddCommentsParams(ApiModel): class AddImageParams(ApiModel): - every_page: bool | None = Field(False, description="Whether to overlay the image onto every page of the PDF.") - x: float | None = Field(0, description="The x-coordinate at which to place the top-left corner of the image.") - y: float | None = Field(0, description="The y-coordinate at which to place the top-left corner of the image.") + every_page: bool = Field(False, description="Whether to overlay the image onto every page of the PDF.") + x: float = Field(0, description="The x-coordinate at which to place the top-left corner of the image.") + y: float = Field(0, description="The y-coordinate at which to place the top-left corner of the image.") class CustomMargin(StrEnum): + """ + Custom margin: small/medium/large/x-large + """ + small = "small" medium = "medium" large = "large" @@ -42,12 +46,20 @@ class CustomMargin(StrEnum): class FontType(StrEnum): + """ + Font type for page numbers + """ + helvetica = "helvetica" courier = "courier" times = "times" class Position(IntEnum): + """ + Position: 1-9 representing positions on the page (1=top-left, 2=top-center, 3=top-right, 4=middle-left, 5=middle-center, 6=middle-right, 7=bottom-left, 8=bottom-center, 9=bottom-right) + """ + integer_1 = 1 integer_2 = 2 integer_3 = 3 @@ -60,42 +72,42 @@ class Position(IntEnum): class AddPageNumbersParams(ApiModel): - custom_margin: CustomMargin | None = Field( - CustomMargin.medium, description="Custom margin: small/medium/large/x-large" - ) - custom_text: str | None = Field( + custom_margin: CustomMargin = Field(CustomMargin.medium, description="Custom margin: small/medium/large/x-large") + custom_text: str = Field( "{n}", description="Custom text pattern. Available variables: {n}=current page number, {total}=total pages, {filename}=original filename", examples=["Page {n} of {total}"], ) - font_color: str | None = Field( - "#000000", description="Hex colour for page numbers (e.g. #FF0000)", examples=["#000000"] - ) - font_size: float | None = Field(12, description="Font size for page numbers", ge=1.0) - font_type: FontType | None = Field(None, description="Font type for page numbers") - page_numbers: str | None = Field( + font_color: str = Field("#000000", description="Hex colour for page numbers (e.g. #FF0000)", examples=["#000000"]) + font_size: float = Field(12, description="Font size for page numbers", ge=1.0) + font_type: FontType = Field(..., description="Font type for page numbers") + page_numbers: str = Field( "all", description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", ) - pages_to_number: str | None = Field("all", description="Which pages to number (e.g. '1,3-5,7' or 'all')") - position: Position | None = Field( - None, + pages_to_number: str = Field("all", description="Which pages to number (e.g. '1,3-5,7' or 'all')") + position: Position = Field( + ..., description="Position: 1-9 representing positions on the page (1=top-left, 2=top-center, 3=top-right, 4=middle-left, 5=middle-center, 6=middle-right, 7=bottom-left, 8=bottom-center, 9=bottom-right)", ) - starting_number: int | None = Field(1, description="Starting number for page numbering", ge=1) - zero_pad: int | None = Field( + starting_number: int = Field(1, description="Starting number for page numbering", ge=1) + zero_pad: int = Field( 0, description="Zero-padding width for page numbers (Bates Stamping). Set to 0 to disable padding", ge=0 ) class KeyLength(IntEnum): + """ + The length of the encryption key + """ + integer_40 = 40 integer_128 = 128 integer_256 = 256 class AddPasswordParams(ApiModel): - key_length: KeyLength | None = Field(None, description="The length of the encryption key") + key_length: KeyLength = Field(..., description="The length of the encryption key") owner_password: SecretStr | None = Field( None, description="The owner password to be added to the PDF file (Restricts what can be done with the document once it is opened)", @@ -103,21 +115,23 @@ class AddPasswordParams(ApiModel): password: SecretStr | None = Field( None, description="The password to be added to the PDF file (Restricts the opening of the document itself.)" ) - prevent_assembly: bool | None = Field(False, description="Whether document assembly is prevented") - prevent_extract_content: bool | None = Field(False, description="Whether content extraction is prevented") - prevent_extract_for_accessibility: bool | None = Field( + prevent_assembly: bool = Field(False, description="Whether document assembly is prevented") + prevent_extract_content: bool = Field(False, description="Whether content extraction is prevented") + prevent_extract_for_accessibility: bool = Field( False, description="Whether content extraction for accessibility is prevented" ) - prevent_fill_in_form: bool | None = Field(False, description="Whether form filling is prevented") - prevent_modify: bool | None = Field(False, description="Whether document modification is prevented") - prevent_modify_annotations: bool | None = Field( - False, description="Whether modification of annotations is prevented" - ) - prevent_printing: bool | None = Field(False, description="Whether printing of the document is prevented") - prevent_printing_faithful: bool | None = Field(False, description="Whether faithful printing is prevented") + prevent_fill_in_form: bool = Field(False, description="Whether form filling is prevented") + prevent_modify: bool = Field(False, description="Whether document modification is prevented") + prevent_modify_annotations: bool = Field(False, description="Whether modification of annotations is prevented") + prevent_printing: bool = Field(False, description="Whether printing of the document is prevented") + prevent_printing_faithful: bool = Field(False, description="Whether faithful printing is prevented") class Alphabet(StrEnum): + """ + The selected alphabet of the stamp text + """ + roman = "roman" arabic = "arabic" japanese = "japanese" @@ -126,92 +140,150 @@ class Alphabet(StrEnum): thai = "thai" +class CustomMargin1(StrEnum): + """ + Specifies the margin size for the stamp. + """ + + small = "small" + medium = "medium" + large = "large" + x_large = "x-large" + + +class Position1(IntEnum): + """ + Position for stamp placement based on a 1-9 grid (1: bottom-left, 2: bottom-center, 3: bottom-right, 4: middle-left, 5: middle-center, 6: middle-right, 7: top-left, 8: top-center, 9: top-right) + """ + + integer_1 = 1 + integer_2 = 2 + integer_3 = 3 + integer_4 = 4 + integer_5 = 5 + integer_6 = 6 + integer_7 = 7 + integer_8 = 8 + integer_9 = 9 + + class StampType(StrEnum): + """ + The stamp type (text or image) + """ + text = "text" image = "image" class AddStampParams(ApiModel): - alphabet: Alphabet | None = Field(Alphabet.roman, description="The selected alphabet of the stamp text") - custom_color: str | None = Field("#d3d3d3", description="The color of the stamp text") - custom_margin: CustomMargin | None = Field( - CustomMargin.medium, description="Specifies the margin size for the stamp." - ) - font_size: float | None = Field(40, description="The font size of the stamp text and image in points.") - opacity: float | None = Field(0.5, description="The opacity of the stamp (0.0 - 1.0)") - override_x: float | None = Field( + alphabet: Alphabet = Field(Alphabet.roman, description="The selected alphabet of the stamp text") + custom_color: str = Field("#d3d3d3", description="The color of the stamp text") + custom_margin: CustomMargin1 = Field(CustomMargin1.medium, description="Specifies the margin size for the stamp.") + font_size: float = Field(40, description="The font size of the stamp text and image in points.") + opacity: float = Field(0.5, description="The opacity of the stamp (0.0 - 1.0)") + override_x: float = Field( -1, description="Override X coordinate for stamp placement. If set, it will override the position-based calculation. Negative value means no override.", ) - override_y: float | None = Field( + override_y: float = Field( -1, description="Override Y coordinate for stamp placement. If set, it will override the position-based calculation. Negative value means no override.", ) - page_numbers: str | None = Field( + page_numbers: str = Field( "all", description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", ) - position: Position | None = Field( - Position.integer_8, + position: Position1 = Field( + Position1.integer_8, description="Position for stamp placement based on a 1-9 grid (1: bottom-left, 2: bottom-center, 3: bottom-right, 4: middle-left, 5: middle-center, 6: middle-right, 7: top-left, 8: top-center, 9: top-right)", ) - rotation: float | None = Field(0, description="The rotation of the stamp in degrees") - stamp_text: str | None = Field("Stirling Software", description="The stamp text") - stamp_type: StampType | None = Field(None, description="The stamp type (text or image)") + rotation: float = Field(0, description="The rotation of the stamp in degrees") + stamp_text: str = Field("Stirling Software", description="The stamp text") + stamp_type: StampType = Field(..., description="The stamp type (text or image)") + + +class Alphabet1(StrEnum): + """ + The selected alphabet + """ + + roman = "roman" + arabic = "arabic" + japanese = "japanese" + korean = "korean" + chinese = "chinese" + thai = "thai" class WatermarkType(StrEnum): + """ + The watermark type (text or image) + """ + text = "text" image = "image" class AddWatermarkParams(ApiModel): - alphabet: Alphabet | None = Field(Alphabet.roman, description="The selected alphabet") - convert_pdf_to_image: bool | None = Field(False, description="Convert the redacted PDF to an image") - custom_color: str | None = Field("#d3d3d3", description="The color for watermark") - font_size: float | None = Field(30, description="The font size of the watermark text", ge=1.0) - height_spacer: int | None = Field(50, description="The height spacer between watermark elements", ge=0) - opacity: float | None = Field(0.5, description="The opacity of the watermark (0.0 - 1.0)") - rotation: float | None = Field(0, description="The rotation of the watermark in degrees") - watermark_text: str | None = Field("Stirling Software", description="The watermark text") - watermark_type: WatermarkType | None = Field(None, description="The watermark type (text or image)") - width_spacer: int | None = Field(50, description="The width spacer between watermark elements", ge=0) + alphabet: Alphabet1 = Field(Alphabet1.roman, description="The selected alphabet") + convert_pdf_to_image: bool = Field(False, description="Convert the redacted PDF to an image") + custom_color: str = Field("#d3d3d3", description="The color for watermark") + font_size: float = Field(30, description="The font size of the watermark text", ge=1.0) + height_spacer: int = Field(50, description="The height spacer between watermark elements", ge=0) + opacity: float = Field(0.5, description="The opacity of the watermark (0.0 - 1.0)") + rotation: float = Field(0, description="The rotation of the watermark in degrees") + watermark_text: str = Field("Stirling Software", description="The watermark text") + watermark_type: WatermarkType = Field(..., description="The watermark type (text or image)") + width_spacer: int = Field(50, description="The width spacer between watermark elements", ge=0) class AutoRedactParams(ApiModel): - convert_pdf_to_image: bool | None = Field(False, description="Convert the redacted PDF to an image") - custom_padding: float | None = Field(None, description="Custom padding for redaction") - list_of_text: str | None = Field("text,text2", description="List of text to redact from the PDF") - redact_color: str | None = Field("#000000", description="The color for redaction") - use_regex: bool | None = Field(False, description="Whether to use regex for the listOfText") - whole_word_search: bool | None = Field(False, description="Whether to use whole word search") + convert_pdf_to_image: bool = Field(False, description="Convert the redacted PDF to an image") + custom_padding: float = Field(..., description="Custom padding for redaction") + list_of_text: str = Field("text,text2", description="List of text to redact from the PDF") + redact_color: str = Field("#000000", description="The color for redaction") + use_regex: bool = Field(False, description="Whether to use regex for the listOfText") + whole_word_search: bool = Field(False, description="Whether to use whole word search") class AutoRenameParams(ApiModel): - use_first_text_as_fallback: bool | None = Field( + use_first_text_as_fallback: bool = Field( False, description="Flag indicating whether to use the first text as a fallback if no suitable title is found. Defaults to false.", ) class AutoSplitPdfParams(ApiModel): - duplex_mode: bool | None = Field( + duplex_mode: bool = Field( False, description="Flag indicating if the duplex mode is active, where the page after the divider also gets removed.", ) class DuplexPass(StrEnum): + """ + For manual duplex: which pass to generate + """ + both = "BOTH" first = "FIRST" second = "SECOND" class PagesPerSheet(Enum): + """ + The number of pages per side for booklet printing (always 2 for proper booklet). + """ + number_2 = 2 class SpineLocation(StrEnum): + """ + The spine location for the booklet. + """ + left = "LEFT" right = "RIGHT" @@ -220,31 +292,31 @@ class BookletImpositionParams(ApiModel): add_border: bool | None = Field(None, description="Boolean for if you wish to add border around the pages") add_gutter: bool | None = Field(None, description="Add gutter margin (inner margin for binding)") double_sided: bool | None = Field(None, description="Generate both front and back sides (double-sided printing)") - duplex_pass: DuplexPass | None = Field(DuplexPass.both, description="For manual duplex: which pass to generate") + duplex_pass: DuplexPass = Field(DuplexPass.both, description="For manual duplex: which pass to generate") flip_on_short_edge: bool | None = Field( None, description="Flip back sides for short-edge duplex printing (default is long-edge)" ) - gutter_size: float | None = Field(12, description="Gutter margin size in points (used when addGutter is true)") - pages_per_sheet: PagesPerSheet | None = Field( + gutter_size: float = Field(12, description="Gutter margin size in points (used when addGutter is true)") + pages_per_sheet: PagesPerSheet = Field( PagesPerSheet.number_2, description="The number of pages per side for booklet printing (always 2 for proper booklet).", ) - spine_location: SpineLocation | None = Field(SpineLocation.left, description="The spine location for the booklet.") + spine_location: SpineLocation = Field(SpineLocation.left, description="The spine location for the booklet.") class CbrToPdfParams(ApiModel): - optimize_for_ebook: bool | None = Field( - False, description="Optimize the output PDF for ebook reading using Ghostscript" - ) + optimize_for_ebook: bool = Field(False, description="Optimize the output PDF for ebook reading using Ghostscript") class CbzToPdfParams(ApiModel): - optimize_for_ebook: bool | None = Field( - False, description="Optimize the output PDF for ebook reading using Ghostscript" - ) + optimize_for_ebook: bool = Field(False, description="Optimize the output PDF for ebook reading using Ghostscript") class CertType(StrEnum): + """ + The type of the digital certificate + """ + pem = "PEM" pkcs12 = "PKCS12" pfx = "PFX" @@ -253,28 +325,34 @@ class CertType(StrEnum): class CertSignParams(ApiModel): - cert_type: CertType | None = Field(None, description="The type of the digital certificate") - location: str | None = Field("SPDF", description="The location where the PDF is signed") - name: str | None = Field("SPDF", description="The name of the signer") - page_number: int | None = Field( + cert_type: CertType = Field(..., description="The type of the digital certificate") + location: str = Field("SPDF", description="The location where the PDF is signed") + name: str = Field("SPDF", description="The name of the signer") + page_number: int = Field( 1, description="The page number where the signature should be visible. This is required if showSignature is set to true", ) password: SecretStr | None = Field(None, description="The password for the keystore or the private key") - reason: str | None = Field("Signed by SPDF", description="The reason for signing the PDF") - show_logo: bool | None = Field( - True, description="Whether to visually show a signature logo along with the signature" - ) - show_signature: bool | None = Field(False, description="Whether to visually show the signature in the PDF file") + reason: str = Field("Signed by SPDF", description="The reason for signing the PDF") + show_logo: bool = Field(True, description="Whether to visually show a signature logo along with the signature") + show_signature: bool = Field(False, description="Whether to visually show the signature in the PDF file") class LineArtEdgeLevel(IntEnum): + """ + Edge detection strength to use for line art conversion (1-3). This maps to ImageMagick's -edge radius. + """ + integer_1 = 1 integer_2 = 2 integer_3 = 3 class OptimizeLevel(IntEnum): + """ + The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality. + """ + integer_1 = 1 integer_2 = 2 integer_3 = 3 @@ -287,24 +365,22 @@ class OptimizeLevel(IntEnum): class CompressPdfParams(ApiModel): - expected_output_size: str | None = Field("25KB", description="The expected output size, e.g. '100MB', '25KB', etc.") - grayscale: bool | None = Field(False, description="Whether to convert the PDF to grayscale. Default is false.") - line_art: bool | None = Field( + expected_output_size: str = Field("25KB", description="The expected output size, e.g. '100MB', '25KB', etc.") + grayscale: bool = Field(False, description="Whether to convert the PDF to grayscale. Default is false.") + line_art: bool = Field( False, description="Whether to convert images to high-contrast line art using ImageMagick. Default is false." ) - line_art_edge_level: LineArtEdgeLevel | None = Field( + line_art_edge_level: LineArtEdgeLevel = Field( LineArtEdgeLevel.integer_1, description="Edge detection strength to use for line art conversion (1-3). This maps to ImageMagick's -edge radius.", ) - line_art_threshold: float | None = Field(55, description="Threshold to use for line art conversion (0-100).") - linearize: bool | None = Field( - False, description="Whether to linearize the PDF for faster web viewing. Default is false." - ) - normalize: bool | None = Field( + line_art_threshold: float = Field(55, description="Threshold to use for line art conversion (0-100).") + linearize: bool = Field(False, description="Whether to linearize the PDF for faster web viewing. Default is false.") + normalize: bool = Field( False, description="Whether to normalize the PDF content for better compatibility. Default is false." ) - optimize_level: OptimizeLevel | None = Field( - None, + optimize_level: OptimizeLevel = Field( + ..., description="The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality.", ) @@ -321,40 +397,56 @@ class CropParams(ApiModel): class DeleteAttachmentParams(ApiModel): - attachment_name: str | None = Field(None, description="The name of the attachment to delete") + attachment_name: str = Field(..., description="The name of the attachment to delete") class EmbedAllFonts(Enum): + """ + Embed all fonts from the eBook into the generated PDF + """ + boolean_true = True boolean_false = False class IncludePageNumbers(Enum): + """ + Add page numbers to the generated PDF + """ + boolean_true = True boolean_false = False class IncludeTableOfContents(Enum): + """ + Add a generated table of contents to the resulting PDF + """ + boolean_true = True boolean_false = False class OptimizeForEbook(Enum): + """ + Optimize the PDF for eBook reading (smaller file size, better rendering on eInk devices) + """ + boolean_true = True boolean_false = False class EbookToPdfParams(ApiModel): - embed_all_fonts: EmbedAllFonts | None = Field( + embed_all_fonts: EmbedAllFonts = Field( EmbedAllFonts.boolean_false, description="Embed all fonts from the eBook into the generated PDF" ) - include_page_numbers: IncludePageNumbers | None = Field( + include_page_numbers: IncludePageNumbers = Field( IncludePageNumbers.boolean_false, description="Add page numbers to the generated PDF" ) - include_table_of_contents: IncludeTableOfContents | None = Field( + include_table_of_contents: IncludeTableOfContents = Field( IncludeTableOfContents.boolean_false, description="Add a generated table of contents to the resulting PDF" ) - optimize_for_ebook: OptimizeForEbook | None = Field( + optimize_for_ebook: OptimizeForEbook = Field( OptimizeForEbook.boolean_false, description="Optimize the PDF for eBook reading (smaller file size, better rendering on eInk devices)", ) @@ -374,20 +466,24 @@ class EditTableOfContentsParams(ApiModel): class EditTextOperation(ApiModel): + """ + Ordered list of find/replace operations. Each replaces every occurrence on the selected pages, in order; later operations see the result of earlier ones (so 'foo'->'foos' then 'foos'->'bars' turns 'foo' into 'bars'). + """ + find: str = Field(..., description="The literal text to find.") replace: str = Field(..., description="The replacement text. May be empty to delete the matched text.") class EditTextParams(ApiModel): - edits: list[EditTextOperation] | None = Field( - None, + edits: list[EditTextOperation] = Field( + ..., description="Ordered list of find/replace operations. Each replaces every occurrence on the selected pages, in order; later operations see the result of earlier ones (so 'foo'->'foos' then 'foos'->'bars' turns 'foo' into 'bars').", ) - page_numbers: str | None = Field( + page_numbers: str = Field( "all", description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", ) - whole_word_search: bool | None = Field( + whole_word_search: bool = Field( False, description="Whether matches must be whole words (boundaries determined by non-word characters)" ) @@ -412,25 +508,29 @@ class EmlToPdfParams(ApiModel): class ExtractImageScansParams(ApiModel): - angle_threshold: int | None = Field(5, description="The angle threshold for the image scan extraction") - border_size: int | None = Field(1, description="The border size for the image scan extraction") - min_area: int | None = Field(8000, description="The minimum area for the image scan extraction") - min_contour_area: int | None = Field(500, description="The minimum contour area for the image scan extraction") - tolerance: int | None = Field(20, description="The tolerance for the image scan extraction") + angle_threshold: int = Field(5, description="The angle threshold for the image scan extraction") + border_size: int = Field(1, description="The border size for the image scan extraction") + min_area: int = Field(8000, description="The minimum area for the image scan extraction") + min_contour_area: int = Field(500, description="The minimum contour area for the image scan extraction") + tolerance: int = Field(20, description="The tolerance for the image scan extraction") class Format(StrEnum): + """ + The output image format e.g., 'png', 'jpeg', or 'gif' + """ + png = "png" jpeg = "jpeg" gif = "gif" class ExtractImagesParams(ApiModel): - format: Format | None = Field(Format.png, description="The output image format e.g., 'png', 'jpeg', or 'gif'") + format: Format = Field(Format.png, description="The output image format e.g., 'png', 'jpeg', or 'gif'") class FlattenParams(ApiModel): - flatten_only_forms: bool | None = Field( + flatten_only_forms: bool = Field( False, description="True to flatten only the forms, false to flatten full PDF (Convert page to image)" ) render_dpi: int | None = Field( @@ -439,32 +539,44 @@ class FlattenParams(ApiModel): class HtmlToPdfParams(ApiModel): - zoom: float | None = Field(1, description="Zoom level for displaying the website. Default is '1'.") + zoom: float = Field(1, description="Zoom level for displaying the website. Default is '1'.") class ColorType(StrEnum): + """ + The color type of the output image(s) + """ + color = "color" greyscale = "greyscale" blackwhite = "blackwhite" class FitOption(StrEnum): + """ + Option to determine how the image will fit onto the page + """ + fill_page = "fillPage" fit_document_to_image = "fitDocumentToImage" maintain_aspect_ratio = "maintainAspectRatio" class ImgToPdfParams(ApiModel): - auto_rotate: bool | None = Field( + auto_rotate: bool = Field( False, description="Whether to automatically rotate the images to better fit the PDF page" ) - color_type: ColorType | None = Field(ColorType.color, description="The color type of the output image(s)") - fit_option: FitOption | None = Field( + color_type: ColorType = Field(ColorType.color, description="The color type of the output image(s)") + fit_option: FitOption = Field( FitOption.fill_page, description="Option to determine how the image will fit onto the page" ) class SortType(StrEnum): + """ + The type of sorting to be applied on the input files before merging. + """ + order_provided = "orderProvided" by_file_name = "byFileName" by_date_modified = "byDateModified" @@ -477,35 +589,51 @@ class MergePdfsParams(ApiModel): None, description="JSON array of client-provided IDs for each uploaded file (same order as fileInput)" ) file_order: str | None = None - generate_toc: bool | None = Field( + generate_toc: bool = Field( False, description="Flag indicating whether to generate a table of contents for the merged PDF. If true, a table of contents will be created using the input filenames as chapter names.", ) - remove_cert_sign: bool | None = Field( + remove_cert_sign: bool = Field( True, description="Flag indicating whether to remove certification signatures from the merged PDF. If true, all certification signatures will be removed from the final merged document.", ) - sort_type: SortType | None = Field( + sort_type: SortType = Field( SortType.order_provided, description="The type of sorting to be applied on the input files before merging." ) class Arrangement(StrEnum): + """ + The arrangement of pages on the sheet: BY_ROWS fills pages row by row, while BY_COLUMNS fills pages column by column. + """ + by_rows = "BY_ROWS" by_columns = "BY_COLUMNS" class Mode(StrEnum): + """ + Input mode: DEFAULT uses pagesPerSheet; CUSTOM uses explicit cols x rows. + """ + default = "DEFAULT" custom = "CUSTOM" class Orientation(StrEnum): + """ + The orientation of the output PDF pages + """ + portrait = "PORTRAIT" landscape = "LANDSCAPE" class PagesPerSheet1(IntEnum): + """ + The number of pages to fit onto a single sheet in the output PDF. + """ + integer_2 = 2 integer_4 = 4 integer_9 = 9 @@ -513,55 +641,67 @@ class PagesPerSheet1(IntEnum): class ReadingDirection(StrEnum): + """ + The direction in which pages are arranged on the sheet: LTR (left-to-right) or RTL (right-to-left). + """ + ltr = "LTR" rtl = "RTL" class MultiPageLayoutParams(ApiModel): add_border: bool | None = Field(None, description="Boolean for if you wish to add border around the pages") - arrangement: Arrangement | None = Field( + arrangement: Arrangement = Field( Arrangement.by_rows, description="The arrangement of pages on the sheet: BY_ROWS fills pages row by row, while BY_COLUMNS fills pages column by column.", ) - border_width: float | None = Field( + border_width: float = Field( 1, description="Border width (in points) to apply around each page when merging", examples=[2], ge=0.0 ) - bottom_margin: float | None = Field( + bottom_margin: float = Field( 0, description="Bottom margin (in points) to apply to the output pages when merging", examples=[200], ge=0.0 ) - cols: float | None = Field(2, description="Number of columns", examples=[2], ge=1.0, le=300.0) - inner_margin: float | None = Field( + cols: float = Field(2, description="Number of columns", examples=[2], ge=1.0, le=300.0) + inner_margin: float = Field( 0, description="Inner margin (in points) to apply around each page when merging", examples=[200], ge=0.0 ) - left_margin: float | None = Field( + left_margin: float = Field( 0, description="Left margin (in points) to apply to the output pages when merging", examples=[200], ge=0.0 ) - mode: Mode | None = Field( + mode: Mode = Field( Mode.default, description="Input mode: DEFAULT uses pagesPerSheet; CUSTOM uses explicit cols x rows." ) - orientation: Orientation | None = Field(Orientation.portrait, description="The orientation of the output PDF pages") + orientation: Orientation = Field(Orientation.portrait, description="The orientation of the output PDF pages") pages_per_sheet: PagesPerSheet1 | None = Field( None, description="The number of pages to fit onto a single sheet in the output PDF." ) - reading_direction: ReadingDirection | None = Field( + reading_direction: ReadingDirection = Field( ReadingDirection.ltr, description="The direction in which pages are arranged on the sheet: LTR (left-to-right) or RTL (right-to-left).", ) - right_margin: float | None = Field( + right_margin: float = Field( 0, description="Right margin (in points) to apply to the output pages when merging", examples=[200], ge=0.0 ) - rows: float | None = Field(1, description="Number of rows", examples=[3], ge=1.0, le=300.0) - top_margin: float | None = Field( + rows: float = Field(1, description="Number of rows", examples=[3], ge=1.0, le=300.0) + top_margin: float = Field( 0, description="Top margin (in points) to apply to the output pages when merging", examples=[200], ge=0.0 ) class OcrRenderType(StrEnum): + """ + Specify the OCR render type, either 'hocr' or 'sandwich' + """ + hocr = "hocr" sandwich = "sandwich" class OcrType(StrEnum): + """ + Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal' + """ + skip_text = "skip-text" force_ocr = "force-ocr" normal = "Normal" @@ -571,26 +711,30 @@ class OcrPdfParams(ApiModel): clean: bool | None = Field(None, description="Clean the input file if set to true") clean_final: bool | None = Field(None, description="Clean the final output if set to true") deskew: bool | None = Field(None, description="Deskew the input file if set to true") - languages: list[str] | None = Field( - ["eng"], description="List of languages to use in OCR processing, e.g., 'eng', 'deu'" - ) - ocr_render_type: OcrRenderType | None = Field( + languages: list[str] = Field(["eng"], description="List of languages to use in OCR processing, e.g., 'eng', 'deu'") + ocr_render_type: OcrRenderType = Field( OcrRenderType.hocr, description="Specify the OCR render type, either 'hocr' or 'sandwich'" ) - ocr_type: OcrType | None = Field( - None, description="Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'" - ) + ocr_type: OcrType = Field(..., description="Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'") remove_images_after: bool | None = Field(None, description="Remove images from the output PDF if set to true") sidecar: bool | None = Field(None, description="Include OCR text in a sidecar text file if set to true") class OverlayMode(StrEnum): + """ + The mode of overlaying: 'SequentialOverlay' for sequential application, 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay' for fixed repetition based on provided counts + """ + sequential_overlay = "SequentialOverlay" interleaved_overlay = "InterleavedOverlay" fixed_repeat_overlay = "FixedRepeatOverlay" class OverlayPosition(Enum): + """ + Overlay position 0 is Foregound, 1 is Background + """ + number_0 = 0 number_1 = 1 @@ -600,44 +744,46 @@ class OverlayPdfsParams(ApiModel): None, description="An array of integers specifying the number of times each corresponding overlay file should be applied in the 'FixedRepeatOverlay' mode. This should match the length of the overlayFiles array.", ) - overlay_files: list[bytes] | None = Field( - None, + overlay_files: list[bytes] = Field( + ..., description="An array of PDF files to be used as overlays on the base PDF. The order in these files is applied based on the selected mode.", ) - overlay_mode: OverlayMode | None = Field( - None, + overlay_mode: OverlayMode = Field( + ..., description="The mode of overlaying: 'SequentialOverlay' for sequential application, 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay' for fixed repetition based on provided counts", ) - overlay_position: OverlayPosition | None = Field( - None, description="Overlay position 0 is Foregound, 1 is Background" - ) + overlay_position: OverlayPosition = Field(..., description="Overlay position 0 is Foregound, 1 is Background") class PdfToCbrParams(ApiModel): - dpi: int | None = Field( - None, description="The DPI (Dots Per Inch) for rendering PDF pages as images", examples=[150] - ) + dpi: int = Field(..., description="The DPI (Dots Per Inch) for rendering PDF pages as images", examples=[150]) class PdfToCbzParams(ApiModel): - dpi: int | None = Field( - None, description="The DPI (Dots Per Inch) for rendering PDF pages as images", examples=[150] - ) + dpi: int = Field(..., description="The DPI (Dots Per Inch) for rendering PDF pages as images", examples=[150]) class PdfToCsvParams(ApiModel): - page_numbers: str | None = Field( + page_numbers: str = Field( "all", description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", ) class DetectChapters(Enum): + """ + Detect headings that look like chapters and insert EPUB page breaks. + """ + boolean_true = True boolean_false = False class OutputFormat(StrEnum): + """ + Choose the output format for the ebook. + """ + epub = "EPUB" azw3 = "AZW3" epub_1 = "EPUB" @@ -645,6 +791,10 @@ class OutputFormat(StrEnum): class TargetDevice(StrEnum): + """ + Choose an output profile optimized for the reader device. + """ + tablet_phone_images = "TABLET_PHONE_IMAGES" kindle_eink_text = "KINDLE_EINK_TEXT" tablet_phone_images_1 = "TABLET_PHONE_IMAGES" @@ -652,16 +802,20 @@ class TargetDevice(StrEnum): class PdfToEpubParams(ApiModel): - detect_chapters: DetectChapters | None = Field( + detect_chapters: DetectChapters = Field( DetectChapters.boolean_true, description="Detect headings that look like chapters and insert EPUB page breaks." ) - output_format: OutputFormat | None = Field(OutputFormat.epub, description="Choose the output format for the ebook.") - target_device: TargetDevice | None = Field( + output_format: OutputFormat = Field(OutputFormat.epub, description="Choose the output format for the ebook.") + target_device: TargetDevice = Field( TargetDevice.tablet_phone_images, description="Choose an output profile optimized for the reader device." ) class ImageFormat(StrEnum): + """ + The output image format + """ + png = "png" jpeg = "jpeg" jpg = "jpg" @@ -670,28 +824,34 @@ class ImageFormat(StrEnum): class SingleOrMultiple(StrEnum): + """ + Choose between a single image containing all pages or separate images for each page + """ + single = "single" multiple = "multiple" class PdfToImgParams(ApiModel): - color_type: ColorType | None = Field(ColorType.color, description="The color type of the output image(s)") - dpi: int | None = Field(300, description="The DPI (dots per inch) for the output image(s)") - image_format: ImageFormat | None = Field(ImageFormat.png, description="The output image format") - include_annotations: bool | None = Field( - False, description="Include annotations such as comments in the output image(s)" - ) - page_numbers: str | None = Field( + color_type: ColorType = Field(ColorType.color, description="The color type of the output image(s)") + dpi: int = Field(300, description="The DPI (dots per inch) for the output image(s)") + image_format: ImageFormat = Field(ImageFormat.png, description="The output image format") + include_annotations: bool = Field(False, description="Include annotations such as comments in the output image(s)") + page_numbers: str = Field( "all", description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", ) - single_or_multiple: SingleOrMultiple | None = Field( + single_or_multiple: SingleOrMultiple = Field( SingleOrMultiple.multiple, description="Choose between a single image containing all pages or separate images for each page", ) class OutputFormat1(StrEnum): + """ + The output format type (PDF/A or PDF/X) + """ + pdfa = "pdfa" pdfa_1 = "pdfa-1" pdfa_2 = "pdfa-2" @@ -702,36 +862,52 @@ class OutputFormat1(StrEnum): class PdfToPdfaParams(ApiModel): - output_format: OutputFormat1 | None = Field(None, description="The output format type (PDF/A or PDF/X)") + output_format: OutputFormat1 = Field(..., description="The output format type (PDF/A or PDF/X)") strict: bool | None = Field( None, description="If true, the conversion will fail if the output is not perfectly compliant" ) class OutputFormat2(StrEnum): + """ + The output Presentation format + """ + ppt = "ppt" pptx = "pptx" odp = "odp" class PdfToPresentationParams(ApiModel): - output_format: OutputFormat2 | None = Field(None, description="The output Presentation format") + output_format: OutputFormat2 = Field(..., description="The output Presentation format") class PdfToTextEditorParams(ApiModel): - lightweight: bool | None = False + """ + Either upload a file or provide a server-side file ID + """ + + lightweight: bool = False class OutputFormat3(StrEnum): + """ + The output Text or RTF format + """ + rtf = "rtf" txt = "txt" class PdfToTextParams(ApiModel): - output_format: OutputFormat3 | None = Field(None, description="The output Text or RTF format") + output_format: OutputFormat3 = Field(..., description="The output Text or RTF format") class OutputFormat4(StrEnum): + """ + Target vector format extension + """ + eps = "eps" ps = "ps" pcl = "pcl" @@ -739,33 +915,52 @@ class OutputFormat4(StrEnum): class Prepress(Enum): + """ + Apply Ghostscript prepress settings + """ + boolean_true = True boolean_false = False class PdfToVectorParams(ApiModel): - output_format: OutputFormat4 | None = Field(OutputFormat4.eps, description="Target vector format extension") - prepress: Prepress | None = Field(Prepress.boolean_false, description="Apply Ghostscript prepress settings") + output_format: OutputFormat4 = Field(OutputFormat4.eps, description="Target vector format extension") + prepress: Prepress = Field(Prepress.boolean_false, description="Apply Ghostscript prepress settings") class OutputFormat5(StrEnum): + """ + The output Word document format + """ + doc = "doc" docx = "docx" odt = "odt" class PdfToWordParams(ApiModel): - output_format: OutputFormat5 | None = Field(None, description="The output Word document format") + output_format: OutputFormat5 = Field(..., description="The output Word document format") class PdfToXlsxParams(ApiModel): - page_numbers: str | None = Field( + page_numbers: str = Field( "all", description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", ) class CustomMode(StrEnum): + """ + The custom mode for page rearrangement. Valid values are: + CUSTOM: Uses order defined in PageNums DUPLICATE: Duplicate pages n times (if Page order defined as 4, then duplicates each page 4 times)REVERSE_ORDER: Reverses the order of all pages. + DUPLEX_SORT: Sorts pages as if all fronts were scanned then all backs in reverse (1, n, 2, n-1, ...). BOOKLET_SORT: Arranges pages for booklet printing (last, first, second, second last, ...). + ODD_EVEN_SPLIT: Splits and arranges pages into odd and even numbered pages. + REMOVE_FIRST: Removes the first page. + REMOVE_LAST: Removes the last page. + REMOVE_FIRST_AND_LAST: Removes both the first and the last pages. + + """ + custom = "CUSTOM" reverse_order = "REVERSE_ORDER" duplex_sort = "DUPLEX_SORT" @@ -783,13 +978,17 @@ class RearrangePagesParams(ApiModel): None, description="The custom mode for page rearrangement. Valid values are:\nCUSTOM: Uses order defined in PageNums DUPLICATE: Duplicate pages n times (if Page order defined as 4, then duplicates each page 4 times)REVERSE_ORDER: Reverses the order of all pages.\nDUPLEX_SORT: Sorts pages as if all fronts were scanned then all backs in reverse (1, n, 2, n-1, ...). BOOKLET_SORT: Arranges pages for booklet printing (last, first, second, second last, ...).\nODD_EVEN_SPLIT: Splits and arranges pages into odd and even numbered pages.\nREMOVE_FIRST: Removes the first page.\nREMOVE_LAST: Removes the last page.\nREMOVE_FIRST_AND_LAST: Removes both the first and the last pages.\n", ) - page_numbers: str | None = Field( + page_numbers: str = Field( "all", description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", ) class RedactionArea(ApiModel): + """ + A list of areas that should be redacted + """ + color: str | None = Field(None, description="The color used to redact the specified area.") height: float | None = Field(None, description="The height of the area to be redacted.") page: int | None = Field(None, description="The page on which the area should be redacted.") @@ -799,14 +998,14 @@ class RedactionArea(ApiModel): class RemoveBlanksParams(ApiModel): - threshold: int | None = Field(10, description="The threshold value to determine blank pages", ge=0, le=255) - white_percent: float | None = Field( + threshold: int = Field(10, description="The threshold value to determine blank pages", ge=0, le=255) + white_percent: float = Field( 99.9, description="The percentage of white color on a page to consider it as blank", ge=0.1, le=100.0 ) class RemovePagesParams(ApiModel): - page_numbers: str | None = Field( + page_numbers: str = Field( "all", description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", ) @@ -817,11 +1016,15 @@ class RemovePasswordParams(ApiModel): class RenameAttachmentParams(ApiModel): - attachment_name: str | None = Field(None, description="The current name of the attachment to rename") - new_name: str | None = Field(None, description="The new name for the attachment") + attachment_name: str = Field(..., description="The current name of the attachment to rename") + new_name: str = Field(..., description="The new name for the attachment") class HighContrastColorCombination(StrEnum): + """ + If HIGH_CONTRAST_COLOR option selected, then pick the default color option for text and background. + """ + white_text_on_black = "WHITE_TEXT_ON_BLACK" black_text_on_white = "BLACK_TEXT_ON_WHITE" yellow_text_on_black = "YELLOW_TEXT_ON_BLACK" @@ -833,6 +1036,10 @@ class HighContrastColorCombination(StrEnum): class ReplaceAndInvertOption(StrEnum): + """ + Replace and Invert color options of a pdf. + """ + high_contrast_color = "HIGH_CONTRAST_COLOR" custom_color = "CUSTOM_COLOR" full_inversion = "FULL_INVERSION" @@ -848,11 +1055,11 @@ class ReplaceInvertPdfParams(ApiModel): None, description="If CUSTOM_COLOR option selected, then pick the custom color for background. Expected color value should be 24bit decimal value of a color", ) - high_contrast_color_combination: HighContrastColorCombination | None = Field( + high_contrast_color_combination: HighContrastColorCombination = Field( HighContrastColorCombination.white_text_on_black, description="If HIGH_CONTRAST_COLOR option selected, then pick the default color option for text and background.", ) - replace_and_invert_option: ReplaceAndInvertOption | None = Field( + replace_and_invert_option: ReplaceAndInvertOption = Field( ReplaceAndInvertOption.high_contrast_color, description="Replace and Invert color options of a pdf." ) text_color: str | None = Field( @@ -862,6 +1069,10 @@ class ReplaceInvertPdfParams(ApiModel): class Angle(IntEnum): + """ + The clockwise angle by which to rotate all pages in the PDF file. Must be a multiple of 90. + """ + integer_0 = 0 integer_90 = 90 integer_180 = 180 @@ -869,21 +1080,25 @@ class Angle(IntEnum): class RotatePdfParams(ApiModel): - angle: Angle | None = Field( - None, description="The clockwise angle by which to rotate the PDF file. Must be a multiple of 90." + angle: Angle = Field( + ..., description="The clockwise angle by which to rotate all pages in the PDF file. Must be a multiple of 90." ) class SanitizePdfParams(ApiModel): - remove_embedded_files: bool | None = Field(True, description="Remove embedded files from the PDF") - remove_fonts: bool | None = Field(False, description="Remove fonts from the PDF") - remove_java_script: bool | None = Field(True, description="Remove JavaScript actions from the PDF") - remove_links: bool | None = Field(False, description="Remove links from the PDF") - remove_metadata: bool | None = Field(False, description="Remove document info metadata from the PDF") - remove_xmp_metadata: bool | None = Field(False, description="Remove XMP metadata from the PDF") + remove_embedded_files: bool = Field(True, description="Remove embedded files from the PDF") + remove_fonts: bool = Field(False, description="Remove fonts from the PDF") + remove_java_script: bool = Field(True, description="Remove JavaScript actions from the PDF") + remove_links: bool = Field(False, description="Remove links from the PDF") + remove_metadata: bool = Field(False, description="Remove document info metadata from the PDF") + remove_xmp_metadata: bool = Field(False, description="Remove XMP metadata from the PDF") class PageSize(StrEnum): + """ + The scale of pages in the output PDF. Acceptable values are A0-A6, LETTER, LEGAL, KEEP. + """ + a0 = "A0" a1 = "A1" a2 = "A2" @@ -897,26 +1112,38 @@ class PageSize(StrEnum): class ScalePagesParams(ApiModel): - page_size: PageSize | None = Field( - None, description="The scale of pages in the output PDF. Acceptable values are A0-A6, LETTER, LEGAL, KEEP." + page_size: PageSize = Field( + ..., description="The scale of pages in the output PDF. Acceptable values are A0-A6, LETTER, LEGAL, KEEP." ) - scale_factor: float | None = Field( + scale_factor: float = Field( 1, description="The scale of the content on the pages of the output PDF. Acceptable values are floats.", ge=0.0 ) class Colorspace(StrEnum): + """ + Colorspace for output image + """ + grayscale = "grayscale" color = "color" class Quality(StrEnum): + """ + Scan quality preset + """ + low = "low" medium = "medium" high = "high" class Rotation(StrEnum): + """ + Rotation preset + """ + none = "none" slight = "slight" moderate = "moderate" @@ -931,11 +1158,11 @@ class ScannerEffectParams(ApiModel): colorspace: Colorspace | None = Field(None, description="Colorspace for output image", examples=["grayscale"]) contrast: float | None = Field(None, description="Contrast multiplier (1.0 = no change)", examples=[1.0]) noise: float | None = Field(None, description="Noise amount (0 = none, higher = more noise)", examples=[8.0]) - quality: Quality | None = Field(None, description="Scan quality preset", examples=["high"]) + quality: Quality = Field(..., description="Scan quality preset", examples=["high"]) resolution: int | None = Field(None, description="Rendering resolution in DPI", examples=[300]) rotate: int | None = Field(None, description="Base rotation in degrees", examples=[0]) rotate_variance: int | None = Field(None, description="Random rotation variance in degrees", examples=[2]) - rotation: Rotation | None = Field(None, description="Rotation preset", examples=["none"]) + rotation: Rotation = Field(..., description="Rotation preset", examples=["none"]) rotation_value: int | None = None yellowish: bool | None = Field(None, description="Simulate yellowed paper", examples=[False]) @@ -962,15 +1189,19 @@ class SessionsParams(ApiModel): class SplitBySizeOrCountParams(ApiModel): - split_type: int | None = Field( + split_type: int = Field( 0, description="Determines the type of split: 0 for size, 1 for page count, 2 for document count" ) - split_value: str | None = Field( + split_value: str = Field( "10MB", description="Value for split: size in MB (e.g., '10MB') or number of pages (e.g., '5')" ) class PageSize1(StrEnum): + """ + Target page size for output chunks (e.g., 'A4', 'Letter', 'A3') + """ + a4 = "A4" letter = "Letter" a3 = "A3" @@ -980,28 +1211,36 @@ class PageSize1(StrEnum): class SplitForPosterPrintParams(ApiModel): - page_size: PageSize1 | None = Field( - None, description="Target page size for output chunks (e.g., 'A4', 'Letter', 'A3')" - ) - right_to_left: bool | None = Field(False, description="Split right-to-left instead of left-to-right") + page_size: PageSize1 = Field(..., description="Target page size for output chunks (e.g., 'A4', 'Letter', 'A3')") + right_to_left: bool = Field(False, description="Split right-to-left instead of left-to-right") xfactor: int | None = None yfactor: int | None = None class SplitPagesParams(ApiModel): - page_numbers: str | None = Field( + page_numbers: str = Field( "all", description='Split points - page numbers after which the PDF will be cut. For example, `"2"` produces two documents (pages 1-2 and pages 3+); `"2,5"` produces three (pages 1-2, 3-5, 6+). Supports ranges (e.g. `"1,3,5-9"` splits after pages 1, 3, 5, 6, 7, 8, 9, yielding 8 documents), `"all"` (split after every page), or functions like `"2n+1"`, `"3n"`, `"6n-5"`.', ) class SplitPdfByChaptersParams(ApiModel): - allow_duplicates: bool | None = Field(False, description="Whether to allow duplicates or not") - bookmark_level: int | None = Field(0, description="Maximum bookmark level required", ge=0) - include_metadata: bool | None = Field(False, description="Whether to include Metadata or not") + allow_duplicates: bool = Field(False, description="Whether to allow duplicates or not") + bookmark_level: int = Field(0, description="Maximum bookmark level required", ge=0) + include_metadata: bool = Field(False, description="Whether to include Metadata or not") class SplitMode(StrEnum): + """ + Modes for page split. Valid values are: + SPLIT_ALL_EXCEPT_FIRST_AND_LAST: Splits all except the first and the last pages. + SPLIT_ALL_EXCEPT_FIRST: Splits all except the first page. + SPLIT_ALL_EXCEPT_LAST: Splits all except the last page. + SPLIT_ALL: Splits all pages. + CUSTOM: Custom split. + + """ + custom = "CUSTOM" split_all_except_first_and_last = "SPLIT_ALL_EXCEPT_FIRST_AND_LAST" split_all_except_first = "SPLIT_ALL_EXCEPT_FIRST" @@ -1010,33 +1249,35 @@ class SplitMode(StrEnum): class SplitPdfBySectionsParams(ApiModel): - horizontal_divisions: int | None = Field( - 0, description="Number of horizontal divisions for each PDF page", ge=0, le=50 - ) - merge: bool | None = Field(False, description="Merge the split documents into a single PDF") - page_numbers: str | None = Field("SPLIT_ALL", description="Pages to be split by section") + horizontal_divisions: int = Field(0, description="Number of horizontal divisions for each PDF page", ge=0, le=50) + merge: bool = Field(False, description="Merge the split documents into a single PDF") + page_numbers: str = Field("SPLIT_ALL", description="Pages to be split by section") split_mode: SplitMode | None = Field( None, description="Modes for page split. Valid values are:\nSPLIT_ALL_EXCEPT_FIRST_AND_LAST: Splits all except the first and the last pages.\nSPLIT_ALL_EXCEPT_FIRST: Splits all except the first page.\nSPLIT_ALL_EXCEPT_LAST: Splits all except the last page.\nSPLIT_ALL: Splits all pages.\nCUSTOM: Custom split.\n", ) - vertical_divisions: int | None = Field(1, description="Number of vertical divisions for each PDF page", ge=0, le=50) + vertical_divisions: int = Field(1, description="Number of vertical divisions for each PDF page", ge=0, le=50) class SvgToPdfParams(ApiModel): - combine_into_single_pdf: bool | None = Field( + combine_into_single_pdf: bool = Field( False, description="Whether to combine all SVG files into a single PDF (each SVG as a separate page) or create separate PDF files for each SVG.", ) class TimestampPdfParams(ApiModel): - tsa_url: str | None = Field( + tsa_url: str = Field( "http://timestamp.digicert.com", description="URL of the RFC 3161 Time Stamp Authority (TSA) server. Must be one of the built-in presets (DigiCert, Sectigo, SSL.com, FreeTSA, MeSign) or an admin-configured URL in settings.yml (security.timestamp.customTsaUrls). If omitted, the server default is used.", ) class Trapped(StrEnum): + """ + The trapped status of the document + """ + true = "True" false = "False" unknown = "Unknown" @@ -1047,28 +1288,28 @@ class UpdateMetadataParams(ApiModel): None, description="Map list of key and value of custom parameters. Note these must start with customKey and customValue if they are non-standard", ) - author: str | None = Field("author", description="The author of the document") - creation_date: str | None = Field( + author: str = Field("author", description="The author of the document") + creation_date: str = Field( "2023/10/01 12:00:00", description="The creation date of the document (format: yyyy/MM/dd HH:mm:ss)", pattern="yyyy/MM/dd HH:mm:ss", ) - creator: str | None = Field("creator", description="The creator of the document") - delete_all: bool | None = Field(False, description="Delete all metadata if set to true") - keywords: str | None = Field("keywords", description="The keywords for the document") - modification_date: str | None = Field( + creator: str = Field("creator", description="The creator of the document") + delete_all: bool = Field(False, description="Delete all metadata if set to true") + keywords: str = Field("keywords", description="The keywords for the document") + modification_date: str = Field( "2023/10/01 12:00:00", description="The modification date of the document (format: yyyy/MM/dd HH:mm:ss)", pattern="yyyy/MM/dd HH:mm:ss", ) - producer: str | None = Field("producer", description="The producer of the document") - subject: str | None = Field("subject", description="The subject of the document") - title: str | None = Field("title", description="The title of the document") - trapped: Trapped | None = Field(Trapped.false, description="The trapped status of the document") + producer: str = Field("producer", description="The producer of the document") + subject: str = Field("subject", description="The subject of the document") + title: str = Field("title", description="The title of the document") + trapped: Trapped = Field(Trapped.false, description="The trapped status of the document") class UrlToPdfParams(ApiModel): - url_input: str | None = Field(None, description="The input URL to be converted to a PDF file") + url_input: str = Field(..., description="The input URL to be converted to a PDF file") class ValidateCertificateParams(ApiModel): @@ -1077,6 +1318,10 @@ class ValidateCertificateParams(ApiModel): class OutputFormat6(StrEnum): + """ + Target vector format extension + """ + eps = "eps" ps = "ps" pcl = "pcl" @@ -1084,18 +1329,18 @@ class OutputFormat6(StrEnum): class VectorToPdfParams(ApiModel): - output_format: OutputFormat6 | None = Field(OutputFormat6.eps, description="Target vector format extension") - prepress: Prepress | None = Field(Prepress.boolean_false, description="Apply Ghostscript prepress settings") + output_format: OutputFormat6 = Field(OutputFormat6.eps, description="Target vector format extension") + prepress: Prepress = Field(Prepress.boolean_false, description="Apply Ghostscript prepress settings") class RedactParams(ApiModel): - convert_pdf_to_image: bool | None = Field(False, description="Convert the redacted PDF to an image") - page_numbers: str | None = Field( + convert_pdf_to_image: bool = Field(False, description="Convert the redacted PDF to an image") + page_numbers: str = Field( "all", description="The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')", ) - page_redaction_color: str | None = Field("#000000", description="The color used to fully redact certain pages") - redactions: list[RedactionArea] | None = Field(None, description="A list of areas that should be redacted") + page_redaction_color: str = Field("#000000", description="The color used to fully redact certain pages") + redactions: list[RedactionArea] = Field(..., description="A list of areas that should be redacted") class Model( diff --git a/engine/tests/test_pdf_edit_agent.py b/engine/tests/test_pdf_edit_agent.py index 312e5a64c..52c40ce9b 100644 --- a/engine/tests/test_pdf_edit_agent.py +++ b/engine/tests/test_pdf_edit_agent.py @@ -136,6 +136,7 @@ async def test_pdf_edit_agent_passes_previous_steps_to_parameter_selector(runtim PdfEditPlanSelection( operations=[ToolEndpoint.ROTATE_PDF, ToolEndpoint.FLATTEN], summary="Rotate the PDF, then compress it.", + rationale="test rationale", ), parameter_selector=parameter_selector, ) @@ -294,6 +295,7 @@ async def test_pdf_edit_agent_passes_page_text_to_parameter_selector(runtime: Ap PdfEditPlanSelection( operations=[ToolEndpoint.ROTATE_PDF], summary="Rotate the PDF.", + rationale="test rationale", ), parameter_selector=parameter_selector, ) @@ -390,6 +392,7 @@ async def test_pdf_edit_agent_rejects_plan_referencing_unavailable_operations( PdfEditPlanSelection( operations=[ToolEndpoint.COMPRESS_PDF], summary="Compress.", + rationale="test rationale", ), parameter_selector=parameter_selector, ) @@ -420,6 +423,7 @@ async def test_pdf_edit_agent_supports_literal_find_replace(runtime: AppRuntime) PdfEditPlanSelection( operations=[ToolEndpoint.EDIT_TEXT], summary="Replace 2025 with 2026 throughout the document.", + rationale="test rationale", ), parameter_selector=parameter_selector, ) @@ -466,6 +470,7 @@ async def test_pdf_edit_agent_supports_copy_edit_using_page_text(runtime: AppRun PdfEditPlanSelection( operations=[ToolEndpoint.EDIT_TEXT], summary="Fix typos on page 3.", + rationale="test rationale", ), parameter_selector=parameter_selector, ) @@ -515,6 +520,7 @@ async def test_pdf_edit_agent_supports_natural_language_directed_edit(runtime: A PdfEditPlanSelection( operations=[ToolEndpoint.EDIT_TEXT], summary="Update the company name to Acme Corp.", + rationale="test rationale", ), parameter_selector=parameter_selector, ) @@ -551,6 +557,7 @@ async def test_pdf_edit_agent_composes_edit_text_with_other_operations(runtime: PdfEditPlanSelection( operations=[ToolEndpoint.EDIT_TEXT, ToolEndpoint.ROTATE_PDF], summary="Remove DRAFT marker, then rotate.", + rationale="test rationale", ), parameter_selector=parameter_selector, )