forked from AstuteSource/chasten
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request AstuteSource#91 from AstuteSource/createchecks
feat: Have AI generate a check file for user
- Loading branch information
Showing
9 changed files
with
1,593 additions
and
643 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
# Import necessary modules and components from the Textual library, | ||
# as well as other Python modules like os and validation tools. | ||
from pathlib import Path | ||
from typing import ClassVar, List | ||
|
||
from textual.app import App, ComposeResult | ||
from textual.validation import Number | ||
from textual.widgets import Button, Input, Pretty, Static | ||
|
||
from chasten import constants | ||
|
||
CHECK_STORAGE = constants.chasten.App_Storage | ||
# Constants to map input field names to their positions in the Check list | ||
CHECK_VALUE = { | ||
"Check": 0, | ||
"Matches": 1, | ||
} | ||
CHECK_DEFAULT = ["", "1", False] | ||
|
||
|
||
def split_file(file_name: Path) -> List[List[str]]: | ||
"""Split a csv file into a list of lists.""" | ||
check_list = [] | ||
with open(file_name) as file: | ||
for row in file: | ||
strip_row = row.strip() # Remove leading/trailing white spaces | ||
if strip_row: | ||
check_list.append(strip_row.split(",")) | ||
return check_list | ||
|
||
|
||
def write_checks(check_list: List[List[str]]) -> str: | ||
"""Generate structured output based on the contents of the file.""" | ||
if len(check_list) != 0: | ||
result = "Make a YAML file that checks for:" | ||
for checks in check_list: | ||
quantity = "exactly" if checks[2] == "True" else "at minimum" | ||
result += f"\n - {quantity} {checks[1]} {checks[0]}" | ||
return result | ||
return "[red][ERROR][/red] No checks were supplied" | ||
|
||
|
||
def store_in_file(File: Path, Pattern, Matches, Exact): | ||
"""Store inputed values into a text file""" | ||
File.touch() | ||
with open(File, "a") as file: | ||
file.write(f"\n{Pattern},{Matches},{Exact}") # Append input data to the file | ||
|
||
|
||
# Define input fields and buttons for the user interface | ||
Check_Input = Input(placeholder="Check For:", id="Check", name="Check") | ||
Match_Input = Input( | ||
placeholder="How many matches do you expect", | ||
id="Matches", | ||
name="Matches", | ||
validators=Number(1, 500), # Validate that Matches is a number between 1 and 500 | ||
) | ||
Exact_button = Button("Exact", id="Exact") # Button to trigger an action | ||
|
||
|
||
# Static widget to display user input and validation results | ||
class answers(Static): | ||
def compose(self) -> ComposeResult: | ||
"""For displaying the user interface""" | ||
yield Check_Input | ||
yield Match_Input | ||
|
||
|
||
# Static widget to display buttons for user interactions | ||
class button_prompts(Static): | ||
def compose(self) -> ComposeResult: | ||
"""For displaying the user interface""" | ||
yield Pretty([]) # Widget to display validation messages | ||
yield Exact_button # Display the "Exact" button | ||
yield Button("Submit Check!", id="next") # Display the "Next Check!" button | ||
yield Button("Done", id="done") | ||
yield Button("Clear Checks", id="clear", variant="error") | ||
|
||
|
||
# Custom App class for the Textual application | ||
class config_App(App): | ||
CSS = """ | ||
Screen { | ||
layout: horizontal; | ||
} | ||
answers { | ||
width: 100%; | ||
align: center top; | ||
dock: top; | ||
background: $boost; | ||
min-width: 50; | ||
padding: 1; | ||
border: wide black; | ||
} | ||
button_prompts { | ||
background: $boost; | ||
layout: vertical; | ||
margin: 1; | ||
align: left top; | ||
border: wide black; | ||
width: 100%; | ||
} | ||
Button { | ||
background: rgb(245, 184, 50); | ||
content-align: center top; | ||
height: 3; | ||
width: 100%; | ||
} | ||
Button:hover { | ||
background: white; | ||
color: black; | ||
} | ||
""" | ||
Check: ClassVar = ["", "1", False] # noqa: RUF012 | ||
Valid: bool = False | ||
|
||
def on_input_changed(self, event: Input.Changed) -> None: | ||
"""When inputs change this updates the values of Check""" | ||
self.Valid = False | ||
if event.input.id == "Check": | ||
self.Check[CHECK_VALUE[str(event.input.name)]] = event.input.value | ||
elif event.validation_result is not None: | ||
if event.validation_result.is_valid: | ||
self.Check[CHECK_VALUE[str(event.input.name)]] = event.input.value | ||
self.Valid = True | ||
|
||
def on_button_pressed(self, event: Button.Pressed) -> None: | ||
if event.button.id == "Exact": | ||
self.Check[2] = True # Mark the "Exact" button as clicked | ||
event.button.disabled = True # Disable the "Exact" button after clicking | ||
elif event.button.id == "done": | ||
config_App.exit( | ||
self | ||
) # Exit the application if the "Done" button is clicked | ||
elif event.button.id == "clear": | ||
with open(CHECK_STORAGE, "w") as file: | ||
file.write( | ||
"" | ||
) # Clears Checks.txt file when "Clear Check" button is clicked | ||
elif self.Valid: | ||
if event.button.id == "next": | ||
# If "Next Check!" is clicked and input is valid, record the input data to a file | ||
store_in_file( | ||
CHECK_STORAGE, self.Check[0], self.Check[1], self.Check[2] | ||
) | ||
self.Check[0] = "" | ||
self.Check[1] = "1" | ||
self.Check[2] = False | ||
# Reset input fields, clear validation messages, and enable the "Exact" button | ||
self.query_one(Pretty).update([]) # Clear any validation messages | ||
Exact_button.disabled = False # Re-enable the "Exact" button | ||
Check_Input.value = "" | ||
Match_Input.value = "" # Refresh the application UI | ||
else: | ||
self.query_one(Pretty).update(["Invalid Input Please enter a Integer"]) | ||
Match_Input.value = "" # Clear the "Matches" input field | ||
|
||
def compose(self) -> ComposeResult: | ||
"""For displaying the user interface""" | ||
yield answers() # Display the input fields for user input | ||
yield button_prompts() # Display the buttons for user interaction |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
from pathlib import Path | ||
|
||
import openai | ||
from cryptography.fernet import Fernet | ||
|
||
genscript = """ | ||
checks: | ||
- name: "A human-readable name for the check, describing its purpose or objective. For example, 'Class Definition' or 'Function Naming Convention'." | ||
code: "A short, unique identifier or code that distinguishes the check from others. This code is often used for reference or automation." | ||
id: "A unique identifier or tag that provides additional context to the check. It may include information such as the check's category or origin. For instance, 'C001' could represent a 'Code Quality' category." | ||
pattern: "An XPath expression specifying the exact code pattern or element that the check aims to locate within the source code. This expression serves as the search criteria for the check's evaluation." | ||
count: | ||
min: "The minimum number of times the specified pattern is expected to appear in the source code. It establishes the lower limit for the check's results, ensuring compliance or a minimum level of occurrence." | ||
max: "The maximum number of times the specified pattern is expected to appear in the source code. It defines an upper limit for the check's results, helping to identify potential issues or excessive occurrences." | ||
Example: | ||
- name: "class-definition" | ||
code: "CDF" | ||
id: "C001" | ||
pattern: './/ClassDef' | ||
count: | ||
min: 1 | ||
max: 50 | ||
- name: "all-function-definition" | ||
code: "AFD" | ||
id: "F001" | ||
pattern: './/FunctionDef' | ||
count: | ||
min: 1 | ||
max: 200 | ||
- name: "non-test-function-definition" | ||
code: "NTF" | ||
id: "F002" | ||
pattern: './/FunctionDef[not(contains(@name, "test_"))]' | ||
count: | ||
min: 40 | ||
max: 70 | ||
- name: "single-nested-if" | ||
code: "SNI" | ||
id: "CL001" | ||
pattern: './/FunctionDef/body//If' | ||
count: | ||
min: 1 | ||
max: 100 | ||
- name: "double-nested-if" | ||
code: "DNI" | ||
id: "CL002" | ||
pattern: './/FunctionDef/body//If[ancestor::If and not(parent::orelse)]' | ||
count: | ||
min: 1 | ||
max: 15 | ||
""" | ||
|
||
API_KEY_FILE = "userapikey.txt" | ||
|
||
|
||
def save_user_api_key(user_api_key): | ||
key = Fernet.generate_key() | ||
fernet = Fernet(key) | ||
encrypted_key = fernet.encrypt(user_api_key.encode()).decode() | ||
with open(API_KEY_FILE, "w") as f: | ||
f.write(key.decode() + "\n" + encrypted_key) | ||
|
||
|
||
def load_user_api_key(file): | ||
with open(file, "r") as f: | ||
lines = f.read().strip().split("\n") | ||
if len(lines) == 2: # noqa: PLR2004 | ||
key = lines[0].encode() | ||
encrypted_key = lines[1] | ||
fernet = Fernet(key) | ||
return fernet.decrypt(encrypted_key.encode()).decode() | ||
|
||
|
||
def is_valid_api_key(api_key): | ||
try: | ||
openai.api_key = api_key | ||
openai.ChatCompletion.create( | ||
model="gpt-3.5-turbo", | ||
messages=[{"role": "system", "content": "Test message"}], | ||
) | ||
return True | ||
except openai.error.OpenAIError: | ||
return False | ||
|
||
|
||
def generate_yaml_config(file: Path, user_api_key, user_input: str) -> str: | ||
try: | ||
openai.api_key = user_api_key | ||
|
||
prompts = [ | ||
genscript | ||
+ "in the same format as what is shown above(do not just generate the example use it as a framework nothing else): " | ||
+ user_input | ||
] | ||
|
||
response = openai.ChatCompletion.create( | ||
model="gpt-3.5-turbo", | ||
messages=[ | ||
{ | ||
"role": "system", | ||
"content": f"You are a helpful assistant that generates YAML configurations. Your task is to {prompts}", | ||
} | ||
], | ||
) | ||
|
||
generated_yaml = response.choices[0].message["content"].strip() | ||
file.touch() | ||
|
||
with open(file, "w") as f: | ||
f.write(generated_yaml) | ||
|
||
return generated_yaml | ||
|
||
except openai.error.OpenAIError: | ||
return "[red][Error][/red] There was an issue with the API key. Make sure you input your API key correctly." |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.