supervertaler 1.9.163__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. Supervertaler.py +48473 -0
  2. modules/__init__.py +10 -0
  3. modules/ai_actions.py +964 -0
  4. modules/ai_attachment_manager.py +343 -0
  5. modules/ai_file_viewer_dialog.py +210 -0
  6. modules/autofingers_engine.py +466 -0
  7. modules/cafetran_docx_handler.py +379 -0
  8. modules/config_manager.py +469 -0
  9. modules/database_manager.py +1911 -0
  10. modules/database_migrations.py +417 -0
  11. modules/dejavurtf_handler.py +779 -0
  12. modules/document_analyzer.py +427 -0
  13. modules/docx_handler.py +689 -0
  14. modules/encoding_repair.py +319 -0
  15. modules/encoding_repair_Qt.py +393 -0
  16. modules/encoding_repair_ui.py +481 -0
  17. modules/feature_manager.py +350 -0
  18. modules/figure_context_manager.py +340 -0
  19. modules/file_dialog_helper.py +148 -0
  20. modules/find_replace.py +164 -0
  21. modules/find_replace_qt.py +457 -0
  22. modules/glossary_manager.py +433 -0
  23. modules/image_extractor.py +188 -0
  24. modules/keyboard_shortcuts_widget.py +571 -0
  25. modules/llm_clients.py +1211 -0
  26. modules/llm_leaderboard.py +737 -0
  27. modules/llm_superbench_ui.py +1401 -0
  28. modules/local_llm_setup.py +1104 -0
  29. modules/model_update_dialog.py +381 -0
  30. modules/model_version_checker.py +373 -0
  31. modules/mqxliff_handler.py +638 -0
  32. modules/non_translatables_manager.py +743 -0
  33. modules/pdf_rescue_Qt.py +1822 -0
  34. modules/pdf_rescue_tkinter.py +909 -0
  35. modules/phrase_docx_handler.py +516 -0
  36. modules/project_home_panel.py +209 -0
  37. modules/prompt_assistant.py +357 -0
  38. modules/prompt_library.py +689 -0
  39. modules/prompt_library_migration.py +447 -0
  40. modules/quick_access_sidebar.py +282 -0
  41. modules/ribbon_widget.py +597 -0
  42. modules/sdlppx_handler.py +874 -0
  43. modules/setup_wizard.py +353 -0
  44. modules/shortcut_manager.py +932 -0
  45. modules/simple_segmenter.py +128 -0
  46. modules/spellcheck_manager.py +727 -0
  47. modules/statuses.py +207 -0
  48. modules/style_guide_manager.py +315 -0
  49. modules/superbench_ui.py +1319 -0
  50. modules/superbrowser.py +329 -0
  51. modules/supercleaner.py +600 -0
  52. modules/supercleaner_ui.py +444 -0
  53. modules/superdocs.py +19 -0
  54. modules/superdocs_viewer_qt.py +382 -0
  55. modules/superlookup.py +252 -0
  56. modules/tag_cleaner.py +260 -0
  57. modules/tag_manager.py +351 -0
  58. modules/term_extractor.py +270 -0
  59. modules/termbase_entry_editor.py +842 -0
  60. modules/termbase_import_export.py +488 -0
  61. modules/termbase_manager.py +1060 -0
  62. modules/termview_widget.py +1176 -0
  63. modules/theme_manager.py +499 -0
  64. modules/tm_editor_dialog.py +99 -0
  65. modules/tm_manager_qt.py +1280 -0
  66. modules/tm_metadata_manager.py +545 -0
  67. modules/tmx_editor.py +1461 -0
  68. modules/tmx_editor_qt.py +2784 -0
  69. modules/tmx_generator.py +284 -0
  70. modules/tracked_changes.py +900 -0
  71. modules/trados_docx_handler.py +430 -0
  72. modules/translation_memory.py +715 -0
  73. modules/translation_results_panel.py +2134 -0
  74. modules/translation_services.py +282 -0
  75. modules/unified_prompt_library.py +659 -0
  76. modules/unified_prompt_manager_qt.py +3951 -0
  77. modules/voice_commands.py +920 -0
  78. modules/voice_dictation.py +477 -0
  79. modules/voice_dictation_lite.py +249 -0
  80. supervertaler-1.9.163.dist-info/METADATA +906 -0
  81. supervertaler-1.9.163.dist-info/RECORD +85 -0
  82. supervertaler-1.9.163.dist-info/WHEEL +5 -0
  83. supervertaler-1.9.163.dist-info/entry_points.txt +2 -0
  84. supervertaler-1.9.163.dist-info/licenses/LICENSE +21 -0
  85. supervertaler-1.9.163.dist-info/top_level.txt +2 -0
modules/ai_actions.py ADDED
@@ -0,0 +1,964 @@
1
+ """
2
+ AI Actions Module
3
+
4
+ Provides structured action interface for AI Assistant to interact with Supervertaler
5
+ resources (Prompt Library, Translation Memories, Termbases).
6
+
7
+ Phase 2 of AI Assistant Enhancement Plan:
8
+ - Parses ACTION markers from AI responses
9
+ - Executes actions on prompt library
10
+ - Returns structured results for display
11
+
12
+ Action Format:
13
+ ACTION:function_name
14
+ PARAMS:{"param1": "value1", "param2": "value2"}
15
+
16
+ Example:
17
+ ACTION:list_prompts
18
+ PARAMS:{"folder": "Domain Expertise"}
19
+
20
+ ACTION:create_prompt
21
+ PARAMS:{"name": "Medical Translator", "content": "...", "folder": "Domain Expertise"}
22
+ """
23
+
24
+ import json
25
+ import re
26
+ from datetime import datetime
27
+ from typing import Dict, List, Optional, Tuple, Any
28
+
29
+
30
+ class AIActionSystem:
31
+ """
32
+ Handles parsing and execution of AI actions on Supervertaler resources.
33
+ """
34
+
35
+ def __init__(self, prompt_library, parent_app=None, log_callback=None):
36
+ """
37
+ Initialize AI Action System.
38
+
39
+ Args:
40
+ prompt_library: UnifiedPromptLibrary instance
41
+ parent_app: Reference to main application (for segment access)
42
+ log_callback: Function to call for logging messages
43
+ """
44
+ self.prompt_library = prompt_library
45
+ self.parent_app = parent_app
46
+ self.log = log_callback if log_callback else print
47
+
48
+ # Map action names to handler methods
49
+ self.action_handlers = {
50
+ 'list_prompts': self._action_list_prompts,
51
+ 'get_prompt': self._action_get_prompt,
52
+ 'create_prompt': self._action_create_prompt,
53
+ 'update_prompt': self._action_update_prompt,
54
+ 'delete_prompt': self._action_delete_prompt,
55
+ 'search_prompts': self._action_search_prompts,
56
+ 'create_folder': self._action_create_folder,
57
+ 'toggle_favorite': self._action_toggle_favorite,
58
+ 'toggle_quick_run': self._action_toggle_quick_run,
59
+ 'get_favorites': self._action_get_favorites,
60
+ 'get_quick_run': self._action_get_quick_run,
61
+ 'get_folder_structure': self._action_get_folder_structure,
62
+ 'get_segment_count': self._action_get_segment_count,
63
+ 'get_segment_info': self._action_get_segment_info,
64
+ 'activate_prompt': self._action_activate_prompt,
65
+ }
66
+
67
+ def parse_and_execute(self, ai_response: str) -> Tuple[str, List[Dict]]:
68
+ """
69
+ Parse AI response for ACTION markers and execute actions.
70
+
71
+ Args:
72
+ ai_response: Full response text from AI
73
+
74
+ Returns:
75
+ Tuple of (cleaned_response, list of action results)
76
+ cleaned_response: AI response with ACTION blocks removed
77
+ action_results: List of {action, params, success, result/error}
78
+ """
79
+ action_results = []
80
+
81
+ # Strip markdown code fences if present (Claude often wraps in ```yaml or ```)
82
+ # Handle both block-level and inline code fences
83
+ self.log(f"[DEBUG] Original response length: {len(ai_response)} chars")
84
+ self.log(f"[DEBUG] First 200 chars: {ai_response[:200]}")
85
+
86
+ # Remove opening fence: ```yaml, ```json, or just ``` (at start or after newline)
87
+ ai_response = re.sub(r'(^|\n)```(?:yaml|json|)?\s*\n?', r'\1', ai_response)
88
+ # Remove closing fence: ``` (at end or before newline)
89
+ ai_response = re.sub(r'\n?```\s*($|\n)', r'\1', ai_response)
90
+ # Remove any remaining standalone backticks or language markers
91
+ ai_response = re.sub(r'^`(?:yaml|json|)\s*$', '', ai_response, flags=re.MULTILINE)
92
+
93
+ self.log(f"[DEBUG] After fence stripping length: {len(ai_response)} chars")
94
+ self.log(f"[DEBUG] After fence stripping first 200 chars: {ai_response[:200]}")
95
+
96
+ cleaned_response = ai_response
97
+
98
+ # Find all ACTION blocks
99
+ # Pattern: ACTION:name PARAMS:... (with optional newline between)
100
+ # Handles both "ACTION:name\nPARAMS:" and "ACTION:name PARAMS:"
101
+ action_pattern = r'ACTION:(\w+)\s+PARAMS:\s*'
102
+ matches = list(re.finditer(action_pattern, ai_response))
103
+
104
+ self.log(f"[DEBUG] Found {len(matches)} ACTION blocks")
105
+
106
+ # Process each ACTION block
107
+ for i, match in enumerate(matches):
108
+ action_name = match.group(1)
109
+ start_pos = match.end()
110
+
111
+ # Find where this action's params end (next ACTION: or end of string)
112
+ if i + 1 < len(matches):
113
+ end_pos = matches[i + 1].start()
114
+ else:
115
+ end_pos = len(ai_response)
116
+
117
+ params_str = ai_response[start_pos:end_pos].strip()
118
+
119
+ # Extract just the JSON part (up to first newline followed by non-JSON char)
120
+ # Look for the closing brace of the JSON object
121
+ brace_count = 0
122
+ json_end = 0
123
+ in_string = False
124
+ escape_next = False
125
+
126
+ for idx, char in enumerate(params_str):
127
+ if escape_next:
128
+ escape_next = False
129
+ continue
130
+
131
+ if char == '\\':
132
+ escape_next = True
133
+ continue
134
+
135
+ if char == '"' and not escape_next:
136
+ in_string = not in_string
137
+
138
+ if not in_string:
139
+ if char == '{':
140
+ brace_count += 1
141
+ elif char == '}':
142
+ brace_count -= 1
143
+ if brace_count == 0:
144
+ json_end = idx + 1
145
+ break
146
+
147
+ if json_end > 0:
148
+ params_str = params_str[:json_end]
149
+
150
+ try:
151
+ # Parse parameters
152
+ params = json.loads(params_str)
153
+
154
+ # Execute action
155
+ result = self.execute_action(action_name, params)
156
+ action_results.append(result)
157
+
158
+ # Reload prompt library immediately if prompt was created/updated/deleted
159
+ # This ensures subsequent actions (like activate_prompt) see the new prompt
160
+ if result['success'] and action_name in ['create_prompt', 'update_prompt', 'delete_prompt']:
161
+ self.prompt_library.load_all_prompts()
162
+ self.log(f"✓ Reloaded prompt library after {action_name}")
163
+
164
+ # Remove ACTION block from response
165
+ # Include the full ACTION block: from start of match to end of JSON
166
+ full_block = ai_response[match.start():start_pos + json_end]
167
+ cleaned_response = cleaned_response.replace(full_block, '')
168
+
169
+ except json.JSONDecodeError as e:
170
+ self.log(f"✗ Failed to parse action parameters: {e}")
171
+ action_results.append({
172
+ 'action': action_name,
173
+ 'params': params_str,
174
+ 'success': False,
175
+ 'error': f"Invalid JSON parameters: {e}"
176
+ })
177
+ except Exception as e:
178
+ self.log(f"✗ Action execution error: {e}")
179
+ action_results.append({
180
+ 'action': action_name,
181
+ 'params': params_str,
182
+ 'success': False,
183
+ 'error': str(e)
184
+ })
185
+
186
+ return cleaned_response.strip(), action_results
187
+
188
+ def execute_action(self, action_name: str, params: Dict) -> Dict:
189
+ """
190
+ Execute a single action with given parameters.
191
+
192
+ Args:
193
+ action_name: Name of the action
194
+ params: Dictionary of parameters
195
+
196
+ Returns:
197
+ Dictionary with action result: {action, params, success, result/error}
198
+ """
199
+ if action_name not in self.action_handlers:
200
+ return {
201
+ 'action': action_name,
202
+ 'params': params,
203
+ 'success': False,
204
+ 'error': f"Unknown action: {action_name}"
205
+ }
206
+
207
+ try:
208
+ handler = self.action_handlers[action_name]
209
+ result = handler(params)
210
+
211
+ return {
212
+ 'action': action_name,
213
+ 'params': params,
214
+ 'success': True,
215
+ 'result': result
216
+ }
217
+ except Exception as e:
218
+ self.log(f"✗ Error executing {action_name}: {e}")
219
+ return {
220
+ 'action': action_name,
221
+ 'params': params,
222
+ 'success': False,
223
+ 'error': str(e)
224
+ }
225
+
226
+ # ========================================================================
227
+ # ACTION HANDLERS
228
+ # ========================================================================
229
+
230
+ def _action_list_prompts(self, params: Dict) -> Dict:
231
+ """
232
+ List prompts in library, optionally filtered by folder.
233
+
234
+ Params:
235
+ folder (optional): Folder path to filter by
236
+ include_content (optional): Include full content (default: False)
237
+
238
+ Returns:
239
+ {count: int, prompts: [{name, path, folder, favorite, quick_run, ...}]}
240
+ """
241
+ folder_filter = params.get('folder')
242
+ include_content = params.get('include_content', False)
243
+
244
+ prompts_list = []
245
+ for path, prompt_data in self.prompt_library.prompts.items():
246
+ # Apply folder filter if specified
247
+ if folder_filter:
248
+ if prompt_data.get('_folder') != folder_filter:
249
+ continue
250
+
251
+ # Build prompt info
252
+ prompt_info = {
253
+ 'name': prompt_data.get('name', path),
254
+ 'path': path,
255
+ 'folder': prompt_data.get('_folder', ''),
256
+ 'description': prompt_data.get('description', ''),
257
+ 'favorite': prompt_data.get('favorite', False),
258
+ 'quick_run': prompt_data.get('quick_run', False),
259
+ 'tags': prompt_data.get('tags', [])
260
+ }
261
+
262
+ if include_content:
263
+ prompt_info['content'] = prompt_data.get('content', '')
264
+
265
+ prompts_list.append(prompt_info)
266
+
267
+ return {
268
+ 'count': len(prompts_list),
269
+ 'prompts': prompts_list
270
+ }
271
+
272
+ def _action_get_prompt(self, params: Dict) -> Dict:
273
+ """
274
+ Get full details of a specific prompt.
275
+
276
+ Params:
277
+ path (required): Relative path to prompt
278
+
279
+ Returns:
280
+ {name, path, folder, content, metadata...}
281
+ """
282
+ path = params.get('path')
283
+ if not path:
284
+ raise ValueError("Missing required parameter: path")
285
+
286
+ if path not in self.prompt_library.prompts:
287
+ raise ValueError(f"Prompt not found: {path}")
288
+
289
+ prompt_data = self.prompt_library.prompts[path]
290
+
291
+ return {
292
+ 'name': prompt_data.get('name', path),
293
+ 'path': path,
294
+ 'folder': prompt_data.get('_folder', ''),
295
+ 'description': prompt_data.get('description', ''),
296
+ 'content': prompt_data.get('content', ''),
297
+ 'favorite': prompt_data.get('favorite', False),
298
+ 'quick_run': prompt_data.get('quick_run', False),
299
+ 'tags': prompt_data.get('tags', []),
300
+ 'domain': prompt_data.get('domain', ''),
301
+ 'task_type': prompt_data.get('task_type', ''),
302
+ 'version': prompt_data.get('version', '1.0'),
303
+ 'created': prompt_data.get('created', ''),
304
+ 'modified': prompt_data.get('modified', '')
305
+ }
306
+
307
+ def _action_create_prompt(self, params: Dict) -> Dict:
308
+ """
309
+ Create a new prompt in the library.
310
+
311
+ Params:
312
+ name (required): Prompt name
313
+ content (required): Prompt content
314
+ folder (optional): Folder to create in (default: root)
315
+ description (optional): Prompt description
316
+ tags (optional): List of tags
317
+ domain (optional): Domain category
318
+ task_type (optional): Task type
319
+ activate (optional): If True, activate as primary after creating
320
+
321
+ Returns:
322
+ {success: bool, path: str, message: str}
323
+ """
324
+ name = params.get('name')
325
+ content = params.get('content')
326
+
327
+ if not name or not content:
328
+ raise ValueError("Missing required parameters: name and content")
329
+
330
+ # Build relative path
331
+ folder = params.get('folder', '')
332
+ # Sanitize filename - remove/replace invalid Windows filename characters
333
+ # Invalid chars: < > : " / \ | ? * and also → (arrow) which AI likes to use
334
+ filename = name
335
+ invalid_chars = ['/', '\\', '<', '>', ':', '"', '|', '?', '*', '→', '←', '↔']
336
+ for char in invalid_chars:
337
+ filename = filename.replace(char, '-')
338
+ # Also clean up multiple dashes and trim
339
+ import re
340
+ filename = re.sub(r'-+', '-', filename).strip('-')
341
+ filename = filename + '.svprompt'
342
+ relative_path = f"{folder}/{filename}" if folder else filename
343
+
344
+ # Build prompt data
345
+ prompt_data = {
346
+ 'name': name,
347
+ 'content': content,
348
+ 'description': params.get('description', ''),
349
+ 'domain': params.get('domain', ''),
350
+ 'task_type': params.get('task_type', 'Translation'),
351
+ 'version': '1.0',
352
+ 'favorite': False,
353
+ 'quick_run': False,
354
+ 'folder': folder,
355
+ 'tags': params.get('tags', []),
356
+ 'created': datetime.now().strftime('%Y-%m-%d'),
357
+ 'modified': datetime.now().strftime('%Y-%m-%d')
358
+ }
359
+
360
+ # Save prompt
361
+ success = self.prompt_library.save_prompt(relative_path, prompt_data)
362
+
363
+ if success:
364
+ message = f"Created prompt: {name}"
365
+
366
+ # Auto-activate if requested
367
+ if params.get('activate', False):
368
+ self.prompt_library.set_primary_prompt(relative_path)
369
+ message += f" and activated as primary"
370
+
371
+ return {
372
+ 'success': True,
373
+ 'path': relative_path,
374
+ 'message': message
375
+ }
376
+ else:
377
+ raise Exception("Failed to save prompt")
378
+
379
+ def _action_update_prompt(self, params: Dict) -> Dict:
380
+ """
381
+ Update an existing prompt.
382
+
383
+ Params:
384
+ path (required): Relative path to prompt
385
+ content (optional): New content
386
+ name (optional): New name
387
+ description (optional): New description
388
+ tags (optional): New tags
389
+
390
+ Returns:
391
+ {success: bool, path: str, message: str}
392
+ """
393
+ path = params.get('path')
394
+ if not path:
395
+ raise ValueError("Missing required parameter: path")
396
+
397
+ if path not in self.prompt_library.prompts:
398
+ raise ValueError(f"Prompt not found: {path}")
399
+
400
+ # Get existing prompt data
401
+ prompt_data = self.prompt_library.prompts[path].copy()
402
+
403
+ # Update fields
404
+ if 'name' in params:
405
+ prompt_data['name'] = params['name']
406
+ if 'content' in params:
407
+ prompt_data['content'] = params['content']
408
+ if 'description' in params:
409
+ prompt_data['description'] = params['description']
410
+ if 'tags' in params:
411
+ prompt_data['tags'] = params['tags']
412
+
413
+ prompt_data['modified'] = datetime.now().strftime('%Y-%m-%d')
414
+
415
+ # Save updated prompt
416
+ success = self.prompt_library.save_prompt(path, prompt_data)
417
+
418
+ if success:
419
+ return {
420
+ 'success': True,
421
+ 'path': path,
422
+ 'message': f"Updated prompt: {prompt_data['name']}"
423
+ }
424
+ else:
425
+ raise Exception("Failed to update prompt")
426
+
427
+ def _action_delete_prompt(self, params: Dict) -> Dict:
428
+ """
429
+ Delete a prompt from the library.
430
+
431
+ Params:
432
+ path (required): Relative path to prompt
433
+
434
+ Returns:
435
+ {success: bool, path: str, message: str}
436
+ """
437
+ path = params.get('path')
438
+ if not path:
439
+ raise ValueError("Missing required parameter: path")
440
+
441
+ if path not in self.prompt_library.prompts:
442
+ raise ValueError(f"Prompt not found: {path}")
443
+
444
+ name = self.prompt_library.prompts[path].get('name', path)
445
+ success = self.prompt_library.delete_prompt(path)
446
+
447
+ if success:
448
+ return {
449
+ 'success': True,
450
+ 'path': path,
451
+ 'message': f"Deleted prompt: {name}"
452
+ }
453
+ else:
454
+ raise Exception("Failed to delete prompt")
455
+
456
+ def _action_search_prompts(self, params: Dict) -> Dict:
457
+ """
458
+ Search prompts by name, content, or tags.
459
+
460
+ Params:
461
+ query (required): Search query
462
+ search_in (optional): 'name', 'content', 'tags', or 'all' (default: all)
463
+
464
+ Returns:
465
+ {count: int, results: [{name, path, folder, match_type, ...}]}
466
+ """
467
+ query = params.get('query', '').lower()
468
+ search_in = params.get('search_in', 'all')
469
+
470
+ if not query:
471
+ raise ValueError("Missing required parameter: query")
472
+
473
+ results = []
474
+ for path, prompt_data in self.prompt_library.prompts.items():
475
+ match_type = None
476
+
477
+ # Search in name
478
+ if search_in in ['name', 'all']:
479
+ name = prompt_data.get('name', '').lower()
480
+ if query in name:
481
+ match_type = 'name'
482
+
483
+ # Search in content
484
+ if not match_type and search_in in ['content', 'all']:
485
+ content = prompt_data.get('content', '').lower()
486
+ if query in content:
487
+ match_type = 'content'
488
+
489
+ # Search in tags
490
+ if not match_type and search_in in ['tags', 'all']:
491
+ tags = prompt_data.get('tags', [])
492
+ if any(query in tag.lower() for tag in tags):
493
+ match_type = 'tags'
494
+
495
+ if match_type:
496
+ results.append({
497
+ 'name': prompt_data.get('name', path),
498
+ 'path': path,
499
+ 'folder': prompt_data.get('_folder', ''),
500
+ 'description': prompt_data.get('description', ''),
501
+ 'match_type': match_type,
502
+ 'tags': prompt_data.get('tags', [])
503
+ })
504
+
505
+ return {
506
+ 'count': len(results),
507
+ 'results': results
508
+ }
509
+
510
+ def _action_create_folder(self, params: Dict) -> Dict:
511
+ """
512
+ Create a new folder in the library.
513
+
514
+ Params:
515
+ path (required): Folder path (e.g., "Domain Expertise/Medical")
516
+
517
+ Returns:
518
+ {success: bool, path: str, message: str}
519
+ """
520
+ path = params.get('path')
521
+ if not path:
522
+ raise ValueError("Missing required parameter: path")
523
+
524
+ success = self.prompt_library.create_folder(path)
525
+
526
+ if success:
527
+ return {
528
+ 'success': True,
529
+ 'path': path,
530
+ 'message': f"Created folder: {path}"
531
+ }
532
+ else:
533
+ raise Exception("Failed to create folder")
534
+
535
+ def _action_toggle_favorite(self, params: Dict) -> Dict:
536
+ """
537
+ Toggle favorite status of a prompt.
538
+
539
+ Params:
540
+ path (required): Relative path to prompt
541
+
542
+ Returns:
543
+ {success: bool, path: str, favorite: bool, message: str}
544
+ """
545
+ path = params.get('path')
546
+ if not path:
547
+ raise ValueError("Missing required parameter: path")
548
+
549
+ success = self.prompt_library.toggle_favorite(path)
550
+
551
+ if success:
552
+ is_favorite = self.prompt_library.prompts[path].get('favorite', False)
553
+ return {
554
+ 'success': True,
555
+ 'path': path,
556
+ 'favorite': is_favorite,
557
+ 'message': f"{'Added to' if is_favorite else 'Removed from'} favorites"
558
+ }
559
+ else:
560
+ raise Exception("Failed to toggle favorite")
561
+
562
+ def _action_toggle_quick_run(self, params: Dict) -> Dict:
563
+ """
564
+ Toggle QuickMenu (legacy name: quick_run) status of a prompt.
565
+
566
+ Params:
567
+ path (required): Relative path to prompt
568
+
569
+ Returns:
570
+ {success: bool, path: str, quick_run: bool, message: str}
571
+ """
572
+ path = params.get('path')
573
+ if not path:
574
+ raise ValueError("Missing required parameter: path")
575
+
576
+ success = self.prompt_library.toggle_quick_run(path)
577
+
578
+ if success:
579
+ is_quick_run = self.prompt_library.prompts[path].get('quick_run', False)
580
+ return {
581
+ 'success': True,
582
+ 'path': path,
583
+ 'quick_run': is_quick_run,
584
+ 'message': f"{'Added to' if is_quick_run else 'Removed from'} QuickMenu"
585
+ }
586
+ else:
587
+ raise Exception("Failed to toggle quick run")
588
+
589
+ def _action_get_favorites(self, params: Dict) -> Dict:
590
+ """
591
+ Get list of favorite prompts.
592
+
593
+ Returns:
594
+ {count: int, favorites: [{name, path}]}
595
+ """
596
+ favorites = self.prompt_library.get_favorites()
597
+
598
+ return {
599
+ 'count': len(favorites),
600
+ 'favorites': [{'name': name, 'path': path} for path, name in favorites]
601
+ }
602
+
603
+ def _action_get_quick_run(self, params: Dict) -> Dict:
604
+ """
605
+ Get list of QuickMenu prompts (legacy name: quick_run).
606
+
607
+ Returns:
608
+ {count: int, prompts: [{name, path}]}
609
+ """
610
+ quick_run = self.prompt_library.get_quick_run_prompts()
611
+
612
+ return {
613
+ 'count': len(quick_run),
614
+ 'prompts': [{'name': name, 'path': path} for path, name in quick_run]
615
+ }
616
+
617
+ def _action_get_folder_structure(self, params: Dict) -> Dict:
618
+ """
619
+ Get complete folder structure of the library.
620
+
621
+ Returns:
622
+ Nested dictionary representing folder structure
623
+ """
624
+ structure = self.prompt_library.get_folder_structure()
625
+
626
+ return structure
627
+
628
+ def _action_get_segment_count(self, params: Dict) -> Dict:
629
+ """
630
+ Get the total number of segments in the current project.
631
+
632
+ Returns:
633
+ {total_segments: int, translated: int, untranslated: int}
634
+ """
635
+ if not self.parent_app:
636
+ raise ValueError("Parent app not available for segment access")
637
+
638
+ if not hasattr(self.parent_app, 'current_project') or not self.parent_app.current_project:
639
+ raise ValueError("No project currently loaded")
640
+
641
+ project = self.parent_app.current_project
642
+ if not hasattr(project, 'segments') or not project.segments:
643
+ return {
644
+ 'total_segments': 0,
645
+ 'translated': 0,
646
+ 'untranslated': 0
647
+ }
648
+
649
+ segments = project.segments
650
+ total = len(segments)
651
+ translated = sum(1 for seg in segments if seg.target and seg.target.strip())
652
+ untranslated = total - translated
653
+
654
+ return {
655
+ 'total_segments': total,
656
+ 'translated': translated,
657
+ 'untranslated': untranslated
658
+ }
659
+
660
+ def _action_get_segment_info(self, params: Dict) -> Dict:
661
+ """
662
+ Get detailed information about specific segment(s).
663
+
664
+ Params:
665
+ segment_id (optional): Specific segment ID to retrieve
666
+ segment_ids (optional): List of segment IDs to retrieve
667
+ start_id (optional): Start of range (inclusive)
668
+ end_id (optional): End of range (inclusive)
669
+
670
+ Returns:
671
+ {segments: [{id, source, target, status, type, notes, ...}]}
672
+ """
673
+ if not self.parent_app:
674
+ raise ValueError("Parent app not available for segment access")
675
+
676
+ if not hasattr(self.parent_app, 'current_project') or not self.parent_app.current_project:
677
+ raise ValueError("No project currently loaded")
678
+
679
+ project = self.parent_app.current_project
680
+ if not hasattr(project, 'segments') or not project.segments:
681
+ return {'segments': []}
682
+
683
+ all_segments = project.segments
684
+
685
+ # Determine which segments to retrieve
686
+ segment_id = params.get('segment_id')
687
+ segment_ids = params.get('segment_ids')
688
+ start_id = params.get('start_id')
689
+ end_id = params.get('end_id')
690
+
691
+ target_segments = []
692
+
693
+ if segment_id is not None:
694
+ # Single segment by ID
695
+ for seg in all_segments:
696
+ if seg.id == segment_id:
697
+ target_segments.append(seg)
698
+ break
699
+ elif segment_ids:
700
+ # Multiple segments by IDs
701
+ id_set = set(segment_ids)
702
+ target_segments = [seg for seg in all_segments if seg.id in id_set]
703
+ elif start_id is not None or end_id is not None:
704
+ # Range of segments
705
+ start = start_id if start_id is not None else 1
706
+ end = end_id if end_id is not None else float('inf')
707
+ target_segments = [seg for seg in all_segments if start <= seg.id <= end]
708
+ else:
709
+ # No filter specified - return all segments (limited to first 50)
710
+ target_segments = all_segments[:50]
711
+
712
+ # Convert segments to dictionaries
713
+ segments_data = []
714
+ for seg in target_segments:
715
+ seg_dict = {
716
+ 'id': seg.id,
717
+ 'source': seg.source,
718
+ 'target': seg.target,
719
+ 'status': seg.status,
720
+ 'type': seg.type,
721
+ 'notes': seg.notes,
722
+ 'match_percent': seg.match_percent,
723
+ 'locked': seg.locked,
724
+ 'paragraph_id': seg.paragraph_id,
725
+ 'style': seg.style,
726
+ 'document_position': seg.document_position,
727
+ 'is_table_cell': seg.is_table_cell
728
+ }
729
+ segments_data.append(seg_dict)
730
+
731
+ return {
732
+ 'segments': segments_data,
733
+ 'count': len(segments_data)
734
+ }
735
+
736
+ def _action_activate_prompt(self, params: Dict) -> Dict:
737
+ """
738
+ Activate/attach a prompt to the current project.
739
+
740
+ Params:
741
+ path (required): Path to the prompt to activate
742
+ mode (optional): 'primary' or 'attach' (default: 'primary')
743
+
744
+ Returns:
745
+ {success: bool, message: str}
746
+ """
747
+ path = params.get('path')
748
+ mode = params.get('mode', 'primary')
749
+
750
+ if not path:
751
+ raise ValueError("Missing required parameter: path")
752
+
753
+ # Check if prompt exists
754
+ if path not in self.prompt_library.prompts:
755
+ raise ValueError(f"Prompt not found: {path}")
756
+
757
+ if mode == 'primary':
758
+ # Set as primary prompt
759
+ self.prompt_library.set_primary_prompt(path)
760
+ return {
761
+ 'success': True,
762
+ 'message': f"✓ Activated as primary prompt: {self.prompt_library.prompts[path].get('name', path)}"
763
+ }
764
+ elif mode == 'attach':
765
+ # Attach as additional prompt
766
+ if path not in self.prompt_library.attached_prompts:
767
+ self.prompt_library.attached_prompts.append(path)
768
+ self.prompt_library.save_active_state()
769
+ return {
770
+ 'success': True,
771
+ 'message': f"✓ Attached prompt: {self.prompt_library.prompts[path].get('name', path)}"
772
+ }
773
+ else:
774
+ return {
775
+ 'success': False,
776
+ 'message': f"Prompt already attached: {path}"
777
+ }
778
+ else:
779
+ raise ValueError(f"Invalid mode: {mode}. Use 'primary' or 'attach'")
780
+
781
+ def get_system_prompt_addition(self) -> str:
782
+ """
783
+ Get text to add to AI system prompt to enable action usage.
784
+
785
+ Returns:
786
+ String to append to AI system prompt
787
+ """
788
+ return """
789
+
790
+ ## 🔧 Available Actions
791
+
792
+ You can interact with the Supervertaler Prompt Library using structured actions.
793
+ When you want to perform an action, use this format:
794
+
795
+ ACTION:function_name
796
+ PARAMS:{"param1": "value1", "param2": "value2"}
797
+
798
+ Available actions:
799
+
800
+ ### 1. list_prompts
801
+ List all prompts, optionally filtered by folder.
802
+ PARAMS: {"folder": "Domain Expertise" (optional), "include_content": false (optional)}
803
+
804
+ ### 2. get_prompt
805
+ Get full details of a specific prompt.
806
+ PARAMS: {"path": "Domain Expertise/Medical.md"}
807
+
808
+ ### 3. create_prompt
809
+ Create a new prompt.
810
+ PARAMS: {
811
+ "name": "Medical Translator",
812
+ "content": "You are an expert medical translator...",
813
+ "folder": "Domain Expertise" (optional),
814
+ "description": "Expert medical translation" (optional),
815
+ "tags": ["medical", "technical"] (optional)
816
+ }
817
+
818
+ ### 4. update_prompt
819
+ Update an existing prompt.
820
+ PARAMS: {
821
+ "path": "Domain Expertise/Medical.md",
822
+ "content": "Updated content..." (optional),
823
+ "name": "New name" (optional),
824
+ "description": "New description" (optional)
825
+ }
826
+
827
+ ### 5. delete_prompt
828
+ Delete a prompt.
829
+ PARAMS: {"path": "Domain Expertise/Medical.md"}
830
+
831
+ ### 6. search_prompts
832
+ Search prompts by query.
833
+ PARAMS: {
834
+ "query": "medical",
835
+ "search_in": "all" (options: name, content, tags, all)
836
+ }
837
+
838
+ ### 7. create_folder
839
+ Create a new folder.
840
+ PARAMS: {"path": "Domain Expertise/Medical"}
841
+
842
+ ### 8. toggle_favorite
843
+ Toggle favorite status.
844
+ PARAMS: {"path": "Domain Expertise/Medical.md"}
845
+
846
+ ### 9. toggle_quick_run
847
+ Toggle QuickMenu status (legacy name: quick_run).
848
+ PARAMS: {"path": "Domain Expertise/Medical.md"}
849
+
850
+ ### 10. get_favorites
851
+ Get list of favorite prompts.
852
+ PARAMS: {}
853
+
854
+ ### 11. get_quick_run
855
+ Get list of QuickMenu prompts (legacy name: quick_run).
856
+ PARAMS: {}
857
+
858
+ ### 12. get_folder_structure
859
+ Get complete folder structure.
860
+ PARAMS: {}
861
+
862
+ ### 13. get_segment_count
863
+ Get total segment count and translation progress.
864
+ PARAMS: {}
865
+
866
+ ### 14. get_segment_info
867
+ Get detailed information about specific segment(s).
868
+ PARAMS: {
869
+ "segment_id": 5 (single segment) OR
870
+ "segment_ids": [1, 5, 10] (multiple segments) OR
871
+ "start_id": 1, "end_id": 10 (range of segments)
872
+ }
873
+
874
+ ### 15. activate_prompt
875
+ Activate/attach a prompt to the current project.
876
+ PARAMS: {
877
+ "path": "Domain Expertise/Medical.md",
878
+ "mode": "primary" (or "attach")
879
+ }
880
+
881
+ **Important:**
882
+ - Actions are executed automatically when you include them in your response
883
+ - You'll see the results immediately
884
+ - You can include multiple actions in one response
885
+ - Always use valid JSON for PARAMS
886
+ - Wrap your normal conversational response around the actions
887
+ """
888
+
889
+ def format_action_results(self, action_results: List[Dict]) -> str:
890
+ """
891
+ Format action results for display in chat.
892
+
893
+ Args:
894
+ action_results: List of action result dictionaries
895
+
896
+ Returns:
897
+ Formatted string for display
898
+ """
899
+ if not action_results:
900
+ return ""
901
+
902
+ output = "\n\n**Action Results:**\n"
903
+
904
+ for result in action_results:
905
+ action_name = result['action']
906
+
907
+ if result['success']:
908
+ output += f"\n✓ **{action_name}**: {result['result'].get('message', 'Success')}\n"
909
+
910
+ # Add additional details based on action type
911
+ if action_name == 'list_prompts':
912
+ count = result['result']['count']
913
+ output += f" Found {count} prompts\n"
914
+
915
+ elif action_name == 'search_prompts':
916
+ count = result['result']['count']
917
+ output += f" Found {count} matching prompts\n"
918
+ for match in result['result']['results'][:5]: # Show first 5
919
+ output += f" - {match['name']} ({match['folder']})\n"
920
+
921
+ elif action_name == 'create_prompt':
922
+ output += f" Path: {result['result']['path']}\n"
923
+
924
+ elif action_name == 'activate_prompt':
925
+ # Just use the message from the result
926
+ pass
927
+
928
+ elif action_name == 'get_segment_count':
929
+ total = result['result']['total_segments']
930
+ translated = result['result']['translated']
931
+ untranslated = result['result']['untranslated']
932
+ output += f" Total segments: {total}\n"
933
+ output += f" Translated: {translated}\n"
934
+ output += f" Untranslated: {untranslated}\n"
935
+
936
+ elif action_name == 'get_segment_info':
937
+ segments = result['result']['segments']
938
+ count = result['result']['count']
939
+ output += f" Retrieved {count} segment(s)\n\n"
940
+ for seg in segments:
941
+ output += f" **Segment {seg['id']}:**\n"
942
+ # Escape HTML entities for display
943
+ # Order matters: & must be first to avoid double-escaping
944
+ source = (seg['source']
945
+ .replace('&', '&amp;')
946
+ .replace('<', '&lt;')
947
+ .replace('>', '&gt;')
948
+ .replace('"', '&quot;'))
949
+ output += f" Source: `{source}`\n"
950
+ if seg['target']:
951
+ target = (seg['target']
952
+ .replace('&', '&amp;')
953
+ .replace('<', '&lt;')
954
+ .replace('>', '&gt;')
955
+ .replace('"', '&quot;'))
956
+ output += f" Target: `{target}`\n"
957
+ output += f" Status: {seg['status']}\n"
958
+ if seg['notes']:
959
+ output += f" Notes: {seg['notes']}\n"
960
+ output += "\n"
961
+ else:
962
+ output += f"\n✗ **{action_name}**: {result['error']}\n"
963
+
964
+ return output