Repository navigation
feat(agent): render .pdf files from Markdown in Create File [PC-5320] - #1192
tudormatei1 wants to merge 2 commits into
Conversation
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>
There was a problem hiding this comment.
🟡 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
pdfextra, rendering tests, and version bump to0.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
| except ImportError: | ||
| return |
There was a problem hiding this comment.
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.
| "-P", | ||
| "-s", |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
Rendering now needs a name that ends in .pdf, so a gzipped workspace PDF uploads unchanged again. Test added. 6415327
| table = Table( | ||
| rows, repeatRows=1 if has_header else 0, hAlign="LEFT", splitInRow=1 |
There was a problem hiding this comment.
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.
|
| 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>






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.pdffile name turns the text into a real PDF, with headings, lists, tables, bold, italic and links.Changes
.pdfname, the text fromcontentor 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.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.pdfextra: reportlab, fonttools and markdown-it-py, described under New dependencies. Without the extra, a.pdfcall returns an error that says how to install it.# type: ignore[import-untyped], like the existingjsonpath_ngimports.New dependencies
pdfextrapdfextrapdfextrarich; declared now because we import it.Warning
Needs a legal check. The reportlab wheel also ships sample fonts in
reportlab/fonts/.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
.pdf, both called the tool, and all four PDFs rendered.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
pdfextra: UiPath/uipath-agents-python#865.EnablePdfFileCreationandcanvas.nodes.agent-tool-create-file-pdf, both off: UiPath/gitops-centralized-cluster#609909.🤖 Generated with Claude Code