ara-cli 0.1.9.95__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,8 +1,12 @@
1
1
  import os
2
+ import errno
2
3
  import argparse
3
4
  import cmd2
5
+
4
6
  from ara_cli.prompt_handler import send_prompt
5
- from ara_cli.file_loaders.markdown_reader import MarkdownReader
7
+
8
+ from . import error_handler
9
+ from ara_cli.error_handler import AraError, AraConfigurationError
6
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
@@ -10,27 +14,42 @@ 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')
19
-
20
-
21
- from ara_cli.file_loaders.document_file_loader import DocumentFileLoader
22
- from ara_cli.file_loaders.binary_file_loader import BinaryFileLoader
23
- from ara_cli.file_loaders.text_file_loader import TextFileLoader
24
-
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
+ )
25
34
 
26
35
  extract_parser = argparse.ArgumentParser()
27
- extract_parser.add_argument('-f', '--force', action='store_true', help='Force extraction')
28
- 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
+ )
29
45
 
30
46
  load_parser = argparse.ArgumentParser()
31
- load_parser.add_argument('file_name', nargs='?', default='', help='File to load')
32
- load_parser.add_argument('--load-images', action='store_true', help='Extract and describe images from documents')
33
-
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
+ )
34
53
 
35
54
 
36
55
  class Chat(cmd2.Cmd):
@@ -60,36 +79,40 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
60
79
  ROLE_RESPONSE = "ara response"
61
80
 
62
81
  BINARY_TYPE_MAPPING = {
63
- ".png": "image/png",
64
- ".jpg": "image/jpeg",
65
- ".jpeg": "image/jpeg",
66
- }
67
-
82
+ ".png": "image/png",
83
+ ".jpg": "image/jpeg",
84
+ ".jpeg": "image/jpeg",
85
+ }
86
+
68
87
  DOCUMENT_TYPE_EXTENSIONS = [".docx", ".doc", ".odt", ".pdf"]
69
88
 
70
89
  def __init__(
71
90
  self,
72
91
  chat_name: str,
73
92
  reset: bool | None = None,
74
- enable_commands: list[str] | None = None
93
+ enable_commands: list[str] | None = None,
75
94
  ):
95
+ from ara_cli.template_loader import TemplateLoader
76
96
  shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
77
97
  if enable_commands:
78
98
  enable_commands.append("quit") # always allow quitting
79
99
  enable_commands.append("eof") # always allow quitting with ctrl-D
80
100
  enable_commands.append("help") # always allow help
81
101
 
82
- 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
+ }
83
107
 
84
- super().__init__(
85
- allow_cli_args=False,
86
- shortcuts=shortcuts
87
- )
108
+ super().__init__(allow_cli_args=False, shortcuts=shortcuts)
88
109
  self.create_default_aliases()
89
110
 
90
111
  if enable_commands:
91
112
  all_commands = self.get_all_commands()
92
- 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
+ ]
93
116
  self.disable_commands(commands_to_disable)
94
117
 
95
118
  self.prompt = "ara> "
@@ -101,12 +124,15 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
101
124
  self.chat_history = []
102
125
  self.message_buffer = []
103
126
  self.config = self._retrieve_ara_config()
127
+ self.template_loader = TemplateLoader(chat_instance=self)
104
128
 
105
129
  def disable_commands(self, commands: list[str]):
106
130
  for command in commands:
107
- setattr(self, f'do_{command}', self.default)
131
+ setattr(self, f"do_{command}", self.default)
108
132
  self.hidden_commands.append(command)
109
- 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
+ ]
110
136
  for alias in aliases_to_remove:
111
137
  del self.aliases[alias]
112
138
 
@@ -141,8 +167,10 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
141
167
  chat_file_short = os.path.split(chat_file)[-1]
142
168
 
143
169
  if reset is None:
144
- user_input = input(f"{chat_file_short} already exists. Do you want to reset the chat? (y/N): ")
145
- 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":
146
174
  self.create_empty_chat_file(chat_file)
147
175
  if reset:
148
176
  self.create_empty_chat_file(chat_file)
@@ -175,10 +203,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
175
203
  def get_last_role_marker(lines):
176
204
  if not lines:
177
205
  return
178
- role_markers = [
179
- f"# {Chat.ROLE_PROMPT}:",
180
- f"# {Chat.ROLE_RESPONSE}"
181
- ]
206
+ role_markers = [f"# {Chat.ROLE_PROMPT}:", f"# {Chat.ROLE_RESPONSE}"]
182
207
  for line in reversed(lines):
183
208
  stripped_line = line.strip()
184
209
  if stripped_line.startswith(tuple(role_markers)):
@@ -186,7 +211,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
186
211
  return None
187
212
 
188
213
  def start_non_interactive(self):
189
- with open(self.chat_name, 'r', encoding='utf-8') as file:
214
+ with open(self.chat_name, "r", encoding="utf-8") as file:
190
215
  content = file.read()
191
216
  print(content)
192
217
 
@@ -221,7 +246,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
221
246
  text_content = []
222
247
  image_data_list = []
223
248
 
224
- image_pattern = re.compile(r'\((data:image/[^;]+;base64,.*?)\)')
249
+ image_pattern = re.compile(r"\((data:image/[^;]+;base64,.*?)\)")
225
250
 
226
251
  for line in message.splitlines():
227
252
  match = image_pattern.search(line)
@@ -231,13 +256,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
231
256
  else:
232
257
  text_content.append(line)
233
258
 
234
- message_content = {
235
- "type": "text",
236
- "text": '\n'.join(text_content)}
237
- message = {
238
- "role": role,
239
- "content": [message_content]
240
- }
259
+ message_content = {"type": "text", "text": "\n".join(text_content)}
260
+ message = {"role": role, "content": [message_content]}
241
261
  message = append_images_to_message(message, image_data_list)
242
262
  return message
243
263
 
@@ -250,7 +270,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
250
270
 
251
271
  split_pattern = re.compile(f"({prompt_marker}|{response_marker})")
252
272
 
253
- parts = re.split(split_pattern, '\n'.join(self.chat_history))
273
+ parts = re.split(split_pattern, "\n".join(self.chat_history))
254
274
 
255
275
  all_prompts_and_responses = []
256
276
  current = ""
@@ -284,7 +304,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
284
304
  prompt_to_send = self.assemble_prompt()
285
305
  role_marker = f"# {Chat.ROLE_RESPONSE}:"
286
306
 
287
- with open(self.chat_name, 'a+', encoding='utf-8') as file:
307
+ with open(self.chat_name, "a+", encoding="utf-8") as file:
288
308
  last_line = self.get_last_line(file)
289
309
 
290
310
  print(role_marker)
@@ -307,24 +327,24 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
307
327
 
308
328
  def save_message(self, role: str, message: str):
309
329
  role_marker = f"# {role}:"
310
- with open(self.chat_name, 'r', encoding='utf-8') as file:
330
+ with open(self.chat_name, "r", encoding="utf-8") as file:
311
331
  stripped_line = self.get_last_non_empty_line(file)
312
332
  line_to_write = f"{message}\n\n"
313
333
  if stripped_line != role_marker:
314
334
  line_to_write = f"\n{role_marker}\n{message}\n"
315
335
 
316
- with open(self.chat_name, 'a', encoding='utf-8') as file:
336
+ with open(self.chat_name, "a", encoding="utf-8") as file:
317
337
  file.write(line_to_write)
318
338
  self.chat_history.append(line_to_write)
319
339
 
320
340
  def resend_message(self):
321
- with open(self.chat_name, 'r', encoding='utf-8') as file:
341
+ with open(self.chat_name, "r", encoding="utf-8") as file:
322
342
  lines = file.readlines()
323
343
  if not lines:
324
344
  return
325
345
  index_to_remove = self.find_last_reply_index(lines)
326
346
  if index_to_remove is not None:
327
- with open(self.chat_name, 'w', encoding='utf-8') as file:
347
+ with open(self.chat_name, "w", encoding="utf-8") as file:
328
348
  file.writelines(lines[:index_to_remove])
329
349
  self.send_message()
330
350
 
@@ -339,45 +359,35 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
339
359
  return index_to_remove
340
360
 
341
361
  def append_strings(self, strings: list[str]):
342
- output = '\n'.join(strings)
343
- with open(self.chat_name, 'a') as file:
344
- file.write(output + '\n')
362
+ output = "\n".join(strings)
363
+ with open(self.chat_name, "a") as file:
364
+ file.write(output + "\n")
345
365
 
346
366
  def load_chat_history(self, chat_file: str):
347
367
  chat_history = []
348
368
  if os.path.exists(chat_file):
349
- with open(chat_file, 'r', encoding='utf-8') as file:
369
+ with open(chat_file, "r", encoding="utf-8") as file:
350
370
  chat_history = file.readlines()
351
371
  return chat_history
352
372
 
353
373
  def create_empty_chat_file(self, chat_file: str):
354
- with open(chat_file, 'w', encoding='utf-8') as file:
374
+ with open(chat_file, "w", encoding="utf-8") as file:
355
375
  file.write(self.default_chat_content)
356
376
  self.chat_history = []
357
377
 
358
378
  def add_prompt_tag_if_needed(self, chat_file: str):
359
- with open(chat_file, 'r', encoding='utf-8') as file:
379
+ with open(chat_file, "r", encoding="utf-8") as file:
360
380
  lines = file.readlines()
361
381
  prompt_tag = f"# {Chat.ROLE_PROMPT}:"
362
382
  if Chat.get_last_role_marker(lines) == prompt_tag:
363
383
  return
364
384
  append = prompt_tag
365
385
  last_line = lines[-1].strip()
366
- if last_line != "" and last_line != '\n':
386
+ if last_line != "" and last_line != "\n":
367
387
  append = f"\n{append}"
368
- with open(chat_file, 'a', encoding='utf-8') as file:
388
+ with open(chat_file, "a", encoding="utf-8") as file:
369
389
  file.write(append)
370
390
 
371
- def determine_file_path(self, file_name: str):
372
- current_directory = os.path.dirname(self.chat_name)
373
- file_path = os.path.join(current_directory, file_name)
374
- if not os.path.exists(file_path):
375
- file_path = file_name
376
- if not os.path.exists(file_path):
377
- print(f"File {file_name} not found")
378
- return None
379
- return file_path
380
-
381
391
  # @file_exists_check
382
392
  def load_text_file(
383
393
  self,
@@ -385,7 +395,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
385
395
  prefix: str = "",
386
396
  suffix: str = "",
387
397
  block_delimiter: str = "",
388
- extract_images: bool = False
398
+ extract_images: bool = False,
389
399
  ):
390
400
  loader = TextFileLoader(self)
391
401
  return loader.load(
@@ -393,18 +403,15 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
393
403
  prefix=prefix,
394
404
  suffix=suffix,
395
405
  block_delimiter=block_delimiter,
396
- extract_images=extract_images
406
+ extract_images=extract_images,
397
407
  )
398
408
 
399
409
  # @file_exists_check
400
- 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
+ ):
401
413
  loader = BinaryFileLoader(self)
402
- return loader.load(
403
- file_path,
404
- mime_type=mime_type,
405
- prefix=prefix,
406
- suffix=suffix
407
- )
414
+ return loader.load(file_path, mime_type=mime_type, prefix=prefix, suffix=suffix)
408
415
 
409
416
  def read_markdown(self, file_path: str, extract_images: bool = False) -> str:
410
417
  """Read markdown file and optionally extract/describe images"""
@@ -420,7 +427,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
420
427
  prefix: str = "",
421
428
  suffix: str = "",
422
429
  block_delimiter: str = "```",
423
- extract_images: bool = False
430
+ extract_images: bool = False,
424
431
  ):
425
432
  loader = DocumentFileLoader(self)
426
433
  return loader.load(
@@ -428,7 +435,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
428
435
  prefix=prefix,
429
436
  suffix=suffix,
430
437
  block_delimiter=block_delimiter,
431
- extract_images=extract_images
438
+ extract_images=extract_images,
432
439
  )
433
440
 
434
441
  def load_file(
@@ -437,7 +444,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
437
444
  prefix: str = "",
438
445
  suffix: str = "",
439
446
  block_delimiter: str = "",
440
- extract_images: bool = False
447
+ extract_images: bool = False,
441
448
  ):
442
449
  binary_type_mapping = Chat.BINARY_TYPE_MAPPING
443
450
  document_type_extensions = Chat.DOCUMENT_TYPE_EXTENSIONS
@@ -449,8 +456,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
449
456
  file_type = mime_type
450
457
  break
451
458
 
452
- is_file_document = any(file_name_lower.endswith(ext)
453
- for ext in document_type_extensions)
459
+ is_file_document = any(
460
+ file_name_lower.endswith(ext) for ext in document_type_extensions
461
+ )
454
462
 
455
463
  if is_file_document:
456
464
  return self.load_document_file(
@@ -458,14 +466,11 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
458
466
  prefix=prefix,
459
467
  suffix=suffix,
460
468
  block_delimiter=block_delimiter,
461
- extract_images=extract_images
469
+ extract_images=extract_images,
462
470
  )
463
471
  elif file_type:
464
472
  return self.load_binary_file(
465
- file_path=file_name,
466
- mime_type=file_type,
467
- prefix=prefix,
468
- suffix=suffix
473
+ file_path=file_name, mime_type=file_type, prefix=prefix, suffix=suffix
469
474
  )
470
475
  else:
471
476
  return self.load_text_file(
@@ -473,7 +478,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
473
478
  prefix=prefix,
474
479
  suffix=suffix,
475
480
  block_delimiter=block_delimiter,
476
- extract_images=extract_images
481
+ extract_images=extract_images,
477
482
  )
478
483
 
479
484
  def choose_file_to_load(self, files: list[str], pattern: str):
@@ -485,11 +490,13 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
485
490
  try:
486
491
  choice_index = int(choice) - 1
487
492
  if choice_index < 0 or choice_index >= len(files):
488
- print("Invalid choice. Aborting load.")
493
+ error_handler.report_error(
494
+ ValueError("Invalid choice. Aborting load.")
495
+ )
489
496
  return None
490
497
  file_path = files[choice_index]
491
- except ValueError:
492
- print("Invalid input. Aborting load.")
498
+ except ValueError as e:
499
+ error_handler.report_error(ValueError("Invalid input. Aborting load."))
493
500
  return None
494
501
  else:
495
502
  file_path = files[0]
@@ -498,7 +505,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
498
505
  def _help_menu(self, verbose: bool = False):
499
506
  super()._help_menu(verbose)
500
507
  if self.aliases:
501
- 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
+ ]
502
511
  self._print_topics("Aliases", aliases, verbose)
503
512
 
504
513
  def do_quit(self, _):
@@ -507,10 +516,19 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
507
516
  self.last_result = True
508
517
  return True
509
518
 
519
+ def onecmd(self, *args, **kwargs):
520
+ try:
521
+ return super().onecmd(*args, **kwargs)
522
+ except Exception as e:
523
+ error_handler.report_error(e)
524
+ return False
525
+
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)
@@ -833,7 +870,6 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
833
870
  force=args.force,
834
871
  write=args.write,
835
872
  output=self.poutput,
836
- error_output=self.perror
837
873
  )
838
874
  command.execute()
839
875
 
@@ -861,13 +897,18 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
861
897
  if path:
862
898
  return [path]
863
899
  relative_path_for_error = os.path.join(base_directory, file_name)
864
- 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
+ )
865
904
  return []
866
905
 
867
906
  # If no file_name, check for defaults
868
907
  default_files_to_check = [
869
908
  os.path.join(base_directory, "prompt.data", "config.prompt_givens.md"),
870
- 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
+ ),
871
912
  ]
872
913
  existing_defaults = [f for f in default_files_to_check if os.path.exists(f)]
873
914
  if existing_defaults:
@@ -878,11 +919,13 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
878
919
  if not user_input:
879
920
  self.poutput("Aborting.")
880
921
  return []
881
-
922
+
882
923
  path = resolve_path(user_input)
883
924
  if path:
884
925
  return [path]
885
- 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
+ )
886
929
  return []
887
930
 
888
931
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
@@ -892,7 +935,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
892
935
 
893
936
  givens_files_to_process = self._find_givens_files(file_name)
894
937
  if not givens_files_to_process:
895
- self.poutput("No givens files to load.")
938
+ error_handler.report_error(AraError("No givens files to load."))
896
939
  return
897
940
 
898
941
  for givens_path in givens_files_to_process:
@@ -900,7 +943,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
900
943
  # from the markdown file. No directory change is needed.
901
944
  content, _ = load_givens(givens_path)
902
945
 
903
- 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:
904
947
  chat_file.write(content)
905
948
 
906
949
  self.poutput(f"Loaded files listed and marked in {givens_path}")
@@ -917,17 +960,20 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
917
960
  """Load artefact template"""
918
961
  from ara_cli.artefact_models.artefact_templates import template_artefact_of_type
919
962
 
920
- artefact = template_artefact_of_type(''.join(template_name))
963
+ artefact = template_artefact_of_type("".join(template_name))
921
964
  if not artefact:
965
+ error_handler.report_error(
966
+ ValueError(f"No template for '{template_name}' found.")
967
+ )
922
968
  return
923
969
  write_content = artefact.serialize()
924
970
  self.add_prompt_tag_if_needed(self.chat_name)
925
- 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:
926
972
  chat_file.write(write_content)
927
973
  print(f"Loaded {template_name} artefact template")
928
974
 
929
975
  def complete_LOAD_TEMPLATE(self, text, line, begidx, endidx):
930
- return self._complete_classifiers(self, text, line, begidx, endidx)
976
+ return self._complete_classifiers(text, line, begidx, endidx)
931
977
 
932
978
  def _complete_classifiers(self, text, line, begidx, endidx):
933
979
  from ara_cli.classifier import Classifier
@@ -936,11 +982,12 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
936
982
  if not text:
937
983
  completions = classifiers
938
984
  else:
939
- completions = [classifier for classifier in classifiers if classifier.startswith(text)]
985
+ completions = [
986
+ classifier for classifier in classifiers if classifier.startswith(text)
987
+ ]
940
988
 
941
989
  return completions
942
990
 
943
-
944
991
  def _get_plural_template_type(self, template_type: str) -> str:
945
992
  """Determines the plural form of a template type."""
946
993
  plurals = {"commands": "commands", "rules": "rules"}
@@ -953,22 +1000,25 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
953
1000
  """
954
1001
  current_dir = os.path.dirname(self.chat_name)
955
1002
  while True:
956
- if os.path.isdir(os.path.join(current_dir, 'ara')):
1003
+ if os.path.isdir(os.path.join(current_dir, "ara")):
957
1004
  return current_dir
958
1005
  parent_dir = os.path.dirname(current_dir)
959
1006
  if parent_dir == current_dir: # Reached the filesystem root
960
1007
  return None
961
1008
  current_dir = parent_dir
962
1009
 
963
- 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
+ ):
964
1013
  """
965
1014
  Scans a given path for items and adds them to the provided set,
966
1015
  optionally prepending a prefix.
967
1016
  """
968
1017
  import glob
1018
+
969
1019
  if not os.path.isdir(search_path):
970
1020
  return
971
- for path in glob.glob(os.path.join(search_path, '*')):
1021
+ for path in glob.glob(os.path.join(search_path, "*")):
972
1022
  templates_set.add(f"{prefix}{os.path.basename(path)}")
973
1023
 
974
1024
  def _get_available_templates(self, template_type: str) -> list[str]:
@@ -992,8 +1042,12 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
992
1042
  # 1. Find Global Templates
993
1043
  try:
994
1044
  global_base_path = TemplatePathManager.get_template_base_path()
995
- global_template_dir = os.path.join(global_base_path, "prompt-modules", plural_type)
996
- 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
+ )
997
1051
  except Exception:
998
1052
  pass # Silently ignore if global templates are not found
999
1053
 
@@ -1001,8 +1055,14 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
1001
1055
  try:
1002
1056
  project_root = self._find_project_root()
1003
1057
  if project_root:
1004
- local_templates_base = os.path.join(project_root, self.config.local_prompt_templates_dir)
1005
- 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
+ )
1006
1066
  self._gather_templates_from_path(custom_dir, templates)
1007
1067
  except Exception:
1008
1068
  pass # Silently ignore if local templates cannot be resolved
@@ -1011,7 +1071,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
1011
1071
 
1012
1072
  def _template_completer(self, text: str, template_type: str) -> list[str]:
1013
1073
  """Generic completer for different template types."""
1014
- 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))
1015
1075
  if not text:
1016
1076
  return available_templates
1017
1077
  return [t for t in available_templates if t.startswith(text)]
@@ -1030,4 +1090,4 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
1030
1090
 
1031
1091
  def complete_LOAD_BLUEPRINT(self, text, line, begidx, endidx):
1032
1092
  """Completer for the LOAD_BLUEPRINT command."""
1033
- return self._template_completer(text, "blueprint")
1093
+ return self._template_completer(text, "blueprint")