ara-cli 0.1.9.94__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 +14 -0
- ara_cli/ara_config.py +17 -2
- ara_cli/artefact_autofix.py +40 -21
- ara_cli/artefact_creator.py +3 -1
- ara_cli/chat.py +75 -32
- 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_handler.py +24 -4
- 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.94.dist-info → ara_cli-0.1.9.95.dist-info}/METADATA +2 -1
- {ara_cli-0.1.9.94.dist-info → ara_cli-0.1.9.95.dist-info}/RECORD +25 -19
- tests/test_ara_config.py +28 -0
- tests/test_chat.py +17 -30
- 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.94.dist-info → ara_cli-0.1.9.95.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.94.dist-info → ara_cli-0.1.9.95.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.94.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_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/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
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
ara_cli/__init__.py,sha256=
|
|
1
|
+
ara_cli/__init__.py,sha256=CzGZUcf4WQP051xeNziNKQj8oNr-n_lZCW5_cR_WNVw,455
|
|
2
2
|
ara_cli/__main__.py,sha256=J5DCDLRZ6UcpYwM1-NkjaLo4PTetcSj2dB4HrrftkUw,2064
|
|
3
3
|
ara_cli/ara_command_action.py,sha256=uyMN05ZYffWqN9nwL53MmQ_yHpuxHVqZ_scAMEoD1jw,21516
|
|
4
4
|
ara_cli/ara_command_parser.py,sha256=A1lMc9Gc0EMJt-380PTcv3aKoxbXGfx5gGax-sZqV3I,21020
|
|
5
|
-
ara_cli/ara_config.py,sha256=
|
|
6
|
-
ara_cli/artefact_autofix.py,sha256=
|
|
7
|
-
ara_cli/artefact_creator.py,sha256=
|
|
5
|
+
ara_cli/ara_config.py,sha256=VJeage_v-446OtSXIfpazUbetpH7kGNv8Un1lKYx5ZE,9321
|
|
6
|
+
ara_cli/artefact_autofix.py,sha256=w6erUYrpPiwtHToiZEfjkDgDDPPSA9zaNc7w2NqrZ2M,20730
|
|
7
|
+
ara_cli/artefact_creator.py,sha256=wchIq1w636ui_kRCfNWPffqiIiXqSb49pgTpQj3KzA0,6132
|
|
8
8
|
ara_cli/artefact_deleter.py,sha256=Co4wwCH3yW8H9NrOq7_2p5571EeHr0TsfE-H8KqoOfY,1900
|
|
9
9
|
ara_cli/artefact_fuzzy_search.py,sha256=iBlDqjZf-_D3VUjFf7ZwkiQbpQDcwRndIU7aG_sRTgE,2668
|
|
10
10
|
ara_cli/artefact_link_updater.py,sha256=nKdxTpDKqWTOAMD8viKmUaklSFGWzJZ8S8E8xW_ADuM,3775
|
|
@@ -12,7 +12,7 @@ ara_cli/artefact_lister.py,sha256=M-ggazAgZ-OLeW9NB48r_sd6zPx0p4hEpeS63qHwI1A,41
|
|
|
12
12
|
ara_cli/artefact_reader.py,sha256=Pho0_Eqm7kD9CNbVMhKb6mkNM0I3iJiCJXbXmVp1DJU,7827
|
|
13
13
|
ara_cli/artefact_renamer.py,sha256=8S4QWD19_FGKsKlWojnu_RUOxx0u9rmLugydM4s4VDc,4219
|
|
14
14
|
ara_cli/artefact_scan.py,sha256=msPCm-vPWOAZ_e_z5GylXxq1MtNlmJ4zvKrsdOFCWF4,4813
|
|
15
|
-
ara_cli/chat.py,sha256=
|
|
15
|
+
ara_cli/chat.py,sha256=i2v-Ctem66sgO-HUV3kvMbk4wfotsc2oNet_i1-wfAI,39901
|
|
16
16
|
ara_cli/classifier.py,sha256=zWskj7rBYdqYBGjksBm46iTgVU5IIf2PZsJr4qeiwVU,1878
|
|
17
17
|
ara_cli/codefusionretriever.py,sha256=fCHgXdIBRzkVAnapX-KI2NQ44XbrrF4tEQmn5J6clUI,1980
|
|
18
18
|
ara_cli/codehierachieretriever.py,sha256=Xd3EgEWWhkSf1TmTWtf8X5_YvyE_4B66nRrqarwSiTU,1182
|
|
@@ -21,17 +21,18 @@ ara_cli/directory_navigator.py,sha256=6QbSAjJrJ5a6Lutol9J4HFgVDMiAQ672ny9TATrh04
|
|
|
21
21
|
ara_cli/file_classifier.py,sha256=A7wilPtIFm81iMgvqD0PjkOVL_QMUc9TB2w2Z9UcPcM,4001
|
|
22
22
|
ara_cli/file_lister.py,sha256=0C-j8IzajXo5qlvnuy5WFfe43ALwJ-0JFh2K6Xx2ccw,2332
|
|
23
23
|
ara_cli/filename_validator.py,sha256=Aw9PL8d5-Ymhp3EY6lDrUBk3cudaNqo1Uw5RzPpI1jA,118
|
|
24
|
+
ara_cli/global_file_lister.py,sha256=IIrtFoN5KYyJ3jVPanXZJ4UbYZfSdONRwxkZzvmq6-k,2806
|
|
24
25
|
ara_cli/list_filter.py,sha256=qKGwwQsrWe7L5FbdxEbBYD1bbbi8c-RMypjXqXvLbgs,5291
|
|
25
26
|
ara_cli/output_suppressor.py,sha256=nwiHaQLwabOjMoJOeUESBnZszGMxrQZfJ3N2OvahX7Y,389
|
|
26
27
|
ara_cli/prompt_chat.py,sha256=kd_OINDQFit6jN04bb7mzgY259JBbRaTaNp9F-webkc,1346
|
|
27
28
|
ara_cli/prompt_extractor.py,sha256=-_17aVYXYH6kPX5FOSb9T8lbEkKPXE6nlHWq1pvO_Og,8423
|
|
28
|
-
ara_cli/prompt_handler.py,sha256=
|
|
29
|
+
ara_cli/prompt_handler.py,sha256=8a9fcMwE_C6ntbw7UeroNJeU5LxrxEppiUtvYNUTB2U,23292
|
|
29
30
|
ara_cli/prompt_rag.py,sha256=ydlhe4CUqz0jdzlY7jBbpKaf_5fjMrAZKnriKea3ZAg,7485
|
|
30
31
|
ara_cli/run_file_lister.py,sha256=XbrrDTJXp1LFGx9Lv91SNsEHZPP-PyEMBF_P4btjbDA,2360
|
|
31
32
|
ara_cli/tag_extractor.py,sha256=k2yRl7dAMZ4YTARzUke4wgY0oEIOmWkOHGet7nXB6uw,3317
|
|
32
|
-
ara_cli/template_manager.py,sha256=
|
|
33
|
-
ara_cli/update_config_prompt.py,sha256=
|
|
34
|
-
ara_cli/version.py,sha256=
|
|
33
|
+
ara_cli/template_manager.py,sha256=l2c785YHB7m0e2TjE0CX-nwXrS4v3EiT9qrS5KuatAc,7105
|
|
34
|
+
ara_cli/update_config_prompt.py,sha256=moqj2Kha7S7fEGzTReU0v2y8UjXC8QfnoiieOQr35C4,5157
|
|
35
|
+
ara_cli/version.py,sha256=rYDZfGIzWScBRjMjgeqPAsdSGThurJ7PgpedF2Ekr2U,146
|
|
35
36
|
ara_cli/artefact_models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
37
|
ara_cli/artefact_models/artefact_data_retrieval.py,sha256=CooXOJBYWSyiViN2xkC8baS8OUaslry3YGVVUeDxRAU,527
|
|
37
38
|
ara_cli/artefact_models/artefact_load.py,sha256=IXzWxP-Q_j_oDGMno0m-OuXCQ7Vd5c_NctshGr4ROBw,621
|
|
@@ -59,8 +60,12 @@ ara_cli/file_loaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
|
|
|
59
60
|
ara_cli/file_loaders/binary_file_loader.py,sha256=1HHH1Nk4lEM83CTnf4z9wYz6rMLgpxydFoRcSgkBHmQ,940
|
|
60
61
|
ara_cli/file_loaders/document_file_loader.py,sha256=VxGFChYyM9K-e6eOCK3yk5jQuEXgz01Mh_NoA6CA_RM,1017
|
|
61
62
|
ara_cli/file_loaders/document_reader.py,sha256=SD9_5-XJ6homKUes6o8GWcG--X63UslfAosPbrJZQvo,7721
|
|
63
|
+
ara_cli/file_loaders/document_readers.py,sha256=aG7xrUJwLxWpuFTYlvxzDqoS00Idpvwzt865L0OuQcA,8124
|
|
62
64
|
ara_cli/file_loaders/file_loader.py,sha256=bc1BrMG4pEtwsZLm3Ct53YsMPgnbSaEvZEd8isRDYRY,1711
|
|
63
|
-
ara_cli/file_loaders/
|
|
65
|
+
ara_cli/file_loaders/file_loaders.py,sha256=9QqArTRDmcUUar58JEr-qnpiAtH9ySP-MV9bvooQNpI,4290
|
|
66
|
+
ara_cli/file_loaders/image_processor.py,sha256=laPThh-i0-obYyS_linQTMcTUwuxMxrSjedGRYb8cIA,3462
|
|
67
|
+
ara_cli/file_loaders/markdown_reader.py,sha256=R-hvvc9Sj9pWwENqJ0j6wrW0eN1tUqEKWcK2YUFsvsU,2542
|
|
68
|
+
ara_cli/file_loaders/text_file_loader.py,sha256=62U59RkWgAML0U0P-sUeFsK51mJM8Fu54gGlnmMwYpY,6804
|
|
64
69
|
ara_cli/templates/agile.artefacts,sha256=nTA8dp98HWKAD-0qhmNpVYIfkVGoJshZqMJGnphiOsE,7932
|
|
65
70
|
ara_cli/templates/template.businessgoal.prompt_log.md,sha256=xF6bkgj_GqAAqHxJWJiQNt11mEuSGemIqoZ2wOo6dI0,214
|
|
66
71
|
ara_cli/templates/template.capability.prompt_log.md,sha256=eO8EzrHgb2vYJ-DP1jGzAfDlMo8nY75hZDfhh0s40uQ,208
|
|
@@ -136,7 +141,7 @@ ara_cli/templates/specification_breakdown_files/template.technology.exploration.
|
|
|
136
141
|
ara_cli/templates/specification_breakdown_files/template.technology.md,sha256=bySiksz-8xtq0Nnj4svqe2MgUftWrVkbK9AcrDUE3KY,952
|
|
137
142
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
138
143
|
tests/test_ara_command_action.py,sha256=JTLqXM9BSMlU33OQgrk_sZnoowFJZKZAx8q-st-wa34,25821
|
|
139
|
-
tests/test_ara_config.py,sha256=
|
|
144
|
+
tests/test_ara_config.py,sha256=RbVhS0SS1lr_SVopEMT1Fake5a-4rWN8MprgJtgI-FA,15883
|
|
140
145
|
tests/test_artefact_autofix.py,sha256=pApZ-N0dW8Ujt-cNLbgvd4bhiIIK8oXb-saLf6QlA-8,25022
|
|
141
146
|
tests/test_artefact_fuzzy_search.py,sha256=5Sh3_l9QK8-WHn6JpGPU1b6h4QEnl2JoMq1Tdp2cj1U,1261
|
|
142
147
|
tests/test_artefact_link_updater.py,sha256=biqbEp2jCOz8giv72hu2P2hDfeJfJ9OrVGdAv5d9cK4,2191
|
|
@@ -144,19 +149,20 @@ tests/test_artefact_lister.py,sha256=35R13UU-YsX1HOsEN8M2-vIiCUA9RSBm6SwestDaFhE
|
|
|
144
149
|
tests/test_artefact_reader.py,sha256=660K-d8ed-j8hulsUB_7baPD2-hhbg9TffUR5yVc4Uo,927
|
|
145
150
|
tests/test_artefact_renamer.py,sha256=lSnKCCfoFGgKhTdDZrEaeBq1xJAak1QoqH5aSeOe9Ro,3494
|
|
146
151
|
tests/test_artefact_scan.py,sha256=uNWgrt7ieZ4ogKACsPqzAsh59JF2BhTKSag31hpVrTQ,16887
|
|
147
|
-
tests/test_chat.py,sha256=
|
|
152
|
+
tests/test_chat.py,sha256=sf4mXmOjXZeaYPNSYXSyfz0b5pZA6Mq7_R3gWjQaJw4,56152
|
|
148
153
|
tests/test_classifier.py,sha256=grYGPksydNdPsaEBQxYHZTuTdcJWz7VQtikCKA6BNaQ,1920
|
|
149
154
|
tests/test_directory_navigator.py,sha256=7G0MVrBbtBvbrFUpL0zb_9EkEWi1dulWuHsrQxMJxDY,140
|
|
150
155
|
tests/test_file_classifier.py,sha256=kLWPiePu3F5mkVuI_lK_2QlLh2kXD_Mt2K8KZZ1fAnA,10940
|
|
151
156
|
tests/test_file_creator.py,sha256=D3G7MbgE0m8JmZihxnTryxLco6iZdbV--2CGc0L20FM,2109
|
|
152
157
|
tests/test_file_lister.py,sha256=Q9HwhKKx540EPzTmfzOCnvtAgON0aMmpJE2eOe1J3EA,4324
|
|
158
|
+
tests/test_global_file_lister.py,sha256=ycvf2YL8q5QSEMwcnQfUdoWnQQ8xTSyEtccAeXwl6QU,5487
|
|
153
159
|
tests/test_list_filter.py,sha256=fJA3d_SdaOAUkE7jn68MOVS0THXGghy1fye_64Zvo1U,7964
|
|
154
|
-
tests/test_prompt_handler.py,sha256=
|
|
160
|
+
tests/test_prompt_handler.py,sha256=kW8FU09ho4I5qC-f4G9r4ZgI-NlqdOkTmAazG7FaTrw,32299
|
|
155
161
|
tests/test_tag_extractor.py,sha256=nSiAYlTKZ7TLAOtcJpwK5zTWHhFYU0tI5xKnivLc1dU,2712
|
|
156
|
-
tests/test_template_manager.py,sha256=
|
|
162
|
+
tests/test_template_manager.py,sha256=qliEeYgAEakn8JIqIHa8u0Ht6DY4L3T6DcHBXkjzR4I,4167
|
|
157
163
|
tests/test_update_config_prompt.py,sha256=xsqj1WTn4BsG5Q2t-sNPfu7EoMURFcS-hfb5VSXUnJc,6765
|
|
158
|
-
ara_cli-0.1.9.
|
|
159
|
-
ara_cli-0.1.9.
|
|
160
|
-
ara_cli-0.1.9.
|
|
161
|
-
ara_cli-0.1.9.
|
|
162
|
-
ara_cli-0.1.9.
|
|
164
|
+
ara_cli-0.1.9.95.dist-info/METADATA,sha256=ZVJWS_s9Xmdeftrsy6337QTfO_BVESYERmL5r_2tT3s,6789
|
|
165
|
+
ara_cli-0.1.9.95.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
166
|
+
ara_cli-0.1.9.95.dist-info/entry_points.txt,sha256=v4h7MzysTgSIDYfEo3oj4Kz_8lzsRa3hq-KJHEcLVX8,45
|
|
167
|
+
ara_cli-0.1.9.95.dist-info/top_level.txt,sha256=WM4cLHT5DYUaWzLtRj-gu3yVNFpGQ6lLRI3FMmC-38I,14
|
|
168
|
+
ara_cli-0.1.9.95.dist-info/RECORD,,
|
tests/test_ara_config.py
CHANGED
|
@@ -115,6 +115,34 @@ class TestARAconfig:
|
|
|
115
115
|
assert config.glossary_dir == "./glossary"
|
|
116
116
|
assert "Warning: Value for 'glossary_dir' is missing or empty. Using default." in mock_stdout.getvalue()
|
|
117
117
|
|
|
118
|
+
@patch('sys.stdout', new_callable=StringIO)
|
|
119
|
+
def test_validator_with_empty_llm_config(self, mock_stdout):
|
|
120
|
+
"""Tests validator when llm_config is empty, setting default and extraction to None."""
|
|
121
|
+
config = ARAconfig(llm_config={})
|
|
122
|
+
assert config.llm_config == {}
|
|
123
|
+
assert config.default_llm is None
|
|
124
|
+
assert config.extraction_llm is None
|
|
125
|
+
assert "Warning: 'llm_config' is empty" in mock_stdout.getvalue()
|
|
126
|
+
|
|
127
|
+
@patch('sys.stdout', new_callable=StringIO)
|
|
128
|
+
def test_validator_with_invalid_default_llm(self, mock_stdout):
|
|
129
|
+
"""Tests that an invalid default_llm is reverted to the first available model."""
|
|
130
|
+
config = ARAconfig(default_llm="non_existent_model")
|
|
131
|
+
first_llm = next(iter(config.llm_config))
|
|
132
|
+
assert config.default_llm == first_llm
|
|
133
|
+
output = mock_stdout.getvalue()
|
|
134
|
+
assert "Warning: The configured 'default_llm' ('non_existent_model') does not exist" in output
|
|
135
|
+
assert f"-> Reverting to the first available model: '{first_llm}'" in output
|
|
136
|
+
|
|
137
|
+
@patch('sys.stdout', new_callable=StringIO)
|
|
138
|
+
def test_validator_with_invalid_extraction_llm(self, mock_stdout):
|
|
139
|
+
"""Tests that an invalid extraction_llm is reverted to the default_llm."""
|
|
140
|
+
config = ARAconfig(default_llm="gpt-4o", extraction_llm="non_existent_model")
|
|
141
|
+
assert config.extraction_llm == "gpt-4o"
|
|
142
|
+
output = mock_stdout.getvalue()
|
|
143
|
+
assert "Warning: The configured 'extraction_llm' ('non_existent_model') does not exist" in output
|
|
144
|
+
assert "-> Reverting to the 'default_llm' value: 'gpt-4o'" in output
|
|
145
|
+
|
|
118
146
|
# --- Test Helper Functions ---
|
|
119
147
|
|
|
120
148
|
class TestEnsureDirectoryExists:
|
tests/test_chat.py
CHANGED
|
@@ -10,6 +10,7 @@ from types import SimpleNamespace
|
|
|
10
10
|
from ara_cli.chat import Chat
|
|
11
11
|
from ara_cli.template_manager import TemplatePathManager
|
|
12
12
|
from ara_cli.ara_config import ConfigManager
|
|
13
|
+
from ara_cli.file_loaders.text_file_loader import TextFileLoader
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def get_default_config():
|
|
@@ -471,42 +472,28 @@ def test_determine_file_path(temp_chat_file):
|
|
|
471
472
|
mock_exists.reset_mock()
|
|
472
473
|
|
|
473
474
|
|
|
474
|
-
@pytest.mark.parametrize("file_name,
|
|
475
|
-
("document.txt", "Hello World
|
|
476
|
-
("
|
|
477
|
-
("document.txt", "Hello World", "", "", "---", "---\nHello World\n---\n"),
|
|
478
|
-
("document.txt", "Hello World", "Prefix", "Suffix", "---", "Prefix---\nHello World\n---Suffix\n"),
|
|
475
|
+
@pytest.mark.parametrize("file_name, expected_content", [
|
|
476
|
+
("document.txt", "Hello World\n"),
|
|
477
|
+
("another_document.txt", "Another World\n"),
|
|
479
478
|
])
|
|
480
|
-
def test_load_text_file(temp_chat_file, file_name,
|
|
481
|
-
|
|
479
|
+
def test_load_text_file(temp_chat_file, file_name, expected_content):
|
|
480
|
+
# Create a mock config
|
|
481
|
+
mock_config = MagicMock()
|
|
482
|
+
|
|
483
|
+
# Patch the get_config method to return the mock config
|
|
482
484
|
with patch('ara_cli.prompt_handler.ConfigManager.get_config', return_value=mock_config):
|
|
483
485
|
chat = Chat(temp_chat_file.name, reset=False)
|
|
484
486
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
assert result is True
|
|
490
|
-
|
|
491
|
-
mock_file.assert_any_call(file_name, 'r', encoding='utf-8', errors="replace")
|
|
492
|
-
|
|
493
|
-
mock_file.assert_any_call(chat.chat_name, 'a', encoding='utf-8')
|
|
494
|
-
|
|
495
|
-
mock_file().write.assert_called_once_with(expected_content)
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
# def test_load_text_file_file_not_found(temp_chat_file):
|
|
499
|
-
# mock_config = get_default_config()
|
|
500
|
-
# with patch('ara_cli.prompt_handler.ConfigManager.get_config', return_value=mock_config):
|
|
501
|
-
# chat = Chat(temp_chat_file.name, reset=False)
|
|
502
|
-
|
|
503
|
-
# with patch.object(chat, 'determine_file_path', return_value=None):
|
|
504
|
-
# with patch("builtins.open", mock_open()) as mock_file:
|
|
505
|
-
# result = chat.load_text_file("nonexistent.txt")
|
|
487
|
+
# Mock the TextFileLoader
|
|
488
|
+
with patch.object(TextFileLoader, 'load', return_value=True) as mock_load:
|
|
489
|
+
# Call the load_text_file method
|
|
490
|
+
result = chat.load_text_file(file_name)
|
|
506
491
|
|
|
507
|
-
#
|
|
492
|
+
# Check that the load method was called once
|
|
493
|
+
mock_load.assert_called_once()
|
|
508
494
|
|
|
509
|
-
#
|
|
495
|
+
# Check that the result is True
|
|
496
|
+
assert result is True
|
|
510
497
|
|
|
511
498
|
|
|
512
499
|
@pytest.mark.parametrize(
|