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
@@ -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}")
@@ -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(dir_path, template_path, f"template.{classifier}.prompt_log.md", f"{classifier}.prompt_log.md")
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 == 'feature':
34
- self._copy_template_file(dir_path, template_path, "template.steps.prompt_log.md", "steps.prompt_log.md")
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(f"Destination directory {destination.parent} does not exist!")
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("File already exists. Do you want to overwrite the existing file and directory? (y/N): ")
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(f"Template file '{template_name}' not found in the specified template path.")
85
+ raise FileNotFoundError(
86
+ f"Template file '{template_name}' not found in the specified template path."
87
+ )
72
88
 
73
- def set_artefact_parent(self, artefact, parent_classifier, parent_file_name) -> Artefact:
74
- classified_artefacts = ArtefactReader.read_artefacts()
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(filter(lambda a: a.file_name == parent_file_name, artefact_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(self, filename, classifier, parent_classifier=None, parent_name=None, rule=None):
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("Invalid classifier provided. Please provide a valid classifier.")
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(sub_directory, f"{filename}.{classifier}")
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, 'w', encoding='utf-8') as artefact_file:
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(':')[1].strip()
165
+ return line.split(":")[1].strip()
142
166
 
143
167
  raise ValueError(f"Title not found in the parent file {file_path}")
@@ -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="creator_unknown",
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(cls, values: 'Scenario') -> 'Scenario':
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 values.steps:
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 values
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(cls, values: 'ScenarioOutline') -> 'ScenarioOutline':
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 values.steps:
225
+ for step in self.steps:
226
226
  found = re.findall(r'<([^>]+)>', step)
227
227
  placeholders.update(found)
228
- for example in values.examples:
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 values
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