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/prompt_extractor.py
CHANGED
|
@@ -77,42 +77,47 @@ def _perform_extraction_for_block(source_lines, block_start, block_end, force, w
|
|
|
77
77
|
else:
|
|
78
78
|
artefact_class = _find_artefact_class(content_lines_after_extract)
|
|
79
79
|
if artefact_class:
|
|
80
|
-
_process_artefact_extraction(
|
|
80
|
+
_process_artefact_extraction(
|
|
81
|
+
artefact_class, content_lines_after_extract, force, write)
|
|
81
82
|
else:
|
|
82
|
-
print(
|
|
83
|
+
print(
|
|
84
|
+
"No filename or valid artefact found, skipping processing for this block.")
|
|
83
85
|
return None, None
|
|
84
86
|
|
|
85
|
-
modified_block_text = original_block_text.replace(
|
|
87
|
+
modified_block_text = original_block_text.replace(
|
|
88
|
+
"# [x] extract", "# [v] extract", 1)
|
|
86
89
|
return original_block_text, modified_block_text
|
|
87
90
|
|
|
88
91
|
|
|
89
92
|
class FenceDetector:
|
|
90
93
|
"""Helper class to detect and match fence blocks."""
|
|
91
|
-
|
|
94
|
+
|
|
92
95
|
def __init__(self, source_lines):
|
|
93
96
|
self.source_lines = source_lines
|
|
94
|
-
|
|
97
|
+
|
|
95
98
|
def is_extract_fence(self, line_num):
|
|
96
99
|
"""Check if line is a fence with extract marker."""
|
|
97
100
|
line = self.source_lines[line_num]
|
|
98
101
|
stripped_line = line.strip()
|
|
99
|
-
|
|
100
|
-
is_fence = stripped_line.startswith(
|
|
102
|
+
|
|
103
|
+
is_fence = stripped_line.startswith(
|
|
104
|
+
'```') or stripped_line.startswith('~~~')
|
|
101
105
|
if not is_fence:
|
|
102
106
|
return False
|
|
103
|
-
|
|
107
|
+
|
|
104
108
|
if not (line_num + 1 < len(self.source_lines)):
|
|
105
109
|
return False
|
|
106
|
-
|
|
110
|
+
|
|
107
111
|
return self.source_lines[line_num + 1].strip().startswith("# [x] extract")
|
|
108
|
-
|
|
112
|
+
|
|
109
113
|
def find_matching_fence_end(self, start_line):
|
|
110
114
|
"""Find the matching end fence for a given start fence."""
|
|
111
115
|
fence_line = self.source_lines[start_line]
|
|
112
116
|
indentation = len(fence_line) - len(fence_line.lstrip())
|
|
113
117
|
stripped_fence_line = fence_line.strip()
|
|
114
118
|
fence_char = stripped_fence_line[0]
|
|
115
|
-
fence_length = len(stripped_fence_line) -
|
|
119
|
+
fence_length = len(stripped_fence_line) - \
|
|
120
|
+
len(stripped_fence_line.lstrip(fence_char))
|
|
116
121
|
|
|
117
122
|
for i in range(start_line + 1, len(self.source_lines)):
|
|
118
123
|
scan_line = self.source_lines[i]
|
|
@@ -120,16 +125,16 @@ class FenceDetector:
|
|
|
120
125
|
|
|
121
126
|
if not stripped_scan_line or stripped_scan_line[0] != fence_char:
|
|
122
127
|
continue
|
|
123
|
-
|
|
128
|
+
|
|
124
129
|
if not all(c == fence_char for c in stripped_scan_line):
|
|
125
130
|
continue
|
|
126
131
|
|
|
127
132
|
candidate_indentation = len(scan_line) - len(scan_line.lstrip())
|
|
128
133
|
candidate_length = len(stripped_scan_line)
|
|
129
|
-
|
|
134
|
+
|
|
130
135
|
if candidate_length == fence_length and candidate_indentation == indentation:
|
|
131
136
|
return i
|
|
132
|
-
|
|
137
|
+
|
|
133
138
|
return -1
|
|
134
139
|
|
|
135
140
|
|
|
@@ -138,17 +143,19 @@ def _process_document_blocks(source_lines, force, write):
|
|
|
138
143
|
fence_detector = FenceDetector(source_lines)
|
|
139
144
|
replacements = []
|
|
140
145
|
line_num = 0
|
|
141
|
-
|
|
146
|
+
|
|
142
147
|
while line_num < len(source_lines):
|
|
143
148
|
if not fence_detector.is_extract_fence(line_num):
|
|
144
149
|
line_num += 1
|
|
145
150
|
continue
|
|
146
151
|
|
|
147
152
|
block_start_line = line_num
|
|
148
|
-
block_end_line = fence_detector.find_matching_fence_end(
|
|
149
|
-
|
|
153
|
+
block_end_line = fence_detector.find_matching_fence_end(
|
|
154
|
+
block_start_line)
|
|
155
|
+
|
|
150
156
|
if block_end_line != -1:
|
|
151
|
-
print(
|
|
157
|
+
print(
|
|
158
|
+
f"Block found and processed starting on line {block_start_line + 1}.")
|
|
152
159
|
original, modified = _perform_extraction_for_block(
|
|
153
160
|
source_lines, block_start_line, block_end_line, force, write
|
|
154
161
|
)
|
|
@@ -157,7 +164,7 @@ def _process_document_blocks(source_lines, force, write):
|
|
|
157
164
|
line_num = block_end_line + 1
|
|
158
165
|
else:
|
|
159
166
|
line_num += 1
|
|
160
|
-
|
|
167
|
+
|
|
161
168
|
return replacements
|
|
162
169
|
|
|
163
170
|
|
|
@@ -180,20 +187,21 @@ def _setup_working_directory(relative_to_ara_root):
|
|
|
180
187
|
|
|
181
188
|
|
|
182
189
|
def extract_responses(document_path, relative_to_ara_root=False, force=False, write=False):
|
|
183
|
-
print(f"Starting extraction from '{document_path}'")
|
|
184
|
-
|
|
190
|
+
print(f"Starting extraction from '{document_path}'", flush=True)
|
|
191
|
+
|
|
185
192
|
try:
|
|
186
193
|
with open(document_path, 'r', encoding='utf-8', errors='replace') as file:
|
|
187
194
|
content = file.read()
|
|
188
195
|
except FileNotFoundError:
|
|
189
|
-
print(
|
|
196
|
+
print(
|
|
197
|
+
f"Error: File not found at '{document_path}'. Skipping extraction.")
|
|
190
198
|
return
|
|
191
199
|
|
|
192
200
|
cwd = _setup_working_directory(relative_to_ara_root)
|
|
193
|
-
|
|
201
|
+
|
|
194
202
|
source_lines = content.split('\n')
|
|
195
203
|
replacements = _process_document_blocks(source_lines, force, write)
|
|
196
|
-
|
|
204
|
+
|
|
197
205
|
updated_content = _apply_replacements(content, replacements)
|
|
198
206
|
|
|
199
207
|
os.chdir(cwd)
|
|
@@ -201,7 +209,8 @@ def extract_responses(document_path, relative_to_ara_root=False, force=False, wr
|
|
|
201
209
|
file.write(updated_content)
|
|
202
210
|
|
|
203
211
|
if replacements:
|
|
204
|
-
print(
|
|
212
|
+
print(
|
|
213
|
+
f"End of extraction. Found and processed {len(replacements)} blocks in '{os.path.basename(document_path)}'.")
|
|
205
214
|
|
|
206
215
|
|
|
207
216
|
def modify_and_save_file(response, file_path):
|
|
@@ -216,7 +225,8 @@ def modify_and_save_file(response, file_path):
|
|
|
216
225
|
""")
|
|
217
226
|
|
|
218
227
|
if filename_from_response != file_path:
|
|
219
|
-
user_decision = prompt_user_decision(
|
|
228
|
+
user_decision = prompt_user_decision(
|
|
229
|
+
"Filename does not match, overwrite? (y/n): ")
|
|
220
230
|
if user_decision.lower() not in ['y', 'yes']:
|
|
221
231
|
print("Debug: User chose not to overwrite")
|
|
222
232
|
print("Skipping block.")
|
|
@@ -235,8 +245,11 @@ def prompt_user_decision(prompt):
|
|
|
235
245
|
|
|
236
246
|
def determine_should_create(skip_query=False):
|
|
237
247
|
if skip_query:
|
|
248
|
+
print("[DEBUG] skip_query is True, allowing creation.", flush=True)
|
|
238
249
|
return True
|
|
239
|
-
|
|
250
|
+
print(f"[DEBUG] About to prompt for file creation: File does not exist. Create? (y/n): ", flush=True)
|
|
251
|
+
user_decision = prompt_user_decision(
|
|
252
|
+
"File does not exist. Create? (y/n): ")
|
|
240
253
|
if user_decision.lower() in ['y', 'yes']:
|
|
241
254
|
return True
|
|
242
255
|
return False
|
|
@@ -267,8 +280,8 @@ def create_prompt_for_file_modification(content_str, filename):
|
|
|
267
280
|
print(f"WARNING: {filename} for merge prompt creation does not exist.")
|
|
268
281
|
return
|
|
269
282
|
|
|
270
|
-
content_of_existing_file =
|
|
271
|
-
content =
|
|
283
|
+
content_of_existing_file = get_file_content(filename)
|
|
284
|
+
content = content_str
|
|
272
285
|
|
|
273
286
|
prompt_text = f"""
|
|
274
287
|
* given this new_content:
|
|
@@ -303,7 +316,8 @@ def handle_existing_file(filename, block_content, skip_query=False, write=False)
|
|
|
303
316
|
create_file_if_not_exist(filename, block_content, skip_query)
|
|
304
317
|
|
|
305
318
|
elif write:
|
|
306
|
-
print(
|
|
319
|
+
print(
|
|
320
|
+
f"File {filename} exists. Overwriting without LLM merge as requested.")
|
|
307
321
|
try:
|
|
308
322
|
directory = os.path.dirname(filename)
|
|
309
323
|
if directory:
|
|
@@ -316,7 +330,8 @@ def handle_existing_file(filename, block_content, skip_query=False, write=False)
|
|
|
316
330
|
print(f"Failed to overwrite file {filename} due to an OS error")
|
|
317
331
|
else:
|
|
318
332
|
print(f"File {filename} exists, creating modification prompt")
|
|
319
|
-
prompt_text = create_prompt_for_file_modification(
|
|
333
|
+
prompt_text = create_prompt_for_file_modification(
|
|
334
|
+
block_content, filename)
|
|
320
335
|
if prompt_text is None:
|
|
321
336
|
return
|
|
322
337
|
|
|
@@ -335,4 +350,4 @@ def extract_and_save_prompt_results(classifier, param, write=False):
|
|
|
335
350
|
prompt_log_file = f"ara/{sub_directory}/{param}.data/{classifier}.prompt_log.md"
|
|
336
351
|
print(f"Extract marked sections from: {prompt_log_file}")
|
|
337
352
|
|
|
338
|
-
extract_responses(prompt_log_file, write=write)
|
|
353
|
+
extract_responses(prompt_log_file, write=write)
|
ara_cli/prompt_handler.py
CHANGED
|
@@ -10,15 +10,79 @@ import logging
|
|
|
10
10
|
import warnings
|
|
11
11
|
from io import StringIO
|
|
12
12
|
from contextlib import redirect_stderr
|
|
13
|
-
from langfuse import Langfuse
|
|
14
|
-
from langfuse.api.resources.commons.errors import Error as LangfuseError, NotFoundError
|
|
15
|
-
import litellm
|
|
16
13
|
from ara_cli.classifier import Classifier
|
|
17
14
|
from ara_cli.artefact_creator import ArtefactCreator
|
|
18
15
|
from ara_cli.template_manager import TemplatePathManager
|
|
19
16
|
from ara_cli.ara_config import ConfigManager
|
|
20
17
|
from ara_cli.file_lister import generate_markdown_listing
|
|
21
18
|
|
|
19
|
+
# Lazy loading for heavy modules - these will be imported on first use
|
|
20
|
+
# Module-level references for backward compatibility with tests that patch these
|
|
21
|
+
litellm = None # Will be lazily loaded
|
|
22
|
+
Langfuse = None # Will be lazily loaded
|
|
23
|
+
_logging_configured = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _configure_logging():
|
|
27
|
+
"""Configure logging for litellm/langfuse - called once on first LLM use."""
|
|
28
|
+
global _logging_configured
|
|
29
|
+
if _logging_configured:
|
|
30
|
+
return
|
|
31
|
+
_logging_configured = True
|
|
32
|
+
|
|
33
|
+
logging.getLogger("LiteLLM").setLevel(logging.CRITICAL)
|
|
34
|
+
logging.getLogger("litellm").setLevel(logging.CRITICAL)
|
|
35
|
+
logging.getLogger("LiteLLM Proxy").setLevel(logging.CRITICAL)
|
|
36
|
+
logging.getLogger("LiteLLM Router").setLevel(logging.CRITICAL)
|
|
37
|
+
logging.getLogger("langfuse").setLevel(logging.CRITICAL)
|
|
38
|
+
logging.getLogger("opentelemetry").setLevel(logging.CRITICAL)
|
|
39
|
+
logging.getLogger("opentelemetry.exporter.otlp.proto.http.trace_exporter").setLevel(
|
|
40
|
+
logging.CRITICAL
|
|
41
|
+
)
|
|
42
|
+
logging.getLogger("httpx").setLevel(logging.CRITICAL)
|
|
43
|
+
logging.getLogger("httpcore").setLevel(logging.CRITICAL)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_litellm():
|
|
47
|
+
"""Lazy load litellm module."""
|
|
48
|
+
global litellm
|
|
49
|
+
if litellm is None:
|
|
50
|
+
_configure_logging()
|
|
51
|
+
import litellm as _litellm
|
|
52
|
+
|
|
53
|
+
_litellm.suppress_debug_info = True
|
|
54
|
+
_litellm.set_verbose = False
|
|
55
|
+
litellm = _litellm
|
|
56
|
+
|
|
57
|
+
# Apply output filtering only when litellm is loaded
|
|
58
|
+
from ara_cli.output_suppressor import FilteredStdout
|
|
59
|
+
import sys
|
|
60
|
+
|
|
61
|
+
if not isinstance(sys.stdout, FilteredStdout):
|
|
62
|
+
sys.stdout = FilteredStdout(sys.stdout)
|
|
63
|
+
return litellm
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _get_langfuse():
|
|
67
|
+
"""Lazy load langfuse module."""
|
|
68
|
+
global Langfuse
|
|
69
|
+
if Langfuse is None:
|
|
70
|
+
_configure_logging()
|
|
71
|
+
from langfuse import Langfuse as _Langfuse
|
|
72
|
+
|
|
73
|
+
Langfuse = _Langfuse
|
|
74
|
+
return Langfuse
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _get_langfuse_errors():
|
|
78
|
+
"""Lazy load langfuse error classes."""
|
|
79
|
+
from langfuse.api.resources.commons.errors import (
|
|
80
|
+
Error as LangfuseError,
|
|
81
|
+
NotFoundError,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return LangfuseError, NotFoundError
|
|
85
|
+
|
|
22
86
|
|
|
23
87
|
class LLMSingleton:
|
|
24
88
|
_instance = None
|
|
@@ -51,6 +115,7 @@ class LLMSingleton:
|
|
|
51
115
|
|
|
52
116
|
captured_stderr = StringIO()
|
|
53
117
|
with redirect_stderr(captured_stderr):
|
|
118
|
+
Langfuse = _get_langfuse()
|
|
54
119
|
self.langfuse = Langfuse(
|
|
55
120
|
public_key=langfuse_public_key,
|
|
56
121
|
secret_key=langfuse_secret_key,
|
|
@@ -218,14 +283,43 @@ def send_prompt(prompt, purpose="default"):
|
|
|
218
283
|
with LLMSingleton.get_instance().langfuse.start_as_current_span(
|
|
219
284
|
name="send_prompt"
|
|
220
285
|
) as span:
|
|
286
|
+
# Sanitize prompt for logging (remove base64 image data)
|
|
287
|
+
def sanitize_message(msg):
|
|
288
|
+
import copy
|
|
289
|
+
|
|
290
|
+
if not isinstance(msg, dict):
|
|
291
|
+
return msg
|
|
292
|
+
|
|
293
|
+
clean_msg = copy.deepcopy(msg)
|
|
294
|
+
content = clean_msg.get("content")
|
|
295
|
+
|
|
296
|
+
if isinstance(content, list):
|
|
297
|
+
new_content = []
|
|
298
|
+
for item in content:
|
|
299
|
+
if item.get("type") == "image_url":
|
|
300
|
+
# Replace image_url with text placeholder to avoid Langfuse parsing errors on invalid base64
|
|
301
|
+
new_content.append(
|
|
302
|
+
{
|
|
303
|
+
"type": "text",
|
|
304
|
+
"text": "[IMAGE DATA TRUNCATED FOR LOGGING]",
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
new_content.append(item)
|
|
309
|
+
clean_msg["content"] = new_content
|
|
310
|
+
return clean_msg
|
|
311
|
+
|
|
312
|
+
sanitized_prompt = [sanitize_message(msg) for msg in prompt]
|
|
313
|
+
|
|
221
314
|
span.update_trace(
|
|
222
|
-
input={"prompt":
|
|
315
|
+
input={"prompt": sanitized_prompt, "purpose": purpose, "model": model_info}
|
|
223
316
|
)
|
|
224
317
|
|
|
225
318
|
config_parameters.pop("provider", None)
|
|
226
319
|
|
|
227
320
|
filtered_prompt = [msg for msg in prompt if _is_valid_message(msg)]
|
|
228
321
|
|
|
322
|
+
litellm = _get_litellm()
|
|
229
323
|
completion = litellm.completion(
|
|
230
324
|
**config_parameters, messages=filtered_prompt, stream=True
|
|
231
325
|
)
|
|
@@ -274,13 +368,12 @@ def describe_image(image_path: str) -> str:
|
|
|
274
368
|
describe_image_prompt = (
|
|
275
369
|
langfuse_prompt.prompt if langfuse_prompt.prompt else None
|
|
276
370
|
)
|
|
277
|
-
except
|
|
278
|
-
|
|
371
|
+
except Exception as e:
|
|
372
|
+
# Silently fallback - no need to show error for describe-image prompt
|
|
279
373
|
describe_image_prompt = None
|
|
280
374
|
|
|
281
375
|
# Fallback to default prompt if Langfuse prompt is not available
|
|
282
376
|
if not describe_image_prompt:
|
|
283
|
-
logging.info("Using default describe-image prompt.")
|
|
284
377
|
describe_image_prompt = (
|
|
285
378
|
"Please describe this image in detail. If it contains text, transcribe it exactly. "
|
|
286
379
|
"If it's a diagram or chart, explain its structure and content. If it's a photo or illustration, "
|
|
@@ -538,7 +631,11 @@ def extract_and_load_markdown_files(md_prompt_file_path):
|
|
|
538
631
|
elif "[x]" in line:
|
|
539
632
|
relative_path = line.split("]")[-1].strip()
|
|
540
633
|
# Use os.path.join for OS-safe joining, then normalize
|
|
541
|
-
full_rel_path =
|
|
634
|
+
full_rel_path = (
|
|
635
|
+
os.path.join(*header_stack, relative_path)
|
|
636
|
+
if header_stack
|
|
637
|
+
else relative_path
|
|
638
|
+
)
|
|
542
639
|
path_accumulator.append(_norm(full_rel_path))
|
|
543
640
|
return path_accumulator
|
|
544
641
|
|
|
@@ -650,19 +747,28 @@ def collect_file_content_by_extension(prompt_data_path, extensions):
|
|
|
650
747
|
|
|
651
748
|
|
|
652
749
|
def prepend_system_prompt(message_list):
|
|
750
|
+
from ara_cli.error_handler import AraError, ErrorLevel, ErrorHandler
|
|
751
|
+
|
|
653
752
|
try:
|
|
654
753
|
langfuse_prompt = LLMSingleton.get_instance().langfuse.get_prompt(
|
|
655
754
|
"ara-cli/system-prompt"
|
|
656
755
|
)
|
|
657
756
|
system_prompt = langfuse_prompt.prompt if langfuse_prompt.prompt else None
|
|
658
|
-
except
|
|
659
|
-
|
|
757
|
+
except Exception as e:
|
|
758
|
+
# Show user-friendly info message about Langfuse connection issue
|
|
759
|
+
info_error = AraError(
|
|
760
|
+
message="Langfuse connection failed. Using default system prompt.",
|
|
761
|
+
error_code=0,
|
|
762
|
+
level=ErrorLevel.INFO,
|
|
763
|
+
)
|
|
764
|
+
ErrorHandler().report_error(info_error)
|
|
660
765
|
system_prompt = None
|
|
661
766
|
|
|
662
767
|
# Fallback to default prompt if Langfuse prompt is not available
|
|
663
768
|
if not system_prompt:
|
|
664
|
-
|
|
665
|
-
|
|
769
|
+
system_prompt = (
|
|
770
|
+
"You are a helpful assistant that can process both text and images."
|
|
771
|
+
)
|
|
666
772
|
|
|
667
773
|
# Prepend the system prompt
|
|
668
774
|
system_prompt_message = {"role": "system", "content": system_prompt}
|
|
@@ -695,7 +801,9 @@ def append_images_to_message(message, image_data_list):
|
|
|
695
801
|
message["content"].extend(image_data_list)
|
|
696
802
|
else:
|
|
697
803
|
# If somehow content is not list or str, coerce to list
|
|
698
|
-
message["content"] = [
|
|
804
|
+
message["content"] = [
|
|
805
|
+
{"type": "text", "text": str(message_content)}
|
|
806
|
+
] + image_data_list
|
|
699
807
|
|
|
700
808
|
logger.debug(f"Updated message content with {len(image_data_list)} images")
|
|
701
809
|
|
|
@@ -818,9 +926,7 @@ def generate_config_prompt_global_givens_file(
|
|
|
818
926
|
return
|
|
819
927
|
|
|
820
928
|
dir_list = [path for d in config.global_dirs for path in d.values()]
|
|
821
|
-
print(
|
|
822
|
-
f"used {dir_list} for global prompt givens file listing with absolute paths"
|
|
823
|
-
)
|
|
929
|
+
print(f"used {dir_list} for global prompt givens file listing with absolute paths")
|
|
824
930
|
generate_global_markdown_listing(
|
|
825
931
|
dir_list, config.ara_prompt_given_list_includes, config_prompt_givens_path
|
|
826
|
-
)
|
|
932
|
+
)
|
ara_cli/tag_extractor.py
CHANGED
|
@@ -6,6 +6,7 @@ from ara_cli.artefact_models.artefact_data_retrieval import (
|
|
|
6
6
|
artefact_tags_retrieval,
|
|
7
7
|
)
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
class TagExtractor:
|
|
10
11
|
def __init__(self, file_system=None):
|
|
11
12
|
self.file_system = file_system or os
|
|
@@ -53,16 +54,16 @@ class TagExtractor:
|
|
|
53
54
|
"""Collect all tags from an artefact including user tags and author."""
|
|
54
55
|
all_tags = []
|
|
55
56
|
all_tags.extend(artefact.tags)
|
|
56
|
-
|
|
57
|
+
|
|
57
58
|
if artefact.status:
|
|
58
59
|
all_tags.append(artefact.status)
|
|
59
|
-
|
|
60
|
+
|
|
60
61
|
user_tags = [f"user_{tag}" for tag in artefact.users]
|
|
61
62
|
all_tags.extend(user_tags)
|
|
62
|
-
|
|
63
|
-
if hasattr(artefact,
|
|
63
|
+
|
|
64
|
+
if hasattr(artefact, "author") and artefact.author:
|
|
64
65
|
all_tags.append(artefact.author)
|
|
65
|
-
|
|
66
|
+
|
|
66
67
|
return [tag for tag in all_tags if tag is not None]
|
|
67
68
|
|
|
68
69
|
def _add_tags_to_groups(self, tag_groups, tags):
|
|
@@ -92,7 +93,7 @@ class TagExtractor:
|
|
|
92
93
|
if navigate_to_target:
|
|
93
94
|
navigator.navigate_to_target()
|
|
94
95
|
|
|
95
|
-
artefacts = ArtefactReader.read_artefacts()
|
|
96
|
+
artefacts = ArtefactReader(self.file_system).read_artefacts()
|
|
96
97
|
|
|
97
98
|
filtered_artefacts = filter_list(
|
|
98
99
|
list_to_filter=artefacts,
|
|
@@ -109,4 +110,4 @@ class TagExtractor:
|
|
|
109
110
|
else:
|
|
110
111
|
self.add_to_tags_set(tag_groups, filtered_artefacts)
|
|
111
112
|
|
|
112
|
-
return tag_groups
|
|
113
|
+
return tag_groups
|
ara_cli/template_loader.py
CHANGED
|
@@ -4,6 +4,7 @@ import glob
|
|
|
4
4
|
from ara_cli.template_manager import TemplatePathManager
|
|
5
5
|
from ara_cli.ara_config import ConfigManager
|
|
6
6
|
from ara_cli.directory_navigator import DirectoryNavigator
|
|
7
|
+
from . import ROLE_PROMPT
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class TemplateLoader:
|
|
@@ -171,7 +172,7 @@ class TemplateLoader:
|
|
|
171
172
|
with open(chat_file_path, 'r', encoding='utf-8') as file:
|
|
172
173
|
lines = file.readlines()
|
|
173
174
|
|
|
174
|
-
prompt_tag = f"# {
|
|
175
|
+
prompt_tag = f"# {ROLE_PROMPT}:"
|
|
175
176
|
if Chat.get_last_role_marker(lines) == prompt_tag:
|
|
176
177
|
return
|
|
177
178
|
|
ara_cli/template_manager.py
CHANGED
|
@@ -5,6 +5,7 @@ from shutil import copy
|
|
|
5
5
|
from ara_cli.classifier import Classifier
|
|
6
6
|
from ara_cli.directory_navigator import DirectoryNavigator
|
|
7
7
|
from ara_cli.artefact_models.artefact_templates import template_artefact_of_type
|
|
8
|
+
from ara_cli.constants import VALID_ASPECTS
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class TemplatePathManager:
|
|
@@ -27,7 +28,10 @@ class TemplatePathManager:
|
|
|
27
28
|
base_path = self.get_template_base_path_aspects()
|
|
28
29
|
return [
|
|
29
30
|
(base_path / f"template.{aspect}.md", f"{aspect}.md"),
|
|
30
|
-
(
|
|
31
|
+
(
|
|
32
|
+
base_path / f"template.{aspect}.exploration.md",
|
|
33
|
+
f"{aspect}.exploration.md",
|
|
34
|
+
),
|
|
31
35
|
]
|
|
32
36
|
|
|
33
37
|
def get_template_content(self, classifier):
|
|
@@ -63,7 +67,9 @@ class ArtefactFileManager:
|
|
|
63
67
|
os.mkdir(data_dir)
|
|
64
68
|
os.chdir(data_dir)
|
|
65
69
|
else:
|
|
66
|
-
raise ValueError(
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"File {artefact_file_path} does not exist. Please create it first."
|
|
72
|
+
)
|
|
67
73
|
|
|
68
74
|
def copy_aspect_templates_to_directory(self, aspect, print_relative_to=""):
|
|
69
75
|
"""Copies the templates for the given aspect to the current directory."""
|
|
@@ -84,7 +90,9 @@ class ArtefactFileManager:
|
|
|
84
90
|
steps_file_path = f"features/steps/{artefact_name}_steps.py"
|
|
85
91
|
|
|
86
92
|
behave_command = f"behave features/{artefact_name}.feature"
|
|
87
|
-
result = subprocess.run(
|
|
93
|
+
result = subprocess.run(
|
|
94
|
+
behave_command, shell=True, capture_output=True, text=True
|
|
95
|
+
)
|
|
88
96
|
|
|
89
97
|
# Stderr command output needs to be reduced to only given-when-then statements
|
|
90
98
|
if len(result.stderr) == 0:
|
|
@@ -94,26 +102,34 @@ class ArtefactFileManager:
|
|
|
94
102
|
|
|
95
103
|
def format_behave_command_output(self, raw_result):
|
|
96
104
|
# Split the input string by lines
|
|
97
|
-
lines = raw_result.split(
|
|
105
|
+
lines = raw_result.split("\n")
|
|
98
106
|
|
|
99
107
|
# Find the first given/when/then and last raise NotImplementedError line
|
|
100
108
|
keywords = ["@given", "@when", "@then"]
|
|
101
|
-
start_index = next(
|
|
102
|
-
|
|
109
|
+
start_index = next(
|
|
110
|
+
i
|
|
111
|
+
for i, line in enumerate(lines)
|
|
112
|
+
if any(keyword in line for keyword in keywords)
|
|
113
|
+
)
|
|
114
|
+
end_index = next(
|
|
115
|
+
i
|
|
116
|
+
for i, line in reversed(list(enumerate(lines)))
|
|
117
|
+
if "raise NotImplementedError" in line
|
|
118
|
+
)
|
|
103
119
|
|
|
104
120
|
# Extract the relevant given-when-then portion
|
|
105
|
-
formatted_code =
|
|
121
|
+
formatted_code = "\n".join(lines[start_index : end_index + 1])
|
|
106
122
|
return formatted_code
|
|
107
123
|
|
|
108
124
|
def save_behave_steps_to_file(self, artefact_name, behave_steps):
|
|
109
125
|
self.navigator.navigate_to_target()
|
|
110
126
|
file_path = f"features/steps/{artefact_name}_steps.py"
|
|
111
|
-
with open(file_path,
|
|
127
|
+
with open(file_path, "w", encoding="utf-8") as file:
|
|
112
128
|
file.write(behave_steps)
|
|
113
129
|
|
|
114
130
|
|
|
115
131
|
class SpecificationBreakdownAspects:
|
|
116
|
-
VALID_ASPECTS =
|
|
132
|
+
VALID_ASPECTS = VALID_ASPECTS
|
|
117
133
|
|
|
118
134
|
def __init__(self):
|
|
119
135
|
self.file_manager = ArtefactFileManager()
|
|
@@ -123,34 +139,49 @@ class SpecificationBreakdownAspects:
|
|
|
123
139
|
if artefact_name in Classifier.valid_classifiers:
|
|
124
140
|
raise ValueError(f"{artefact_name} is not a valid artefact name")
|
|
125
141
|
|
|
126
|
-
if
|
|
142
|
+
if (
|
|
143
|
+
not Classifier.is_valid_classifier(classifier)
|
|
144
|
+
or classifier in self.VALID_ASPECTS
|
|
145
|
+
):
|
|
127
146
|
raise ValueError(f"{classifier} is not a valid classifier.")
|
|
128
147
|
|
|
129
148
|
if aspect not in self.VALID_ASPECTS:
|
|
130
|
-
raise ValueError(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"{aspect} does not exist. Please choose one of the {self.VALID_ASPECTS} list."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def create(
|
|
154
|
+
self,
|
|
155
|
+
artefact_name="artefact_name",
|
|
156
|
+
classifier="classifier",
|
|
157
|
+
aspect="specification_breakdown_aspect",
|
|
158
|
+
):
|
|
134
159
|
original_directory = os.getcwd()
|
|
135
160
|
navigator = DirectoryNavigator()
|
|
136
161
|
navigator.navigate_to_target()
|
|
137
162
|
|
|
138
163
|
self.validate_input(artefact_name, classifier, aspect)
|
|
139
|
-
artefact_file_path = self.file_manager.get_artefact_file_path(
|
|
164
|
+
artefact_file_path = self.file_manager.get_artefact_file_path(
|
|
165
|
+
artefact_name, classifier
|
|
166
|
+
)
|
|
140
167
|
data_dir = self.file_manager.get_data_directory_path(artefact_name, classifier)
|
|
141
168
|
self.file_manager.create_directory(artefact_file_path, data_dir)
|
|
142
|
-
self.file_manager.copy_aspect_templates_to_directory(
|
|
143
|
-
|
|
144
|
-
|
|
169
|
+
self.file_manager.copy_aspect_templates_to_directory(
|
|
170
|
+
aspect, print_relative_to=original_directory
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if aspect == "step":
|
|
145
174
|
# Instead of generating from behave command, read from the template file
|
|
146
175
|
template_file_path = f"{aspect}.md"
|
|
147
176
|
try:
|
|
148
|
-
with open(template_file_path,
|
|
177
|
+
with open(template_file_path, "r", encoding="utf-8") as file:
|
|
149
178
|
steps_content = file.read()
|
|
150
|
-
self.file_manager.save_behave_steps_to_file(
|
|
179
|
+
self.file_manager.save_behave_steps_to_file(
|
|
180
|
+
artefact_name, steps_content
|
|
181
|
+
)
|
|
151
182
|
except FileNotFoundError:
|
|
152
183
|
# Fallback to the original behavior if template doesn't exist
|
|
153
184
|
steps = self.file_manager.generate_behave_steps(artefact_name)
|
|
154
185
|
self.file_manager.save_behave_steps_to_file(artefact_name, steps)
|
|
155
186
|
|
|
156
|
-
os.chdir(original_directory)
|
|
187
|
+
os.chdir(original_directory)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
print('Hello Global')
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Given the existing feature file {feature_file_path} and the implementation change described in {implementation_change_reference} (for example, a specific source file or change request), create new scenarios and update the feature file directly so that the new behaviour is fully covered.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
I have changed the implementation file {updated_implementation_file_path} by adding new functionality after the existing comment markers. Update the related feature file {feature_file_path} so that the Then-steps match the new behaviour and, if needed, create additional scenarios to cover the new logic.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Analyse the codebase and all related feature files using the semantic search tool {codebase_collector_tool_name} (either codebase_collector_features or codebase_collector_frontend, depending on the project). Use a detailed semantic query {codebase_query_description} derived from the user story in {user_story_task_file_path}. Based on the retrieved context, list which new tasks are needed for code changes, tests, and documentation. Then, use the appropriate agents (autocoder_behavetests_agent, autocoder_agent, documentation_agent, feature_creation_agent, test_generation_agent, web_autocoder_agent) so that the necessary feature files and tests are created or updated according to your findings.
|
ara_cli/templates/prompt-modules/commands/choose_best_parent_artefact.interview_agent.commands.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Using the find_the_best_suited_parent_artefact tool for the given artifact, determine which artefact is best suited to capture the value. Explain your reasoning and, if needed, propose concrete artefact names or formats.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
From the artefact content at {example_artefact_file_path}, create a set of implementation tasks with the exact specified names and content and assign them logically to the responsible user {assignee_identifier}. Do not modify the provided task texts; just materialise them as individual task entries in the appropriate location.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Create pytest tests for all modules that currently have 0% coverage according to the coverage report at {coverage_report_path}. Generate test files for each uncovered module in {tests_output_directory}, ensuring that basic behaviour and key error cases are tested.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Based on the information provided in the video described in {video_description_source} (or the attached video), derive the matching feature files and their content under {features_directory_path}. Use the parent user story in {user_story_file_path} and the existing matching rules in {matching_rules_file_path}, then update existing feature files or create new ones so that all described behaviours are covered.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Describe in detail what this agent can do for the user. Focus on supported capabilities, limitations, and typical workflows, and output the description as user-facing documentation. Do not use any tools.
|