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.
Files changed (151) hide show
  1. ara_cli/__init__.py +51 -6
  2. ara_cli/__main__.py +87 -75
  3. ara_cli/ara_command_action.py +189 -101
  4. ara_cli/ara_config.py +187 -128
  5. ara_cli/ara_subcommands/common.py +2 -2
  6. ara_cli/ara_subcommands/config.py +221 -0
  7. ara_cli/ara_subcommands/convert.py +107 -0
  8. ara_cli/ara_subcommands/fetch.py +41 -0
  9. ara_cli/ara_subcommands/fetch_agents.py +22 -0
  10. ara_cli/ara_subcommands/fetch_scripts.py +19 -0
  11. ara_cli/ara_subcommands/fetch_templates.py +15 -10
  12. ara_cli/ara_subcommands/list.py +97 -23
  13. ara_cli/ara_subcommands/prompt.py +266 -106
  14. ara_cli/artefact_autofix.py +117 -64
  15. ara_cli/artefact_converter.py +355 -0
  16. ara_cli/artefact_creator.py +41 -17
  17. ara_cli/artefact_lister.py +3 -3
  18. ara_cli/artefact_models/artefact_model.py +1 -1
  19. ara_cli/artefact_models/artefact_templates.py +0 -9
  20. ara_cli/artefact_models/feature_artefact_model.py +8 -8
  21. ara_cli/artefact_reader.py +62 -43
  22. ara_cli/artefact_scan.py +39 -17
  23. ara_cli/chat.py +300 -71
  24. ara_cli/chat_agent/__init__.py +0 -0
  25. ara_cli/chat_agent/agent_process_manager.py +155 -0
  26. ara_cli/chat_script_runner/__init__.py +0 -0
  27. ara_cli/chat_script_runner/script_completer.py +23 -0
  28. ara_cli/chat_script_runner/script_finder.py +41 -0
  29. ara_cli/chat_script_runner/script_lister.py +36 -0
  30. ara_cli/chat_script_runner/script_runner.py +36 -0
  31. ara_cli/chat_web_search/__init__.py +0 -0
  32. ara_cli/chat_web_search/web_search.py +263 -0
  33. ara_cli/children_contribution_updater.py +737 -0
  34. ara_cli/classifier.py +34 -0
  35. ara_cli/commands/agent_run_command.py +98 -0
  36. ara_cli/commands/fetch_agents_command.py +106 -0
  37. ara_cli/commands/fetch_scripts_command.py +43 -0
  38. ara_cli/commands/fetch_templates_command.py +39 -0
  39. ara_cli/commands/fetch_templates_commands.py +39 -0
  40. ara_cli/commands/list_agents_command.py +39 -0
  41. ara_cli/commands/load_command.py +4 -3
  42. ara_cli/commands/load_image_command.py +1 -1
  43. ara_cli/commands/read_command.py +23 -27
  44. ara_cli/completers.py +95 -35
  45. ara_cli/constants.py +2 -0
  46. ara_cli/directory_navigator.py +37 -4
  47. ara_cli/error_handler.py +26 -11
  48. ara_cli/file_loaders/document_reader.py +0 -178
  49. ara_cli/file_loaders/factories/__init__.py +0 -0
  50. ara_cli/file_loaders/factories/document_reader_factory.py +32 -0
  51. ara_cli/file_loaders/factories/file_loader_factory.py +27 -0
  52. ara_cli/file_loaders/file_loader.py +1 -30
  53. ara_cli/file_loaders/loaders/__init__.py +0 -0
  54. ara_cli/file_loaders/{document_file_loader.py → loaders/document_file_loader.py} +1 -1
  55. ara_cli/file_loaders/loaders/text_file_loader.py +47 -0
  56. ara_cli/file_loaders/readers/__init__.py +0 -0
  57. ara_cli/file_loaders/readers/docx_reader.py +49 -0
  58. ara_cli/file_loaders/readers/excel_reader.py +27 -0
  59. ara_cli/file_loaders/{markdown_reader.py → readers/markdown_reader.py} +1 -1
  60. ara_cli/file_loaders/readers/odt_reader.py +59 -0
  61. ara_cli/file_loaders/readers/pdf_reader.py +54 -0
  62. ara_cli/file_loaders/readers/pptx_reader.py +104 -0
  63. ara_cli/file_loaders/tools/__init__.py +0 -0
  64. ara_cli/llm_utils.py +58 -0
  65. ara_cli/output_suppressor.py +53 -0
  66. ara_cli/prompt_chat.py +20 -4
  67. ara_cli/prompt_extractor.py +47 -32
  68. ara_cli/prompt_handler.py +123 -17
  69. ara_cli/tag_extractor.py +8 -7
  70. ara_cli/template_loader.py +2 -1
  71. ara_cli/template_manager.py +52 -21
  72. ara_cli/templates/global-scripts/hello_global.py +1 -0
  73. ara_cli/templates/prompt-modules/commands/add_scenarios_for_new_behaviour.feature_creation_agent.commands.md +1 -0
  74. ara_cli/templates/prompt-modules/commands/align_feature_with_implementation_changes.interview_agent.commands.md +1 -0
  75. ara_cli/templates/prompt-modules/commands/analyze_codebase_and_plan_tasks.interview_agent.commands.md +1 -0
  76. ara_cli/templates/prompt-modules/commands/choose_best_parent_artefact.interview_agent.commands.md +1 -0
  77. ara_cli/templates/prompt-modules/commands/create_tasks_from_artefact_content.interview_agent.commands.md +1 -0
  78. ara_cli/templates/prompt-modules/commands/create_tests_for_uncovered_modules.test_generation_agent.commands.md +1 -0
  79. ara_cli/templates/prompt-modules/commands/derive_features_from_video_description.feature_creation_agent.commands.md +1 -0
  80. ara_cli/templates/prompt-modules/commands/describe_agent_capabilities.agent.commands.md +1 -0
  81. ara_cli/templates/prompt-modules/commands/empty.commands.md +2 -12
  82. ara_cli/templates/prompt-modules/commands/execute_scoped_todos_in_task.interview_agent.commands.md +1 -0
  83. ara_cli/templates/prompt-modules/commands/explain_single_file_purpose.interview_agent.commands.md +1 -0
  84. ara_cli/templates/prompt-modules/commands/extract_file_information_bullets.interview_agent.commands.md +1 -0
  85. ara_cli/templates/prompt-modules/commands/extract_general.commands.md +12 -0
  86. ara_cli/templates/prompt-modules/commands/extract_markdown.commands.md +11 -0
  87. ara_cli/templates/prompt-modules/commands/extract_python.commands.md +13 -0
  88. ara_cli/templates/prompt-modules/commands/feature_add_or_modifiy_specified_behavior.commands.md +36 -0
  89. ara_cli/templates/prompt-modules/commands/feature_generate_initial_specified_bevahior.commands.md +53 -0
  90. ara_cli/templates/prompt-modules/commands/fix_failing_behave_step_definitions.interview_agent.commands.md +1 -0
  91. ara_cli/templates/prompt-modules/commands/fix_failing_pytest_tests.interview_agent.commands.md +1 -0
  92. ara_cli/templates/prompt-modules/commands/general_instruction_policy.commands.md +47 -0
  93. ara_cli/templates/prompt-modules/commands/generate_and_fix_pytest_tests.test_generation_agent.commands.md +1 -0
  94. ara_cli/templates/prompt-modules/commands/prompt_template_tech_stack_transformer.commands.md +95 -0
  95. ara_cli/templates/prompt-modules/commands/python_bug_fixing_code.commands.md +34 -0
  96. ara_cli/templates/prompt-modules/commands/python_generate_code.commands.md +27 -0
  97. ara_cli/templates/prompt-modules/commands/python_refactoring_code.commands.md +39 -0
  98. ara_cli/templates/prompt-modules/commands/python_step_definitions_generation_and_fixing.commands.md +40 -0
  99. ara_cli/templates/prompt-modules/commands/python_unittest_generation_and_fixing.commands.md +48 -0
  100. ara_cli/templates/prompt-modules/commands/suggest_next_story_child_tasks.interview_agent.commands.md +1 -0
  101. ara_cli/templates/prompt-modules/commands/summarize_or_transcribe_media.interview_agent.commands.md +1 -0
  102. ara_cli/templates/prompt-modules/commands/update_feature_to_match_implementation.feature_creation_agent.commands.md +1 -0
  103. ara_cli/templates/prompt-modules/commands/update_user_story_with_requirements.interview_agent.commands.md +1 -0
  104. ara_cli/version.py +1 -1
  105. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/METADATA +49 -11
  106. ara_cli-0.1.14.0.dist-info/RECORD +253 -0
  107. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/WHEEL +1 -1
  108. tests/test_ara_command_action.py +31 -19
  109. tests/test_ara_config.py +177 -90
  110. tests/test_artefact_autofix.py +170 -97
  111. tests/test_artefact_autofix_integration.py +495 -0
  112. tests/test_artefact_converter.py +312 -0
  113. tests/test_artefact_extraction.py +564 -0
  114. tests/test_artefact_lister.py +11 -8
  115. tests/test_chat.py +166 -130
  116. tests/test_chat_givens_images.py +603 -0
  117. tests/test_chat_script_runner.py +454 -0
  118. tests/test_children_contribution_updater.py +98 -0
  119. tests/test_document_loader_office.py +267 -0
  120. tests/test_llm_utils.py +164 -0
  121. tests/test_prompt_chat.py +343 -0
  122. tests/test_prompt_extractor.py +683 -0
  123. tests/test_prompt_handler.py +416 -214
  124. tests/test_setup_default_chat_prompt_mode.py +198 -0
  125. tests/test_tag_extractor.py +95 -49
  126. tests/test_web_search.py +467 -0
  127. ara_cli/file_loaders/document_readers.py +0 -233
  128. ara_cli/file_loaders/file_loaders.py +0 -123
  129. ara_cli/file_loaders/text_file_loader.py +0 -187
  130. ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +0 -27
  131. ara_cli/templates/prompt-modules/blueprints/pytest_unittest_prompt.blueprint.md +0 -32
  132. ara_cli/templates/prompt-modules/blueprints/task_todo_list_implement_feature_BDD_way.blueprint.md +0 -30
  133. ara_cli/templates/prompt-modules/commands/artefact_classification.commands.md +0 -9
  134. ara_cli/templates/prompt-modules/commands/artefact_extension.commands.md +0 -17
  135. ara_cli/templates/prompt-modules/commands/artefact_formulation.commands.md +0 -14
  136. ara_cli/templates/prompt-modules/commands/behave_step_generation.commands.md +0 -102
  137. ara_cli/templates/prompt-modules/commands/code_generation_complex.commands.md +0 -20
  138. ara_cli/templates/prompt-modules/commands/code_generation_simple.commands.md +0 -13
  139. ara_cli/templates/prompt-modules/commands/error_fixing.commands.md +0 -20
  140. ara_cli/templates/prompt-modules/commands/feature_file_update.commands.md +0 -18
  141. ara_cli/templates/prompt-modules/commands/feature_formulation.commands.md +0 -43
  142. ara_cli/templates/prompt-modules/commands/js_code_generation_simple.commands.md +0 -13
  143. ara_cli/templates/prompt-modules/commands/refactoring.commands.md +0 -15
  144. ara_cli/templates/prompt-modules/commands/refactoring_analysis.commands.md +0 -9
  145. ara_cli/templates/prompt-modules/commands/reverse_engineer_feature_file.commands.md +0 -15
  146. ara_cli/templates/prompt-modules/commands/reverse_engineer_program_flow.commands.md +0 -19
  147. ara_cli-0.1.10.5.dist-info/RECORD +0 -194
  148. /ara_cli/file_loaders/{binary_file_loader.py → loaders/binary_file_loader.py} +0 -0
  149. /ara_cli/file_loaders/{image_processor.py → tools/image_processor.py} +0 -0
  150. {ara_cli-0.1.10.5.dist-info → ara_cli-0.1.14.0.dist-info}/entry_points.txt +0 -0
  151. {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.template_manager import SpecificationBreakdownAspects
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
- classifiers = Classifier.ordered_classifiers()
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 = SpecificationBreakdownAspects.VALID_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(f'.{classifier}'):
53
+ if file.startswith(incomplete) and file.endswith(suffix):
47
54
  # Remove the extension to get the artefact name
48
- name = file[:-len(f'.{classifier}')]
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
- artefacts = complete_artefact_name(classifier)
60
- return [a for a in artefacts if a.startswith(incomplete)]
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
- # Look for .md files in current directory
71
- for file in current_dir.glob("*.md"):
72
- name = file.stem
73
- if name.startswith(incomplete):
74
- chat_files.append(name)
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, 'params') and 'classifier' in ctx.params:
117
- classifier = ctx.params['classifier']
118
- if hasattr(classifier, 'value'):
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
- artefacts = complete_artefact_name(classifier)
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, 'params') and 'parent_classifier' in ctx.params:
131
- parent_classifier = ctx.params['parent_classifier']
132
- if hasattr(parent_classifier, 'value'):
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
- artefacts = complete_artefact_name(parent_classifier)
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
@@ -0,0 +1,2 @@
1
+
2
+ VALID_ASPECTS = ["technology", "concept", "persona", "customer", "step"]
@@ -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
- while current_directory != dirname(current_directory): # Ensure loop breaks at root
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(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()
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(f"Unable to locate the '{self.target_directory}' directory and user declined to create 'ara' folder.")
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(f"Unable to navigate to '{relative_path}' relative to the target directory.")
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 = '\033[91m'
9
- RESET = '\033[0m'
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(f"{RED}{error_prefix} {context}: {error.message}{RESET}", file=sys.stderr)
91
+ print(
92
+ f"{color}{error_prefix} {context}: {error.message}{RESET}",
93
+ file=sys.stderr,
94
+ )
87
95
  else:
88
- print(f"{RED}{error_prefix} {error.message}{RESET}", file=sys.stderr)
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
- def _report_generic_error(self, error: Exception, context: Optional[str] = None) -> None:
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(f"{RED}[ERROR] {context}: {error_type}: {str(error)}{RESET}", file=sys.stderr)
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(_func=None, context: Optional[str] = None, error_handler: Optional[ErrorHandler] = None):
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
- class FileLoaderFactory:
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
@@ -1,4 +1,4 @@
1
- from ara_cli.file_loaders.document_reader import DocumentReaderFactory
1
+ from ara_cli.file_loaders.factories.document_reader_factory import DocumentReaderFactory
2
2
  from ara_cli.file_loaders.file_loader import FileLoader
3
3
 
4
4