Skip to content

Commit

Permalink
Merge pull request AstuteSource#91 from AstuteSource/createchecks
Browse files Browse the repository at this point in the history
feat: Have AI generate a check file for user
  • Loading branch information
laurennevill authored Nov 28, 2023
2 parents c0a7779 + 70ca922 commit 6e8dd03
Show file tree
Hide file tree
Showing 9 changed files with 1,593 additions and 643 deletions.
161 changes: 161 additions & 0 deletions chasten/configApp.py
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
4 changes: 4 additions & 0 deletions chasten/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class Chasten:
Analyze_Storage: Path
Application_Name: str
Application_Author: str
App_Storage: Path
API_Key_Storage: Path
Chasten_Database_View: str
Emoji: str
Executable_Fly: str
Expand All @@ -31,6 +33,8 @@ class Chasten:
Analyze_Storage=Path("analysis.md"),
Application_Name="chasten",
Application_Author="ChastenedTeam",
App_Storage=Path("check.txt"),
API_Key_Storage=Path("userapikey.txt"),
Chasten_Database_View="chasten_complete",
Emoji=":dizzy:",
Executable_Fly="fly",
Expand Down
118 changes: 118 additions & 0 deletions chasten/createchecks.py
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."
53 changes: 52 additions & 1 deletion chasten/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@

from chasten import (
checks,
configApp,
configuration,
constants,
createchecks,
database,
debug,
enumerations,
Expand All @@ -26,9 +28,11 @@

# create a Typer object to support the command-line interface
cli = typer.Typer(no_args_is_help=True)

app = configApp.config_App()
# create a small bullet for display in the output
small_bullet_unicode = constants.markers.Small_Bullet_Unicode
CHECK_STORAGE = constants.chasten.App_Storage
API_KEY_STORAGE = constants.chasten.API_Key_Storage
ANALYSIS_FILE = constants.chasten.Analyze_Storage


Expand Down Expand Up @@ -98,6 +102,53 @@ def display_serve_or_publish_details(
# ---


@cli.command()
def create_checks(
filename: Path = typer.Option("checks.yml", help="YAML file name")
) -> None:
"""🔧 Interactively specify for checks and have a checks.yml file created(Requires API key)"""
# creates a textual object for better user interface
app.run()
# Checks if the file storing the wanted checks exists and is valid
if filesystem.confirm_valid_file(CHECK_STORAGE):
# stores the human readable version of the checks
result = configApp.write_checks(configApp.split_file(CHECK_STORAGE))
# Checks if API key storage file exists
if filesystem.confirm_valid_file(API_KEY_STORAGE):
# prints the human readable checks to the terminal
output.console.print(result)
# loads the decrypted API Key
api_key = createchecks.load_user_api_key(API_KEY_STORAGE)
# calls the function to generate the yaml file
output.console.print(
createchecks.generate_yaml_config(filename, api_key, result)
)
else:
# prompts the user to input there API key to the terminal
api_key = input("Please Enter your openai API Key:")
# If not a valid API key prompts user again
while not createchecks.is_valid_api_key(api_key):
output.console.print(
"[red][ERROR][/red] Invalid API key. Please enter a valid API key."
)
api_key = input("Please Enter your openai API Key:")
# stores the API key in a file
createchecks.save_user_api_key(api_key)
# prints the human readable checks to the terminal
output.console.print(result)
# gets the decrypted API Key
api_key = createchecks.load_user_api_key(API_KEY_STORAGE)
# prints the generated YAML file to the terminal
output.console.print(
createchecks.generate_yaml_config(filename, api_key, result)
)
else:
# displays an error message if the CHECK_STORAGE file does not exist
output.console.print(
f"[red][ERROR][/red] No {CHECK_STORAGE} file exists\n - Rerun the command and specify checks"
)


@cli.command()
def configure( # noqa: PLR0913
task: enumerations.ConfigureTask = typer.Argument(
Expand Down
Loading

0 comments on commit 6e8dd03

Please sign in to comment.