ara-cli 0.1.10.5__py3-none-any.whl → 0.1.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ara_cli/__init__.py +51 -6
- ara_cli/__main__.py +87 -75
- ara_cli/ara_command_action.py +189 -101
- ara_cli/ara_config.py +187 -128
- ara_cli/ara_subcommands/common.py +2 -2
- ara_cli/ara_subcommands/config.py +221 -0
- ara_cli/ara_subcommands/convert.py +107 -0
- ara_cli/ara_subcommands/fetch.py +41 -0
- ara_cli/ara_subcommands/fetch_agents.py +22 -0
- ara_cli/ara_subcommands/fetch_scripts.py +19 -0
- ara_cli/ara_subcommands/fetch_templates.py +15 -10
- ara_cli/ara_subcommands/list.py +97 -23
- ara_cli/ara_subcommands/prompt.py +266 -106
- ara_cli/artefact_autofix.py +117 -64
- ara_cli/artefact_converter.py +355 -0
- ara_cli/artefact_creator.py +41 -17
- ara_cli/artefact_lister.py +3 -3
- ara_cli/artefact_models/artefact_model.py +1 -1
- ara_cli/artefact_models/artefact_templates.py +0 -9
- ara_cli/artefact_models/feature_artefact_model.py +8 -8
- ara_cli/artefact_reader.py +62 -43
- ara_cli/artefact_scan.py +39 -17
- ara_cli/chat.py +300 -71
- ara_cli/chat_agent/__init__.py +0 -0
- ara_cli/chat_agent/agent_process_manager.py +155 -0
- ara_cli/chat_script_runner/__init__.py +0 -0
- ara_cli/chat_script_runner/script_completer.py +23 -0
- ara_cli/chat_script_runner/script_finder.py +41 -0
- ara_cli/chat_script_runner/script_lister.py +36 -0
- ara_cli/chat_script_runner/script_runner.py +36 -0
- ara_cli/chat_web_search/__init__.py +0 -0
- ara_cli/chat_web_search/web_search.py +263 -0
- ara_cli/children_contribution_updater.py +737 -0
- ara_cli/classifier.py +34 -0
- ara_cli/commands/agent_run_command.py +98 -0
- ara_cli/commands/fetch_agents_command.py +106 -0
- ara_cli/commands/fetch_scripts_command.py +43 -0
- ara_cli/commands/fetch_templates_command.py +39 -0
- ara_cli/commands/fetch_templates_commands.py +39 -0
- ara_cli/commands/list_agents_command.py +39 -0
- ara_cli/commands/load_command.py +4 -3
- ara_cli/commands/load_image_command.py +1 -1
- ara_cli/commands/read_command.py +23 -27
- ara_cli/completers.py +95 -35
- ara_cli/constants.py +2 -0
- ara_cli/directory_navigator.py +37 -4
- ara_cli/error_handler.py +26 -11
- ara_cli/file_loaders/document_reader.py +0 -178
- ara_cli/file_loaders/factories/__init__.py +0 -0
- ara_cli/file_loaders/factories/document_reader_factory.py +32 -0
- ara_cli/file_loaders/factories/file_loader_factory.py +27 -0
- ara_cli/file_loaders/file_loader.py +1 -30
- ara_cli/file_loaders/loaders/__init__.py +0 -0
- ara_cli/file_loaders/{document_file_loader.py → loaders/document_file_loader.py} +1 -1
- ara_cli/file_loaders/loaders/text_file_loader.py +47 -0
- ara_cli/file_loaders/readers/__init__.py +0 -0
- ara_cli/file_loaders/readers/docx_reader.py +49 -0
- ara_cli/file_loaders/readers/excel_reader.py +27 -0
- ara_cli/file_loaders/{markdown_reader.py → readers/markdown_reader.py} +1 -1
- ara_cli/file_loaders/readers/odt_reader.py +59 -0
- ara_cli/file_loaders/readers/pdf_reader.py +54 -0
- ara_cli/file_loaders/readers/pptx_reader.py +104 -0
- ara_cli/file_loaders/tools/__init__.py +0 -0
- ara_cli/llm_utils.py +58 -0
- ara_cli/output_suppressor.py +53 -0
- ara_cli/prompt_chat.py +20 -4
- ara_cli/prompt_extractor.py +47 -32
- ara_cli/prompt_handler.py +123 -17
- ara_cli/tag_extractor.py +8 -7
- ara_cli/template_loader.py +2 -1
- ara_cli/template_manager.py +52 -21
- ara_cli/templates/global-scripts/hello_global.py +1 -0
- ara_cli/templates/prompt-modules/commands/add_scenarios_for_new_behaviour.feature_creation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/align_feature_with_implementation_changes.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/analyze_codebase_and_plan_tasks.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/choose_best_parent_artefact.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/create_tasks_from_artefact_content.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/create_tests_for_uncovered_modules.test_generation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/derive_features_from_video_description.feature_creation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/describe_agent_capabilities.agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
- ara_cli/templates/prompt-modules/commands/execute_scoped_todos_in_task.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/explain_single_file_purpose.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/extract_file_information_bullets.interview_agent.commands.md +1 -0
- 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/fix_failing_behave_step_definitions.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/fix_failing_pytest_tests.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/general_instruction_policy.commands.md +47 -0
- ara_cli/templates/prompt-modules/commands/generate_and_fix_pytest_tests.test_generation_agent.commands.md +1 -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/templates/prompt-modules/commands/suggest_next_story_child_tasks.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/summarize_or_transcribe_media.interview_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/update_feature_to_match_implementation.feature_creation_agent.commands.md +1 -0
- ara_cli/templates/prompt-modules/commands/update_user_story_with_requirements.interview_agent.commands.md +1 -0
- ara_cli/version.py +1 -1
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/METADATA +49 -11
- ara_cli-0.1.14.0.dist-info/RECORD +253 -0
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/WHEEL +1 -1
- tests/test_ara_command_action.py +31 -19
- tests/test_ara_config.py +177 -90
- tests/test_artefact_autofix.py +170 -97
- tests/test_artefact_autofix_integration.py +495 -0
- tests/test_artefact_converter.py +312 -0
- tests/test_artefact_extraction.py +564 -0
- tests/test_artefact_lister.py +11 -8
- tests/test_chat.py +166 -130
- tests/test_chat_givens_images.py +603 -0
- tests/test_chat_script_runner.py +454 -0
- tests/test_children_contribution_updater.py +98 -0
- tests/test_document_loader_office.py +267 -0
- tests/test_llm_utils.py +164 -0
- tests/test_prompt_chat.py +343 -0
- tests/test_prompt_extractor.py +683 -0
- tests/test_prompt_handler.py +416 -214
- tests/test_setup_default_chat_prompt_mode.py +198 -0
- tests/test_tag_extractor.py +95 -49
- tests/test_web_search.py +467 -0
- ara_cli/file_loaders/document_readers.py +0 -233
- ara_cli/file_loaders/file_loaders.py +0 -123
- ara_cli/file_loaders/text_file_loader.py +0 -187
- ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
- ara_cli/templates/prompt-modules/blueprints/pytest_unittest_prompt.blueprint.md +0 -32
- 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.10.5.dist-info/RECORD +0 -194
- /ara_cli/file_loaders/{binary_file_loader.py → loaders/binary_file_loader.py} +0 -0
- /ara_cli/file_loaders/{image_processor.py → tools/image_processor.py} +0 -0
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/top_level.txt +0 -0
ara_cli/chat.py
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import
|
|
2
|
+
import sys
|
|
3
3
|
import argparse
|
|
4
4
|
import cmd2
|
|
5
5
|
|
|
6
|
-
from
|
|
6
|
+
from . import (
|
|
7
|
+
CATEGORY_CHAT_CONTROL,
|
|
8
|
+
CATEGORY_LLM_CONTROL,
|
|
9
|
+
CATEGORY_SCRIPT_CONTROL,
|
|
10
|
+
CATEGORY_AGENT_CONTROL,
|
|
11
|
+
)
|
|
12
|
+
from . import ROLE_PROMPT, ROLE_RESPONSE, INTRO
|
|
13
|
+
from . import BINARY_TYPE_MAPPING, DOCUMENT_TYPE_EXTENSIONS
|
|
7
14
|
|
|
8
15
|
from . import error_handler
|
|
9
16
|
from ara_cli.error_handler import AraError, AraConfigurationError
|
|
10
17
|
|
|
11
|
-
from ara_cli.
|
|
12
|
-
|
|
13
|
-
from ara_cli.
|
|
18
|
+
from ara_cli.chat_agent.agent_process_manager import AgentProcessManager
|
|
19
|
+
|
|
20
|
+
from ara_cli.chat_script_runner.script_runner import ScriptRunner
|
|
21
|
+
from ara_cli.chat_script_runner.script_completer import ScriptCompleter
|
|
22
|
+
from ara_cli.chat_script_runner.script_lister import ScriptLister
|
|
14
23
|
|
|
15
24
|
|
|
16
25
|
extract_parser = argparse.ArgumentParser()
|
|
@@ -34,39 +43,6 @@ load_parser.add_argument(
|
|
|
34
43
|
|
|
35
44
|
|
|
36
45
|
class Chat(cmd2.Cmd):
|
|
37
|
-
CATEGORY_CHAT_CONTROL = "Chat control commands"
|
|
38
|
-
CATEGORY_LLM_CONTROL = "Language model controls"
|
|
39
|
-
|
|
40
|
-
INTRO = """/***************************************/
|
|
41
|
-
araarar
|
|
42
|
-
aa ara
|
|
43
|
-
aa aa aara
|
|
44
|
-
a araarar
|
|
45
|
-
a ar ar
|
|
46
|
-
aa ara
|
|
47
|
-
a a
|
|
48
|
-
a aa
|
|
49
|
-
a a
|
|
50
|
-
ar aa aa
|
|
51
|
-
(c) ara chat by talsen team
|
|
52
|
-
aa aa
|
|
53
|
-
aa a
|
|
54
|
-
a aa
|
|
55
|
-
aa
|
|
56
|
-
/***************************************/
|
|
57
|
-
Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat mode):"""
|
|
58
|
-
|
|
59
|
-
ROLE_PROMPT = "ara prompt"
|
|
60
|
-
ROLE_RESPONSE = "ara response"
|
|
61
|
-
|
|
62
|
-
BINARY_TYPE_MAPPING = {
|
|
63
|
-
".png": "image/png",
|
|
64
|
-
".jpg": "image/jpeg",
|
|
65
|
-
".jpeg": "image/jpeg",
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
DOCUMENT_TYPE_EXTENSIONS = [".docx", ".doc", ".odt", ".pdf"]
|
|
69
|
-
|
|
70
46
|
def __init__(
|
|
71
47
|
self,
|
|
72
48
|
chat_name: str,
|
|
@@ -74,6 +50,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
74
50
|
enable_commands: list[str] | None = None,
|
|
75
51
|
):
|
|
76
52
|
from ara_cli.template_loader import TemplateLoader
|
|
53
|
+
|
|
77
54
|
shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
|
|
78
55
|
if enable_commands:
|
|
79
56
|
enable_commands.append("quit") # always allow quitting
|
|
@@ -97,15 +74,21 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
97
74
|
self.disable_commands(commands_to_disable)
|
|
98
75
|
|
|
99
76
|
self.prompt = "ara> "
|
|
100
|
-
self.intro =
|
|
77
|
+
self.intro = INTRO
|
|
101
78
|
|
|
102
|
-
self.default_chat_content = f"# {
|
|
79
|
+
self.default_chat_content = f"# {ROLE_PROMPT}:\n"
|
|
103
80
|
self.chat_name = self.setup_chat(chat_name, reset)
|
|
104
81
|
self.chat_name = os.path.abspath(self.chat_name)
|
|
105
82
|
self.chat_history = []
|
|
106
83
|
self.message_buffer = []
|
|
107
84
|
self.config = self._retrieve_ara_config()
|
|
108
85
|
self.template_loader = TemplateLoader(chat_instance=self)
|
|
86
|
+
self.script_runner = ScriptRunner(chat_instance=self)
|
|
87
|
+
self.script_lister = ScriptLister()
|
|
88
|
+
self.script_completer = ScriptCompleter()
|
|
89
|
+
|
|
90
|
+
# Initialize agent process manager
|
|
91
|
+
self.agent_manager = AgentProcessManager(self)
|
|
109
92
|
|
|
110
93
|
def disable_commands(self, commands: list[str]):
|
|
111
94
|
for command in commands:
|
|
@@ -127,6 +110,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
127
110
|
self.aliases["h"] = "help"
|
|
128
111
|
self.aliases["n"] = "NEW"
|
|
129
112
|
self.aliases["e"] = "EXTRACT"
|
|
113
|
+
self.aliases["SEARCH"] = "search"
|
|
130
114
|
self.aliases["l"] = "LOAD"
|
|
131
115
|
self.aliases["lr"] = "LOAD_RULES"
|
|
132
116
|
self.aliases["li"] = "LOAD_INTENTION"
|
|
@@ -134,6 +118,11 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
134
118
|
self.aliases["lg"] = "LOAD_GIVENS"
|
|
135
119
|
self.aliases["lb"] = "LOAD_BLUEPRINT"
|
|
136
120
|
self.aliases["lt"] = "LOAD_TEMPLATE"
|
|
121
|
+
self.aliases["rpy"] = "run_pyscript"
|
|
122
|
+
self.aliases["a"] = "AGENT_RUN"
|
|
123
|
+
self.aliases["al"] = "LIST_AGENTS"
|
|
124
|
+
self.aliases["la"] = "LIST_AGENTS"
|
|
125
|
+
self.aliases["AGENT_LIST"] = "LIST_AGENTS"
|
|
137
126
|
|
|
138
127
|
def setup_chat(self, chat_name, reset: bool = None):
|
|
139
128
|
if os.path.exists(chat_name):
|
|
@@ -148,9 +137,12 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
148
137
|
chat_file_short = os.path.split(chat_file)[-1]
|
|
149
138
|
|
|
150
139
|
if reset is None:
|
|
151
|
-
|
|
152
|
-
f"{chat_file_short} already exists. Do you want to reset the chat? (y/N): "
|
|
140
|
+
print(
|
|
141
|
+
f"{chat_file_short} already exists. Do you want to reset the chat? (y/N): ",
|
|
142
|
+
end="",
|
|
143
|
+
flush=True,
|
|
153
144
|
)
|
|
145
|
+
user_input = sys.stdin.readline().strip()
|
|
154
146
|
if user_input.lower() == "y":
|
|
155
147
|
self.create_empty_chat_file(chat_file)
|
|
156
148
|
if reset:
|
|
@@ -178,13 +170,14 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
178
170
|
print(f"File {file_name} not found.")
|
|
179
171
|
return False
|
|
180
172
|
return method(self, file_path, *args, **kwargs)
|
|
173
|
+
|
|
181
174
|
return wrapper
|
|
182
175
|
|
|
183
176
|
@staticmethod
|
|
184
177
|
def get_last_role_marker(lines):
|
|
185
178
|
if not lines:
|
|
186
179
|
return
|
|
187
|
-
role_markers = [f"# {
|
|
180
|
+
role_markers = [f"# {ROLE_PROMPT}:", f"# {ROLE_RESPONSE}"]
|
|
188
181
|
for line in reversed(lines):
|
|
189
182
|
stripped_line = line.strip()
|
|
190
183
|
if stripped_line.startswith(tuple(role_markers)):
|
|
@@ -246,8 +239,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
246
239
|
import re
|
|
247
240
|
from ara_cli.prompt_handler import prepend_system_prompt
|
|
248
241
|
|
|
249
|
-
prompt_marker = f"# {
|
|
250
|
-
response_marker = f"# {
|
|
242
|
+
prompt_marker = f"# {ROLE_PROMPT}:"
|
|
243
|
+
response_marker = f"# {ROLE_RESPONSE}:"
|
|
251
244
|
|
|
252
245
|
split_pattern = re.compile(f"({prompt_marker}|{response_marker})")
|
|
253
246
|
|
|
@@ -283,26 +276,29 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
283
276
|
def send_message(self):
|
|
284
277
|
self.chat_history = self.load_chat_history(self.chat_name)
|
|
285
278
|
prompt_to_send = self.assemble_prompt()
|
|
286
|
-
role_marker = f"# {
|
|
279
|
+
role_marker = f"# {ROLE_RESPONSE}:"
|
|
287
280
|
|
|
288
281
|
with open(self.chat_name, "a+", encoding="utf-8") as file:
|
|
289
282
|
last_line = self.get_last_line(file)
|
|
290
283
|
|
|
291
|
-
|
|
284
|
+
self.poutput(role_marker)
|
|
292
285
|
|
|
293
286
|
if not last_line.startswith(role_marker):
|
|
294
287
|
if last_line:
|
|
295
288
|
file.write("\n")
|
|
296
289
|
file.write(role_marker + "\n")
|
|
290
|
+
file.flush()
|
|
291
|
+
|
|
292
|
+
from ara_cli.prompt_handler import send_prompt
|
|
297
293
|
|
|
298
294
|
for chunk in send_prompt(prompt_to_send):
|
|
299
295
|
chunk_content = chunk.choices[0].delta.content
|
|
300
296
|
if not chunk_content:
|
|
301
297
|
continue
|
|
302
|
-
|
|
298
|
+
self.poutput(chunk_content, end="")
|
|
303
299
|
file.write(chunk_content)
|
|
304
300
|
file.flush()
|
|
305
|
-
|
|
301
|
+
self.poutput("")
|
|
306
302
|
|
|
307
303
|
self.message_buffer.clear()
|
|
308
304
|
|
|
@@ -332,9 +328,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
332
328
|
def find_last_reply_index(self, lines: list[str]):
|
|
333
329
|
index_to_remove = None
|
|
334
330
|
for i, line in enumerate(reversed(lines)):
|
|
335
|
-
if line.strip().startswith(f"# {
|
|
331
|
+
if line.strip().startswith(f"# {ROLE_PROMPT}"):
|
|
336
332
|
break
|
|
337
|
-
if line.strip().startswith(f"# {
|
|
333
|
+
if line.strip().startswith(f"# {ROLE_RESPONSE}"):
|
|
338
334
|
index_to_remove = len(lines) - i - 1
|
|
339
335
|
break
|
|
340
336
|
return index_to_remove
|
|
@@ -359,7 +355,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
359
355
|
def add_prompt_tag_if_needed(self, chat_file: str):
|
|
360
356
|
with open(chat_file, "r", encoding="utf-8") as file:
|
|
361
357
|
lines = file.readlines()
|
|
362
|
-
|
|
358
|
+
|
|
359
|
+
prompt_tag = f"# {ROLE_PROMPT}:"
|
|
363
360
|
if Chat.get_last_role_marker(lines) == prompt_tag:
|
|
364
361
|
return
|
|
365
362
|
append = prompt_tag
|
|
@@ -369,7 +366,6 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
369
366
|
with open(chat_file, "a", encoding="utf-8") as file:
|
|
370
367
|
file.write(append)
|
|
371
368
|
|
|
372
|
-
# @file_exists_check
|
|
373
369
|
def load_text_file(
|
|
374
370
|
self,
|
|
375
371
|
file_path,
|
|
@@ -378,6 +374,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
378
374
|
block_delimiter: str = "",
|
|
379
375
|
extract_images: bool = False,
|
|
380
376
|
):
|
|
377
|
+
from ara_cli.file_loaders.loaders.text_file_loader import TextFileLoader
|
|
378
|
+
|
|
381
379
|
loader = TextFileLoader(self)
|
|
382
380
|
return loader.load(
|
|
383
381
|
file_path,
|
|
@@ -387,21 +385,21 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
387
385
|
extract_images=extract_images,
|
|
388
386
|
)
|
|
389
387
|
|
|
390
|
-
# @file_exists_check
|
|
391
388
|
def load_binary_file(
|
|
392
389
|
self, file_path, mime_type: str, prefix: str = "", suffix: str = ""
|
|
393
390
|
):
|
|
391
|
+
from ara_cli.file_loaders.loaders.binary_file_loader import BinaryFileLoader
|
|
392
|
+
|
|
394
393
|
loader = BinaryFileLoader(self)
|
|
395
394
|
return loader.load(file_path, mime_type=mime_type, prefix=prefix, suffix=suffix)
|
|
396
395
|
|
|
397
396
|
def read_markdown(self, file_path: str, extract_images: bool = False) -> str:
|
|
398
397
|
"""Read markdown file and optionally extract/describe images"""
|
|
399
|
-
from ara_cli.file_loaders.
|
|
398
|
+
from ara_cli.file_loaders.readers.markdown_reader import MarkdownReader
|
|
400
399
|
|
|
401
400
|
reader = MarkdownReader(file_path)
|
|
402
401
|
return reader.read(extract_images=extract_images)
|
|
403
402
|
|
|
404
|
-
# @file_exists_check
|
|
405
403
|
def load_document_file(
|
|
406
404
|
self,
|
|
407
405
|
file_path: str,
|
|
@@ -410,6 +408,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
410
408
|
block_delimiter: str = "```",
|
|
411
409
|
extract_images: bool = False,
|
|
412
410
|
):
|
|
411
|
+
from ara_cli.file_loaders.loaders.document_file_loader import DocumentFileLoader
|
|
412
|
+
|
|
413
413
|
loader = DocumentFileLoader(self)
|
|
414
414
|
return loader.load(
|
|
415
415
|
file_path,
|
|
@@ -427,8 +427,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
427
427
|
block_delimiter: str = "",
|
|
428
428
|
extract_images: bool = False,
|
|
429
429
|
):
|
|
430
|
-
binary_type_mapping =
|
|
431
|
-
document_type_extensions =
|
|
430
|
+
binary_type_mapping = BINARY_TYPE_MAPPING
|
|
431
|
+
document_type_extensions = DOCUMENT_TYPE_EXTENSIONS
|
|
432
432
|
|
|
433
433
|
file_type = None
|
|
434
434
|
file_name_lower = file_name.lower()
|
|
@@ -467,7 +467,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
467
467
|
files.sort()
|
|
468
468
|
for i, file in enumerate(files):
|
|
469
469
|
print(f"{i + 1}: {os.path.basename(file)}")
|
|
470
|
-
|
|
470
|
+
print("Please choose a file to load (enter number): ", end="", flush=True)
|
|
471
|
+
choice = sys.stdin.readline().strip()
|
|
471
472
|
try:
|
|
472
473
|
choice_index = int(choice) - 1
|
|
473
474
|
if choice_index < 0 or choice_index >= len(files):
|
|
@@ -493,6 +494,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
493
494
|
|
|
494
495
|
def do_quit(self, _):
|
|
495
496
|
"""Exit ara-cli"""
|
|
497
|
+
self.agent_manager.cleanup_agent_process()
|
|
496
498
|
print("Chat ended")
|
|
497
499
|
self.last_result = True
|
|
498
500
|
return True
|
|
@@ -546,7 +548,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
546
548
|
def complete_LOAD(self, text, line, begidx, endidx):
|
|
547
549
|
import glob
|
|
548
550
|
|
|
549
|
-
return [x for x in glob.glob(text + "*")]
|
|
551
|
+
return [x for x in glob.glob(glob.escape(text) + "*")]
|
|
550
552
|
|
|
551
553
|
def _retrieve_ara_config(self):
|
|
552
554
|
from ara_cli.prompt_handler import ConfigManager
|
|
@@ -562,8 +564,13 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
562
564
|
import glob
|
|
563
565
|
|
|
564
566
|
if file_name == "":
|
|
565
|
-
|
|
567
|
+
print("What file do you want to load? ", end="", flush=True)
|
|
568
|
+
file_name = sys.stdin.readline().strip()
|
|
566
569
|
file_pattern = os.path.join(os.path.dirname(self.chat_name), file_name)
|
|
570
|
+
|
|
571
|
+
if os.path.exists(file_pattern):
|
|
572
|
+
return [file_pattern]
|
|
573
|
+
|
|
567
574
|
matching_files = glob.glob(file_pattern)
|
|
568
575
|
if not matching_files:
|
|
569
576
|
error_handler.report_error(
|
|
@@ -573,7 +580,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
573
580
|
return matching_files
|
|
574
581
|
|
|
575
582
|
def load_image(self, file_name: str, prefix: str = "", suffix: str = ""):
|
|
576
|
-
binary_type_mapping =
|
|
583
|
+
binary_type_mapping = BINARY_TYPE_MAPPING
|
|
577
584
|
|
|
578
585
|
file_type = None
|
|
579
586
|
file_name_lower = file_name.lower()
|
|
@@ -602,6 +609,67 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
602
609
|
return False
|
|
603
610
|
return True
|
|
604
611
|
|
|
612
|
+
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
613
|
+
def do_search(self, query: str):
|
|
614
|
+
"""Perform a web search and append the results to the chat.
|
|
615
|
+
Usage: search <query>
|
|
616
|
+
"""
|
|
617
|
+
if not query:
|
|
618
|
+
self.poutput("Please provide a search query.")
|
|
619
|
+
return
|
|
620
|
+
|
|
621
|
+
# Check if web search is supported by the current model
|
|
622
|
+
from ara_cli.prompt_handler import LLMSingleton
|
|
623
|
+
from ara_cli.chat_web_search.web_search import (
|
|
624
|
+
perform_web_search_completion,
|
|
625
|
+
is_web_search_supported,
|
|
626
|
+
get_supported_models_message,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
chat_instance = LLMSingleton.get_instance()
|
|
630
|
+
config_parameters = chat_instance.get_config_by_purpose("default")
|
|
631
|
+
default_llm = config_parameters.get("model")
|
|
632
|
+
|
|
633
|
+
is_supported, _ = is_web_search_supported(default_llm)
|
|
634
|
+
if not is_supported:
|
|
635
|
+
self.poutput(get_supported_models_message(default_llm))
|
|
636
|
+
return
|
|
637
|
+
|
|
638
|
+
self.add_prompt_tag_if_needed(self.chat_name)
|
|
639
|
+
|
|
640
|
+
role_marker = f"# Web Search Results for '{query}':"
|
|
641
|
+
|
|
642
|
+
with open(self.chat_name, "a+", encoding="utf-8") as file:
|
|
643
|
+
last_line = self.get_last_line(file)
|
|
644
|
+
|
|
645
|
+
self.poutput(role_marker)
|
|
646
|
+
|
|
647
|
+
if not last_line.startswith(role_marker):
|
|
648
|
+
if last_line:
|
|
649
|
+
file.write("\n")
|
|
650
|
+
file.write(role_marker + "\n")
|
|
651
|
+
|
|
652
|
+
try:
|
|
653
|
+
# perform_web_search_completion now returns a generator or a string
|
|
654
|
+
search_result = perform_web_search_completion(query)
|
|
655
|
+
|
|
656
|
+
if isinstance(search_result, str):
|
|
657
|
+
# If it's a string, it's an error/info message
|
|
658
|
+
self.poutput(search_result)
|
|
659
|
+
file.write(search_result + "\n")
|
|
660
|
+
else:
|
|
661
|
+
# Otherwise, it's a generator, stream the content
|
|
662
|
+
for chunk in search_result:
|
|
663
|
+
chunk_content = chunk.choices[0].delta.content
|
|
664
|
+
if not chunk_content:
|
|
665
|
+
continue
|
|
666
|
+
self.poutput(chunk_content, end="")
|
|
667
|
+
file.write(chunk_content)
|
|
668
|
+
file.flush()
|
|
669
|
+
self.poutput("")
|
|
670
|
+
except Exception as e:
|
|
671
|
+
error_handler.report_error(e)
|
|
672
|
+
|
|
605
673
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
606
674
|
def do_LOAD_IMAGE(self, file_name):
|
|
607
675
|
"""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"""
|
|
@@ -619,7 +687,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
619
687
|
# Determine mime type
|
|
620
688
|
file_type = None
|
|
621
689
|
file_path_lower = file_path.lower()
|
|
622
|
-
for extension, mime_type in
|
|
690
|
+
for extension, mime_type in BINARY_TYPE_MAPPING.items():
|
|
623
691
|
if file_path_lower.endswith(extension):
|
|
624
692
|
file_type = mime_type
|
|
625
693
|
break
|
|
@@ -726,7 +794,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
726
794
|
def do_NEW(self, chat_name):
|
|
727
795
|
"""Create a new chat. Optionally provide a chat name in-line: NEW new_chat"""
|
|
728
796
|
if chat_name == "":
|
|
729
|
-
|
|
797
|
+
print("What should be the new chat name? ", end="", flush=True)
|
|
798
|
+
chat_name = sys.stdin.readline().strip()
|
|
730
799
|
current_directory = os.path.dirname(self.chat_name)
|
|
731
800
|
chat_file_path = os.path.join(current_directory, chat_name)
|
|
732
801
|
self.__init__(chat_file_path)
|
|
@@ -739,7 +808,8 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
739
808
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
740
809
|
def do_CLEAR(self, _):
|
|
741
810
|
"""Clear the chat and the file containing it"""
|
|
742
|
-
|
|
811
|
+
print("Are you sure you want to clear the chat? (y/N): ", end="", flush=True)
|
|
812
|
+
user_input = sys.stdin.readline().strip()
|
|
743
813
|
if user_input.lower() != "y":
|
|
744
814
|
return
|
|
745
815
|
self.create_empty_chat_file(self.chat_name)
|
|
@@ -750,17 +820,23 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
750
820
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
751
821
|
def do_LOAD_RULES(self, rules_name):
|
|
752
822
|
"""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"""
|
|
753
|
-
self.template_loader.load_template(
|
|
823
|
+
self.template_loader.load_template(
|
|
824
|
+
rules_name, "rules", self.chat_name, "*.rules.md"
|
|
825
|
+
)
|
|
754
826
|
|
|
755
827
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
756
828
|
def do_LOAD_INTENTION(self, intention_name):
|
|
757
829
|
"""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"""
|
|
758
|
-
self.template_loader.load_template(
|
|
830
|
+
self.template_loader.load_template(
|
|
831
|
+
intention_name, "intention", self.chat_name, "*.intention.md"
|
|
832
|
+
)
|
|
759
833
|
|
|
760
834
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
761
835
|
def do_LOAD_COMMANDS(self, commands_name):
|
|
762
836
|
"""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"""
|
|
763
|
-
self.template_loader.load_template(
|
|
837
|
+
self.template_loader.load_template(
|
|
838
|
+
commands_name, "commands", self.chat_name, "*.commands.md"
|
|
839
|
+
)
|
|
764
840
|
|
|
765
841
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
766
842
|
def do_LOAD_BLUEPRINT(self, blueprint_name):
|
|
@@ -934,7 +1010,7 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
934
1010
|
def do_SEND(self, _):
|
|
935
1011
|
"""Send prompt to the LLM"""
|
|
936
1012
|
message = "\n".join(self.message_buffer)
|
|
937
|
-
self.save_message(
|
|
1013
|
+
self.save_message(ROLE_PROMPT, message)
|
|
938
1014
|
self.send_message()
|
|
939
1015
|
|
|
940
1016
|
@cmd2.with_category(CATEGORY_CHAT_CONTROL)
|
|
@@ -1053,7 +1129,9 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
1053
1129
|
|
|
1054
1130
|
def _template_completer(self, text: str, template_type: str) -> list[str]:
|
|
1055
1131
|
"""Generic completer for different template types."""
|
|
1056
|
-
available_templates = self.template_loader.get_available_templates(
|
|
1132
|
+
available_templates = self.template_loader.get_available_templates(
|
|
1133
|
+
template_type, os.path.dirname(self.chat_name)
|
|
1134
|
+
)
|
|
1057
1135
|
if not text:
|
|
1058
1136
|
return available_templates
|
|
1059
1137
|
return [t for t in available_templates if t.startswith(text)]
|
|
@@ -1072,4 +1150,155 @@ Start chatting (type 'HELP'/'h' for available commands, 'QUIT'/'q' to exit chat
|
|
|
1072
1150
|
|
|
1073
1151
|
def complete_LOAD_BLUEPRINT(self, text, line, begidx, endidx):
|
|
1074
1152
|
"""Completer for the LOAD_BLUEPRINT command."""
|
|
1075
|
-
return self._template_completer(text, "blueprint")
|
|
1153
|
+
return self._template_completer(text, "blueprint")
|
|
1154
|
+
|
|
1155
|
+
def _select_script_from_list(
|
|
1156
|
+
self, scripts: list[str], not_found_message: str, prompt: str
|
|
1157
|
+
) -> str | None:
|
|
1158
|
+
"""Displays a list of scripts and prompts the user to select one."""
|
|
1159
|
+
if not scripts:
|
|
1160
|
+
self.poutput(not_found_message)
|
|
1161
|
+
return None
|
|
1162
|
+
|
|
1163
|
+
# Sort the scripts alphabetically by their basename for consistent display
|
|
1164
|
+
# Create a list of (basename, full_script_name) tuples for sorting and later retrieval
|
|
1165
|
+
scripts_with_basenames = [(os.path.basename(s), s) for s in scripts]
|
|
1166
|
+
scripts_with_basenames.sort(
|
|
1167
|
+
key=lambda x: x[0].lower()
|
|
1168
|
+
) # Sort by lowercase basename
|
|
1169
|
+
|
|
1170
|
+
for i, (basename, full_script_name) in enumerate(scripts_with_basenames):
|
|
1171
|
+
self.poutput(f"{i + 1}: {basename}")
|
|
1172
|
+
|
|
1173
|
+
try:
|
|
1174
|
+
choice = input(prompt)
|
|
1175
|
+
choice_index = int(choice) - 1
|
|
1176
|
+
if 0 <= choice_index < len(scripts_with_basenames):
|
|
1177
|
+
# Return the full script name from the sorted list
|
|
1178
|
+
return scripts_with_basenames[choice_index][1]
|
|
1179
|
+
else:
|
|
1180
|
+
self.poutput("Invalid choice. Aborting.")
|
|
1181
|
+
return None
|
|
1182
|
+
except (ValueError, EOFError):
|
|
1183
|
+
self.poutput("Invalid input. Aborting.")
|
|
1184
|
+
return None
|
|
1185
|
+
|
|
1186
|
+
@cmd2.with_category(CATEGORY_SCRIPT_CONTROL)
|
|
1187
|
+
def do_run_pyscript(self, args):
|
|
1188
|
+
"""Run a python script from the chat.
|
|
1189
|
+
Usage: run_pyscript <script_name> [args...]
|
|
1190
|
+
"""
|
|
1191
|
+
script_name, script_args = self._parse_run_pyscript_args(args)
|
|
1192
|
+
|
|
1193
|
+
# If no script name provided, list available scripts grouped by type
|
|
1194
|
+
if not script_name:
|
|
1195
|
+
self._list_available_scripts()
|
|
1196
|
+
return
|
|
1197
|
+
|
|
1198
|
+
script_to_run = self._resolve_script_to_run(script_name, script_args)
|
|
1199
|
+
|
|
1200
|
+
if not script_to_run:
|
|
1201
|
+
return
|
|
1202
|
+
|
|
1203
|
+
# Pass arguments to script runner
|
|
1204
|
+
output = self.script_runner.run_script(script_to_run, script_args)
|
|
1205
|
+
if output:
|
|
1206
|
+
self.poutput(output.strip())
|
|
1207
|
+
|
|
1208
|
+
def _list_available_scripts(self):
|
|
1209
|
+
"""Lists available scripts grouped by type (global and custom)."""
|
|
1210
|
+
global_scripts = self.script_lister.get_global_scripts()
|
|
1211
|
+
custom_scripts = self.script_lister.get_custom_scripts()
|
|
1212
|
+
|
|
1213
|
+
if not global_scripts and not custom_scripts:
|
|
1214
|
+
self.poutput("No scripts found.")
|
|
1215
|
+
return
|
|
1216
|
+
|
|
1217
|
+
self.poutput("Available scripts:")
|
|
1218
|
+
self.poutput("")
|
|
1219
|
+
|
|
1220
|
+
if custom_scripts:
|
|
1221
|
+
self.poutput("Custom scripts:")
|
|
1222
|
+
for script in sorted(custom_scripts):
|
|
1223
|
+
self.poutput(f" {script}")
|
|
1224
|
+
self.poutput("")
|
|
1225
|
+
|
|
1226
|
+
if global_scripts:
|
|
1227
|
+
self.poutput("Global scripts:")
|
|
1228
|
+
for script in sorted(global_scripts):
|
|
1229
|
+
self.poutput(f" global/{script}")
|
|
1230
|
+
|
|
1231
|
+
def _parse_run_pyscript_args(self, args):
|
|
1232
|
+
"""Parses arguments for run_pyscript command."""
|
|
1233
|
+
import shlex
|
|
1234
|
+
|
|
1235
|
+
if not args:
|
|
1236
|
+
return "", []
|
|
1237
|
+
|
|
1238
|
+
# args is a cmd2.Statement (subclass of str), so we can use it directly
|
|
1239
|
+
full_args = str(args)
|
|
1240
|
+
# Use shlex to split arguments, enabling quoted args support
|
|
1241
|
+
split_args = shlex.split(full_args)
|
|
1242
|
+
if not split_args:
|
|
1243
|
+
return "", []
|
|
1244
|
+
|
|
1245
|
+
script_name = split_args[0]
|
|
1246
|
+
script_args = split_args[1:] if len(split_args) > 1 else []
|
|
1247
|
+
return script_name, script_args
|
|
1248
|
+
|
|
1249
|
+
def _resolve_script_to_run(self, script_name, script_args):
|
|
1250
|
+
"""Resolves the script name to run."""
|
|
1251
|
+
return script_name
|
|
1252
|
+
|
|
1253
|
+
def complete_run_pyscript(self, text, line, begidx, endidx):
|
|
1254
|
+
"""Completer for the run_pyscript command."""
|
|
1255
|
+
# Get all scripts: ['custom.py', 'global/global.py']
|
|
1256
|
+
available_scripts = self.script_lister.get_all_scripts()
|
|
1257
|
+
|
|
1258
|
+
# Add special commands
|
|
1259
|
+
special_commands = [
|
|
1260
|
+
# "global/"
|
|
1261
|
+
# "*", "global/*"
|
|
1262
|
+
]
|
|
1263
|
+
|
|
1264
|
+
possible_completions = sorted(list(set(available_scripts + special_commands)))
|
|
1265
|
+
|
|
1266
|
+
# Filter based on what the user has typed
|
|
1267
|
+
return [s for s in possible_completions if s.startswith(text)]
|
|
1268
|
+
|
|
1269
|
+
# ===== AGENT CONTROL COMMANDS =====
|
|
1270
|
+
|
|
1271
|
+
@cmd2.with_category(CATEGORY_AGENT_CONTROL)
|
|
1272
|
+
def do_LIST_AGENTS(self, _):
|
|
1273
|
+
"""Lists all available executable binary agents."""
|
|
1274
|
+
from ara_cli.commands.list_agents_command import ListAgentsCommand
|
|
1275
|
+
|
|
1276
|
+
command = ListAgentsCommand(chat_instance=self)
|
|
1277
|
+
command.execute()
|
|
1278
|
+
|
|
1279
|
+
@cmd2.with_category(CATEGORY_AGENT_CONTROL)
|
|
1280
|
+
def do_AGENT_RUN(self, args):
|
|
1281
|
+
"""Run a binary agent interactively from the 'ara/.araconfig/agents' directory.
|
|
1282
|
+
Usage: AGENT_RUN <agent_name> [arg1] [arg2] ...
|
|
1283
|
+
Example:
|
|
1284
|
+
AGENT_RUN feature-creation -b .
|
|
1285
|
+
"""
|
|
1286
|
+
|
|
1287
|
+
from ara_cli.commands.agent_run_command import AgentRunCommand
|
|
1288
|
+
|
|
1289
|
+
command = AgentRunCommand(self, args)
|
|
1290
|
+
command.execute()
|
|
1291
|
+
|
|
1292
|
+
def complete_AGENT_RUN(self, text, line, begidx, endidx):
|
|
1293
|
+
"""Completer for AGENT_RUN command."""
|
|
1294
|
+
from ara_cli.commands.list_agents_command import list_available_binary_agents
|
|
1295
|
+
|
|
1296
|
+
parts = line.split()
|
|
1297
|
+
# This completer runs when the user is typing the first argument (the agent name)
|
|
1298
|
+
if len(parts) < 2 or (len(parts) == 2 and not line.endswith(" ")):
|
|
1299
|
+
available_agents = list_available_binary_agents(self)
|
|
1300
|
+
if not text:
|
|
1301
|
+
return available_agents
|
|
1302
|
+
return [a for a in available_agents if a.startswith(text)]
|
|
1303
|
+
# For subsequent arguments, we can offer file/directory completion
|
|
1304
|
+
return self.path_complete(text, line, begidx, endidx)
|
|
File without changes
|