ara-cli 0.1.9.96__py3-none-any.whl → 0.1.10.0__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.
ara_cli/chat.py CHANGED
@@ -1,29 +1,55 @@
1
1
  import os
2
+ import errno
2
3
  import argparse
3
4
  import cmd2
4
- from . import error_handler
5
+
5
6
  from ara_cli.prompt_handler import send_prompt
6
7
 
8
+ from . import error_handler
9
+ from ara_cli.error_handler import AraError, AraConfigurationError
10
+
7
11
  from ara_cli.file_loaders.document_file_loader import DocumentFileLoader
8
12
  from ara_cli.file_loaders.binary_file_loader import BinaryFileLoader
9
13
  from ara_cli.file_loaders.text_file_loader import TextFileLoader
10
14
 
11
15
 
12
16
  extract_parser = argparse.ArgumentParser()
13
- extract_parser.add_argument('-f', '--force', action='store_true', help='Force extraction')
14
- extract_parser.add_argument('-w','--write', action='store_true', help='Overwrite existing files without using LLM for merging.')
17
+ extract_parser.add_argument(
18
+ "-f", "--force", action="store_true", help="Force extraction"
19
+ )
20
+ extract_parser.add_argument(
21
+ "-w",
22
+ "--write",
23
+ action="store_true",
24
+ help="Overwrite existing files without using LLM for merging.",
25
+ )
15
26
 
16
27
  load_parser = argparse.ArgumentParser()
17
- load_parser.add_argument('file_name', nargs='?', default='', help='File to load')
18
- load_parser.add_argument('--load-images', action='store_true', help='Extract and describe images from documents')
28
+ load_parser.add_argument("file_name", nargs="?", default="", help="File to load")
29
+ load_parser.add_argument(
30
+ "--load-images",
31
+ action="store_true",
32
+ help="Extract and describe images from documents",
33
+ )
19
34
 
20
35
  extract_parser = argparse.ArgumentParser()
21
- extract_parser.add_argument('-f', '--force', action='store_true', help='Force extraction')
22
- extract_parser.add_argument('-w','--write', action='store_true', help='Overwrite existing files without using LLM for merging.')
36
+ extract_parser.add_argument(
37
+ "-f", "--force", action="store_true", help="Force extraction"
38
+ )
39
+ extract_parser.add_argument(
40
+ "-w",
41
+ "--write",
42
+ action="store_true",
43
+ help="Overwrite existing files without using LLM for merging.",
44
+ )
23
45
 
24
46
  load_parser = argparse.ArgumentParser()
25
- load_parser.add_argument('file_name', nargs='?', default='', help='File to load')
26
- load_parser.add_argument('--load-images', action='store_true', help='Extract and describe images from documents')
47
+ load_parser.add_argument("file_name", nargs="?", default="", help="File to load")
48
+ load_parser.add_argument(
49
+ "--load-images",
50
+ action="store_true",
51
+ help="Extract and describe images from documents",
52
+ )
27
53
 
28
54
 
29
55
  class Chat(cmd2.Cmd):
@@ -53,10 +79,10 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
53
79
  ROLE_RESPONSE = "ara response"
54
80
 
55
81
  BINARY_TYPE_MAPPING = {
56
- ".png": "image/png",
57
- ".jpg": "image/jpeg",
58
- ".jpeg": "image/jpeg",
59
- }
82
+ ".png": "image/png",
83
+ ".jpg": "image/jpeg",
84
+ ".jpeg": "image/jpeg",
85
+ }
60
86
 
61
87
  DOCUMENT_TYPE_EXTENSIONS = [".docx", ".doc", ".odt", ".pdf"]
62
88
 
@@ -64,25 +90,29 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
64
90
  self,
65
91
  chat_name: str,
66
92
  reset: bool | None = None,
67
- enable_commands: list[str] | None = None
93
+ enable_commands: list[str] | None = None,
68
94
  ):
95
+ from ara_cli.template_loader import TemplateLoader
69
96
  shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
70
97
  if enable_commands:
71
98
  enable_commands.append("quit") # always allow quitting
72
99
  enable_commands.append("eof") # always allow quitting with ctrl-D
73
100
  enable_commands.append("help") # always allow help
74
101
 
75
- shortcuts = {key: value for key, value in shortcuts.items() if value in enable_commands}
102
+ shortcuts = {
103
+ key: value
104
+ for key, value in shortcuts.items()
105
+ if value in enable_commands
106
+ }
76
107
 
77
- super().__init__(
78
- allow_cli_args=False,
79
- shortcuts=shortcuts
80
- )
108
+ super().__init__(allow_cli_args=False, shortcuts=shortcuts)
81
109
  self.create_default_aliases()
82
110
 
83
111
  if enable_commands:
84
112
  all_commands = self.get_all_commands()
85
- commands_to_disable = [command for command in all_commands if command not in enable_commands]
113
+ commands_to_disable = [
114
+ command for command in all_commands if command not in enable_commands
115
+ ]
86
116
  self.disable_commands(commands_to_disable)
87
117
 
88
118
  self.prompt = "ara> "
@@ -94,12 +124,15 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
94
124
  self.chat_history = []
95
125
  self.message_buffer = []
96
126
  self.config = self._retrieve_ara_config()
127
+ self.template_loader = TemplateLoader(chat_instance=self)
97
128
 
98
129
  def disable_commands(self, commands: list[str]):
99
130
  for command in commands:
100
- setattr(self, f'do_{command}', self.default)
131
+ setattr(self, f"do_{command}", self.default)
101
132
  self.hidden_commands.append(command)
102
- aliases_to_remove = [alias for alias, cmd in self.aliases.items() if cmd in commands]
133
+ aliases_to_remove = [
134
+ alias for alias, cmd in self.aliases.items() if cmd in commands
135
+ ]
103
136
  for alias in aliases_to_remove:
104
137
  del self.aliases[alias]
105
138
 
@@ -134,8 +167,10 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
134
167
  chat_file_short = os.path.split(chat_file)[-1]
135
168
 
136
169
  if reset is None:
137
- user_input = input(f"{chat_file_short} already exists. Do you want to reset the chat? (y/N): ")
138
- if user_input.lower() == 'y':
170
+ user_input = input(
171
+ f"{chat_file_short} already exists. Do you want to reset the chat? (y/N): "
172
+ )
173
+ if user_input.lower() == "y":
139
174
  self.create_empty_chat_file(chat_file)
140
175
  if reset:
141
176
  self.create_empty_chat_file(chat_file)
@@ -168,10 +203,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
168
203
  def get_last_role_marker(lines):
169
204
  if not lines:
170
205
  return
171
- role_markers = [
172
- f"# {Chat.ROLE_PROMPT}:",
173
- f"# {Chat.ROLE_RESPONSE}"
174
- ]
206
+ role_markers = [f"# {Chat.ROLE_PROMPT}:", f"# {Chat.ROLE_RESPONSE}"]
175
207
  for line in reversed(lines):
176
208
  stripped_line = line.strip()
177
209
  if stripped_line.startswith(tuple(role_markers)):
@@ -179,7 +211,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
179
211
  return None
180
212
 
181
213
  def start_non_interactive(self):
182
- with open(self.chat_name, 'r', encoding='utf-8') as file:
214
+ with open(self.chat_name, "r", encoding="utf-8") as file:
183
215
  content = file.read()
184
216
  print(content)
185
217
 
@@ -214,7 +246,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
214
246
  text_content = []
215
247
  image_data_list = []
216
248
 
217
- image_pattern = re.compile(r'\((data:image/[^;]+;base64,.*?)\)')
249
+ image_pattern = re.compile(r"\((data:image/[^;]+;base64,.*?)\)")
218
250
 
219
251
  for line in message.splitlines():
220
252
  match = image_pattern.search(line)
@@ -224,13 +256,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
224
256
  else:
225
257
  text_content.append(line)
226
258
 
227
- message_content = {
228
- "type": "text",
229
- "text": '\n'.join(text_content)}
230
- message = {
231
- "role": role,
232
- "content": [message_content]
233
- }
259
+ message_content = {"type": "text", "text": "\n".join(text_content)}
260
+ message = {"role": role, "content": [message_content]}
234
261
  message = append_images_to_message(message, image_data_list)
235
262
  return message
236
263
 
@@ -243,7 +270,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
243
270
 
244
271
  split_pattern = re.compile(f"({prompt_marker}|{response_marker})")
245
272
 
246
- parts = re.split(split_pattern, '\n'.join(self.chat_history))
273
+ parts = re.split(split_pattern, "\n".join(self.chat_history))
247
274
 
248
275
  all_prompts_and_responses = []
249
276
  current = ""
@@ -277,7 +304,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
277
304
  prompt_to_send = self.assemble_prompt()
278
305
  role_marker = f"# {Chat.ROLE_RESPONSE}:"
279
306
 
280
- with open(self.chat_name, 'a+', encoding='utf-8') as file:
307
+ with open(self.chat_name, "a+", encoding="utf-8") as file:
281
308
  last_line = self.get_last_line(file)
282
309
 
283
310
  print(role_marker)
@@ -300,24 +327,24 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
300
327
 
301
328
  def save_message(self, role: str, message: str):
302
329
  role_marker = f"# {role}:"
303
- with open(self.chat_name, 'r', encoding='utf-8') as file:
330
+ with open(self.chat_name, "r", encoding="utf-8") as file:
304
331
  stripped_line = self.get_last_non_empty_line(file)
305
332
  line_to_write = f"{message}\n\n"
306
333
  if stripped_line != role_marker:
307
334
  line_to_write = f"\n{role_marker}\n{message}\n"
308
335
 
309
- with open(self.chat_name, 'a', encoding='utf-8') as file:
336
+ with open(self.chat_name, "a", encoding="utf-8") as file:
310
337
  file.write(line_to_write)
311
338
  self.chat_history.append(line_to_write)
312
339
 
313
340
  def resend_message(self):
314
- with open(self.chat_name, 'r', encoding='utf-8') as file:
341
+ with open(self.chat_name, "r", encoding="utf-8") as file:
315
342
  lines = file.readlines()
316
343
  if not lines:
317
344
  return
318
345
  index_to_remove = self.find_last_reply_index(lines)
319
346
  if index_to_remove is not None:
320
- with open(self.chat_name, 'w', encoding='utf-8') as file:
347
+ with open(self.chat_name, "w", encoding="utf-8") as file:
321
348
  file.writelines(lines[:index_to_remove])
322
349
  self.send_message()
323
350
 
@@ -332,45 +359,35 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
332
359
  return index_to_remove
333
360
 
334
361
  def append_strings(self, strings: list[str]):
335
- output = '\n'.join(strings)
336
- with open(self.chat_name, 'a') as file:
337
- file.write(output + '\n')
362
+ output = "\n".join(strings)
363
+ with open(self.chat_name, "a") as file:
364
+ file.write(output + "\n")
338
365
 
339
366
  def load_chat_history(self, chat_file: str):
340
367
  chat_history = []
341
368
  if os.path.exists(chat_file):
342
- with open(chat_file, 'r', encoding='utf-8') as file:
369
+ with open(chat_file, "r", encoding="utf-8") as file:
343
370
  chat_history = file.readlines()
344
371
  return chat_history
345
372
 
346
373
  def create_empty_chat_file(self, chat_file: str):
347
- with open(chat_file, 'w', encoding='utf-8') as file:
374
+ with open(chat_file, "w", encoding="utf-8") as file:
348
375
  file.write(self.default_chat_content)
349
376
  self.chat_history = []
350
377
 
351
378
  def add_prompt_tag_if_needed(self, chat_file: str):
352
- with open(chat_file, 'r', encoding='utf-8') as file:
379
+ with open(chat_file, "r", encoding="utf-8") as file:
353
380
  lines = file.readlines()
354
381
  prompt_tag = f"# {Chat.ROLE_PROMPT}:"
355
382
  if Chat.get_last_role_marker(lines) == prompt_tag:
356
383
  return
357
384
  append = prompt_tag
358
385
  last_line = lines[-1].strip()
359
- if last_line != "" and last_line != '\n':
386
+ if last_line != "" and last_line != "\n":
360
387
  append = f"\n{append}"
361
- with open(chat_file, 'a', encoding='utf-8') as file:
388
+ with open(chat_file, "a", encoding="utf-8") as file:
362
389
  file.write(append)
363
390
 
364
- def determine_file_path(self, file_name: str):
365
- current_directory = os.path.dirname(self.chat_name)
366
- file_path = os.path.join(current_directory, file_name)
367
- if not os.path.exists(file_path):
368
- file_path = file_name
369
- if not os.path.exists(file_path):
370
- print(f"File {file_name} not found")
371
- return None
372
- return file_path
373
-
374
391
  # @file_exists_check
375
392
  def load_text_file(
376
393
  self,
@@ -378,7 +395,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
378
395
  prefix: str = "",
379
396
  suffix: str = "",
380
397
  block_delimiter: str = "",
381
- extract_images: bool = False
398
+ extract_images: bool = False,
382
399
  ):
383
400
  loader = TextFileLoader(self)
384
401
  return loader.load(
@@ -386,18 +403,15 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
386
403
  prefix=prefix,
387
404
  suffix=suffix,
388
405
  block_delimiter=block_delimiter,
389
- extract_images=extract_images
406
+ extract_images=extract_images,
390
407
  )
391
408
 
392
409
  # @file_exists_check
393
- def load_binary_file(self, file_path, mime_type: str, prefix: str = "", suffix: str = ""):
410
+ def load_binary_file(
411
+ self, file_path, mime_type: str, prefix: str = "", suffix: str = ""
412
+ ):
394
413
  loader = BinaryFileLoader(self)
395
- return loader.load(
396
- file_path,
397
- mime_type=mime_type,
398
- prefix=prefix,
399
- suffix=suffix
400
- )
414
+ return loader.load(file_path, mime_type=mime_type, prefix=prefix, suffix=suffix)
401
415
 
402
416
  def read_markdown(self, file_path: str, extract_images: bool = False) -> str:
403
417
  """Read markdown file and optionally extract/describe images"""
@@ -413,7 +427,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
413
427
  prefix: str = "",
414
428
  suffix: str = "",
415
429
  block_delimiter: str = "```",
416
- extract_images: bool = False
430
+ extract_images: bool = False,
417
431
  ):
418
432
  loader = DocumentFileLoader(self)
419
433
  return loader.load(
@@ -421,7 +435,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
421
435
  prefix=prefix,
422
436
  suffix=suffix,
423
437
  block_delimiter=block_delimiter,
424
- extract_images=extract_images
438
+ extract_images=extract_images,
425
439
  )
426
440
 
427
441
  def load_file(
@@ -430,7 +444,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
430
444
  prefix: str = "",
431
445
  suffix: str = "",
432
446
  block_delimiter: str = "",
433
- extract_images: bool = False
447
+ extract_images: bool = False,
434
448
  ):
435
449
  binary_type_mapping = Chat.BINARY_TYPE_MAPPING
436
450
  document_type_extensions = Chat.DOCUMENT_TYPE_EXTENSIONS
@@ -442,8 +456,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
442
456
  file_type = mime_type
443
457
  break
444
458
 
445
- is_file_document = any(file_name_lower.endswith(ext)
446
- for ext in document_type_extensions)
459
+ is_file_document = any(
460
+ file_name_lower.endswith(ext) for ext in document_type_extensions
461
+ )
447
462
 
448
463
  if is_file_document:
449
464
  return self.load_document_file(
@@ -451,14 +466,11 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
451
466
  prefix=prefix,
452
467
  suffix=suffix,
453
468
  block_delimiter=block_delimiter,
454
- extract_images=extract_images
469
+ extract_images=extract_images,
455
470
  )
456
471
  elif file_type:
457
472
  return self.load_binary_file(
458
- file_path=file_name,
459
- mime_type=file_type,
460
- prefix=prefix,
461
- suffix=suffix
473
+ file_path=file_name, mime_type=file_type, prefix=prefix, suffix=suffix
462
474
  )
463
475
  else:
464
476
  return self.load_text_file(
@@ -466,7 +478,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
466
478
  prefix=prefix,
467
479
  suffix=suffix,
468
480
  block_delimiter=block_delimiter,
469
- extract_images=extract_images
481
+ extract_images=extract_images,
470
482
  )
471
483
 
472
484
  def choose_file_to_load(self, files: list[str], pattern: str):
@@ -478,11 +490,13 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
478
490
  try:
479
491
  choice_index = int(choice) - 1
480
492
  if choice_index < 0 or choice_index >= len(files):
481
- print("Invalid choice. Aborting load.")
493
+ error_handler.report_error(
494
+ ValueError("Invalid choice. Aborting load.")
495
+ )
482
496
  return None
483
497
  file_path = files[choice_index]
484
- except ValueError:
485
- print("Invalid input. Aborting load.")
498
+ except ValueError as e:
499
+ error_handler.report_error(ValueError("Invalid input. Aborting load."))
486
500
  return None
487
501
  else:
488
502
  file_path = files[0]
@@ -491,7 +505,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
491
505
  def _help_menu(self, verbose: bool = False):
492
506
  super()._help_menu(verbose)
493
507
  if self.aliases:
494
- aliases = [f"{alias} -> {command}" for alias, command in self.aliases.items()]
508
+ aliases = [
509
+ f"{alias} -> {command}" for alias, command in self.aliases.items()
510
+ ]
495
511
  self._print_topics("Aliases", aliases, verbose)
496
512
 
497
513
  def do_quit(self, _):
@@ -510,7 +526,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
510
526
  def onecmd_plus_hooks(self, line, orig_rl_history_length):
511
527
  # store the full line for use with default()
512
528
  self.full_input = line
513
- return super().onecmd_plus_hooks(line, orig_rl_history_length=orig_rl_history_length)
529
+ return super().onecmd_plus_hooks(
530
+ line, orig_rl_history_length=orig_rl_history_length
531
+ )
514
532
 
515
533
  def default(self, line):
516
534
  self.message_buffer.append(self.full_input)
@@ -523,7 +541,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
523
541
 
524
542
  file_name = args.file_name
525
543
  load_images = args.load_images
526
-
544
+
527
545
  matching_files = self.find_matching_files_to_load(file_name)
528
546
  if not matching_files:
529
547
  return
@@ -532,7 +550,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
532
550
  block_delimiter = "```"
533
551
  prefix = f"\nFile: {file_path}\n"
534
552
  self.add_prompt_tag_if_needed(self.chat_name)
535
-
553
+
536
554
  if not os.path.isdir(file_path):
537
555
  command = LoadCommand(
538
556
  chat_instance=self,
@@ -540,16 +558,18 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
540
558
  prefix=prefix,
541
559
  block_delimiter=block_delimiter,
542
560
  extract_images=load_images,
543
- output=self.poutput
561
+ output=self.poutput,
544
562
  )
545
563
  command.execute()
546
564
 
547
565
  def complete_LOAD(self, text, line, begidx, endidx):
548
566
  import glob
549
- return [x for x in glob.glob(text + '*')]
567
+
568
+ return [x for x in glob.glob(text + "*")]
550
569
 
551
570
  def _retrieve_ara_config(self):
552
571
  from ara_cli.prompt_handler import ConfigManager
572
+
553
573
  return ConfigManager().get_config()
554
574
 
555
575
  def _retrieve_llm_config(self):
@@ -565,7 +585,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
565
585
  file_pattern = os.path.join(os.path.dirname(self.chat_name), file_name)
566
586
  matching_files = glob.glob(file_pattern)
567
587
  if not matching_files:
568
- print(f"No files matching pattern {file_name} found.")
588
+ error_handler.report_error(
589
+ AraError(f"No files matching pattern '{file_name}' found.")
590
+ )
569
591
  return
570
592
  return matching_files
571
593
 
@@ -581,27 +603,29 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
581
603
 
582
604
  if file_type:
583
605
  return self.load_binary_file(
584
- file_path=file_name,
585
- mime_type=file_type,
586
- prefix=prefix,
587
- suffix=suffix
606
+ file_path=file_name, mime_type=file_type, prefix=prefix, suffix=suffix
588
607
  )
589
- print(f"File {file_name} not recognized as image, could not load")
608
+ error_handler.report_error(
609
+ AraError(f"File {file_name} not recognized as image, could not load")
610
+ )
590
611
 
591
612
  def _verify_llm_choice(self, model_name):
592
613
  llm_config = self._retrieve_llm_config()
593
614
  models = [name for name in llm_config.keys()]
594
615
  if model_name not in models:
595
- print(f"Model {model_name} unavailable. Retrieve the list of available models using the LIST_MODELS command.")
616
+ error_handler.report_error(
617
+ AraConfigurationError(
618
+ f"Model {model_name} unavailable. Retrieve the list of available models using the LIST_MODELS command."
619
+ )
620
+ )
596
621
  return False
597
622
  return True
598
623
 
599
-
600
624
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
601
625
  def do_LOAD_IMAGE(self, file_name):
602
626
  """Load an image file and append it to chat file. Can be given the file name in-line. Will attempt to find the file relative to chat file first, then treat the given path as absolute"""
603
627
  from ara_cli.commands.load_image_command import LoadImageCommand
604
-
628
+
605
629
  matching_files = self.find_matching_files_to_load(file_name)
606
630
  if not matching_files:
607
631
  return
@@ -609,7 +633,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
609
633
  for file_path in matching_files:
610
634
  prefix = f"\nFile: {file_path}\n"
611
635
  self.add_prompt_tag_if_needed(self.chat_name)
612
-
636
+
613
637
  if not os.path.isdir(file_path):
614
638
  # Determine mime type
615
639
  file_type = None
@@ -618,18 +642,22 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
618
642
  if file_path_lower.endswith(extension):
619
643
  file_type = mime_type
620
644
  break
621
-
645
+
622
646
  if file_type:
623
647
  command = LoadImageCommand(
624
648
  chat_instance=self,
625
649
  file_path=file_path,
626
650
  mime_type=file_type,
627
651
  prefix=prefix,
628
- output=self.poutput
652
+ output=self.poutput,
629
653
  )
630
654
  command.execute()
631
655
  else:
632
- self.perror(f"File {file_path} not recognized as image, could not load")
656
+ error_handler.report_error(
657
+ AraError(
658
+ f"File {file_path} not recognized as image, could not load"
659
+ )
660
+ )
633
661
 
634
662
  @cmd2.with_category(CATEGORY_LLM_CONTROL)
635
663
  def do_LIST_MODELS(self, _):
@@ -648,7 +676,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
648
676
  original_dir = os.getcwd()
649
677
  navigator = DirectoryNavigator()
650
678
  navigator.navigate_to_target()
651
- os.chdir('..')
679
+ os.chdir("..")
652
680
 
653
681
  if not self._verify_llm_choice(model_name):
654
682
  return
@@ -670,14 +698,14 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
670
698
  original_dir = os.getcwd()
671
699
  navigator = DirectoryNavigator()
672
700
  navigator.navigate_to_target()
673
- os.chdir('..')
701
+ os.chdir("..")
674
702
 
675
703
  if not self._verify_llm_choice(model_name):
676
704
  return
677
705
 
678
706
  self.config.extraction_llm = model_name
679
707
  save_data(filepath=DEFAULT_CONFIG_LOCATION, config=self.config)
680
-
708
+
681
709
  LLMSingleton.set_extraction_model(model_name)
682
710
  print(f"Extraction model switched to '{model_name}'")
683
711
 
@@ -693,7 +721,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
693
721
  def do_CURRENT_EXTRACTION_MODEL(self, _):
694
722
  """Displays the current extraction language model."""
695
723
  from ara_cli.prompt_handler import LLMSingleton
696
-
724
+
697
725
  print(LLMSingleton.get_extraction_model())
698
726
 
699
727
  def _complete_llms(self, text, line, begidx, endidx):
@@ -731,7 +759,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
731
759
  def do_CLEAR(self, _):
732
760
  """Clear the chat and the file containing it"""
733
761
  user_input = input("Are you sure you want to clear the chat? (y/N): ")
734
- if user_input.lower() != 'y':
762
+ if user_input.lower() != "y":
735
763
  return
736
764
  self.create_empty_chat_file(self.chat_name)
737
765
  self.chat_history = self.load_chat_history(self.chat_name)
@@ -740,24 +768,30 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
740
768
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
741
769
  def do_LOAD_RULES(self, rules_name):
742
770
  """Load rules from ./prompt.data/*.rules.md or from a specified template directory if an argument is given. Specify global/<rules_template> to access globally defined rules templates"""
743
- self._load_template_helper(rules_name, "rules", "*.rules.md")
771
+ self.template_loader.load_template(rules_name, "rules", self.chat_name, "*.rules.md")
744
772
 
745
773
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
746
774
  def do_LOAD_INTENTION(self, intention_name):
747
775
  """Load intention from ./prompt.data/*.intention.md or from a specified template directory if an argument is given. Specify global/<intention_template> to access globally defined intention templates"""
748
- self._load_template_helper(intention_name, "intention", "*.intention.md")
776
+ self.template_loader.load_template(intention_name, "intention", self.chat_name, "*.intention.md")
749
777
 
750
778
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
751
779
  def do_LOAD_COMMANDS(self, commands_name):
752
780
  """Load commands from ./prompt.data/*.commands.md or from a specified template directory if an argument is given. Specify global/<commands_template> to access globally defined commands templates"""
753
- self._load_template_helper(commands_name, "commands", "*.commands.md")
781
+ self.template_loader.load_template(commands_name, "commands", self.chat_name, "*.commands.md")
754
782
 
755
783
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
756
784
  def do_LOAD_BLUEPRINT(self, blueprint_name):
757
785
  """Load specified blueprint. Specify global/<blueprint_name> to access globally defined blueprints"""
758
- self._load_template_from_global_or_local(blueprint_name, "blueprint")
786
+ self.template_loader.load_template(blueprint_name, "blueprint", self.chat_name)
759
787
 
760
- def _load_helper(self, directory: str, pattern: str, file_type: str, exclude_pattern: str | None = None):
788
+ def _load_helper(
789
+ self,
790
+ directory: str,
791
+ pattern: str,
792
+ file_type: str,
793
+ exclude_pattern: str | None = None,
794
+ ):
761
795
  import glob
762
796
 
763
797
  directory_path = os.path.join(os.path.dirname(self.chat_name), directory)
@@ -770,7 +804,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
770
804
  matching_files = list(set(matching_files) - set(exclude_files))
771
805
 
772
806
  if not matching_files:
773
- print(f"No {file_type} file found.")
807
+ error_handler.report_error(AraError(f"No {file_type} file found."))
774
808
  return
775
809
 
776
810
  file_path = self.choose_file_to_load(matching_files, pattern)
@@ -787,10 +821,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
787
821
  from ara_cli.ara_config import ConfigManager
788
822
  from ara_cli.directory_navigator import DirectoryNavigator
789
823
 
790
- plurals = {
791
- "commands": "commands",
792
- "rules": "rules"
793
- }
824
+ plurals = {"commands": "commands", "rules": "rules"}
794
825
 
795
826
  plural = f"{template_type}s"
796
827
  if template_type in plurals:
@@ -798,7 +829,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
798
829
 
799
830
  if template_name.startswith("global/"):
800
831
  directory = f"{TemplatePathManager.get_template_base_path()}/prompt-modules/{plural}/"
801
- self._load_helper(directory, template_name.removeprefix("global/"), template_type)
832
+ self._load_helper(
833
+ directory, template_name.removeprefix("global/"), template_type
834
+ )
802
835
  return
803
836
 
804
837
  ara_config = ConfigManager.get_config()
@@ -812,7 +845,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
812
845
  os.chdir(original_directory)
813
846
 
814
847
  custom_prompt_templates_subdir = self.config.custom_prompt_templates_subdir
815
- template_directory = f"{local_templates_path}/{custom_prompt_templates_subdir}/{plural}"
848
+ template_directory = (
849
+ f"{local_templates_path}/{custom_prompt_templates_subdir}/{plural}"
850
+ )
816
851
  self._load_helper(template_directory, template_name, template_type)
817
852
 
818
853
  def _load_template_helper(self, template_name, template_type, default_pattern):
@@ -820,7 +855,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
820
855
  self._load_helper("prompt.data", default_pattern, template_type)
821
856
  return
822
857
 
823
- self._load_template_from_global_or_local(template_name=template_name, template_type=template_type)
858
+ self._load_template_from_global_or_local(
859
+ template_name=template_name, template_type=template_type
860
+ )
824
861
 
825
862
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
826
863
  @cmd2.with_argparser(extract_parser)
@@ -832,7 +869,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
832
869
  file_name=self.chat_name,
833
870
  force=args.force,
834
871
  write=args.write,
835
- output=self.poutput
872
+ output=self.poutput,
836
873
  )
837
874
  command.execute()
838
875
 
@@ -860,13 +897,18 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
860
897
  if path:
861
898
  return [path]
862
899
  relative_path_for_error = os.path.join(base_directory, file_name)
863
- self.perror(f"No givens file found at {relative_path_for_error} or {file_name}")
900
+ error_handler.report_error(
901
+ AraError,
902
+ f"No givens file found at {relative_path_for_error} or {file_name}",
903
+ )
864
904
  return []
865
905
 
866
906
  # If no file_name, check for defaults
867
907
  default_files_to_check = [
868
908
  os.path.join(base_directory, "prompt.data", "config.prompt_givens.md"),
869
- os.path.join(base_directory, "prompt.data", "config.prompt_global_givens.md")
909
+ os.path.join(
910
+ base_directory, "prompt.data", "config.prompt_global_givens.md"
911
+ ),
870
912
  ]
871
913
  existing_defaults = [f for f in default_files_to_check if os.path.exists(f)]
872
914
  if existing_defaults:
@@ -877,11 +919,13 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
877
919
  if not user_input:
878
920
  self.poutput("Aborting.")
879
921
  return []
880
-
922
+
881
923
  path = resolve_path(user_input)
882
924
  if path:
883
925
  return [path]
884
- self.perror(f"No givens file found at {user_input}. Aborting.")
926
+ error_handler.report_error(
927
+ AraError(f"No givens file found at {user_input}. Aborting.")
928
+ )
885
929
  return []
886
930
 
887
931
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
@@ -891,7 +935,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
891
935
 
892
936
  givens_files_to_process = self._find_givens_files(file_name)
893
937
  if not givens_files_to_process:
894
- self.poutput("No givens files to load.")
938
+ error_handler.report_error(AraError("No givens files to load."))
895
939
  return
896
940
 
897
941
  for givens_path in givens_files_to_process:
@@ -899,7 +943,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
899
943
  # from the markdown file. No directory change is needed.
900
944
  content, _ = load_givens(givens_path)
901
945
 
902
- with open(self.chat_name, 'a', encoding='utf-8') as chat_file:
946
+ with open(self.chat_name, "a", encoding="utf-8") as chat_file:
903
947
  chat_file.write(content)
904
948
 
905
949
  self.poutput(f"Loaded files listed and marked in {givens_path}")
@@ -916,12 +960,15 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
916
960
  """Load artefact template"""
917
961
  from ara_cli.artefact_models.artefact_templates import template_artefact_of_type
918
962
 
919
- artefact = template_artefact_of_type(''.join(template_name))
963
+ artefact = template_artefact_of_type("".join(template_name))
920
964
  if not artefact:
921
- raise ValueError(f"No template for '{template_name}' found.")
965
+ error_handler.report_error(
966
+ ValueError(f"No template for '{template_name}' found.")
967
+ )
968
+ return
922
969
  write_content = artefact.serialize()
923
970
  self.add_prompt_tag_if_needed(self.chat_name)
924
- with open(self.chat_name, 'a', encoding='utf-8') as chat_file:
971
+ with open(self.chat_name, "a", encoding="utf-8") as chat_file:
925
972
  chat_file.write(write_content)
926
973
  print(f"Loaded {template_name} artefact template")
927
974
 
@@ -935,11 +982,12 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
935
982
  if not text:
936
983
  completions = classifiers
937
984
  else:
938
- completions = [classifier for classifier in classifiers if classifier.startswith(text)]
985
+ completions = [
986
+ classifier for classifier in classifiers if classifier.startswith(text)
987
+ ]
939
988
 
940
989
  return completions
941
990
 
942
-
943
991
  def _get_plural_template_type(self, template_type: str) -> str:
944
992
  """Determines the plural form of a template type."""
945
993
  plurals = {"commands": "commands", "rules": "rules"}
@@ -952,22 +1000,25 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
952
1000
  """
953
1001
  current_dir = os.path.dirname(self.chat_name)
954
1002
  while True:
955
- if os.path.isdir(os.path.join(current_dir, 'ara')):
1003
+ if os.path.isdir(os.path.join(current_dir, "ara")):
956
1004
  return current_dir
957
1005
  parent_dir = os.path.dirname(current_dir)
958
1006
  if parent_dir == current_dir: # Reached the filesystem root
959
1007
  return None
960
1008
  current_dir = parent_dir
961
1009
 
962
- def _gather_templates_from_path(self, search_path: str, templates_set: set, prefix: str = ""):
1010
+ def _gather_templates_from_path(
1011
+ self, search_path: str, templates_set: set, prefix: str = ""
1012
+ ):
963
1013
  """
964
1014
  Scans a given path for items and adds them to the provided set,
965
1015
  optionally prepending a prefix.
966
1016
  """
967
1017
  import glob
1018
+
968
1019
  if not os.path.isdir(search_path):
969
1020
  return
970
- for path in glob.glob(os.path.join(search_path, '*')):
1021
+ for path in glob.glob(os.path.join(search_path, "*")):
971
1022
  templates_set.add(f"{prefix}{os.path.basename(path)}")
972
1023
 
973
1024
  def _get_available_templates(self, template_type: str) -> list[str]:
@@ -991,8 +1042,12 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
991
1042
  # 1. Find Global Templates
992
1043
  try:
993
1044
  global_base_path = TemplatePathManager.get_template_base_path()
994
- global_template_dir = os.path.join(global_base_path, "prompt-modules", plural_type)
995
- self._gather_templates_from_path(global_template_dir, templates, prefix="global/")
1045
+ global_template_dir = os.path.join(
1046
+ global_base_path, "prompt-modules", plural_type
1047
+ )
1048
+ self._gather_templates_from_path(
1049
+ global_template_dir, templates, prefix="global/"
1050
+ )
996
1051
  except Exception:
997
1052
  pass # Silently ignore if global templates are not found
998
1053
 
@@ -1000,8 +1055,14 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
1000
1055
  try:
1001
1056
  project_root = self._find_project_root()
1002
1057
  if project_root:
1003
- local_templates_base = os.path.join(project_root, self.config.local_prompt_templates_dir)
1004
- custom_dir = os.path.join(local_templates_base, self.config.custom_prompt_templates_subdir, plural_type)
1058
+ local_templates_base = os.path.join(
1059
+ project_root, self.config.local_prompt_templates_dir
1060
+ )
1061
+ custom_dir = os.path.join(
1062
+ local_templates_base,
1063
+ self.config.custom_prompt_templates_subdir,
1064
+ plural_type,
1065
+ )
1005
1066
  self._gather_templates_from_path(custom_dir, templates)
1006
1067
  except Exception:
1007
1068
  pass # Silently ignore if local templates cannot be resolved
@@ -1010,7 +1071,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
1010
1071
 
1011
1072
  def _template_completer(self, text: str, template_type: str) -> list[str]:
1012
1073
  """Generic completer for different template types."""
1013
- available_templates = self._get_available_templates(template_type)
1074
+ available_templates = self.template_loader.get_available_templates(template_type, os.path.dirname(self.chat_name))
1014
1075
  if not text:
1015
1076
  return available_templates
1016
1077
  return [t for t in available_templates if t.startswith(text)]
@@ -1029,4 +1090,4 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
1029
1090
 
1030
1091
  def complete_LOAD_BLUEPRINT(self, text, line, begidx, endidx):
1031
1092
  """Completer for the LOAD_BLUEPRINT command."""
1032
- return self._template_completer(text, "blueprint")
1093
+ return self._template_completer(text, "blueprint")