---
title: Creating an AI Assistant That Produces PDF Files
url: https://calvin.my/posts/creating-an-ai-assistant-that-produces-pdf-files
published: 2025-10-05
updated: 2026-09-16
category: AI
tags:
- OpenAI
- Function Calling
- Ruby
summary: The post explains how to extend text-based AI assistants to create PDF documents through function calling. A model generates HTML content, while a Ruby-based conversion function produces and attaches the PDF to a conversation. The workflow covers defining the tool, handling model tool-call events and results, and returning a final response. It emphasizes retaining call identifiers and sanitizing HTML in production. The approach can be adapted for other document formats.
---

# Creating an AI Assistant That Produces PDF Files

## Background
Most large language models (LLMs) output text strings. Some advanced multimodal models, which go beyond text-only, can generate images, audio, and video. However, they typically do not produce output in specific file formats such as DOCX, PDF, or technical drawings.

In some scenarios, the final output must be in one of these formats. To close this gap, we can rely on the LLM's strong text-generation capability to produce content in text form, then transform that text into the desired format. The transformation can be done via function calling or MCP (Model Context Protocol). For example: HTML text to PDF, or a Mermaid diagram written in Markdown to a rendered graph.  

This article shows the steps to produce PDF files via function calling.

* * *

## Create an executable function (Ruby)

1. Define what the function can do and its inputs.

   ```json
   {
     "type": "function",
     "function": {
       "name": "html_to_pdf",
       "description": "Convert HTML code to a PDF file and attach it to the conversation.",
       "parameters": {
         "type": "object",
         "properties": {
           "html": {
             "type": "string",
             "description": "HTML code to convert to PDF (maximum 100000 characters)"
           },
           "filename": {
             "type": "string",
             "description": "Optional filename for the PDF (e.g., 'report.pdf'). If not provided, a UUID-based filename will be generated."
           }
         },
         "required": [ "html" ]
       }
     }
   }
   ```

2. Implement the actual HTML-to-PDF conversion. This example uses WickedPdf.

   ```ruby
   html = ""
   pdf_generator = WickedPdf.new
   pdf_binary = pdf_generator.pdf_from_string(
       html,
       page_size: "A4",
       margin: {
         top: 15,
         bottom: 20,
         left: 15,
         right: 15
       },
       encoding: "UTF-8",
       # Footer with page numbers
       footer: {
         center: "Page [page] of [topage]",
         font_size: 9
       },
       # Security options
       disable_javascript: true,
       no_stop_slow_scripts: true,
       disable_external_links: true,
       enable_local_file_access: false,
       load_error_handling: "ignore"
   )

   filename = "#{SecureRandom.uuid}.pdf" if filename.blank?
   filename = "#{filename}.pdf" unless filename.end_with?(".pdf")
   ```

3. Then attach the file to your conversation or message.

   ```ruby
   message.files.attach(
     io: StringIO.new(pdf_binary),
     filename: filename,
     content_type: "application/pdf"
   )
   ```

   ```ruby
   class Message < ApplicationRecord
     has_many_attached :files
   end
   ```

4. And inform the LLM that the function calling is completed.

   ```ruby
   {
     success: true,
     message: "PDF generated successfully and attached to the conversation"
   }
   ```

5. This sample code does not perform HTML sanitization. In real use cases, you should validate and sanitize input HTML before conversion.

   The ActionView's [SanitizeHelper](https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/SanitizeHelper.html) is a good start.

* * *

## Use the Function with AI Models

Next, choose a model that outputs text and supports function calling. OpenAI GPT-5 Mini matches these criteria.

1. Enable function calling via [tool calls](https://platform.openai.com/docs/api-reference/responses/create#responses-create-tools). Add these fields in the request payload:

   ```ruby
   {
      max_tool_calls: 5,
      tools: [
        {
          type: "function",
           name: func_definition[:function][:name],
           description: func_definition[:function][:description],
           parameters: func_definition[:function][:parameters]
        }
      ]
   }
   ```

2. When the model needs to call a function, a series of events like the following will be returned:

   - `response.output_item.added` event with type set to function\_call ([Reference](https://platform.openai.com/docs/api-reference/responses-streaming/response/output_item/added#responses-streaming/response/output_item/added-item-function-tool-call))  
   - `response.output_item.done` event  
   - `response.function_call_arguments.delta` event ([Reference](https://platform.openai.com/docs/api-reference/responses-streaming/response/function_call_arguments/delta))  
   - `response.function_call_arguments.done` event

   The first two events request execution, and the next two provide the input data. Please ensure that you store the `call_id` from the first event.

3. Call the function to produce the PDF file with the provided input data, and return the function's result.

   ```ruby
   function_call_results << {
     call_id: "#{call_id}",
     type: "function_call_output",
     output: {
       "success": true,
       "message": "PDF generated successfully and attached to the conversation"
     }
   }
   ```

4. Send a request to the AI model, with the function\_call\_results added. At this point, your conversation histories should be similar to:

   - user
   - reasoning
   - function\_call
   - function\_call\_output

5. If the function call output contains a positive result, the AI model would usually respond with a final success message (may vary depending on your system prompt).

   - user
   - reasoning
   - function\_call
   - function\_call\_output
   - assistant

* * *

## The result

1. We test the feature by requesting a recipe in PDF format:

   ![](https://camy-pub.s3.ap-southeast-1.amazonaws.com/850ab476-f977-4b42-9509-2d49301bf7d1.png)

2. The PDF is generated correctly. Sample:

   ![](https://camy-pub.s3.ap-southeast-1.amazonaws.com/58e0cac8-85ed-4d4b-b41f-0b8067f8158d.png)

* * *

The same approach can be applied to other types of documents by modifying the function implementation.
