ara-cli 0.1.9.93__py3-none-any.whl → 0.1.9.95__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.

Potentially problematic release.


This version of ara-cli might be problematic. Click here for more details.

Files changed (35) hide show
  1. ara_cli/__init__.py +15 -1
  2. ara_cli/ara_command_action.py +23 -43
  3. ara_cli/ara_command_parser.py +16 -1
  4. ara_cli/ara_config.py +17 -2
  5. ara_cli/artefact_autofix.py +40 -21
  6. ara_cli/artefact_creator.py +3 -1
  7. ara_cli/artefact_lister.py +29 -55
  8. ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
  9. ara_cli/artefact_renamer.py +6 -2
  10. ara_cli/chat.py +80 -34
  11. ara_cli/commands/extract_command.py +4 -3
  12. ara_cli/commands/read_command.py +104 -0
  13. ara_cli/file_loaders/document_readers.py +233 -0
  14. ara_cli/file_loaders/file_loaders.py +123 -0
  15. ara_cli/file_loaders/image_processor.py +89 -0
  16. ara_cli/file_loaders/markdown_reader.py +75 -0
  17. ara_cli/file_loaders/text_file_loader.py +9 -11
  18. ara_cli/global_file_lister.py +61 -0
  19. ara_cli/prompt_extractor.py +21 -6
  20. ara_cli/prompt_handler.py +24 -4
  21. ara_cli/tag_extractor.py +21 -11
  22. ara_cli/template_manager.py +14 -4
  23. ara_cli/update_config_prompt.py +7 -1
  24. ara_cli/version.py +1 -1
  25. {ara_cli-0.1.9.93.dist-info → ara_cli-0.1.9.95.dist-info}/METADATA +18 -17
  26. {ara_cli-0.1.9.93.dist-info → ara_cli-0.1.9.95.dist-info}/RECORD +35 -27
  27. tests/test_ara_config.py +28 -0
  28. tests/test_artefact_lister.py +52 -132
  29. tests/test_chat.py +28 -40
  30. tests/test_global_file_lister.py +131 -0
  31. tests/test_prompt_handler.py +26 -1
  32. tests/test_template_manager.py +5 -4
  33. {ara_cli-0.1.9.93.dist-info → ara_cli-0.1.9.95.dist-info}/WHEEL +0 -0
  34. {ara_cli-0.1.9.93.dist-info → ara_cli-0.1.9.95.dist-info}/entry_points.txt +0 -0
  35. {ara_cli-0.1.9.93.dist-info → ara_cli-0.1.9.95.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,89 @@
1
+ import os
2
+ import base64
3
+ import tempfile
4
+ import requests
5
+ from typing import Optional, Tuple
6
+ import re
7
+ from ara_cli.prompt_handler import describe_image
8
+
9
+
10
+ class ImageProcessor:
11
+ """Handles image processing operations"""
12
+
13
+ @staticmethod
14
+ def process_base64_image(image_ref: str, base64_pattern: re.Pattern) -> Optional[Tuple[str, str]]:
15
+ """Process base64 encoded image and return description"""
16
+ base64_match = base64_pattern.match(image_ref)
17
+ if not base64_match:
18
+ return None
19
+
20
+ image_format = base64_match.group(1)
21
+ base64_data = base64_match.group(2)
22
+ image_data = base64.b64decode(base64_data)
23
+
24
+ # Create a temporary file to send to LLM
25
+ with tempfile.NamedTemporaryFile(suffix=f'.{image_format}', delete=False) as tmp_file:
26
+ tmp_file.write(image_data)
27
+ tmp_file_path = tmp_file.name
28
+
29
+ try:
30
+ description = describe_image(tmp_file_path)
31
+ return f"Image: (base64 embedded {image_format} image)\n[{description}]", None
32
+ finally:
33
+ os.unlink(tmp_file_path)
34
+
35
+ @staticmethod
36
+ def process_url_image(image_ref: str) -> Tuple[str, Optional[str]]:
37
+ """Process image from URL and return description"""
38
+ if not image_ref.startswith(('http://', 'https://')):
39
+ return "", None
40
+
41
+ try:
42
+ response = requests.get(image_ref, timeout=10)
43
+ response.raise_for_status()
44
+
45
+ # Determine file extension from content-type
46
+ content_type = response.headers.get('content-type', '')
47
+ ext = ImageProcessor._get_extension_from_content_type(content_type, image_ref)
48
+
49
+ # Create temporary file
50
+ with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp_file:
51
+ tmp_file.write(response.content)
52
+ tmp_file_path = tmp_file.name
53
+
54
+ try:
55
+ description = describe_image(tmp_file_path)
56
+ return f"Image: {image_ref}\n[{description}]", None
57
+ finally:
58
+ os.unlink(tmp_file_path)
59
+
60
+ except Exception as e:
61
+ error_msg = f"Could not download image: {str(e)}"
62
+ return f"Image: {image_ref}\n[{error_msg}]", error_msg
63
+
64
+ @staticmethod
65
+ def process_local_image(image_ref: str, base_dir: str) -> Tuple[str, Optional[str]]:
66
+ """Process local image file and return description"""
67
+ if os.path.isabs(image_ref):
68
+ local_image_path = image_ref
69
+ else:
70
+ local_image_path = os.path.join(base_dir, image_ref)
71
+
72
+ if os.path.exists(local_image_path):
73
+ description = describe_image(local_image_path)
74
+ return f"Image: {image_ref}\n[{description}]", None
75
+ else:
76
+ error_msg = f"Image file not found"
77
+ return f"Image: {image_ref}\n[{error_msg}]", f"Local image not found: {local_image_path}"
78
+
79
+ @staticmethod
80
+ def _get_extension_from_content_type(content_type: str, url: str) -> str:
81
+ """Determine file extension from content type or URL"""
82
+ if 'image/jpeg' in content_type:
83
+ return '.jpg'
84
+ elif 'image/png' in content_type:
85
+ return '.png'
86
+ elif 'image/gif' in content_type:
87
+ return '.gif'
88
+ else:
89
+ return os.path.splitext(url)[1] or '.png'
@@ -0,0 +1,75 @@
1
+ import os
2
+ import re
3
+ from typing import Optional
4
+ from charset_normalizer import from_path
5
+ from ara_cli.file_loaders.image_processor import ImageProcessor
6
+
7
+
8
+ class MarkdownReader:
9
+ """Handles markdown file reading with optional image extraction"""
10
+
11
+ def __init__(self, file_path: str):
12
+ self.file_path = file_path
13
+ self.base_dir = os.path.dirname(file_path)
14
+ self.image_processor = ImageProcessor()
15
+
16
+ def read(self, extract_images: bool = False) -> str:
17
+ """Read markdown file and optionally extract/describe images"""
18
+ # Detect and use the most appropriate encoding
19
+ result = from_path(self.file_path).best()
20
+ if not result:
21
+ print(f"Failed to detect encoding for {self.file_path}")
22
+ return ""
23
+ content = str(result)
24
+
25
+ if not extract_images:
26
+ return content
27
+
28
+ return self._process_images(content)
29
+
30
+ def _process_images(self, content: str) -> str:
31
+ """Process all images in markdown content"""
32
+ # Pattern to match markdown images: ![alt text](url or path)
33
+ image_pattern = re.compile(r"!\[([^\]]*)\]\(([^\)]+)\)")
34
+ base64_pattern = re.compile(r"data:image/([^;]+);base64,([^)]+)")
35
+
36
+ # Process each image reference
37
+ for match in image_pattern.finditer(content):
38
+ image_ref = match.group(2)
39
+ replacement = self._process_single_image(image_ref, base64_pattern)
40
+
41
+ if replacement:
42
+ content = content.replace(match.group(0), replacement, 1)
43
+
44
+ return content
45
+
46
+ def _process_single_image(
47
+ self, image_ref: str, base64_pattern: re.Pattern
48
+ ) -> Optional[str]:
49
+ """Process a single image reference"""
50
+ try:
51
+ # Try base64 first
52
+ result = self.image_processor.process_base64_image(
53
+ image_ref, base64_pattern
54
+ )
55
+ if result:
56
+ return result[0]
57
+
58
+ # Try URL
59
+ result, error = self.image_processor.process_url_image(image_ref)
60
+ if result:
61
+ if error:
62
+ print(f"Warning: {error}")
63
+ return result
64
+
65
+ # Try local file
66
+ result, error = self.image_processor.process_local_image(
67
+ image_ref, self.base_dir
68
+ )
69
+ if error:
70
+ print(f"Warning: {error}")
71
+ return result
72
+
73
+ except Exception as e:
74
+ print(f"Warning: Could not process image {image_ref}: {e}")
75
+ return None
@@ -4,21 +4,15 @@ import base64
4
4
  import tempfile
5
5
  from typing import Optional, Tuple
6
6
  import requests
7
+ from charset_normalizer import from_path
7
8
  from ara_cli.prompt_handler import describe_image
8
9
  from ara_cli.file_loaders.file_loader import FileLoader
9
10
 
10
11
 
11
12
  class TextFileLoader(FileLoader):
12
13
  """Loads text files"""
13
-
14
- def load(
15
- self,
16
- file_path: str,
17
- prefix: str = "",
18
- suffix: str = "",
19
- block_delimiter: str = "",
20
- extract_images: bool = False
21
- ) -> bool:
14
+ def load(self, file_path: str, prefix: str = "", suffix: str = "",
15
+ block_delimiter: str = "", extract_images: bool = False, **kwargs) -> bool:
22
16
  """Load text file with optional markdown image extraction"""
23
17
 
24
18
  is_md_file = file_path.lower().endswith('.md')
@@ -27,8 +21,12 @@ class TextFileLoader(FileLoader):
27
21
  reader = MarkdownReader(file_path)
28
22
  file_content = reader.read(extract_images=True)
29
23
  else:
30
- with open(file_path, 'r', encoding='utf-8', errors='replace') as file:
31
- file_content = file.read()
24
+ # Use charset-normalizer to detect encoding
25
+ encoded_content = from_path(file_path).best()
26
+ if not encoded_content:
27
+ print(f"Failed to detect encoding for {file_path}")
28
+ return False
29
+ file_content = str(encoded_content)
32
30
 
33
31
  if block_delimiter:
34
32
  file_content = f"{block_delimiter}\n{file_content}\n{block_delimiter}"
@@ -0,0 +1,61 @@
1
+ import os
2
+ import fnmatch
3
+ from typing import List, Dict, Any
4
+
5
+ # Ağaç yapımız için bir tip tanımı yapalım
6
+ DirTree = Dict[str, Any]
7
+
8
+ def _build_tree(root_path: str, patterns: List[str]) -> DirTree:
9
+ """Belirtilen yoldaki dizin yapısını temsil eden iç içe bir sözlük oluşturur."""
10
+ tree: DirTree = {'files': [], 'dirs': {}}
11
+ try:
12
+ for item in os.listdir(root_path):
13
+ item_path = os.path.join(root_path, item)
14
+ if os.path.isdir(item_path):
15
+ subtree = _build_tree(item_path, patterns)
16
+ # Sadece içinde dosya olan veya dosyası olan alt klasörleri ekle
17
+ if subtree['files'] or subtree['dirs']:
18
+ tree['dirs'][item] = subtree
19
+ elif os.path.isfile(item_path):
20
+ # Dosyanın verilen desenlerden herhangi biriyle eşleşip eşleşmediğini kontrol et
21
+ if any(fnmatch.fnmatch(item, pattern) for pattern in patterns):
22
+ tree['files'].append(item)
23
+ except OSError as e:
24
+ print(f"Warning: Could not access path {root_path}: {e}")
25
+ return tree
26
+
27
+ def _write_tree_to_markdown(md_file, tree: DirTree, level: int):
28
+ """Ağaç veri yapısını markdown formatında dosyaya yazar."""
29
+ # Dosyaları girintili olarak yaz
30
+ indent = ' ' * level
31
+ for filename in sorted(tree['files']):
32
+ md_file.write(f"{indent}- [] {filename}\n")
33
+
34
+ # Alt dizinler için başlık oluştur ve recursive olarak devam et
35
+ for dirname, subtree in sorted(tree['dirs'].items()):
36
+ # Alt başlıklar için girinti yok, sadece başlık seviyesi artıyor
37
+ md_file.write(f"{' ' * (level -1)}{'#' * (level + 1)} {dirname}\n")
38
+ _write_tree_to_markdown(md_file, subtree, level + 1)
39
+
40
+ def generate_global_markdown_listing(directories: List[str], file_patterns: List[str], output_file: str):
41
+ """
42
+ Global dizinler için hiyerarşik bir markdown dosya listesi oluşturur.
43
+ En üst başlık olarak mutlak yolu kullanır, alt öğeler için göreceli isimler kullanır.
44
+ """
45
+ with open(output_file, 'w', encoding='utf-8') as md_file:
46
+ for directory in directories:
47
+ abs_dir = os.path.abspath(directory)
48
+
49
+ if not os.path.isdir(abs_dir):
50
+ print(f"Warning: Global directory not found: {abs_dir}")
51
+ md_file.write(f"# {directory}\n")
52
+ md_file.write(f" - !! UYARI: Dizin bulunamadı: {abs_dir}\n\n")
53
+ continue
54
+
55
+ tree = _build_tree(abs_dir, file_patterns)
56
+
57
+ # Sadece ağaç boş değilse yaz
58
+ if tree['files'] or tree['dirs']:
59
+ md_file.write(f"# {abs_dir}\n")
60
+ _write_tree_to_markdown(md_file, tree, 1)
61
+ md_file.write("\n")
@@ -16,7 +16,7 @@ def extract_code_blocks_md(markdown_text):
16
16
  return code_blocks
17
17
 
18
18
 
19
- def extract_responses(document_path, relative_to_ara_root=False, skip_queries=False):
19
+ def extract_responses(document_path, relative_to_ara_root=False, force=False, write=False):
20
20
  print(f"Debug: Starting extraction from {document_path}")
21
21
  block_extraction_counter = 0
22
22
 
@@ -50,7 +50,7 @@ def extract_responses(document_path, relative_to_ara_root=False, skip_queries=Fa
50
50
  block_lines = block_lines[1:] # Remove first line again after removing filename line
51
51
  block = '\n'.join(block_lines)
52
52
 
53
- handle_existing_file(file_path, block, skip_queries)
53
+ handle_existing_file(file_path, block, force, write)
54
54
  block_extraction_counter += 1
55
55
 
56
56
  # Update the markdown content
@@ -79,7 +79,7 @@ def extract_responses(document_path, relative_to_ara_root=False, skip_queries=Fa
79
79
  artefact_path = artefact.file_path
80
80
  directory = os.path.dirname(artefact_path)
81
81
  os.makedirs(directory, exist_ok=True)
82
- handle_existing_file(artefact_path, serialized_artefact, skip_queries)
82
+ handle_existing_file(artefact_path, serialized_artefact, force, write)
83
83
 
84
84
  os.chdir(original_directory)
85
85
 
@@ -182,13 +182,28 @@ def create_prompt_for_file_modification(content_str, filename):
182
182
  return prompt_text
183
183
 
184
184
 
185
- def handle_existing_file(filename, block_content, skip_query=False):
185
+ def handle_existing_file(filename, block_content, skip_query=False, write=False):
186
186
  if not os.path.isfile(filename):
187
187
  print(f"File {filename} does not exist, attempting to create")
188
188
  create_file_if_not_exist(filename, block_content, skip_query)
189
+ elif write:
190
+ print(f"File {filename} exists. Overwriting without LLM merge as requested.")
191
+ try:
192
+ directory = os.path.dirname(filename)
193
+ if directory:
194
+ os.makedirs(directory, exist_ok=True)
195
+ with open(filename, 'w', encoding='utf-8', errors='replace') as file:
196
+ file.write(block_content)
197
+ print(f"File {filename} overwritten successfully.")
198
+ except OSError as e:
199
+ print(f"Error: {e}")
200
+ print(f"Failed to overwrite file {filename} due to an OS error")
189
201
  else:
190
202
  print(f"File {filename} exists, creating modification prompt")
191
203
  prompt_text = create_prompt_for_file_modification(block_content, filename)
204
+ if prompt_text is None:
205
+ return
206
+
192
207
  messages = [{"role": "user", "content": prompt_text}]
193
208
  response = ""
194
209
 
@@ -199,12 +214,12 @@ def handle_existing_file(filename, block_content, skip_query=False):
199
214
  modify_and_save_file(response, filename)
200
215
 
201
216
 
202
- def extract_and_save_prompt_results(classifier, param):
217
+ def extract_and_save_prompt_results(classifier, param, write=False):
203
218
  sub_directory = Classifier.get_sub_directory(classifier)
204
219
  prompt_log_file = f"ara/{sub_directory}/{param}.data/{classifier}.prompt_log.md"
205
220
  print(f"Extract marked sections from: {prompt_log_file}")
206
221
 
207
- extract_responses(prompt_log_file)
222
+ extract_responses(prompt_log_file, write=write)
208
223
 
209
224
 
210
225
  def update_markdown(original_content, block_content, filename):
ara_cli/prompt_handler.py CHANGED
@@ -256,6 +256,8 @@ def initialize_prompt_templates(classifier, parameter):
256
256
  # Mark the relevant artefact in the givens list
257
257
  generate_config_prompt_givens_file(prompt_data_path, "config.prompt_givens.md", artefact_to_mark=f"{parameter}.{classifier}")
258
258
 
259
+ generate_config_prompt_global_givens_file(prompt_data_path, "config.prompt_global_givens.md")
260
+
259
261
 
260
262
  def write_template_files_to_config(template_type, config_file, base_template_path):
261
263
  template_path = os.path.join(base_template_path, template_type)
@@ -383,10 +385,16 @@ def extract_and_load_markdown_files(md_prompt_file_path):
383
385
 
384
386
 
385
387
  def load_givens(file_path):
386
- content = "### GIVENS\n\n"
388
+ content = ""
387
389
  image_data_list = []
388
390
  markdown_items = extract_and_load_markdown_files(file_path)
389
391
 
392
+ # Only proceed and add the header if there are marked items to load.
393
+ if not markdown_items:
394
+ return "", []
395
+
396
+ content = "### GIVENS\n\n"
397
+
390
398
  for item in markdown_items:
391
399
  if item.lower().endswith(('.png', '.jpeg', '.jpg')):
392
400
  with open(item, "rb") as image_file:
@@ -449,7 +457,7 @@ def collect_file_content_by_extension(prompt_data_path, extensions):
449
457
  files = find_files_with_endings(prompt_data_path, [ext])
450
458
  for file_name in files:
451
459
  file_path = join(prompt_data_path, file_name)
452
- if ext == ".prompt_givens.md":
460
+ if ext in [".prompt_givens.md", ".prompt_global_givens.md"]:
453
461
  givens, image_data = load_givens(file_path)
454
462
  combined_content += givens
455
463
  image_data_list.extend(image_data)
@@ -494,7 +502,7 @@ def create_and_send_custom_prompt(classifier, parameter):
494
502
  prompt_data_path = f"ara/{sub_directory}/{parameter}.data/prompt.data"
495
503
  prompt_file_path_markdown = join(prompt_data_path, f"{classifier}.prompt.md")
496
504
 
497
- extensions = [".blueprint.md", ".rules.md", ".prompt_givens.md", ".intention.md", ".commands.md"]
505
+ extensions = [".blueprint.md", ".rules.md", ".prompt_givens.md", ".prompt_global_givens.md", ".intention.md", ".commands.md"]
498
506
  combined_content_markdown, image_data_list = collect_file_content_by_extension(prompt_data_path, extensions)
499
507
 
500
508
  with open(prompt_file_path_markdown, 'w', encoding='utf-8') as file:
@@ -561,4 +569,16 @@ def generate_config_prompt_givens_file(prompt_data_path, config_prompt_givens_na
561
569
 
562
570
  # Write the updated listing back to the file
563
571
  with open(config_prompt_givens_path, 'w', encoding='utf-8') as file:
564
- file.write("".join(updated_listing))
572
+ file.write("".join(updated_listing))
573
+
574
+ def generate_config_prompt_global_givens_file(prompt_data_path, config_prompt_givens_name, artefact_to_mark=None):
575
+ from ara_cli.global_file_lister import generate_global_markdown_listing
576
+ config_prompt_givens_path = os.path.join(prompt_data_path, config_prompt_givens_name)
577
+ config = ConfigManager.get_config()
578
+
579
+ if not hasattr(config, 'global_dirs') or not config.global_dirs:
580
+ return
581
+
582
+ dir_list = [path for d in config.global_dirs for path in d.values()]
583
+ print(f"used {dir_list} for global prompt givens file listing with absolute paths")
584
+ generate_global_markdown_listing(dir_list, config.ara_prompt_given_list_includes, config_prompt_givens_path)
ara_cli/tag_extractor.py CHANGED
@@ -1,6 +1,10 @@
1
1
  import os
2
2
  from ara_cli.list_filter import ListFilter, filter_list
3
- from ara_cli.artefact_lister import ArtefactLister
3
+ from ara_cli.artefact_models.artefact_data_retrieval import (
4
+ artefact_content_retrieval,
5
+ artefact_path_retrieval,
6
+ artefact_tags_retrieval,
7
+ )
4
8
 
5
9
 
6
10
  class TagExtractor:
@@ -10,7 +14,9 @@ class TagExtractor:
10
14
  def filter_column(self, tags_set, filtered_artefacts):
11
15
  status_tags = {"to-do", "in-progress", "review", "done", "closed"}
12
16
 
13
- artefacts_to_process = self._get_artefacts_without_status_tags(filtered_artefacts, status_tags)
17
+ artefacts_to_process = self._get_artefacts_without_status_tags(
18
+ filtered_artefacts, status_tags
19
+ )
14
20
  self._add_non_status_tags_to_set(tags_set, artefacts_to_process, status_tags)
15
21
 
16
22
  def _get_artefacts_without_status_tags(self, filtered_artefacts, status_tags):
@@ -28,7 +34,9 @@ class TagExtractor:
28
34
 
29
35
  def _add_non_status_tags_to_set(self, tags_set, artefacts, status_tags):
30
36
  for artefact in artefacts:
31
- tags = [tag for tag in (artefact.tags + [artefact.status]) if tag is not None]
37
+ tags = [
38
+ tag for tag in (artefact.tags + [artefact.status]) if tag is not None
39
+ ]
32
40
  for tag in tags:
33
41
  if self._is_skipped_tag(tag, status_tags):
34
42
  continue
@@ -36,23 +44,25 @@ class TagExtractor:
36
44
 
37
45
  def _is_skipped_tag(self, tag, status_tags):
38
46
  return (
39
- tag in status_tags
40
- or tag.startswith("priority_")
41
- or tag.startswith("user_")
47
+ tag in status_tags or tag.startswith("priority_") or tag.startswith("user_")
42
48
  )
43
49
 
44
50
  def add_to_tags_set(self, tags_set, filtered_artefacts):
45
51
  for artefact_list in filtered_artefacts.values():
46
52
  for artefact in artefact_list:
47
53
  user_tags = [f"user_{tag}" for tag in artefact.users]
48
- tags = [tag for tag in (artefact.tags + [artefact.status] + user_tags) if tag is not None]
54
+ tags = [
55
+ tag
56
+ for tag in (artefact.tags + [artefact.status] + user_tags)
57
+ if tag is not None
58
+ ]
49
59
  tags_set.update(tags)
50
60
 
51
61
  def extract_tags(
52
62
  self,
53
63
  navigate_to_target=False,
54
64
  filtered_extra_column=False,
55
- list_filter: ListFilter | None = None
65
+ list_filter: ListFilter | None = None,
56
66
  ):
57
67
  from ara_cli.template_manager import DirectoryNavigator
58
68
  from ara_cli.artefact_reader import ArtefactReader
@@ -66,9 +76,9 @@ class TagExtractor:
66
76
  filtered_artefacts = filter_list(
67
77
  list_to_filter=artefacts,
68
78
  list_filter=list_filter,
69
- content_retrieval_strategy=ArtefactLister.artefact_content_retrieval,
70
- file_path_retrieval=ArtefactLister.artefact_path_retrieval,
71
- tag_retrieval=ArtefactLister.artefact_tags_retrieval
79
+ content_retrieval_strategy=artefact_content_retrieval,
80
+ file_path_retrieval=artefact_path_retrieval,
81
+ tag_retrieval=artefact_tags_retrieval,
72
82
  )
73
83
 
74
84
  unique_tags = set()
@@ -129,6 +129,7 @@ class SpecificationBreakdownAspects:
129
129
  if aspect not in self.VALID_ASPECTS:
130
130
  raise ValueError(f"{aspect} does not exist. Please choose one of the {self.VALID_ASPECTS} list.")
131
131
 
132
+
132
133
  def create(self, artefact_name='artefact_name', classifier='classifier', aspect='specification_breakdown_aspect'):
133
134
  original_directory = os.getcwd()
134
135
  navigator = DirectoryNavigator()
@@ -139,8 +140,17 @@ class SpecificationBreakdownAspects:
139
140
  data_dir = self.file_manager.get_data_directory_path(artefact_name, classifier)
140
141
  self.file_manager.create_directory(artefact_file_path, data_dir)
141
142
  self.file_manager.copy_aspect_templates_to_directory(aspect, print_relative_to=original_directory)
143
+
142
144
  if (aspect == "step"):
143
- steps = self.file_manager.generate_behave_steps(artefact_name)
144
- self.file_manager.save_behave_steps_to_file(artefact_name, steps)
145
-
146
- os.chdir(original_directory)
145
+ # Instead of generating from behave command, read from the template file
146
+ template_file_path = f"{aspect}.md"
147
+ try:
148
+ with open(template_file_path, 'r', encoding='utf-8') as file:
149
+ steps_content = file.read()
150
+ self.file_manager.save_behave_steps_to_file(artefact_name, steps_content)
151
+ except FileNotFoundError:
152
+ # Fallback to the original behavior if template doesn't exist
153
+ steps = self.file_manager.generate_behave_steps(artefact_name)
154
+ self.file_manager.save_behave_steps_to_file(artefact_name, steps)
155
+
156
+ os.chdir(original_directory)
@@ -103,6 +103,7 @@ def handle_existing_file(file_path, tmp_file_path, generate_file_func, automatic
103
103
 
104
104
 
105
105
  def update_artefact_config_prompt_files(classifier, param, automatic_update=False):
106
+ from ara_cli.prompt_handler import generate_config_prompt_global_givens_file
106
107
  sub_directory = Classifier.get_sub_directory(classifier)
107
108
  artefact_data_path = os.path.join("ara", sub_directory, f"{param}.data")
108
109
  prompt_data_path = os.path.join(artefact_data_path, "prompt.data")
@@ -111,13 +112,18 @@ def update_artefact_config_prompt_files(classifier, param, automatic_update=Fals
111
112
 
112
113
  givens_file_name = "config.prompt_givens.md"
113
114
  givens_tmp_file_name = "config.prompt_givens_tmp.md"
115
+ global_givens_file_name = "config.prompt_global_givens.md"
116
+ global_givens_tmp_file_name = "config.prompt_global_givens_tmp.md"
114
117
  template_file_name = "config.prompt_templates.md"
115
118
  template_tmp_file_name = "config.prompt_templates_tmp.md"
116
119
 
117
120
  prompt_config_givens = os.path.join(prompt_data_path, givens_file_name)
118
121
  prompt_config_givens_tmp = os.path.join(prompt_data_path, givens_tmp_file_name)
122
+ prompt_config_global_givens = os.path.join(prompt_data_path, global_givens_file_name)
123
+ prompt_config_global_givens_tmp = os.path.join(prompt_data_path, global_givens_tmp_file_name)
119
124
  prompt_config_templates = os.path.join(prompt_data_path, template_file_name)
120
125
  prompt_config_templates_tmp = os.path.join(prompt_data_path, template_tmp_file_name)
121
126
 
122
127
  handle_existing_file(prompt_config_givens, prompt_config_givens_tmp, generate_config_prompt_givens_file, automatic_update)
123
- handle_existing_file(prompt_config_templates, prompt_config_templates_tmp, generate_config_prompt_template_file, automatic_update)
128
+ handle_existing_file(prompt_config_global_givens, prompt_config_global_givens_tmp, generate_config_prompt_global_givens_file, automatic_update)
129
+ handle_existing_file(prompt_config_templates, prompt_config_templates_tmp, generate_config_prompt_template_file, automatic_update)
ara_cli/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  # version.py
2
- __version__ = "0.1.9.93" # fith parameter like .0 for local install test purposes only. official numbers should be 4 digit numbers
2
+ __version__ = "0.1.9.95" # fith parameter like .0 for local install test purposes only. official numbers should be 4 digit numbers
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ara_cli
3
- Version: 0.1.9.93
3
+ Version: 0.1.9.95
4
4
  Summary: Powerful, open source command-line tool for managing, structuring and automating software development artifacts in line with Business-Driven Development (BDD) and AI-assisted processes
5
5
  Description-Content-Type: text/markdown
6
6
  Requires-Dist: litellm
@@ -13,6 +13,7 @@ Requires-Dist: json-repair
13
13
  Requires-Dist: argparse
14
14
  Requires-Dist: argcomplete
15
15
  Requires-Dist: cmd2>=2.5
16
+ Requires-Dist: charset-normalizer
16
17
  Requires-Dist: pydantic
17
18
  Requires-Dist: pydantic_ai
18
19
  Requires-Dist: python-docx
@@ -108,22 +109,22 @@ ara autofix
108
109
 
109
110
  ## Command Overview
110
111
 
111
- | Action | Description |
112
- |--------------------|-----------------------------------------------------------------------------|
113
- | create | Create a classified artefact with data directory |
114
- | delete | Delete an artefact and its data directory |
115
- | rename | Rename an artefact and its data directory |
116
- | list, list-tags | List artefacts, show tags, filter by content, extension, hierarchy etc. |
117
- | prompt, chat | Use AI-powered chat and prompt templates for artefact management |
118
- | template | Print artefact templates in the terminal |
119
- | fetch-templates | Download and manage reusable prompt templates |
120
- | read | Output artefact contents and their full contribution chain |
121
- | reconnect | Connect artefacts to parent artefacts |
122
- | read-status, set-status | Query and assign status to artefacts |
123
- | read-user, set-user | Query and assign responsible users |
124
- | classifier-directory | Show directory of artefact classifiers |
125
- | scan | Scan the ARA tree for incompatible or inconsistent artefacts |
126
- | autofix | Automatically correct artefact issues with LLM assistance |
112
+ | Action | Description |
113
+ | ----------------------- | ----------------------------------------------------------------------- |
114
+ | create | Create a classified artefact with data directory |
115
+ | delete | Delete an artefact and its data directory |
116
+ | rename | Rename an artefact and its data directory |
117
+ | list, list-tags | List artefacts, show tags, filter by content, extension, hierarchy etc. |
118
+ | prompt, chat | Use AI-powered chat and prompt templates for artefact management |
119
+ | template | Print artefact templates in the terminal |
120
+ | fetch-templates | Download and manage reusable prompt templates |
121
+ | read | Output artefact contents and their full contribution chain |
122
+ | reconnect | Connect artefacts to parent artefacts |
123
+ | read-status, set-status | Query and assign status to artefacts |
124
+ | read-user, set-user | Query and assign responsible users |
125
+ | classifier-directory | Show directory of artefact classifiers |
126
+ | scan | Scan the ARA tree for incompatible or inconsistent artefacts |
127
+ | autofix | Automatically correct artefact issues with LLM assistance |
127
128
 
128
129
  See `ara -h` for the complete list of commands and usage examples.
129
130