Word and Excel Outputs

Hi - For me it’s super important to be able to output documents for the user in word and excel formats. At the moment I am asking the Ai to create a VBA macro for the output and telling users how to run it. The macros it create work great, but really it would be a lot more user friendly if this was done for the user and it was just ready for them to download.

Yes, I’ve read all the threads about this here in the forum. the pdf export tool works really well, but for the type of stuff I’m doing it needs to be in word so the user can make edits, etc. I’ve tried the word document plugin there is now, but it makes the output pretty messy. Not asking for a super complicated formatted document, just the output presented in the chat window to go into a word doc with the headings and lists as they appear on chat screen.

Thanks!

Hi @lachb, I’ve modified the word action to have a better formatting (code below). Note @cortcorwin went even further so you might want to reach out to him.

import requests
import markdown
import io
from docx import Document
from docx.shared import Pt, Inches, RGBColor
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from bs4 import BeautifulSoup
from bs4 import NavigableString
from docx.oxml.ns import nsdecls
from docx.oxml import parse_xml
from docx.oxml import OxmlElement
from docx.oxml.ns import qn

# Constants for formatting
DEFAULT_LINE_SPACING = 2.5  # line spacing for paragraphs (change this value to suit)
LIST_INDENT_PER_LEVEL = 44  # in points (change this value to suit)

def ultimate_word_doc_generator(markdown_input: str, file_name: str, template_link: str, caption: str):
    """
    Allows the user to generate a word doc.

    Args:
        markdown_input (string): Markdown that will be turned into the word document
        file_name (string): Name of the document
        template_link (string): the link of the word template
        caption (string): the action caption
    """

    # Insert your PYTHON code below. You can access environment variables using os.environ[].
    # Currently, only the requests library is supported, but more libraries will be available soon.
    # Use print statements or return values to display results to the user.
    # If you save a png, pdf, csv, jpg, webp, gif, or html file in the root directory, it will be automatically displayed to the user.
    # You do not have to call this function as the bot will automatically call and fill in the parameters.

    # Convert Markdown to HTML
    html_content = markdown.markdown(markdown_input, extensions=['extra'])

    # Parse HTML using BeautifulSoup
    soup = BeautifulSoup(html_content, "html.parser")

    # Decide whether to load a template or create a new document
    # Replace with your Dropbox / Google Drive shared link, ensuring it ends with '?dl=1' for direct download
    # Example of dropbox_link = "https://www.dropbox.com/scl/fi/kul8ybun0mll786454gd7q/Pickaxe_Template.docx?rlkey=s91uqsvapy2n27ur6q6lzb5hu&st=uqzx7jy3&dl=1"
    
    # 1) Decide whether to load a template or create a new doc
    if template_link and template_link.strip():
        # Attempt to download the template
        try:
            response = requests.get(template_link.strip())
        except Exception as e:
            # If there's any error with the request, create a blank doc instead
            print(f"Error fetching template link ({template_link}): {e}")
            print("Falling back to a new blank document.")
            doc = Document()
        else:
            if response.status_code == 200:
                try:
                    template_stream = io.BytesIO(response.content)
                    doc = Document(template_stream)
                except Exception as e:
                    # If the file is not a valid Word doc, fall back
                    print(f"Error loading template as Word doc: {e}")
                    print("Falling back to a new blank document.")
                    doc = Document()
            else:
                print(f"Template download failed with status {response.status_code}.")
                print("Falling back to a new blank document.")
                doc = Document()
    else:
        # No link provided or empty link => create new doc
        doc = Document()


    # 1A) Create a new Word document (comment out if using template)
    # doc = Document()
    
    # 1B) Import a Template (comment out if creating a new document)
    # Replace with your Dropbox shared link, ensuring it ends with '?dl=1' for direct download
    #dropbox_link = "https://www.dropbox.com/scl/fi/kul8ybun0mll1v868gd7q/Pickaxe_Template.docx?rlkey=s91uqsvapy2n27ur6q6lzb5hu&st=uqzx7hr3&dl=1"
    
    #response = requests.get(dropbox_link)
    #if response.status_code == 200:
    #    template_stream = io.BytesIO(response.content)
    #    doc = Document(template_stream)
    #else:
    #    raise Exception("Failed to download the template from Dropbox")   

    # 4) Set Normal style for paragraphs
    style_normal = doc.styles['Normal']
    font_normal = style_normal.font
    font_normal.name = 'Roboto' # Change font here as required
    font_normal.size = Pt(11) # Change size here as required
    
    # 5A) Set Heading 1 
    style_h1 = doc.styles['Heading 1']
    font_h1 = style_h1.font
    font_h1.name = 'Roboto' # Change font here as required
    font_h1.size = Pt(38) # Change size here as required

    # 5B) Set Heading 2 style
    style_h2 = doc.styles['Heading 2']
    font_h2 = style_h2.font
    font_h2.name = 'Roboto' # Change font here as required
    font_h2.size = Pt(28) # Change size here as required

    # 5C) Set Heading 3 
    style_h3 = doc.styles['Heading 3']
    font_h3 = style_h3.font
    font_h3.name = 'Roboto' # Change font here as required
    font_h3.size = Pt(28) # Change size here as required
    
    # 6) Set color to black for Normal and Heading 1 styles
    font_normal.color.rgb = RGBColor(0x00, 0x00, 0x00) # Change RGB colour here as required (e.g. blue would be 0x00, 0x00, 0xFF)
    font_h1.color.rgb = RGBColor(0x00, 0x00, 0x00) # Change RGB colour here as required (e.g. blue would be 0x00, 0x00, 0xFF)

    # 7) Styling of the text
    def parse_inline(soup_element, paragraph):
        """
        Recursively adds text and inline formatting (e.g., bold) to a paragraph,
        forcing all runs to be black and trimming extra spaces.
        """
        for content in soup_element.contents:
            if isinstance(content, NavigableString):
                text = content.strip()
                if text:
                    run = paragraph.add_run(text)
                    run.font.color.rgb = RGBColor(0x00, 0x00, 0x00)
            elif content.name in ["strong", "b"]:
                text = content.get_text(strip=True)
                if text:
                    run = paragraph.add_run(text)
                    run.bold = True
                    run.font.color.rgb = RGBColor(0x00, 0x00, 0x00)
            else:
                parse_inline(content, paragraph)

    def parse_element(element):
        """
        Processes top-level elements: headings, paragraphs, and lists.
        """
        if element.name in ["h1", "h2", "h3", "h4", "h5", "h6"]:
            level = int(element.name[1])
            heading = doc.add_heading(level=level)
            parse_inline(element, heading)
        elif element.name == "p":
            para = doc.add_paragraph()
            # Set text justification and line spacing
            para.alignment = WD_PARAGRAPH_ALIGNMENT.JUSTIFY
            para.paragraph_format.line_spacing = DEFAULT_LINE_SPACING
            parse_inline(element, para)            
        elif element.name in ["ul", "ol"]:
            parse_list(element, level=0)

    # 8) Styling of the lists
    def parse_list(list_element, level):
        """
        Processes list elements. Supports one level of nesting.
        """
        list_style = "List Number" if list_element.name == "ol" else "List Bullet"
    
        for li in list_element.find_all("li", recursive=False):
            nested_list = li.find(["ul", "ol"], recursive=False)
            if nested_list:
                nested_list.extract()
    
            paragraph = doc.add_paragraph(style=list_style)
            # Set left indent based on level
            paragraph.paragraph_format.left_indent = Pt(LIST_INDENT_PER_LEVEL * (level + 1))
            # Optionally, you can also justify list items if desired:
            paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.JUSTIFY
            # Set line spacing for list paragraphs
            paragraph.paragraph_format.line_spacing = DEFAULT_LINE_SPACING
    
            parse_inline(li, paragraph)
    
            if nested_list:
                parse_list(nested_list, level + 1)

    # Process each top-level element in the HTML
    for element in soup.find_all(recursive=False):
        parse_element(element)

    # 9) Styling of the table --- Add the approval table at the end ---
    table = doc.add_table(rows=3, cols=2) # Chnage size of the table by enetering the number of rows / columns
    table.style = 'Table Grid'

    table.cell(0, 0).text = "Approved By:" # Change text of selected cell. In this case 0,0. If you are adding more rows or want to change the text in other cells add a line that points to the correct cell.
    table.cell(1, 0).text = "Date:"
    table.cell(2, 0).text = "Signature:"

    table.cell(0, 1).text = ""
    table.cell(1, 1).text = ""
    table.cell(2, 1).text = ""

    table.columns[0].width = Inches(2) # Change the width of the table column 0
    table.columns[1].width = Inches(4) # Change the width of the table column 1

    desired_row_height = 500 # Change the height of the rows
    for row in table.rows:
        tr = row._tr
        trPr = tr.get_or_add_trPr()
        trHeight = OxmlElement('w:trHeight')
        trHeight.set(qn('w:val'), str(desired_row_height))
        trHeight.set(qn('w:hRule'), 'exact')
        trPr.append(trHeight)

    for row in table.rows:
        for cell in row.cells:
            shading_elm = parse_xml(
                r'<w:shd {} w:fill="f3f6fa"/>'.format(nsdecls('w'))
            )
            cell._tc.get_or_add_tcPr().append(shading_elm)

    # 10) The below code avoids the text in the table to be split on different pages
    def set_cant_split(row):
        trPr = row._tr.get_or_add_trPr()
        cant_split = OxmlElement('w:cantSplit')
        trPr.append(cant_split)
    for row in table.rows:
        set_cant_split(row)

    # 11) New function to force font on all runs
    def force_font(doc, font_name):
        for para in doc.paragraphs:
            for run in para.runs:
                run.font.name = font_name
                try:
                    rPr = run._element.get_or_add_rPr()
                    rFonts = rPr.find(qn('w:rFonts'))
                    if rFonts is None:
                        rFonts = OxmlElement('w:rFonts')
                        rPr.append(rFonts)
                    rFonts.set(qn('w:ascii'), font_name)
                    rFonts.set(qn('w:hAnsi'), font_name)
                    rFonts.set(qn('w:eastAsia'), font_name)
                    rFonts.set(qn('w:cs'), font_name)
                except Exception as e:
                    print("Error forcing font on run:", e)

    # Force all runs to use the desired font (e.g., Arial, Times New Roman)
    force_font(doc, "Verdana")

    # 11) Save the Word document
    doc.save("MyDocument.docx")

    #12) Send the file to a Make.com webhook

    # Open the generated document in binary mode
    with open("MyDocument.docx", "rb") as file_data:
        files = {
            "file": ("MyDocument.docx", file_data, "application/vnd.openxmlformats-officedocument.wordprocessingml.document")
        }
        # Replace this URL with your actual webhook endpoint URL
        webhook_url = "https://hook.eu2.make.com/xxx"
        
        response = requests.post(webhook_url, files=files)
        print("Webhook response status:", response.status_code)
        print("Webhook response:", response.text)`

Hi @ab2308 - thank you so much this is amazing! I’m now able to get a decently formatted word document out. game changer, really.

I’m playing around with the code a bit just to get super nice formatting and have a couple of questions.

  1. Could you explain about the force font function? if run it overrides the font settings made earlier at 4) and 5). But if I don’t run it seems as though the selected fonts are maintained but sizes and colors don’t change?
  2. Same question as above but with the forcing of black color in 7) when i remove these lines I’m able to set all the text to one color, however the whole document become formatted as normal (no headings).
  3. It seems that a similar problem I had with the base action - when the Ai uses a numbered list of some kind, in the word document the numbering sort of runs away and every single line after the numbered list become also a numbered list. I fixed this by just instructing the Ai to never use numbering! but would be nice if we could.

Thanks again - totally awesome work :slight_smile:

1 Like

Hey @lachb here is a link to the word doc gen that I’m using. I used @ab2308 's code as a seed, then Grok did most of the hard work. Started w/ OpenAI o3mini and it was a doom loop, Grok (and I imagine Sonnet 3.5) is significantly better at code. I would plug it in and ask the questions above. I ended up removing force font and also removed the dropbox link and hard coded the template. It’s only importing a few images for the header / footer and the notes section that must be the exact text/format. If you DM me i’ll send you a export copy if you’d like.

Again, huge thanks to @ab2308, wouldn’t have gotten here w/o him!

word doc gen code:

Thanks @cortcorwin! really helpful. Yes, of course asking an Ai bot for help is that way to do this - instead of me bugging you guys with Python questions :slight_smile: - I had actually started doing this and failing - but then I realised that each time I change the Python code you need to disconnect and reconnect the action. When you are working on this kind of thing is there a python environment you would recommend where I can test a bit easier and debug, etc. Like feed it the markdown and quickly create the word doc out externally to pickaxe? Pretty painful to have to do it all through the pickaxe and run the chatbot again and again, etc. Thanks again!

Because it needs my markdown input, I have not been able to run this in a development environment. My process is fairly fast, >>> cut/paste the new code into the action, save action, save pickaxe, then continue in the current open chat and ask it to try the action again. I’ve been using Windsurf IDE as well, which makes code updates very very fast and connects to Github for version control.

2 Likes

@cortcorwin same here. @lachb Cort’s process is what I would follow

Thanks again guys! I was able to use chat gpt to create a similar function to export an ppt file. I’ve been trying round and round with different Ais and approaches to do the same for an excel (or even .csv) file. And i can’t quite manage to crack it! Any ideas for excel? Thanks!