ara-cli 0.1.9.77__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 +245 -66
- ara_cli/ara_command_action.py +128 -63
- ara_cli/ara_config.py +201 -177
- 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 +214 -28
- ara_cli/artefact_creator.py +5 -8
- ara_cli/artefact_deleter.py +2 -4
- ara_cli/artefact_fuzzy_search.py +13 -6
- ara_cli/artefact_lister.py +29 -55
- ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
- ara_cli/artefact_models/artefact_model.py +106 -25
- ara_cli/artefact_models/artefact_templates.py +23 -13
- ara_cli/artefact_models/epic_artefact_model.py +11 -2
- ara_cli/artefact_models/feature_artefact_model.py +56 -1
- ara_cli/artefact_models/userstory_artefact_model.py +15 -3
- ara_cli/artefact_reader.py +4 -5
- ara_cli/artefact_renamer.py +6 -2
- ara_cli/artefact_scan.py +2 -2
- ara_cli/chat.py +594 -219
- 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 +3 -2
- 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/prompt_extractor.py +214 -87
- ara_cli/prompt_handler.py +508 -146
- ara_cli/tag_extractor.py +54 -24
- ara_cli/template_loader.py +245 -0
- ara_cli/template_manager.py +14 -4
- 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 +7 -1
- ara_cli/version.py +1 -1
- ara_cli-0.1.10.8.dist-info/METADATA +241 -0
- {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/RECORD +104 -59
- tests/test_ara_command_action.py +66 -52
- tests/test_ara_config.py +200 -279
- tests/test_artefact_autofix.py +361 -5
- tests/test_artefact_lister.py +52 -132
- tests/test_artefact_scan.py +1 -1
- tests/test_chat.py +2009 -603
- tests/test_file_classifier.py +23 -0
- tests/test_file_creator.py +3 -5
- tests/test_global_file_lister.py +131 -0
- 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
- ara_cli/ara_command_parser.py +0 -536
- 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-0.1.9.77.dist-info/METADATA +0 -18
- {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.77.dist-info → ara_cli-0.1.10.8.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.77.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,37 +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
|
-
|
|
37
|
-
|
|
73
|
+
".png": "image/png",
|
|
74
|
+
".jpg": "image/jpeg",
|
|
75
|
+
".jpeg": "image/jpeg",
|
|
76
|
+
}
|
|
77
|
+
|
|
38
78
|
DOCUMENT_TYPE_EXTENSIONS = [".docx", ".doc", ".odt", ".pdf"]
|
|
39
79
|
|
|
40
80
|
def __init__(
|
|
41
81
|
self,
|
|
42
82
|
chat_name: str,
|
|
43
83
|
reset: bool | None = None,
|
|
44
|
-
enable_commands: list[str] | None = None
|
|
84
|
+
enable_commands: list[str] | None = None,
|
|
45
85
|
):
|
|
86
|
+
from ara_cli.template_loader import TemplateLoader
|
|
87
|
+
|
|
46
88
|
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
|
|
47
89
|
if enable_commands:
|
|
48
90
|
enable_commands.append("quit") # always allow quitting
|
|
49
91
|
enable_commands.append("eof") # always allow quitting with ctrl-D
|
|
50
92
|
enable_commands.append("help") # always allow help
|
|
51
93
|
|
|
52
|
-
shortcuts = {
|
|
94
|
+
shortcuts = {
|
|
95
|
+
key: value
|
|
96
|
+
for key, value in shortcuts.items()
|
|
97
|
+
if value in enable_commands
|
|
98
|
+
}
|
|
53
99
|
|
|
54
|
-
super().__init__(
|
|
55
|
-
allow_cli_args=False,
|
|
56
|
-
shortcuts=shortcuts
|
|
57
|
-
)
|
|
100
|
+
super().__init__(allow_cli_args=False, shortcuts=shortcuts)
|
|
58
101
|
self.create_default_aliases()
|
|
59
102
|
|
|
60
103
|
if enable_commands:
|
|
61
104
|
all_commands = self.get_all_commands()
|
|
62
|
-
commands_to_disable = [
|
|
105
|
+
commands_to_disable = [
|
|
106
|
+
command for command in all_commands if command not in enable_commands
|
|
107
|
+
]
|
|
63
108
|
self.disable_commands(commands_to_disable)
|
|
64
109
|
|
|
65
110
|
self.prompt = "ara> "
|
|
@@ -71,12 +116,18 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
71
116
|
self.chat_history = []
|
|
72
117
|
self.message_buffer = []
|
|
73
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)
|
|
74
123
|
|
|
75
124
|
def disable_commands(self, commands: list[str]):
|
|
76
125
|
for command in commands:
|
|
77
|
-
setattr(self, f
|
|
126
|
+
setattr(self, f"do_{command}", self.default)
|
|
78
127
|
self.hidden_commands.append(command)
|
|
79
|
-
aliases_to_remove = [
|
|
128
|
+
aliases_to_remove = [
|
|
129
|
+
alias for alias, cmd in self.aliases.items() if cmd in commands
|
|
130
|
+
]
|
|
80
131
|
for alias in aliases_to_remove:
|
|
81
132
|
del self.aliases[alias]
|
|
82
133
|
|
|
@@ -97,6 +148,11 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
97
148
|
self.aliases["lg"] = "LOAD_GIVENS"
|
|
98
149
|
self.aliases["lb"] = "LOAD_BLUEPRINT"
|
|
99
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"
|
|
100
156
|
|
|
101
157
|
def setup_chat(self, chat_name, reset: bool = None):
|
|
102
158
|
if os.path.exists(chat_name):
|
|
@@ -111,8 +167,10 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
111
167
|
chat_file_short = os.path.split(chat_file)[-1]
|
|
112
168
|
|
|
113
169
|
if reset is None:
|
|
114
|
-
user_input = input(
|
|
115
|
-
|
|
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":
|
|
116
174
|
self.create_empty_chat_file(chat_file)
|
|
117
175
|
if reset:
|
|
118
176
|
self.create_empty_chat_file(chat_file)
|
|
@@ -139,16 +197,14 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
139
197
|
print(f"File {file_name} not found.")
|
|
140
198
|
return False
|
|
141
199
|
return method(self, file_path, *args, **kwargs)
|
|
200
|
+
|
|
142
201
|
return wrapper
|
|
143
202
|
|
|
144
203
|
@staticmethod
|
|
145
204
|
def get_last_role_marker(lines):
|
|
146
205
|
if not lines:
|
|
147
206
|
return
|
|
148
|
-
role_markers = [
|
|
149
|
-
f"# {Chat.ROLE_PROMPT}:",
|
|
150
|
-
f"# {Chat.ROLE_RESPONSE}"
|
|
151
|
-
]
|
|
207
|
+
role_markers = [f"# {Chat.ROLE_PROMPT}:", f"# {Chat.ROLE_RESPONSE}"]
|
|
152
208
|
for line in reversed(lines):
|
|
153
209
|
stripped_line = line.strip()
|
|
154
210
|
if stripped_line.startswith(tuple(role_markers)):
|
|
@@ -156,7 +212,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
156
212
|
return None
|
|
157
213
|
|
|
158
214
|
def start_non_interactive(self):
|
|
159
|
-
with open(self.chat_name,
|
|
215
|
+
with open(self.chat_name, "r", encoding="utf-8") as file:
|
|
160
216
|
content = file.read()
|
|
161
217
|
print(content)
|
|
162
218
|
|
|
@@ -191,23 +247,19 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
191
247
|
text_content = []
|
|
192
248
|
image_data_list = []
|
|
193
249
|
|
|
194
|
-
image_pattern = re.compile(r
|
|
250
|
+
image_pattern = re.compile(r"\((data:image/[^;]+;base64,.*?)\)")
|
|
195
251
|
|
|
196
252
|
for line in message.splitlines():
|
|
197
253
|
match = image_pattern.search(line)
|
|
198
254
|
if match:
|
|
199
|
-
image_data = {"type": "image_url",
|
|
255
|
+
image_data = {"type": "image_url",
|
|
256
|
+
"image_url": {"url": match.group(1)}}
|
|
200
257
|
image_data_list.append(image_data)
|
|
201
258
|
else:
|
|
202
259
|
text_content.append(line)
|
|
203
260
|
|
|
204
|
-
message_content = {
|
|
205
|
-
|
|
206
|
-
"text": '\n'.join(text_content)}
|
|
207
|
-
message = {
|
|
208
|
-
"role": role,
|
|
209
|
-
"content": [message_content]
|
|
210
|
-
}
|
|
261
|
+
message_content = {"type": "text", "text": "\n".join(text_content)}
|
|
262
|
+
message = {"role": role, "content": [message_content]}
|
|
211
263
|
message = append_images_to_message(message, image_data_list)
|
|
212
264
|
return message
|
|
213
265
|
|
|
@@ -220,7 +272,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
220
272
|
|
|
221
273
|
split_pattern = re.compile(f"({prompt_marker}|{response_marker})")
|
|
222
274
|
|
|
223
|
-
parts = re.split(split_pattern,
|
|
275
|
+
parts = re.split(split_pattern, "\n".join(self.chat_history))
|
|
224
276
|
|
|
225
277
|
all_prompts_and_responses = []
|
|
226
278
|
current = ""
|
|
@@ -254,7 +306,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
254
306
|
prompt_to_send = self.assemble_prompt()
|
|
255
307
|
role_marker = f"# {Chat.ROLE_RESPONSE}:"
|
|
256
308
|
|
|
257
|
-
with open(self.chat_name,
|
|
309
|
+
with open(self.chat_name, "a+", encoding="utf-8") as file:
|
|
258
310
|
last_line = self.get_last_line(file)
|
|
259
311
|
|
|
260
312
|
print(role_marker)
|
|
@@ -277,24 +329,24 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
277
329
|
|
|
278
330
|
def save_message(self, role: str, message: str):
|
|
279
331
|
role_marker = f"# {role}:"
|
|
280
|
-
with open(self.chat_name,
|
|
332
|
+
with open(self.chat_name, "r", encoding="utf-8") as file:
|
|
281
333
|
stripped_line = self.get_last_non_empty_line(file)
|
|
282
334
|
line_to_write = f"{message}\n\n"
|
|
283
335
|
if stripped_line != role_marker:
|
|
284
336
|
line_to_write = f"\n{role_marker}\n{message}\n"
|
|
285
337
|
|
|
286
|
-
with open(self.chat_name,
|
|
338
|
+
with open(self.chat_name, "a", encoding="utf-8") as file:
|
|
287
339
|
file.write(line_to_write)
|
|
288
340
|
self.chat_history.append(line_to_write)
|
|
289
341
|
|
|
290
342
|
def resend_message(self):
|
|
291
|
-
with open(self.chat_name,
|
|
343
|
+
with open(self.chat_name, "r", encoding="utf-8") as file:
|
|
292
344
|
lines = file.readlines()
|
|
293
345
|
if not lines:
|
|
294
346
|
return
|
|
295
347
|
index_to_remove = self.find_last_reply_index(lines)
|
|
296
348
|
if index_to_remove is not None:
|
|
297
|
-
with open(self.chat_name,
|
|
349
|
+
with open(self.chat_name, "w", encoding="utf-8") as file:
|
|
298
350
|
file.writelines(lines[:index_to_remove])
|
|
299
351
|
self.send_message()
|
|
300
352
|
|
|
@@ -309,114 +361,91 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
309
361
|
return index_to_remove
|
|
310
362
|
|
|
311
363
|
def append_strings(self, strings: list[str]):
|
|
312
|
-
output =
|
|
313
|
-
with open(self.chat_name,
|
|
314
|
-
file.write(output +
|
|
364
|
+
output = "\n".join(strings)
|
|
365
|
+
with open(self.chat_name, "a") as file:
|
|
366
|
+
file.write(output + "\n")
|
|
315
367
|
|
|
316
368
|
def load_chat_history(self, chat_file: str):
|
|
317
369
|
chat_history = []
|
|
318
370
|
if os.path.exists(chat_file):
|
|
319
|
-
with open(chat_file,
|
|
371
|
+
with open(chat_file, "r", encoding="utf-8") as file:
|
|
320
372
|
chat_history = file.readlines()
|
|
321
373
|
return chat_history
|
|
322
374
|
|
|
323
375
|
def create_empty_chat_file(self, chat_file: str):
|
|
324
|
-
with open(chat_file,
|
|
376
|
+
with open(chat_file, "w", encoding="utf-8") as file:
|
|
325
377
|
file.write(self.default_chat_content)
|
|
326
378
|
self.chat_history = []
|
|
327
379
|
|
|
328
380
|
def add_prompt_tag_if_needed(self, chat_file: str):
|
|
329
|
-
with open(chat_file,
|
|
381
|
+
with open(chat_file, "r", encoding="utf-8") as file:
|
|
330
382
|
lines = file.readlines()
|
|
383
|
+
|
|
331
384
|
prompt_tag = f"# {Chat.ROLE_PROMPT}:"
|
|
332
385
|
if Chat.get_last_role_marker(lines) == prompt_tag:
|
|
333
386
|
return
|
|
334
387
|
append = prompt_tag
|
|
335
388
|
last_line = lines[-1].strip()
|
|
336
|
-
if last_line != "" and last_line !=
|
|
389
|
+
if last_line != "" and last_line != "\n":
|
|
337
390
|
append = f"\n{append}"
|
|
338
|
-
with open(chat_file,
|
|
391
|
+
with open(chat_file, "a", encoding="utf-8") as file:
|
|
339
392
|
file.write(append)
|
|
340
393
|
|
|
341
|
-
def
|
|
342
|
-
|
|
343
|
-
file_path
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
write_content = f"{prefix}{file_content}{suffix}\n"
|
|
358
|
-
|
|
359
|
-
with open(self.chat_name, 'a', encoding='utf-8') as chat_file:
|
|
360
|
-
chat_file.write(write_content)
|
|
361
|
-
return True
|
|
362
|
-
|
|
363
|
-
@file_exists_check
|
|
364
|
-
def load_binary_file(self, file_path, mime_type: str, prefix: str = "", suffix: str = ""):
|
|
365
|
-
import base64
|
|
366
|
-
|
|
367
|
-
with open(file_path, 'rb') as file:
|
|
368
|
-
file_content = file.read()
|
|
369
|
-
base64_image = base64.b64encode(file_content).decode("utf-8")
|
|
370
|
-
|
|
371
|
-
write_content = f"{prefix}{suffix}\n"
|
|
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
|
+
)
|
|
372
410
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
import docx
|
|
379
|
-
doc = docx.Document(file_path)
|
|
380
|
-
return '\n'.join(para.text for para in doc.paragraphs)
|
|
381
|
-
|
|
382
|
-
def read_pdf(self, file_path):
|
|
383
|
-
import pymupdf4llm
|
|
384
|
-
return pymupdf4llm.to_markdown(file_path, write_images=False)
|
|
385
|
-
|
|
386
|
-
def read_odt(self, file_path):
|
|
387
|
-
import pymupdf4llm
|
|
388
|
-
return pymupdf4llm.to_markdown(file_path, write_images=False)
|
|
389
|
-
|
|
390
|
-
@file_exists_check
|
|
391
|
-
def load_document_file(self, file_path: str, prefix: str = "", suffix: str = "", block_delimiter: str = "```"):
|
|
392
|
-
import os
|
|
393
|
-
|
|
394
|
-
_, ext = os.path.splitext(file_path)
|
|
395
|
-
ext = ext.lower()
|
|
396
|
-
|
|
397
|
-
text_content = ""
|
|
398
|
-
match ext:
|
|
399
|
-
case ".docx":
|
|
400
|
-
text_content = self.read_docx(file_path)
|
|
401
|
-
case ".pdf":
|
|
402
|
-
text_content = self.read_pdf(file_path)
|
|
403
|
-
case ".odt":
|
|
404
|
-
text_content = self.read_odt(file_path)
|
|
405
|
-
# Add more cases if needed.
|
|
406
|
-
case _:
|
|
407
|
-
print("Unsupported document type.")
|
|
408
|
-
return False
|
|
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)
|
|
409
416
|
|
|
410
|
-
|
|
411
|
-
|
|
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
|
|
412
420
|
|
|
413
|
-
|
|
421
|
+
reader = MarkdownReader(file_path)
|
|
422
|
+
return reader.read(extract_images=extract_images)
|
|
414
423
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
+
)
|
|
418
440
|
|
|
419
|
-
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
|
+
):
|
|
420
449
|
binary_type_mapping = Chat.BINARY_TYPE_MAPPING
|
|
421
450
|
document_type_extensions = Chat.DOCUMENT_TYPE_EXTENSIONS
|
|
422
451
|
|
|
@@ -427,28 +456,29 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
427
456
|
file_type = mime_type
|
|
428
457
|
break
|
|
429
458
|
|
|
430
|
-
is_file_document = any(
|
|
459
|
+
is_file_document = any(
|
|
460
|
+
file_name_lower.endswith(ext) for ext in document_type_extensions
|
|
461
|
+
)
|
|
431
462
|
|
|
432
463
|
if is_file_document:
|
|
433
464
|
return self.load_document_file(
|
|
434
|
-
|
|
465
|
+
file_path=file_name,
|
|
435
466
|
prefix=prefix,
|
|
436
467
|
suffix=suffix,
|
|
437
|
-
block_delimiter=block_delimiter
|
|
468
|
+
block_delimiter=block_delimiter,
|
|
469
|
+
extract_images=extract_images,
|
|
438
470
|
)
|
|
439
471
|
elif file_type:
|
|
440
472
|
return self.load_binary_file(
|
|
441
|
-
|
|
442
|
-
mime_type=file_type,
|
|
443
|
-
prefix=prefix,
|
|
444
|
-
suffix=suffix
|
|
473
|
+
file_path=file_name, mime_type=file_type, prefix=prefix, suffix=suffix
|
|
445
474
|
)
|
|
446
475
|
else:
|
|
447
476
|
return self.load_text_file(
|
|
448
|
-
|
|
477
|
+
file_path=file_name,
|
|
449
478
|
prefix=prefix,
|
|
450
479
|
suffix=suffix,
|
|
451
|
-
block_delimiter=block_delimiter
|
|
480
|
+
block_delimiter=block_delimiter,
|
|
481
|
+
extract_images=extract_images,
|
|
452
482
|
)
|
|
453
483
|
|
|
454
484
|
def choose_file_to_load(self, files: list[str], pattern: str):
|
|
@@ -460,11 +490,14 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
460
490
|
try:
|
|
461
491
|
choice_index = int(choice) - 1
|
|
462
492
|
if choice_index < 0 or choice_index >= len(files):
|
|
463
|
-
|
|
493
|
+
error_handler.report_error(
|
|
494
|
+
ValueError("Invalid choice. Aborting load.")
|
|
495
|
+
)
|
|
464
496
|
return None
|
|
465
497
|
file_path = files[choice_index]
|
|
466
|
-
except ValueError:
|
|
467
|
-
|
|
498
|
+
except ValueError as e:
|
|
499
|
+
error_handler.report_error(
|
|
500
|
+
ValueError("Invalid input. Aborting load."))
|
|
468
501
|
return None
|
|
469
502
|
else:
|
|
470
503
|
file_path = files[0]
|
|
@@ -473,26 +506,53 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
473
506
|
def _help_menu(self, verbose: bool = False):
|
|
474
507
|
super()._help_menu(verbose)
|
|
475
508
|
if self.aliases:
|
|
476
|
-
aliases = [
|
|
509
|
+
aliases = [
|
|
510
|
+
f"{alias} -> {command}" for alias, command in self.aliases.items()
|
|
511
|
+
]
|
|
477
512
|
self._print_topics("Aliases", aliases, verbose)
|
|
478
513
|
|
|
479
514
|
def do_quit(self, _):
|
|
480
515
|
"""Exit ara-cli"""
|
|
516
|
+
self.agent_manager.cleanup_agent_process()
|
|
481
517
|
print("Chat ended")
|
|
482
518
|
self.last_result = True
|
|
483
519
|
return True
|
|
484
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
|
+
|
|
485
528
|
def onecmd_plus_hooks(self, line, orig_rl_history_length):
|
|
486
529
|
# store the full line for use with default()
|
|
487
530
|
self.full_input = line
|
|
488
|
-
return super().onecmd_plus_hooks(
|
|
531
|
+
return super().onecmd_plus_hooks(
|
|
532
|
+
line, orig_rl_history_length=orig_rl_history_length
|
|
533
|
+
)
|
|
489
534
|
|
|
490
535
|
def default(self, line):
|
|
491
|
-
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)
|
|
492
546
|
|
|
493
547
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
494
|
-
|
|
495
|
-
|
|
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
|
+
|
|
496
556
|
matching_files = self.find_matching_files_to_load(file_name)
|
|
497
557
|
if not matching_files:
|
|
498
558
|
return
|
|
@@ -501,16 +561,26 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
501
561
|
block_delimiter = "```"
|
|
502
562
|
prefix = f"\nFile: {file_path}\n"
|
|
503
563
|
self.add_prompt_tag_if_needed(self.chat_name)
|
|
504
|
-
|
|
505
|
-
|
|
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()
|
|
506
575
|
|
|
507
576
|
def complete_LOAD(self, text, line, begidx, endidx):
|
|
508
577
|
import glob
|
|
509
578
|
|
|
510
|
-
return [x for x in glob.glob(text +
|
|
579
|
+
return [x for x in glob.glob(text + "*")]
|
|
511
580
|
|
|
512
581
|
def _retrieve_ara_config(self):
|
|
513
582
|
from ara_cli.prompt_handler import ConfigManager
|
|
583
|
+
|
|
514
584
|
return ConfigManager().get_config()
|
|
515
585
|
|
|
516
586
|
def _retrieve_llm_config(self):
|
|
@@ -526,7 +596,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
526
596
|
file_pattern = os.path.join(os.path.dirname(self.chat_name), file_name)
|
|
527
597
|
matching_files = glob.glob(file_pattern)
|
|
528
598
|
if not matching_files:
|
|
529
|
-
|
|
599
|
+
error_handler.report_error(
|
|
600
|
+
AraError(f"No files matching pattern '{file_name}' found.")
|
|
601
|
+
)
|
|
530
602
|
return
|
|
531
603
|
return matching_files
|
|
532
604
|
|
|
@@ -542,37 +614,30 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
542
614
|
|
|
543
615
|
if file_type:
|
|
544
616
|
return self.load_binary_file(
|
|
545
|
-
|
|
546
|
-
mime_type=file_type,
|
|
547
|
-
prefix=prefix,
|
|
548
|
-
suffix=suffix
|
|
617
|
+
file_path=file_name, mime_type=file_type, prefix=prefix, suffix=suffix
|
|
549
618
|
)
|
|
550
|
-
|
|
619
|
+
error_handler.report_error(
|
|
620
|
+
AraError(
|
|
621
|
+
f"File {file_name} not recognized as image, could not load")
|
|
622
|
+
)
|
|
551
623
|
|
|
552
624
|
def _verify_llm_choice(self, model_name):
|
|
553
625
|
llm_config = self._retrieve_llm_config()
|
|
554
626
|
models = [name for name in llm_config.keys()]
|
|
555
627
|
if model_name not in models:
|
|
556
|
-
|
|
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
|
+
)
|
|
557
633
|
return False
|
|
558
634
|
return True
|
|
559
635
|
|
|
560
|
-
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
561
|
-
def do_LOAD_DOCUMENT(self, file_name):
|
|
562
|
-
"""Load a document file (PDF, DOCX, DOC, ODT) and append its text content 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"""
|
|
563
|
-
matching_files = self.find_matching_files_to_load(file_name)
|
|
564
|
-
if not matching_files:
|
|
565
|
-
return
|
|
566
|
-
|
|
567
|
-
for file_path in matching_files:
|
|
568
|
-
prefix = f"\nFile: {file_path}\n"
|
|
569
|
-
self.add_prompt_tag_if_needed(self.chat_name)
|
|
570
|
-
if not os.path.isdir(file_path) and self.load_document_file(file_path, prefix=prefix):
|
|
571
|
-
print(f"Loaded document file {file_path}")
|
|
572
|
-
|
|
573
636
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
574
637
|
def do_LOAD_IMAGE(self, file_name):
|
|
575
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
|
+
|
|
576
641
|
matching_files = self.find_matching_files_to_load(file_name)
|
|
577
642
|
if not matching_files:
|
|
578
643
|
return
|
|
@@ -580,8 +645,31 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
580
645
|
for file_path in matching_files:
|
|
581
646
|
prefix = f"\nFile: {file_path}\n"
|
|
582
647
|
self.add_prompt_tag_if_needed(self.chat_name)
|
|
583
|
-
|
|
584
|
-
|
|
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
|
+
)
|
|
585
673
|
|
|
586
674
|
@cmd2.with_category(CATEGORY_LLM_CONTROL)
|
|
587
675
|
def do_LIST_MODELS(self, _):
|
|
@@ -600,14 +688,38 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
600
688
|
original_dir = os.getcwd()
|
|
601
689
|
navigator = DirectoryNavigator()
|
|
602
690
|
navigator.navigate_to_target()
|
|
603
|
-
os.chdir(
|
|
691
|
+
os.chdir("..")
|
|
604
692
|
|
|
605
693
|
if not self._verify_llm_choice(model_name):
|
|
606
694
|
return
|
|
695
|
+
|
|
607
696
|
self.config.default_llm = model_name
|
|
608
697
|
save_data(filepath=DEFAULT_CONFIG_LOCATION, config=self.config)
|
|
609
698
|
|
|
610
|
-
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}'")
|
|
611
723
|
|
|
612
724
|
os.chdir(original_dir)
|
|
613
725
|
|
|
@@ -615,7 +727,14 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
615
727
|
def do_CURRENT_MODEL(self, _):
|
|
616
728
|
from ara_cli.prompt_handler import LLMSingleton
|
|
617
729
|
|
|
618
|
-
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())
|
|
619
738
|
|
|
620
739
|
def _complete_llms(self, text, line, begidx, endidx):
|
|
621
740
|
llm_config = self._retrieve_llm_config()
|
|
@@ -631,6 +750,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
631
750
|
def complete_CHOOSE_MODEL(self, text, line, begidx, endidx):
|
|
632
751
|
return self._complete_llms(text, line, begidx, endidx)
|
|
633
752
|
|
|
753
|
+
def complete_CHOOSE_EXTRACTION_MODEL(self, text, line, begidx, endidx):
|
|
754
|
+
return self._complete_llms(text, line, begidx, endidx)
|
|
755
|
+
|
|
634
756
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
635
757
|
def do_NEW(self, chat_name):
|
|
636
758
|
"""Create a new chat. Optionally provide a chat name in-line: NEW new_chat"""
|
|
@@ -649,36 +771,51 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
649
771
|
def do_CLEAR(self, _):
|
|
650
772
|
"""Clear the chat and the file containing it"""
|
|
651
773
|
user_input = input("Are you sure you want to clear the chat? (y/N): ")
|
|
652
|
-
if user_input.lower() !=
|
|
774
|
+
if user_input.lower() != "y":
|
|
653
775
|
return
|
|
654
776
|
self.create_empty_chat_file(self.chat_name)
|
|
655
777
|
self.chat_history = self.load_chat_history(self.chat_name)
|
|
778
|
+
self.message_buffer.clear()
|
|
656
779
|
print(f"Cleared content of {self.chat_name}")
|
|
657
780
|
|
|
658
781
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
659
782
|
def do_LOAD_RULES(self, rules_name):
|
|
660
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"""
|
|
661
|
-
self.
|
|
784
|
+
self.template_loader.load_template(
|
|
785
|
+
rules_name, "rules", self.chat_name, "*.rules.md"
|
|
786
|
+
)
|
|
662
787
|
|
|
663
788
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
664
789
|
def do_LOAD_INTENTION(self, intention_name):
|
|
665
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"""
|
|
666
|
-
self.
|
|
791
|
+
self.template_loader.load_template(
|
|
792
|
+
intention_name, "intention", self.chat_name, "*.intention.md"
|
|
793
|
+
)
|
|
667
794
|
|
|
668
795
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
669
796
|
def do_LOAD_COMMANDS(self, commands_name):
|
|
670
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"""
|
|
671
|
-
self.
|
|
798
|
+
self.template_loader.load_template(
|
|
799
|
+
commands_name, "commands", self.chat_name, "*.commands.md"
|
|
800
|
+
)
|
|
672
801
|
|
|
673
802
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
674
803
|
def do_LOAD_BLUEPRINT(self, blueprint_name):
|
|
675
804
|
"""Load specified blueprint. Specify global/<blueprint_name> to access globally defined blueprints"""
|
|
676
|
-
self.
|
|
805
|
+
self.template_loader.load_template(
|
|
806
|
+
blueprint_name, "blueprint", self.chat_name)
|
|
677
807
|
|
|
678
|
-
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
|
+
):
|
|
679
815
|
import glob
|
|
680
816
|
|
|
681
|
-
directory_path = os.path.join(
|
|
817
|
+
directory_path = os.path.join(
|
|
818
|
+
os.path.dirname(self.chat_name), directory)
|
|
682
819
|
file_pattern = os.path.join(directory_path, pattern)
|
|
683
820
|
|
|
684
821
|
exclude_files = []
|
|
@@ -688,7 +825,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
688
825
|
matching_files = list(set(matching_files) - set(exclude_files))
|
|
689
826
|
|
|
690
827
|
if not matching_files:
|
|
691
|
-
|
|
828
|
+
error_handler.report_error(AraError(f"No {file_type} file found."))
|
|
692
829
|
return
|
|
693
830
|
|
|
694
831
|
file_path = self.choose_file_to_load(matching_files, pattern)
|
|
@@ -705,10 +842,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
705
842
|
from ara_cli.ara_config import ConfigManager
|
|
706
843
|
from ara_cli.directory_navigator import DirectoryNavigator
|
|
707
844
|
|
|
708
|
-
plurals = {
|
|
709
|
-
"commands": "commands",
|
|
710
|
-
"rules": "rules"
|
|
711
|
-
}
|
|
845
|
+
plurals = {"commands": "commands", "rules": "rules"}
|
|
712
846
|
|
|
713
847
|
plural = f"{template_type}s"
|
|
714
848
|
if template_type in plurals:
|
|
@@ -716,7 +850,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
716
850
|
|
|
717
851
|
if template_name.startswith("global/"):
|
|
718
852
|
directory = f"{TemplatePathManager.get_template_base_path()}/prompt-modules/{plural}/"
|
|
719
|
-
self._load_helper(
|
|
853
|
+
self._load_helper(
|
|
854
|
+
directory, template_name.removeprefix("global/"), template_type
|
|
855
|
+
)
|
|
720
856
|
return
|
|
721
857
|
|
|
722
858
|
ara_config = ConfigManager.get_config()
|
|
@@ -730,7 +866,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
730
866
|
os.chdir(original_directory)
|
|
731
867
|
|
|
732
868
|
custom_prompt_templates_subdir = self.config.custom_prompt_templates_subdir
|
|
733
|
-
template_directory =
|
|
869
|
+
template_directory = (
|
|
870
|
+
f"{local_templates_path}/{custom_prompt_templates_subdir}/{plural}"
|
|
871
|
+
)
|
|
734
872
|
self._load_helper(template_directory, template_name, template_type)
|
|
735
873
|
|
|
736
874
|
def _load_template_helper(self, template_name, template_type, default_pattern):
|
|
@@ -738,55 +876,100 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
738
876
|
self._load_helper("prompt.data", default_pattern, template_type)
|
|
739
877
|
return
|
|
740
878
|
|
|
741
|
-
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
|
+
)
|
|
742
882
|
|
|
743
883
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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)
|
|
747
906
|
|
|
748
|
-
|
|
749
|
-
|
|
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 []
|
|
750
953
|
|
|
751
954
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
752
955
|
def do_LOAD_GIVENS(self, file_name):
|
|
753
|
-
"""Load all files listed in a ./prompt.data/config.prompt_givens.md"""
|
|
754
|
-
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"""
|
|
755
957
|
from ara_cli.prompt_handler import load_givens
|
|
756
958
|
|
|
757
|
-
|
|
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
|
|
758
963
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
relative_givens_path = os.path.join(base_directory, file_name)
|
|
764
|
-
if os.path.exists(relative_givens_path):
|
|
765
|
-
givens_path = relative_givens_path
|
|
766
|
-
elif os.path.exists(file_name): # Check the absolute path
|
|
767
|
-
givens_path = file_name
|
|
768
|
-
else:
|
|
769
|
-
print(f"No givens file found at {relative_givens_path} or {file_name}")
|
|
770
|
-
user_input = input("Please specify a givens file: ")
|
|
771
|
-
if os.path.exists(os.path.join(base_directory, user_input)):
|
|
772
|
-
givens_path = os.path.join(base_directory, user_input)
|
|
773
|
-
elif os.path.exists(user_input):
|
|
774
|
-
givens_path = user_input
|
|
775
|
-
else:
|
|
776
|
-
print(f"No givens file found at {user_input}. Aborting.")
|
|
777
|
-
return
|
|
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)
|
|
778
968
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
navigator.navigate_to_target()
|
|
782
|
-
os.chdir('..')
|
|
783
|
-
content, image_data = load_givens(givens_path)
|
|
784
|
-
os.chdir(cwd)
|
|
785
|
-
|
|
786
|
-
with open(self.chat_name, 'a', encoding='utf-8') as chat_file:
|
|
787
|
-
chat_file.write(content)
|
|
969
|
+
with open(self.chat_name, "a", encoding="utf-8") as chat_file:
|
|
970
|
+
chat_file.write(content)
|
|
788
971
|
|
|
789
|
-
|
|
972
|
+
self.poutput(f"Loaded files listed and marked in {givens_path}")
|
|
790
973
|
|
|
791
974
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
792
975
|
def do_SEND(self, _):
|
|
@@ -800,17 +983,20 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
800
983
|
"""Load artefact template"""
|
|
801
984
|
from ara_cli.artefact_models.artefact_templates import template_artefact_of_type
|
|
802
985
|
|
|
803
|
-
artefact = template_artefact_of_type(
|
|
986
|
+
artefact = template_artefact_of_type("".join(template_name))
|
|
804
987
|
if not artefact:
|
|
988
|
+
error_handler.report_error(
|
|
989
|
+
ValueError(f"No template for '{template_name}' found.")
|
|
990
|
+
)
|
|
805
991
|
return
|
|
806
992
|
write_content = artefact.serialize()
|
|
807
993
|
self.add_prompt_tag_if_needed(self.chat_name)
|
|
808
|
-
with open(self.chat_name,
|
|
994
|
+
with open(self.chat_name, "a", encoding="utf-8") as chat_file:
|
|
809
995
|
chat_file.write(write_content)
|
|
810
996
|
print(f"Loaded {template_name} artefact template")
|
|
811
997
|
|
|
812
998
|
def complete_LOAD_TEMPLATE(self, text, line, begidx, endidx):
|
|
813
|
-
return self._complete_classifiers(
|
|
999
|
+
return self._complete_classifiers(text, line, begidx, endidx)
|
|
814
1000
|
|
|
815
1001
|
def _complete_classifiers(self, text, line, begidx, endidx):
|
|
816
1002
|
from ara_cli.classifier import Classifier
|
|
@@ -819,6 +1005,195 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
819
1005
|
if not text:
|
|
820
1006
|
completions = classifiers
|
|
821
1007
|
else:
|
|
822
|
-
completions = [
|
|
1008
|
+
completions = [
|
|
1009
|
+
classifier for classifier in classifiers if classifier.startswith(text)
|
|
1010
|
+
]
|
|
823
1011
|
|
|
824
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)
|
|
1198
|
+
|
|
1199
|
+
return []
|