ara-cli 0.1.9.69__py3-none-any.whl → 0.1.10.8__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.

Potentially problematic release.


This version of ara-cli might be problematic. Click here for more details.

Files changed (150) hide show
  1. ara_cli/__init__.py +18 -2
  2. ara_cli/__main__.py +248 -62
  3. ara_cli/ara_command_action.py +155 -86
  4. ara_cli/ara_config.py +226 -80
  5. ara_cli/ara_subcommands/__init__.py +0 -0
  6. ara_cli/ara_subcommands/autofix.py +26 -0
  7. ara_cli/ara_subcommands/chat.py +27 -0
  8. ara_cli/ara_subcommands/classifier_directory.py +16 -0
  9. ara_cli/ara_subcommands/common.py +100 -0
  10. ara_cli/ara_subcommands/create.py +75 -0
  11. ara_cli/ara_subcommands/delete.py +22 -0
  12. ara_cli/ara_subcommands/extract.py +22 -0
  13. ara_cli/ara_subcommands/fetch_templates.py +14 -0
  14. ara_cli/ara_subcommands/list.py +65 -0
  15. ara_cli/ara_subcommands/list_tags.py +25 -0
  16. ara_cli/ara_subcommands/load.py +48 -0
  17. ara_cli/ara_subcommands/prompt.py +136 -0
  18. ara_cli/ara_subcommands/read.py +47 -0
  19. ara_cli/ara_subcommands/read_status.py +20 -0
  20. ara_cli/ara_subcommands/read_user.py +20 -0
  21. ara_cli/ara_subcommands/reconnect.py +27 -0
  22. ara_cli/ara_subcommands/rename.py +22 -0
  23. ara_cli/ara_subcommands/scan.py +14 -0
  24. ara_cli/ara_subcommands/set_status.py +22 -0
  25. ara_cli/ara_subcommands/set_user.py +22 -0
  26. ara_cli/ara_subcommands/template.py +16 -0
  27. ara_cli/artefact_autofix.py +649 -68
  28. ara_cli/artefact_creator.py +8 -11
  29. ara_cli/artefact_deleter.py +2 -4
  30. ara_cli/artefact_fuzzy_search.py +22 -10
  31. ara_cli/artefact_link_updater.py +4 -4
  32. ara_cli/artefact_lister.py +29 -55
  33. ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
  34. ara_cli/artefact_models/artefact_load.py +11 -3
  35. ara_cli/artefact_models/artefact_model.py +146 -39
  36. ara_cli/artefact_models/artefact_templates.py +70 -44
  37. ara_cli/artefact_models/businessgoal_artefact_model.py +23 -25
  38. ara_cli/artefact_models/epic_artefact_model.py +34 -26
  39. ara_cli/artefact_models/feature_artefact_model.py +203 -64
  40. ara_cli/artefact_models/keyfeature_artefact_model.py +21 -24
  41. ara_cli/artefact_models/serialize_helper.py +1 -1
  42. ara_cli/artefact_models/task_artefact_model.py +83 -15
  43. ara_cli/artefact_models/userstory_artefact_model.py +37 -27
  44. ara_cli/artefact_models/vision_artefact_model.py +23 -42
  45. ara_cli/artefact_reader.py +92 -91
  46. ara_cli/artefact_renamer.py +8 -4
  47. ara_cli/artefact_scan.py +66 -3
  48. ara_cli/chat.py +622 -162
  49. ara_cli/chat_agent/__init__.py +0 -0
  50. ara_cli/chat_agent/agent_communicator.py +62 -0
  51. ara_cli/chat_agent/agent_process_manager.py +211 -0
  52. ara_cli/chat_agent/agent_status_manager.py +73 -0
  53. ara_cli/chat_agent/agent_workspace_manager.py +76 -0
  54. ara_cli/commands/__init__.py +0 -0
  55. ara_cli/commands/command.py +7 -0
  56. ara_cli/commands/extract_command.py +15 -0
  57. ara_cli/commands/load_command.py +65 -0
  58. ara_cli/commands/load_image_command.py +34 -0
  59. ara_cli/commands/read_command.py +117 -0
  60. ara_cli/completers.py +144 -0
  61. ara_cli/directory_navigator.py +37 -4
  62. ara_cli/error_handler.py +134 -0
  63. ara_cli/file_classifier.py +6 -5
  64. ara_cli/file_lister.py +1 -1
  65. ara_cli/file_loaders/__init__.py +0 -0
  66. ara_cli/file_loaders/binary_file_loader.py +33 -0
  67. ara_cli/file_loaders/document_file_loader.py +34 -0
  68. ara_cli/file_loaders/document_reader.py +245 -0
  69. ara_cli/file_loaders/document_readers.py +233 -0
  70. ara_cli/file_loaders/file_loader.py +50 -0
  71. ara_cli/file_loaders/file_loaders.py +123 -0
  72. ara_cli/file_loaders/image_processor.py +89 -0
  73. ara_cli/file_loaders/markdown_reader.py +75 -0
  74. ara_cli/file_loaders/text_file_loader.py +187 -0
  75. ara_cli/global_file_lister.py +51 -0
  76. ara_cli/list_filter.py +1 -1
  77. ara_cli/output_suppressor.py +1 -1
  78. ara_cli/prompt_extractor.py +215 -88
  79. ara_cli/prompt_handler.py +521 -134
  80. ara_cli/prompt_rag.py +2 -2
  81. ara_cli/tag_extractor.py +83 -38
  82. ara_cli/template_loader.py +245 -0
  83. ara_cli/template_manager.py +18 -13
  84. ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
  85. ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
  86. ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
  87. ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
  88. ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
  89. ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
  90. ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
  91. ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
  92. ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
  93. ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
  94. ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
  95. ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
  96. ara_cli/update_config_prompt.py +9 -3
  97. ara_cli/version.py +1 -1
  98. ara_cli-0.1.10.8.dist-info/METADATA +241 -0
  99. ara_cli-0.1.10.8.dist-info/RECORD +193 -0
  100. tests/test_ara_command_action.py +73 -59
  101. tests/test_ara_config.py +341 -36
  102. tests/test_artefact_autofix.py +1060 -0
  103. tests/test_artefact_link_updater.py +3 -3
  104. tests/test_artefact_lister.py +52 -132
  105. tests/test_artefact_renamer.py +2 -2
  106. tests/test_artefact_scan.py +327 -33
  107. tests/test_chat.py +2063 -498
  108. tests/test_file_classifier.py +24 -1
  109. tests/test_file_creator.py +3 -5
  110. tests/test_file_lister.py +1 -1
  111. tests/test_global_file_lister.py +131 -0
  112. tests/test_list_filter.py +2 -2
  113. tests/test_prompt_handler.py +746 -0
  114. tests/test_tag_extractor.py +19 -13
  115. tests/test_template_loader.py +192 -0
  116. tests/test_template_manager.py +5 -4
  117. tests/test_update_config_prompt.py +2 -2
  118. ara_cli/ara_command_parser.py +0 -327
  119. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
  120. ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
  121. ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
  122. ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
  123. ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
  124. ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
  125. ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
  126. ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
  127. ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
  128. ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
  129. ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
  130. ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
  131. ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
  132. ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
  133. ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
  134. ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
  135. ara_cli/templates/template.businessgoal +0 -10
  136. ara_cli/templates/template.capability +0 -10
  137. ara_cli/templates/template.epic +0 -15
  138. ara_cli/templates/template.example +0 -6
  139. ara_cli/templates/template.feature +0 -26
  140. ara_cli/templates/template.issue +0 -14
  141. ara_cli/templates/template.keyfeature +0 -15
  142. ara_cli/templates/template.task +0 -6
  143. ara_cli/templates/template.userstory +0 -17
  144. ara_cli/templates/template.vision +0 -14
  145. ara_cli-0.1.9.69.dist-info/METADATA +0 -16
  146. ara_cli-0.1.9.69.dist-info/RECORD +0 -158
  147. tests/test_ara_autofix.py +0 -219
  148. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/WHEEL +0 -0
  149. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/entry_points.txt +0 -0
  150. {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/top_level.txt +0 -0
ara_cli/chat.py CHANGED
@@ -1,11 +1,43 @@
1
1
  import os
2
+ import argparse
2
3
  import cmd2
4
+
3
5
  from ara_cli.prompt_handler import send_prompt
4
6
 
7
+ from . import error_handler
8
+ from ara_cli.error_handler import AraError, AraConfigurationError
9
+
10
+ from ara_cli.file_loaders.document_file_loader import DocumentFileLoader
11
+ from ara_cli.file_loaders.binary_file_loader import BinaryFileLoader
12
+ from ara_cli.file_loaders.text_file_loader import TextFileLoader
13
+ from ara_cli.chat_agent.agent_process_manager import AgentProcessManager
14
+
15
+
16
+ extract_parser = argparse.ArgumentParser()
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
+ )
26
+
27
+ load_parser = argparse.ArgumentParser()
28
+ load_parser.add_argument("file_name", nargs="?",
29
+ default="", help="File to load")
30
+ load_parser.add_argument(
31
+ "--load-images",
32
+ action="store_true",
33
+ help="Extract and describe images from documents",
34
+ )
35
+
5
36
 
6
37
  class Chat(cmd2.Cmd):
7
38
  CATEGORY_CHAT_CONTROL = "Chat control commands"
8
39
  CATEGORY_LLM_CONTROL = "Language model controls"
40
+ CATEGORY_AGENT_CONTROL = "Agent control commands"
9
41
 
10
42
  INTRO = """/***************************************/
11
43
  araarar
@@ -29,35 +61,50 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
29
61
  ROLE_PROMPT = "ara prompt"
30
62
  ROLE_RESPONSE = "ara response"
31
63
 
64
+ # Available agents
65
+ AVAILABLE_AGENTS = [
66
+ "interview_agent",
67
+ "autocoder_v2_agent",
68
+ "question_and_answer_agent",
69
+ "feature_creation_guide_agent",
70
+ ]
71
+
32
72
  BINARY_TYPE_MAPPING = {
33
- ".png": "image/png",
34
- ".jpg": "image/jpeg",
35
- ".jpeg": "image/jpeg",
36
- }
73
+ ".png": "image/png",
74
+ ".jpg": "image/jpeg",
75
+ ".jpeg": "image/jpeg",
76
+ }
77
+
78
+ DOCUMENT_TYPE_EXTENSIONS = [".docx", ".doc", ".odt", ".pdf"]
37
79
 
38
80
  def __init__(
39
81
  self,
40
82
  chat_name: str,
41
83
  reset: bool | None = None,
42
- enable_commands: list[str] | None = None
84
+ enable_commands: list[str] | None = None,
43
85
  ):
86
+ from ara_cli.template_loader import TemplateLoader
87
+
44
88
  shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
45
89
  if enable_commands:
46
90
  enable_commands.append("quit") # always allow quitting
47
91
  enable_commands.append("eof") # always allow quitting with ctrl-D
48
92
  enable_commands.append("help") # always allow help
49
93
 
50
- shortcuts = {key: value for key, value in shortcuts.items() if value in enable_commands}
94
+ shortcuts = {
95
+ key: value
96
+ for key, value in shortcuts.items()
97
+ if value in enable_commands
98
+ }
51
99
 
52
- super().__init__(
53
- allow_cli_args=False,
54
- shortcuts=shortcuts
55
- )
100
+ super().__init__(allow_cli_args=False, shortcuts=shortcuts)
56
101
  self.create_default_aliases()
57
102
 
58
103
  if enable_commands:
59
104
  all_commands = self.get_all_commands()
60
- commands_to_disable = [command for command in all_commands if command not in enable_commands]
105
+ commands_to_disable = [
106
+ command for command in all_commands if command not in enable_commands
107
+ ]
61
108
  self.disable_commands(commands_to_disable)
62
109
 
63
110
  self.prompt = "ara> "
@@ -69,12 +116,18 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
69
116
  self.chat_history = []
70
117
  self.message_buffer = []
71
118
  self.config = self._retrieve_ara_config()
119
+ self.template_loader = TemplateLoader(chat_instance=self)
120
+
121
+ # Initialize agent process manager
122
+ self.agent_manager = AgentProcessManager(self)
72
123
 
73
124
  def disable_commands(self, commands: list[str]):
74
125
  for command in commands:
75
- setattr(self, f'do_{command}', self.default)
126
+ setattr(self, f"do_{command}", self.default)
76
127
  self.hidden_commands.append(command)
77
- aliases_to_remove = [alias for alias, cmd in self.aliases.items() if cmd in commands]
128
+ aliases_to_remove = [
129
+ alias for alias, cmd in self.aliases.items() if cmd in commands
130
+ ]
78
131
  for alias in aliases_to_remove:
79
132
  del self.aliases[alias]
80
133
 
@@ -95,6 +148,11 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
95
148
  self.aliases["lg"] = "LOAD_GIVENS"
96
149
  self.aliases["lb"] = "LOAD_BLUEPRINT"
97
150
  self.aliases["lt"] = "LOAD_TEMPLATE"
151
+ # Agent control aliases
152
+ self.aliases["a"] = "AGENT_RUN"
153
+ self.aliases["as"] = "AGENT_STOP"
154
+ self.aliases["ac"] = "AGENT_CONTINUE"
155
+ self.aliases["astat"] = "AGENT_STATUS"
98
156
 
99
157
  def setup_chat(self, chat_name, reset: bool = None):
100
158
  if os.path.exists(chat_name):
@@ -109,8 +167,10 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
109
167
  chat_file_short = os.path.split(chat_file)[-1]
110
168
 
111
169
  if reset is None:
112
- user_input = input(f"{chat_file_short} already exists. Do you want to reset the chat? (y/N): ")
113
- 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":
114
174
  self.create_empty_chat_file(chat_file)
115
175
  if reset:
116
176
  self.create_empty_chat_file(chat_file)
@@ -137,16 +197,14 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
137
197
  print(f"File {file_name} not found.")
138
198
  return False
139
199
  return method(self, file_path, *args, **kwargs)
200
+
140
201
  return wrapper
141
202
 
142
203
  @staticmethod
143
204
  def get_last_role_marker(lines):
144
205
  if not lines:
145
206
  return
146
- role_markers = [
147
- f"# {Chat.ROLE_PROMPT}:",
148
- f"# {Chat.ROLE_RESPONSE}"
149
- ]
207
+ role_markers = [f"# {Chat.ROLE_PROMPT}:", f"# {Chat.ROLE_RESPONSE}"]
150
208
  for line in reversed(lines):
151
209
  stripped_line = line.strip()
152
210
  if stripped_line.startswith(tuple(role_markers)):
@@ -154,7 +212,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
154
212
  return None
155
213
 
156
214
  def start_non_interactive(self):
157
- with open(self.chat_name, 'r') as file:
215
+ with open(self.chat_name, "r", encoding="utf-8") as file:
158
216
  content = file.read()
159
217
  print(content)
160
218
 
@@ -189,23 +247,19 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
189
247
  text_content = []
190
248
  image_data_list = []
191
249
 
192
- image_pattern = re.compile(r'\((data:image/[^;]+;base64,.*?)\)')
250
+ image_pattern = re.compile(r"\((data:image/[^;]+;base64,.*?)\)")
193
251
 
194
252
  for line in message.splitlines():
195
253
  match = image_pattern.search(line)
196
254
  if match:
197
- image_data = {"type": "image_url", "image_url": {"url": match.group(1)}}
255
+ image_data = {"type": "image_url",
256
+ "image_url": {"url": match.group(1)}}
198
257
  image_data_list.append(image_data)
199
258
  else:
200
259
  text_content.append(line)
201
260
 
202
- message_content = {
203
- "type": "text",
204
- "text": '\n'.join(text_content)}
205
- message = {
206
- "role": role,
207
- "content": [message_content]
208
- }
261
+ message_content = {"type": "text", "text": "\n".join(text_content)}
262
+ message = {"role": role, "content": [message_content]}
209
263
  message = append_images_to_message(message, image_data_list)
210
264
  return message
211
265
 
@@ -218,7 +272,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
218
272
 
219
273
  split_pattern = re.compile(f"({prompt_marker}|{response_marker})")
220
274
 
221
- parts = re.split(split_pattern, '\n'.join(self.chat_history))
275
+ parts = re.split(split_pattern, "\n".join(self.chat_history))
222
276
 
223
277
  all_prompts_and_responses = []
224
278
  current = ""
@@ -252,7 +306,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
252
306
  prompt_to_send = self.assemble_prompt()
253
307
  role_marker = f"# {Chat.ROLE_RESPONSE}:"
254
308
 
255
- with open(self.chat_name, 'a+', encoding='utf-8') as file:
309
+ with open(self.chat_name, "a+", encoding="utf-8") as file:
256
310
  last_line = self.get_last_line(file)
257
311
 
258
312
  print(role_marker)
@@ -275,24 +329,24 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
275
329
 
276
330
  def save_message(self, role: str, message: str):
277
331
  role_marker = f"# {role}:"
278
- with open(self.chat_name, 'r', encoding='utf-8') as file:
332
+ with open(self.chat_name, "r", encoding="utf-8") as file:
279
333
  stripped_line = self.get_last_non_empty_line(file)
280
334
  line_to_write = f"{message}\n\n"
281
335
  if stripped_line != role_marker:
282
336
  line_to_write = f"\n{role_marker}\n{message}\n"
283
337
 
284
- with open(self.chat_name, 'a', encoding='utf-8') as file:
338
+ with open(self.chat_name, "a", encoding="utf-8") as file:
285
339
  file.write(line_to_write)
286
340
  self.chat_history.append(line_to_write)
287
341
 
288
342
  def resend_message(self):
289
- with open(self.chat_name, 'r', encoding='utf-8') as file:
343
+ with open(self.chat_name, "r", encoding="utf-8") as file:
290
344
  lines = file.readlines()
291
345
  if not lines:
292
346
  return
293
347
  index_to_remove = self.find_last_reply_index(lines)
294
348
  if index_to_remove is not None:
295
- with open(self.chat_name, 'w', encoding='utf-8') as file:
349
+ with open(self.chat_name, "w", encoding="utf-8") as file:
296
350
  file.writelines(lines[:index_to_remove])
297
351
  self.send_message()
298
352
 
@@ -307,73 +361,93 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
307
361
  return index_to_remove
308
362
 
309
363
  def append_strings(self, strings: list[str]):
310
- output = '\n'.join(strings)
311
- with open(self.chat_name, 'a') as file:
312
- file.write(output + '\n')
364
+ output = "\n".join(strings)
365
+ with open(self.chat_name, "a") as file:
366
+ file.write(output + "\n")
313
367
 
314
368
  def load_chat_history(self, chat_file: str):
315
369
  chat_history = []
316
370
  if os.path.exists(chat_file):
317
- with open(chat_file, 'r', encoding='utf-8') as file:
371
+ with open(chat_file, "r", encoding="utf-8") as file:
318
372
  chat_history = file.readlines()
319
373
  return chat_history
320
374
 
321
375
  def create_empty_chat_file(self, chat_file: str):
322
- with open(chat_file, 'w', encoding='utf-8') as file:
376
+ with open(chat_file, "w", encoding="utf-8") as file:
323
377
  file.write(self.default_chat_content)
324
378
  self.chat_history = []
325
379
 
326
380
  def add_prompt_tag_if_needed(self, chat_file: str):
327
- with open(chat_file, 'r', encoding='utf-8') as file:
381
+ with open(chat_file, "r", encoding="utf-8") as file:
328
382
  lines = file.readlines()
383
+
329
384
  prompt_tag = f"# {Chat.ROLE_PROMPT}:"
330
385
  if Chat.get_last_role_marker(lines) == prompt_tag:
331
386
  return
332
387
  append = prompt_tag
333
388
  last_line = lines[-1].strip()
334
- if last_line != "" and last_line != '\n':
389
+ if last_line != "" and last_line != "\n":
335
390
  append = f"\n{append}"
336
- with open(chat_file, 'a', encoding='utf-8') as file:
391
+ with open(chat_file, "a", encoding="utf-8") as file:
337
392
  file.write(append)
338
393
 
339
- def determine_file_path(self, file_name: str):
340
- current_directory = os.path.dirname(self.chat_name)
341
- file_path = os.path.join(current_directory, file_name)
342
- if not os.path.exists(file_path):
343
- file_path = file_name
344
- if not os.path.exists(file_path):
345
- print(f"File {file_name} not found")
346
- return None
347
- return file_path
348
-
349
- @file_exists_check
350
- def load_text_file(self, file_path, prefix: str = "", suffix: str = "", block_delimiter: str = ""):
351
- with open(file_path, 'r', encoding='utf-8') as file:
352
- file_content = file.read()
353
- if block_delimiter:
354
- file_content = f"{block_delimiter}\n{file_content}\n{block_delimiter}"
355
- write_content = f"{prefix}{file_content}{suffix}\n"
356
-
357
- with open(self.chat_name, 'a', encoding='utf-8') as chat_file:
358
- chat_file.write(write_content)
359
- return True
394
+ def load_text_file(
395
+ self,
396
+ file_path,
397
+ prefix: str = "",
398
+ suffix: str = "",
399
+ block_delimiter: str = "",
400
+ extract_images: bool = False,
401
+ ):
402
+ loader = TextFileLoader(self)
403
+ return loader.load(
404
+ file_path,
405
+ prefix=prefix,
406
+ suffix=suffix,
407
+ block_delimiter=block_delimiter,
408
+ extract_images=extract_images,
409
+ )
360
410
 
361
- @file_exists_check
362
- def load_binary_file(self, file_path, mime_type: str, prefix: str = "", suffix: str = ""):
363
- import base64
411
+ def load_binary_file(
412
+ self, file_path, mime_type: str, prefix: str = "", suffix: str = ""
413
+ ):
414
+ loader = BinaryFileLoader(self)
415
+ return loader.load(file_path, mime_type=mime_type, prefix=prefix, suffix=suffix)
364
416
 
365
- with open(file_path, 'rb') as file:
366
- file_content = file.read()
367
- base64_image = base64.b64encode(file_content).decode("utf-8")
417
+ def read_markdown(self, file_path: str, extract_images: bool = False) -> str:
418
+ """Read markdown file and optionally extract/describe images"""
419
+ from ara_cli.file_loaders.text_file_loader import MarkdownReader
368
420
 
369
- write_content = f"{prefix}![{os.path.basename(file_path)}](data:{mime_type};base64,{base64_image}){suffix}\n"
421
+ reader = MarkdownReader(file_path)
422
+ return reader.read(extract_images=extract_images)
370
423
 
371
- with open(self.chat_name, 'a', encoding='utf-8') as chat_file:
372
- chat_file.write(write_content)
373
- return True
424
+ def load_document_file(
425
+ self,
426
+ file_path: str,
427
+ prefix: str = "",
428
+ suffix: str = "",
429
+ block_delimiter: str = "```",
430
+ extract_images: bool = False,
431
+ ):
432
+ loader = DocumentFileLoader(self)
433
+ return loader.load(
434
+ file_path,
435
+ prefix=prefix,
436
+ suffix=suffix,
437
+ block_delimiter=block_delimiter,
438
+ extract_images=extract_images,
439
+ )
374
440
 
375
- def load_file(self, file_name: str, prefix: str = "", suffix: str = "", block_delimiter: str = ""):
441
+ def load_file(
442
+ self,
443
+ file_name: str,
444
+ prefix: str = "",
445
+ suffix: str = "",
446
+ block_delimiter: str = "",
447
+ extract_images: bool = False,
448
+ ):
376
449
  binary_type_mapping = Chat.BINARY_TYPE_MAPPING
450
+ document_type_extensions = Chat.DOCUMENT_TYPE_EXTENSIONS
377
451
 
378
452
  file_type = None
379
453
  file_name_lower = file_name.lower()
@@ -382,19 +456,29 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
382
456
  file_type = mime_type
383
457
  break
384
458
 
385
- if file_type:
386
- return self.load_binary_file(
387
- file_name=file_name,
388
- mime_type=file_type,
459
+ is_file_document = any(
460
+ file_name_lower.endswith(ext) for ext in document_type_extensions
461
+ )
462
+
463
+ if is_file_document:
464
+ return self.load_document_file(
465
+ file_path=file_name,
389
466
  prefix=prefix,
390
- suffix=suffix
467
+ suffix=suffix,
468
+ block_delimiter=block_delimiter,
469
+ extract_images=extract_images,
470
+ )
471
+ elif file_type:
472
+ return self.load_binary_file(
473
+ file_path=file_name, mime_type=file_type, prefix=prefix, suffix=suffix
391
474
  )
392
475
  else:
393
476
  return self.load_text_file(
394
- file_name=file_name,
477
+ file_path=file_name,
395
478
  prefix=prefix,
396
479
  suffix=suffix,
397
- block_delimiter=block_delimiter
480
+ block_delimiter=block_delimiter,
481
+ extract_images=extract_images,
398
482
  )
399
483
 
400
484
  def choose_file_to_load(self, files: list[str], pattern: str):
@@ -406,11 +490,14 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
406
490
  try:
407
491
  choice_index = int(choice) - 1
408
492
  if choice_index < 0 or choice_index >= len(files):
409
- print("Invalid choice. Aborting load.")
493
+ error_handler.report_error(
494
+ ValueError("Invalid choice. Aborting load.")
495
+ )
410
496
  return None
411
497
  file_path = files[choice_index]
412
- except ValueError:
413
- print("Invalid input. Aborting load.")
498
+ except ValueError as e:
499
+ error_handler.report_error(
500
+ ValueError("Invalid input. Aborting load."))
414
501
  return None
415
502
  else:
416
503
  file_path = files[0]
@@ -419,26 +506,53 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
419
506
  def _help_menu(self, verbose: bool = False):
420
507
  super()._help_menu(verbose)
421
508
  if self.aliases:
422
- aliases = [f"{alias} -> {command}" for alias, command in self.aliases.items()]
509
+ aliases = [
510
+ f"{alias} -> {command}" for alias, command in self.aliases.items()
511
+ ]
423
512
  self._print_topics("Aliases", aliases, verbose)
424
513
 
425
514
  def do_quit(self, _):
426
515
  """Exit ara-cli"""
516
+ self.agent_manager.cleanup_agent_process()
427
517
  print("Chat ended")
428
518
  self.last_result = True
429
519
  return True
430
520
 
521
+ def onecmd(self, *args, **kwargs):
522
+ try:
523
+ return super().onecmd(*args, **kwargs)
524
+ except Exception as e:
525
+ error_handler.report_error(e)
526
+ return False
527
+
431
528
  def onecmd_plus_hooks(self, line, orig_rl_history_length):
432
529
  # store the full line for use with default()
433
530
  self.full_input = line
434
- return super().onecmd_plus_hooks(line, orig_rl_history_length=orig_rl_history_length)
531
+ return super().onecmd_plus_hooks(
532
+ line, orig_rl_history_length=orig_rl_history_length
533
+ )
435
534
 
436
535
  def default(self, line):
437
- self.message_buffer.append(self.full_input)
536
+ if self.agent_manager.agent_mode:
537
+ if self.full_input.lower() in ["quit", "exit"]:
538
+ self.agent_manager.cleanup_agent_process()
539
+ print("Agent stopped.")
540
+ else:
541
+ # In agent mode, send input directly to agent
542
+ self.agent_manager.send_to_agent(self.full_input)
543
+ else:
544
+ # In normal chat mode, buffer the message
545
+ self.message_buffer.append(self.full_input)
438
546
 
439
547
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
440
- def do_LOAD(self, file_name):
441
- """Load a file and append its contents 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"""
548
+ @cmd2.with_argparser(load_parser)
549
+ def do_LOAD(self, args):
550
+ """Load a file and append its contents 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. Use --load-images flag to extract and describe images from documents."""
551
+ from ara_cli.commands.load_command import LoadCommand
552
+
553
+ file_name = args.file_name
554
+ load_images = args.load_images
555
+
442
556
  matching_files = self.find_matching_files_to_load(file_name)
443
557
  if not matching_files:
444
558
  return
@@ -447,16 +561,26 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
447
561
  block_delimiter = "```"
448
562
  prefix = f"\nFile: {file_path}\n"
449
563
  self.add_prompt_tag_if_needed(self.chat_name)
450
- if not os.path.isdir(file_path) and self.load_file(file_path, prefix=prefix, block_delimiter=block_delimiter):
451
- print(f"Loaded contents of file {file_path}")
564
+
565
+ if not os.path.isdir(file_path):
566
+ command = LoadCommand(
567
+ chat_instance=self,
568
+ file_path=file_path,
569
+ prefix=prefix,
570
+ block_delimiter=block_delimiter,
571
+ extract_images=load_images,
572
+ output=self.poutput,
573
+ )
574
+ command.execute()
452
575
 
453
576
  def complete_LOAD(self, text, line, begidx, endidx):
454
577
  import glob
455
578
 
456
- return [x for x in glob.glob(text + '*')]
579
+ return [x for x in glob.glob(text + "*")]
457
580
 
458
581
  def _retrieve_ara_config(self):
459
582
  from ara_cli.prompt_handler import ConfigManager
583
+
460
584
  return ConfigManager().get_config()
461
585
 
462
586
  def _retrieve_llm_config(self):
@@ -472,7 +596,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
472
596
  file_pattern = os.path.join(os.path.dirname(self.chat_name), file_name)
473
597
  matching_files = glob.glob(file_pattern)
474
598
  if not matching_files:
475
- print(f"No files matching pattern {file_name} found.")
599
+ error_handler.report_error(
600
+ AraError(f"No files matching pattern '{file_name}' found.")
601
+ )
476
602
  return
477
603
  return matching_files
478
604
 
@@ -488,24 +614,30 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
488
614
 
489
615
  if file_type:
490
616
  return self.load_binary_file(
491
- file_name=file_name,
492
- mime_type=file_type,
493
- prefix=prefix,
494
- suffix=suffix
617
+ file_path=file_name, mime_type=file_type, prefix=prefix, suffix=suffix
495
618
  )
496
- print(f"File {file_name} not recognized as image, could not load")
619
+ error_handler.report_error(
620
+ AraError(
621
+ f"File {file_name} not recognized as image, could not load")
622
+ )
497
623
 
498
624
  def _verify_llm_choice(self, model_name):
499
625
  llm_config = self._retrieve_llm_config()
500
626
  models = [name for name in llm_config.keys()]
501
627
  if model_name not in models:
502
- print(f"Model {model_name} unavailable. Retrieve the list of available models using the LIST_MODELS command.")
628
+ error_handler.report_error(
629
+ AraConfigurationError(
630
+ f"Model {model_name} unavailable. Retrieve the list of available models using the LIST_MODELS command."
631
+ )
632
+ )
503
633
  return False
504
634
  return True
505
635
 
506
636
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
507
637
  def do_LOAD_IMAGE(self, file_name):
508
638
  """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"""
639
+ from ara_cli.commands.load_image_command import LoadImageCommand
640
+
509
641
  matching_files = self.find_matching_files_to_load(file_name)
510
642
  if not matching_files:
511
643
  return
@@ -513,8 +645,31 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
513
645
  for file_path in matching_files:
514
646
  prefix = f"\nFile: {file_path}\n"
515
647
  self.add_prompt_tag_if_needed(self.chat_name)
516
- if not os.path.isdir(file_path) and self.load_image(file_path, prefix=prefix):
517
- print(f"Loaded image file {file_path}")
648
+
649
+ if not os.path.isdir(file_path):
650
+ # Determine mime type
651
+ file_type = None
652
+ file_path_lower = file_path.lower()
653
+ for extension, mime_type in Chat.BINARY_TYPE_MAPPING.items():
654
+ if file_path_lower.endswith(extension):
655
+ file_type = mime_type
656
+ break
657
+
658
+ if file_type:
659
+ command = LoadImageCommand(
660
+ chat_instance=self,
661
+ file_path=file_path,
662
+ mime_type=file_type,
663
+ prefix=prefix,
664
+ output=self.poutput,
665
+ )
666
+ command.execute()
667
+ else:
668
+ error_handler.report_error(
669
+ AraError(
670
+ f"File {file_path} not recognized as image, could not load"
671
+ )
672
+ )
518
673
 
519
674
  @cmd2.with_category(CATEGORY_LLM_CONTROL)
520
675
  def do_LIST_MODELS(self, _):
@@ -533,14 +688,38 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
533
688
  original_dir = os.getcwd()
534
689
  navigator = DirectoryNavigator()
535
690
  navigator.navigate_to_target()
536
- os.chdir('..')
691
+ os.chdir("..")
537
692
 
538
693
  if not self._verify_llm_choice(model_name):
539
694
  return
695
+
540
696
  self.config.default_llm = model_name
541
697
  save_data(filepath=DEFAULT_CONFIG_LOCATION, config=self.config)
542
698
 
543
- LLMSingleton.set_model(model_name)
699
+ LLMSingleton.set_default_model(model_name)
700
+ print(f"Language model switched to '{model_name}'")
701
+
702
+ os.chdir(original_dir)
703
+
704
+ @cmd2.with_category(CATEGORY_LLM_CONTROL)
705
+ def do_CHOOSE_EXTRACTION_MODEL(self, model_name):
706
+ from ara_cli.prompt_handler import LLMSingleton
707
+ from ara_cli.ara_config import DEFAULT_CONFIG_LOCATION, save_data
708
+ from ara_cli.directory_navigator import DirectoryNavigator
709
+
710
+ original_dir = os.getcwd()
711
+ navigator = DirectoryNavigator()
712
+ navigator.navigate_to_target()
713
+ os.chdir("..")
714
+
715
+ if not self._verify_llm_choice(model_name):
716
+ return
717
+
718
+ self.config.extraction_llm = model_name
719
+ save_data(filepath=DEFAULT_CONFIG_LOCATION, config=self.config)
720
+
721
+ LLMSingleton.set_extraction_model(model_name)
722
+ print(f"Extraction model switched to '{model_name}'")
544
723
 
545
724
  os.chdir(original_dir)
546
725
 
@@ -548,7 +727,14 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
548
727
  def do_CURRENT_MODEL(self, _):
549
728
  from ara_cli.prompt_handler import LLMSingleton
550
729
 
551
- print(LLMSingleton.get_model())
730
+ print(LLMSingleton.get_default_model())
731
+
732
+ @cmd2.with_category(CATEGORY_LLM_CONTROL)
733
+ def do_CURRENT_EXTRACTION_MODEL(self, _):
734
+ """Displays the current extraction language model."""
735
+ from ara_cli.prompt_handler import LLMSingleton
736
+
737
+ print(LLMSingleton.get_extraction_model())
552
738
 
553
739
  def _complete_llms(self, text, line, begidx, endidx):
554
740
  llm_config = self._retrieve_llm_config()
@@ -564,6 +750,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
564
750
  def complete_CHOOSE_MODEL(self, text, line, begidx, endidx):
565
751
  return self._complete_llms(text, line, begidx, endidx)
566
752
 
753
+ def complete_CHOOSE_EXTRACTION_MODEL(self, text, line, begidx, endidx):
754
+ return self._complete_llms(text, line, begidx, endidx)
755
+
567
756
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
568
757
  def do_NEW(self, chat_name):
569
758
  """Create a new chat. Optionally provide a chat name in-line: NEW new_chat"""
@@ -582,36 +771,51 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
582
771
  def do_CLEAR(self, _):
583
772
  """Clear the chat and the file containing it"""
584
773
  user_input = input("Are you sure you want to clear the chat? (y/N): ")
585
- if user_input.lower() != 'y':
774
+ if user_input.lower() != "y":
586
775
  return
587
776
  self.create_empty_chat_file(self.chat_name)
588
777
  self.chat_history = self.load_chat_history(self.chat_name)
778
+ self.message_buffer.clear()
589
779
  print(f"Cleared content of {self.chat_name}")
590
780
 
591
781
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
592
782
  def do_LOAD_RULES(self, rules_name):
593
783
  """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"""
594
- self._load_template_helper(rules_name, "rules", "*.rules.md")
784
+ self.template_loader.load_template(
785
+ rules_name, "rules", self.chat_name, "*.rules.md"
786
+ )
595
787
 
596
788
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
597
789
  def do_LOAD_INTENTION(self, intention_name):
598
790
  """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"""
599
- self._load_template_helper(intention_name, "intention", "*.intention.md")
791
+ self.template_loader.load_template(
792
+ intention_name, "intention", self.chat_name, "*.intention.md"
793
+ )
600
794
 
601
795
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
602
796
  def do_LOAD_COMMANDS(self, commands_name):
603
797
  """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"""
604
- self._load_template_helper(commands_name, "commands", "*.commands.md")
798
+ self.template_loader.load_template(
799
+ commands_name, "commands", self.chat_name, "*.commands.md"
800
+ )
605
801
 
606
802
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
607
803
  def do_LOAD_BLUEPRINT(self, blueprint_name):
608
804
  """Load specified blueprint. Specify global/<blueprint_name> to access globally defined blueprints"""
609
- self._load_template_from_global_or_local(blueprint_name, "blueprint")
805
+ self.template_loader.load_template(
806
+ blueprint_name, "blueprint", self.chat_name)
610
807
 
611
- def _load_helper(self, directory: str, pattern: str, file_type: str, exclude_pattern: str | None = None):
808
+ def _load_helper(
809
+ self,
810
+ directory: str,
811
+ pattern: str,
812
+ file_type: str,
813
+ exclude_pattern: str | None = None,
814
+ ):
612
815
  import glob
613
816
 
614
- directory_path = os.path.join(os.path.dirname(self.chat_name), directory)
817
+ directory_path = os.path.join(
818
+ os.path.dirname(self.chat_name), directory)
615
819
  file_pattern = os.path.join(directory_path, pattern)
616
820
 
617
821
  exclude_files = []
@@ -621,7 +825,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
621
825
  matching_files = list(set(matching_files) - set(exclude_files))
622
826
 
623
827
  if not matching_files:
624
- print(f"No {file_type} file found.")
828
+ error_handler.report_error(AraError(f"No {file_type} file found."))
625
829
  return
626
830
 
627
831
  file_path = self.choose_file_to_load(matching_files, pattern)
@@ -638,10 +842,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
638
842
  from ara_cli.ara_config import ConfigManager
639
843
  from ara_cli.directory_navigator import DirectoryNavigator
640
844
 
641
- plurals = {
642
- "commands": "commands",
643
- "rules": "rules"
644
- }
845
+ plurals = {"commands": "commands", "rules": "rules"}
645
846
 
646
847
  plural = f"{template_type}s"
647
848
  if template_type in plurals:
@@ -649,7 +850,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
649
850
 
650
851
  if template_name.startswith("global/"):
651
852
  directory = f"{TemplatePathManager.get_template_base_path()}/prompt-modules/{plural}/"
652
- self._load_helper(directory, template_name.removeprefix("global/"), template_type)
853
+ self._load_helper(
854
+ directory, template_name.removeprefix("global/"), template_type
855
+ )
653
856
  return
654
857
 
655
858
  ara_config = ConfigManager.get_config()
@@ -663,7 +866,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
663
866
  os.chdir(original_directory)
664
867
 
665
868
  custom_prompt_templates_subdir = self.config.custom_prompt_templates_subdir
666
- template_directory = f"{local_templates_path}/{custom_prompt_templates_subdir}/{plural}"
869
+ template_directory = (
870
+ f"{local_templates_path}/{custom_prompt_templates_subdir}/{plural}"
871
+ )
667
872
  self._load_helper(template_directory, template_name, template_type)
668
873
 
669
874
  def _load_template_helper(self, template_name, template_type, default_pattern):
@@ -671,55 +876,100 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
671
876
  self._load_helper("prompt.data", default_pattern, template_type)
672
877
  return
673
878
 
674
- self._load_template_from_global_or_local(template_name=template_name, template_type=template_type)
879
+ self._load_template_from_global_or_local(
880
+ template_name=template_name, template_type=template_type
881
+ )
675
882
 
676
883
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
677
- def do_EXTRACT(self, _):
678
- """Search for markdown code blocks containing \"# [x] extract\" as first line and \"# filename: <path/filename>\" as second line and copy the content of the code block to the specified file. The extracted code block is then marked with \"# [v] extract\""""
679
- from ara_cli.prompt_extractor import extract_responses
884
+ @cmd2.with_argparser(extract_parser)
885
+ def do_EXTRACT(self, args):
886
+ """Search for markdown code blocks containing "# [x] extract" as first line and "# filename: <path/filename>" as second line and copy the content of the code block to the specified file. The extracted code block is then marked with "# [v] extract"."""
887
+ from ara_cli.commands.extract_command import ExtractCommand
888
+
889
+ command = ExtractCommand(
890
+ file_name=self.chat_name,
891
+ force=args.force,
892
+ write=args.write,
893
+ output=self.poutput,
894
+ )
895
+ command.execute()
896
+
897
+ def _find_givens_files(self, file_name: str) -> list[str]:
898
+ """
899
+ Finds the givens files to be processed.
900
+ - If file_name is provided, it resolves that path.
901
+ - Otherwise, it looks for default givens files.
902
+ - If no defaults are found, it prompts the user.
903
+ Returns a list of absolute file paths or an empty list if none are found.
904
+ """
905
+ base_directory = os.path.dirname(self.chat_name)
680
906
 
681
- extract_responses(self.chat_name, True)
682
- print("End of extraction")
907
+ def resolve_path(name):
908
+ """Inner helper to resolve a path relative to chat, then absolute."""
909
+ relative_path = os.path.join(base_directory, name)
910
+ if os.path.exists(relative_path):
911
+ return relative_path
912
+ if os.path.exists(name):
913
+ return name
914
+ return None
915
+
916
+ if file_name:
917
+ path = resolve_path(file_name)
918
+ if path:
919
+ return [path]
920
+ relative_path_for_error = os.path.join(base_directory, file_name)
921
+ error_handler.report_error(
922
+ AraError,
923
+ f"No givens file found at {relative_path_for_error} or {file_name}",
924
+ )
925
+ return []
926
+
927
+ # If no file_name, check for defaults
928
+ default_files_to_check = [
929
+ os.path.join(base_directory, "prompt.data",
930
+ "config.prompt_givens.md"),
931
+ os.path.join(
932
+ base_directory, "prompt.data", "config.prompt_global_givens.md"
933
+ ),
934
+ ]
935
+ existing_defaults = [
936
+ f for f in default_files_to_check if os.path.exists(f)]
937
+ if existing_defaults:
938
+ return existing_defaults
939
+
940
+ # No defaults found, prompt user
941
+ user_input = input("Please specify a givens file: ")
942
+ if not user_input:
943
+ self.poutput("Aborting.")
944
+ return []
945
+
946
+ path = resolve_path(user_input)
947
+ if path:
948
+ return [path]
949
+ error_handler.report_error(
950
+ AraError(f"No givens file found at {user_input}. Aborting.")
951
+ )
952
+ return []
683
953
 
684
954
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
685
955
  def do_LOAD_GIVENS(self, file_name):
686
- """Load all files listed in a ./prompt.data/config.prompt_givens.md"""
687
- from ara_cli.directory_navigator import DirectoryNavigator
956
+ """Load all files listed in a ./prompt.data/config.prompt_givens.md and ./prompt.data/config.prompt_global_givens.md"""
688
957
  from ara_cli.prompt_handler import load_givens
689
958
 
690
- base_directory = os.path.dirname(self.chat_name)
691
-
692
- if file_name == "":
693
- file_name = f"{base_directory}/prompt.data/config.prompt_givens.md"
694
-
695
- # Check the relative path first
696
- relative_givens_path = os.path.join(base_directory, file_name)
697
- if os.path.exists(relative_givens_path):
698
- givens_path = relative_givens_path
699
- elif os.path.exists(file_name): # Check the absolute path
700
- givens_path = file_name
701
- else:
702
- print(f"No givens file found at {relative_givens_path} or {file_name}")
703
- user_input = input("Please specify a givens file: ")
704
- if os.path.exists(os.path.join(base_directory, user_input)):
705
- givens_path = os.path.join(base_directory, user_input)
706
- elif os.path.exists(user_input):
707
- givens_path = user_input
708
- else:
709
- print(f"No givens file found at {user_input}. Aborting.")
710
- return
959
+ givens_files_to_process = self._find_givens_files(file_name)
960
+ if not givens_files_to_process:
961
+ error_handler.report_error(AraError("No givens files to load."))
962
+ return
711
963
 
712
- cwd = os.getcwd()
713
- navigator = DirectoryNavigator()
714
- navigator.navigate_to_target()
715
- os.chdir('..')
716
- content, image_data = load_givens(givens_path)
717
- os.chdir(cwd)
964
+ for givens_path in givens_files_to_process:
965
+ # The givens_path is absolute, and load_givens reconstructs absolute paths
966
+ # from the markdown file. No directory change is needed.
967
+ content, _ = load_givens(givens_path)
718
968
 
719
- with open(self.chat_name, 'a', encoding='utf-8') as chat_file:
720
- chat_file.write(content)
969
+ with open(self.chat_name, "a", encoding="utf-8") as chat_file:
970
+ chat_file.write(content)
721
971
 
722
- print(f"Loaded files listed and marked in {givens_path}")
972
+ self.poutput(f"Loaded files listed and marked in {givens_path}")
723
973
 
724
974
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
725
975
  def do_SEND(self, _):
@@ -731,9 +981,219 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
731
981
  @cmd2.with_category(CATEGORY_CHAT_CONTROL)
732
982
  def do_LOAD_TEMPLATE(self, template_name):
733
983
  """Load artefact template"""
734
- directory = os.path.join(os.path.dirname(__file__), 'templates')
735
- pattern = f"template.{template_name}"
736
- file_type = "template"
737
- exclude_pattern = os.path.join(directory, "template.*.prompt_log.md")
984
+ from ara_cli.artefact_models.artefact_templates import template_artefact_of_type
985
+
986
+ artefact = template_artefact_of_type("".join(template_name))
987
+ if not artefact:
988
+ error_handler.report_error(
989
+ ValueError(f"No template for '{template_name}' found.")
990
+ )
991
+ return
992
+ write_content = artefact.serialize()
993
+ self.add_prompt_tag_if_needed(self.chat_name)
994
+ with open(self.chat_name, "a", encoding="utf-8") as chat_file:
995
+ chat_file.write(write_content)
996
+ print(f"Loaded {template_name} artefact template")
997
+
998
+ def complete_LOAD_TEMPLATE(self, text, line, begidx, endidx):
999
+ return self._complete_classifiers(text, line, begidx, endidx)
1000
+
1001
+ def _complete_classifiers(self, text, line, begidx, endidx):
1002
+ from ara_cli.classifier import Classifier
1003
+
1004
+ classifiers = Classifier.ordered_classifiers()
1005
+ if not text:
1006
+ completions = classifiers
1007
+ else:
1008
+ completions = [
1009
+ classifier for classifier in classifiers if classifier.startswith(text)
1010
+ ]
1011
+
1012
+ return completions
1013
+
1014
+ def _get_plural_template_type(self, template_type: str) -> str:
1015
+ """Determines the plural form of a template type."""
1016
+ plurals = {"commands": "commands", "rules": "rules"}
1017
+ return plurals.get(template_type, f"{template_type}s")
1018
+
1019
+ def _find_project_root(self) -> str | None:
1020
+ """
1021
+ Finds the project root by searching for an 'ara' directory,
1022
+ starting from the chat file's directory and moving upwards.
1023
+ """
1024
+ current_dir = os.path.dirname(self.chat_name)
1025
+ while True:
1026
+ if os.path.isdir(os.path.join(current_dir, "ara")):
1027
+ return current_dir
1028
+ parent_dir = os.path.dirname(current_dir)
1029
+ if parent_dir == current_dir: # Reached the filesystem root
1030
+ return None
1031
+ current_dir = parent_dir
1032
+
1033
+ def _gather_templates_from_path(
1034
+ self, search_path: str, templates_set: set, prefix: str = ""
1035
+ ):
1036
+ """
1037
+ Scans a given path for items and adds them to the provided set,
1038
+ optionally prepending a prefix.
1039
+ """
1040
+ import glob
1041
+
1042
+ if not os.path.isdir(search_path):
1043
+ return
1044
+ for path in glob.glob(os.path.join(search_path, "*")):
1045
+ templates_set.add(f"{prefix}{os.path.basename(path)}")
1046
+
1047
+ def _get_available_templates(self, template_type: str) -> list[str]:
1048
+ """
1049
+ Scans for available global and project-local custom templates.
1050
+ This method safely searches for template files without changing the
1051
+ current directory, making it safe for use in autocompleters.
1052
+
1053
+ Args:
1054
+ template_type: The type of template to search for (e.g., 'rules').
1055
+
1056
+ Returns:
1057
+ A sorted list of unique template names. Global templates are
1058
+ prefixed with 'global/'.
1059
+ """
1060
+ from ara_cli.template_manager import TemplatePathManager
1061
+
1062
+ plural_type = self._get_plural_template_type(template_type)
1063
+ templates = set()
1064
+
1065
+ # 1. Find Global Templates
1066
+ try:
1067
+ global_base_path = TemplatePathManager.get_template_base_path()
1068
+ global_template_dir = os.path.join(
1069
+ global_base_path, "prompt-modules", plural_type
1070
+ )
1071
+ self._gather_templates_from_path(
1072
+ global_template_dir, templates, prefix="global/"
1073
+ )
1074
+ except Exception:
1075
+ pass # Silently ignore if global templates are not found
1076
+
1077
+ # 2. Find Local Custom Templates
1078
+ try:
1079
+ project_root = self._find_project_root()
1080
+ if project_root:
1081
+ local_templates_base = os.path.join(
1082
+ project_root, self.config.local_prompt_templates_dir
1083
+ )
1084
+ custom_dir = os.path.join(
1085
+ local_templates_base,
1086
+ self.config.custom_prompt_templates_subdir,
1087
+ plural_type,
1088
+ )
1089
+ self._gather_templates_from_path(custom_dir, templates)
1090
+ except Exception:
1091
+ pass # Silently ignore if local templates cannot be resolved
1092
+
1093
+ return sorted(list(templates))
1094
+
1095
+ def _template_completer(self, text: str, template_type: str) -> list[str]:
1096
+ """Generic completer for different template types."""
1097
+ available_templates = self.template_loader.get_available_templates(
1098
+ template_type, os.path.dirname(self.chat_name)
1099
+ )
1100
+ if not text:
1101
+ return available_templates
1102
+ return [t for t in available_templates if t.startswith(text)]
1103
+
1104
+ def complete_LOAD_RULES(self, text, line, begidx, endidx):
1105
+ """Completer for the LOAD_RULES command."""
1106
+ return self._template_completer(text, "rules")
1107
+
1108
+ def complete_LOAD_INTENTION(self, text, line, begidx, endidx):
1109
+ """Completer for the LOAD_INTENTION command."""
1110
+ return self._template_completer(text, "intention")
1111
+
1112
+ def complete_LOAD_COMMANDS(self, text, line, begidx, endidx):
1113
+ """Completer for the LOAD_COMMANDS command."""
1114
+ return self._template_completer(text, "commands")
1115
+
1116
+ def complete_LOAD_BLUEPRINT(self, text, line, begidx, endidx):
1117
+ """Completer for the LOAD_BLUEPRINT command."""
1118
+ return self._template_completer(text, "blueprint")
1119
+
1120
+ # ===== AGENT CONTROL COMMANDS =====
1121
+
1122
+ @cmd2.with_category(CATEGORY_AGENT_CONTROL)
1123
+ def do_AGENT_RUN(self, args):
1124
+ """Run an agent. Usage: AGENT_RUN <agent_name> [artefact_classifier artefact_name]
1125
+
1126
+ Examples:
1127
+ AGENT_RUN interview_agent
1128
+ AGENT_RUN interview_agent feature my_feature
1129
+ """
1130
+ parts = args.split()
1131
+ if not parts:
1132
+ print(f"Available agents: {', '.join(self.AVAILABLE_AGENTS)}")
1133
+ print(
1134
+ "Usage: AGENT_RUN <agent_name> [artefact_classifier artefact_name]")
1135
+ return
1136
+
1137
+ agent_name = parts[0]
1138
+ if agent_name not in self.AVAILABLE_AGENTS:
1139
+ print(f"Unknown agent: {agent_name}")
1140
+ print(f"Available agents: {', '.join(self.AVAILABLE_AGENTS)}")
1141
+ return
1142
+
1143
+ # Get initial prompt
1144
+ initial_prompt = input(
1145
+ "Enter initial prompt (or press Enter for default): "
1146
+ ).strip()
1147
+ if not initial_prompt:
1148
+ initial_prompt = "Let's begin the interview."
1149
+
1150
+ # Extract artefact info if provided
1151
+ artefact_classifier = None
1152
+ artefact_name = None
1153
+ if len(parts) >= 3:
1154
+ artefact_classifier = parts[1]
1155
+ artefact_name = parts[2]
1156
+
1157
+ try:
1158
+ self.agent_manager.start_agent(
1159
+ agent_name, initial_prompt, artefact_classifier, artefact_name
1160
+ )
1161
+ except AraError as e:
1162
+ print(f"Error: {e}")
1163
+
1164
+ @cmd2.with_category(CATEGORY_AGENT_CONTROL)
1165
+ def do_AGENT_STOP(self, _):
1166
+ """Stop the currently running agent gracefully (sends termination signal)."""
1167
+ if not self.agent_manager.agent_process:
1168
+ print("No agent is currently running.")
1169
+ return
1170
+
1171
+ print(f"Stopping agent {self.agent_manager.agent_name}...")
1172
+ self.agent_manager.cleanup_agent_process()
1173
+ print("Agent stopped.")
1174
+
1175
+ @cmd2.with_category(CATEGORY_AGENT_CONTROL)
1176
+ def do_AGENT_CONTINUE(self, _):
1177
+ """Continue with the agent without providing new input (sends empty line)."""
1178
+ self.agent_manager.continue_agent()
1179
+
1180
+ @cmd2.with_category(CATEGORY_AGENT_CONTROL)
1181
+ def do_AGENT_STATUS(self, _):
1182
+ """Show status of the current agent."""
1183
+ print(self.agent_manager.get_agent_status())
1184
+
1185
+ def complete_AGENT_RUN(self, text, line, begidx, endidx):
1186
+ """Completer for AGENT_RUN command."""
1187
+ parts = line.split()
1188
+
1189
+ # Complete agent name
1190
+ if len(parts) <= 2:
1191
+ if not text:
1192
+ return self.AVAILABLE_AGENTS
1193
+ return [a for a in self.AVAILABLE_AGENTS if a.startswith(text)]
1194
+
1195
+ # Complete classifier
1196
+ if len(parts) == 3:
1197
+ return self._complete_classifiers(text, line, begidx, endidx)
738
1198
 
739
- self._load_helper(directory, pattern, file_type, exclude_pattern)
1199
+ return []