From 1371fec3d650f98814d2c2a6510c44418b22bae7 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 11:29:23 -0700 Subject: [PATCH 1/9] feat(latex): add LaTeX integration with PDF compilation tool, block, and docs --- apps/docs/components/icons.tsx | 27 ++ apps/docs/components/ui/icon-mapping.ts | 2 + .../content/docs/en/integrations/latex.mdx | 58 +++++ .../content/docs/en/integrations/meta.json | 1 + apps/sim/app/api/tools/latex/route.ts | 238 ++++++++++++++++++ apps/sim/blocks/blocks/latex.ts | 231 +++++++++++++++++ apps/sim/blocks/registry.ts | 3 + apps/sim/components/icons.tsx | 27 ++ apps/sim/lib/api/contracts/tools/index.ts | 1 + apps/sim/lib/api/contracts/tools/latex.ts | 76 ++++++ apps/sim/lib/integrations/icon-mapping.ts | 2 + apps/sim/lib/integrations/integrations.json | 18 ++ apps/sim/tools/latex/compile.ts | 123 +++++++++ apps/sim/tools/latex/index.ts | 1 + apps/sim/tools/latex/types.ts | 31 +++ apps/sim/tools/registry.ts | 2 + 16 files changed, 841 insertions(+) create mode 100644 apps/docs/content/docs/en/integrations/latex.mdx create mode 100644 apps/sim/app/api/tools/latex/route.ts create mode 100644 apps/sim/blocks/blocks/latex.ts create mode 100644 apps/sim/lib/api/contracts/tools/latex.ts create mode 100644 apps/sim/tools/latex/compile.ts create mode 100644 apps/sim/tools/latex/index.ts create mode 100644 apps/sim/tools/latex/types.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 6f5df2b76b..8583b7886e 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -2589,6 +2589,33 @@ export function LangsmithIcon(props: SVGProps) { ) } +export function LatexIcon(props: SVGProps) { + return ( + + + + + + + + ) +} + export function LaunchDarklyIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 7a8e927a2c..965c9471e8 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -108,6 +108,7 @@ import { KalshiIcon, KetchIcon, LangsmithIcon, + LatexIcon, LaunchDarklyIcon, LemlistIcon, LinearIcon, @@ -339,6 +340,7 @@ export const blockTypeToIconMap: Record = { ketch: KetchIcon, knowledge: PackageSearchIcon, langsmith: LangsmithIcon, + latex: LatexIcon, launchdarkly: LaunchDarklyIcon, lemlist: LemlistIcon, linear: LinearIcon, diff --git a/apps/docs/content/docs/en/integrations/latex.mdx b/apps/docs/content/docs/en/integrations/latex.mdx new file mode 100644 index 0000000000..c1c6123df4 --- /dev/null +++ b/apps/docs/content/docs/en/integrations/latex.mdx @@ -0,0 +1,58 @@ +--- +title: LaTeX +description: Compile LaTeX documents into PDFs +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[LaTeX](https://www.latex-project.org/) is a typesetting system that is the de facto standard for producing high-quality scientific and technical documents. Unlike word processors, LaTeX uses plain-text markup to describe a document's structure and lets the compiler handle layout, which makes it ideal for documents with mathematical notation, citations, cross-references, and consistent professional formatting. + +With LaTeX, you can: + +- **Compile documents to PDF**: Turn LaTeX source into a polished, print-ready PDF +- **Typeset mathematics**: Render equations, theorems, and scientific notation with precision +- **Choose your compiler**: Build with pdfLaTeX, XeLaTeX, LuaLaTeX, pLaTeX, upLaTeX, or ConTeXt +- **Include supporting files**: Add images, included `.tex` files, bibliographies, and custom classes or styles to the build +- **Automate document production**: Generate reports, invoices, certificates, and papers from templates + +In Sim, the LaTeX integration enables your agents to compile LaTeX source into PDF files as part of any workflow — no OAuth or API key required. Agents can draft documents in LaTeX, attach supporting resources such as images or BibTeX bibliographies, and produce a finished PDF that downstream blocks can email, upload, or store. This makes it easy to build agents that generate reports, typeset research digests, or produce templated documents like invoices and certificates. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrates LaTeX into the workflow. Compiles LaTeX source into a PDF file with pdflatex, xelatex, lualatex, platex, uplatex, or context, and supports additional resources such as images, included .tex files, and bibliographies. Does not require OAuth or an API key. + + + +## Actions + +### `latex_compile` + +Compile a LaTeX document into a PDF. Supports pdflatex, xelatex, lualatex, platex, uplatex, and context, plus supporting resources such as images, included .tex files, and bibliographies. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `content` | string | Yes | LaTeX source of the main document, from \\documentclass to \\end\{document\} | +| `compiler` | string | No | LaTeX compiler: pdflatex \(default\), xelatex, lualatex, platex, uplatex, or context | +| `fileName` | string | No | Name for the generated PDF file \(default: document.pdf\) | +| `resources` | array | No | Supporting files for the compilation. Each entry has a "path" plus exactly one of "content" \(plain text\), "file" \(base64\), or "url" \(remote file\), e.g. \[\{"path": "refs.bib", "content": "..."\}, \{"path": "logo.png", "url": "https://..."\}\] | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pdf` | file | Compiled PDF file | +| `pdfUrl` | string | URL of the compiled PDF | +| `fileName` | string | Name of the compiled PDF file | +| `compiler` | string | LaTeX compiler used for the build | + + diff --git a/apps/docs/content/docs/en/integrations/meta.json b/apps/docs/content/docs/en/integrations/meta.json index dd4558a99c..cd7ed6e3e1 100644 --- a/apps/docs/content/docs/en/integrations/meta.json +++ b/apps/docs/content/docs/en/integrations/meta.json @@ -108,6 +108,7 @@ "ketch", "knowledge", "langsmith", + "latex", "launchdarkly", "lemlist", "linear", diff --git a/apps/sim/app/api/tools/latex/route.ts b/apps/sim/app/api/tools/latex/route.ts new file mode 100644 index 0000000000..578995f896 --- /dev/null +++ b/apps/sim/app/api/tools/latex/route.ts @@ -0,0 +1,238 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { truncate } from '@sim/utils/string' +import { type NextRequest, NextResponse } from 'next/server' +import { type LatexCompileBody, latexCompileContract } from '@/lib/api/contracts/tools/latex' +import { getValidationErrorMessage, parseRequest, validationErrorResponse } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { + isPayloadSizeLimitError, + readResponseJsonWithLimit, + readResponseToBufferWithLimit, +} from '@/lib/core/utils/stream-limits' +import { getBaseUrl } from '@/lib/core/utils/urls' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +const logger = createLogger('LatexCompileAPI') + +const LATEX_COMPILE_URL = 'https://latex.ytotech.com/builds/sync' +const DEFAULT_COMPILER = 'pdflatex' +const MAX_PDF_BYTES = 25 * 1024 * 1024 +const MAX_ERROR_JSON_BYTES = 4 * 1024 * 1024 +const MAX_ERROR_MESSAGE_CHARS = 4000 + +export const dynamic = 'force-dynamic' +export const maxDuration = 60 + +interface StoredPdfResponse { + pdfFile?: unknown + pdfUrl: string + fileName: string + contentType: string + compiler: string +} + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success || !authResult.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseRequest( + latexCompileContract, + request, + {}, + { + validationErrorResponse: (error) => { + logger.warn(`[${requestId}] Invalid LaTeX compile request:`, error.issues) + return validationErrorResponse( + error, + getValidationErrorMessage(error, 'Invalid request data') + ) + }, + } + ) + if (!parsed.success) return parsed.response + + const body = parsed.data.body + const compiler = body.compiler || DEFAULT_COMPILER + + logger.info(`[${requestId}] Compiling LaTeX document`, { + compiler, + contentLength: body.content.length, + resourceCount: body.resources?.length ?? 0, + }) + + const upstreamResponse = await fetch(LATEX_COMPILE_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + compiler, + resources: [{ main: true, content: body.content }, ...(body.resources ?? [])], + }), + }) + + const upstreamContentType = upstreamResponse.headers.get('content-type') || '' + if (!upstreamResponse.ok || !upstreamContentType.includes('application/pdf')) { + return await buildCompileErrorResponse(upstreamResponse, requestId) + } + + const pdfBuffer = await readResponseToBufferWithLimit(upstreamResponse, { + maxBytes: MAX_PDF_BYTES, + label: 'compiled PDF', + }) + if (pdfBuffer.length === 0) { + logger.error(`[${requestId}] LaTeX compile service returned an empty PDF`) + return NextResponse.json( + { error: 'LaTeX compile service returned an empty PDF' }, + { status: 502 } + ) + } + + const storedPdf = await storeCompiledPdf(pdfBuffer, body, compiler, authResult.userId) + + logger.info(`[${requestId}] LaTeX compilation completed`, { + compiler, + fileName: storedPdf.fileName, + size: pdfBuffer.length, + }) + + return NextResponse.json(storedPdf) + } catch (error) { + logger.error(`[${requestId}] LaTeX compile route error:`, error) + return NextResponse.json( + { error: getErrorMessage(error, 'LaTeX compilation failed') }, + { status: isPayloadSizeLimitError(error) ? 413 : 500 } + ) + } +}) + +/** + * Builds the output PDF filename: strips any directory components and + * normalizes to a single `.pdf` extension. + */ +function buildPdfFileName(fileName: string | undefined): string { + const base = (fileName || 'document').split(/[/\\]/).pop()?.trim() || 'document' + const withoutExtension = base.toLowerCase().endsWith('.pdf') ? base.slice(0, -4) : base + return `${withoutExtension || 'document'}.pdf` +} + +/** + * Extracts TeX error lines (lines starting with `!`, each with two lines of + * context) from the compiler log files returned by the compile service. + */ +function extractCompilationErrors(logFiles: unknown): string | undefined { + if (typeof logFiles !== 'object' || logFiles === null) return undefined + + const snippets: string[] = [] + for (const log of Object.values(logFiles)) { + if (typeof log !== 'string') continue + const lines = log.split('\n') + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith('!')) { + snippets.push(lines.slice(i, i + 3).join('\n')) + } + } + } + + if (snippets.length === 0) return undefined + return truncate([...new Set(snippets)].join('\n\n'), MAX_ERROR_MESSAGE_CHARS) +} + +/** + * Maps a failed compile-service response to a JSON error response: 422 with + * extracted TeX errors for compilation failures, 502 for anything unexpected. + */ +async function buildCompileErrorResponse( + upstreamResponse: Response, + requestId: string +): Promise { + const errorBody = await readResponseJsonWithLimit(upstreamResponse, { + maxBytes: MAX_ERROR_JSON_BYTES, + label: 'LaTeX compile error response', + }).catch(() => undefined) + + const errorRecord = + typeof errorBody === 'object' && errorBody !== null + ? (errorBody as Record) + : undefined + const errorCode = typeof errorRecord?.error === 'string' ? errorRecord.error : undefined + const compilationErrors = extractCompilationErrors(errorRecord?.log_files) + + if (upstreamResponse.status === 400 && errorCode) { + logger.warn(`[${requestId}] LaTeX compilation failed`, { errorCode }) + const details = compilationErrors ? `:\n${compilationErrors}` : '' + return NextResponse.json( + { error: `LaTeX compilation failed (${errorCode})${details}` }, + { status: 422 } + ) + } + + logger.error(`[${requestId}] LaTeX compile service error`, { + status: upstreamResponse.status, + errorCode, + }) + return NextResponse.json( + { error: `LaTeX compile service error: ${upstreamResponse.status}` }, + { status: 502 } + ) +} + +/** + * Stores the compiled PDF as an execution file when execution context is + * available, falling back to general storage otherwise. + */ +async function storeCompiledPdf( + pdfBuffer: Buffer, + body: LatexCompileBody, + compiler: string, + userId: string +): Promise { + const fileName = buildPdfFileName(body.fileName) + const executionContext = + body.workspaceId && body.workflowId && body.executionId + ? { + workspaceId: body.workspaceId, + workflowId: body.workflowId, + executionId: body.executionId, + } + : null + + if (executionContext) { + const { uploadExecutionFile } = await import('@/lib/uploads/contexts/execution') + const pdfFile = await uploadExecutionFile( + executionContext, + pdfBuffer, + fileName, + 'application/pdf', + userId + ) + + return { + pdfFile, + pdfUrl: pdfFile.url, + fileName, + contentType: 'application/pdf', + compiler, + } + } + + const { StorageService } = await import('@/lib/uploads') + const fileInfo = await StorageService.uploadFile({ + file: pdfBuffer, + fileName, + contentType: 'application/pdf', + context: 'copilot', + }) + + return { + pdfUrl: `${getBaseUrl()}${fileInfo.path}`, + fileName, + contentType: 'application/pdf', + compiler, + } +} diff --git a/apps/sim/blocks/blocks/latex.ts b/apps/sim/blocks/blocks/latex.ts new file mode 100644 index 0000000000..6fab5d180f --- /dev/null +++ b/apps/sim/blocks/blocks/latex.ts @@ -0,0 +1,231 @@ +import { LatexIcon } from '@/components/icons' +import type { BlockConfig, BlockMeta } from '@/blocks/types' +import { IntegrationType } from '@/blocks/types' +import type { LatexCompileResponse } from '@/tools/latex/types' + +export const LatexBlock: BlockConfig = { + type: 'latex', + name: 'LaTeX', + description: 'Compile LaTeX documents into PDFs', + longDescription: + 'Integrates LaTeX into the workflow. Compiles LaTeX source into a PDF file with pdflatex, xelatex, lualatex, platex, uplatex, or context, and supports additional resources such as images, included .tex files, and bibliographies. Does not require OAuth or an API key.', + docsLink: 'https://docs.sim.ai/integrations/latex', + category: 'tools', + integrationType: IntegrationType.Documents, + bgColor: '#FFFFFF', + icon: LatexIcon, + subBlocks: [ + { + id: 'content', + title: 'LaTeX Source', + type: 'long-input', + placeholder: + '\\documentclass{article}\n\\begin{document}\nHello, world! $E = mc^2$\n\\end{document}', + rows: 10, + required: true, + }, + { + id: 'compiler', + title: 'Compiler', + type: 'dropdown', + options: [ + { label: 'pdfLaTeX', id: 'pdflatex' }, + { label: 'XeLaTeX', id: 'xelatex' }, + { label: 'LuaLaTeX', id: 'lualatex' }, + { label: 'pLaTeX', id: 'platex' }, + { label: 'upLaTeX', id: 'uplatex' }, + { label: 'ConTeXt', id: 'context' }, + ], + value: () => 'pdflatex', + }, + { + id: 'resources', + title: 'Resources', + type: 'code', + language: 'json', + mode: 'advanced', + placeholder: '[{"path": "refs.bib", "content": "..."}]', + wandConfig: { + enabled: true, + prompt: `Generate a JSON array of supporting files for a LaTeX compilation based on the user's description. Each entry must have a "path" (relative file path the LaTeX source references) plus exactly one of: +- "content": plain-text file content (for .tex, .bib, .cls, .sty files) +- "url": URL to download the file from (for images or other binary files) +- "file": base64-encoded file content + +Example: +[{"path": "refs.bib", "content": "@article{knuth1984, author={Donald Knuth}, title={Literate Programming}, journal={The Computer Journal}, year={1984}}"}, {"path": "logo.png", "url": "https://example.com/logo.png"}] + +Return ONLY the JSON array - no explanations, no markdown, no extra text.`, + placeholder: 'Describe the supporting files you need...', + generationType: 'json-object', + }, + }, + { + id: 'fileName', + title: 'File Name', + type: 'short-input', + mode: 'advanced', + placeholder: 'document.pdf', + }, + ], + tools: { + access: ['latex_compile'], + config: { + tool: () => 'latex_compile', + params: (params) => { + const { compiler, fileName, resources, ...rest } = params + + let parsedResources: unknown + if (typeof resources === 'string' && resources.trim()) { + try { + parsedResources = JSON.parse(resources) + } catch { + throw new Error('Resources must be a valid JSON array.') + } + } else if (Array.isArray(resources)) { + parsedResources = resources + } + + return { + ...rest, + compiler: typeof compiler === 'string' && compiler.trim() ? compiler : undefined, + fileName: typeof fileName === 'string' && fileName.trim() ? fileName : undefined, + resources: parsedResources, + } + }, + }, + }, + inputs: { + content: { type: 'string', description: 'LaTeX source of the main document' }, + compiler: { type: 'string', description: 'LaTeX compiler to use' }, + resources: { type: 'json', description: 'Supporting files for the compilation' }, + fileName: { type: 'string', description: 'Name for the generated PDF file' }, + }, + outputs: { + pdf: { type: 'file', description: 'Compiled PDF file' }, + pdfUrl: { type: 'string', description: 'URL of the compiled PDF' }, + fileName: { type: 'string', description: 'Name of the compiled PDF file' }, + compiler: { type: 'string', description: 'LaTeX compiler used for the build' }, + }, +} + +export const LatexBlockMeta = { + tags: ['document-processing'], + templates: [ + { + icon: LatexIcon, + title: 'LaTeX invoice generator', + prompt: + 'Build a workflow that takes invoice line items from a table, fills a LaTeX invoice template, compiles it to PDF, and emails the invoice to the customer.', + modules: ['tables', 'workflows'], + category: 'operations', + tags: ['documents', 'automation'], + alsoIntegrations: ['gmail'], + }, + { + icon: LatexIcon, + title: 'LaTeX research report writer', + prompt: + 'Create a workflow where an agent researches a topic from the knowledge base, writes the findings as a LaTeX article, and compiles a polished PDF report.', + modules: ['agent', 'knowledge-base', 'workflows'], + category: 'productivity', + tags: ['documents', 'research'], + }, + { + icon: LatexIcon, + title: 'LaTeX weekly metrics report', + prompt: + 'Build a scheduled weekly workflow that pulls metrics from a table, typesets a LaTeX report with charts and tables, compiles it to PDF, and posts it to Slack.', + modules: ['scheduled', 'tables', 'workflows'], + category: 'operations', + tags: ['documents', 'reporting'], + alsoIntegrations: ['slack'], + }, + { + icon: LatexIcon, + title: 'LaTeX offer letter generator', + prompt: + 'Create a workflow that takes candidate details from a form, fills a LaTeX offer letter template, compiles it to PDF, and sends it for e-signature.', + modules: ['workflows'], + category: 'operations', + tags: ['documents', 'hr'], + alsoIntegrations: ['docusign'], + }, + { + icon: LatexIcon, + title: 'LaTeX math worksheet builder', + prompt: + 'Build a workflow where an agent generates practice problems for a given math topic and difficulty, typesets them with LaTeX equations, and compiles a printable worksheet PDF.', + modules: ['agent', 'workflows'], + category: 'productivity', + tags: ['documents', 'education'], + }, + { + icon: LatexIcon, + title: 'LaTeX proposal generator', + prompt: + 'Create a workflow that pulls deal details from HubSpot, has an agent draft a tailored proposal in LaTeX, compiles it to PDF, and saves it to the deal record.', + modules: ['agent', 'workflows'], + category: 'sales', + tags: ['documents', 'proposals'], + alsoIntegrations: ['hubspot'], + }, + { + icon: LatexIcon, + title: 'LaTeX paper digest compiler', + prompt: + 'Build a scheduled workflow that fetches new ArXiv papers on tracked topics, has an agent summarize each one, typesets the digest as a LaTeX document, and compiles a weekly PDF.', + modules: ['scheduled', 'agent', 'files', 'workflows'], + category: 'productivity', + tags: ['documents', 'research'], + alsoIntegrations: ['arxiv'], + }, + { + icon: LatexIcon, + title: 'LaTeX certificate generator', + prompt: + 'Create a workflow that reads attendee names from a spreadsheet, fills a LaTeX certificate template for each attendee, compiles the PDFs, and emails each certificate to its recipient.', + modules: ['workflows'], + category: 'operations', + tags: ['documents', 'automation'], + alsoIntegrations: ['google_sheets', 'gmail'], + }, + ], + skills: [ + { + name: 'compile-document-to-pdf', + description: + 'Compile LaTeX source into a finished PDF, choosing the right compiler and reporting any compilation errors. Use whenever a polished, print-ready document is needed.', + content: + '# Compile Document to PDF\n\nTurn LaTeX source into a finished PDF.\n\n## Steps\n1. Assemble the complete LaTeX source, from \\documentclass to \\end{document}.\n2. Pick the compiler: pdflatex for standard documents, xelatex or lualatex when custom fonts or full unicode are needed.\n3. Attach any supporting files (images, included .tex files, .bib bibliographies) as resources with the paths the source references.\n4. Compile and capture the resulting PDF.\n\n## Output\nThe compiled PDF file and its URL. If compilation fails, report the TeX errors verbatim so they can be fixed.', + }, + { + name: 'generate-document-from-template', + description: + 'Fill a LaTeX template with structured data and compile it to PDF. Use for repeatable documents like invoices, certificates, letters, and contracts.', + content: + '# Generate Document from Template\n\nProduce a templated document with real data filled in.\n\n## Steps\n1. Start from the LaTeX template and identify its placeholders.\n2. Substitute each placeholder with the provided data, escaping LaTeX special characters (&, %, $, #, _, {, }) in user-supplied values.\n3. Compile the filled-in source to PDF.\n4. Name the output file after the document, e.g. invoice-1042.pdf.\n\n## Output\nA compiled PDF per record, named for its contents. List any records that failed to compile and why.', + }, + { + name: 'typeset-math-content', + description: + 'Write mathematical content — equations, proofs, problem sets — in LaTeX and compile a printable PDF. Use for worksheets, solution sheets, and technical notes.', + content: + '# Typeset Math Content\n\nProduce a clean PDF of mathematical material.\n\n## Steps\n1. Draft the content using proper LaTeX math: inline $...$, display equations, and environments like align and theorem as appropriate.\n2. Load only the packages the content needs (amsmath, amssymb, amsthm).\n3. Structure the document with sections and consistent numbering.\n4. Compile to PDF and verify there are no errors.\n\n## Output\nA printable PDF of the typeset material, plus the LaTeX source so it can be edited later.', + }, + { + name: 'build-report-with-bibliography', + description: + 'Compile a report or paper that cites sources, attaching a BibTeX bibliography as a resource. Use for research reports, literature reviews, and academic writing.', + content: + '# Build Report with Bibliography\n\nCompile a citing document with its references resolved.\n\n## Steps\n1. Write the report source with \\cite commands and a \\bibliography{refs} (or biblatex equivalent) reference.\n2. Attach the BibTeX entries as a resource at the cited path, e.g. refs.bib.\n3. Compile — the bibliography pass runs automatically.\n4. Check the output for unresolved citation warnings and fix missing entries.\n\n## Output\nThe compiled PDF with a formatted reference list. Note any citations that could not be resolved.', + }, + { + name: 'fix-compilation-errors', + description: + 'Diagnose failed LaTeX builds from the compiler error output and iterate until the document compiles. Use when a compilation returns errors instead of a PDF.', + content: + '# Fix Compilation Errors\n\nGet a failing LaTeX document to build.\n\n## Steps\n1. Read the TeX error lines from the failed compile (lines starting with !), which name the problem and its location.\n2. Apply the targeted fix: missing packages, unescaped special characters, unmatched braces or environments, or commands needing a different compiler (e.g. fontspec requires xelatex or lualatex).\n3. Recompile and repeat until the build succeeds.\n4. Keep edits minimal — fix the errors without rewriting the document.\n\n## Output\nThe compiled PDF and a short list of the fixes that were applied.', + }, + ], +} as const satisfies BlockMeta diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 2703ead3bd..75e4e13c65 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -155,6 +155,7 @@ import { import { KetchBlock, KetchBlockMeta } from '@/blocks/blocks/ketch' import { KnowledgeBlock } from '@/blocks/blocks/knowledge' import { LangsmithBlock, LangsmithBlockMeta } from '@/blocks/blocks/langsmith' +import { LatexBlock, LatexBlockMeta } from '@/blocks/blocks/latex' import { LaunchDarklyBlock, LaunchDarklyBlockMeta } from '@/blocks/blocks/launchdarkly' import { LemlistBlock, LemlistBlockMeta } from '@/blocks/blocks/lemlist' import { LinearBlock, LinearBlockMeta, LinearV2Block } from '@/blocks/blocks/linear' @@ -456,6 +457,7 @@ const BLOCK_REGISTRY: Record = { ketch: KetchBlock, knowledge: KnowledgeBlock, langsmith: LangsmithBlock, + latex: LatexBlock, launchdarkly: LaunchDarklyBlock, lemlist: LemlistBlock, linear: LinearBlock, @@ -726,6 +728,7 @@ const BLOCK_META_REGISTRY: Record = { kalshi_v2: KalshiV2BlockMeta, ketch: KetchBlockMeta, langsmith: LangsmithBlockMeta, + latex: LatexBlockMeta, launchdarkly: LaunchDarklyBlockMeta, lemlist: LemlistBlockMeta, linear: LinearBlockMeta, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 6f5df2b76b..8583b7886e 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -2589,6 +2589,33 @@ export function LangsmithIcon(props: SVGProps) { ) } +export function LatexIcon(props: SVGProps) { + return ( + + + + + + + + ) +} + export function LaunchDarklyIcon(props: SVGProps) { return ( diff --git a/apps/sim/lib/api/contracts/tools/index.ts b/apps/sim/lib/api/contracts/tools/index.ts index f65acf4cb6..b287452fe1 100644 --- a/apps/sim/lib/api/contracts/tools/index.ts +++ b/apps/sim/lib/api/contracts/tools/index.ts @@ -13,6 +13,7 @@ export * from './firecrawl' export * from './github' export * from './google' export * from './imap' +export * from './latex' export * from './mail' export * from './media' export * from './microsoft' diff --git a/apps/sim/lib/api/contracts/tools/latex.ts b/apps/sim/lib/api/contracts/tools/latex.ts new file mode 100644 index 0000000000..45477e1969 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/latex.ts @@ -0,0 +1,76 @@ +import { z } from 'zod' +import { genericToolResponseSchema } from '@/lib/api/contracts/tools/shared' +import { defineRouteContract } from '@/lib/api/contracts/types' + +export const latexCompilers = [ + 'pdflatex', + 'xelatex', + 'lualatex', + 'platex', + 'uplatex', + 'context', +] as const + +const MAX_LATEX_SOURCE_CHARS = 1_000_000 +const MAX_LATEX_RESOURCES = 25 + +const latexResourceSchema = z + .object({ + path: z + .string() + .min(1, 'resource path cannot be empty') + .max(512, 'resource path must be at most 512 characters'), + content: z + .string() + .max(MAX_LATEX_SOURCE_CHARS, 'resource content must be at most 1,000,000 characters') + .optional(), + file: z + .string() + .max(MAX_LATEX_SOURCE_CHARS, 'resource file must be at most 1,000,000 characters of base64') + .optional(), + url: z + .string() + .url('resource url must be a valid URL') + .max(2048, 'resource url must be at most 2048 characters') + .optional(), + }) + .superRefine((resource, ctx) => { + const provided = [resource.content, resource.file, resource.url].filter( + (value) => value !== undefined + ) + if (provided.length !== 1) { + ctx.addIssue({ + code: 'custom', + path: ['path'], + message: `resource "${resource.path}" must provide exactly one of content, file, or url`, + }) + } + }) + +export const latexCompileBodySchema = z.object({ + content: z + .string() + .min(1, 'content cannot be empty') + .max(MAX_LATEX_SOURCE_CHARS, 'content must be at most 1,000,000 characters'), + compiler: z.enum(latexCompilers).optional(), + fileName: z.string().max(255, 'fileName must be at most 255 characters').optional(), + resources: z + .array(latexResourceSchema) + .max(MAX_LATEX_RESOURCES, `resources must contain at most ${MAX_LATEX_RESOURCES} entries`) + .optional(), + workspaceId: z.string().optional(), + workflowId: z.string().optional(), + executionId: z.string().optional(), +}) + +export type LatexCompileBody = z.input + +export const latexCompileContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/latex', + body: latexCompileBodySchema, + response: { + mode: 'json', + schema: genericToolResponseSchema, + }, +}) diff --git a/apps/sim/lib/integrations/icon-mapping.ts b/apps/sim/lib/integrations/icon-mapping.ts index b7d26906bb..866dc8c95f 100644 --- a/apps/sim/lib/integrations/icon-mapping.ts +++ b/apps/sim/lib/integrations/icon-mapping.ts @@ -107,6 +107,7 @@ import { KalshiIcon, KetchIcon, LangsmithIcon, + LatexIcon, LaunchDarklyIcon, LemlistIcon, LinearIcon, @@ -323,6 +324,7 @@ export const blockTypeToIconMap: Record = { ketch: KetchIcon, knowledge: PackageSearchIcon, langsmith: LangsmithIcon, + latex: LatexIcon, launchdarkly: LaunchDarklyIcon, lemlist: LemlistIcon, linear_v2: LinearIcon, diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index b6b6beeaf4..f85fb951e0 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -8239,6 +8239,24 @@ "integrationType": "observability", "tags": ["monitoring", "llm"] }, + { + "type": "latex", + "slug": "latex", + "name": "LaTeX", + "description": "Compile LaTeX documents into PDFs", + "longDescription": "Integrates LaTeX into the workflow. Compiles LaTeX source into a PDF file with pdflatex, xelatex, lualatex, platex, uplatex, or context, and supports additional resources such as images, included .tex files, and bibliographies. Does not require OAuth or an API key.", + "bgColor": "#FFFFFF", + "iconName": "LatexIcon", + "docsUrl": "https://docs.sim.ai/integrations/latex", + "operations": [], + "operationCount": 0, + "triggers": [], + "triggerCount": 0, + "authType": "none", + "category": "tools", + "integrationType": "documents", + "tags": ["document-processing"] + }, { "type": "launchdarkly", "slug": "launchdarkly", diff --git a/apps/sim/tools/latex/compile.ts b/apps/sim/tools/latex/compile.ts new file mode 100644 index 0000000000..9c91858ae9 --- /dev/null +++ b/apps/sim/tools/latex/compile.ts @@ -0,0 +1,123 @@ +import type { LatexCompileParams, LatexCompileResponse } from '@/tools/latex/types' +import type { ToolConfig } from '@/tools/types' + +export const latexCompileTool: ToolConfig = { + id: 'latex_compile', + name: 'LaTeX Compile', + description: + 'Compile a LaTeX document into a PDF. Supports pdflatex, xelatex, lualatex, platex, uplatex, and context, plus supporting resources such as images, included .tex files, and bibliographies.', + version: '1.0.0', + + params: { + content: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'LaTeX source of the main document, from \\documentclass to \\end{document}', + }, + compiler: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'LaTeX compiler: pdflatex (default), xelatex, lualatex, platex, uplatex, or context', + }, + fileName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Name for the generated PDF file (default: document.pdf)', + }, + resources: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: + 'Supporting files for the compilation. Each entry has a "path" plus exactly one of "content" (plain text), "file" (base64), or "url" (remote file), e.g. [{"path": "refs.bib", "content": "..."}, {"path": "logo.png", "url": "https://..."}]', + items: { + type: 'object', + properties: { + path: { type: 'string', description: 'Relative path the file is available at' }, + content: { type: 'string', description: 'Plain-text file content' }, + file: { type: 'string', description: 'Base64-encoded file content' }, + url: { type: 'string', description: 'URL the file is downloaded from' }, + }, + }, + }, + }, + + request: { + url: '/api/tools/latex', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: ( + params: LatexCompileParams & { + _context?: { workspaceId?: string; workflowId?: string; executionId?: string } + } + ) => ({ + content: params.content, + compiler: params.compiler, + fileName: params.fileName, + resources: params.resources, + workspaceId: params._context?.workspaceId, + workflowId: params._context?.workflowId, + executionId: params._context?.executionId, + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as { + error?: string + pdfFile?: unknown + pdfUrl?: string + fileName?: string + compiler?: string + } + + if (!response.ok || data.error) { + return { + success: false, + error: data.error || 'LaTeX compilation failed', + output: { + pdf: '', + pdfUrl: '', + fileName: '', + compiler: data.compiler || '', + }, + } + } + + const pdf = + data.pdfFile || + (data.pdfUrl + ? { + name: data.fileName || 'document.pdf', + url: data.pdfUrl, + mimeType: 'application/pdf', + } + : '') + + return { + success: true, + output: { + pdf, + pdfUrl: data.pdfUrl || '', + fileName: data.fileName || '', + compiler: data.compiler || '', + }, + } + }, + + outputs: { + pdf: { + type: 'file', + description: 'Compiled PDF file', + fileConfig: { mimeType: 'application/pdf', extension: 'pdf' }, + }, + pdfUrl: { type: 'string', description: 'URL of the compiled PDF' }, + fileName: { type: 'string', description: 'Name of the compiled PDF file' }, + compiler: { type: 'string', description: 'LaTeX compiler used for the build' }, + }, +} diff --git a/apps/sim/tools/latex/index.ts b/apps/sim/tools/latex/index.ts new file mode 100644 index 0000000000..e055b10e6e --- /dev/null +++ b/apps/sim/tools/latex/index.ts @@ -0,0 +1 @@ +export { latexCompileTool } from '@/tools/latex/compile' diff --git a/apps/sim/tools/latex/types.ts b/apps/sim/tools/latex/types.ts new file mode 100644 index 0000000000..bb993e7bc6 --- /dev/null +++ b/apps/sim/tools/latex/types.ts @@ -0,0 +1,31 @@ +import type { ToolResponse } from '@/tools/types' + +export type LatexCompiler = 'pdflatex' | 'xelatex' | 'lualatex' | 'platex' | 'uplatex' | 'context' + +/** + * Supporting file made available to the compiler alongside the main document. + * Exactly one of `content` (plain text), `file` (base64), or `url` (remote + * file) must be provided. + */ +export interface LatexResource { + path: string + content?: string + file?: string + url?: string +} + +export interface LatexCompileParams { + content: string + compiler?: LatexCompiler + fileName?: string + resources?: LatexResource[] +} + +export interface LatexCompileResponse extends ToolResponse { + output: { + pdf: unknown + pdfUrl: string + fileName: string + compiler: string + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 35572109fb..c8c869fe7d 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1694,6 +1694,7 @@ import { knowledgeUpsertDocumentTool, } from '@/tools/knowledge' import { langsmithCreateRunsBatchTool, langsmithCreateRunTool } from '@/tools/langsmith' +import { latexCompileTool } from '@/tools/latex' import { launchDarklyCreateFlagTool, launchDarklyDeleteFlagTool, @@ -5792,6 +5793,7 @@ export const tools: Record = { linear_list_project_statuses: linearListProjectStatusesTool, langsmith_create_run: langsmithCreateRunTool, langsmith_create_runs_batch: langsmithCreateRunsBatchTool, + latex_compile: latexCompileTool, launchdarkly_create_flag: launchDarklyCreateFlagTool, launchdarkly_delete_flag: launchDarklyDeleteFlagTool, launchdarkly_get_audit_log: launchDarklyGetAuditLogTool, From 07f54aa585a094894067dafb76dc747c1fc4eb4b Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 11:35:12 -0700 Subject: [PATCH 2/9] fix(latex): surface extracted TeX errors on all failed compile responses --- apps/sim/app/api/tools/latex/route.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/api/tools/latex/route.ts b/apps/sim/app/api/tools/latex/route.ts index 578995f896..31407ec1f7 100644 --- a/apps/sim/app/api/tools/latex/route.ts +++ b/apps/sim/app/api/tools/latex/route.ts @@ -162,12 +162,20 @@ async function buildCompileErrorResponse( : undefined const errorCode = typeof errorRecord?.error === 'string' ? errorRecord.error : undefined const compilationErrors = extractCompilationErrors(errorRecord?.log_files) + const details = compilationErrors ? `:\n${compilationErrors}` : '' - if (upstreamResponse.status === 400 && errorCode) { - logger.warn(`[${requestId}] LaTeX compilation failed`, { errorCode }) - const details = compilationErrors ? `:\n${compilationErrors}` : '' + const isCompilationFailure = + upstreamResponse.status >= 400 && + upstreamResponse.status < 500 && + Boolean(errorCode || compilationErrors) + + if (isCompilationFailure) { + logger.warn(`[${requestId}] LaTeX compilation failed`, { + status: upstreamResponse.status, + errorCode, + }) return NextResponse.json( - { error: `LaTeX compilation failed (${errorCode})${details}` }, + { error: `LaTeX compilation failed (${errorCode || upstreamResponse.status})${details}` }, { status: 422 } ) } @@ -177,7 +185,7 @@ async function buildCompileErrorResponse( errorCode, }) return NextResponse.json( - { error: `LaTeX compile service error: ${upstreamResponse.status}` }, + { error: `LaTeX compile service error: ${upstreamResponse.status}${details}` }, { status: 502 } ) } From c908687c2a6fb7533795427b30e8974228aa82fb Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 11:47:08 -0700 Subject: [PATCH 3/9] improvement(latex): add compile timeout, cap upstream error code, reject empty resource payloads --- apps/sim/app/api/tools/latex/route.ts | 36 +++++++++++++++++------ apps/sim/lib/api/contracts/tools/latex.ts | 2 ++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/apps/sim/app/api/tools/latex/route.ts b/apps/sim/app/api/tools/latex/route.ts index 31407ec1f7..103848ff2a 100644 --- a/apps/sim/app/api/tools/latex/route.ts +++ b/apps/sim/app/api/tools/latex/route.ts @@ -21,6 +21,9 @@ const DEFAULT_COMPILER = 'pdflatex' const MAX_PDF_BYTES = 25 * 1024 * 1024 const MAX_ERROR_JSON_BYTES = 4 * 1024 * 1024 const MAX_ERROR_MESSAGE_CHARS = 4000 +const MAX_ERROR_CODE_CHARS = 100 +/** Leaves headroom within `maxDuration` to store the PDF after compilation. */ +const COMPILE_TIMEOUT_MS = 50_000 export const dynamic = 'force-dynamic' export const maxDuration = 60 @@ -67,14 +70,26 @@ export const POST = withRouteHandler(async (request: NextRequest) => { resourceCount: body.resources?.length ?? 0, }) - const upstreamResponse = await fetch(LATEX_COMPILE_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - compiler, - resources: [{ main: true, content: body.content }, ...(body.resources ?? [])], - }), - }) + let upstreamResponse: Response + try { + upstreamResponse = await fetch(LATEX_COMPILE_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + compiler, + resources: [{ main: true, content: body.content }, ...(body.resources ?? [])], + }), + signal: AbortSignal.timeout(COMPILE_TIMEOUT_MS), + }) + } catch (error) { + if (error instanceof DOMException && error.name === 'TimeoutError') { + logger.error(`[${requestId}] LaTeX compile service timed out`, { + timeoutMs: COMPILE_TIMEOUT_MS, + }) + return NextResponse.json({ error: 'LaTeX compile service timed out' }, { status: 504 }) + } + throw error + } const upstreamContentType = upstreamResponse.headers.get('content-type') || '' if (!upstreamResponse.ok || !upstreamContentType.includes('application/pdf')) { @@ -160,7 +175,10 @@ async function buildCompileErrorResponse( typeof errorBody === 'object' && errorBody !== null ? (errorBody as Record) : undefined - const errorCode = typeof errorRecord?.error === 'string' ? errorRecord.error : undefined + const errorCode = + typeof errorRecord?.error === 'string' + ? truncate(errorRecord.error, MAX_ERROR_CODE_CHARS) + : undefined const compilationErrors = extractCompilationErrors(errorRecord?.log_files) const details = compilationErrors ? `:\n${compilationErrors}` : '' diff --git a/apps/sim/lib/api/contracts/tools/latex.ts b/apps/sim/lib/api/contracts/tools/latex.ts index 45477e1969..ebbab83937 100644 --- a/apps/sim/lib/api/contracts/tools/latex.ts +++ b/apps/sim/lib/api/contracts/tools/latex.ts @@ -22,10 +22,12 @@ const latexResourceSchema = z .max(512, 'resource path must be at most 512 characters'), content: z .string() + .min(1, 'resource content cannot be empty') .max(MAX_LATEX_SOURCE_CHARS, 'resource content must be at most 1,000,000 characters') .optional(), file: z .string() + .min(1, 'resource file cannot be empty') .max(MAX_LATEX_SOURCE_CHARS, 'resource file must be at most 1,000,000 characters of base64') .optional(), url: z From bd67bd72e8d5125ed7f798f460b729886b3c20ed Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 11:59:20 -0700 Subject: [PATCH 4/9] feat(latex): add package search/lookup and font listing tools --- .../content/docs/en/integrations/latex.mdx | 71 +++++++++- apps/sim/blocks/blocks/latex.ts | 132 +++++++++++++++++- apps/sim/lib/integrations/integrations.json | 23 ++- apps/sim/tools/latex/get_package.ts | 91 ++++++++++++ apps/sim/tools/latex/index.ts | 3 + apps/sim/tools/latex/list_fonts.ts | 92 ++++++++++++ apps/sim/tools/latex/search_packages.ts | 102 ++++++++++++++ apps/sim/tools/latex/types.ts | 64 +++++++++ apps/sim/tools/registry.ts | 10 +- 9 files changed, 576 insertions(+), 12 deletions(-) create mode 100644 apps/sim/tools/latex/get_package.ts create mode 100644 apps/sim/tools/latex/list_fonts.ts create mode 100644 apps/sim/tools/latex/search_packages.ts diff --git a/apps/docs/content/docs/en/integrations/latex.mdx b/apps/docs/content/docs/en/integrations/latex.mdx index c1c6123df4..e6f4400cef 100644 --- a/apps/docs/content/docs/en/integrations/latex.mdx +++ b/apps/docs/content/docs/en/integrations/latex.mdx @@ -27,7 +27,7 @@ In Sim, the LaTeX integration enables your agents to compile LaTeX source into P ## Usage Instructions -Integrates LaTeX into the workflow. Compiles LaTeX source into a PDF file with pdflatex, xelatex, lualatex, platex, uplatex, or context, and supports additional resources such as images, included .tex files, and bibliographies. Does not require OAuth or an API key. +Integrates LaTeX into the workflow. Compiles LaTeX source into a PDF file with pdflatex, xelatex, lualatex, platex, uplatex, or context, and supports additional resources such as images, included .tex files, and bibliographies. Can also look up the TeX Live packages and system fonts available to the compiler. Does not require OAuth or an API key. @@ -55,4 +55,73 @@ Compile a LaTeX document into a PDF. Supports pdflatex, xelatex, lualatex, plate | `fileName` | string | Name of the compiled PDF file | | `compiler` | string | LaTeX compiler used for the build | +### `latex_search_packages` + +Search the TeX Live packages available to the LaTeX compiler by name or description, e.g. to check which packages can be used in a document. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `query` | string | Yes | Search terms matched against package names and descriptions | +| `maxResults` | number | No | Maximum number of packages to return \(default: $\{DEFAULT_MAX_RESULTS\}, max: $\{MAX_RESULTS_LIMIT\}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `packages` | array | TeX Live packages matching the query | +| ↳ `name` | string | Package name | +| ↳ `shortDescription` | string | One-line package description | +| ↳ `installed` | boolean | Whether the package is installed | +| ↳ `ctanUrl` | string | CTAN page for the package | +| `totalMatches` | number | Total number of packages matching the query, before truncation | + +### `latex_get_package` + +Get details about a specific TeX Live package available to the LaTeX compiler, including whether it is installed, its description, license, and related packages. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `name` | string | Yes | Exact package name, e.g. amsmath, tikz, or biblatex | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `package` | json | TeX Live package details | +| ↳ `name` | string | Package name | +| ↳ `installed` | boolean | Whether the package is installed | +| ↳ `shortDescription` | string | One-line package description | +| ↳ `longDescription` | string | Full package description | +| ↳ `category` | string | Package category | +| ↳ `license` | string | Package license identifier | +| ↳ `topics` | array | CTAN topic tags | +| ↳ `relatedPackages` | array | Names of related packages | +| ↳ `homepage` | string | Package homepage URL | +| ↳ `ctanUrl` | string | CTAN page for the package | + +### `latex_list_fonts` + +List the system fonts available to the LaTeX compiler, optionally filtered by name, e.g. to pick a font for xelatex or lualatex documents using fontspec. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `query` | string | No | Filter matched against font family and full font name, e.g. "Noto Serif" | +| `maxResults` | number | No | Maximum number of fonts to return \(default: $\{DEFAULT_MAX_RESULTS\}, max: $\{MAX_RESULTS_LIMIT\}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `fonts` | array | Fonts available to the LaTeX compiler | +| ↳ `family` | string | Font family name | +| ↳ `name` | string | Full font name | +| ↳ `styles` | array | Available styles, e.g. Bold or Italic | +| `totalMatches` | number | Total number of fonts matching the filter, before truncation | + diff --git a/apps/sim/blocks/blocks/latex.ts b/apps/sim/blocks/blocks/latex.ts index 6fab5d180f..21f8a6e5bb 100644 --- a/apps/sim/blocks/blocks/latex.ts +++ b/apps/sim/blocks/blocks/latex.ts @@ -1,20 +1,33 @@ import { LatexIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { IntegrationType } from '@/blocks/types' -import type { LatexCompileResponse } from '@/tools/latex/types' +import type { LatexResponse } from '@/tools/latex/types' -export const LatexBlock: BlockConfig = { +export const LatexBlock: BlockConfig = { type: 'latex', name: 'LaTeX', description: 'Compile LaTeX documents into PDFs', longDescription: - 'Integrates LaTeX into the workflow. Compiles LaTeX source into a PDF file with pdflatex, xelatex, lualatex, platex, uplatex, or context, and supports additional resources such as images, included .tex files, and bibliographies. Does not require OAuth or an API key.', + 'Integrates LaTeX into the workflow. Compiles LaTeX source into a PDF file with pdflatex, xelatex, lualatex, platex, uplatex, or context, and supports additional resources such as images, included .tex files, and bibliographies. Can also look up the TeX Live packages and system fonts available to the compiler. Does not require OAuth or an API key.', docsLink: 'https://docs.sim.ai/integrations/latex', category: 'tools', integrationType: IntegrationType.Documents, bgColor: '#FFFFFF', icon: LatexIcon, subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Compile Document', id: 'latex_compile' }, + { label: 'Search Packages', id: 'latex_search_packages' }, + { label: 'Get Package Details', id: 'latex_get_package' }, + { label: 'List Fonts', id: 'latex_list_fonts' }, + ], + value: () => 'latex_compile', + }, + // Compile Document operation inputs { id: 'content', title: 'LaTeX Source', @@ -22,6 +35,7 @@ export const LatexBlock: BlockConfig = { placeholder: '\\documentclass{article}\n\\begin{document}\nHello, world! $E = mc^2$\n\\end{document}', rows: 10, + condition: { field: 'operation', value: 'latex_compile' }, required: true, }, { @@ -37,6 +51,7 @@ export const LatexBlock: BlockConfig = { { label: 'ConTeXt', id: 'context' }, ], value: () => 'pdflatex', + condition: { field: 'operation', value: 'latex_compile' }, }, { id: 'resources', @@ -45,6 +60,7 @@ export const LatexBlock: BlockConfig = { language: 'json', mode: 'advanced', placeholder: '[{"path": "refs.bib", "content": "..."}]', + condition: { field: 'operation', value: 'latex_compile' }, wandConfig: { enabled: true, prompt: `Generate a JSON array of supporting files for a LaTeX compilation based on the user's description. Each entry must have a "path" (relative file path the LaTeX source references) plus exactly one of: @@ -66,14 +82,91 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, type: 'short-input', mode: 'advanced', placeholder: 'document.pdf', + condition: { field: 'operation', value: 'latex_compile' }, + }, + // Search Packages operation inputs + { + id: 'packageQuery', + title: 'Search Query', + type: 'short-input', + placeholder: 'Search package names and descriptions (e.g. "chemistry", "tikz")...', + condition: { field: 'operation', value: 'latex_search_packages' }, + required: true, + }, + // Get Package Details operation inputs + { + id: 'packageName', + title: 'Package Name', + type: 'short-input', + placeholder: 'Exact package name (e.g. amsmath, tikz, biblatex)', + condition: { field: 'operation', value: 'latex_get_package' }, + required: true, + }, + // List Fonts operation inputs + { + id: 'fontQuery', + title: 'Font Filter', + type: 'short-input', + placeholder: 'Filter by font family or name (e.g. "Noto Serif")...', + condition: { field: 'operation', value: 'latex_list_fonts' }, + }, + { + id: 'maxResults', + title: 'Max Results', + type: 'short-input', + mode: 'advanced', + placeholder: '25', + condition: { + field: 'operation', + value: ['latex_search_packages', 'latex_list_fonts'], + }, }, ], tools: { - access: ['latex_compile'], + access: ['latex_compile', 'latex_search_packages', 'latex_get_package', 'latex_list_fonts'], config: { - tool: () => 'latex_compile', + tool: (params) => { + switch (params.operation) { + case 'latex_search_packages': + return 'latex_search_packages' + case 'latex_get_package': + return 'latex_get_package' + case 'latex_list_fonts': + return 'latex_list_fonts' + default: + return 'latex_compile' + } + }, params: (params) => { - const { compiler, fileName, resources, ...rest } = params + const { + operation, + compiler, + fileName, + resources, + packageQuery, + packageName, + fontQuery, + maxResults, + ...rest + } = params + + if (operation === 'latex_search_packages') { + return { + query: packageQuery, + ...(maxResults ? { maxResults: Number(maxResults) } : {}), + } + } + + if (operation === 'latex_get_package') { + return { name: packageName } + } + + if (operation === 'latex_list_fonts') { + return { + ...(typeof fontQuery === 'string' && fontQuery.trim() ? { query: fontQuery } : {}), + ...(maxResults ? { maxResults: Number(maxResults) } : {}), + } + } let parsedResources: unknown if (typeof resources === 'string' && resources.trim()) { @@ -96,16 +189,41 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, }, }, inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + // Compile Document operation content: { type: 'string', description: 'LaTeX source of the main document' }, compiler: { type: 'string', description: 'LaTeX compiler to use' }, resources: { type: 'json', description: 'Supporting files for the compilation' }, fileName: { type: 'string', description: 'Name for the generated PDF file' }, + // Search Packages operation + packageQuery: { type: 'string', description: 'Package search terms' }, + // Get Package Details operation + packageName: { type: 'string', description: 'Exact TeX Live package name' }, + // List Fonts operation + fontQuery: { type: 'string', description: 'Font family or name filter' }, + maxResults: { type: 'number', description: 'Maximum results to return' }, }, outputs: { + // Compile Document output pdf: { type: 'file', description: 'Compiled PDF file' }, pdfUrl: { type: 'string', description: 'URL of the compiled PDF' }, fileName: { type: 'string', description: 'Name of the compiled PDF file' }, compiler: { type: 'string', description: 'LaTeX compiler used for the build' }, + // Search Packages output + packages: { + type: 'json', + description: 'Matching TeX Live packages [{name, shortDescription, installed, ctanUrl}]', + }, + // Get Package Details output + package: { + type: 'json', + description: + 'Package details (name, installed, shortDescription, longDescription, category, license, topics, relatedPackages, homepage, ctanUrl)', + }, + // List Fonts output + fonts: { type: 'json', description: 'Available fonts [{family, name, styles}]' }, + // Shared search/list output + totalMatches: { type: 'number', description: 'Total matches found before truncation' }, }, } @@ -225,7 +343,7 @@ export const LatexBlockMeta = { description: 'Diagnose failed LaTeX builds from the compiler error output and iterate until the document compiles. Use when a compilation returns errors instead of a PDF.', content: - '# Fix Compilation Errors\n\nGet a failing LaTeX document to build.\n\n## Steps\n1. Read the TeX error lines from the failed compile (lines starting with !), which name the problem and its location.\n2. Apply the targeted fix: missing packages, unescaped special characters, unmatched braces or environments, or commands needing a different compiler (e.g. fontspec requires xelatex or lualatex).\n3. Recompile and repeat until the build succeeds.\n4. Keep edits minimal — fix the errors without rewriting the document.\n\n## Output\nThe compiled PDF and a short list of the fixes that were applied.', + '# Fix Compilation Errors\n\nGet a failing LaTeX document to build.\n\n## Steps\n1. Read the TeX error lines from the failed compile (lines starting with !), which name the problem and its location.\n2. Apply the targeted fix: missing packages (verify availability with Get Package Details or Search Packages), unescaped special characters, unmatched braces or environments, or commands needing a different compiler (e.g. fontspec requires xelatex or lualatex — confirm the font exists with List Fonts).\n3. Recompile and repeat until the build succeeds.\n4. Keep edits minimal — fix the errors without rewriting the document.\n\n## Output\nThe compiled PDF and a short list of the fixes that were applied.', }, ], } as const satisfies BlockMeta diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index f85fb951e0..60f2664fe9 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -8244,12 +8244,29 @@ "slug": "latex", "name": "LaTeX", "description": "Compile LaTeX documents into PDFs", - "longDescription": "Integrates LaTeX into the workflow. Compiles LaTeX source into a PDF file with pdflatex, xelatex, lualatex, platex, uplatex, or context, and supports additional resources such as images, included .tex files, and bibliographies. Does not require OAuth or an API key.", + "longDescription": "Integrates LaTeX into the workflow. Compiles LaTeX source into a PDF file with pdflatex, xelatex, lualatex, platex, uplatex, or context, and supports additional resources such as images, included .tex files, and bibliographies. Can also look up the TeX Live packages and system fonts available to the compiler. Does not require OAuth or an API key.", "bgColor": "#FFFFFF", "iconName": "LatexIcon", "docsUrl": "https://docs.sim.ai/integrations/latex", - "operations": [], - "operationCount": 0, + "operations": [ + { + "name": "Compile Document", + "description": "Compile a LaTeX document into a PDF. Supports pdflatex, xelatex, lualatex, platex, uplatex, and context, plus supporting resources such as images, included .tex files, and bibliographies." + }, + { + "name": "Search Packages", + "description": "Search the TeX Live packages available to the LaTeX compiler by name or description, e.g. to check which packages can be used in a document." + }, + { + "name": "Get Package Details", + "description": "Get details about a specific TeX Live package available to the LaTeX compiler, including whether it is installed, its description, license, and related packages." + }, + { + "name": "List Fonts", + "description": "List the system fonts available to the LaTeX compiler, optionally filtered by name, e.g. to pick a font for xelatex or lualatex documents using fontspec." + } + ], + "operationCount": 4, "triggers": [], "triggerCount": 0, "authType": "none", diff --git a/apps/sim/tools/latex/get_package.ts b/apps/sim/tools/latex/get_package.ts new file mode 100644 index 0000000000..6441d42747 --- /dev/null +++ b/apps/sim/tools/latex/get_package.ts @@ -0,0 +1,91 @@ +import type { LatexGetPackageParams, LatexGetPackageResponse } from '@/tools/latex/types' +import type { ToolConfig } from '@/tools/types' + +export const latexGetPackageTool: ToolConfig = { + id: 'latex_get_package', + name: 'LaTeX Get Package', + description: + 'Get details about a specific TeX Live package available to the LaTeX compiler, including whether it is installed, its description, license, and related packages.', + version: '1.0.0', + + params: { + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Exact package name, e.g. amsmath, tikz, or biblatex', + }, + }, + + request: { + url: (params) => `https://latex.ytotech.com/packages/${encodeURIComponent(params.name.trim())}`, + method: 'GET', + headers: () => ({ + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as { + package?: { + package?: string + installed?: boolean + shortdesc?: string + longdesc?: string + category?: string + 'cat-license'?: string + 'cat-topics'?: string[] + 'cat-related'?: string + 'cat-contact-home'?: string + url_ctan?: string + } + } + + const pkg = data.package ?? {} + + return { + success: true, + output: { + package: { + name: pkg.package ?? '', + installed: pkg.installed ?? false, + shortDescription: pkg.shortdesc ?? null, + longDescription: pkg.longdesc ?? null, + category: pkg.category ?? null, + license: pkg['cat-license'] ?? null, + topics: pkg['cat-topics'] ?? [], + relatedPackages: pkg['cat-related']?.split(/\s+/).filter(Boolean) ?? [], + homepage: pkg['cat-contact-home'] ?? null, + ctanUrl: pkg.url_ctan ?? null, + }, + }, + } + }, + + outputs: { + package: { + type: 'json', + description: 'TeX Live package details', + properties: { + name: { type: 'string', description: 'Package name' }, + installed: { type: 'boolean', description: 'Whether the package is installed' }, + shortDescription: { + type: 'string', + description: 'One-line package description', + optional: true, + }, + longDescription: { + type: 'string', + description: 'Full package description', + optional: true, + }, + category: { type: 'string', description: 'Package category', optional: true }, + license: { type: 'string', description: 'Package license identifier', optional: true }, + topics: { type: 'array', description: 'CTAN topic tags' }, + relatedPackages: { type: 'array', description: 'Names of related packages' }, + homepage: { type: 'string', description: 'Package homepage URL', optional: true }, + ctanUrl: { type: 'string', description: 'CTAN page for the package', optional: true }, + }, + }, + }, +} diff --git a/apps/sim/tools/latex/index.ts b/apps/sim/tools/latex/index.ts index e055b10e6e..42b1e0808b 100644 --- a/apps/sim/tools/latex/index.ts +++ b/apps/sim/tools/latex/index.ts @@ -1 +1,4 @@ export { latexCompileTool } from '@/tools/latex/compile' +export { latexGetPackageTool } from '@/tools/latex/get_package' +export { latexListFontsTool } from '@/tools/latex/list_fonts' +export { latexSearchPackagesTool } from '@/tools/latex/search_packages' diff --git a/apps/sim/tools/latex/list_fonts.ts b/apps/sim/tools/latex/list_fonts.ts new file mode 100644 index 0000000000..4368e482ba --- /dev/null +++ b/apps/sim/tools/latex/list_fonts.ts @@ -0,0 +1,92 @@ +import type { LatexFont, LatexListFontsParams, LatexListFontsResponse } from '@/tools/latex/types' +import type { ToolConfig } from '@/tools/types' + +const DEFAULT_MAX_RESULTS = 50 +const MAX_RESULTS_LIMIT = 200 + +export const latexListFontsTool: ToolConfig = { + id: 'latex_list_fonts', + name: 'LaTeX List Fonts', + description: + 'List the system fonts available to the LaTeX compiler, optionally filtered by name, e.g. to pick a font for xelatex or lualatex documents using fontspec.', + version: '1.0.0', + + params: { + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter matched against font family and full font name, e.g. "Noto Serif"', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: `Maximum number of fonts to return (default: ${DEFAULT_MAX_RESULTS}, max: ${MAX_RESULTS_LIMIT})`, + }, + }, + + request: { + url: 'https://latex.ytotech.com/fonts', + method: 'GET', + headers: () => ({ + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response, params?: LatexListFontsParams) => { + const data = (await response.json()) as { + fonts?: Array<{ + family?: string + name?: string + styles?: string[] + }> + } + + const query = (params?.query ?? '').trim().toLowerCase() + const matches = (data.fonts ?? []).filter((font) => { + if (!query) return true + return ( + (font.family ?? '').toLowerCase().includes(query) || + (font.name ?? '').toLowerCase().includes(query) + ) + }) + + const maxResults = Math.min( + Math.max(Math.trunc(params?.maxResults ?? DEFAULT_MAX_RESULTS), 1), + MAX_RESULTS_LIMIT + ) + const fonts: LatexFont[] = matches.slice(0, maxResults).map((font) => ({ + family: font.family ?? '', + name: font.name ?? '', + styles: font.styles ?? [], + })) + + return { + success: true, + output: { + fonts, + totalMatches: matches.length, + }, + } + }, + + outputs: { + fonts: { + type: 'array', + description: 'Fonts available to the LaTeX compiler', + items: { + type: 'object', + properties: { + family: { type: 'string', description: 'Font family name' }, + name: { type: 'string', description: 'Full font name' }, + styles: { type: 'array', description: 'Available styles, e.g. Bold or Italic' }, + }, + }, + }, + totalMatches: { + type: 'number', + description: 'Total number of fonts matching the filter, before truncation', + }, + }, +} diff --git a/apps/sim/tools/latex/search_packages.ts b/apps/sim/tools/latex/search_packages.ts new file mode 100644 index 0000000000..3478c22463 --- /dev/null +++ b/apps/sim/tools/latex/search_packages.ts @@ -0,0 +1,102 @@ +import type { + LatexPackageSummary, + LatexSearchPackagesParams, + LatexSearchPackagesResponse, +} from '@/tools/latex/types' +import type { ToolConfig } from '@/tools/types' + +const DEFAULT_MAX_RESULTS = 25 +const MAX_RESULTS_LIMIT = 100 + +export const latexSearchPackagesTool: ToolConfig< + LatexSearchPackagesParams, + LatexSearchPackagesResponse +> = { + id: 'latex_search_packages', + name: 'LaTeX Search Packages', + description: + 'Search the TeX Live packages available to the LaTeX compiler by name or description, e.g. to check which packages can be used in a document.', + version: '1.0.0', + + params: { + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Search terms matched against package names and descriptions', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: `Maximum number of packages to return (default: ${DEFAULT_MAX_RESULTS}, max: ${MAX_RESULTS_LIMIT})`, + }, + }, + + request: { + url: 'https://latex.ytotech.com/packages', + method: 'GET', + headers: () => ({ + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response, params?: LatexSearchPackagesParams) => { + const data = (await response.json()) as { + packages?: Array<{ + name?: string + shortdesc?: string + installed?: boolean + url_ctan?: string + }> + } + + const query = (params?.query ?? '').trim().toLowerCase() + const matches = (data.packages ?? []).filter((pkg) => { + if (!query) return true + return ( + (pkg.name ?? '').toLowerCase().includes(query) || + (pkg.shortdesc ?? '').toLowerCase().includes(query) + ) + }) + + const maxResults = Math.min( + Math.max(Math.trunc(params?.maxResults ?? DEFAULT_MAX_RESULTS), 1), + MAX_RESULTS_LIMIT + ) + const packages: LatexPackageSummary[] = matches.slice(0, maxResults).map((pkg) => ({ + name: pkg.name ?? '', + shortDescription: pkg.shortdesc ?? null, + installed: pkg.installed ?? false, + ctanUrl: pkg.url_ctan ?? null, + })) + + return { + success: true, + output: { + packages, + totalMatches: matches.length, + }, + } + }, + + outputs: { + packages: { + type: 'array', + description: 'TeX Live packages matching the query', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Package name' }, + shortDescription: { type: 'string', description: 'One-line package description' }, + installed: { type: 'boolean', description: 'Whether the package is installed' }, + ctanUrl: { type: 'string', description: 'CTAN page for the package' }, + }, + }, + }, + totalMatches: { + type: 'number', + description: 'Total number of packages matching the query, before truncation', + }, + }, +} diff --git a/apps/sim/tools/latex/types.ts b/apps/sim/tools/latex/types.ts index bb993e7bc6..1dde62c186 100644 --- a/apps/sim/tools/latex/types.ts +++ b/apps/sim/tools/latex/types.ts @@ -29,3 +29,67 @@ export interface LatexCompileResponse extends ToolResponse { compiler: string } } + +export interface LatexSearchPackagesParams { + query: string + maxResults?: number +} + +export interface LatexPackageSummary { + name: string + shortDescription: string | null + installed: boolean + ctanUrl: string | null +} + +export interface LatexSearchPackagesResponse extends ToolResponse { + output: { + packages: LatexPackageSummary[] + totalMatches: number + } +} + +export interface LatexGetPackageParams { + name: string +} + +export interface LatexGetPackageResponse extends ToolResponse { + output: { + package: { + name: string + installed: boolean + shortDescription: string | null + longDescription: string | null + category: string | null + license: string | null + topics: string[] + relatedPackages: string[] + homepage: string | null + ctanUrl: string | null + } + } +} + +export interface LatexListFontsParams { + query?: string + maxResults?: number +} + +export interface LatexFont { + family: string + name: string + styles: string[] +} + +export interface LatexListFontsResponse extends ToolResponse { + output: { + fonts: LatexFont[] + totalMatches: number + } +} + +export type LatexResponse = + | LatexCompileResponse + | LatexSearchPackagesResponse + | LatexGetPackageResponse + | LatexListFontsResponse diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index c8c869fe7d..0674a87f51 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1694,7 +1694,12 @@ import { knowledgeUpsertDocumentTool, } from '@/tools/knowledge' import { langsmithCreateRunsBatchTool, langsmithCreateRunTool } from '@/tools/langsmith' -import { latexCompileTool } from '@/tools/latex' +import { + latexCompileTool, + latexGetPackageTool, + latexListFontsTool, + latexSearchPackagesTool, +} from '@/tools/latex' import { launchDarklyCreateFlagTool, launchDarklyDeleteFlagTool, @@ -5794,6 +5799,9 @@ export const tools: Record = { langsmith_create_run: langsmithCreateRunTool, langsmith_create_runs_batch: langsmithCreateRunsBatchTool, latex_compile: latexCompileTool, + latex_get_package: latexGetPackageTool, + latex_list_fonts: latexListFontsTool, + latex_search_packages: latexSearchPackagesTool, launchdarkly_create_flag: launchDarklyCreateFlagTool, launchdarkly_delete_flag: launchDarklyDeleteFlagTool, launchdarkly_get_audit_log: launchDarklyGetAuditLogTool, From e84a50abe6926bbc97c62295783b80163ba01309 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 12:08:13 -0700 Subject: [PATCH 5/9] =?UTF-8?q?improvement(latex):=20address=20review=20fi?= =?UTF-8?q?ndings=20=E2=80=94=20path=20traversal=20guard,=20http(s)-only?= =?UTF-8?q?=20resource=20urls,=20timeout=20abort=20handling,=20typed=20pdf?= =?UTF-8?q?=20output,=20NaN-safe=20maxResults,=20empty-query=20rejection,?= =?UTF-8?q?=20third-party=20disclosure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/docs/en/integrations/latex.mdx | 12 +++++---- apps/sim/app/api/tools/latex/route.ts | 7 ++++- apps/sim/blocks/blocks/latex.ts | 12 ++++++--- apps/sim/lib/api/contracts/tools/latex.ts | 10 ++++++- apps/sim/lib/integrations/integrations.json | 4 +-- apps/sim/tools/latex/compile.ts | 5 ++-- apps/sim/tools/latex/list_fonts.ts | 11 ++++---- apps/sim/tools/latex/search_packages.ts | 27 ++++++++++++------- apps/sim/tools/latex/types.ts | 13 ++++++++- 9 files changed, 71 insertions(+), 30 deletions(-) diff --git a/apps/docs/content/docs/en/integrations/latex.mdx b/apps/docs/content/docs/en/integrations/latex.mdx index e6f4400cef..e594a58441 100644 --- a/apps/docs/content/docs/en/integrations/latex.mdx +++ b/apps/docs/content/docs/en/integrations/latex.mdx @@ -21,13 +21,15 @@ With LaTeX, you can: - **Include supporting files**: Add images, included `.tex` files, bibliographies, and custom classes or styles to the build - **Automate document production**: Generate reports, invoices, certificates, and papers from templates -In Sim, the LaTeX integration enables your agents to compile LaTeX source into PDF files as part of any workflow — no OAuth or API key required. Agents can draft documents in LaTeX, attach supporting resources such as images or BibTeX bibliographies, and produce a finished PDF that downstream blocks can email, upload, or store. This makes it easy to build agents that generate reports, typeset research digests, or produce templated documents like invoices and certificates. +In Sim, the LaTeX integration enables your agents to compile LaTeX source into PDF files as part of any workflow — no OAuth or API key required. Agents can draft documents in LaTeX, attach supporting resources such as images or BibTeX bibliographies, and produce a finished PDF that downstream blocks can email, upload, or store. Agents can also search the available TeX Live packages and system fonts to pick the right tools for a document. This makes it easy to build agents that generate reports, typeset research digests, or produce templated documents like invoices and certificates. + +Note: compilation runs on the public [LaTeX-on-HTTP](https://github.com/YtoTech/latex-on-http) service at latex.ytotech.com, so the document source and any attached resources are sent to that third-party service. Avoid compiling documents whose contents must not leave your environment. {/* MANUAL-CONTENT-END */} ## Usage Instructions -Integrates LaTeX into the workflow. Compiles LaTeX source into a PDF file with pdflatex, xelatex, lualatex, platex, uplatex, or context, and supports additional resources such as images, included .tex files, and bibliographies. Can also look up the TeX Live packages and system fonts available to the compiler. Does not require OAuth or an API key. +Integrates LaTeX into the workflow. Compiles LaTeX source into a PDF file with pdflatex, xelatex, lualatex, platex, uplatex, or context, and supports additional resources such as images, included .tex files, and bibliographies. Can also look up the TeX Live packages and system fonts available to the compiler. Does not require OAuth or an API key. Compilation runs on the public LaTeX-on-HTTP service (latex.ytotech.com), so document source and resources are sent to that third-party service. @@ -35,7 +37,7 @@ Integrates LaTeX into the workflow. Compiles LaTeX source into a PDF file with p ### `latex_compile` -Compile a LaTeX document into a PDF. Supports pdflatex, xelatex, lualatex, platex, uplatex, and context, plus supporting resources such as images, included .tex files, and bibliographies. +Compile a LaTeX document into a PDF via the public LaTeX-on-HTTP service (latex.ytotech.com). Supports pdflatex, xelatex, lualatex, platex, uplatex, and context, plus supporting resources such as images, included .tex files, and bibliographies. #### Input @@ -64,7 +66,7 @@ Search the TeX Live packages available to the LaTeX compiler by name or descript | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `query` | string | Yes | Search terms matched against package names and descriptions | -| `maxResults` | number | No | Maximum number of packages to return \(default: $\{DEFAULT_MAX_RESULTS\}, max: $\{MAX_RESULTS_LIMIT\}\) | +| `maxResults` | number | No | Maximum number of packages to return \(default: 25, max: 100\) | #### Output @@ -112,7 +114,7 @@ List the system fonts available to the LaTeX compiler, optionally filtered by na | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `query` | string | No | Filter matched against font family and full font name, e.g. "Noto Serif" | -| `maxResults` | number | No | Maximum number of fonts to return \(default: $\{DEFAULT_MAX_RESULTS\}, max: $\{MAX_RESULTS_LIMIT\}\) | +| `maxResults` | number | No | Maximum number of fonts to return \(default: 50, max: 200\) | #### Output diff --git a/apps/sim/app/api/tools/latex/route.ts b/apps/sim/app/api/tools/latex/route.ts index 103848ff2a..3890ab2b2f 100644 --- a/apps/sim/app/api/tools/latex/route.ts +++ b/apps/sim/app/api/tools/latex/route.ts @@ -82,7 +82,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => { signal: AbortSignal.timeout(COMPILE_TIMEOUT_MS), }) } catch (error) { - if (error instanceof DOMException && error.name === 'TimeoutError') { + // The timeout signal is the only abort source on this fetch, so an + // AbortError here is a timeout regardless of which name undici uses. + if ( + error instanceof DOMException && + (error.name === 'TimeoutError' || error.name === 'AbortError') + ) { logger.error(`[${requestId}] LaTeX compile service timed out`, { timeoutMs: COMPILE_TIMEOUT_MS, }) diff --git a/apps/sim/blocks/blocks/latex.ts b/apps/sim/blocks/blocks/latex.ts index 21f8a6e5bb..4c1516eb76 100644 --- a/apps/sim/blocks/blocks/latex.ts +++ b/apps/sim/blocks/blocks/latex.ts @@ -8,7 +8,7 @@ export const LatexBlock: BlockConfig = { name: 'LaTeX', description: 'Compile LaTeX documents into PDFs', longDescription: - 'Integrates LaTeX into the workflow. Compiles LaTeX source into a PDF file with pdflatex, xelatex, lualatex, platex, uplatex, or context, and supports additional resources such as images, included .tex files, and bibliographies. Can also look up the TeX Live packages and system fonts available to the compiler. Does not require OAuth or an API key.', + 'Integrates LaTeX into the workflow. Compiles LaTeX source into a PDF file with pdflatex, xelatex, lualatex, platex, uplatex, or context, and supports additional resources such as images, included .tex files, and bibliographies. Can also look up the TeX Live packages and system fonts available to the compiler. Does not require OAuth or an API key. Compilation runs on the public LaTeX-on-HTTP service (latex.ytotech.com), so document source and resources are sent to that third-party service.', docsLink: 'https://docs.sim.ai/integrations/latex', category: 'tools', integrationType: IntegrationType.Documents, @@ -150,10 +150,16 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, ...rest } = params + const parsedMaxResults = Number(maxResults) + const maxResultsParam = + Number.isFinite(parsedMaxResults) && parsedMaxResults > 0 + ? { maxResults: parsedMaxResults } + : {} + if (operation === 'latex_search_packages') { return { query: packageQuery, - ...(maxResults ? { maxResults: Number(maxResults) } : {}), + ...maxResultsParam, } } @@ -164,7 +170,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, if (operation === 'latex_list_fonts') { return { ...(typeof fontQuery === 'string' && fontQuery.trim() ? { query: fontQuery } : {}), - ...(maxResults ? { maxResults: Number(maxResults) } : {}), + ...maxResultsParam, } } diff --git a/apps/sim/lib/api/contracts/tools/latex.ts b/apps/sim/lib/api/contracts/tools/latex.ts index ebbab83937..665619711e 100644 --- a/apps/sim/lib/api/contracts/tools/latex.ts +++ b/apps/sim/lib/api/contracts/tools/latex.ts @@ -19,7 +19,11 @@ const latexResourceSchema = z path: z .string() .min(1, 'resource path cannot be empty') - .max(512, 'resource path must be at most 512 characters'), + .max(512, 'resource path must be at most 512 characters') + .refine( + (path) => !path.startsWith('/') && path.split(/[/\\]/).every((segment) => segment !== '..'), + 'resource path must be relative and must not contain ".." segments' + ), content: z .string() .min(1, 'resource content cannot be empty') @@ -34,6 +38,10 @@ const latexResourceSchema = z .string() .url('resource url must be a valid URL') .max(2048, 'resource url must be at most 2048 characters') + .refine( + (url) => url.startsWith('https://') || url.startsWith('http://'), + 'resource url must use http or https' + ) .optional(), }) .superRefine((resource, ctx) => { diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 60f2664fe9..8241512509 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -8244,14 +8244,14 @@ "slug": "latex", "name": "LaTeX", "description": "Compile LaTeX documents into PDFs", - "longDescription": "Integrates LaTeX into the workflow. Compiles LaTeX source into a PDF file with pdflatex, xelatex, lualatex, platex, uplatex, or context, and supports additional resources such as images, included .tex files, and bibliographies. Can also look up the TeX Live packages and system fonts available to the compiler. Does not require OAuth or an API key.", + "longDescription": "Integrates LaTeX into the workflow. Compiles LaTeX source into a PDF file with pdflatex, xelatex, lualatex, platex, uplatex, or context, and supports additional resources such as images, included .tex files, and bibliographies. Can also look up the TeX Live packages and system fonts available to the compiler. Does not require OAuth or an API key. Compilation runs on the public LaTeX-on-HTTP service (latex.ytotech.com), so document source and resources are sent to that third-party service.", "bgColor": "#FFFFFF", "iconName": "LatexIcon", "docsUrl": "https://docs.sim.ai/integrations/latex", "operations": [ { "name": "Compile Document", - "description": "Compile a LaTeX document into a PDF. Supports pdflatex, xelatex, lualatex, platex, uplatex, and context, plus supporting resources such as images, included .tex files, and bibliographies." + "description": "Compile a LaTeX document into a PDF via the public LaTeX-on-HTTP service (latex.ytotech.com). Supports pdflatex, xelatex, lualatex, platex, uplatex, and context, plus supporting resources such as images, included .tex files, and bibliographies." }, { "name": "Search Packages", diff --git a/apps/sim/tools/latex/compile.ts b/apps/sim/tools/latex/compile.ts index 9c91858ae9..4be1c4352a 100644 --- a/apps/sim/tools/latex/compile.ts +++ b/apps/sim/tools/latex/compile.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { LatexCompileParams, LatexCompileResponse } from '@/tools/latex/types' import type { ToolConfig } from '@/tools/types' @@ -5,7 +6,7 @@ export const latexCompileTool: ToolConfig { const data = (await response.json()) as { error?: string - pdfFile?: unknown + pdfFile?: UserFile pdfUrl?: string fileName?: string compiler?: string diff --git a/apps/sim/tools/latex/list_fonts.ts b/apps/sim/tools/latex/list_fonts.ts index 4368e482ba..686268a1d6 100644 --- a/apps/sim/tools/latex/list_fonts.ts +++ b/apps/sim/tools/latex/list_fonts.ts @@ -22,7 +22,7 @@ export const latexListFontsTool: ToolConfig 0 + ? Math.min(requested, MAX_RESULTS_LIMIT) + : DEFAULT_MAX_RESULTS const fonts: LatexFont[] = matches.slice(0, maxResults).map((font) => ({ family: font.family ?? '', name: font.name ?? '', diff --git a/apps/sim/tools/latex/search_packages.ts b/apps/sim/tools/latex/search_packages.ts index 3478c22463..a249233260 100644 --- a/apps/sim/tools/latex/search_packages.ts +++ b/apps/sim/tools/latex/search_packages.ts @@ -29,7 +29,7 @@ export const latexSearchPackagesTool: ToolConfig< type: 'number', required: false, visibility: 'user-or-llm', - description: `Maximum number of packages to return (default: ${DEFAULT_MAX_RESULTS}, max: ${MAX_RESULTS_LIMIT})`, + description: 'Maximum number of packages to return (default: 25, max: 100)', }, }, @@ -52,18 +52,25 @@ export const latexSearchPackagesTool: ToolConfig< } const query = (params?.query ?? '').trim().toLowerCase() - const matches = (data.packages ?? []).filter((pkg) => { - if (!query) return true - return ( + if (!query) { + return { + success: false, + error: 'Search query cannot be empty', + output: { packages: [], totalMatches: 0 }, + } + } + + const matches = (data.packages ?? []).filter( + (pkg) => (pkg.name ?? '').toLowerCase().includes(query) || (pkg.shortdesc ?? '').toLowerCase().includes(query) - ) - }) - - const maxResults = Math.min( - Math.max(Math.trunc(params?.maxResults ?? DEFAULT_MAX_RESULTS), 1), - MAX_RESULTS_LIMIT ) + + const requested = Math.trunc(Number(params?.maxResults)) + const maxResults = + Number.isFinite(requested) && requested > 0 + ? Math.min(requested, MAX_RESULTS_LIMIT) + : DEFAULT_MAX_RESULTS const packages: LatexPackageSummary[] = matches.slice(0, maxResults).map((pkg) => ({ name: pkg.name ?? '', shortDescription: pkg.shortdesc ?? null, diff --git a/apps/sim/tools/latex/types.ts b/apps/sim/tools/latex/types.ts index 1dde62c186..c9b536e2f6 100644 --- a/apps/sim/tools/latex/types.ts +++ b/apps/sim/tools/latex/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { ToolResponse } from '@/tools/types' export type LatexCompiler = 'pdflatex' | 'xelatex' | 'lualatex' | 'platex' | 'uplatex' | 'context' @@ -21,9 +22,19 @@ export interface LatexCompileParams { resources?: LatexResource[] } +/** + * Reference to the compiled PDF when no execution-file context is available; + * with execution context the output is a full {@link UserFile}. + */ +export interface LatexPdfReference { + name: string + url: string + mimeType: string +} + export interface LatexCompileResponse extends ToolResponse { output: { - pdf: unknown + pdf: UserFile | LatexPdfReference | '' pdfUrl: string fileName: string compiler: string From 81bf0927d58f97bb7bf3f420a9cd5751966ae565 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 12:17:50 -0700 Subject: [PATCH 6/9] fix(latex): fail compile/package lookups that return no payload --- apps/sim/tools/latex/compile.ts | 13 +++++++++++++ apps/sim/tools/latex/get_package.ts | 24 ++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/apps/sim/tools/latex/compile.ts b/apps/sim/tools/latex/compile.ts index 4be1c4352a..818de6fd37 100644 --- a/apps/sim/tools/latex/compile.ts +++ b/apps/sim/tools/latex/compile.ts @@ -100,6 +100,19 @@ export const latexCompileTool: ToolConfig Date: Thu, 11 Jun 2026 12:37:32 -0700 Subject: [PATCH 7/9] improvement(latex): guard lookup tool responses against upstream errors --- apps/sim/tools/latex/get_package.ts | 7 +++++-- apps/sim/tools/latex/list_fonts.ts | 9 +++++++++ apps/sim/tools/latex/search_packages.ts | 9 +++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/sim/tools/latex/get_package.ts b/apps/sim/tools/latex/get_package.ts index af50c73a05..d108e94b41 100644 --- a/apps/sim/tools/latex/get_package.ts +++ b/apps/sim/tools/latex/get_package.ts @@ -27,6 +27,7 @@ export const latexGetPackageTool: ToolConfig { const data = (await response.json()) as { + error?: string package?: { package?: string installed?: boolean @@ -42,10 +43,12 @@ export const latexGetPackageTool: ToolConfig { const data = (await response.json()) as { + error?: string fonts?: Array<{ family?: string name?: string @@ -43,6 +44,14 @@ export const latexListFontsTool: ToolConfig } + if (!response.ok || data.error) { + return { + success: false, + error: data.error || `LaTeX font listing failed (${response.status})`, + output: { fonts: [], totalMatches: 0 }, + } + } + const query = (params?.query ?? '').trim().toLowerCase() const matches = (data.fonts ?? []).filter((font) => { if (!query) return true diff --git a/apps/sim/tools/latex/search_packages.ts b/apps/sim/tools/latex/search_packages.ts index a249233260..e41acb09cf 100644 --- a/apps/sim/tools/latex/search_packages.ts +++ b/apps/sim/tools/latex/search_packages.ts @@ -43,6 +43,7 @@ export const latexSearchPackagesTool: ToolConfig< transformResponse: async (response: Response, params?: LatexSearchPackagesParams) => { const data = (await response.json()) as { + error?: string packages?: Array<{ name?: string shortdesc?: string @@ -51,6 +52,14 @@ export const latexSearchPackagesTool: ToolConfig< }> } + if (!response.ok || data.error) { + return { + success: false, + error: data.error || `LaTeX package search failed (${response.status})`, + output: { packages: [], totalMatches: 0 }, + } + } + const query = (params?.query ?? '').trim().toLowerCase() if (!query) { return { From 22516fe341e189d6e76a0f8a328bbed4af653159 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 12:44:14 -0700 Subject: [PATCH 8/9] improvement(latex): reject whitespace-only package names at the block boundary --- apps/sim/blocks/blocks/latex.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/sim/blocks/blocks/latex.ts b/apps/sim/blocks/blocks/latex.ts index 4c1516eb76..badc42689b 100644 --- a/apps/sim/blocks/blocks/latex.ts +++ b/apps/sim/blocks/blocks/latex.ts @@ -164,7 +164,11 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, } if (operation === 'latex_get_package') { - return { name: packageName } + const effectivePackageName = typeof packageName === 'string' ? packageName.trim() : '' + if (!effectivePackageName) { + throw new Error('Package name is required.') + } + return { name: effectivePackageName } } if (operation === 'latex_list_fonts') { From 9c98876dd8a2db4c4b8c44e5f83e7a4fed61ce2a Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 11 Jun 2026 12:50:59 -0700 Subject: [PATCH 9/9] improvement(latex): make relatedPackages fallback explicit --- apps/sim/tools/latex/get_package.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/sim/tools/latex/get_package.ts b/apps/sim/tools/latex/get_package.ts index d108e94b41..a8261b955c 100644 --- a/apps/sim/tools/latex/get_package.ts +++ b/apps/sim/tools/latex/get_package.ts @@ -77,7 +77,9 @@ export const latexGetPackageTool: ToolConfig