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/completers.py
CHANGED
|
@@ -4,18 +4,17 @@ from pathlib import Path
|
|
|
4
4
|
import typer
|
|
5
5
|
|
|
6
6
|
from ara_cli.classifier import Classifier
|
|
7
|
-
from ara_cli.
|
|
7
|
+
from ara_cli.constants import VALID_ASPECTS
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def complete_classifier(incomplete: str) -> List[str]:
|
|
11
11
|
"""Complete classifier names."""
|
|
12
|
-
|
|
13
|
-
return [c for c in classifiers if c.startswith(incomplete)]
|
|
12
|
+
return [c for c in Classifier.ordered_classifiers() if c.startswith(incomplete)]
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def complete_aspect(incomplete: str) -> List[str]:
|
|
17
16
|
"""Complete aspect names."""
|
|
18
|
-
aspects =
|
|
17
|
+
aspects = VALID_ASPECTS
|
|
19
18
|
return [a for a in aspects if a.startswith(incomplete)]
|
|
20
19
|
|
|
21
20
|
|
|
@@ -31,23 +30,31 @@ def complete_template_type(incomplete: str) -> List[str]:
|
|
|
31
30
|
return [t for t in template_types if t.startswith(incomplete)]
|
|
32
31
|
|
|
33
32
|
|
|
34
|
-
def complete_artefact_name(classifier: str) -> List[str]:
|
|
33
|
+
def complete_artefact_name(classifier: str, incomplete: str = "") -> List[str]:
|
|
35
34
|
"""Complete artefact names for a given classifier."""
|
|
36
35
|
try:
|
|
37
36
|
# Get the directory for the classifier
|
|
38
37
|
classifier_dir = f"ara/{Classifier.get_sub_directory(classifier)}"
|
|
39
|
-
|
|
38
|
+
|
|
40
39
|
if not os.path.exists(classifier_dir):
|
|
41
40
|
return []
|
|
42
|
-
|
|
41
|
+
|
|
43
42
|
# Find all files with the classifier extension
|
|
44
43
|
artefacts = []
|
|
44
|
+
suffix = f".{classifier}"
|
|
45
|
+
|
|
46
|
+
# We only care about files that match the incomplete prefix AND the suffix
|
|
47
|
+
# Since os.listdir gives filenames, we can check startswith(incomplete)
|
|
48
|
+
# but we must be careful because the file starts with the artefact name, not necessarily the incomplete part + suffix directly unless full match.
|
|
49
|
+
# Actually, incomplete corresponds to the artefact name part.
|
|
50
|
+
# So file must start with incomplete AND end with suffix.
|
|
51
|
+
|
|
45
52
|
for file in os.listdir(classifier_dir):
|
|
46
|
-
if file.endswith(
|
|
53
|
+
if file.startswith(incomplete) and file.endswith(suffix):
|
|
47
54
|
# Remove the extension to get the artefact name
|
|
48
|
-
name = file[
|
|
55
|
+
name = file[: -len(suffix)]
|
|
49
56
|
artefacts.append(name)
|
|
50
|
-
|
|
57
|
+
|
|
51
58
|
return sorted(artefacts)
|
|
52
59
|
except Exception:
|
|
53
60
|
return []
|
|
@@ -55,9 +62,10 @@ def complete_artefact_name(classifier: str) -> List[str]:
|
|
|
55
62
|
|
|
56
63
|
def complete_artefact_name_for_classifier(classifier: str):
|
|
57
64
|
"""Create a completer function for artefact names of a specific classifier."""
|
|
65
|
+
|
|
58
66
|
def completer(incomplete: str) -> List[str]:
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
return complete_artefact_name(classifier, incomplete)
|
|
68
|
+
|
|
61
69
|
return completer
|
|
62
70
|
|
|
63
71
|
|
|
@@ -66,79 +74,131 @@ def complete_chat_files(incomplete: str) -> List[str]:
|
|
|
66
74
|
try:
|
|
67
75
|
chat_files = []
|
|
68
76
|
current_dir = Path.cwd()
|
|
69
|
-
|
|
70
|
-
#
|
|
71
|
-
for
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
|
|
78
|
+
# Optimize by using glob with the incomplete prefix
|
|
79
|
+
# Note: glob pattern needs to be careful with special chars, but for autocompletion usually fine.
|
|
80
|
+
pattern = f"{incomplete}*.md" if incomplete else "*.md"
|
|
81
|
+
|
|
82
|
+
for file in current_dir.glob(pattern):
|
|
83
|
+
chat_files.append(file.stem)
|
|
84
|
+
|
|
76
85
|
return sorted(chat_files)
|
|
77
86
|
except Exception:
|
|
78
87
|
return []
|
|
79
88
|
|
|
80
89
|
|
|
90
|
+
def complete_prompt_step(incomplete: str) -> List[str]:
|
|
91
|
+
"""Complete prompt step/subcommand names."""
|
|
92
|
+
steps = [
|
|
93
|
+
"init",
|
|
94
|
+
"load",
|
|
95
|
+
"send",
|
|
96
|
+
"load-and-send",
|
|
97
|
+
"extract",
|
|
98
|
+
"update",
|
|
99
|
+
"chat",
|
|
100
|
+
"init-rag",
|
|
101
|
+
]
|
|
102
|
+
return [s for s in steps if s.startswith(incomplete)]
|
|
103
|
+
|
|
104
|
+
|
|
81
105
|
# Dynamic completers that need context
|
|
82
106
|
class DynamicCompleters:
|
|
83
107
|
@staticmethod
|
|
84
108
|
def create_classifier_completer():
|
|
85
109
|
"""Create a completer for classifiers."""
|
|
110
|
+
|
|
86
111
|
def completer(ctx: typer.Context, incomplete: str) -> List[str]:
|
|
87
112
|
return complete_classifier(incomplete)
|
|
113
|
+
|
|
88
114
|
return completer
|
|
89
|
-
|
|
115
|
+
|
|
90
116
|
@staticmethod
|
|
91
117
|
def create_aspect_completer():
|
|
92
118
|
"""Create a completer for aspects."""
|
|
119
|
+
|
|
93
120
|
def completer(ctx: typer.Context, incomplete: str) -> List[str]:
|
|
94
121
|
return complete_aspect(incomplete)
|
|
122
|
+
|
|
95
123
|
return completer
|
|
96
|
-
|
|
124
|
+
|
|
97
125
|
@staticmethod
|
|
98
126
|
def create_status_completer():
|
|
99
127
|
"""Create a completer for status values."""
|
|
128
|
+
|
|
100
129
|
def completer(ctx: typer.Context, incomplete: str) -> List[str]:
|
|
101
130
|
return complete_status(incomplete)
|
|
131
|
+
|
|
102
132
|
return completer
|
|
103
|
-
|
|
133
|
+
|
|
104
134
|
@staticmethod
|
|
105
135
|
def create_template_type_completer():
|
|
106
136
|
"""Create a completer for template types."""
|
|
137
|
+
|
|
107
138
|
def completer(ctx: typer.Context, incomplete: str) -> List[str]:
|
|
108
139
|
return complete_template_type(incomplete)
|
|
140
|
+
|
|
109
141
|
return completer
|
|
110
|
-
|
|
142
|
+
|
|
111
143
|
@staticmethod
|
|
112
144
|
def create_artefact_name_completer():
|
|
113
145
|
"""Create a completer for artefact names based on classifier context."""
|
|
146
|
+
|
|
114
147
|
def completer(ctx: typer.Context, incomplete: str) -> List[str]:
|
|
115
148
|
# Try to get classifier from context
|
|
116
|
-
if hasattr(ctx,
|
|
117
|
-
classifier = ctx.params[
|
|
118
|
-
if hasattr(classifier,
|
|
149
|
+
if hasattr(ctx, "params") and "classifier" in ctx.params:
|
|
150
|
+
classifier = ctx.params["classifier"]
|
|
151
|
+
if hasattr(classifier, "value"):
|
|
119
152
|
classifier = classifier.value
|
|
120
|
-
|
|
121
|
-
return [a for a in artefacts if a.startswith(incomplete)]
|
|
153
|
+
return complete_artefact_name(classifier, incomplete)
|
|
122
154
|
return []
|
|
155
|
+
|
|
123
156
|
return completer
|
|
124
|
-
|
|
157
|
+
|
|
125
158
|
@staticmethod
|
|
126
159
|
def create_parent_name_completer():
|
|
127
160
|
"""Create a completer for parent artefact names based on parent classifier context."""
|
|
161
|
+
|
|
128
162
|
def completer(ctx: typer.Context, incomplete: str) -> List[str]:
|
|
129
163
|
# Try to get parent_classifier from context
|
|
130
|
-
if hasattr(ctx,
|
|
131
|
-
parent_classifier = ctx.params[
|
|
132
|
-
if hasattr(parent_classifier,
|
|
164
|
+
if hasattr(ctx, "params") and "parent_classifier" in ctx.params:
|
|
165
|
+
parent_classifier = ctx.params["parent_classifier"]
|
|
166
|
+
if hasattr(parent_classifier, "value"):
|
|
133
167
|
parent_classifier = parent_classifier.value
|
|
134
|
-
|
|
135
|
-
return [a for a in artefacts if a.startswith(incomplete)]
|
|
168
|
+
return complete_artefact_name(parent_classifier, incomplete)
|
|
136
169
|
return []
|
|
170
|
+
|
|
137
171
|
return completer
|
|
138
|
-
|
|
172
|
+
|
|
139
173
|
@staticmethod
|
|
140
174
|
def create_chat_file_completer():
|
|
141
175
|
"""Create a completer for chat files."""
|
|
176
|
+
|
|
142
177
|
def completer(ctx: typer.Context, incomplete: str) -> List[str]:
|
|
143
178
|
return complete_chat_files(incomplete)
|
|
179
|
+
|
|
180
|
+
return completer
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def create_prompt_step_completer():
|
|
184
|
+
"""Create a completer for prompt step/subcommand names."""
|
|
185
|
+
|
|
186
|
+
def completer(ctx: typer.Context, incomplete: str) -> List[str]:
|
|
187
|
+
return complete_prompt_step(incomplete)
|
|
188
|
+
|
|
189
|
+
return completer
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def create_convert_source_artefact_name_completer():
|
|
193
|
+
"""Create a completer for convert command source artefact names based on old_classifier context."""
|
|
194
|
+
|
|
195
|
+
def completer(ctx: typer.Context, incomplete: str) -> List[str]:
|
|
196
|
+
# Try to get old_classifier from context
|
|
197
|
+
if hasattr(ctx, "params") and "old_classifier" in ctx.params:
|
|
198
|
+
old_classifier = ctx.params["old_classifier"]
|
|
199
|
+
if hasattr(old_classifier, "value"):
|
|
200
|
+
old_classifier = old_classifier.value
|
|
201
|
+
return complete_artefact_name(old_classifier, incomplete)
|
|
202
|
+
return []
|
|
203
|
+
|
|
144
204
|
return completer
|
ara_cli/constants.py
ADDED
ara_cli/directory_navigator.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import sys
|
|
2
3
|
from os.path import join, exists, isdir, dirname, basename
|
|
3
4
|
# from ara_cli.directory_searcher import DirectorySearcher
|
|
4
5
|
|
|
@@ -23,7 +24,8 @@ class DirectoryNavigator:
|
|
|
23
24
|
return original_directory
|
|
24
25
|
|
|
25
26
|
current_directory = original_directory
|
|
26
|
-
|
|
27
|
+
# Ensure loop breaks at root
|
|
28
|
+
while current_directory != dirname(current_directory):
|
|
27
29
|
potential_path = join(current_directory, self.target_directory)
|
|
28
30
|
if self.exists(potential_path):
|
|
29
31
|
os.chdir(potential_path)
|
|
@@ -31,7 +33,8 @@ class DirectoryNavigator:
|
|
|
31
33
|
current_directory = dirname(current_directory)
|
|
32
34
|
|
|
33
35
|
# If the loop completes, the target directory was not found
|
|
34
|
-
user_input = input(
|
|
36
|
+
user_input = input(
|
|
37
|
+
f"Unable to locate the '{self.target_directory}' directory. Do you want to create an 'ara' folder in the working directory? (y/N): ").strip().lower()
|
|
35
38
|
|
|
36
39
|
if user_input == '' or user_input == 'y':
|
|
37
40
|
ara_folder_path = join(original_directory, 'ara')
|
|
@@ -40,7 +43,8 @@ class DirectoryNavigator:
|
|
|
40
43
|
os.chdir(ara_folder_path)
|
|
41
44
|
return original_directory
|
|
42
45
|
else:
|
|
43
|
-
print(
|
|
46
|
+
print(
|
|
47
|
+
f"Unable to locate the '{self.target_directory}' directory and user declined to create 'ara' folder.")
|
|
44
48
|
sys.exit(0)
|
|
45
49
|
|
|
46
50
|
def navigate_to_relative(self, relative_path):
|
|
@@ -56,7 +60,36 @@ class DirectoryNavigator:
|
|
|
56
60
|
if self.exists(path):
|
|
57
61
|
os.chdir(path)
|
|
58
62
|
else:
|
|
59
|
-
raise Exception(
|
|
63
|
+
raise Exception(
|
|
64
|
+
f"Unable to navigate to '{relative_path}' relative to the target directory.")
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def find_ara_directory_root():
|
|
68
|
+
"""Find the root ara directory by traversing up the directory tree."""
|
|
69
|
+
current_dir = os.getcwd()
|
|
70
|
+
|
|
71
|
+
# Check if we're already inside an ara directory structure
|
|
72
|
+
path_parts = current_dir.split(os.sep)
|
|
73
|
+
|
|
74
|
+
# Look for 'ara' in the path parts
|
|
75
|
+
if 'ara' in path_parts:
|
|
76
|
+
ara_index = path_parts.index('ara')
|
|
77
|
+
# Reconstruct path up to and including 'ara'
|
|
78
|
+
ara_root_parts = path_parts[:ara_index + 1]
|
|
79
|
+
potential_ara_root = os.sep.join(ara_root_parts)
|
|
80
|
+
if os.path.exists(potential_ara_root) and os.path.isdir(potential_ara_root):
|
|
81
|
+
return potential_ara_root
|
|
82
|
+
|
|
83
|
+
# If not inside ara directory, check current directory and parents
|
|
84
|
+
check_dir = current_dir
|
|
85
|
+
# Stop at filesystem root
|
|
86
|
+
while check_dir != os.path.dirname(check_dir):
|
|
87
|
+
ara_path = os.path.join(check_dir, 'ara')
|
|
88
|
+
if os.path.exists(ara_path) and os.path.isdir(ara_path):
|
|
89
|
+
return ara_path
|
|
90
|
+
check_dir = os.path.dirname(check_dir)
|
|
91
|
+
|
|
92
|
+
return None
|
|
60
93
|
|
|
61
94
|
# debug version
|
|
62
95
|
# def get_ara_directory(self):
|
ara_cli/error_handler.py
CHANGED
|
@@ -5,8 +5,9 @@ from enum import Enum
|
|
|
5
5
|
from functools import wraps
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
RED =
|
|
9
|
-
|
|
8
|
+
RED = "\033[91m"
|
|
9
|
+
YELLOW = "\033[93m"
|
|
10
|
+
RESET = "\033[0m"
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class ErrorLevel(Enum):
|
|
@@ -69,7 +70,6 @@ class ErrorHandler:
|
|
|
69
70
|
|
|
70
71
|
sys.exit(1)
|
|
71
72
|
|
|
72
|
-
|
|
73
73
|
def report_error(self, error: Exception, context: Optional[str] = None) -> None:
|
|
74
74
|
"""Report error with standardized formatting but don't exit"""
|
|
75
75
|
if isinstance(error, AraError):
|
|
@@ -77,33 +77,44 @@ class ErrorHandler:
|
|
|
77
77
|
else:
|
|
78
78
|
self._report_generic_error(error, context)
|
|
79
79
|
|
|
80
|
-
|
|
81
80
|
def _report_ara_error(self, error: AraError, context: Optional[str] = None) -> None:
|
|
82
81
|
"""Report ARA-specific errors without exiting"""
|
|
83
82
|
error_prefix = f"[{error.level.value}]"
|
|
84
83
|
|
|
84
|
+
# Choose color based on error level
|
|
85
|
+
if error.level in (ErrorLevel.INFO, ErrorLevel.WARNING):
|
|
86
|
+
color = YELLOW
|
|
87
|
+
else:
|
|
88
|
+
color = RED
|
|
89
|
+
|
|
85
90
|
if context:
|
|
86
|
-
print(
|
|
91
|
+
print(
|
|
92
|
+
f"{color}{error_prefix} {context}: {error.message}{RESET}",
|
|
93
|
+
file=sys.stderr,
|
|
94
|
+
)
|
|
87
95
|
else:
|
|
88
|
-
print(f"{
|
|
96
|
+
print(f"{color}{error_prefix} {error.message}{RESET}", file=sys.stderr)
|
|
89
97
|
|
|
90
98
|
if self.debug_mode:
|
|
91
99
|
traceback.print_exc()
|
|
92
100
|
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
def _report_generic_error(
|
|
102
|
+
self, error: Exception, context: Optional[str] = None
|
|
103
|
+
) -> None:
|
|
95
104
|
"""Report generic Python errors without exiting"""
|
|
96
105
|
error_type = type(error).__name__
|
|
97
106
|
|
|
98
107
|
if context:
|
|
99
|
-
print(
|
|
108
|
+
print(
|
|
109
|
+
f"{RED}[ERROR] {context}: {error_type}: {str(error)}{RESET}",
|
|
110
|
+
file=sys.stderr,
|
|
111
|
+
)
|
|
100
112
|
else:
|
|
101
113
|
print(f"{RED}[ERROR] {error_type}: {str(error)}{RESET}", file=sys.stderr)
|
|
102
114
|
|
|
103
115
|
if self.debug_mode:
|
|
104
116
|
traceback.print_exc()
|
|
105
117
|
|
|
106
|
-
|
|
107
118
|
def validate_and_exit(
|
|
108
119
|
self, condition: bool, message: str, error_code: int = 1
|
|
109
120
|
) -> None:
|
|
@@ -112,7 +123,11 @@ class ErrorHandler:
|
|
|
112
123
|
raise AraValidationError(message)
|
|
113
124
|
|
|
114
125
|
|
|
115
|
-
def handle_errors(
|
|
126
|
+
def handle_errors(
|
|
127
|
+
_func=None,
|
|
128
|
+
context: Optional[str] = None,
|
|
129
|
+
error_handler: Optional[ErrorHandler] = None,
|
|
130
|
+
):
|
|
116
131
|
"""Decorator to handle errors in action functions"""
|
|
117
132
|
|
|
118
133
|
def decorator(func):
|
|
@@ -61,185 +61,7 @@ class DocumentReader(ABC):
|
|
|
61
61
|
return relative_image_path, description
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
class DocxReader(DocumentReader):
|
|
65
|
-
"""Reader for DOCX files"""
|
|
66
64
|
|
|
67
|
-
def read(self, extract_images: bool = False) -> str:
|
|
68
|
-
import docx
|
|
69
|
-
|
|
70
|
-
doc = docx.Document(self.file_path)
|
|
71
|
-
text_content = '\n'.join(para.text for para in doc.paragraphs)
|
|
72
|
-
|
|
73
|
-
if not extract_images:
|
|
74
|
-
return text_content
|
|
75
|
-
|
|
76
|
-
from PIL import Image
|
|
77
|
-
import io
|
|
78
|
-
|
|
79
|
-
# Create data directory for images
|
|
80
|
-
images_dir = self.create_image_data_dir("docx")
|
|
81
|
-
|
|
82
|
-
# Extract and process images
|
|
83
|
-
image_descriptions = []
|
|
84
|
-
image_counter = 1
|
|
85
|
-
|
|
86
|
-
for rel in doc.part.rels.values():
|
|
87
|
-
if "image" in rel.reltype:
|
|
88
|
-
image_data = rel.target_part.blob
|
|
89
|
-
|
|
90
|
-
# Determine image format
|
|
91
|
-
image = Image.open(io.BytesIO(image_data))
|
|
92
|
-
image_format = image.format.lower()
|
|
93
|
-
|
|
94
|
-
# Save and describe image
|
|
95
|
-
relative_path, description = self.save_and_describe_image(
|
|
96
|
-
image_data, image_format, images_dir, image_counter
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
# Add formatted description to list
|
|
100
|
-
image_description = f"\nImage: {relative_path}\n[{description}]\n"
|
|
101
|
-
image_descriptions.append(image_description)
|
|
102
|
-
|
|
103
|
-
image_counter += 1
|
|
104
|
-
|
|
105
|
-
# Combine text content with image descriptions
|
|
106
|
-
if image_descriptions:
|
|
107
|
-
text_content += "\n\n### Extracted Images\n" + \
|
|
108
|
-
"\n".join(image_descriptions)
|
|
109
|
-
|
|
110
|
-
return text_content
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
class PdfReader(DocumentReader):
|
|
114
|
-
"""Reader for PDF files"""
|
|
115
|
-
|
|
116
|
-
def read(self, extract_images: bool = False) -> str:
|
|
117
|
-
import pymupdf4llm
|
|
118
|
-
|
|
119
|
-
if not extract_images:
|
|
120
|
-
return pymupdf4llm.to_markdown(self.file_path, write_images=False)
|
|
121
|
-
|
|
122
|
-
import fitz # PyMuPDF
|
|
123
|
-
|
|
124
|
-
# Create images directory
|
|
125
|
-
images_dir = self.create_image_data_dir("pdf")
|
|
126
|
-
|
|
127
|
-
# Extract text without images first
|
|
128
|
-
text_content = pymupdf4llm.to_markdown(
|
|
129
|
-
self.file_path, write_images=False)
|
|
130
|
-
|
|
131
|
-
# Extract and process images
|
|
132
|
-
doc = fitz.open(self.file_path)
|
|
133
|
-
image_descriptions = []
|
|
134
|
-
image_counter = 1
|
|
135
|
-
|
|
136
|
-
for page_num, page in enumerate(doc):
|
|
137
|
-
image_list = page.get_images()
|
|
138
|
-
|
|
139
|
-
for img_index, img in enumerate(image_list):
|
|
140
|
-
# Extract image
|
|
141
|
-
xref = img[0]
|
|
142
|
-
base_image = doc.extract_image(xref)
|
|
143
|
-
image_bytes = base_image["image"]
|
|
144
|
-
image_ext = base_image["ext"]
|
|
145
|
-
|
|
146
|
-
# Save and describe image
|
|
147
|
-
relative_path, description = self.save_and_describe_image(
|
|
148
|
-
image_bytes, image_ext, images_dir, image_counter
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
# Add formatted description to list
|
|
152
|
-
image_description = f"\nImage: {relative_path}\n[{description}]\n"
|
|
153
|
-
image_descriptions.append(image_description)
|
|
154
|
-
|
|
155
|
-
image_counter += 1
|
|
156
|
-
|
|
157
|
-
doc.close()
|
|
158
|
-
|
|
159
|
-
# Combine text content with image descriptions
|
|
160
|
-
if image_descriptions:
|
|
161
|
-
text_content += "\n\n### Extracted Images\n" + \
|
|
162
|
-
"\n".join(image_descriptions)
|
|
163
|
-
|
|
164
|
-
return text_content
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
class OdtReader(DocumentReader):
|
|
168
|
-
"""Reader for ODT files"""
|
|
169
|
-
|
|
170
|
-
def read(self, extract_images: bool = False) -> str:
|
|
171
|
-
import pymupdf4llm
|
|
172
|
-
|
|
173
|
-
if not extract_images:
|
|
174
|
-
return pymupdf4llm.to_markdown(self.file_path, write_images=False)
|
|
175
|
-
|
|
176
|
-
import zipfile
|
|
177
|
-
from PIL import Image
|
|
178
|
-
import io
|
|
179
|
-
|
|
180
|
-
# Create data directory for images
|
|
181
|
-
images_dir = self.create_image_data_dir("odt")
|
|
182
|
-
|
|
183
|
-
# Get text content
|
|
184
|
-
text_content = pymupdf4llm.to_markdown(
|
|
185
|
-
self.file_path, write_images=False)
|
|
186
|
-
|
|
187
|
-
# Extract and process images from ODT
|
|
188
|
-
image_descriptions = []
|
|
189
|
-
image_counter = 1
|
|
190
|
-
|
|
191
|
-
try:
|
|
192
|
-
with zipfile.ZipFile(self.file_path, 'r') as odt_zip:
|
|
193
|
-
# List all files in the Pictures directory
|
|
194
|
-
picture_files = [
|
|
195
|
-
f for f in odt_zip.namelist() if f.startswith('Pictures/')]
|
|
196
|
-
|
|
197
|
-
for picture_file in picture_files:
|
|
198
|
-
# Extract image data
|
|
199
|
-
image_data = odt_zip.read(picture_file)
|
|
200
|
-
|
|
201
|
-
# Determine image format
|
|
202
|
-
image = Image.open(io.BytesIO(image_data))
|
|
203
|
-
image_format = image.format.lower()
|
|
204
|
-
|
|
205
|
-
# Save and describe image
|
|
206
|
-
relative_path, description = self.save_and_describe_image(
|
|
207
|
-
image_data, image_format, images_dir, image_counter
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
# Add formatted description to list
|
|
211
|
-
image_description = f"\nImage: {relative_path}\n[{description}]\n"
|
|
212
|
-
image_descriptions.append(image_description)
|
|
213
|
-
|
|
214
|
-
image_counter += 1
|
|
215
|
-
except Exception as e:
|
|
216
|
-
print(f"Warning: Could not extract images from ODT: {e}")
|
|
217
|
-
|
|
218
|
-
# Combine text content with image descriptions
|
|
219
|
-
if image_descriptions:
|
|
220
|
-
text_content += "\n\n### Extracted Images\n" + \
|
|
221
|
-
"\n".join(image_descriptions)
|
|
222
|
-
|
|
223
|
-
return text_content
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
class DocumentReaderFactory:
|
|
227
|
-
"""Factory for creating appropriate document readers"""
|
|
228
|
-
|
|
229
|
-
@staticmethod
|
|
230
|
-
def create_reader(file_path: str) -> Optional[DocumentReader]:
|
|
231
|
-
"""Create appropriate reader based on file extension"""
|
|
232
|
-
_, ext = os.path.splitext(file_path)
|
|
233
|
-
ext = ext.lower()
|
|
234
65
|
|
|
235
|
-
readers = {
|
|
236
|
-
'.docx': DocxReader,
|
|
237
|
-
'.pdf': PdfReader,
|
|
238
|
-
'.odt': OdtReader
|
|
239
|
-
}
|
|
240
66
|
|
|
241
|
-
reader_class = readers.get(ext)
|
|
242
|
-
if reader_class:
|
|
243
|
-
return reader_class(file_path)
|
|
244
67
|
|
|
245
|
-
return None
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from ara_cli.file_loaders.document_reader import DocumentReader
|
|
4
|
+
from ara_cli.file_loaders.readers.docx_reader import DocxReader
|
|
5
|
+
from ara_cli.file_loaders.readers.pdf_reader import PdfReader
|
|
6
|
+
from ara_cli.file_loaders.readers.odt_reader import OdtReader
|
|
7
|
+
from ara_cli.file_loaders.readers.excel_reader import ExcelReader
|
|
8
|
+
from ara_cli.file_loaders.readers.pptx_reader import PptxReader
|
|
9
|
+
|
|
10
|
+
class DocumentReaderFactory:
|
|
11
|
+
"""Factory for creating appropriate document readers"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def create_reader(file_path: str) -> Optional[DocumentReader]:
|
|
15
|
+
"""Create appropriate reader based on file extension"""
|
|
16
|
+
_, ext = os.path.splitext(file_path)
|
|
17
|
+
ext = ext.lower()
|
|
18
|
+
|
|
19
|
+
readers = {
|
|
20
|
+
'.docx': DocxReader,
|
|
21
|
+
'.pdf': PdfReader,
|
|
22
|
+
'.odt': OdtReader,
|
|
23
|
+
'.xlsx': ExcelReader,
|
|
24
|
+
'.xls': ExcelReader,
|
|
25
|
+
'.pptx': PptxReader
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
reader_class = readers.get(ext)
|
|
29
|
+
if reader_class:
|
|
30
|
+
return reader_class(file_path)
|
|
31
|
+
|
|
32
|
+
return None
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from ara_cli import BINARY_TYPE_MAPPING, DOCUMENT_TYPE_EXTENSIONS
|
|
3
|
+
from ara_cli.file_loaders.file_loader import FileLoader
|
|
4
|
+
|
|
5
|
+
class FileLoaderFactory:
|
|
6
|
+
"""Factory for creating appropriate file loaders"""
|
|
7
|
+
|
|
8
|
+
@staticmethod
|
|
9
|
+
def create_loader(file_name: str, chat_instance) -> Optional[FileLoader]:
|
|
10
|
+
"""Create appropriate loader based on file type"""
|
|
11
|
+
from ara_cli.file_loaders.loaders.binary_file_loader import BinaryFileLoader
|
|
12
|
+
from ara_cli.file_loaders.loaders.text_file_loader import TextFileLoader
|
|
13
|
+
from ara_cli.file_loaders.loaders.document_file_loader import DocumentFileLoader
|
|
14
|
+
|
|
15
|
+
file_name_lower = file_name.lower()
|
|
16
|
+
|
|
17
|
+
# Check if it's a binary file
|
|
18
|
+
for extension, mime_type in BINARY_TYPE_MAPPING.items():
|
|
19
|
+
if file_name_lower.endswith(extension):
|
|
20
|
+
return BinaryFileLoader(chat_instance)
|
|
21
|
+
|
|
22
|
+
# Check if it's a document
|
|
23
|
+
if any(file_name_lower.endswith(ext) for ext in DOCUMENT_TYPE_EXTENSIONS):
|
|
24
|
+
return DocumentFileLoader(chat_instance)
|
|
25
|
+
|
|
26
|
+
# Default to text file loader
|
|
27
|
+
return TextFileLoader(chat_instance)
|
|
@@ -18,33 +18,4 @@ class FileLoader(ABC):
|
|
|
18
18
|
self.chat.add_prompt_tag_if_needed(self.chat.chat_name)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
"""Factory for creating appropriate file loaders"""
|
|
23
|
-
BINARY_TYPE_MAPPING = {
|
|
24
|
-
".png": "image/png",
|
|
25
|
-
".jpg": "image/jpeg",
|
|
26
|
-
".jpeg": "image/jpeg",
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
DOCUMENT_TYPE_EXTENSIONS = [".docx", ".doc", ".odt", ".pdf"]
|
|
30
|
-
|
|
31
|
-
@staticmethod
|
|
32
|
-
def create_loader(file_name: str, chat_instance) -> Optional[FileLoader]:
|
|
33
|
-
"""Create appropriate loader based on file type"""
|
|
34
|
-
from ara_cli.file_loaders.binary_file_loader import BinaryFileLoader
|
|
35
|
-
from ara_cli.file_loaders.text_file_loader import TextFileLoader
|
|
36
|
-
from ara_cli.file_loaders.document_file_loader import DocumentFileLoader
|
|
37
|
-
|
|
38
|
-
file_name_lower = file_name.lower()
|
|
39
|
-
|
|
40
|
-
# Check if it's a binary file
|
|
41
|
-
for extension, mime_type in FileLoaderFactory.BINARY_TYPE_MAPPING.items():
|
|
42
|
-
if file_name_lower.endswith(extension):
|
|
43
|
-
return BinaryFileLoader(chat_instance)
|
|
44
|
-
|
|
45
|
-
# Check if it's a document
|
|
46
|
-
if any(file_name_lower.endswith(ext) for ext in FileLoaderFactory.DOCUMENT_TYPE_EXTENSIONS):
|
|
47
|
-
return DocumentFileLoader(chat_instance)
|
|
48
|
-
|
|
49
|
-
# Default to text file loader
|
|
50
|
-
return TextFileLoader(chat_instance)
|
|
21
|
+
|
|
File without changes
|