Action

Draft from Email thread with AI

Posted by David Degner, Last update about 8 hours ago

UPDATES

about 8 hours ago

Fixed the return/linefeed error again

show all updates...

about 8 hours ago

Fixed the return/linefeed error again

3 days ago

Replaced the shell script which appears to fix the beach-balling delay.

8 days ago

Updating the name

8 days ago

Fix text and MarkDown

8 days ago

Converted the output to Markdown

8 days ago

Fixed the Long Title bug through Return character handling

Changed the URL encoding to use AppleScript instead of Python for eventual complete removal of Python

Removed Temporary file cleanup to try and speed up the action

Changed OpenAI model to gpt-4o for speed

9 days ago

Here’s an enumeration of the changes and improvements between V1 and V4 of the email thread processor script:

  1. Improved removeQuotedText function:

    • Added a more sophisticated quote detection system using patterns
    • Introduced a quoteHeaderPattern to identify and skip quote headers
    • Improved handling of different quote formats
  2. New helper functions:

    • Added a trim function to remove leading and trailing whitespace
    • Introduced a matchesPattern function for regex-like pattern matching
  3. Enhanced email processing:

    • Now includes the creation of message links for each email in the thread
    • Improved formatting of email details in the threadContent
  4. Cleanup functionality:

    • Added a cleanupTempFiles function to remove temporary files created during processing
  5. Error handling:

    • Improved error handling and user feedback throughout the script
  6. Prompt improvements:

    • Updated the prompt text with more detailed instructions
    • Added a requirement to include section headings even for empty sections
  7. File handling:

    • Improved temporary file naming convention (using “email_processor_” prefix)
    • Better management of file paths and cleanup
  8. Script structure:

    • Reorganized the script into more clearly defined functions
    • Added comments to improve code readability and maintainability
  9. Execution flow:

    • Added an execute function to encapsulate the main script logic
    • The script now runs the execute function automatically when launched
  10. Minor adjustments:

    • Updated variable names for clarity
    • Improved string concatenation and formatting throughout the script
  11. Compatibility:

    • Explicitly mentioned compatibility with modern MacOS and Python3

These improvements make the V4 script more robust, efficient, and user-friendly compared to the V1 version, with better handling of email threads and improved information extraction capabilities.

9 days ago

Here’s an enumeration of the changes and improvements between V1 and V4 of the email thread processor script:

  1. Improved removeQuotedText function:

    • Added a more sophisticated quote detection system using patterns
    • Introduced a quoteHeaderPattern to identify and skip quote headers
    • Improved handling of different quote formats
  2. New helper functions:

    • Added a trim function to remove leading and trailing whitespace
    • Introduced a matchesPattern function for regex-like pattern matching
  3. Enhanced email processing:

    • Now includes the creation of message links for each email in the thread
    • Improved formatting of email details in the threadContent
  4. Cleanup functionality:

    • Added a cleanupTempFiles function to remove temporary files created during processing
  5. Error handling:

    • Improved error handling and user feedback throughout the script
  6. Prompt improvements:

    • Updated the prompt text with more detailed instructions
    • Added a requirement to include section headings even for empty sections
  7. File handling:

    • Improved temporary file naming convention (using “email_processor_” prefix)
    • Better management of file paths and cleanup
  8. Script structure:

    • Reorganized the script into more clearly defined functions
    • Added comments to improve code readability and maintainability
  9. Execution flow:

    • Added an execute function to encapsulate the main script logic
    • The script now runs the execute function automatically when launched
  10. Minor adjustments:

    • Updated variable names for clarity
    • Improved string concatenation and formatting throughout the script
  11. Compatibility:

    • Explicitly mentioned compatibility with modern MacOS and Python3

These improvements make the V4 script more robust, efficient, and user-friendly compared to the V1 version, with better handling of email threads and improved information extraction capabilities.

ProjectToDraft

A Draft AppleScript that takes a highlighted email thread in the Mail App, extracts the important information for a project, and creates a new Draft out of it.

Code Versions: https://github.com/ddegner/ProjectToDraft/tree/main

Detailed writeup: https://www.daviddegner.com

Overview

This AppleScript takes the selected email thread in the macOS Mail app and sends it to OpenAI’s API for processing to extracts key information. The output is saved in a new draft, making it easy to organize and summarize email exchanges with clients.

This script is designed specifically for photographers who need to keep track of client communication.

Features

  • Integrates with OpenAI API: Extracts and formats key information from emails using OpenAI’s API.
  • Removes Quoted Text: Eliminates quoted text from email threads to prevent redundant information.
  • Email Analysis: Extracts important details such as client information, project timeline, and deliverables.
  • Drafts App Integration: Saves the processed response as a new draft in the Drafts app for easy reference.

Dependencies

  • OpenAI API Key: You need an OpenAI API key to access GPT-4. Store this key in your macOS Keychain under the label OpenAI_API_Key. sh security add-generic-password -a "<username>" -s "OpenAI_API_Key" -w "<YOUR_OPENAI_API_KEY>"
  • macOS Mail Application: The script works with email threads selected in the macOS Mail app.
  • Python 3: Required for making API calls. Python 3 is pre-installed on most modern macOS systems.
  • Drafts Application: The script creates a new draft in the Drafts app, which must be installed and set up for automation.

Installation

  1. Set Up OpenAI API Key: Store your OpenAI API key in macOS Keychain using the command above.
  2. Install Python 3: If it’s not already installed, use Homebrew: sh brew install python
  3. Create a new action in Drafts: In that action, add a Run AppleScript step. Paste the AppleScript into the script window. Uncheck all visibility in iOS because unfortunately, this only runs on macOS.
  4. Grant Action Permissions: When running the first time, it will likely ask for permissions to access the Mail app and the filesystem because it has to write a temporary text file.

Getting an OpenAI API Key

To use this script, you’ll need an OpenAI API key. Here’s how to get one:

  1. Sign Up or Log In: Go to OpenAI’s website and sign up for an account, or log in if you already have one.
  2. Create an API Key: Navigate to the API keys section in your OpenAI account settings. Click on Create new secret key.
  3. Copy the Key: Once created, copy the API key. Be sure to store it securely, as you’ll need it for configuring this script.
  4. Add to Keychain: Store your API key in your macOS Keychain with the following terminal command: sh security add-generic-password -a "<username>" -s "OpenAI_API_Key" -w "<YOUR_OPENAI_API_KEY>"

Usage

  1. Select Emails: Open the macOS Mail app and select the email thread you want to analyze.
  2. Run the Drafts Action: The script will:
    • Extract key information from the selected email thread.
    • Use OpenAI’s API to summarize the details.
    • Save the formatted output in a new draft in the Drafts app.
  3. Customize the Output: The output is organized into sections and can be easily edited by modifying the ChatGPT prompt.

Adjusting the Prompt for Your Project

The default prompt used in this script is designed for general email analysis, but you can easily adjust it to fit the specific needs of your project. Here’s how to do it:

  1. Locate the Prompt in the Script: Find the section of the script where the prompt is defined. It will typically look something like this: applescript set promptText to "I am David Degner, the photographer, and this is an email thread with my client. Please extract the following information from the email thread for me..."
  2. Modify the Prompt: Adjust the text to include specific details related to your project. For example, if you’re a designer, you could modify it to say: applescript set promptText to "I am [Your Name], a graphic designer, and this is an email thread with my client. Please extract the following design-related information from the email thread..."
  3. Tailor Sections: You can also change the sections you want the output to include. For example, if you’re working on a marketing campaign, you might want to add sections like “Campaign Goals” or “Target Audience”.
  4. Test the Changes: Run the script with a sample email thread to ensure the adjusted prompt provides the output you need.

Script Details

Key Functions

  • execute(): The main function that retrieves selected emails, processes them, and sends a request to OpenAI.
  • removeQuotedText(emailBody): Cleans the email body by removing quoted sections to avoid redundant content.

Error Handling

  • Keychain Retrieval: Displays an alert if the OpenAI API key is not found in Keychain.
  • API Call Errors: Alerts the user if the API request fails.
  • Email Selection: Alerts the user if no emails are selected.

Security Considerations

  • Sensitive Information: The OpenAI API key is stored in macOS Keychain for security. Only use the script on trusted devices.
  • Internet Access: An internet connection is required to call the OpenAI API.

License

This script is open source and available under the MIT License.

Contributing

Feel free to fork the repository and submit pull requests. Contributions are welcome to enhance functionality, improve error handling, or add support for other email clients or automation tools.

Troubleshooting

  • No Response from API: Ensure your internet connection is stable and that the API key is correctly added to Keychain.
  • Permission Issues: Make sure you have granted automation permissions to Mail, Terminal, and Drafts in System Preferences > Security & Privacy.

For questions or assistance, please open an issue on the github repository: https://github.com/ddegner/ProjectToDraft/tree/main

Steps

  • runAppleScript (macOS only)

    -- Email Thread Processor for Drafts
    -- This AppleScript runs as an action in the Drafts app it processes selected email threads from Mail,
    -- pulls out the imporant information using OpenAI's GPT-4o, creates a new draft in the Drafts app with the
    -- extracted information and original thread content, and includes links to each original message in the Mail app.
    -- It runs on a modern MacOS with a bit of Python3.
    
    use framework "Foundation"
    use scripting additions
    
    -- Function to remove quoted text from an email body
    on removeQuotedText(emailBody)
        set nsEmailBody to current application's NSString's stringWithString:emailBody
        -- Regular expression patterns to match quoted text
        set patterns to {"(?m)^(>.*)$", "(?s)^On .*? wrote:.*", "(?m)^-----Original Message-----.*", "(?m)^From:.*", "(?m)^Sent:.*", "(?m)^To:.*", "(?m)^Subject:.*"}
        repeat with pattern in patterns
            set regex to current application's NSRegularExpression's regularExpressionWithPattern:pattern options:0 |error|:(missing value)
            set nsEmailBody to regex's stringByReplacingMatchesInString:nsEmailBody options:0 range:{0, nsEmailBody's |length|()} withTemplate:""
        end repeat
        return nsEmailBody as string
    end removeQuotedText
    
    -- Function to replace characters in a string
    on replace_chars(theText, searchString, replacementString)
        set AppleScript's text item delimiters to searchString
        set theItems to text items of theText
        set AppleScript's text item delimiters to replacementString
        set theText to theItems as string
        set AppleScript's text item delimiters to ""
        return theText
    end replace_chars
    
    -- Helper function to trim whitespace from a string
    on trim(someText)
        set nsText to current application's NSString's stringWithString:someText
        set trimmedText to nsText's stringByTrimmingCharactersInSet:(current application's NSCharacterSet's whitespaceAndNewlineCharacterSet())
        return trimmedText as string
    end trim
    
    -- Helper function to URL-encode a string
    on urlEncode(inputString)
        set nsString to current application's NSString's stringWithString:inputString
        set allowedChars to current application's NSCharacterSet's URLQueryAllowedCharacterSet()
        set encodedString to nsString's stringByAddingPercentEncodingWithAllowedCharacters:allowedChars
        return encodedString as string
    end urlEncode
    
    -- Function to create a message link for a given message
    on createMessageLink(theMessage)
        set messageID to id of theMessage as string
        set encodedMessageID to my urlEncode(messageID)
        return "message://%3C" & encodedMessageID & "%3E"
    end createMessageLink
    
    -- Function to write text to a file using ASObjC
    on writeToFile(theText, theFilePath)
        try
            set theNSString to current application's NSString's stringWithString:theText
            set theNSData to theNSString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
            theNSData's writeToFile:theFilePath atomically:true
            return true
        on error errMsg
            display alert "Failed to write to file: " & errMsg
            return false
        end try
    end writeToFile
    
    -- Function to retrieve API key from Keychain
    on getAPIKeyFromKeychain(keyName)
        try
            set apiKey to do shell script "security find-generic-password -w -s " & quoted form of keyName
            return apiKey
        on error
            return missing value
        end try
    end getAPIKeyFromKeychain
    
    -- Function to sort messages by date using a custom sort
    on sortMessagesByDate(messageList)
        set sortedMessages to messageList
        set messageCount to count of sortedMessages
        tell application "Mail"
            repeat with i from 1 to (messageCount - 1)
                repeat with j from (i + 1) to messageCount
                    set messageI to item i of sortedMessages
                    set messageJ to item j of sortedMessages
                    set dateI to date received of messageI
                    set dateJ to date received of messageJ
                    if dateI > dateJ then
                        -- Swap the messages
                        set item i of sortedMessages to messageJ
                        set item j of sortedMessages to messageI
                    end if
                end repeat
            end repeat
        end tell
        return sortedMessages
    end sortMessagesByDate
    
    -- Function to call OpenAI API using Python
    on callOpenAIAPI(apiKey, promptFilePath)
        set pythonScript to "
    import json
    import urllib.request
    import urllib.error
    import sys
    
    api_key = '" & apiKey & "'
    
    with open('" & promptFilePath & "', 'r', encoding='utf-8') as f:
        prompt = f.read()
    
    messages = [
        {\"role\": \"user\", \"content\": prompt}
    ]
    
    payload = {
        \"model\": \"gpt-4o\",
        \"messages\": messages
    }
    
    headers = {
        \"Content-Type\": \"application/json\",
        \"Authorization\": f\"Bearer {api_key}\"
    }
    
    req = urllib.request.Request(\"https://api.openai.com/v1/chat/completions\", data=json.dumps(payload).encode('utf-8'), headers=headers)
    
    try:
        with urllib.request.urlopen(req) as response:
            result = json.loads(response.read())
            print(result['choices'][0]['message']['content'])
    except urllib.error.HTTPError as e:
        error_info = e.read().decode('utf-8')
        print(f\"API request failed: {error_info}\")
        sys.exit(1)
    "
        try
            set apiResponse to do shell script "/usr/bin/python3 -c " & quoted form of pythonScript
            return apiResponse
        on error errMsg
            return errMsg
        end try
    end callOpenAIAPI
    
    -- Function to execute the main script
    on execute()
        try
            tell application "Mail"
                -- Get the selected messages from Mail
                set selectedMessages to selection
                if selectedMessages is {} then
                    display alert "No email selected" message "Please select an email thread." buttons {"OK"} default button "OK"
                    return
                end if
    
                -- Sort the messages by date received
                set sortedMessages to my sortMessagesByDate(selectedMessages)
    
                -- Initialize variables for thread content
                set threadContent to ""
    
                -- Process the messages in sorted order
                repeat with eachMessage in sortedMessages
                    -- Get email details
                    set emailSender to sender of eachMessage
                    set emailSubject to subject of eachMessage
                    set emailDate to date received of eachMessage
                    set emailBody to content of eachMessage
    
                    -- Remove quoted text
                    set cleanedBody to my removeQuotedText(emailBody)
    
                    -- Create and add the message link
                    set messageLink to my createMessageLink(eachMessage)
    
                    -- Append email details to threadContent
                    set threadContent to threadContent & "From: " & emailSender & " / Subject: " & emailSubject & " / Date: " & emailDate & linefeed & cleanedBody & linefeed & "Message Link: " & messageLink & linefeed & "---" & linefeed & linefeed
                end repeat
            end tell
    
            -- Combine the prompt and the thread content
            set promptText to "I am David Degner, the photographer, and this is an email thread with my client. Please extract the following information from the email thread for me. Focus on the most relevant and clear information for each section, using explicit details mentioned in the emails.
    
    Ensure that all detailed information is included and is accurate.
    
    Here is the format to follow:
    
    - Format in markdown
    - The first line should be the project title, written directly without a heading.
    - Subsequent sections should include a heading.
    - If a section has no relevant information only write the heading and leave the section blank.
    
    CLIENT INFORMATION:
    Include the client or company requesting the work, with their main contact and role if available and relevant contact details such as email or phone numbers. Do not label each piece of information.
    
    AGENCY:
    Mention any agency or intermediary company involved, if applicable, including their name and relevant contact information.
    
    PROJECT TIMELINE:
    Extract relevant dates, including deadlines, shoot dates, or delivery timelines.
    
    LOCATION:
    Where exactly will the photography take place or what is the address of the client.
    
    PROJECT DESCRIPTION:
    Summarize the key objectives and scope of the project. Include any style, goals, or focus areas mentioned.
    
    ART DIRECTION:
    Highlight any references to stylistic direction, art direction, or creative requirements.
    
    DELIVERABLES:
    List the required outputs, such as final images, edited photos, reports, or videos. Include quantity, format, and deadlines.
    
    BUDGET:
    Extract all mentions of costs, estimates, or budgets. Look for terms such as 'budget', 'cost', 'estimate', 'fee', 'pricing', 'cost breakdown', 'quote', 'rate', or any mention of monetary values (e.g., '$500', 'USD', 'total cost'). Ensure every detail about finances is captured, even if mentioned indirectly.
    
    TEAM AND ROLES:
    Identify any other team members mentioned (e.g., assistants, models, makeup artists) and their roles.
    
    LICENSING AND USAGE RIGHTS:
    Include any mentions of licensing terms, usage rights, or agreements on how the photos will be used.
    
    REVISIONS OR FEEDBACK:
    Extract any details regarding rounds of revisions, feedback processes, or client approval steps.
    
    SPECIAL REQUIREMENTS:
    Mention any additional or unique requirements related to the shoot (e.g., equipment, props, permits, travel arrangements).
    
    EXTRA NOTES:
    Capture any additional information, such as meeting schedules, important discussions, or extra tasks.
    
    Make sure the project title appears as the first line without a label, and the remaining sections follow with clear headings and spacing."
    
            set fullPrompt to promptText & linefeed & linefeed & "Email Thread Content:" & linefeed & threadContent
    
            -- Generate a unique temporary file path
            set promptFilePath to do shell script "mktemp /tmp/email_processor_prompt.XXXXXX"
    
            -- Write the full prompt to the temporary file using the updated writeToFile function
            my writeToFile(fullPrompt, promptFilePath)
    
            -- Retrieve the OpenAI API key from Keychain
            set openAIAPIKey to my getAPIKeyFromKeychain("OpenAI_API_Key")
            if openAIAPIKey is missing value then
                display alert "API Key Not Found" message "Please store your OpenAI API Key in the Keychain." buttons {"OK"} default button "OK"
                return
            end if
    
            -- Use Python3 to make the API request
            set apiResponse to my callOpenAIAPI(openAIAPIKey, promptFilePath)
            if apiResponse starts with "API request failed:" then
                display alert "API Error" message apiResponse buttons {"OK"} default button "OK"
                return
            end if
    
            -- Create a new draft in Drafts app with the extracted information and original thread content
            tell application "Drafts"
                set fullContent to my replace_chars(apiResponse, return, linefeed) & linefeed & linefeed & "------------------" & linefeed & my replace_chars(threadContent, "return", linefeed)
                make new draft with properties {content:fullContent, flagged:false, tags:{"commercial job"}}
            end tell
    
        on error errMsg number errNum
            display alert "An error occurred: " & errMsg & " (Error " & errNum & ")"
        end try
    end execute
    
    -- Run the execute function
    execute()
    

Options

  • After Success Default
    Notification Info
    Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.