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
@@ -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(artefact_class, content_lines_after_extract, force, write)
80
+ _process_artefact_extraction(
81
+ artefact_class, content_lines_after_extract, force, write)
81
82
  else:
82
- print("No filename or valid artefact found, skipping processing for this block.")
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("# [x] extract", "# [v] extract", 1)
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('```') or 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) - len(stripped_fence_line.lstrip(fence_char))
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(block_start_line)
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(f"Block found and processed starting on line {block_start_line + 1}.")
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(f"Error: File not found at '{document_path}'. Skipping extraction.")
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(f"End of extraction. Found and processed {len(replacements)} blocks in '{os.path.basename(document_path)}'.")
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("Filename does not match, overwrite? (y/n): ")
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
- user_decision = prompt_user_decision("File does not exist. Create? (y/n): ")
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 = json.dumps(get_file_content(filename))
271
- content = json.dumps(content_str)
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(f"File {filename} exists. Overwriting without LLM merge as requested.")
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(block_content, filename)
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": prompt, "purpose": purpose, "model": model_info}
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 (LangfuseError, NotFoundError, Exception) as e:
278
- logging.info(f"Could not fetch Langfuse prompt: {e}")
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 = os.path.join(*header_stack, relative_path) if header_stack else relative_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 (LangfuseError, NotFoundError, Exception) as e:
659
- logging.info(f"Could not fetch Langfuse system prompt: {e}")
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
- logging.info("Using default system prompt.")
665
- system_prompt = "You are a helpful assistant that can process both text and images."
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"] = [{"type": "text", "text": str(message_content)}] + image_data_list
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, 'author') and artefact.author:
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
@@ -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"# {Chat.ROLE_PROMPT}:"
175
+ prompt_tag = f"# {ROLE_PROMPT}:"
175
176
  if Chat.get_last_role_marker(lines) == prompt_tag:
176
177
  return
177
178
 
@@ -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
- (base_path / f"template.{aspect}.exploration.md", f"{aspect}.exploration.md")
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(f"File {artefact_file_path} does not exist. Please create it first.")
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(behave_command, shell=True, capture_output=True, text=True)
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('\n')
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(i for i, line in enumerate(lines) if any(keyword in line for keyword in keywords))
102
- end_index = next(i for i, line in reversed(list(enumerate(lines))) if "raise NotImplementedError" in line)
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 = '\n'.join(lines[start_index:end_index + 1])
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, 'w', encoding='utf-8') as file:
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 = ['technology', 'concept', 'persona', 'customer', 'step']
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 not Classifier.is_valid_classifier(classifier) or classifier in self.VALID_ASPECTS:
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(f"{aspect} does not exist. Please choose one of the {self.VALID_ASPECTS} list.")
131
-
132
-
133
- def create(self, artefact_name='artefact_name', classifier='classifier', aspect='specification_breakdown_aspect'):
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(artefact_name, classifier)
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(aspect, print_relative_to=original_directory)
143
-
144
- if (aspect == "step"):
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, 'r', encoding='utf-8') as file:
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(artefact_name, steps_content)
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.
@@ -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.