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
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from ara_cli.prompt_handler import LLMSingleton
|
|
5
|
+
from langfuse.api.resources.commons.errors import Error as LangfuseError, NotFoundError
|
|
6
|
+
from ara_cli.classifier import Classifier
|
|
7
|
+
from ara_cli.artefact_reader import ArtefactReader
|
|
8
|
+
from ara_cli.artefact_creator import ArtefactCreator
|
|
9
|
+
from ara_cli.error_handler import AraError
|
|
10
|
+
from ara_cli.directory_navigator import DirectoryNavigator
|
|
11
|
+
from ara_cli.artefact_deleter import ArtefactDeleter
|
|
12
|
+
from ara_cli.children_contribution_updater import ChildrenContributionUpdater
|
|
13
|
+
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AraArtefactConverter:
|
|
17
|
+
def __init__(self, file_system=None):
|
|
18
|
+
self.file_system = file_system or os
|
|
19
|
+
self.reader = ArtefactReader()
|
|
20
|
+
self.creator = ArtefactCreator(self.file_system)
|
|
21
|
+
self.children_updater = ChildrenContributionUpdater(self.file_system)
|
|
22
|
+
|
|
23
|
+
def convert(
|
|
24
|
+
self,
|
|
25
|
+
old_classifier: str,
|
|
26
|
+
artefact_name: str,
|
|
27
|
+
new_classifier: str,
|
|
28
|
+
merge: bool = False,
|
|
29
|
+
override: bool = False,
|
|
30
|
+
force: bool = False,
|
|
31
|
+
children_action: Optional[str] = None,
|
|
32
|
+
new_parent_classifier: Optional[str] = None,
|
|
33
|
+
new_parent_name: Optional[str] = None,
|
|
34
|
+
json_output: bool = False,
|
|
35
|
+
):
|
|
36
|
+
self._validate_classifiers(old_classifier, new_classifier)
|
|
37
|
+
|
|
38
|
+
content = self._read_and_validate_source(artefact_name, old_classifier)
|
|
39
|
+
|
|
40
|
+
# Handle children contributions BEFORE conversion
|
|
41
|
+
if not self._handle_children_if_needed(
|
|
42
|
+
artefact_name,
|
|
43
|
+
old_classifier,
|
|
44
|
+
new_classifier,
|
|
45
|
+
force,
|
|
46
|
+
children_action,
|
|
47
|
+
new_parent_classifier,
|
|
48
|
+
new_parent_name,
|
|
49
|
+
json_output,
|
|
50
|
+
):
|
|
51
|
+
return # User cancelled
|
|
52
|
+
|
|
53
|
+
target_content_existing = self._resolve_target_content(
|
|
54
|
+
artefact_name, new_classifier, merge, override
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
self._execute_conversion(
|
|
58
|
+
old_classifier,
|
|
59
|
+
new_classifier,
|
|
60
|
+
artefact_name,
|
|
61
|
+
content,
|
|
62
|
+
target_content_existing,
|
|
63
|
+
merge,
|
|
64
|
+
override,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
self._cleanup_after_conversion(old_classifier, new_classifier, artefact_name)
|
|
68
|
+
|
|
69
|
+
def _read_and_validate_source(self, artefact_name: str, classifier: str) -> str:
|
|
70
|
+
"""Read source artefact and validate it can be parsed."""
|
|
71
|
+
content, artefact_info = self.reader.read_artefact_data(
|
|
72
|
+
artefact_name, classifier
|
|
73
|
+
)
|
|
74
|
+
if not content or not artefact_info:
|
|
75
|
+
raise AraError(
|
|
76
|
+
f"Artefact '{artefact_name}' of type '{classifier}' not found"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
self._validate_artefact_parseable(content, artefact_name, is_source=True)
|
|
80
|
+
return content
|
|
81
|
+
|
|
82
|
+
def _validate_artefact_parseable(
|
|
83
|
+
self, content: str, artefact_name: str, is_source: bool = True
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Validate that artefact content can be parsed."""
|
|
86
|
+
artefact_type = "input" if is_source else "target"
|
|
87
|
+
try:
|
|
88
|
+
artefact = artefact_from_content(content)
|
|
89
|
+
if artefact is None:
|
|
90
|
+
raise AraError(
|
|
91
|
+
f'Invalid {artefact_type} artefact: {artefact_name}. Run "ara scan" and "ara autofix" first.'
|
|
92
|
+
)
|
|
93
|
+
except AraError:
|
|
94
|
+
raise
|
|
95
|
+
except Exception:
|
|
96
|
+
raise AraError(
|
|
97
|
+
f'Invalid {artefact_type} artefact: {artefact_name}. Run "ara scan" and "ara autofix" first.'
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def _handle_children_if_needed(
|
|
101
|
+
self,
|
|
102
|
+
artefact_name: str,
|
|
103
|
+
old_classifier: str,
|
|
104
|
+
new_classifier: str,
|
|
105
|
+
force: bool,
|
|
106
|
+
children_action: Optional[str],
|
|
107
|
+
new_parent_classifier: Optional[str],
|
|
108
|
+
new_parent_name: Optional[str],
|
|
109
|
+
json_output: bool,
|
|
110
|
+
) -> bool:
|
|
111
|
+
"""Handle children contributions if classifier is changing. Returns True to continue."""
|
|
112
|
+
if old_classifier == new_classifier:
|
|
113
|
+
return True
|
|
114
|
+
return self.children_updater.update_children_contributions(
|
|
115
|
+
artefact_name,
|
|
116
|
+
old_classifier,
|
|
117
|
+
new_classifier,
|
|
118
|
+
force,
|
|
119
|
+
children_action=children_action,
|
|
120
|
+
new_parent_classifier=new_parent_classifier,
|
|
121
|
+
new_parent_name=new_parent_name,
|
|
122
|
+
json_output=json_output,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _execute_conversion(
|
|
126
|
+
self,
|
|
127
|
+
old_classifier: str,
|
|
128
|
+
new_classifier: str,
|
|
129
|
+
artefact_name: str,
|
|
130
|
+
content: str,
|
|
131
|
+
target_content_existing: Optional[str],
|
|
132
|
+
merge: bool,
|
|
133
|
+
override: bool,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Execute the LLM conversion and write the result."""
|
|
136
|
+
target_class = self._get_target_class(new_classifier)
|
|
137
|
+
|
|
138
|
+
prompt = self._get_prompt(
|
|
139
|
+
old_classifier=old_classifier,
|
|
140
|
+
new_classifier=new_classifier,
|
|
141
|
+
artefact_name=artefact_name,
|
|
142
|
+
content=content,
|
|
143
|
+
target_content_existing=target_content_existing,
|
|
144
|
+
merge=merge,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
action = "Merging" if merge and target_content_existing else "Converting"
|
|
148
|
+
print(
|
|
149
|
+
f"\n{action} '{artefact_name}' from {old_classifier} to {new_classifier}..."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
converted_artefact = self._run_conversion_agent(prompt, target_class)
|
|
153
|
+
self._write_artefact(
|
|
154
|
+
new_classifier,
|
|
155
|
+
artefact_name,
|
|
156
|
+
converted_artefact.serialize(),
|
|
157
|
+
merge,
|
|
158
|
+
override,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _cleanup_after_conversion(
|
|
162
|
+
self, old_classifier: str, new_classifier: str, artefact_name: str
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Move data folder and delete old artefact after successful conversion."""
|
|
165
|
+
if old_classifier == new_classifier:
|
|
166
|
+
return
|
|
167
|
+
self._move_data_folder_content(old_classifier, new_classifier, artefact_name)
|
|
168
|
+
ArtefactDeleter(self.file_system).delete(
|
|
169
|
+
artefact_name, old_classifier, force=True
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _validate_classifiers(self, old_classifier: str, new_classifier: str):
|
|
173
|
+
if not Classifier.is_valid_classifier(old_classifier):
|
|
174
|
+
raise ValueError(f"Invalid classifier: {old_classifier}")
|
|
175
|
+
if not Classifier.is_valid_classifier(new_classifier):
|
|
176
|
+
raise ValueError(f"Invalid classifier: {new_classifier}")
|
|
177
|
+
|
|
178
|
+
def _move_data_folder_content(self, old_classifier, new_classifier, artefact_name):
|
|
179
|
+
import shutil
|
|
180
|
+
|
|
181
|
+
navigator = DirectoryNavigator()
|
|
182
|
+
navigator.navigate_to_target()
|
|
183
|
+
|
|
184
|
+
sub_directory_old = Classifier.get_sub_directory(old_classifier)
|
|
185
|
+
dir_path_old = self.file_system.path.join(
|
|
186
|
+
sub_directory_old, f"{artefact_name}.data"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
sub_directory_new = Classifier.get_sub_directory(new_classifier)
|
|
190
|
+
dir_path_new = self.file_system.path.join(
|
|
191
|
+
sub_directory_new, f"{artefact_name}.data"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if self.file_system.path.exists(dir_path_old):
|
|
195
|
+
backup_folder_name = f"{artefact_name}.data.old"
|
|
196
|
+
destination_path = self.file_system.path.join(
|
|
197
|
+
dir_path_new, backup_folder_name
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if not self.file_system.path.exists(dir_path_new):
|
|
201
|
+
os.makedirs(dir_path_new, exist_ok=True)
|
|
202
|
+
|
|
203
|
+
if self.file_system.path.exists(destination_path):
|
|
204
|
+
shutil.rmtree(destination_path)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
shutil.move(dir_path_old, destination_path)
|
|
208
|
+
print(f"Moved old data to {destination_path}")
|
|
209
|
+
except Exception as e:
|
|
210
|
+
print(f"Error moving data directory: {e}")
|
|
211
|
+
|
|
212
|
+
def _resolve_target_content(
|
|
213
|
+
self, artefact_name: str, new_classifier: str, merge: bool, override: bool
|
|
214
|
+
):
|
|
215
|
+
if merge:
|
|
216
|
+
target_content_existing, _ = self.reader.read_artefact_data(
|
|
217
|
+
artefact_name, new_classifier
|
|
218
|
+
)
|
|
219
|
+
return target_content_existing
|
|
220
|
+
|
|
221
|
+
if override:
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
# Check if target already exists
|
|
225
|
+
target_content, new_artefact_info = self.reader.read_artefact_data(
|
|
226
|
+
artefact_name, new_classifier
|
|
227
|
+
)
|
|
228
|
+
if new_artefact_info:
|
|
229
|
+
# Only validate if content is not None (artefact file exists and is readable)
|
|
230
|
+
if target_content is not None:
|
|
231
|
+
self._validate_artefact_parseable(
|
|
232
|
+
target_content, artefact_name, is_source=False
|
|
233
|
+
)
|
|
234
|
+
raise ValueError(
|
|
235
|
+
f"Found already existing {new_classifier} {artefact_name}. Rerun the command with --override or --merge."
|
|
236
|
+
)
|
|
237
|
+
return None
|
|
238
|
+
|
|
239
|
+
def _get_target_class(self, new_classifier: str):
|
|
240
|
+
from ara_cli.artefact_models.artefact_mapping import artefact_type_mapping
|
|
241
|
+
from ara_cli.artefact_models.artefact_model import ArtefactType
|
|
242
|
+
|
|
243
|
+
target_type = ArtefactType(new_classifier)
|
|
244
|
+
target_class = artefact_type_mapping.get(target_type)
|
|
245
|
+
|
|
246
|
+
if not target_class:
|
|
247
|
+
raise AraError(f"No artefact class found for classifier: {new_classifier}")
|
|
248
|
+
return target_class
|
|
249
|
+
|
|
250
|
+
def _get_prompt(
|
|
251
|
+
self,
|
|
252
|
+
old_classifier,
|
|
253
|
+
new_classifier,
|
|
254
|
+
artefact_name,
|
|
255
|
+
content,
|
|
256
|
+
target_content_existing,
|
|
257
|
+
merge,
|
|
258
|
+
):
|
|
259
|
+
try:
|
|
260
|
+
langfuse = LLMSingleton.get_instance().langfuse
|
|
261
|
+
if langfuse is None:
|
|
262
|
+
# This mimics the behavior if authentication failed or env vars missing in Singleton
|
|
263
|
+
raise Exception("Langfuse not initialized in Singleton")
|
|
264
|
+
|
|
265
|
+
if merge and target_content_existing:
|
|
266
|
+
prompt_template = langfuse.get_prompt("ara-cli/artefact-convert/merge")
|
|
267
|
+
return prompt_template.compile(
|
|
268
|
+
old_classifier=old_classifier,
|
|
269
|
+
new_classifier=new_classifier,
|
|
270
|
+
artefact_name=artefact_name,
|
|
271
|
+
content=content,
|
|
272
|
+
target_content_existing=target_content_existing,
|
|
273
|
+
)
|
|
274
|
+
else:
|
|
275
|
+
prompt_template = langfuse.get_prompt(
|
|
276
|
+
"ara-cli/artefact-convert/default"
|
|
277
|
+
)
|
|
278
|
+
return prompt_template.compile(
|
|
279
|
+
old_classifier=old_classifier,
|
|
280
|
+
new_classifier=new_classifier,
|
|
281
|
+
artefact_name=artefact_name,
|
|
282
|
+
content=content,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
except (LangfuseError, NotFoundError, Exception) as e:
|
|
286
|
+
logging.info(f"Could not fetch Langfuse prompt: {e}. Using fallback.")
|
|
287
|
+
# Fallback prompts
|
|
288
|
+
formatting_instructions = (
|
|
289
|
+
"### Data Extraction Rules:\n"
|
|
290
|
+
"- **Users**: Extract usernames ONLY. Do NOT add '@' or 'user_' prefixes. "
|
|
291
|
+
"The system adds these automatically. (e.g., return 'hans', NOT '@user_hans').\n"
|
|
292
|
+
"- **Author**: Extract the author as 'creator_<name>'. Do NOT add the '@' symbol. "
|
|
293
|
+
"(e.g., return 'creator_unknown', NOT '@creator_unknown').\n"
|
|
294
|
+
f"- **Artefact Name**: Use strictly '{artefact_name}'.\n"
|
|
295
|
+
"- **Content**: Adapt the content to the target schema fields."
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if merge and target_content_existing:
|
|
299
|
+
return (
|
|
300
|
+
f"Merge the following {old_classifier} artefact into the existing {new_classifier} artefact. "
|
|
301
|
+
"Combine the information from both, prioritizing the structure of the target artefact schema. "
|
|
302
|
+
"Ensure no critical information is lost. "
|
|
303
|
+
"\n\n"
|
|
304
|
+
f"{formatting_instructions}"
|
|
305
|
+
"\n\n"
|
|
306
|
+
f"Source Artefact ({old_classifier}):\n```\n{content}\n```"
|
|
307
|
+
f"\n\nTarget Artefact ({new_classifier}):\n```\n{target_content_existing}\n```"
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
return (
|
|
311
|
+
f"Convert the following {old_classifier} artefact to a {new_classifier} artefact. "
|
|
312
|
+
"Preserve the core meaning, business value, and description. "
|
|
313
|
+
"Map the content to the fields required by the target schema. "
|
|
314
|
+
"\n\n"
|
|
315
|
+
f"{formatting_instructions}"
|
|
316
|
+
"\n\n"
|
|
317
|
+
f"Source Artefact Content:\n```\n{content}\n```"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
def _run_conversion_agent(self, prompt, target_class):
|
|
321
|
+
from ara_cli.llm_utils import create_pydantic_ai_agent
|
|
322
|
+
|
|
323
|
+
agent = create_pydantic_ai_agent(output_type=target_class, instrument=True)
|
|
324
|
+
try:
|
|
325
|
+
result = agent.run_sync(prompt)
|
|
326
|
+
return result.output
|
|
327
|
+
except Exception as e:
|
|
328
|
+
raise AraError(f"LLM conversion failed: {e}")
|
|
329
|
+
|
|
330
|
+
def _write_artefact(
|
|
331
|
+
self, new_classifier, artefact_name, artefact_content, merge, override
|
|
332
|
+
):
|
|
333
|
+
from shutil import rmtree
|
|
334
|
+
|
|
335
|
+
navigator = DirectoryNavigator()
|
|
336
|
+
navigator.navigate_to_target()
|
|
337
|
+
|
|
338
|
+
sub_directory = Classifier.get_sub_directory(new_classifier)
|
|
339
|
+
file_path = self.file_system.path.join(
|
|
340
|
+
sub_directory, f"{artefact_name}.{new_classifier}"
|
|
341
|
+
)
|
|
342
|
+
dir_path = self.file_system.path.join(sub_directory, f"{artefact_name}.data")
|
|
343
|
+
|
|
344
|
+
if self.file_system.path.exists(file_path) and not (override or merge):
|
|
345
|
+
raise ValueError(f"Target file {file_path} already exists.")
|
|
346
|
+
|
|
347
|
+
if not merge:
|
|
348
|
+
rmtree(dir_path, ignore_errors=True)
|
|
349
|
+
|
|
350
|
+
os.makedirs(dir_path, exist_ok=True)
|
|
351
|
+
|
|
352
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
353
|
+
f.write(artefact_content)
|
|
354
|
+
|
|
355
|
+
print(f"Conversion successful. Created: {file_path}")
|
ara_cli/artefact_creator.py
CHANGED
|
@@ -27,11 +27,21 @@ class ArtefactCreator:
|
|
|
27
27
|
raise ValueError("classifier must not be None or empty!")
|
|
28
28
|
|
|
29
29
|
# Standard prompt log artefact
|
|
30
|
-
self._copy_template_file(
|
|
30
|
+
self._copy_template_file(
|
|
31
|
+
dir_path,
|
|
32
|
+
template_path,
|
|
33
|
+
f"template.{classifier}.prompt_log.md",
|
|
34
|
+
f"{classifier}.prompt_log.md",
|
|
35
|
+
)
|
|
31
36
|
|
|
32
37
|
# Additional prompt log artefact for 'feature' classifier
|
|
33
|
-
if classifier ==
|
|
34
|
-
self._copy_template_file(
|
|
38
|
+
if classifier == "feature":
|
|
39
|
+
self._copy_template_file(
|
|
40
|
+
dir_path,
|
|
41
|
+
template_path,
|
|
42
|
+
"template.steps.prompt_log.md",
|
|
43
|
+
"steps.prompt_log.md",
|
|
44
|
+
)
|
|
35
45
|
|
|
36
46
|
def _copy_template_file(self, dir_path, template_path, source_name, dest_name):
|
|
37
47
|
source = Path(template_path) / source_name
|
|
@@ -41,7 +51,9 @@ class ArtefactCreator:
|
|
|
41
51
|
raise FileNotFoundError(f"Source file {source} not found!")
|
|
42
52
|
|
|
43
53
|
if not destination.parent.exists():
|
|
44
|
-
raise NotADirectoryError(
|
|
54
|
+
raise NotADirectoryError(
|
|
55
|
+
f"Destination directory {destination.parent} does not exist!"
|
|
56
|
+
)
|
|
45
57
|
|
|
46
58
|
copyfile(source, destination)
|
|
47
59
|
|
|
@@ -59,7 +71,9 @@ class ArtefactCreator:
|
|
|
59
71
|
|
|
60
72
|
def handle_existing_files(self, file_exists):
|
|
61
73
|
if file_exists:
|
|
62
|
-
user_choice = input(
|
|
74
|
+
user_choice = input(
|
|
75
|
+
"File already exists. Do you want to overwrite the existing file and directory? (y/N): "
|
|
76
|
+
)
|
|
63
77
|
if user_choice.lower() != "y":
|
|
64
78
|
print("No changes were made to the existing file and directory.")
|
|
65
79
|
return False
|
|
@@ -68,14 +82,20 @@ class ArtefactCreator:
|
|
|
68
82
|
def validate_template(self, template_path, classifier):
|
|
69
83
|
template_name = f"template.{classifier}"
|
|
70
84
|
if not self.template_exists(template_path, template_name):
|
|
71
|
-
raise FileNotFoundError(
|
|
85
|
+
raise FileNotFoundError(
|
|
86
|
+
f"Template file '{template_name}' not found in the specified template path."
|
|
87
|
+
)
|
|
72
88
|
|
|
73
|
-
def set_artefact_parent(
|
|
74
|
-
|
|
89
|
+
def set_artefact_parent(
|
|
90
|
+
self, artefact, parent_classifier, parent_file_name
|
|
91
|
+
) -> Artefact:
|
|
92
|
+
classified_artefacts = ArtefactReader(self.file_system).read_artefacts()
|
|
75
93
|
if parent_classifier not in classified_artefacts:
|
|
76
94
|
return artefact
|
|
77
95
|
artefact_list = classified_artefacts[parent_classifier]
|
|
78
|
-
matching_artefacts = list(
|
|
96
|
+
matching_artefacts = list(
|
|
97
|
+
filter(lambda a: a.file_name == parent_file_name, artefact_list)
|
|
98
|
+
)
|
|
79
99
|
if not matching_artefacts:
|
|
80
100
|
artefact_names_list = [a.file_name for a in artefact_list]
|
|
81
101
|
suggest_close_name_matches(parent_file_name, artefact_names_list)
|
|
@@ -83,17 +103,23 @@ class ArtefactCreator:
|
|
|
83
103
|
artefact._parent = matching_artefacts[0]
|
|
84
104
|
return artefact
|
|
85
105
|
|
|
86
|
-
def run(
|
|
106
|
+
def run(
|
|
107
|
+
self, filename, classifier, parent_classifier=None, parent_name=None, rule=None
|
|
108
|
+
):
|
|
87
109
|
# make sure this function is always called from the ara top level directory
|
|
88
110
|
original_directory = os.getcwd()
|
|
89
111
|
navigator = DirectoryNavigator()
|
|
90
112
|
navigator.navigate_to_target()
|
|
91
113
|
|
|
92
114
|
if not Classifier.is_valid_classifier(classifier):
|
|
93
|
-
raise ValueError(
|
|
115
|
+
raise ValueError(
|
|
116
|
+
"Invalid classifier provided. Please provide a valid classifier."
|
|
117
|
+
)
|
|
94
118
|
|
|
95
119
|
sub_directory = Classifier.get_sub_directory(classifier)
|
|
96
|
-
file_path = self.file_system.path.join(
|
|
120
|
+
file_path = self.file_system.path.join(
|
|
121
|
+
sub_directory, f"{filename}.{classifier}"
|
|
122
|
+
)
|
|
97
123
|
dir_path = self.file_system.path.join(sub_directory, f"{filename}.data")
|
|
98
124
|
|
|
99
125
|
file_exists = self.file_system.path.exists(file_path)
|
|
@@ -105,9 +131,7 @@ class ArtefactCreator:
|
|
|
105
131
|
|
|
106
132
|
if parent_classifier and parent_name:
|
|
107
133
|
artefact.set_contribution(
|
|
108
|
-
artefact_name=parent_name,
|
|
109
|
-
classifier=parent_classifier,
|
|
110
|
-
rule=rule
|
|
134
|
+
artefact_name=parent_name, classifier=parent_classifier, rule=rule
|
|
111
135
|
)
|
|
112
136
|
else:
|
|
113
137
|
artefact.set_contribution(None, None, None)
|
|
@@ -115,7 +139,7 @@ class ArtefactCreator:
|
|
|
115
139
|
artefact_content = artefact.serialize()
|
|
116
140
|
rmtree(dir_path, ignore_errors=True)
|
|
117
141
|
os.makedirs(dir_path, exist_ok=True)
|
|
118
|
-
with open(file_path,
|
|
142
|
+
with open(file_path, "w", encoding="utf-8") as artefact_file:
|
|
119
143
|
artefact_file.write(artefact_content)
|
|
120
144
|
|
|
121
145
|
relative_file_path = os.path.relpath(file_path, original_directory)
|
|
@@ -138,6 +162,6 @@ class ArtefactCreator:
|
|
|
138
162
|
with open(file_path, "r", encoding="utf-8") as file:
|
|
139
163
|
for line in file:
|
|
140
164
|
if line.strip().startswith(title):
|
|
141
|
-
return line.split(
|
|
165
|
+
return line.split(":")[1].strip()
|
|
142
166
|
|
|
143
167
|
raise ValueError(f"Title not found in the parent file {file_path}")
|
ara_cli/artefact_lister.py
CHANGED
|
@@ -28,7 +28,7 @@ class ArtefactLister:
|
|
|
28
28
|
def list_files(
|
|
29
29
|
self, tags=None, navigate_to_target=False, list_filter: ListFilter | None = None
|
|
30
30
|
):
|
|
31
|
-
artefact_list = ArtefactReader.read_artefacts(tags=tags)
|
|
31
|
+
artefact_list = ArtefactReader(self.file_system).read_artefacts(tags=tags)
|
|
32
32
|
artefact_list = self.filter_artefacts(artefact_list, list_filter)
|
|
33
33
|
|
|
34
34
|
filtered_artefact_list = {
|
|
@@ -54,7 +54,7 @@ class ArtefactLister:
|
|
|
54
54
|
)
|
|
55
55
|
|
|
56
56
|
artefacts_by_classifier = {classifier: []}
|
|
57
|
-
ArtefactReader.step_through_value_chain(
|
|
57
|
+
ArtefactReader(self.file_system).step_through_value_chain(
|
|
58
58
|
artefact_name=artefact_name,
|
|
59
59
|
classifier=classifier,
|
|
60
60
|
artefacts_by_classifier=artefacts_by_classifier,
|
|
@@ -79,7 +79,7 @@ class ArtefactLister:
|
|
|
79
79
|
artefact_name, [info["title"] for info in artefact_info]
|
|
80
80
|
)
|
|
81
81
|
|
|
82
|
-
child_artefacts = ArtefactReader.find_children(
|
|
82
|
+
child_artefacts = ArtefactReader(self.file_system).find_children(
|
|
83
83
|
artefact_name=artefact_name, classifier=classifier
|
|
84
84
|
)
|
|
85
85
|
|
|
@@ -185,7 +185,7 @@ class Artefact(BaseModel, ABC):
|
|
|
185
185
|
description="Optional list of tags (0-many)",
|
|
186
186
|
)
|
|
187
187
|
author: Optional[str] = Field(
|
|
188
|
-
default=
|
|
188
|
+
default=None,
|
|
189
189
|
description="Author of the artefact, must be a single entry of the form 'creator_<someone>'."
|
|
190
190
|
)
|
|
191
191
|
title: str = Field(
|
|
@@ -30,7 +30,6 @@ def _default_vision(title: str, use_default_contribution: bool) -> VisionArtefac
|
|
|
30
30
|
)
|
|
31
31
|
return VisionArtefact(
|
|
32
32
|
tags=[],
|
|
33
|
-
author="creator_unknown",
|
|
34
33
|
title=title,
|
|
35
34
|
description="<further optional description to understand the vision, markdown capable text formatting>",
|
|
36
35
|
intent=intent,
|
|
@@ -46,7 +45,6 @@ def _default_businessgoal(title: str, use_default_contribution: bool) -> Busines
|
|
|
46
45
|
)
|
|
47
46
|
return BusinessgoalArtefact(
|
|
48
47
|
tags=[],
|
|
49
|
-
author="creator_unknown",
|
|
50
48
|
title=title,
|
|
51
49
|
description="<further optional description to understand the businessgoal, markdown capable text formatting>",
|
|
52
50
|
intent=intent,
|
|
@@ -60,7 +58,6 @@ def _default_capability(title: str, use_default_contribution: bool) -> Capabilit
|
|
|
60
58
|
)
|
|
61
59
|
return CapabilityArtefact(
|
|
62
60
|
tags=[],
|
|
63
|
-
author="creator_unknown",
|
|
64
61
|
title=title,
|
|
65
62
|
description="<further optional description to understand the capability, markdown capable text formatting>",
|
|
66
63
|
intent=intent,
|
|
@@ -81,7 +78,6 @@ def _default_epic(title: str, use_default_contribution: bool) -> EpicArtefact:
|
|
|
81
78
|
]
|
|
82
79
|
return EpicArtefact(
|
|
83
80
|
tags=[],
|
|
84
|
-
author="creator_unknown",
|
|
85
81
|
title=title,
|
|
86
82
|
description="<further optional description to understand the epic, markdown capable text formatting>",
|
|
87
83
|
intent=intent,
|
|
@@ -103,7 +99,6 @@ def _default_userstory(title: str, use_default_contribution: bool) -> UserstoryA
|
|
|
103
99
|
]
|
|
104
100
|
return UserstoryArtefact(
|
|
105
101
|
tags=[],
|
|
106
|
-
author="creator_unknown",
|
|
107
102
|
title=title,
|
|
108
103
|
description="<further optional description to understand the userstory, markdown capable text formatting>",
|
|
109
104
|
intent=intent,
|
|
@@ -116,7 +111,6 @@ def _default_userstory(title: str, use_default_contribution: bool) -> UserstoryA
|
|
|
116
111
|
def _default_example(title: str, use_default_contribution: bool) -> ExampleArtefact:
|
|
117
112
|
return ExampleArtefact(
|
|
118
113
|
tags=[],
|
|
119
|
-
author="creator_unknown",
|
|
120
114
|
title=title,
|
|
121
115
|
description="<further optional description to understand the example, markdown capable text formatting>",
|
|
122
116
|
contribution=default_contribution() if use_default_contribution else None
|
|
@@ -137,7 +131,6 @@ def _default_keyfeature(title: str, use_default_contribution: bool) -> Keyfeatur
|
|
|
137
131
|
AND some other result is to be expected>"""
|
|
138
132
|
return KeyfeatureArtefact(
|
|
139
133
|
tags=[],
|
|
140
|
-
author="creator_unknown",
|
|
141
134
|
title=title,
|
|
142
135
|
description=description,
|
|
143
136
|
intent=intent,
|
|
@@ -193,7 +186,6 @@ def _default_feature(title: str, use_default_contribution: bool) -> FeatureArtef
|
|
|
193
186
|
|
|
194
187
|
return FeatureArtefact(
|
|
195
188
|
tags=[],
|
|
196
|
-
author="creator_unknown",
|
|
197
189
|
title=title,
|
|
198
190
|
description=description,
|
|
199
191
|
intent=intent,
|
|
@@ -224,7 +216,6 @@ def _default_issue(title: str, use_default_contribution: bool) -> IssueArtefact:
|
|
|
224
216
|
|
|
225
217
|
return IssueArtefact(
|
|
226
218
|
tags=[],
|
|
227
|
-
author="creator_unknown",
|
|
228
219
|
title=title,
|
|
229
220
|
description=description,
|
|
230
221
|
additional_description=additional_description,
|
|
@@ -149,10 +149,10 @@ class Scenario(BaseModel):
|
|
|
149
149
|
return steps
|
|
150
150
|
|
|
151
151
|
@model_validator(mode='after')
|
|
152
|
-
def check_no_placeholders(
|
|
152
|
+
def check_no_placeholders(self) -> 'Scenario':
|
|
153
153
|
"""Ensure regular scenarios don't contain placeholders that should be in scenario outlines."""
|
|
154
154
|
placeholders = set()
|
|
155
|
-
for step in
|
|
155
|
+
for step in self.steps:
|
|
156
156
|
# Skip validation if step contains docstring placeholders (during parsing)
|
|
157
157
|
if '__DOCSTRING_PLACEHOLDER_' in step:
|
|
158
158
|
continue
|
|
@@ -170,7 +170,7 @@ class Scenario(BaseModel):
|
|
|
170
170
|
f"Scenario Contains Placeholders ({placeholder_list}) but is not a Scenario Outline. "
|
|
171
171
|
f"Use 'Scenario Outline:' instead of 'Scenario:' and provide an Examples table."
|
|
172
172
|
)
|
|
173
|
-
return
|
|
173
|
+
return self
|
|
174
174
|
|
|
175
175
|
@classmethod
|
|
176
176
|
def from_lines(cls, lines: List[str], start_idx: int) -> Tuple['Scenario', int]:
|
|
@@ -219,18 +219,18 @@ class ScenarioOutline(BaseModel):
|
|
|
219
219
|
return v
|
|
220
220
|
|
|
221
221
|
@model_validator(mode='after')
|
|
222
|
-
def check_placeholders(
|
|
222
|
+
def check_placeholders(self) -> 'ScenarioOutline':
|
|
223
223
|
"""Ensure all placeholders in steps have corresponding values in examples."""
|
|
224
224
|
placeholders = set()
|
|
225
|
-
for step in
|
|
225
|
+
for step in self.steps:
|
|
226
226
|
found = re.findall(r'<([^>]+)>', step)
|
|
227
227
|
placeholders.update(found)
|
|
228
|
-
for example in
|
|
228
|
+
for example in self.examples:
|
|
229
229
|
missing = placeholders - set(example.values.keys())
|
|
230
230
|
if missing:
|
|
231
231
|
raise ValueError(
|
|
232
232
|
f"Example is missing values for placeholders: {missing}")
|
|
233
|
-
return
|
|
233
|
+
return self
|
|
234
234
|
|
|
235
235
|
@classmethod
|
|
236
236
|
def from_lines(cls, lines: List[str], start_idx: int) -> Tuple['ScenarioOutline', int]:
|
|
@@ -298,7 +298,7 @@ class FeatureArtefact(Artefact):
|
|
|
298
298
|
def validate_artefact_type(cls, v):
|
|
299
299
|
if v != ArtefactType.feature:
|
|
300
300
|
raise ValueError(
|
|
301
|
-
f"FeatureArtefact must have artefact_type of '{ArtefactType.feature}', not '{v}'")
|
|
301
|
+
f"FeatureArtefact must have artefact_type of '{ArtefactType.feature.value}', not '{v}'")
|
|
302
302
|
return v
|
|
303
303
|
|
|
304
304
|
@classmethod
|