Skip to content

feat(agent): render .pdf files from Markdown in Create File [PC-5320] - #1192

Open
tudormatei1 wants to merge 2 commits into
mainfrom
feat/PC-5320-create-file-pdf
Open

tudormatei1 wants to merge 2 commits into
mainfrom
feat/PC-5320-create-file-pdf

Conversation

@tudormatei1

@tudormatei1 tudormatei1 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Agents can't make a PDF today. When the model calls Create File with report.pdf, the tool stores the Markdown text under a PDF MIME type, and the file doesn't open. With this change a .pdf file name turns the text into a real PDF, with headings, lists, tables, bold, italic and links.

Changes

  • Create File: for a .pdf name, the text from content or from a workspace file is rendered and uploaded instead of the raw text. A workspace file that already is a PDF is uploaded as is. Other file types don't change.
  • Child process (pdf_renderer.py): this is the part to review closely. Rendering runs there with a 60 s timeout, a 1 GB memory limit and a 25 MB output cap. Input is capped at 100,000 characters. Each limit returns a message that tells the model to simplify or split the document.
    • I added the child process because some inputs under the input cap made reportlab hang or write huge files. 33k hard line breaks took 559 s, and a CJK link gave a 1.5 GB PDF.
    • A timeout can't stop a render running in a thread; killing a child process can.
    • The child runs the renderer as a script, not through the package, so it starts in about 0.3 s instead of about 4 s.
  • Fonts: come from the agents image, not shipped with the package.
    • Liberation covers Latin, Greek and Cyrillic.
    • Noto CJK covers Chinese, Japanese and Korean. reportlab can't read those CFF font collections, so fontTools cuts each font down to the characters the document uses and converts it to TrueType.
    • Characters no font covers come out blank and are logged.
  • New pdf extra: reportlab, fonttools and markdown-it-py, described under New dependencies. Without the extra, a .pdf call returns an error that says how to install it.
  • mypy: fontTools has no type information and no stub package, so its 8 imports use # type: ignore[import-untyped], like the existing jsonpath_ng imports.
  • Version: 0.18.56.

New dependencies

Package Version Where What it does here License Already used at UiPath
reportlab 5.1.0 pdf extra Lays out and writes the PDF: paragraphs, lists, tables, embedded fonts. Pure Python. Needs pillow and charset-normalizer, both already locked. 6.8 MB installed. BSD-3-Clause Autopilot backend, ML document understanding, uipath-llm-client-python (dev)
fonttools 4.68.0 pdf extra Cuts the Noto CJK fonts down to the characters a document uses and converts them to TrueType, so reportlab can embed them. No dependencies. 26 MB installed. MIT ixp-platform, ML OCR
markdown-it-py 4.2.0 pdf extra Parses the model's Markdown, including tables, into a tree the renderer walks. Already installed through rich; declared now because we import it. MIT already in our lock
pypdf 6.21.4 dev Tests read the PDFs back to check their text. BSD-3-Clause uipath-agents-python runtime dependency
types-reportlab 5.0.0.20261008 dev Types for mypy. Apache-2.0 typeshed stubs
  • Licenses: all five package licenses are on the GO list of UiPath's FOSS license standard. I didn't pick fpdf2 because it is LGPL-3.0-only, which is on the STOP list.
  • Vulnerabilities: OSV lists none for the locked versions (checked 2026-11-04).
  • reportlab's past CVE: its one code-execution bug, CVE-2023-33733, came in through attributes in paragraph markup. It was fixed in 3.6.13. Our renderer builds all markup itself. It escapes the model's text and link targets, allows only safe link schemes, and never takes colors or other attributes from the model.
  • Fonts in the PDFs: they come from the agents image, not from these packages: Liberation 2.1.5 and Noto CJK, both OFL-1.1. The FOSS standard lists OFL-1.1 as CAUTION and says to embed it in documents, which is all we do. We don't ship or sell the font files. I checked the Ubuntu noble packages, not a running container.

Warning

Needs a legal check. The reportlab wheel also ships sample fonts in reportlab/fonts/.

  • DarkGarden is GPL-2.0-or-later with a font exception for embedding in documents. GPL-2.0-or-later is on the STOP list.
  • The Bitstream Vera fonts have their own permissive license, which isn't on the list.

The renderer never loads either, but both are on disk in every image that installs the extra. Autopilot's backend already ships the same wheel.

Testing

  • The unit tests render real PDFs and read them back with pypdf. They cover text, tables, CJK, missing characters, the size and length limits, the timeout, running out of memory, the missing extra, and a workspace PDF uploaded as is. The tests build their own fonts with fontTools, so they don't depend on fonts installed on the CI runner (Linux or Windows).
  • I ran it end to end on a local robot, from Agent Builder and from a Maestro flow. The PDFs opened and looked right.
  • I gave Haiku and Sonnet the tool schema in two requests (a quarterly report and an invoice).
    • With today's argument descriptions, both models refused: "a .pdf extension would produce an invalid PDF".
    • With descriptions that mention Markdown for .pdf, both called the tool, and all four PDFs rendered.
    • Those descriptions ship in the Agent Builder and Maestro PRs, behind a flag.
  • Not checked: the fonts inside a running serverless container. I checked the Ubuntu font packages the image installs, not the container itself.

Note

Each render has its own limits, but nothing limits several renders running at the same time. In my test, 8 heavy renders at once used 3.4 GB. I left it like that until we know how much memory the pods have.

Rollout

  1. This PR, then a release of uipath-langchain 0.18.56.
  2. uipath-agents-python moves to 0.18.56 with the pdf extra: UiPath/uipath-agents-python#865.
  3. Agent Builder (UiPath/Agents#6896) and Maestro (UiPath/flow-workbench#5857) show the new descriptions behind EnablePdfFileCreation and canvas.nodes.agent-tool-create-file-pdf, both off: UiPath/gitops-centralized-cluster#609909.
  4. The flags go on per ring once the runtime is live there.

🤖 Generated with Claude Code

Create File with a .pdf name stored the Markdown text under a PDF MIME
type, so the file didn't open. A .pdf name now renders the text from
content or a workspace file as Markdown into a real PDF. A workspace
file that already is a PDF is uploaded as is.

Rendering runs in a child process with a 60 s timeout, a 1 GB memory
limit and a 25 MB output cap, because some inputs made reportlab run
for minutes or write GB-sized files. Input is capped at 100,000
characters.

Fonts come from the image: Liberation for Latin, Greek and Cyrillic,
and Noto CJK subset with fontTools and converted to TrueType, because
reportlab can't read the CFF collections. The new pdf extra brings
reportlab, fonttools and markdown-it-py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tudormatei1
tudormatei1 marked this pull request as ready for review November 4, 2026 16:05
Copilot AI balanced review requested due to automatic review settings November 4, 2026 16:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

PDF rendering proceeds without a memory limit on Windows, allowing pathological documents to exhaust host memory before the output-size check runs.

4 open issues
What changed in this PR

Adds Markdown-to-PDF rendering to the agent’s Create File tool, replacing invalid text-only .pdf uploads with real PDFs while preserving existing workspace PDFs.

Changes:

  • Introduces a subprocess renderer with time, memory, and output-size controls.
  • Adds Markdown formatting and system-font support, including CJK font conversion.
  • Adds the pdf extra, rendering tests, and version bump to 0.18.56.
File Description
uv.lock Locks PDF dependencies and updates the package version.
tests/​agent/​tools/​internal_tools/​test_pdf_renderer.py Tests PDF layout, fonts, text preservation, and repeated renders.
tests/​agent/​tools/​internal_tools/​test_create_file_tool.py Tests PDF creation, workspace inputs, limits, and subprocess failures.
tests/​agent/​tools/​internal_tools/​conftest.py Provides generated font fixtures independent of installed fonts.
src/​uipath_langchain/​agent/​tools/​internal_tools/​pdf_renderer.py Implements Markdown rendering, font handling, and child-process limits.
src/​uipath_langchain/​agent/​tools/​internal_tools/​create_file_tool.py Integrates PDF rendering and reports rendering failures.
pyproject.toml Declares PDF extras and test dependencies; bumps the version.

Review effort: Balanced

Comment on lines +201 to +202
except ImportError:
return

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as is. The agents runtime runs this on Linux, where the child gets the 1 GB RLIMIT_AS. On Windows it only runs when someone runs an agent locally, and the 60 s timeout still stops the render. A Job Object limit would be Windows-only code that the Linux runtime never runs.

Comment on lines +141 to +142
"-P",
"-s",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed -s, so the child sees the same packages as the parent. Inside a venv it made no difference, because the user site is already off there. 6415327


body: str | bytes | None = content
mime_type = guess_mime_type(attachment_name)
if is_pdf(mime_type) and not _is_pdf_file(source_path):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rendering now needs a name that ends in .pdf, so a gzipped workspace PDF uploads unchanged again. Test added. 6415327

Comment on lines +510 to +511
table = Table(
rows, repeatRows=1 if has_header else 0, hAlign="LEFT", splitInRow=1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as is: tables don't clip here. I measured 2 to 15 columns, prose cells and a 250-character URL, and every table fit the 470 pt frame. What does fail is one unbroken cell value of about 320 to 380 characters. reportlab then raises negative availWidth, and the model gets "could not be rendered", so it can rewrite the table. Fixed column widths would change the layout of every table to cover that case.

@sonarqubecloud

sonarqubecloud Bot commented Nov 4, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

raise
if args.max_pdf_bytes and len(rendered.content) > args.max_pdf_bytes:
return EXIT_PDF_TOO_LARGE
args.output.write_bytes(rendered.content)
_LIST_INDENT = 0.6 * cm
_QUOTE_INDENT = 0.6 * cm
_HEADING_SIZES = {1: 18.0, 2: 15.0, 3: 13.0, 4: 11.5, 5: 11.0, 6: 10.5}
_SAFE_LINK_SCHEMES = ("http://", "https://", "mailto:")
mimetypes reports report.pdf.gz as application/pdf, so a gzipped PDF
in the workspace went down the render path and failed as non-UTF-8
text. Rendering now needs a name that ends in .pdf.

The renderer child writes a JSON line with the missing characters,
then the PDF bytes, to stdout instead of to a path it is given, so
there is no temp dir and no output path argument. It also starts
without -s, so it sees the same packages as the parent when that one
runs outside a venv with user-site installs.

Links stay clickable when urlsplit gives an http, https or mailto
scheme, which also keeps upper-case schemes such as HTTPS://.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants