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.
- ara_cli/__init__.py +15 -1
- ara_cli/ara_command_action.py +23 -43
- ara_cli/ara_command_parser.py +16 -1
- ara_cli/ara_config.py +17 -2
- ara_cli/artefact_autofix.py +40 -21
- ara_cli/artefact_creator.py +3 -1
- ara_cli/artefact_lister.py +29 -55
- ara_cli/artefact_models/artefact_data_retrieval.py +23 -0
- ara_cli/artefact_renamer.py +6 -2
- ara_cli/chat.py +80 -34
- ara_cli/commands/extract_command.py +4 -3
- ara_cli/commands/read_command.py +104 -0
- ara_cli/file_loaders/document_readers.py +233 -0
- ara_cli/file_loaders/file_loaders.py +123 -0
- ara_cli/file_loaders/image_processor.py +89 -0
- ara_cli/file_loaders/markdown_reader.py +75 -0
- ara_cli/file_loaders/text_file_loader.py +9 -11
- ara_cli/global_file_lister.py +61 -0
- ara_cli/prompt_extractor.py +21 -6
- ara_cli/prompt_handler.py +24 -4
- ara_cli/tag_extractor.py +21 -11
- ara_cli/template_manager.py +14 -4
- ara_cli/update_config_prompt.py +7 -1
- ara_cli/version.py +1 -1
- {ara_cli-0.1.9.93.dist-info → ara_cli-0.1.9.95.dist-info}/METADATA +18 -17
- {ara_cli-0.1.9.93.dist-info → ara_cli-0.1.9.95.dist-info}/RECORD +35 -27
- tests/test_ara_config.py +28 -0
- tests/test_artefact_lister.py +52 -132
- tests/test_chat.py +28 -40
- tests/test_global_file_lister.py +131 -0
- tests/test_prompt_handler.py +26 -1
- tests/test_template_manager.py +5 -4
- {ara_cli-0.1.9.93.dist-info → ara_cli-0.1.9.95.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.93.dist-info → ara_cli-0.1.9.95.dist-info}/entry_points.txt +0 -0
- {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: 
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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")
|
ara_cli/prompt_extractor.py
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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 = "
|
|
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
|
|
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.
|
|
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(
|
|
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 = [
|
|
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 = [
|
|
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=
|
|
70
|
-
file_path_retrieval=
|
|
71
|
-
tag_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()
|
ara_cli/template_manager.py
CHANGED
|
@@ -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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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)
|
ara_cli/update_config_prompt.py
CHANGED
|
@@ -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(
|
|
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.
|
|
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.
|
|
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
|
|
112
|
-
|
|
113
|
-
| create
|
|
114
|
-
| delete
|
|
115
|
-
| rename
|
|
116
|
-
| list, list-tags
|
|
117
|
-
| prompt, chat
|
|
118
|
-
| template
|
|
119
|
-
| fetch-templates
|
|
120
|
-
| read
|
|
121
|
-
| reconnect
|
|
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
|
|
126
|
-
| autofix
|
|
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
|
|