ara-cli 0.1.9.69__py3-none-any.whl → 0.1.9.71__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.
- ara_cli/ara_command_action.py +16 -12
- ara_cli/ara_config.py +24 -10
- ara_cli/artefact_autofix.py +278 -23
- ara_cli/artefact_creator.py +3 -3
- ara_cli/artefact_fuzzy_search.py +9 -4
- ara_cli/artefact_link_updater.py +4 -4
- ara_cli/artefact_models/artefact_model.py +14 -7
- ara_cli/artefact_models/artefact_templates.py +1 -1
- ara_cli/artefact_models/feature_artefact_model.py +72 -18
- ara_cli/artefact_models/serialize_helper.py +1 -1
- ara_cli/artefact_reader.py +16 -38
- ara_cli/artefact_renamer.py +2 -2
- ara_cli/artefact_scan.py +28 -3
- ara_cli/chat.py +1 -1
- ara_cli/file_classifier.py +3 -3
- ara_cli/file_lister.py +1 -1
- ara_cli/list_filter.py +1 -1
- ara_cli/output_suppressor.py +1 -1
- ara_cli/prompt_extractor.py +3 -3
- ara_cli/prompt_handler.py +9 -10
- ara_cli/prompt_rag.py +2 -2
- ara_cli/template_manager.py +2 -2
- ara_cli/templates/prompt-modules/blueprints/complete_pytest_unittest.blueprint.md +1 -1
- ara_cli/update_config_prompt.py +2 -2
- ara_cli/version.py +1 -1
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.9.71.dist-info}/METADATA +1 -1
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.9.71.dist-info}/RECORD +39 -39
- tests/test_ara_command_action.py +7 -7
- tests/{test_ara_autofix.py → test_artefact_autofix.py} +163 -29
- tests/test_artefact_link_updater.py +3 -3
- tests/test_artefact_renamer.py +2 -2
- tests/test_artefact_scan.py +52 -19
- tests/test_file_classifier.py +1 -1
- tests/test_file_lister.py +1 -1
- tests/test_list_filter.py +2 -2
- tests/test_update_config_prompt.py +2 -2
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.9.71.dist-info}/WHEEL +0 -0
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.9.71.dist-info}/entry_points.txt +0 -0
- {ara_cli-0.1.9.69.dist-info → ara_cli-0.1.9.71.dist-info}/top_level.txt +0 -0
ara_cli/ara_command_action.py
CHANGED
|
@@ -33,7 +33,7 @@ def create_action(args):
|
|
|
33
33
|
if parent_classifier and parent_name and rule:
|
|
34
34
|
check_validity(Classifier.is_valid_classifier(parent_classifier), invalid_classifier_message)
|
|
35
35
|
check_validity(is_valid_filename(parent_name), invalid_name_message)
|
|
36
|
-
parent_artefact = ArtefactReader.
|
|
36
|
+
parent_artefact = ArtefactReader.read_artefact(artefact_name=parent_name, classifier=parent_classifier)
|
|
37
37
|
rule = find_closest_rule(parent_artefact, rule)
|
|
38
38
|
return parent_classifier, parent_name, rule
|
|
39
39
|
if parent_classifier and parent_name:
|
|
@@ -359,7 +359,7 @@ def reconnect_action(args):
|
|
|
359
359
|
|
|
360
360
|
feedback_message = f"Updated contribution of {classifier} '{artefact_name}' to {parent_classifier} '{parent_name}'"
|
|
361
361
|
|
|
362
|
-
content, artefact_info = ArtefactReader.
|
|
362
|
+
content, artefact_info = ArtefactReader.read_artefact_data(
|
|
363
363
|
artefact_name=artefact_name,
|
|
364
364
|
classifier=classifier
|
|
365
365
|
)
|
|
@@ -367,7 +367,7 @@ def reconnect_action(args):
|
|
|
367
367
|
print(read_error_message)
|
|
368
368
|
return
|
|
369
369
|
|
|
370
|
-
parent_content, parent_info = ArtefactReader.
|
|
370
|
+
parent_content, parent_info = ArtefactReader.read_artefact_data(
|
|
371
371
|
artefact_name=parent_name,
|
|
372
372
|
classifier=parent_classifier
|
|
373
373
|
)
|
|
@@ -399,7 +399,7 @@ def reconnect_action(args):
|
|
|
399
399
|
exit(1)
|
|
400
400
|
|
|
401
401
|
artefact.contribution = contribution
|
|
402
|
-
with open(artefact.file_path, 'w') as file:
|
|
402
|
+
with open(artefact.file_path, 'w', encoding='utf-8') as file:
|
|
403
403
|
artefact_content = artefact.serialize()
|
|
404
404
|
file.write(artefact_content)
|
|
405
405
|
|
|
@@ -426,7 +426,7 @@ def read_status_action(args):
|
|
|
426
426
|
lambda x: x["title"] == artefact_name, artefact_info_dicts
|
|
427
427
|
))
|
|
428
428
|
|
|
429
|
-
with open(artefact_info["file_path"], 'r') as file:
|
|
429
|
+
with open(artefact_info["file_path"], 'r', encoding='utf-8') as file:
|
|
430
430
|
content = file.read()
|
|
431
431
|
artefact = artefact_from_content(content)
|
|
432
432
|
|
|
@@ -458,7 +458,7 @@ def read_user_action(args):
|
|
|
458
458
|
lambda x: x["title"] == artefact_name, artefact_info_dicts
|
|
459
459
|
))
|
|
460
460
|
|
|
461
|
-
with open(artefact_info["file_path"], 'r') as file:
|
|
461
|
+
with open(artefact_info["file_path"], 'r', encoding='utf-8') as file:
|
|
462
462
|
content = file.read()
|
|
463
463
|
artefact = artefact_from_content(content)
|
|
464
464
|
|
|
@@ -500,14 +500,14 @@ def set_status_action(args):
|
|
|
500
500
|
lambda x: x["title"] == artefact_name, classified_artefact_dict
|
|
501
501
|
))
|
|
502
502
|
|
|
503
|
-
with open(artefact_info["file_path"], 'r') as file:
|
|
503
|
+
with open(artefact_info["file_path"], 'r', encoding='utf-8') as file:
|
|
504
504
|
content = file.read()
|
|
505
505
|
artefact = artefact_from_content(content)
|
|
506
506
|
|
|
507
507
|
artefact.status = new_status
|
|
508
508
|
|
|
509
509
|
serialized_content = artefact.serialize()
|
|
510
|
-
with open(f"{artefact_info['file_path']}", 'w') as file:
|
|
510
|
+
with open(f"{artefact_info['file_path']}", 'w', encoding='utf-8') as file:
|
|
511
511
|
file.write(serialized_content)
|
|
512
512
|
|
|
513
513
|
print(f"Status of task '{artefact_name}' has been updated to '{new_status}'.")
|
|
@@ -537,7 +537,7 @@ def set_user_action(args):
|
|
|
537
537
|
lambda x: x["title"] == artefact_name, classified_artefact_dict
|
|
538
538
|
))
|
|
539
539
|
|
|
540
|
-
with open(artefact_info["file_path"], 'r') as file:
|
|
540
|
+
with open(artefact_info["file_path"], 'r', encoding='utf-8') as file:
|
|
541
541
|
content = file.read()
|
|
542
542
|
artefact = artefact_from_content(content)
|
|
543
543
|
|
|
@@ -545,7 +545,7 @@ def set_user_action(args):
|
|
|
545
545
|
|
|
546
546
|
serialized_content = artefact.serialize()
|
|
547
547
|
|
|
548
|
-
with open(artefact_info["file_path"], 'w') as file:
|
|
548
|
+
with open(artefact_info["file_path"], 'w', encoding='utf-8') as file:
|
|
549
549
|
file.write(serialized_content)
|
|
550
550
|
|
|
551
551
|
print(f"User of task '{artefact_name}' has been updated to '{new_user}'.")
|
|
@@ -575,6 +575,7 @@ def scan_action(args):
|
|
|
575
575
|
|
|
576
576
|
def autofix_action(args):
|
|
577
577
|
from ara_cli.artefact_autofix import parse_report, apply_autofix, read_report_file
|
|
578
|
+
from ara_cli.file_classifier import FileClassifier
|
|
578
579
|
|
|
579
580
|
# If the user passes --non-deterministic, only_deterministic_fix becomes False.
|
|
580
581
|
# If the user passes --deterministic, only_non_deterministic_fix becomes False.
|
|
@@ -591,6 +592,9 @@ def autofix_action(args):
|
|
|
591
592
|
print("No issues found in the report. Nothing to fix.")
|
|
592
593
|
return
|
|
593
594
|
|
|
595
|
+
file_classifier = FileClassifier(os)
|
|
596
|
+
classified_artefact_info = file_classifier.classify_files()
|
|
597
|
+
|
|
594
598
|
# print("\nStarting autofix process...")
|
|
595
599
|
for classifier, files in issues.items():
|
|
596
600
|
print(f"\nClassifier: {classifier}")
|
|
@@ -600,8 +604,8 @@ def autofix_action(args):
|
|
|
600
604
|
classifier,
|
|
601
605
|
reason,
|
|
602
606
|
deterministic=run_deterministic,
|
|
603
|
-
non_deterministic=run_non_deterministic
|
|
607
|
+
non_deterministic=run_non_deterministic,
|
|
608
|
+
classified_artefact_info=classified_artefact_info
|
|
604
609
|
)
|
|
605
610
|
|
|
606
611
|
print("\nAutofix process completed. Please review the changes.")
|
|
607
|
-
|
ara_cli/ara_config.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List, Dict,
|
|
1
|
+
from typing import List, Dict, Optional
|
|
2
2
|
from pydantic import BaseModel
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
@@ -10,6 +10,13 @@ from functools import lru_cache
|
|
|
10
10
|
DEFAULT_CONFIG_LOCATION = "./ara/.araconfig/ara_config.json"
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class LLMConfigItem(BaseModel):
|
|
14
|
+
provider: str
|
|
15
|
+
model: str
|
|
16
|
+
temperature: float
|
|
17
|
+
max_tokens: Optional[int] = None
|
|
18
|
+
|
|
19
|
+
|
|
13
20
|
class ARAconfig(BaseModel):
|
|
14
21
|
ext_code_dirs: List[Dict[str, str]] = [
|
|
15
22
|
{"source_dir_1": "./src"},
|
|
@@ -36,42 +43,49 @@ class ARAconfig(BaseModel):
|
|
|
36
43
|
"*.jpg",
|
|
37
44
|
"*.jpeg",
|
|
38
45
|
]
|
|
39
|
-
llm_config: Dict[str,
|
|
46
|
+
llm_config: Dict[str, LLMConfigItem] = {
|
|
40
47
|
"gpt-4o": {
|
|
41
48
|
"provider": "openai",
|
|
42
49
|
"model": "openai/gpt-4o",
|
|
43
|
-
"temperature": 0.8
|
|
50
|
+
"temperature": 0.8,
|
|
51
|
+
"max_tokens": 16384
|
|
44
52
|
},
|
|
45
53
|
"gpt-4.1": {
|
|
46
54
|
"provider": "openai",
|
|
47
55
|
"model": "openai/gpt-4.1",
|
|
48
56
|
"temperature": 0.8,
|
|
57
|
+
"max_tokens": 1024
|
|
49
58
|
},
|
|
50
59
|
"o3-mini": {
|
|
51
60
|
"provider": "openai",
|
|
52
61
|
"model": "openai/o3-mini",
|
|
53
62
|
"temperature": 1.0,
|
|
63
|
+
"max_tokens": 1024
|
|
54
64
|
},
|
|
55
65
|
"opus-4": {
|
|
56
66
|
"provider": "anthropic",
|
|
57
67
|
"model": "anthropic/claude-opus-4-20250514",
|
|
58
68
|
"temperature": 0.8,
|
|
69
|
+
"max_tokens": 32000
|
|
59
70
|
},
|
|
60
71
|
"sonnet-4": {
|
|
61
72
|
"provider": "anthropic",
|
|
62
73
|
"model": "anthropic/claude-sonnet-4-20250514",
|
|
63
74
|
"temperature": 0.8,
|
|
75
|
+
"max_tokens": 1024
|
|
64
76
|
},
|
|
65
77
|
"together-ai-llama-2": {
|
|
66
78
|
"provider": "together_ai",
|
|
67
79
|
"model": "together_ai/togethercomputer/llama-2-70b",
|
|
68
80
|
"temperature": 0.8,
|
|
81
|
+
"max_tokens": 1024
|
|
69
82
|
},
|
|
70
83
|
"groq-llama-3": {
|
|
71
84
|
"provider": "groq",
|
|
72
85
|
"model": "groq/llama3-70b-8192",
|
|
73
86
|
"temperature": 0.8,
|
|
74
|
-
|
|
87
|
+
"max_tokens": 1024
|
|
88
|
+
}
|
|
75
89
|
}
|
|
76
90
|
default_llm: Optional[str] = "gpt-4o"
|
|
77
91
|
|
|
@@ -86,7 +100,7 @@ def ensure_directory_exists(directory: str):
|
|
|
86
100
|
|
|
87
101
|
|
|
88
102
|
def validate_config_data(filepath: str):
|
|
89
|
-
with open(filepath, "r") as file:
|
|
103
|
+
with open(filepath, "r", encoding="utf-8") as file:
|
|
90
104
|
data = json.load(file)
|
|
91
105
|
return data
|
|
92
106
|
|
|
@@ -98,8 +112,8 @@ def read_data(filepath: str) -> ARAconfig:
|
|
|
98
112
|
# If file does not exist, create it with default values
|
|
99
113
|
default_config = ARAconfig()
|
|
100
114
|
|
|
101
|
-
with open(filepath, "w") as file:
|
|
102
|
-
json.dump(default_config.model_dump(), file, indent=4)
|
|
115
|
+
with open(filepath, "w", encoding="utf-8") as file:
|
|
116
|
+
json.dump(default_config.model_dump(mode='json'), file, indent=4)
|
|
103
117
|
|
|
104
118
|
print(
|
|
105
119
|
f"ara-cli configuration file '{filepath}' created with default configuration. Please modify it as needed and re-run your command"
|
|
@@ -112,8 +126,8 @@ def read_data(filepath: str) -> ARAconfig:
|
|
|
112
126
|
|
|
113
127
|
# Function to save the modified configuration back to the JSON file
|
|
114
128
|
def save_data(filepath: str, config: ARAconfig):
|
|
115
|
-
with open(filepath, "w") as file:
|
|
116
|
-
json.dump(config.
|
|
129
|
+
with open(filepath, "w", encoding="utf-8") as file:
|
|
130
|
+
json.dump(config.model_dump(mode='json'), file, indent=4)
|
|
117
131
|
|
|
118
132
|
|
|
119
133
|
# Singleton for configuration management
|
|
@@ -129,4 +143,4 @@ class ConfigManager:
|
|
|
129
143
|
makedirs(config_dir)
|
|
130
144
|
|
|
131
145
|
cls._config_instance = read_data(filepath)
|
|
132
|
-
return cls._config_instance
|
|
146
|
+
return cls._config_instance
|
ara_cli/artefact_autofix.py
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
from ara_cli.artefact_fuzzy_search import (
|
|
2
|
+
find_closest_name_matches,
|
|
3
|
+
extract_artefact_names_of_classifier,
|
|
4
|
+
)
|
|
5
|
+
from ara_cli.file_classifier import FileClassifier
|
|
6
|
+
from ara_cli.artefact_reader import ArtefactReader
|
|
7
|
+
from ara_cli.artefact_models.artefact_load import artefact_from_content
|
|
8
|
+
from ara_cli.artefact_models.artefact_model import Artefact
|
|
9
|
+
from typing import Optional, Dict, List, Tuple
|
|
10
|
+
import difflib
|
|
1
11
|
import os
|
|
2
|
-
|
|
12
|
+
|
|
3
13
|
|
|
4
14
|
def read_report_file():
|
|
5
15
|
file_path = "incompatible_artefacts_report.md"
|
|
@@ -7,7 +17,9 @@ def read_report_file():
|
|
|
7
17
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
8
18
|
content = f.read()
|
|
9
19
|
except OSError:
|
|
10
|
-
print(
|
|
20
|
+
print(
|
|
21
|
+
'Artefact scan results file not found. Did you run the "ara scan" command?'
|
|
22
|
+
)
|
|
11
23
|
return None
|
|
12
24
|
return content
|
|
13
25
|
|
|
@@ -23,9 +35,11 @@ def parse_report(content: str) -> Dict[str, List[Tuple[str, str]]]:
|
|
|
23
35
|
|
|
24
36
|
if not lines or lines[0] != "# Artefact Check Report":
|
|
25
37
|
return issues
|
|
38
|
+
return issues
|
|
26
39
|
|
|
27
40
|
if len(lines) >= 3 and lines[2] == "No problems found.":
|
|
28
41
|
return issues
|
|
42
|
+
return issues
|
|
29
43
|
|
|
30
44
|
for line in lines[1:]:
|
|
31
45
|
line = line.strip()
|
|
@@ -51,7 +65,7 @@ def parse_report(content: str) -> Dict[str, List[Tuple[str, str]]]:
|
|
|
51
65
|
def read_artefact(file_path):
|
|
52
66
|
"""Reads the artefact text from the given file path."""
|
|
53
67
|
try:
|
|
54
|
-
with open(file_path,
|
|
68
|
+
with open(file_path, "r", encoding="utf-8") as file:
|
|
55
69
|
return file.read()
|
|
56
70
|
except FileNotFoundError:
|
|
57
71
|
print(f"File not found: {file_path}")
|
|
@@ -84,7 +98,7 @@ def construct_prompt(artefact_type, reason, file_path, artefact_text):
|
|
|
84
98
|
"Provide the corrected artefact. Do not reformulate the artefact, "
|
|
85
99
|
"just fix the pydantic model errors, use correct grammar. "
|
|
86
100
|
"You should follow the name of the file "
|
|
87
|
-
f"from its path {file_path} for naming the
|
|
101
|
+
f"from its path {file_path} for naming the artefact's title. "
|
|
88
102
|
"You are not allowed to use file extention in the artefact title. "
|
|
89
103
|
"You are not allowed to modify, delete or add tags. "
|
|
90
104
|
"User tag should be '@user_<username>'. The pydantic model already provides the '@user_' prefix. "
|
|
@@ -97,43 +111,234 @@ def construct_prompt(artefact_type, reason, file_path, artefact_text):
|
|
|
97
111
|
"then just delete those action items."
|
|
98
112
|
)
|
|
99
113
|
|
|
100
|
-
prompt +=
|
|
101
|
-
"\nThe current artefact is:\n"
|
|
102
|
-
"```\n"
|
|
103
|
-
f"{artefact_text}\n"
|
|
104
|
-
"```"
|
|
105
|
-
)
|
|
114
|
+
prompt += "\nThe current artefact is:\n" "```\n" f"{artefact_text}\n" "```"
|
|
106
115
|
|
|
107
116
|
return prompt
|
|
108
117
|
|
|
109
118
|
|
|
110
119
|
def run_agent(prompt, artefact_class):
|
|
111
120
|
from pydantic_ai import Agent
|
|
121
|
+
|
|
112
122
|
# gpt-4o
|
|
113
123
|
# anthropic:claude-3-7-sonnet-20250219
|
|
114
124
|
# anthropic:claude-4-sonnet-20250514
|
|
115
|
-
agent = Agent(
|
|
116
|
-
|
|
125
|
+
agent = Agent(
|
|
126
|
+
model="anthropic:claude-4-sonnet-20250514",
|
|
127
|
+
result_type=artefact_class,
|
|
128
|
+
instrument=True,
|
|
129
|
+
)
|
|
117
130
|
result = agent.run_sync(prompt)
|
|
118
131
|
return result.data
|
|
119
132
|
|
|
120
133
|
|
|
121
134
|
def write_corrected_artefact(file_path, corrected_text):
|
|
122
|
-
with open(file_path,
|
|
135
|
+
with open(file_path, "w", encoding="utf-8") as file:
|
|
123
136
|
file.write(corrected_text)
|
|
124
137
|
print(f"Fixed artefact at {file_path}")
|
|
125
138
|
|
|
126
139
|
|
|
127
|
-
def
|
|
140
|
+
def ask_for_correct_contribution(
|
|
141
|
+
artefact_info: Optional[tuple[str, str]] = None
|
|
142
|
+
) -> tuple[str, str]:
|
|
143
|
+
"""
|
|
144
|
+
Ask the user to provide a valid contribution when no match can be found.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
artefact_info: Optional tuple containing (artefact_name, artefact_classifier)
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
A tuple of (name, classifier) for the contribution
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
artefact_name, artefact_classifier = (
|
|
154
|
+
artefact_info if artefact_info else (None, None)
|
|
155
|
+
)
|
|
156
|
+
contribution_message = (
|
|
157
|
+
f"of {artefact_classifier} artefact '{artefact_name}'" if artefact_name else ""
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
print(
|
|
161
|
+
f"Can not determine a match for contribution {contribution_message}. "
|
|
162
|
+
f"Please provide a valid contribution or contribution will be empty (<classifier> <file_name>)."
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
user_input = input().strip()
|
|
166
|
+
|
|
167
|
+
if not user_input:
|
|
168
|
+
return None, None
|
|
169
|
+
|
|
170
|
+
parts = user_input.split(maxsplit=1)
|
|
171
|
+
if len(parts) != 2:
|
|
172
|
+
print("Invalid input format. Expected: <classifier> <file_name>")
|
|
173
|
+
return None, None
|
|
174
|
+
|
|
175
|
+
classifier, name = parts
|
|
176
|
+
return name, classifier
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def ask_for_contribution_choice(
|
|
180
|
+
choices, artefact_info: Optional[tuple[str, str]] = None
|
|
181
|
+
) -> Optional[str]:
|
|
182
|
+
artefact_name, artefact_classifier = artefact_info
|
|
183
|
+
message = "Found multiple close matches for the contribution"
|
|
184
|
+
if artefact_name and artefact_classifier:
|
|
185
|
+
message += f" of the {artefact_classifier} '{artefact_name}'"
|
|
186
|
+
print(f"{message}.")
|
|
187
|
+
for i, contribution in enumerate(choices):
|
|
188
|
+
print(f"{i + 1}: {contribution}")
|
|
189
|
+
choice_number = input(
|
|
190
|
+
"Please choose the artefact to use for contribution (enter number): "
|
|
191
|
+
)
|
|
192
|
+
try:
|
|
193
|
+
choice_index = int(choice_number) - 1
|
|
194
|
+
if choice_index < 0 or choice_index >= len(choices):
|
|
195
|
+
print("Invalid choice. Aborting contribution choice.")
|
|
196
|
+
return None
|
|
197
|
+
choice = choices[choice_index]
|
|
198
|
+
except ValueError:
|
|
199
|
+
print("Invalid input. Aborting contribution choice.")
|
|
200
|
+
return None
|
|
201
|
+
return choice
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _has_valid_contribution(artefact: Artefact) -> bool:
|
|
205
|
+
contribution = artefact.contribution
|
|
206
|
+
return contribution and contribution.artefact_name and contribution.classifier
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _update_rule(
|
|
210
|
+
artefact: Artefact, name: str, classifier: str, classified_file_info: dict
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Updates the rule in the contribution if a close match is found."""
|
|
213
|
+
rule = artefact.contribution.rule
|
|
214
|
+
|
|
215
|
+
content, artefact_data = ArtefactReader.read_artefact_data(
|
|
216
|
+
artefact_name=name,
|
|
217
|
+
classifier=classifier,
|
|
218
|
+
classified_file_info=classified_file_info,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
parent = artefact_from_content(content=content)
|
|
222
|
+
rules = parent.rules
|
|
223
|
+
|
|
224
|
+
closest_rule_match = difflib.get_close_matches(rule, rules, cutoff=0.5)
|
|
225
|
+
if closest_rule_match:
|
|
226
|
+
artefact.contribution.rule = closest_rule_match[0]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _set_contribution_multiple_matches(
|
|
230
|
+
artefact: Artefact,
|
|
231
|
+
closest_matches: list,
|
|
232
|
+
artefact_tuple: tuple,
|
|
233
|
+
classified_file_info: dict,
|
|
234
|
+
) -> tuple[Artefact, bool]:
|
|
235
|
+
contribution = artefact.contribution
|
|
236
|
+
classifier = contribution.classifier
|
|
237
|
+
original_name = contribution.artefact_name
|
|
238
|
+
|
|
239
|
+
closest_match = closest_matches[0]
|
|
240
|
+
if len(closest_matches) > 1:
|
|
241
|
+
closest_match = ask_for_contribution_choice(closest_matches, artefact_tuple)
|
|
242
|
+
|
|
243
|
+
if not closest_match:
|
|
244
|
+
print(
|
|
245
|
+
f"Contribution of {artefact_tuple[1]} '{artefact_tuple[0]}' will be empty."
|
|
246
|
+
)
|
|
247
|
+
artefact.contribution = None
|
|
248
|
+
return artefact, True
|
|
249
|
+
|
|
250
|
+
print(
|
|
251
|
+
f"Updating contribution of {artefact_tuple[1]} '{artefact_tuple[0]}' to {classifier} '{closest_match}'"
|
|
252
|
+
)
|
|
253
|
+
contribution.artefact_name = closest_match
|
|
254
|
+
artefact.contribution = contribution
|
|
255
|
+
|
|
256
|
+
if contribution.rule:
|
|
257
|
+
_update_rule(artefact, original_name, classifier, classified_file_info)
|
|
258
|
+
|
|
259
|
+
return artefact, True
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def set_closest_contribution(
|
|
263
|
+
artefact: Artefact, classified_file_info=None
|
|
264
|
+
) -> tuple[Artefact, bool]:
|
|
265
|
+
if not _has_valid_contribution(artefact):
|
|
266
|
+
return artefact, False
|
|
267
|
+
contribution = artefact.contribution
|
|
268
|
+
name = contribution.artefact_name
|
|
269
|
+
classifier = contribution.classifier
|
|
270
|
+
rule = contribution.rule
|
|
271
|
+
|
|
272
|
+
if not classified_file_info:
|
|
273
|
+
file_classifier = FileClassifier(os)
|
|
274
|
+
classified_file_info = file_classifier.classify_files()
|
|
275
|
+
|
|
276
|
+
all_artefact_names = extract_artefact_names_of_classifier(
|
|
277
|
+
classified_files=classified_file_info, classifier=classifier
|
|
278
|
+
)
|
|
279
|
+
closest_matches = find_closest_name_matches(
|
|
280
|
+
artefact_name=name, all_artefact_names=all_artefact_names
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
artefact_tuple = (artefact.title, artefact._artefact_type().value)
|
|
284
|
+
|
|
285
|
+
if not closest_matches:
|
|
286
|
+
name, classifier = ask_for_correct_contribution(artefact_tuple)
|
|
287
|
+
if not name or not classifier:
|
|
288
|
+
artefact.contribution = None
|
|
289
|
+
return artefact, True
|
|
290
|
+
print(f"Updating contribution of {artefact._artefact_type().value} '{artefact.title}' to {classifier} '{name}'")
|
|
291
|
+
contribution.artefact_name = name
|
|
292
|
+
contribution.classifier = classifier
|
|
293
|
+
artefact.contribution = contribution
|
|
294
|
+
return artefact, True
|
|
295
|
+
|
|
296
|
+
if closest_matches[0] == name:
|
|
297
|
+
return artefact, False
|
|
298
|
+
|
|
299
|
+
return _set_contribution_multiple_matches(
|
|
300
|
+
artefact=artefact,
|
|
301
|
+
closest_matches=closest_matches,
|
|
302
|
+
artefact_tuple=artefact_tuple,
|
|
303
|
+
classified_file_info=classified_file_info,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
print(
|
|
307
|
+
f"Updating contribution of {artefact._artefact_type().value} '{artefact.title}' to {classifier} '{closest_match}'"
|
|
308
|
+
)
|
|
309
|
+
contribution.artefact_name = closest_match
|
|
310
|
+
artefact.contribution = contribution
|
|
311
|
+
|
|
312
|
+
if not rule:
|
|
313
|
+
return artefact, True
|
|
314
|
+
|
|
315
|
+
content, artefact = ArtefactReader.read_artefact_data(
|
|
316
|
+
artefact_name=name,
|
|
317
|
+
classifier=classifier,
|
|
318
|
+
classified_file_info=classified_file_info,
|
|
319
|
+
)
|
|
320
|
+
parent = artefact_from_content(content=content)
|
|
321
|
+
rules = parent.rules
|
|
322
|
+
|
|
323
|
+
closest_rule_match = difflib.get_close_matches(rule, rules, cutoff=0.5)
|
|
324
|
+
if closest_rule_match:
|
|
325
|
+
contribution.rule = closest_rule_match
|
|
326
|
+
artefact.contribution = contribution
|
|
327
|
+
return artefact, True
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def fix_title_mismatch(
|
|
331
|
+
file_path: str, artefact_text: str, artefact_class, **kwargs
|
|
332
|
+
) -> str:
|
|
128
333
|
"""
|
|
129
334
|
Deterministically fixes the title in the artefact text to match the filename.
|
|
130
335
|
"""
|
|
131
336
|
base_name = os.path.basename(file_path)
|
|
132
337
|
correct_title_underscores, _ = os.path.splitext(base_name)
|
|
133
|
-
correct_title_spaces = correct_title_underscores.replace(
|
|
338
|
+
correct_title_spaces = correct_title_underscores.replace("_", " ")
|
|
134
339
|
|
|
135
340
|
title_prefix = artefact_class._title_prefix()
|
|
136
|
-
|
|
341
|
+
|
|
137
342
|
lines = artefact_text.splitlines()
|
|
138
343
|
new_lines = []
|
|
139
344
|
title_found_and_replaced = False
|
|
@@ -144,15 +349,40 @@ def fix_title_mismatch(file_path: str, artefact_text: str, artefact_class) -> st
|
|
|
144
349
|
title_found_and_replaced = True
|
|
145
350
|
else:
|
|
146
351
|
new_lines.append(line)
|
|
147
|
-
|
|
352
|
+
|
|
148
353
|
if not title_found_and_replaced:
|
|
149
|
-
print(
|
|
354
|
+
print(
|
|
355
|
+
f"Warning: Title prefix '{title_prefix}' not found in {file_path}. Title could not be fixed."
|
|
356
|
+
)
|
|
150
357
|
return artefact_text
|
|
151
358
|
|
|
152
359
|
return "\n".join(new_lines)
|
|
153
360
|
|
|
154
361
|
|
|
155
|
-
def
|
|
362
|
+
def fix_contribution(
|
|
363
|
+
file_path: str,
|
|
364
|
+
artefact_text: str,
|
|
365
|
+
artefact_class: str,
|
|
366
|
+
classified_artefact_info: dict,
|
|
367
|
+
**kwargs,
|
|
368
|
+
):
|
|
369
|
+
if not classified_artefact_info:
|
|
370
|
+
file_classifier = FileClassifier(os)
|
|
371
|
+
classified_artefact_info = file_classifier.classify_files()
|
|
372
|
+
artefact = artefact_class.deserialize(artefact_text)
|
|
373
|
+
artefact, _ = set_closest_contribution(artefact)
|
|
374
|
+
artefact_text = artefact.serialize()
|
|
375
|
+
return artefact_text
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def apply_autofix(
|
|
379
|
+
file_path: str,
|
|
380
|
+
classifier: str,
|
|
381
|
+
reason: str,
|
|
382
|
+
deterministic: bool = True,
|
|
383
|
+
non_deterministic: bool = True,
|
|
384
|
+
classified_artefact_info: Optional[Dict[str, List[Dict[str, str]]]] = None,
|
|
385
|
+
) -> bool:
|
|
156
386
|
artefact_text = read_artefact(file_path)
|
|
157
387
|
if artefact_text is None:
|
|
158
388
|
return False
|
|
@@ -161,11 +391,36 @@ def apply_autofix(file_path: str, classifier: str, reason: str, deterministic: b
|
|
|
161
391
|
if artefact_type is None or artefact_class is None:
|
|
162
392
|
return False
|
|
163
393
|
|
|
164
|
-
|
|
394
|
+
if classified_artefact_info is None:
|
|
395
|
+
file_classifier = FileClassifier(os)
|
|
396
|
+
classified_file_info = file_classifier.classified_files()
|
|
397
|
+
|
|
398
|
+
deterministic_markers_to_functions = {
|
|
399
|
+
"Filename-Title Mismatch": fix_title_mismatch,
|
|
400
|
+
"Invalid Contribution Reference": fix_contribution,
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
deterministic_issue = next(
|
|
405
|
+
(
|
|
406
|
+
marker
|
|
407
|
+
for marker in deterministic_markers_to_functions.keys()
|
|
408
|
+
if marker in reason
|
|
409
|
+
),
|
|
410
|
+
None,
|
|
411
|
+
)
|
|
412
|
+
except StopIteration:
|
|
413
|
+
pass
|
|
414
|
+
is_deterministic_issue = deterministic_issue is not None
|
|
165
415
|
|
|
166
416
|
if deterministic and is_deterministic_issue:
|
|
167
417
|
print(f"Attempting deterministic fix for {file_path}...")
|
|
168
|
-
corrected_text =
|
|
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
|
+
)
|
|
169
424
|
write_corrected_artefact(file_path, corrected_text)
|
|
170
425
|
return True
|
|
171
426
|
|
|
@@ -181,11 +436,11 @@ def apply_autofix(file_path: str, classifier: str, reason: str, deterministic: b
|
|
|
181
436
|
except Exception as e:
|
|
182
437
|
print(f"LLM agent failed to fix artefact at {file_path}: {e}")
|
|
183
438
|
return False
|
|
184
|
-
|
|
439
|
+
|
|
185
440
|
# Log if a fix was skipped due to flags
|
|
186
441
|
if is_deterministic_issue and not deterministic:
|
|
187
442
|
print(f"Skipping deterministic fix for {file_path} as per request.")
|
|
188
443
|
elif not is_deterministic_issue and not non_deterministic:
|
|
189
444
|
print(f"Skipping non-deterministic fix for {file_path} as per request.")
|
|
190
445
|
|
|
191
|
-
return False
|
|
446
|
+
return False
|
ara_cli/artefact_creator.py
CHANGED
|
@@ -16,7 +16,7 @@ class ArtefactCreator:
|
|
|
16
16
|
|
|
17
17
|
@lru_cache(maxsize=None)
|
|
18
18
|
def read_template_content(self, template_file_path):
|
|
19
|
-
with open(template_file_path, "r") as template_file:
|
|
19
|
+
with open(template_file_path, "r", encoding="utf-8") as template_file:
|
|
20
20
|
return template_file.read()
|
|
21
21
|
|
|
22
22
|
def create_artefact_prompt_files(self, dir_path, template_path, classifier):
|
|
@@ -118,7 +118,7 @@ class ArtefactCreator:
|
|
|
118
118
|
artefact_content = artefact.serialize()
|
|
119
119
|
rmtree(dir_path, ignore_errors=True)
|
|
120
120
|
os.makedirs(dir_path, exist_ok=True)
|
|
121
|
-
with open(file_path, 'w') as artefact_file:
|
|
121
|
+
with open(file_path, 'w', encoding='utf-8') as artefact_file:
|
|
122
122
|
artefact_file.write(artefact_content)
|
|
123
123
|
|
|
124
124
|
relative_file_path = os.path.relpath(file_path, original_directory)
|
|
@@ -138,7 +138,7 @@ class ArtefactCreator:
|
|
|
138
138
|
|
|
139
139
|
title = Classifier.get_artefact_title(classifier)
|
|
140
140
|
|
|
141
|
-
with open(file_path, "r") as file:
|
|
141
|
+
with open(file_path, "r", encoding="utf-8") as file:
|
|
142
142
|
for line in file:
|
|
143
143
|
if line.strip().startswith(title):
|
|
144
144
|
return line.split(':')[1].strip()
|
ara_cli/artefact_fuzzy_search.py
CHANGED
|
@@ -33,12 +33,17 @@ def suggest_close_name_matches_for_parent(artefact_name: str, all_artefact_names
|
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def
|
|
37
|
-
closest_matches = difflib.get_close_matches(artefact_name, all_artefact_names, cutoff=0.5
|
|
36
|
+
def find_closest_name_matches(artefact_name: str, all_artefact_names: list[str]) -> Optional[str]:
|
|
37
|
+
closest_matches = difflib.get_close_matches(artefact_name, all_artefact_names, cutoff=0.5)
|
|
38
38
|
if not closest_matches:
|
|
39
39
|
return None
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
return closest_matches
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def extract_artefact_names_of_classifier(classified_files: dict[str, list[dict]], classifier: str):
|
|
44
|
+
artefact_info_of_classifier = classified_files.get(classifier, [])
|
|
45
|
+
titles = list(map(lambda artefact: artefact['title'], artefact_info_of_classifier))
|
|
46
|
+
return titles
|
|
42
47
|
|
|
43
48
|
|
|
44
49
|
def find_closest_rule(parent_artefact: 'Artefact', rule: str):
|