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..e594a58441 --- /dev/null +++ b/apps/docs/content/docs/en/integrations/latex.mdx @@ -0,0 +1,129 @@ +--- +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. 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. Compilation runs on the public LaTeX-on-HTTP service (latex.ytotech.com), so document source and resources are sent to that third-party service. + + + +## Actions + +### `latex_compile` + +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 + +| 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 | + +### `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: 25, max: 100\) | + +#### 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: 50, max: 200\) | + +#### 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/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..3890ab2b2f --- /dev/null +++ b/apps/sim/app/api/tools/latex/route.ts @@ -0,0 +1,269 @@ +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 +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 + +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, + }) + + 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) { + // 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, + }) + 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')) { + 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' + ? truncate(errorRecord.error, MAX_ERROR_CODE_CHARS) + : undefined + const compilationErrors = extractCompilationErrors(errorRecord?.log_files) + 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 || upstreamResponse.status})${details}` }, + { status: 422 } + ) + } + + logger.error(`[${requestId}] LaTeX compile service error`, { + status: upstreamResponse.status, + errorCode, + }) + return NextResponse.json( + { error: `LaTeX compile service error: ${upstreamResponse.status}${details}` }, + { 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..badc42689b --- /dev/null +++ b/apps/sim/blocks/blocks/latex.ts @@ -0,0 +1,359 @@ +import { LatexIcon } from '@/components/icons' +import type { BlockConfig, BlockMeta } from '@/blocks/types' +import { IntegrationType } from '@/blocks/types' +import type { LatexResponse } 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. 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, + 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', + type: 'long-input', + placeholder: + '\\documentclass{article}\n\\begin{document}\nHello, world! $E = mc^2$\n\\end{document}', + rows: 10, + condition: { field: 'operation', value: 'latex_compile' }, + 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', + condition: { field: 'operation', value: 'latex_compile' }, + }, + { + id: 'resources', + title: 'Resources', + type: 'code', + 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: +- "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', + 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', 'latex_search_packages', 'latex_get_package', 'latex_list_fonts'], + config: { + 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 { + operation, + compiler, + fileName, + resources, + packageQuery, + packageName, + fontQuery, + maxResults, + ...rest + } = params + + const parsedMaxResults = Number(maxResults) + const maxResultsParam = + Number.isFinite(parsedMaxResults) && parsedMaxResults > 0 + ? { maxResults: parsedMaxResults } + : {} + + if (operation === 'latex_search_packages') { + return { + query: packageQuery, + ...maxResultsParam, + } + } + + if (operation === 'latex_get_package') { + const effectivePackageName = typeof packageName === 'string' ? packageName.trim() : '' + if (!effectivePackageName) { + throw new Error('Package name is required.') + } + return { name: effectivePackageName } + } + + if (operation === 'latex_list_fonts') { + return { + ...(typeof fontQuery === 'string' && fontQuery.trim() ? { query: fontQuery } : {}), + ...maxResultsParam, + } + } + + 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: { + 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' }, + }, +} + +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 (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/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..665619711e --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/latex.ts @@ -0,0 +1,86 @@ +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') + .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') + .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 + .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) => { + 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..8241512509 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -8239,6 +8239,41 @@ "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. 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 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", + "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", + "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..818de6fd37 --- /dev/null +++ b/apps/sim/tools/latex/compile.ts @@ -0,0 +1,137 @@ +import type { UserFile } from '@/executor/types' +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 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.', + 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?: UserFile + 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', + } + : '') + + if (!pdf) { + return { + success: false, + error: 'LaTeX compile response did not include a PDF', + output: { + pdf: '', + pdfUrl: '', + fileName: '', + compiler: data.compiler || '', + }, + } + } + + 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/get_package.ts b/apps/sim/tools/latex/get_package.ts new file mode 100644 index 0000000000..a8261b955c --- /dev/null +++ b/apps/sim/tools/latex/get_package.ts @@ -0,0 +1,116 @@ +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 { + error?: string + 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 + if (!response.ok || data.error || !pkg?.package) { + return { + success: false, + error: + data.error || + (response.ok ? 'Package not found' : `LaTeX package lookup failed (${response.status})`), + output: { + package: { + name: '', + installed: false, + shortDescription: null, + longDescription: null, + category: null, + license: null, + topics: [], + relatedPackages: [], + homepage: null, + ctanUrl: null, + }, + }, + } + } + + 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'] + ? 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 new file mode 100644 index 0000000000..42b1e0808b --- /dev/null +++ b/apps/sim/tools/latex/index.ts @@ -0,0 +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..ca496675ff --- /dev/null +++ b/apps/sim/tools/latex/list_fonts.ts @@ -0,0 +1,102 @@ +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: 50, max: 200)', + }, + }, + + 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 { + error?: string + fonts?: Array<{ + family?: string + name?: string + styles?: string[] + }> + } + + 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 + return ( + (font.family ?? '').toLowerCase().includes(query) || + (font.name ?? '').toLowerCase().includes(query) + ) + }) + + const requested = Math.trunc(Number(params?.maxResults)) + const maxResults = + Number.isFinite(requested) && requested > 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 ?? '', + 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..e41acb09cf --- /dev/null +++ b/apps/sim/tools/latex/search_packages.ts @@ -0,0 +1,118 @@ +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: 25, max: 100)', + }, + }, + + 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 { + error?: string + packages?: Array<{ + name?: string + shortdesc?: string + installed?: boolean + url_ctan?: string + }> + } + + 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 { + 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 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, + 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 new file mode 100644 index 0000000000..c9b536e2f6 --- /dev/null +++ b/apps/sim/tools/latex/types.ts @@ -0,0 +1,106 @@ +import type { UserFile } from '@/executor/types' +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[] +} + +/** + * 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: UserFile | LatexPdfReference | '' + pdfUrl: string + fileName: string + 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 35572109fb..0674a87f51 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1694,6 +1694,12 @@ import { knowledgeUpsertDocumentTool, } from '@/tools/knowledge' import { langsmithCreateRunsBatchTool, langsmithCreateRunTool } from '@/tools/langsmith' +import { + latexCompileTool, + latexGetPackageTool, + latexListFontsTool, + latexSearchPackagesTool, +} from '@/tools/latex' import { launchDarklyCreateFlagTool, launchDarklyDeleteFlagTool, @@ -5792,6 +5798,10 @@ export const tools: Record = { linear_list_project_statuses: linearListProjectStatusesTool, 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,