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.
- ara_cli/__init__.py +18 -2
- ara_cli/__main__.py +248 -62
- ara_cli/ara_command_action.py +155 -86
- ara_cli/ara_config.py +226 -80
- ara_cli/ara_subcommands/__init__.py +0 -0
- ara_cli/ara_subcommands/autofix.py +26 -0
- ara_cli/ara_subcommands/chat.py +27 -0
- ara_cli/ara_subcommands/classifier_directory.py +16 -0
- ara_cli/ara_subcommands/common.py +100 -0
- ara_cli/ara_subcommands/create.py +75 -0
- ara_cli/ara_subcommands/delete.py +22 -0
- ara_cli/ara_subcommands/extract.py +22 -0
- ara_cli/ara_subcommands/fetch_templates.py +14 -0
- ara_cli/ara_subcommands/list.py +65 -0
- ara_cli/ara_subcommands/list_tags.py +25 -0
- ara_cli/ara_subcommands/load.py +48 -0
- ara_cli/ara_subcommands/prompt.py +136 -0
- ara_cli/ara_subcommands/read.py +47 -0
- ara_cli/ara_subcommands/read_status.py +20 -0
- ara_cli/ara_subcommands/read_user.py +20 -0
- ara_cli/ara_subcommands/reconnect.py +27 -0
- ara_cli/ara_subcommands/rename.py +22 -0
- ara_cli/ara_subcommands/scan.py +14 -0
- ara_cli/ara_subcommands/set_status.py +22 -0
- ara_cli/ara_subcommands/set_user.py +22 -0
- ara_cli/ara_subcommands/template.py +16 -0
- ara_cli/artefact_autofix.py +649 -68
- ara_cli/artefact_creator.py +8 -11
- ara_cli/artefact_deleter.py +2 -4
- ara_cli/artefact_fuzzy_search.py +22 -10
- ara_cli/artefact_link_updater.py +4 -4
- ara_cli/artefact_lister.py +29 -55
- ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
- ara_cli/artefact_models/artefact_load.py +11 -3
- ara_cli/artefact_models/artefact_model.py +146 -39
- ara_cli/artefact_models/artefact_templates.py +70 -44
- ara_cli/artefact_models/businessgoal_artefact_model.py +23 -25
- ara_cli/artefact_models/epic_artefact_model.py +34 -26
- ara_cli/artefact_models/feature_artefact_model.py +203 -64
- ara_cli/artefact_models/keyfeature_artefact_model.py +21 -24
- ara_cli/artefact_models/serialize_helper.py +1 -1
- ara_cli/artefact_models/task_artefact_model.py +83 -15
- ara_cli/artefact_models/userstory_artefact_model.py +37 -27
- ara_cli/artefact_models/vision_artefact_model.py +23 -42
- ara_cli/artefact_reader.py +92 -91
- ara_cli/artefact_renamer.py +8 -4
- ara_cli/artefact_scan.py +66 -3
- ara_cli/chat.py +622 -162
- ara_cli/chat_agent/__init__.py +0 -0
- ara_cli/chat_agent/agent_communicator.py +62 -0
- ara_cli/chat_agent/agent_process_manager.py +211 -0
- ara_cli/chat_agent/agent_status_manager.py +73 -0
- ara_cli/chat_agent/agent_workspace_manager.py +76 -0
- ara_cli/commands/__init__.py +0 -0
- ara_cli/commands/command.py +7 -0
- ara_cli/commands/extract_command.py +15 -0
- ara_cli/commands/load_command.py +65 -0
- ara_cli/commands/load_image_command.py +34 -0
- ara_cli/commands/read_command.py +117 -0
- ara_cli/completers.py +144 -0
- ara_cli/directory_navigator.py +37 -4
- ara_cli/error_handler.py +134 -0
- ara_cli/file_classifier.py +6 -5
- ara_cli/file_lister.py +1 -1
- ara_cli/file_loaders/__init__.py +0 -0
- ara_cli/file_loaders/binary_file_loader.py +33 -0
- ara_cli/file_loaders/document_file_loader.py +34 -0
- ara_cli/file_loaders/document_reader.py +245 -0
- ara_cli/file_loaders/document_readers.py +233 -0
- ara_cli/file_loaders/file_loader.py +50 -0
- ara_cli/file_loaders/file_loaders.py +123 -0
- ara_cli/file_loaders/image_processor.py +89 -0
- ara_cli/file_loaders/markdown_reader.py +75 -0
- ara_cli/file_loaders/text_file_loader.py +187 -0
- ara_cli/global_file_lister.py +51 -0
- ara_cli/list_filter.py +1 -1
- ara_cli/output_suppressor.py +1 -1
- ara_cli/prompt_extractor.py +215 -88
- ara_cli/prompt_handler.py +521 -134
- ara_cli/prompt_rag.py +2 -2
- ara_cli/tag_extractor.py +83 -38
- ara_cli/template_loader.py +245 -0
- ara_cli/template_manager.py +18 -13
- ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
- ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
- ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
- ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
- ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
- ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
- ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
- ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
- ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
- ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
- ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
- ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
- ara_cli/update_config_prompt.py +9 -3
- ara_cli/version.py +1 -1
- ara_cli-0.1.10.8.dist-info/METADATA +241 -0
- ara_cli-0.1.10.8.dist-info/RECORD +193 -0
- tests/test_ara_command_action.py +73 -59
- tests/test_ara_config.py +341 -36
- tests/test_artefact_autofix.py +1060 -0
- tests/test_artefact_link_updater.py +3 -3
- tests/test_artefact_lister.py +52 -132
- tests/test_artefact_renamer.py +2 -2
- tests/test_artefact_scan.py +327 -33
- tests/test_chat.py +2063 -498
- tests/test_file_classifier.py +24 -1
- tests/test_file_creator.py +3 -5
- tests/test_file_lister.py +1 -1
- tests/test_global_file_lister.py +131 -0
- tests/test_list_filter.py +2 -2
- tests/test_prompt_handler.py +746 -0
- tests/test_tag_extractor.py +19 -13
- tests/test_template_loader.py +192 -0
- tests/test_template_manager.py +5 -4
- tests/test_update_config_prompt.py +2 -2
- ara_cli/ara_command_parser.py +0 -327
- ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
- ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
- ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
- ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
- ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
- ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
- ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
- ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
- ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
- ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
- ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
- ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
- ara_cli/templates/template.businessgoal +0 -10
- ara_cli/templates/template.capability +0 -10
- ara_cli/templates/template.epic +0 -15
- ara_cli/templates/template.example +0 -6
- ara_cli/templates/template.feature +0 -26
- ara_cli/templates/template.issue +0 -14
- ara_cli/templates/template.keyfeature +0 -15
- ara_cli/templates/template.task +0 -6
- ara_cli/templates/template.userstory +0 -17
- ara_cli/templates/template.vision +0 -14
- ara_cli-0.1.9.69.dist-info/METADATA +0 -16
- ara_cli-0.1.9.69.dist-info/RECORD +0 -158
- tests/test_ara_autofix.py +0 -219
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.10.8.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 = {
|
|
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 = [
|
|
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
|
|
126
|
+
setattr(self, f"do_{command}", self.default)
|
|
76
127
|
self.hidden_commands.append(command)
|
|
77
|
-
aliases_to_remove = [
|
|
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(
|
|
113
|
-
|
|
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,
|
|
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
|
|
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",
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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 =
|
|
311
|
-
with open(self.chat_name,
|
|
312
|
-
file.write(output +
|
|
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,
|
|
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,
|
|
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,
|
|
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 !=
|
|
389
|
+
if last_line != "" and last_line != "\n":
|
|
335
390
|
append = f"\n{append}"
|
|
336
|
-
with open(chat_file,
|
|
391
|
+
with open(chat_file, "a", encoding="utf-8") as file:
|
|
337
392
|
file.write(append)
|
|
338
393
|
|
|
339
|
-
def
|
|
340
|
-
|
|
341
|
-
file_path
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
421
|
+
reader = MarkdownReader(file_path)
|
|
422
|
+
return reader.read(extract_images=extract_images)
|
|
370
423
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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(
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = [
|
|
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(
|
|
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.
|
|
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
|
-
|
|
441
|
-
|
|
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
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
517
|
-
|
|
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.
|
|
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.
|
|
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() !=
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
805
|
+
self.template_loader.load_template(
|
|
806
|
+
blueprint_name, "blueprint", self.chat_name)
|
|
610
807
|
|
|
611
|
-
def _load_helper(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
682
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
|
|
720
|
-
|
|
969
|
+
with open(self.chat_name, "a", encoding="utf-8") as chat_file:
|
|
970
|
+
chat_file.write(content)
|
|
721
971
|
|
|
722
|
-
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
1199
|
+
return []
|