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.

@@ -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")
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)
@@ -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.94" # 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.94
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=oieeo07y5FLq0tmPzWZdp65j6tN8NOucSjSzrJw8qrQ,203
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=5uBo_flNgZSk7B9lmyfvzWyxfIQzb13LbieCpJfdZJI,8765
6
- ara_cli/artefact_autofix.py,sha256=WVTiIR-jo4YKmmz4eS3qTFvl45W1YKwAk1XSuz9QX10,20015
7
- ara_cli/artefact_creator.py,sha256=0Ory6cB-Ahkw-BDNb8QHnTbp_OHGABdkb9bhwcEdcIc,6063
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=t17TCmx9xvKj5wrnUOJBqjWoSjEVOFrVd8RivChVX50,37980
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=6yfiMFNHGHANREAsjT8dv9jKxBKeazPkF7xQQI4l6vQ,22312
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=YwrN6AYPpl6ZrW8BVQpVXx8yTRf-oNpJUIKeg4NAggs,6606
33
- ara_cli/update_config_prompt.py,sha256=Oy9vNTw6UhDohyTEfSKkqE5ifEMPlmWNYkKHgUrK_pY,4607
34
- ara_cli/version.py,sha256=MWb524qaDHLrSJsHhOq7o0m78CPLFil9qeyel6kPW-A,146
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/text_file_loader.py,sha256=x_ZqnOUb2glpdxziceWCBX8oEBk7JZhAluLRRvBjuQs,6626
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=H5GwDbab0GMSa6IbHdruzmbsHy5Ia0xX0uteJdfZ9Rg,14277
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=cCNIuYiSGoNtjgjyFiTvkMHJgCmMNXQhpawNc23-fmM,57037
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=3-lYBvyHLQgD29MODkXB3YylUWXmRCYdAwrQrtlW8WU,30871
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=q-LMHRG4rHkD6ON6YW4cpZxUx9hul6Or8wVVRC2kb-8,4099
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.94.dist-info/METADATA,sha256=CPz0aDXI8_4hJtbN9tm56-aNUNEum3G01dD9DMIwGdg,6755
159
- ara_cli-0.1.9.94.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
160
- ara_cli-0.1.9.94.dist-info/entry_points.txt,sha256=v4h7MzysTgSIDYfEo3oj4Kz_8lzsRa3hq-KJHEcLVX8,45
161
- ara_cli-0.1.9.94.dist-info/top_level.txt,sha256=WM4cLHT5DYUaWzLtRj-gu3yVNFpGQ6lLRI3FMmC-38I,14
162
- ara_cli-0.1.9.94.dist-info/RECORD,,
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, file_content, prefix, suffix, block_delimiter, expected_content", [
475
- ("document.txt", "Hello World", "", "", "", "Hello World\n"),
476
- ("document.txt", "Hello World", "Prefix-", "-Suffix", "", "Prefix-Hello World-Suffix\n"),
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, file_content, prefix, suffix, block_delimiter, expected_content):
481
- mock_config = get_default_config()
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
- with patch.object(chat, 'determine_file_path', return_value=file_name):
486
- with patch("builtins.open", mock_open(read_data=file_content)) as mock_file:
487
- result = chat.load_text_file(file_name, prefix, suffix, block_delimiter)
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
- # assert result is False
492
+ # Check that the load method was called once
493
+ mock_load.assert_called_once()
508
494
 
509
- # mock_file.assert_not_called()
495
+ # Check that the result is True
496
+ assert result is True
510
497
 
511
498
 
512
499
  @pytest.mark.parametrize(