DT4 - show window to review changes

Warning

Please note:

Update

This version adds/changes/requires:

  • ADD field to change existing tags
  • ADD: Integrates with AI to rename documents
  • REQUIRES: AI which handles the renaming - I prefer local MstyStudio which includes Ollama as backend + phi4 as model for that, but Ollama standallone, LM Studio etc. should be fine, too.
  • ADD: Presets which sets fields within the document DYNAMICALLY - but there’s a price to pay: Each run within Script Debugger of the script requires a recompile
  • ADD: Re-evaluate rules (Redo) after changing them during runtime
  • ADD: Delete file from dialogue
  • ADD: Databases are filled dynamically from databases opened in DT
  • CHANGES: Total refactoring of tell application id "DNtp". I wrapped Dialog Toolkit Plus with that code. This breaks “Dialog Toolkit Plus” with a “Main Thread warning”. Now I only wrap code which requires DT

AI renaming

It takes about 10 s to 20 s per file on a M2 Mac with 15.x to rename a file. Most times the result is fine. I use the renaming as part of my input pipeline to change file names generated by my Epson scanner (scan-.pdf). For other documents I got by mail, I added the button in my review process.

I think about moving to two tier approach:

  • LLM generate unstructured output containing the name
  • SLM generate structured output from the LLM output

I read some blogs mentioning an improvement in speed.

More information

This code is based on examples found here: Change elements dynamically using Shane's Dialog Toolkit? - #11 by t.spoon - AppleScript | Mac OS X - MacScripter plus own research within the Dialog Toolkit Plus code base.

The code allows a developer to use a dropdown to change values of other fields within the dialog. I use it to configure presets. The presets are defined within a property as array of dictionaries. The ¬ makes it possible to break up the long string - <opt>+L.

property dialogPresets : {¬
	{presetTitle:"Global: No preset", yearlyLocation:true, basePath:"", databaseName:"", addTags:{}, companyName:""}, ¬
	{presetTitle:"Global: No rule", yearlyLocation:false, basePath:"/_Review/.NORULE", databaseName:"Inbox", addTags:{}, companyName:""}, ¬
	{presetTitle:"Global: Handbücher", yearlyLocation:false, basePath:"/Handbuecher", databaseName:"knowledge", addTags:{"handbuch"}, companyName:""}, ¬
	{presetTitle:"Global: Rente", yearlyLocation:true, basePath:"/Rente", databaseName:"business", addTags:{"rente"}, companyName:""}, ¬
	{presetTitle:"Global: Projekte", yearlyLocation:false, basePath:"/_Projekte/" & currentYear & "/CHANGEME", databaseName:"personal", addTags:{"projekt"}, companyName:""}, ¬
	{presetTitle:"Global: Sonstiges-Behoerden", yearlyLocation:true, basePath:"/Vertraege-und-Rechnungen/_Sonstiges-Behoerden", databaseName:"business", addTags:{"verwaltung", "behoerden"}, companyName:""}, ¬
	{presetTitle:"Global: Sostiges-Gesundheit", yearlyLocation:true, basePath:"/Vertraege-und-Rechnungen/_Sonstiges-Gesundheit", databaseName:"business", addTags:{"health", "gesundheit"}, companyName:""}, ¬
	{presetTitle:"Global: Sonstiges-Kaufbelege", yearlyLocation:true, basePath:"/Vertraege-und-Rechnungen/_Sonstiges-Kaufbelege", databaseName:"business", addTags:{"rechnungen", "invoice"}, companyName:""}, ¬
	{presetTitle:"Global: Tipps", yearlyLocation:false, basePath:"/Tipps/Assets", databaseName:"knowledge", addTags:{"tipp"}, companyName:""}, ¬
	{presetTitle:"Global: Urlaub", yearlyLocation:true, basePath:"/_Urlaub", databaseName:"personal", addTags:{"urlaub", "holidays"}, companyName:""}, ¬
	{presetTitle:"Global: Vorlagen", yearlyLocation:false, basePath:"/_Neu", databaseName:"templates", addTags:{"template"}, companyName:""} ¬
		}

It requires properties for each supported field defined.

on updateOtherFields:sender
	try
		-- set index of selected dropdown item
		local selectedPresetIndex
		set selectedPresetIndex to (my presetsPopup's indexOfSelectedItem() as integer) + 1
		set selectedPreset to item selectedPresetIndex of dialogPresets
		
		-- get values for selected dropdown
		local selectedCompanyName, selectedLocation, selectedTags, joinedTags, selectedDatabase, selectedYearlyLocation
		set selectedCompanyName to (companyName of selectedPreset as text)
		set selectedLocation to (basePath of selectedPreset as text)
		
		local selectedTags
		set selectedTags to (addTags of selectedPreset)
		
		set joinedTags to (join strings selectedTags using delimiter ",")
		
		set selectedDatabase to (databaseName of selectedPreset as text)
		set selectedYearlyLocation to (yearlyLocation of selectedPreset as boolean)
		
		-- set values
		-- https://www.macscripter.net/t/change-elements-dynamically-using-shanes-dialog-toolkit/70409/11
		my (companyField's setStringValue:selectedCompanyName)
		my (locationPathField's setStringValue:selectedLocation)
		my (tagsField's setStringValue:joinedTags)
		my (databaseField's setStringValue:selectedDatabase)
		my (yearlyLocationCheckbox's setState:selectedYearlyLocation)
	on error errMsg number errNum partial result partialError
		set AppleScript's text item delimiters to {return}
		-- An unknown error occurred. Resignal, so the caller
		-- can handle it, or AppleScript can display the number.
		display alert errMsg & ("Error number: ") & errNum & return & (partialError as text)
		error errMsg & ("Error number: ") & errNum & return & (partialError as text)
	end try
end updateOtherFields:
1 Like

Information

Store the files at ~/Library/Script Libraries. Review my code. I copied it over and removed some parts as these are not required for the demonstrated use case. I hope there’s some use of these scripts for others - even in the case when they demonstrate what you should NOT do ;-).

Idea behind the solution

  • I prefer to review changes to my documents that are suggested by scripts.
  • I set up rules for my documents to generate a good baseline for where the documents I regularly receive should be stored.
  • For other documents, I configured templates that I can choose from via a dropdown menu.
  • Ultimately, this script should speed up my process of moving documents to their destination.
1 Like

document-rename.scpt

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

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¬
- 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¬
- Avoid generic terms at all costs like \"document\", \"file\", or \"text\"¬
- Avoid any names of persons at all costs¬
- Follow the format: topic_subtopic_descriptor (if applicable)¬
# Steps¬
1. Create a management summary for the text¬
2. Capture the main topic or purpose based on the text¬
3. Extract dates from the text - ignore ones before the date saved in the NOW-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 and prefix the name with the date¬
7. Remove first names and surnames of persons + prices from filename
8. Translate the captured text into German - if given in any other language
# Expected output¬
The result is in JSON notation. The filename is stored with key \"new_filename\"¬ 
Example:¬
```json¬
{¬
 \"new_filename\": \"1970-10-01_this-is-the-file-name\"¬
}¬
```¬
# Variables

"

-- test performSmartRule
property globalIsDev : true



on GetNewNameLoop(theRecord, localIsDev, theEngine, theModel, thePrompt, localAiRequestTimeout)
	local oldName
	tell application id "DNtp"
		set oldName to filename of theRecord
	end tell
	
	local newName
	
	local repeatCounter
	set repeatCounter to 1
	repeat
		try
			my recordLog(theRecord, "DEBUG", quoted form of "Send request to AI model", missing value)
			set newName to my GetNewName(theRecord, localIsDev, theEngine, theModel, thePrompt, localAiRequestTimeout)
			exit repeat
		on error errStr number errorNumber
			-- nothing
		end try
		
		if repeatCounter ≥ 3 then
			error "Repeat counter ≥ 3 for GetNewName(): " & errStr & ". Exit"
		end if
		
		set repeatCounter to repeatCounter + 1
	end repeat
	
	my recordLog(theRecord, "INFO", quoted form of "Renamed file with AI", "old_name=" & quoted form of oldName & " new_name=" & quoted form of newName)
	
	return newName
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 & "-" & 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 theResponse
	set theResponse to AiBase's AskModelTimeout(theRecord, theEngine, theModel, thePrompt, "JSON", localAiRequestTimeout)
	
	if theResponse is missing value then
		log "[WARN] No (valid) result from AI"
		return missing value
	end if
	
	return new_filename of theResponse
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

1 Like