dtSpark 1.1.0a2__py3-none-any.whl → 1.1.0a6__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 (56) hide show
  1. dtSpark/_version.txt +1 -1
  2. dtSpark/aws/authentication.py +1 -1
  3. dtSpark/aws/bedrock.py +238 -239
  4. dtSpark/aws/costs.py +9 -5
  5. dtSpark/aws/pricing.py +25 -21
  6. dtSpark/cli_interface.py +69 -62
  7. dtSpark/conversation_manager.py +54 -47
  8. dtSpark/core/application.py +151 -111
  9. dtSpark/core/context_compaction.py +241 -226
  10. dtSpark/daemon/__init__.py +36 -22
  11. dtSpark/daemon/action_monitor.py +46 -17
  12. dtSpark/daemon/daemon_app.py +126 -104
  13. dtSpark/daemon/daemon_manager.py +59 -23
  14. dtSpark/daemon/pid_file.py +3 -2
  15. dtSpark/database/autonomous_actions.py +3 -0
  16. dtSpark/database/credential_prompt.py +52 -54
  17. dtSpark/files/manager.py +6 -12
  18. dtSpark/limits/__init__.py +1 -1
  19. dtSpark/limits/tokens.py +2 -2
  20. dtSpark/llm/anthropic_direct.py +246 -141
  21. dtSpark/llm/ollama.py +3 -1
  22. dtSpark/mcp_integration/manager.py +4 -4
  23. dtSpark/mcp_integration/tool_selector.py +83 -77
  24. dtSpark/resources/config.yaml.template +10 -0
  25. dtSpark/safety/patterns.py +45 -46
  26. dtSpark/safety/prompt_inspector.py +8 -1
  27. dtSpark/scheduler/creation_tools.py +273 -181
  28. dtSpark/scheduler/executor.py +503 -221
  29. dtSpark/tools/builtin.py +70 -53
  30. dtSpark/web/endpoints/autonomous_actions.py +12 -9
  31. dtSpark/web/endpoints/chat.py +18 -6
  32. dtSpark/web/endpoints/conversations.py +57 -17
  33. dtSpark/web/endpoints/main_menu.py +132 -105
  34. dtSpark/web/endpoints/streaming.py +2 -2
  35. dtSpark/web/server.py +65 -5
  36. dtSpark/web/ssl_utils.py +3 -3
  37. dtSpark/web/static/css/dark-theme.css +8 -29
  38. dtSpark/web/static/js/actions.js +2 -1
  39. dtSpark/web/static/js/chat.js +6 -8
  40. dtSpark/web/static/js/main.js +8 -8
  41. dtSpark/web/static/js/sse-client.js +130 -122
  42. dtSpark/web/templates/actions.html +5 -5
  43. dtSpark/web/templates/base.html +13 -0
  44. dtSpark/web/templates/chat.html +52 -50
  45. dtSpark/web/templates/conversations.html +50 -22
  46. dtSpark/web/templates/goodbye.html +2 -2
  47. dtSpark/web/templates/main_menu.html +17 -17
  48. dtSpark/web/templates/new_conversation.html +51 -20
  49. dtSpark/web/web_interface.py +2 -2
  50. {dtspark-1.1.0a2.dist-info → dtspark-1.1.0a6.dist-info}/METADATA +9 -2
  51. dtspark-1.1.0a6.dist-info/RECORD +96 -0
  52. dtspark-1.1.0a2.dist-info/RECORD +0 -96
  53. {dtspark-1.1.0a2.dist-info → dtspark-1.1.0a6.dist-info}/WHEEL +0 -0
  54. {dtspark-1.1.0a2.dist-info → dtspark-1.1.0a6.dist-info}/entry_points.txt +0 -0
  55. {dtspark-1.1.0a2.dist-info → dtspark-1.1.0a6.dist-info}/licenses/LICENSE +0 -0
  56. {dtspark-1.1.0a2.dist-info → dtspark-1.1.0a6.dist-info}/top_level.txt +0 -0
@@ -84,6 +84,16 @@ def copy_to_clipboard(text: str) -> bool:
84
84
  logging.error(f"Failed to copy to clipboard: {e}")
85
85
  return False
86
86
 
87
+ # Common string constants (SonarCloud S1192)
88
+ _SETTING_BEDROCK_COST_TRACKING = 'llm_providers.aws_bedrock.cost_tracking.enabled'
89
+ _SETTING_COST_TRACKING = 'aws.cost_tracking.enabled'
90
+ _SETTING_OLLAMA_ENABLED = 'llm_providers.ollama.enabled'
91
+ _DEFAULT_RUNNING_DIR = "./running"
92
+ _PROMPT_ACCESS_MODE = "Select access mode"
93
+ _MSG_NO_MODELS = "No models available"
94
+ _MSG_MANAGED_CONVERSATION = "This conversation is managed by configuration"
95
+ _MSG_DELETE_CANCELLED = "Delete operation cancelled"
96
+
87
97
  class AWSBedrockCLI(AbstractApp):
88
98
  def __init__(self):
89
99
  super().__init__(short_name=agent_type(), full_name=full_name(), version=version(),
@@ -371,10 +381,11 @@ class AWSBedrockCLI(AbstractApp):
371
381
  aws_session_token = self.settings.get('aws.session_token', None)
372
382
 
373
383
  # Configure CLI cost tracking display
374
- cost_tracking_enabled = self._get_nested_setting('llm_providers.aws_bedrock.cost_tracking.enabled', None)
384
+ cost_tracking_enabled = self._get_nested_setting(_SETTING_BEDROCK_COST_TRACKING, None)
375
385
  if cost_tracking_enabled is None:
376
- cost_tracking_enabled = self.settings.get('aws.cost_tracking.enabled', False)
386
+ cost_tracking_enabled = self.settings.get(_SETTING_COST_TRACKING, False)
377
387
  self.cli.cost_tracking_enabled = cost_tracking_enabled
388
+ self.cli.actions_enabled = self._get_nested_setting('autonomous_actions.enabled', False)
378
389
 
379
390
  progress.update(task_config, advance=100)
380
391
 
@@ -391,7 +402,7 @@ class AWSBedrockCLI(AbstractApp):
391
402
  # Handle missing or various value types
392
403
  if aws_enabled_raw is None:
393
404
  # Setting not found via any method - check if other providers are enabled
394
- ollama_check = self._get_nested_setting('llm_providers.ollama.enabled', False)
405
+ ollama_check = self._get_nested_setting(_SETTING_OLLAMA_ENABLED, False)
395
406
  anthropic_check = self._get_nested_setting('llm_providers.anthropic.enabled', False)
396
407
  if ollama_check or anthropic_check:
397
408
  # Other providers configured, don't default to AWS
@@ -432,7 +443,7 @@ class AWSBedrockCLI(AbstractApp):
432
443
  self.cli.print_warning("Failed to authenticate with AWS Bedrock")
433
444
 
434
445
  # Check if Ollama is available as fallback
435
- ollama_enabled = self._get_nested_setting('llm_providers.ollama.enabled', False)
446
+ ollama_enabled = self._get_nested_setting(_SETTING_OLLAMA_ENABLED, False)
436
447
  if not ollama_enabled:
437
448
  self.cli.print_error("AWS authentication required (Ollama not configured)")
438
449
  self.cli.print_info(f"Run: aws sso login --profile {aws_profile}")
@@ -457,7 +468,7 @@ class AWSBedrockCLI(AbstractApp):
457
468
  progress.update(task_llm, advance=20, description="[cyan]Checking Ollama...")
458
469
 
459
470
  # Check Ollama configuration
460
- ollama_enabled = self._get_nested_setting('llm_providers.ollama.enabled', False)
471
+ ollama_enabled = self._get_nested_setting(_SETTING_OLLAMA_ENABLED, False)
461
472
  if ollama_enabled:
462
473
  try:
463
474
  ollama_url = self._get_nested_setting(
@@ -529,9 +540,9 @@ class AWSBedrockCLI(AbstractApp):
529
540
 
530
541
  # Task 3.5: Retrieve Bedrock cost information (silently, display later)
531
542
  # Only if cost tracking is enabled in configuration
532
- cost_tracking_enabled = self._get_nested_setting('llm_providers.aws_bedrock.cost_tracking.enabled', None)
543
+ cost_tracking_enabled = self._get_nested_setting(_SETTING_BEDROCK_COST_TRACKING, None)
533
544
  if cost_tracking_enabled is None:
534
- cost_tracking_enabled = self.settings.get('aws.cost_tracking.enabled', False)
545
+ cost_tracking_enabled = self.settings.get(_SETTING_COST_TRACKING, False)
535
546
  if cost_tracking_enabled and self.authenticator:
536
547
  task_costs = progress.add_task("[cyan]Retrieving usage costs...", total=100)
537
548
  self.bedrock_costs = None
@@ -796,77 +807,87 @@ class AWSBedrockCLI(AbstractApp):
796
807
  )
797
808
  progress.update(task_conv, advance=100)
798
809
 
799
- # Task 7: Initialise autonomous action scheduler
810
+ # Task 7: Initialise autonomous action scheduler (if enabled)
811
+ self.actions_enabled = self._get_nested_setting('autonomous_actions.enabled', False)
800
812
  task_scheduler = progress.add_task("[cyan]Initialising action scheduler...", total=100)
801
- try:
802
- from dtSpark.scheduler import (
803
- ActionSchedulerManager,
804
- ActionExecutionQueue,
805
- ActionExecutor
806
- )
807
813
 
808
- # Get database path for scheduler job store
809
- db_path = self.database.db_path or ':memory:'
814
+ if not self.actions_enabled:
815
+ logging.info("Autonomous actions are disabled via configuration")
816
+ self.action_scheduler = None
817
+ self.execution_queue = None
818
+ self.action_executor = None
819
+ self.daemon_is_running = False
820
+ progress.update(task_scheduler, advance=100)
821
+ else:
822
+ try:
823
+ from dtSpark.scheduler import (
824
+ ActionSchedulerManager,
825
+ ActionExecutionQueue,
826
+ ActionExecutor
827
+ )
810
828
 
811
- # Create executor with LLM manager and optional MCP manager
812
- get_tools_func = None
813
- if self.mcp_manager:
814
- def get_tools_func():
815
- import asyncio
816
- loop = getattr(self.mcp_manager, '_initialization_loop', None)
817
- if loop and not loop.is_closed():
818
- return loop.run_until_complete(self.mcp_manager.list_all_tools())
819
- return []
820
-
821
- self.action_executor = ActionExecutor(
822
- database=self.database,
823
- llm_manager=self.llm_manager,
824
- mcp_manager=self.mcp_manager,
825
- get_tools_func=get_tools_func,
826
- config=config_for_manager
827
- )
829
+ # Get database path for scheduler job store
830
+ db_path = self.database.db_path or ':memory:'
828
831
 
829
- # Create execution queue
830
- self.execution_queue = ActionExecutionQueue(
831
- executor_func=self.action_executor.execute
832
- )
832
+ # Create executor with LLM manager and optional MCP manager
833
+ get_tools_func = None
834
+ if self.mcp_manager:
835
+ def get_tools_func():
836
+ import asyncio
837
+ loop = getattr(self.mcp_manager, '_initialization_loop', None)
838
+ if loop and not loop.is_closed():
839
+ return loop.run_until_complete(self.mcp_manager.list_all_tools())
840
+ return []
841
+
842
+ self.action_executor = ActionExecutor(
843
+ database=self.database,
844
+ llm_manager=self.llm_manager,
845
+ mcp_manager=self.mcp_manager,
846
+ get_tools_func=get_tools_func,
847
+ config=config_for_manager
848
+ )
833
849
 
834
- # Create scheduler manager
835
- self.action_scheduler = ActionSchedulerManager(
836
- db_path=db_path,
837
- execution_callback=lambda action_id, user_guid: self.execution_queue.enqueue(
838
- action_id, user_guid, is_manual=False
850
+ # Create execution queue
851
+ self.execution_queue = ActionExecutionQueue(
852
+ executor_func=self.action_executor.execute
839
853
  )
840
- )
841
854
 
842
- # Check if daemon is running (for warning display later)
843
- self.daemon_is_running = self._check_daemon_running()
855
+ # Create scheduler manager
856
+ self.action_scheduler = ActionSchedulerManager(
857
+ db_path=db_path,
858
+ execution_callback=lambda action_id, user_guid: self.execution_queue.enqueue(
859
+ action_id, user_guid, is_manual=False
860
+ )
861
+ )
844
862
 
845
- # Initialise execution components (for manual "Run Now" from UI)
846
- # Note: Scheduled execution is ONLY handled by the daemon process
847
- self.action_scheduler.initialise()
848
- self.execution_queue.start()
863
+ # Check if daemon is running (for warning display later)
864
+ self.daemon_is_running = self._check_daemon_running()
849
865
 
850
- # UI never starts the scheduler - daemon handles all scheduled execution
851
- if self.daemon_is_running:
852
- logging.info("Daemon is running - scheduled actions will be executed by daemon")
853
- else:
854
- logging.warning("Daemon is not running - scheduled actions will NOT execute until daemon is started")
866
+ # Initialise execution components (for manual "Run Now" from UI)
867
+ # Note: Scheduled execution is ONLY handled by the daemon process
868
+ self.action_scheduler.initialise()
869
+ self.execution_queue.start()
855
870
 
856
- progress.update(task_scheduler, advance=100)
871
+ # UI never starts the scheduler - daemon handles all scheduled execution
872
+ if self.daemon_is_running:
873
+ logging.info("Daemon is running - scheduled actions will be executed by daemon")
874
+ else:
875
+ logging.warning("Daemon is not running - scheduled actions will NOT execute until daemon is started")
857
876
 
858
- except ImportError as e:
859
- logging.warning(f"Action scheduler not available (APScheduler not installed): {e}")
860
- self.action_scheduler = None
861
- self.execution_queue = None
862
- self.action_executor = None
863
- progress.update(task_scheduler, advance=100)
864
- except Exception as e:
865
- logging.error(f"Failed to initialise action scheduler: {e}")
866
- self.action_scheduler = None
867
- self.execution_queue = None
868
- self.action_executor = None
869
- progress.update(task_scheduler, advance=100)
877
+ progress.update(task_scheduler, advance=100)
878
+
879
+ except ImportError as e:
880
+ logging.warning(f"Action scheduler not available (APScheduler not installed): {e}")
881
+ self.action_scheduler = None
882
+ self.execution_queue = None
883
+ self.action_executor = None
884
+ progress.update(task_scheduler, advance=100)
885
+ except Exception as e:
886
+ logging.error(f"Failed to initialise action scheduler: {e}")
887
+ self.action_scheduler = None
888
+ self.execution_queue = None
889
+ self.action_executor = None
890
+ progress.update(task_scheduler, advance=100)
870
891
 
871
892
  # Display application info first (user identification)
872
893
  self.cli.display_application_info(self.user_guid)
@@ -881,8 +902,8 @@ class AWSBedrockCLI(AbstractApp):
881
902
  if mcp_enabled and self.mcp_manager:
882
903
  self.cli.display_mcp_status(self.mcp_manager)
883
904
 
884
- # Display daemon status warning if daemon is not running
885
- if hasattr(self, 'daemon_is_running') and not self.daemon_is_running:
905
+ # Display daemon status warning if daemon is not running (only when actions are enabled)
906
+ if self.actions_enabled and hasattr(self, 'daemon_is_running') and not self.daemon_is_running:
886
907
  # Check if there are any scheduled actions
887
908
  try:
888
909
  actions = self.database.get_all_actions(include_disabled=False)
@@ -1016,8 +1037,9 @@ class AWSBedrockCLI(AbstractApp):
1016
1037
  logging.debug("Predefined conversations not enabled in config")
1017
1038
  return
1018
1039
 
1019
- # Get the mandatory model setting
1040
+ # Get the mandatory model and provider settings
1020
1041
  mandatory_model = self._get_nested_setting('llm_providers.mandatory_model', None)
1042
+ mandatory_provider = self._get_nested_setting('llm_providers.mandatory_provider', None)
1021
1043
 
1022
1044
  # Get list of predefined conversations
1023
1045
  predefined_convs = self.settings.get('predefined_conversations.conversations', [])
@@ -1135,27 +1157,45 @@ class AWSBedrockCLI(AbstractApp):
1135
1157
  if not source or not source.strip():
1136
1158
  return source
1137
1159
 
1138
- # Try ResourceManager first
1139
- try:
1140
- resource_content = ResourceManager().load_resource(source)
1141
- if resource_content is not None:
1142
- logging.info(f"Loaded {description} via ResourceManager from: {source}")
1143
- return resource_content
1144
- except Exception as e:
1145
- logging.debug(f"ResourceManager could not load {description} from '{source}': {e}")
1160
+ import os
1146
1161
 
1147
- # Try direct file path
1148
- try:
1149
- import os
1150
- if os.path.isfile(source):
1151
- with open(source, 'r', encoding='utf-8') as f:
1152
- content = f.read()
1153
- logging.info(f"Loaded {description} from file path: {source}")
1154
- return content
1155
- except Exception as e:
1156
- logging.debug(f"Could not load {description} from file path '{source}': {e}")
1162
+ # Determine if source looks like a file path or resource name
1163
+ # (contains path separators, has a file extension, or doesn't contain spaces)
1164
+ looks_like_path = (
1165
+ os.sep in source
1166
+ or '/' in source
1167
+ or '\\' in source
1168
+ or (
1169
+ '.' in source
1170
+ and not source.strip().endswith('.')
1171
+ and ' ' not in source.strip()
1172
+ )
1173
+ )
1174
+
1175
+ if looks_like_path:
1176
+ # Try ResourceManager first (for package resources)
1177
+ try:
1178
+ resource_content = ResourceManager().load_resource(source)
1179
+ if resource_content is not None:
1180
+ logging.info(f"Loaded {description} via ResourceManager from: {source}")
1181
+ return resource_content
1182
+ except Exception as e:
1183
+ logging.debug(f"ResourceManager could not load {description} from '{source}': {e}")
1184
+
1185
+ # Try direct file path
1186
+ try:
1187
+ if os.path.isfile(source):
1188
+ with open(source, 'r', encoding='utf-8') as f:
1189
+ content = f.read()
1190
+ logging.info(f"Loaded {description} from file path: {source}")
1191
+ return content
1192
+ except Exception as e:
1193
+ logging.debug(f"Could not load {description} from file path '{source}': {e}")
1194
+
1195
+ # Path-like source but couldn't load - log warning and return as-is
1196
+ logging.warning(f"Could not load {description} from path '{source}', using as inline text")
1157
1197
 
1158
- # If both methods fail, assume it's inline text
1198
+ # Treat as inline text
1159
1199
  logging.debug(f"Using inline text for {description}")
1160
1200
  return source
1161
1201
 
@@ -1619,7 +1659,7 @@ class AWSBedrockCLI(AbstractApp):
1619
1659
  enable_filesystem_tools = Confirm.ask("Enable embedded filesystem tools?", default=False)
1620
1660
 
1621
1661
  # Default values
1622
- filesystem_allowed_path = "./running"
1662
+ filesystem_allowed_path = _DEFAULT_RUNNING_DIR
1623
1663
  filesystem_access_mode = "read_write"
1624
1664
 
1625
1665
  if enable_filesystem_tools:
@@ -1629,7 +1669,7 @@ class AWSBedrockCLI(AbstractApp):
1629
1669
 
1630
1670
  filesystem_allowed_path = Prompt.ask(
1631
1671
  "Allowed directory path (tools can only access files within this directory)",
1632
- default="./running"
1672
+ default=_DEFAULT_RUNNING_DIR
1633
1673
  )
1634
1674
 
1635
1675
  # Access mode
@@ -1642,7 +1682,7 @@ class AWSBedrockCLI(AbstractApp):
1642
1682
  cli.console.print(" [2] Read/Write - Full access (read + write files, create directories)")
1643
1683
  cli.console.print()
1644
1684
  access_mode_choice = Prompt.ask(
1645
- "Select access mode",
1685
+ _PROMPT_ACCESS_MODE,
1646
1686
  choices=["1", "2"],
1647
1687
  default="2"
1648
1688
  )
@@ -1655,7 +1695,7 @@ class AWSBedrockCLI(AbstractApp):
1655
1695
  enable_document_tools = Confirm.ask("Enable embedded document tools (MS Office & PDF)?", default=False)
1656
1696
 
1657
1697
  # Default values
1658
- document_allowed_path = "./running"
1698
+ document_allowed_path = _DEFAULT_RUNNING_DIR
1659
1699
  document_access_mode = "read"
1660
1700
  document_max_file_size = "50"
1661
1701
  document_max_pdf_pages = "100"
@@ -1670,7 +1710,7 @@ class AWSBedrockCLI(AbstractApp):
1670
1710
 
1671
1711
  document_allowed_path = Prompt.ask(
1672
1712
  "Allowed directory path for documents",
1673
- default="./running"
1713
+ default=_DEFAULT_RUNNING_DIR
1674
1714
  )
1675
1715
 
1676
1716
  # Access mode
@@ -1683,7 +1723,7 @@ class AWSBedrockCLI(AbstractApp):
1683
1723
  cli.console.print(" [2] Read/Write - Read and create documents")
1684
1724
  cli.console.print()
1685
1725
  doc_access_mode_choice = Prompt.ask(
1686
- "Select access mode",
1726
+ _PROMPT_ACCESS_MODE,
1687
1727
  choices=["1", "2"],
1688
1728
  default="1"
1689
1729
  )
@@ -1723,7 +1763,7 @@ class AWSBedrockCLI(AbstractApp):
1723
1763
  enable_archive_tools = Confirm.ask("Enable embedded archive tools (ZIP, TAR)?", default=False)
1724
1764
 
1725
1765
  # Default values
1726
- archive_allowed_path = "./running"
1766
+ archive_allowed_path = _DEFAULT_RUNNING_DIR
1727
1767
  archive_access_mode = "read"
1728
1768
  archive_max_file_size = "100"
1729
1769
  archive_max_files_to_list = "1000"
@@ -1735,7 +1775,7 @@ class AWSBedrockCLI(AbstractApp):
1735
1775
 
1736
1776
  archive_allowed_path = Prompt.ask(
1737
1777
  "Allowed directory path for archives",
1738
- default="./running"
1778
+ default=_DEFAULT_RUNNING_DIR
1739
1779
  )
1740
1780
 
1741
1781
  # Access mode
@@ -1748,7 +1788,7 @@ class AWSBedrockCLI(AbstractApp):
1748
1788
  cli.console.print(" [2] Read/Write - Read and extract archives to disk")
1749
1789
  cli.console.print()
1750
1790
  archive_access_mode_choice = Prompt.ask(
1751
- "Select access mode",
1791
+ _PROMPT_ACCESS_MODE,
1752
1792
  choices=["1", "2"],
1753
1793
  default="1"
1754
1794
  )
@@ -2192,14 +2232,14 @@ class AWSBedrockCLI(AbstractApp):
2192
2232
  if document_templates_path:
2193
2233
  escaped_templates_path = document_templates_path.replace('\\', '/')
2194
2234
  config_content = re.sub(
2195
- r'(templates_path:\s+)(null|[^\s#]+)',
2235
+ r'(templates_path:\s+)([^\s#]+)',
2196
2236
  f'\\g<1>{escaped_templates_path}',
2197
2237
  config_content
2198
2238
  )
2199
2239
  # Default author (if provided)
2200
2240
  if document_default_author:
2201
2241
  config_content = re.sub(
2202
- r'(default_author:\s+)(null|[^\s#]+)',
2242
+ r'(default_author:\s+)([^\s#]+)',
2203
2243
  f'\\g<1>{document_default_author}',
2204
2244
  config_content
2205
2245
  )
@@ -2358,9 +2398,9 @@ class AWSBedrockCLI(AbstractApp):
2358
2398
  def regather_and_display_costs(self):
2359
2399
  """Re-gather AWS Bedrock cost information and display it."""
2360
2400
  # Check if cost tracking is enabled (new path with legacy fallback)
2361
- cost_tracking_enabled = self._get_nested_setting('llm_providers.aws_bedrock.cost_tracking.enabled', None)
2401
+ cost_tracking_enabled = self._get_nested_setting(_SETTING_BEDROCK_COST_TRACKING, None)
2362
2402
  if cost_tracking_enabled is None:
2363
- cost_tracking_enabled = self.settings.get('aws.cost_tracking.enabled', False)
2403
+ cost_tracking_enabled = self.settings.get(_SETTING_COST_TRACKING, False)
2364
2404
  if not cost_tracking_enabled:
2365
2405
  self.cli.print_warning("Cost tracking is disabled in configuration")
2366
2406
  return
@@ -2729,7 +2769,7 @@ class AWSBedrockCLI(AbstractApp):
2729
2769
  # Step 1: Select model (will be used for both creation and execution)
2730
2770
  models = self.llm_manager.list_all_models()
2731
2771
  if not models:
2732
- self.cli.print_error("No models available")
2772
+ self.cli.print_error(_MSG_NO_MODELS)
2733
2773
  return
2734
2774
 
2735
2775
  self.cli.console.print("\n[bold cyan]Select Model[/bold cyan]")
@@ -2923,7 +2963,7 @@ class AWSBedrockCLI(AbstractApp):
2923
2963
  progress.update(task, advance=100)
2924
2964
 
2925
2965
  if not models:
2926
- self.cli.print_error("No models available")
2966
+ self.cli.print_error(_MSG_NO_MODELS)
2927
2967
  return None
2928
2968
 
2929
2969
  model_id = self.cli.display_models(models)
@@ -3204,7 +3244,7 @@ class AWSBedrockCLI(AbstractApp):
3204
3244
  # Check if conversation is predefined - if so, block file deletion
3205
3245
  if self.database.is_conversation_predefined(self.conversation_manager.current_conversation_id):
3206
3246
  self.cli.print_error("Cannot delete files from predefined conversations")
3207
- self.cli.print_info("This conversation is managed by configuration")
3247
+ self.cli.print_info(_MSG_MANAGED_CONVERSATION)
3208
3248
  self.cli.wait_for_enter()
3209
3249
  continue
3210
3250
 
@@ -3228,7 +3268,7 @@ class AWSBedrockCLI(AbstractApp):
3228
3268
  file_ids_input = self.cli.get_input("File IDs to delete").strip()
3229
3269
 
3230
3270
  if not file_ids_input:
3231
- self.cli.print_info("Delete operation cancelled")
3271
+ self.cli.print_info(_MSG_DELETE_CANCELLED)
3232
3272
  self.cli.wait_for_enter()
3233
3273
  continue
3234
3274
 
@@ -3248,7 +3288,7 @@ class AWSBedrockCLI(AbstractApp):
3248
3288
  else:
3249
3289
  self.cli.print_error("Failed to delete files")
3250
3290
  else:
3251
- self.cli.print_info("Delete operation cancelled")
3291
+ self.cli.print_info(_MSG_DELETE_CANCELLED)
3252
3292
  else:
3253
3293
  # Parse comma-separated IDs
3254
3294
  try:
@@ -3278,7 +3318,7 @@ class AWSBedrockCLI(AbstractApp):
3278
3318
  if failed_ids:
3279
3319
  self.cli.print_error(f"Failed to delete files with IDs: {', '.join(map(str, failed_ids))}")
3280
3320
  else:
3281
- self.cli.print_info("Delete operation cancelled")
3321
+ self.cli.print_info(_MSG_DELETE_CANCELLED)
3282
3322
 
3283
3323
  except ValueError:
3284
3324
  self.cli.print_error("Invalid file IDs. Please enter comma-separated numbers or 'all'")
@@ -3290,7 +3330,7 @@ class AWSBedrockCLI(AbstractApp):
3290
3330
  # Check if conversation is predefined - if so, block model changes
3291
3331
  if self.database.is_conversation_predefined(self.conversation_manager.current_conversation_id):
3292
3332
  self.cli.print_error("Cannot change model for predefined conversations")
3293
- self.cli.print_info("This conversation is managed by configuration")
3333
+ self.cli.print_info(_MSG_MANAGED_CONVERSATION)
3294
3334
  self.cli.wait_for_enter()
3295
3335
  continue
3296
3336
 
@@ -3308,7 +3348,7 @@ class AWSBedrockCLI(AbstractApp):
3308
3348
  progress.update(task, advance=100)
3309
3349
 
3310
3350
  if not models:
3311
- self.cli.print_error("No models available")
3351
+ self.cli.print_error(_MSG_NO_MODELS)
3312
3352
  self.cli.wait_for_enter()
3313
3353
  continue
3314
3354
 
@@ -3355,7 +3395,7 @@ class AWSBedrockCLI(AbstractApp):
3355
3395
  # Check if conversation is predefined - if so, block instruction changes
3356
3396
  if self.database.is_conversation_predefined(self.conversation_manager.current_conversation_id):
3357
3397
  self.cli.print_error("Cannot change instructions for predefined conversations")
3358
- self.cli.print_info("This conversation is managed by configuration")
3398
+ self.cli.print_info(_MSG_MANAGED_CONVERSATION)
3359
3399
  self.cli.wait_for_enter()
3360
3400
  continue
3361
3401