ara-cli 0.1.9.73__py3-none-any.whl → 0.1.9.75__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/ara_command_action.py +15 -15
- ara_cli/ara_command_parser.py +2 -1
- ara_cli/ara_config.py +181 -73
- ara_cli/artefact_autofix.py +130 -68
- ara_cli/artefact_creator.py +1 -1
- ara_cli/artefact_models/artefact_model.py +26 -7
- ara_cli/artefact_models/artefact_templates.py +47 -31
- ara_cli/artefact_models/businessgoal_artefact_model.py +23 -25
- ara_cli/artefact_models/epic_artefact_model.py +23 -24
- ara_cli/artefact_models/feature_artefact_model.py +76 -46
- ara_cli/artefact_models/keyfeature_artefact_model.py +21 -24
- ara_cli/artefact_models/task_artefact_model.py +73 -13
- ara_cli/artefact_models/userstory_artefact_model.py +22 -24
- ara_cli/artefact_models/vision_artefact_model.py +23 -42
- ara_cli/artefact_scan.py +55 -17
- ara_cli/chat.py +23 -5
- ara_cli/prompt_handler.py +4 -4
- ara_cli/tag_extractor.py +43 -28
- ara_cli/template_manager.py +3 -8
- ara_cli/version.py +1 -1
- {ara_cli-0.1.9.73.dist-info → ara_cli-0.1.9.75.dist-info}/METADATA +1 -1
- {ara_cli-0.1.9.73.dist-info → ara_cli-0.1.9.75.dist-info}/RECORD +29 -39
- tests/test_ara_config.py +420 -36
- tests/test_artefact_autofix.py +289 -25
- tests/test_artefact_scan.py +296 -35
- tests/test_chat.py +35 -15
- ara_cli/templates/template.businessgoal +0 -10
- ara_cli/templates/template.capability +0 -10
- ara_cli/templates/template.epic +0 -15
- ara_cli/templates/template.example +0 -6
- ara_cli/templates/template.feature +0 -26
- ara_cli/templates/template.issue +0 -14
- ara_cli/templates/template.keyfeature +0 -15
- ara_cli/templates/template.task +0 -6
- ara_cli/templates/template.userstory +0 -17
- ara_cli/templates/template.vision +0 -14
- {ara_cli-0.1.9.73.dist-info → ara_cli-0.1.9.75.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.73.dist-info → ara_cli-0.1.9.75.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.73.dist-info → ara_cli-0.1.9.75.dist-info}/top_level.txt +0 -0
ara_cli/ara_command_action.py
CHANGED
|
@@ -347,6 +347,7 @@ def reconnect_action(args):
|
|
|
347
347
|
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
348
348
|
from ara_cli.artefact_models.artefact_model import Contribution
|
|
349
349
|
from ara_cli.artefact_reader import ArtefactReader
|
|
350
|
+
from ara_cli.file_classifier import FileClassifier
|
|
350
351
|
from ara_cli.artefact_fuzzy_search import find_closest_rule
|
|
351
352
|
|
|
352
353
|
classifier = args.classifier
|
|
@@ -359,31 +360,29 @@ def reconnect_action(args):
|
|
|
359
360
|
|
|
360
361
|
feedback_message = f"Updated contribution of {classifier} '{artefact_name}' to {parent_classifier} '{parent_name}'"
|
|
361
362
|
|
|
362
|
-
|
|
363
|
+
file_classifier = FileClassifier(os)
|
|
364
|
+
classified_file_info = file_classifier.classify_files()
|
|
365
|
+
|
|
366
|
+
artefact = ArtefactReader.read_artefact(
|
|
363
367
|
artefact_name=artefact_name,
|
|
364
|
-
classifier=classifier
|
|
368
|
+
classifier=classifier,
|
|
369
|
+
classified_file_info=classified_file_info
|
|
365
370
|
)
|
|
366
|
-
|
|
371
|
+
|
|
372
|
+
if not artefact:
|
|
367
373
|
print(read_error_message)
|
|
368
374
|
return
|
|
369
375
|
|
|
370
|
-
|
|
376
|
+
parent = ArtefactReader.read_artefact(
|
|
371
377
|
artefact_name=parent_name,
|
|
372
|
-
classifier=parent_classifier
|
|
378
|
+
classifier=parent_classifier,
|
|
379
|
+
classified_file_info=classified_file_info
|
|
373
380
|
)
|
|
374
|
-
|
|
381
|
+
|
|
382
|
+
if not parent:
|
|
375
383
|
print(read_error_message)
|
|
376
384
|
return
|
|
377
385
|
|
|
378
|
-
artefact = artefact_from_content(
|
|
379
|
-
content=content,
|
|
380
|
-
)
|
|
381
|
-
artefact._file_path = artefact_info["file_path"]
|
|
382
|
-
|
|
383
|
-
parent = artefact_from_content(
|
|
384
|
-
content=parent_content
|
|
385
|
-
)
|
|
386
|
-
|
|
387
386
|
contribution = Contribution(
|
|
388
387
|
artefact_name=parent.title,
|
|
389
388
|
classifier=parent.artefact_type
|
|
@@ -603,6 +602,7 @@ def autofix_action(args):
|
|
|
603
602
|
file_path,
|
|
604
603
|
classifier,
|
|
605
604
|
reason,
|
|
605
|
+
single_pass=args.single_pass,
|
|
606
606
|
deterministic=run_deterministic,
|
|
607
607
|
non_deterministic=run_non_deterministic,
|
|
608
608
|
classified_artefact_info=classified_artefact_info
|
ara_cli/ara_command_parser.py
CHANGED
|
@@ -229,7 +229,8 @@ def scan_parser(subparsers):
|
|
|
229
229
|
|
|
230
230
|
|
|
231
231
|
def autofix_parser(subparsers):
|
|
232
|
-
autofix_parser = subparsers.add_parser("autofix", help="Fix ARA tree with llm models for scanned artefacts with ara scan command.")
|
|
232
|
+
autofix_parser = subparsers.add_parser("autofix", help="Fix ARA tree with llm models for scanned artefacts with ara scan command. By default three attemps for every file.")
|
|
233
|
+
autofix_parser.add_argument("--single-pass", action="store_true", help="Run the autofix once for every scaned file.")
|
|
233
234
|
determinism_group = autofix_parser.add_mutually_exclusive_group()
|
|
234
235
|
determinism_group.add_argument("--deterministic", "-d", action="store_true", help="Run only deterministic fixes e.g Title-FileName Mismatch fix")
|
|
235
236
|
determinism_group.add_argument("--non-deterministic", "-nd", action="store_true", help="Run only non-deterministic fixes")
|
ara_cli/ara_config.py
CHANGED
|
@@ -1,33 +1,43 @@
|
|
|
1
|
-
from typing import List, Dict, Optional
|
|
2
|
-
from pydantic import BaseModel
|
|
1
|
+
from typing import List, Dict, Optional, Any
|
|
2
|
+
from pydantic import BaseModel, ValidationError, Field, field_validator, model_validator
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
from os.path import exists, dirname
|
|
6
6
|
from os import makedirs
|
|
7
7
|
from functools import lru_cache
|
|
8
|
-
|
|
8
|
+
import sys
|
|
9
9
|
|
|
10
10
|
DEFAULT_CONFIG_LOCATION = "./ara/.araconfig/ara_config.json"
|
|
11
11
|
|
|
12
|
-
|
|
13
12
|
class LLMConfigItem(BaseModel):
|
|
14
13
|
provider: str
|
|
15
14
|
model: str
|
|
16
|
-
temperature: float
|
|
15
|
+
temperature: float = Field(ge=0.0, le=1.0)
|
|
17
16
|
max_tokens: Optional[int] = None
|
|
17
|
+
|
|
18
|
+
@field_validator('temperature')
|
|
19
|
+
@classmethod
|
|
20
|
+
def validate_temperature(cls, v: float, info) -> float:
|
|
21
|
+
if not 0.0 <= v <= 1.0:
|
|
22
|
+
print(f"Warning: Temperature is outside the 0.0 to 1.0 range")
|
|
23
|
+
# Return a valid default
|
|
24
|
+
return 0.8
|
|
25
|
+
return v
|
|
18
26
|
|
|
27
|
+
class ExtCodeDirItem(BaseModel):
|
|
28
|
+
source_dir: str
|
|
19
29
|
|
|
20
30
|
class ARAconfig(BaseModel):
|
|
21
|
-
ext_code_dirs: List[
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
]
|
|
31
|
+
ext_code_dirs: List[ExtCodeDirItem] = Field(default_factory=lambda: [
|
|
32
|
+
ExtCodeDirItem(source_dir="./src"),
|
|
33
|
+
ExtCodeDirItem(source_dir="./tests")
|
|
34
|
+
])
|
|
25
35
|
glossary_dir: str = "./glossary"
|
|
26
36
|
doc_dir: str = "./docs"
|
|
27
37
|
local_prompt_templates_dir: str = "./ara/.araconfig"
|
|
28
38
|
custom_prompt_templates_subdir: Optional[str] = "custom-prompt-modules"
|
|
29
39
|
local_ara_templates_dir: str = "./ara/.araconfig/templates/"
|
|
30
|
-
ara_prompt_given_list_includes: List[str] = [
|
|
40
|
+
ara_prompt_given_list_includes: List[str] = Field(default_factory=lambda: [
|
|
31
41
|
"*.businessgoal",
|
|
32
42
|
"*.vision",
|
|
33
43
|
"*.capability",
|
|
@@ -42,53 +52,76 @@ class ARAconfig(BaseModel):
|
|
|
42
52
|
"*.png",
|
|
43
53
|
"*.jpg",
|
|
44
54
|
"*.jpeg",
|
|
45
|
-
]
|
|
46
|
-
llm_config: Dict[str, LLMConfigItem] = {
|
|
47
|
-
"gpt-4o":
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"gpt-4.1":
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"o3-mini":
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"opus-4":
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"sonnet-4":
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"together-ai-llama-2":
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"groq-llama-3":
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
55
|
+
])
|
|
56
|
+
llm_config: Dict[str, LLMConfigItem] = Field(default_factory=lambda: {
|
|
57
|
+
"gpt-4o": LLMConfigItem(
|
|
58
|
+
provider="openai",
|
|
59
|
+
model="openai/gpt-4o",
|
|
60
|
+
temperature=0.8,
|
|
61
|
+
max_tokens=16384
|
|
62
|
+
),
|
|
63
|
+
"gpt-4.1": LLMConfigItem(
|
|
64
|
+
provider="openai",
|
|
65
|
+
model="openai/gpt-4.1",
|
|
66
|
+
temperature=0.8,
|
|
67
|
+
max_tokens=1024
|
|
68
|
+
),
|
|
69
|
+
"o3-mini": LLMConfigItem(
|
|
70
|
+
provider="openai",
|
|
71
|
+
model="openai/o3-mini",
|
|
72
|
+
temperature=1.0,
|
|
73
|
+
max_tokens=1024
|
|
74
|
+
),
|
|
75
|
+
"opus-4": LLMConfigItem(
|
|
76
|
+
provider="anthropic",
|
|
77
|
+
model="anthropic/claude-opus-4-20250514",
|
|
78
|
+
temperature=0.8,
|
|
79
|
+
max_tokens=32000
|
|
80
|
+
),
|
|
81
|
+
"sonnet-4": LLMConfigItem(
|
|
82
|
+
provider="anthropic",
|
|
83
|
+
model="anthropic/claude-sonnet-4-20250514",
|
|
84
|
+
temperature=0.8,
|
|
85
|
+
max_tokens=1024
|
|
86
|
+
),
|
|
87
|
+
"together-ai-llama-2": LLMConfigItem(
|
|
88
|
+
provider="together_ai",
|
|
89
|
+
model="together_ai/togethercomputer/llama-2-70b",
|
|
90
|
+
temperature=0.8,
|
|
91
|
+
max_tokens=1024
|
|
92
|
+
),
|
|
93
|
+
"groq-llama-3": LLMConfigItem(
|
|
94
|
+
provider="groq",
|
|
95
|
+
model="groq/llama3-70b-8192",
|
|
96
|
+
temperature=0.8,
|
|
97
|
+
max_tokens=1024
|
|
98
|
+
)
|
|
99
|
+
})
|
|
90
100
|
default_llm: Optional[str] = "gpt-4o"
|
|
101
|
+
|
|
102
|
+
model_config = {
|
|
103
|
+
"extra": "forbid" # This will help identify unrecognized keys
|
|
104
|
+
}
|
|
91
105
|
|
|
106
|
+
@model_validator(mode='after')
|
|
107
|
+
def check_critical_fields(self) -> 'ARAconfig':
|
|
108
|
+
"""Check for empty critical fields and use defaults if needed"""
|
|
109
|
+
critical_fields = {
|
|
110
|
+
'ext_code_dirs': [ExtCodeDirItem(source_dir="./src"), ExtCodeDirItem(source_dir="./tests")],
|
|
111
|
+
'local_ara_templates_dir': "./ara/.araconfig/templates/",
|
|
112
|
+
'local_prompt_templates_dir': "./ara/.araconfig",
|
|
113
|
+
'glossary_dir': "./glossary"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for field, default_value in critical_fields.items():
|
|
117
|
+
current_value = getattr(self, field)
|
|
118
|
+
if (not current_value or
|
|
119
|
+
(isinstance(current_value, list) and len(current_value) == 0) or
|
|
120
|
+
(isinstance(current_value, str) and current_value.strip() == "")):
|
|
121
|
+
print(f"Warning: Value for '{field}' is missing or empty.")
|
|
122
|
+
setattr(self, field, default_value)
|
|
123
|
+
|
|
124
|
+
return self
|
|
92
125
|
|
|
93
126
|
# Function to ensure the necessary directories exist
|
|
94
127
|
@lru_cache(maxsize=None)
|
|
@@ -98,37 +131,106 @@ def ensure_directory_exists(directory: str):
|
|
|
98
131
|
print(f"New directory created at {directory}")
|
|
99
132
|
return directory
|
|
100
133
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
134
|
+
def handle_unrecognized_keys(data: dict, known_fields: set) -> dict:
|
|
135
|
+
"""Remove unrecognized keys and warn the user"""
|
|
136
|
+
cleaned_data = {}
|
|
137
|
+
for key, value in data.items():
|
|
138
|
+
if key not in known_fields:
|
|
139
|
+
print(f"Warning: {key} is not recognized as a valid configuration option.")
|
|
140
|
+
else:
|
|
141
|
+
cleaned_data[key] = value
|
|
142
|
+
return cleaned_data
|
|
143
|
+
|
|
144
|
+
def fix_llm_temperatures(data: dict) -> dict:
|
|
145
|
+
"""Fix invalid temperatures in LLM configurations"""
|
|
146
|
+
if 'llm_config' in data:
|
|
147
|
+
for model_key, model_config in data['llm_config'].items():
|
|
148
|
+
if isinstance(model_config, dict) and 'temperature' in model_config:
|
|
149
|
+
temp = model_config['temperature']
|
|
150
|
+
if not 0.0 <= temp <= 1.0:
|
|
151
|
+
print(f"Warning: Temperature for model '{model_key}' is outside the 0.0 to 1.0 range")
|
|
152
|
+
model_config['temperature'] = 0.8
|
|
105
153
|
return data
|
|
106
154
|
|
|
155
|
+
def validate_and_fix_config_data(filepath: str) -> dict:
|
|
156
|
+
"""Load, validate, and fix configuration data"""
|
|
157
|
+
try:
|
|
158
|
+
with open(filepath, "r", encoding="utf-8") as file:
|
|
159
|
+
data = json.load(file)
|
|
160
|
+
|
|
161
|
+
# Get known fields from the ARAconfig model
|
|
162
|
+
known_fields = set(ARAconfig.model_fields.keys())
|
|
163
|
+
|
|
164
|
+
# Handle unrecognized keys
|
|
165
|
+
data = handle_unrecognized_keys(data, known_fields)
|
|
166
|
+
|
|
167
|
+
# Fix LLM temperatures before validation
|
|
168
|
+
data = fix_llm_temperatures(data)
|
|
169
|
+
|
|
170
|
+
return data
|
|
171
|
+
except json.JSONDecodeError as e:
|
|
172
|
+
print(f"Error: Invalid JSON in configuration file: {e}")
|
|
173
|
+
print("Creating new configuration with defaults...")
|
|
174
|
+
return {}
|
|
175
|
+
except Exception as e:
|
|
176
|
+
print(f"Error reading configuration file: {e}")
|
|
177
|
+
return {}
|
|
107
178
|
|
|
108
179
|
# Function to read the JSON file and return an ARAconfig model
|
|
109
180
|
@lru_cache(maxsize=1)
|
|
110
181
|
def read_data(filepath: str) -> ARAconfig:
|
|
182
|
+
# Ensure the directory for the config file exists
|
|
183
|
+
config_dir = dirname(filepath)
|
|
184
|
+
ensure_directory_exists(config_dir)
|
|
185
|
+
|
|
111
186
|
if not exists(filepath):
|
|
112
|
-
# If file does not exist, create it with default values
|
|
187
|
+
# If the file does not exist, create it with default values
|
|
113
188
|
default_config = ARAconfig()
|
|
114
|
-
|
|
115
|
-
with open(filepath, "w", encoding="utf-8") as file:
|
|
116
|
-
json.dump(default_config.model_dump(mode='json'), file, indent=4)
|
|
117
|
-
|
|
189
|
+
save_data(filepath, default_config)
|
|
118
190
|
print(
|
|
119
|
-
f"ara-cli configuration file '{filepath}' created with default configuration.
|
|
191
|
+
f"ara-cli configuration file '{filepath}' created with default configuration."
|
|
192
|
+
f" Please modify it as needed and re-run your command"
|
|
120
193
|
)
|
|
121
|
-
exit() # Exit the application
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
194
|
+
sys.exit(0) # Exit the application
|
|
195
|
+
|
|
196
|
+
# Validate and load the existing configuration
|
|
197
|
+
data = validate_and_fix_config_data(filepath)
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
# Try to create the config with the loaded data
|
|
201
|
+
config = ARAconfig(**data)
|
|
202
|
+
|
|
203
|
+
# Save the potentially fixed configuration back
|
|
204
|
+
save_data(filepath, config)
|
|
205
|
+
|
|
206
|
+
return config
|
|
207
|
+
except ValidationError as e:
|
|
208
|
+
print(f"ValidationError: {e}")
|
|
209
|
+
print("Correcting configuration with default values...")
|
|
210
|
+
|
|
211
|
+
# Create a default config
|
|
212
|
+
default_config = ARAconfig()
|
|
213
|
+
|
|
214
|
+
# Try to preserve valid fields from the original data
|
|
215
|
+
for field_name, field_value in data.items():
|
|
216
|
+
if field_name in ARAconfig.model_fields:
|
|
217
|
+
try:
|
|
218
|
+
# Attempt to set the field value
|
|
219
|
+
setattr(default_config, field_name, field_value)
|
|
220
|
+
except:
|
|
221
|
+
# If it fails, keep the default
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
# Save the corrected configuration
|
|
225
|
+
save_data(filepath, default_config)
|
|
226
|
+
print("Fixed configuration saved to file.")
|
|
227
|
+
|
|
228
|
+
return default_config
|
|
126
229
|
|
|
127
230
|
# Function to save the modified configuration back to the JSON file
|
|
128
231
|
def save_data(filepath: str, config: ARAconfig):
|
|
129
232
|
with open(filepath, "w", encoding="utf-8") as file:
|
|
130
|
-
json.dump(config.model_dump(
|
|
131
|
-
|
|
233
|
+
json.dump(config.model_dump(), file, indent=4)
|
|
132
234
|
|
|
133
235
|
# Singleton for configuration management
|
|
134
236
|
class ConfigManager:
|
|
@@ -143,4 +245,10 @@ class ConfigManager:
|
|
|
143
245
|
makedirs(config_dir)
|
|
144
246
|
|
|
145
247
|
cls._config_instance = read_data(filepath)
|
|
146
|
-
return cls._config_instance
|
|
248
|
+
return cls._config_instance
|
|
249
|
+
|
|
250
|
+
@classmethod
|
|
251
|
+
def reset(cls):
|
|
252
|
+
"""Reset the configuration instance (useful for testing)"""
|
|
253
|
+
cls._config_instance = None
|
|
254
|
+
read_data.cache_clear()
|
ara_cli/artefact_autofix.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from ara_cli.artefact_scan import check_file
|
|
1
2
|
from ara_cli.artefact_fuzzy_search import (
|
|
2
3
|
find_closest_name_matches,
|
|
3
4
|
extract_artefact_names_of_classifier,
|
|
@@ -29,36 +30,45 @@ def parse_report(content: str) -> Dict[str, List[Tuple[str, str]]]:
|
|
|
29
30
|
Parses the incompatible artefacts report and returns structured data.
|
|
30
31
|
Returns a dictionary where keys are artefact classifiers, and values are lists of (file_path, reason) tuples.
|
|
31
32
|
"""
|
|
33
|
+
def is_valid_report(lines: List[str]) -> bool:
|
|
34
|
+
return bool(lines) and lines[0] == "# Artefact Check Report"
|
|
35
|
+
|
|
36
|
+
def has_no_problems(lines: List[str]) -> bool:
|
|
37
|
+
return len(lines) >= 3 and lines[2] == "No problems found."
|
|
38
|
+
|
|
39
|
+
def parse_classifier(line: str) -> Optional[str]:
|
|
40
|
+
if line.startswith("## "):
|
|
41
|
+
return line[3:].strip()
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
def parse_issue(line: str) -> Optional[Tuple[str, str]]:
|
|
45
|
+
if not line.startswith("- "):
|
|
46
|
+
return None
|
|
47
|
+
parts = line.split("`", 2)
|
|
48
|
+
if len(parts) < 3:
|
|
49
|
+
return None
|
|
50
|
+
file_path = parts[1]
|
|
51
|
+
reason = parts[2].split(":", 1)[1].strip() if ":" in parts[2] else ""
|
|
52
|
+
return file_path, reason
|
|
53
|
+
|
|
32
54
|
lines = content.splitlines()
|
|
55
|
+
if not is_valid_report(lines) or has_no_problems(lines):
|
|
56
|
+
return {}
|
|
57
|
+
|
|
33
58
|
issues = {}
|
|
34
59
|
current_classifier = None
|
|
35
60
|
|
|
36
|
-
|
|
37
|
-
return issues
|
|
38
|
-
return issues
|
|
39
|
-
|
|
40
|
-
if len(lines) >= 3 and lines[2] == "No problems found.":
|
|
41
|
-
return issues
|
|
42
|
-
return issues
|
|
43
|
-
|
|
44
|
-
for line in lines[1:]:
|
|
45
|
-
line = line.strip()
|
|
61
|
+
for line in map(str.strip, lines[1:]):
|
|
46
62
|
if not line:
|
|
47
63
|
continue
|
|
48
|
-
|
|
49
|
-
if
|
|
50
|
-
current_classifier =
|
|
64
|
+
classifier = parse_classifier(line)
|
|
65
|
+
if classifier is not None:
|
|
66
|
+
current_classifier = classifier
|
|
51
67
|
issues[current_classifier] = []
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
continue
|
|
57
|
-
|
|
58
|
-
file_path = parts[1]
|
|
59
|
-
reason = parts[2].split(":", 1)[1].strip() if ":" in parts[2] else ""
|
|
60
|
-
issues[current_classifier].append((file_path, reason))
|
|
61
|
-
|
|
68
|
+
continue
|
|
69
|
+
issue = parse_issue(line)
|
|
70
|
+
if issue and current_classifier is not None:
|
|
71
|
+
issues[current_classifier].append(issue)
|
|
62
72
|
return issues
|
|
63
73
|
|
|
64
74
|
|
|
@@ -159,7 +169,7 @@ def ask_for_correct_contribution(
|
|
|
159
169
|
|
|
160
170
|
print(
|
|
161
171
|
f"Can not determine a match for contribution {contribution_message}. "
|
|
162
|
-
f"Please provide a valid contribution or contribution will be empty (
|
|
172
|
+
f"Please provide a valid contribution or contribution will be empty ([classifier] [file_name])."
|
|
163
173
|
)
|
|
164
174
|
|
|
165
175
|
user_input = input().strip()
|
|
@@ -179,7 +189,9 @@ def ask_for_correct_contribution(
|
|
|
179
189
|
def ask_for_contribution_choice(
|
|
180
190
|
choices, artefact_info: Optional[tuple[str, str]] = None
|
|
181
191
|
) -> Optional[str]:
|
|
182
|
-
artefact_name, artefact_classifier =
|
|
192
|
+
artefact_name, artefact_classifier = (
|
|
193
|
+
artefact_info if artefact_info else (None, None)
|
|
194
|
+
)
|
|
183
195
|
message = "Found multiple close matches for the contribution"
|
|
184
196
|
if artefact_name and artefact_classifier:
|
|
185
197
|
message += f" of the {artefact_classifier} '{artefact_name}'"
|
|
@@ -379,68 +391,118 @@ def apply_autofix(
|
|
|
379
391
|
file_path: str,
|
|
380
392
|
classifier: str,
|
|
381
393
|
reason: str,
|
|
394
|
+
single_pass: bool = False,
|
|
382
395
|
deterministic: bool = True,
|
|
383
396
|
non_deterministic: bool = True,
|
|
384
397
|
classified_artefact_info: Optional[Dict[str, List[Dict[str, str]]]] = None,
|
|
385
398
|
) -> bool:
|
|
386
|
-
|
|
387
|
-
|
|
399
|
+
"""
|
|
400
|
+
Applies fixes to a single artefact file iteratively until it is valid
|
|
401
|
+
or a fix cannot be applied. If single_pass is True, it runs for only one attempt.
|
|
402
|
+
"""
|
|
403
|
+
deterministic_markers_to_functions = {
|
|
404
|
+
"Filename-Title Mismatch": fix_title_mismatch,
|
|
405
|
+
"Invalid Contribution Reference": fix_contribution,
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
def populate_classified_artefact_info(force: bool = False):
|
|
409
|
+
nonlocal classified_artefact_info
|
|
410
|
+
if force or classified_artefact_info is None:
|
|
411
|
+
file_classifier = FileClassifier(os)
|
|
412
|
+
classified_artefact_info = file_classifier.classify_files()
|
|
413
|
+
|
|
414
|
+
def determine_attempt_count() -> int:
|
|
415
|
+
nonlocal single_pass, file_path
|
|
416
|
+
if single_pass:
|
|
417
|
+
print(f"Single-pass mode enabled for {file_path}. Running for 1 attempt.")
|
|
418
|
+
return 1
|
|
419
|
+
return 3
|
|
420
|
+
|
|
421
|
+
def apply_deterministic_fix() -> str:
|
|
422
|
+
nonlocal deterministic, deterministic_issue, corrected_text, file_path, artefact_text, artefact_class, classified_artefact_info
|
|
423
|
+
if deterministic and deterministic_issue:
|
|
424
|
+
print(f"Applying deterministic fix for '{deterministic_issue}'...")
|
|
425
|
+
fix_function = deterministic_markers_to_functions[deterministic_issue]
|
|
426
|
+
return fix_function(
|
|
427
|
+
file_path=file_path,
|
|
428
|
+
artefact_text=artefact_text,
|
|
429
|
+
artefact_class=artefact_class,
|
|
430
|
+
classified_artefact_info=classified_artefact_info,
|
|
431
|
+
)
|
|
432
|
+
return corrected_text
|
|
433
|
+
|
|
434
|
+
def apply_non_deterministic_fix() -> Optional[str]:
|
|
435
|
+
"""
|
|
436
|
+
Applies LLM fix. Return None in case of an exception
|
|
437
|
+
"""
|
|
438
|
+
nonlocal non_deterministic, deterministic_issue, corrected_text, artefact_type, current_reason, file_path, artefact_text
|
|
439
|
+
if non_deterministic and not deterministic_issue:
|
|
440
|
+
print("Applying non-deterministic (LLM) fix...")
|
|
441
|
+
prompt = construct_prompt(artefact_type, current_reason, file_path, artefact_text)
|
|
442
|
+
try:
|
|
443
|
+
corrected_artefact = run_agent(prompt, artefact_class)
|
|
444
|
+
corrected_text = corrected_artefact.serialize()
|
|
445
|
+
except Exception as e:
|
|
446
|
+
print(f" ❌ LLM agent failed to fix artefact at {file_path}: {e}")
|
|
447
|
+
return None
|
|
448
|
+
return corrected_text
|
|
449
|
+
|
|
450
|
+
def should_skip() -> bool:
|
|
451
|
+
nonlocal deterministic_issue, deterministic, non_deterministic
|
|
452
|
+
if not non_deterministic and not deterministic_issue:
|
|
453
|
+
print(f"Skipping non-deterministic fix for {file_path} as per request.")
|
|
454
|
+
return True
|
|
455
|
+
if not deterministic and deterministic_issue:
|
|
456
|
+
print(f"Skipping fix for {file_path} as per request flags.")
|
|
457
|
+
return True
|
|
388
458
|
return False
|
|
389
459
|
|
|
390
460
|
artefact_type, artefact_class = determine_artefact_type_and_class(classifier)
|
|
391
461
|
if artefact_type is None or artefact_class is None:
|
|
392
462
|
return False
|
|
393
463
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
classified_file_info = file_classifier.classified_files()
|
|
464
|
+
populate_classified_artefact_info()
|
|
465
|
+
max_attempts = determine_attempt_count()
|
|
397
466
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
467
|
+
for attempt in range(max_attempts):
|
|
468
|
+
is_valid, current_reason = check_file(file_path, artefact_class, classified_artefact_info)
|
|
469
|
+
|
|
470
|
+
if is_valid:
|
|
471
|
+
print(f"✅ Artefact at {file_path} is now valid.")
|
|
472
|
+
return True
|
|
473
|
+
|
|
474
|
+
print(f"Attempting to fix {file_path} (Attempt {attempt + 1}/{max_attempts})...")
|
|
475
|
+
print(f" Reason: {current_reason}")
|
|
476
|
+
|
|
477
|
+
artefact_text = read_artefact(file_path)
|
|
478
|
+
if artefact_text is None:
|
|
479
|
+
return False
|
|
402
480
|
|
|
403
|
-
try:
|
|
404
481
|
deterministic_issue = next(
|
|
405
482
|
(
|
|
406
483
|
marker
|
|
407
|
-
for marker in deterministic_markers_to_functions
|
|
408
|
-
if marker in
|
|
484
|
+
for marker in deterministic_markers_to_functions
|
|
485
|
+
if marker in current_reason
|
|
409
486
|
),
|
|
410
487
|
None,
|
|
411
488
|
)
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
is_deterministic_issue = deterministic_issue is not None
|
|
415
|
-
|
|
416
|
-
if deterministic and is_deterministic_issue:
|
|
417
|
-
print(f"Attempting deterministic fix for {file_path}...")
|
|
418
|
-
corrected_text = deterministic_markers_to_functions[deterministic_issue](
|
|
419
|
-
file_path=file_path,
|
|
420
|
-
artefact_text=artefact_text,
|
|
421
|
-
artefact_class=artefact_class,
|
|
422
|
-
classified_artefact_info=classified_artefact_info,
|
|
423
|
-
)
|
|
424
|
-
write_corrected_artefact(file_path, corrected_text)
|
|
425
|
-
return True
|
|
426
|
-
|
|
427
|
-
# Attempt non-deterministic fix if requested and the issue is NOT deterministic
|
|
428
|
-
if non_deterministic and not is_deterministic_issue:
|
|
429
|
-
print(f"Attempting non-deterministic (LLM) fix for {file_path}...")
|
|
430
|
-
prompt = construct_prompt(artefact_type, reason, file_path, artefact_text)
|
|
431
|
-
try:
|
|
432
|
-
corrected_artefact = run_agent(prompt, artefact_class)
|
|
433
|
-
corrected_text = corrected_artefact.serialize()
|
|
434
|
-
write_corrected_artefact(file_path, corrected_text)
|
|
435
|
-
return True
|
|
436
|
-
except Exception as e:
|
|
437
|
-
print(f"LLM agent failed to fix artefact at {file_path}: {e}")
|
|
489
|
+
|
|
490
|
+
if should_skip():
|
|
438
491
|
return False
|
|
439
492
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
493
|
+
corrected_text = None
|
|
494
|
+
|
|
495
|
+
corrected_text = apply_deterministic_fix()
|
|
496
|
+
corrected_text = apply_non_deterministic_fix()
|
|
497
|
+
|
|
498
|
+
if corrected_text is None or corrected_text.strip() == artefact_text.strip():
|
|
499
|
+
print(" Fixing attempt did not alter the file. Stopping to prevent infinite loop.")
|
|
500
|
+
return False
|
|
501
|
+
|
|
502
|
+
write_corrected_artefact(file_path, corrected_text)
|
|
503
|
+
|
|
504
|
+
print(" File modified. Re-classifying artefact information for next check...")
|
|
505
|
+
populate_classified_artefact_info(force=True)
|
|
445
506
|
|
|
507
|
+
print(f"❌ Failed to fix {file_path} after {max_attempts} attempts.")
|
|
446
508
|
return False
|
ara_cli/artefact_creator.py
CHANGED
|
@@ -106,7 +106,7 @@ class ArtefactCreator:
|
|
|
106
106
|
if not self.handle_existing_files(file_exists):
|
|
107
107
|
return
|
|
108
108
|
|
|
109
|
-
artefact = template_artefact_of_type(classifier, filename)
|
|
109
|
+
artefact = template_artefact_of_type(classifier, filename, False)
|
|
110
110
|
|
|
111
111
|
if parent_classifier and parent_name:
|
|
112
112
|
artefact.set_contribution(
|