Full script
rename script - document-rename.scpt
use AppleScript version "2.5" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"
use script "RegexAndStuffLib" version "1.0.7"
property globalSuccessTags : {"auto-processed-rename-ai"}
property globalAiRequestTimeout : 120
property globalPrompt : "¬
Generate a concise, descriptive filename for the following text.¬
# Rules/Expectations for the filename:¬
- 3-6 words maximum, no more than 50 letters¬
- Use only lowercase letters, numbers, and underscores¬
- Capture the main topic or purpose of the given text¬
- Be specific enough to identify the content¬
- For invoices: if a company name is given always use it¬
- For invoices: use the main product¬
- Ignore city names in invoices¬
- For bookings: use the name of the place or the destination¬
- Ignore prices for filename¬
- Ignore first names or surnames of persons¬
- Add type of document, but avoid generic terms at all costs like \"document\", \"file\", or \"text\"¬
- Avoid any names of persons at all costs¬
- Avoid adding a date to the new filename¬
- Use \"\" for date if no date can be found in document
# Steps¬
2. Capture the main topic or purpose based on the text¬
3. Determine type of document
3. Extract dates from the text - ignore ones before the date saved in the CURRENT_DATE-variable¬
4. Order dates from newest to oldest¬
5. Use the oldest date in YYYY-MM-DD notation¬
6. Generate the name in German for the file - follow the rule mentioned ealier¬
7. Make sure the filename follows the rules mentioned earlier - fix it, if it does stick to the rules¬
8. Make sure the filename follows the rules mentioned earlier - fix it, if it does stick to the rules¬
¬
# Expected output¬
You are an excellent JSON generator. You leave out any explanation! You only output the JSON object.¬
Extract date and filename from the input.¬
Ignore dates before the CURRENT_DATE-variable
¬
Filename format: [topic]-[subtopic]-[descriptor]-[doc_type] (if applicable)¬
# expected json output fields with data type¬
```json¬
{¬
\"new_filename\": \"string\",¬
\"new_date\": \"date\",¬
\"doc_type\": \"enum(rechnung, vertrag, quittung, versicherungspolice, mietvertrag, garantieschein, steuerbescheid, kontoauszug, kreditvertrag, kostenvoranschlag, bescheinigung, anderes_dokument)\"¬
}¬
¬
# example for expected output¬
```json¬
{¬
\"new_filename\": \"this-is-the-file-name\",¬
\"new_date\": \"1970-10-01\",¬
\"doc_type\": \"rechnung\"¬
}¬
```¬
¬
# Variables¬
"
-- code to run within Script Debugger
tell application id "DNtp" to my performSmartRule(selected records)
-- ==========
-- test performSmartRule
property globalIsDev : true
-- copy template for smart rules
on performSmartRule(theRecords)
local libraryHandler
set libraryHandler to script "library-handling"
tell application id "DNtp"
set currentDb to current database
end tell
libraryHandler's canRun(currentDb, theRecords)
--- start script 1
local scriptName1
set scriptName1 to "ai-base"
local theScriptHandler1
set theScriptHandler1 to libraryHandler's loadScript(scriptName1, globalIsDev)
theScriptHandler1's globalLog("INFO", "Run script", "script=" & quoted form of scriptName1)
theScriptHandler1's SetupLocalAiRuntime()
theScriptHandler1's globalLog("INFO", "Script finished", "script=" & quoted form of scriptName1)
--- start script 2
local scriptName2
set scriptName2 to "document-rename"
local theScriptHandler2
set theScriptHandler2 to libraryHandler's loadScriptSilent(scriptName2, globalIsDev)
local thePrompt
set thePrompt to get globalPrompt of theScriptHandler2
local aiRequestTimeout
set aiRequestTimeout to globalAiRequestTimeout of theScriptHandler2
local aiEngine
local theModel
tell application id "DNtp"
-- set aiEngine to Ollama
-- set theModel to "phi4"
set aiEngine to LM Studio
set theModel to "microsoft/phi-4"
-- set theModel to "mistralai/mistral-nemo-instruct-2407"
-- set theModel to "microsoft/phi-4-mini-reasoning"
-- set theModel to "qwen/qwen3-4b-2507"
end tell
theScriptHandler2's globalLog("INFO", "Run script", "script=" & quoted form of scriptName2)
theScriptHandler2's ForAll(theRecords, globalIsDev, aiEngine, theModel, thePrompt, aiRequestTimeout)
theScriptHandler2's globalLog("INFO", "Script finished", "script=" & quoted form of scriptName1)
end performSmartRule
-- Run job for all given records
on ForAll(theRecords, localIsDev, theEngine, theModel, thePrompt, localAiRequestTimeout)
local titleProgressBar
set titleProgressBar to "Rename documents"
tell application id "DNtp"
show progress indicator titleProgressBar steps (count of theRecords) with cancel button
-- https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/reference/ASLR_error_xmpls.html
end tell
set len to length of theRecords
set i to 1
my globalLog("INFO", quoted form of "Job started", "count_of_records=" & len)
try
local theRecord
repeat with theRecord in theRecords
tell application id "DNtp"
if cancelled progress then
my recordLog(theRecord, "INFO", quoted form of "Cancel current run of script", missing value)
exit repeat
end if
set theFilename to the filename of theRecord
step progress indicator theFilename
end tell
my recordLog(theRecord, "INFO", quoted form of "Work on record", "index=" & i & "/" & len)
set modifiedRecord to my ForOne(theRecord, localIsDev, theEngine, theModel, thePrompt, localAiRequestTimeout)
tell application id "DNtp"
set tags of modifiedRecord to (get tags of modifiedRecord) & globalSuccessTags
end tell
set i to i + 1
end repeat
on error errStr number errorNumber
-- An unknown error occurred. Resignal, so the caller
-- can handle it, or AppleScript can display the number.
display alert errStr
error errStr number errorNumber
end try
tell application id "DNtp"
hide progress indicator
end tell
my globalLog("INFO", quoted form of "Job completed", "count_of_records=" & len)
end ForAll
-- Run job for single record
on ForOne(theRecord, localIsDev, theEngine, theModel, thePrompt, localAiRequestTimeout)
local theResponse
set theResponse to my GetNewNameLoop(theRecord, localIsDev, theEngine, theModel, thePrompt, localAiRequestTimeout)
tell application id "DNtp"
local oldName
set oldName to quoted form of (name of theRecord as string)
end tell
local newName
set newName to new_filename of theResponse
local newDate
set newDate to new_date of theResponse
local docType
set docType to doc_type of theResponse
my recordLog(theRecord, "INFO", quoted form of "Change name of record", "oldname=" & oldName & " newname=" & quoted form of newName & " new_creation_date=" & quoted form of newDate)
tell application id "DNtp"
set name of theRecord to newDate & "-" & newName
if newDate is not "" then
set creation date of theRecord to my toDate(newDate)
end if
if docType is not "" then
set tags of theRecord to (tags of theRecord) & {docType}
end if
end tell
return theRecord
end ForOne
on GetNewNameLoop(theRecord, localIsDev, theEngine, theModel, thePrompt, localAiRequestTimeout)
local oldName
tell application id "DNtp"
set oldName to filename of theRecord
end tell
local theResponse
local repeatCounter
set repeatCounter to 1
repeat
try
my recordLog(theRecord, "DEBUG", quoted form of "Send request to AI models", missing value)
set theResponse to my GetNewName(theRecord, localIsDev, theEngine, theModel, thePrompt, localAiRequestTimeout)
exit repeat
on error errStr number errorNumber
delay 1
-- nothing
end try
if repeatCounter ≥ 3 then
error "Repeat counter ≥ 3 for GetNewName(): " & errStr & ". Exit"
end if
set repeatCounter to repeatCounter + 1
end repeat
return theResponse
end GetNewNameLoop
on GetNewName(theRecord, localIsDev, theEngine, theModel, thePrompt, localAiRequestTimeout)
local AiBase
set AiBase to script "ai-base"
local now
set now to current date
tell application id "DNtp"
-- Get date components
set currentYear to year of now
set currentMonth to rich texts -2 thru -1 of ("00" & ((month of now) as integer))
set currentDay to rich texts -2 thru -1 of ("00" & (day of now))
end tell
set currentDate to currentYear - 40 & "-" & currentMonth & "-" & currentDay as string
set thePrompt to thePrompt & "¬
CURRENT_DATE=" & currentDate
-- log "[DEBUG] Validate chosen AI engine"
-- set theEngine to AiBase's ValidateAndReturnEngine(theEngine)
local theJsonResponse
set theJsonResponse to AiBase's AskModelTimeout(theRecord, theEngine, theModel, thePrompt, "JSON", localAiRequestTimeout)
if theJsonResponse is missing value then
log "[WARN] No (valid) result from AI"
return missing value
end if
return theJsonResponse
end GetNewName
on recordLog(theRecord, level, msg, msgInfo)
if msgInfo is missing value then
set logMsg to "msg=" & msg
else
set logMsg to "msg=" & msg & " " & msgInfo
end if
if level is not "DEBUG" then
tell application id "DNtp"
log message record theRecord info "level=INFO " & logMsg
end tell
return
end if
if globalIsDev is true then
tell application id "DNtp"
log message record theRecord info "level=DEBUG " & logMsg
end tell
end if
end recordLog
on globalLog(level, msg, msgInfo)
if msgInfo is missing value then
set logMsg to "msg=" & msg
else
set logMsg to "msg=" & msg & " " & msgInfo
end if
if level is not "DEBUG" then
tell application id "DNtp"
log message "level=INFO " & logMsg
end tell
return
end if
if globalIsDev is true then
tell application id "DNtp"
log message "level=DEBUG " & logMsg
end tell
end if
end globalLog
on toDate(isotDate)
set df to current application's NSDateFormatter's new()
df's setDateFormat:"y-M-d"
local theDate
set theDate to (df's dateFromString:isotDate) as date
return theDate
end toDate
ai runner script - ai-base.scpt
use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"
use script "RegexAndStuffLib" version "1.0.7"
property globalIsDev : false
-- get list of known AI engines
tell application id "DNtp" to my ListModelsForEngines()
on ListModelsForEngines()
tell application id "DNtp"
set mChatGPT to get chat models for engine ChatGPT
log message (join strings mChatGPT using delimiter ",")
set mMistral to (get chat models for engine Mistral)
log message (join strings mMistral using delimiter ",")
set mOllama to (get chat models for engine Ollama)
log message (join strings mOllama using delimiter ",")
set mLMStudio to (get chat models for engine LM Studio)
log message (join strings mLMStudio using delimiter ",")
set mClaude to (get chat models for engine Claude)
log message (join strings mClaude using delimiter ",")
end tell
end ListModelsForEngines
on AskModelTimeout(theRecord, theEngine, theModel, thePrompt, theResultFormat, theTimeout)
if theResultFormat is missing value then
set theResultFormat to "JSON"
end if
if theTimeout is missing value then
set theTimeout to 60 as integer
end if
tell application id "DNtp"
with timeout of theTimeout seconds
local theResponse
my recordLog(theRecord, "DEBUG", "Get response based on engine and model selection", "engine=" & theEngine & " model=" & theModel)
if theModel is missing value then
set theResponse to get chat response for message thePrompt engine theEngine record theRecord temperature 0 as theResultFormat
else
set theResponse to get chat response for message thePrompt engine theEngine model theModel record theRecord temperature 0 as theResultFormat
end if
return theResponse
end timeout
end tell
end AskModelTimeout
on AskModel(theRecord, theEngine, theModel, thePrompt, theResultFormat)
if theResultFormat is missing value then
set theResultFormat to "JSON"
end if
tell application id "DNtp"
with timeout of 60 seconds
local theResponse
my recordLog(theRecord, "DEBUG", "Get response based on engine and model selection", "engine=" & theEngine & " model=" & theModel)
if theModel is missing value then
set theResponse to get chat response for message thePrompt engine theEngine record theRecord temperature 0 as theResultFormat
else
set theResponse to get chat response for message thePrompt engine theEngine model theModel record theRecord temperature 0 as theResultFormat
end if
return theResponse
end timeout
end tell
end AskModel
on AskModelForUrlTimeout(theUrl, theEngine, theModel, thePrompt, theResultFormat, theTimeout)
if theResultFormat is missing value then
set theResultFormat to "JSON"
end if
if theTimeout is missing value then
set theTimeout to 60 as integer
end if
tell application id "DNtp"
with timeout of theTimeout seconds
local theResponse
my recordLog(theRecord, "DEBUG", "Get response based on engine and model selection for URL", missing value)
if theModel is missing value then
set theResponse to get chat response for message thePrompt engine theEngine URL theUrl temperature 0 as theResultFormat
else
set theResponse to get chat response for message thePrompt engine theEngine model theModel URL theUrl temperature 0 as theResultFormat
end if
return theResponse
end timeout
end tell
end AskModelForUrlTimeout
on AskModelForUrl(theUrl, theEngine, theModel, thePrompt, theResultFormat)
if theResultFormat is missing value then
set theResultFormat to "JSON"
end if
tell application id "DNtp"
with timeout of 60 seconds
local theResponse
my recordLog(theRecord, "DEBUG", "Get response based on engine and model selection for URL", missing value)
if theModel is missing value then
set theResponse to get chat response for message thePrompt engine theEngine URL theUrl temperature 0 as theResultFormat
else
set theResponse to get chat response for message thePrompt engine theEngine model theModel URL theUrl temperature 0 as theResultFormat
end if
return theResponse
end timeout
end tell
end AskModelForUrl
on GenImage(theRecord, theEngine, thePrompt, theSize)
if theSize is missing value then
set theSize to "1024x1024"
end if
tell application id "DNtp"
with timeout of 60 seconds
local theResponse
my recordLog(theRecord, "DEBUG", "Get response based on engine selection", missing value)
set theResponse to download image for prompt thePrompt engine theEngine size theSize
return theResponse
end timeout
end tell
end GenImage
on GetParentLocation(theRecord)
tell application id "DNtp"
local theParents
set theParents to parents of theRecord
local theParentGroup
set theParentGroup to (item 1 of theParents)
return theParentGroup
end tell
end GetParentLocation
on SetupLocalAiRuntime()
if isLocalAppRunning("MstyStudio") then
return
end if
tell application "MstyStudio" to activate
tell application "System Events"
set visible of application process "MstyStudio" to false
end tell
tell application id "DNtp" to activate
delay 10
end SetupLocalAiRuntime
on isLocalAppRunning(appName)
tell application "System Events" to (name of processes) contains appName
end isLocalAppRunning
on recordLog(theRecord, level, msg, msgInfo)
if msgInfo is missing value then
set logMsg to "msg=" & msg
else
set logMsg to "msg=" & msg & " " & msgInfo
end if
if level is not "DEBUG" then
tell application id "DNtp"
log message record theRecord info "level=INFO " & logMsg
end tell
return
end if
if globalIsDev is true then
tell application id "DNtp"
log message record theRecord info "level=DEBUG " & logMsg
end tell
end if
end recordLog
on globalLog(level, msg, msgInfo)
if msgInfo is missing value then
set logMsg to "msg=" & msg
else
set logMsg to "msg=" & msg & " " & msgInfo
end if
if level is not "DEBUG" then
tell application id "DNtp"
log message "level=INFO " & logMsg
end tell
return
end if
if globalIsDev is true then
tell application id "DNtp"
log message "level=DEBUG " & logMsg
end tell
end if
end globalLog
library loader with logging - library-handling.scpt
use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
property globalIsDev : true
on loadScript(scriptName, localIsDev)
if localIsDev is true then
display alert "Development Mode enabled"
end if
set theScriptHandler to my loadScriptSilent(scriptName, localIsDev)
return theScriptHandler
end loadScript
on loadScriptSilent(scriptName, localIsDev)
local theScriptPath
set theScriptPath to (system attribute "HOME") & "/Library/Script Libraries/" & scriptName & ".scpt"
my globalLog("DEBUG", "Set script path", "path=" & quoted form of theScriptPath)
-- local theScriptHandler
set theScriptHandler to load script (POSIX file theScriptPath as alias)
if localIsDev is true then
set theScriptHandler to script scriptName
end if
return theScriptHandler
end loadScriptSilent
on canRun(theDatabase, theRecords)
tell application id "DNtp"
if not (exists theDatabase) then error "No database is in use."
if the length of theRecords is less than 1 then error "One or more record must be selected."
end tell
end canRun
on globalLog(level, msg, msgInfo)
if msgInfo is missing value then
set logMsg to "msg=" & msg
else
set logMsg to "msg=" & msg & " " & msgInfo
end if
if level is not "DEBUG" then
tell application id "DNtp"
log message "level=INFO " & logMsg
end tell
return
end if
if globalIsDev is true then
tell application id "DNtp"
log message "level=DEBUG " & logMsg
end tell
end if
end globalLog