Spaces:
Sleeping
Sleeping
Daniel Nichols
commited on
Commit
•
8ab167c
0
Parent(s):
initial commit
Browse files- .gitignore +5 -0
- requirements.txt +4 -0
- src/models.py +99 -0
- src/perfguru.py +164 -0
- src/profiles.py +26 -0
- src/rag.py +70 -0
.gitignore
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
__pycache__
|
2 |
+
*.pyc
|
3 |
+
*.pyo
|
4 |
+
|
5 |
+
.env
|
requirements.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio==4.39.0
|
2 |
+
hatchet==1.4.0
|
3 |
+
google-generativeai==0.7.2
|
4 |
+
openai==1.37.0
|
src/models.py
ADDED
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
""" A light wrapper around a bunch of chat LLMs. The class should define a method that takes text input and returns a response from the model.
|
2 |
+
"""
|
3 |
+
from abc import ABC, abstractmethod
|
4 |
+
from typing import Generator, Optional, AsyncGenerator
|
5 |
+
import os
|
6 |
+
import random
|
7 |
+
import openai
|
8 |
+
import google.generativeai as genai
|
9 |
+
|
10 |
+
class ChatModel(ABC):
|
11 |
+
def __init__(self, name):
|
12 |
+
self.name = name
|
13 |
+
|
14 |
+
def __str__(self):
|
15 |
+
return self.name
|
16 |
+
|
17 |
+
def __repr__(self):
|
18 |
+
return self.name
|
19 |
+
|
20 |
+
@abstractmethod
|
21 |
+
def get_response(self, prompt) -> Generator[str, None, None]:
|
22 |
+
pass
|
23 |
+
|
24 |
+
|
25 |
+
class DummyModel(ChatModel):
|
26 |
+
|
27 |
+
def __init__(self):
|
28 |
+
super().__init__("dummy")
|
29 |
+
|
30 |
+
def get_response(self, prompt: str) -> Generator[str, None, None]:
|
31 |
+
response = f"Dummy response to: {prompt}"
|
32 |
+
for idx in range(len(response)):
|
33 |
+
yield response[:idx+1]
|
34 |
+
|
35 |
+
|
36 |
+
class OpenAIModel(ChatModel):
|
37 |
+
|
38 |
+
def __init__(self, model: str, client: openai.OpenAI):
|
39 |
+
super().__init__(model)
|
40 |
+
self.model = model
|
41 |
+
self.client = client
|
42 |
+
|
43 |
+
def get_response(self, prompt: str) -> Generator[str, None, None]:
|
44 |
+
stream = self.client.chat.completions.create(
|
45 |
+
model=self.model,
|
46 |
+
messages=[
|
47 |
+
{"role": "system", "content": "You are PerfGuru, a helpful assistant for assisting developers in identifying performance bottlenecks in their code and optimizing them."},
|
48 |
+
{"role": "user", "content": prompt}
|
49 |
+
],
|
50 |
+
stream=True,
|
51 |
+
max_tokens=4096,
|
52 |
+
)
|
53 |
+
response = ""
|
54 |
+
for chunk in stream:
|
55 |
+
response += chunk.choices[0].delta.content or ""
|
56 |
+
yield response
|
57 |
+
|
58 |
+
|
59 |
+
|
60 |
+
class GeminiModel(ChatModel):
|
61 |
+
|
62 |
+
def __init__(self, model: str, api_key: Optional[str] = None):
|
63 |
+
super().__init__(model)
|
64 |
+
if api_key:
|
65 |
+
genai.configure(api_key=api_key)
|
66 |
+
|
67 |
+
self.model = genai.GenerativeModel(model)
|
68 |
+
self.config = genai.types.GenerationConfig(
|
69 |
+
candidate_count=1,
|
70 |
+
max_output_tokens=4096,
|
71 |
+
)
|
72 |
+
|
73 |
+
def get_response(self, prompt: str) -> Generator[str, None, None]:
|
74 |
+
stream = self.model.generate_content(prompt, stream=True, generation_config=self.config)
|
75 |
+
response = ""
|
76 |
+
for chunk in stream:
|
77 |
+
response += chunk.text or ""
|
78 |
+
yield response
|
79 |
+
|
80 |
+
|
81 |
+
AVAILABLE_MODELS = []
|
82 |
+
|
83 |
+
#AVAILABLE_MODELS.append( DummyModel() )
|
84 |
+
|
85 |
+
if os.environ.get("OPENAI_API_KEY"):
|
86 |
+
openai_client = openai.OpenAI()
|
87 |
+
AVAILABLE_MODELS.append( OpenAIModel("gpt-4o-mini", openai_client) )
|
88 |
+
AVAILABLE_MODELS.append( OpenAIModel("gpt-3.5-turbo", openai_client) )
|
89 |
+
|
90 |
+
if os.environ.get("GOOGLE_API_KEY"):
|
91 |
+
AVAILABLE_MODELS.append( GeminiModel("gemini-1.5-flash") )
|
92 |
+
AVAILABLE_MODELS.append( GeminiModel("gemini-1.5-pro") )
|
93 |
+
|
94 |
+
|
95 |
+
if not AVAILABLE_MODELS:
|
96 |
+
raise ValueError("No models available. Please set OPENAI_API_KEY or GOOGLE_API_KEY environment variables.")
|
97 |
+
|
98 |
+
def select_random_model() -> ChatModel:
|
99 |
+
return random.choice(AVAILABLE_MODELS)
|
src/perfguru.py
ADDED
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import json
|
3 |
+
import os
|
4 |
+
import datetime
|
5 |
+
from itertools import zip_longest
|
6 |
+
|
7 |
+
from models import select_random_model
|
8 |
+
from rag import select_random_formatter
|
9 |
+
|
10 |
+
|
11 |
+
def error_helper(msg: str, duration: int = 10):
|
12 |
+
raise gr.Error(msg, duration=duration)
|
13 |
+
|
14 |
+
|
15 |
+
def code_upload(code_file_select):
|
16 |
+
if code_file_select is None:
|
17 |
+
return gr.Button(interactive=False)
|
18 |
+
else:
|
19 |
+
return gr.Button(interactive=True)
|
20 |
+
|
21 |
+
|
22 |
+
def chat_with_llms(prompt, code_files, profile_file, profile_type):
|
23 |
+
model1 = select_random_model()
|
24 |
+
model2 = select_random_model()
|
25 |
+
formatter1 = select_random_formatter()
|
26 |
+
formatter2 = select_random_formatter()
|
27 |
+
|
28 |
+
print(f"Selected models: {model1.name} and {model2.name}")
|
29 |
+
|
30 |
+
formatted1 = formatter1.format_prompt(prompt, code_files, profile_file, profile_type, error_fn=error_helper)
|
31 |
+
formatted2 = formatter2.format_prompt(prompt, code_files, profile_file, profile_type, error_fn=error_helper)
|
32 |
+
|
33 |
+
if formatted1 is None or formatted2 is None:
|
34 |
+
error_helper("Failed to format prompt. Please try again.")
|
35 |
+
|
36 |
+
response1 = model1.get_response(formatted1)
|
37 |
+
response2 = model2.get_response(formatted2)
|
38 |
+
|
39 |
+
if response1 is None:
|
40 |
+
error_helper(f"Failed to get response from {model1.name}. Please try again.")
|
41 |
+
|
42 |
+
if response2 is None:
|
43 |
+
error_helper(f"Failed to get response from {model2.name}. Please try again.")
|
44 |
+
|
45 |
+
source1 = gr.Markdown(f"{model1.name} + {formatter1.name}", visible=False, elem_classes=["not-voted"])
|
46 |
+
source2 = gr.Markdown(f"{model2.name} + {formatter2.name}", visible=False, elem_classes=["not-voted"])
|
47 |
+
|
48 |
+
# set vote buttons to deactive
|
49 |
+
vote_buttons = gr.Button(interactive=False), gr.Button(interactive=False), gr.Button(interactive=False), gr.Button(interactive=False)
|
50 |
+
|
51 |
+
for c1, c2 in zip_longest(response1, response2):
|
52 |
+
yield c1 or gr.Textbox(), source1, formatted1, c2 or gr.Textbox(), source2, formatted2, *vote_buttons
|
53 |
+
|
54 |
+
vote_buttons = gr.Button(interactive=True), gr.Button(interactive=True), gr.Button(interactive=True), gr.Button(interactive=True)
|
55 |
+
yield c1 or gr.Textbox(), source1, formatted1, c2 or gr.Textbox(), source2, formatted2, *vote_buttons
|
56 |
+
|
57 |
+
def log_interaction(prompt, vote, response1, model1, formatter1, full_prompt1, response2, model2, formatter2, full_prompt2):
|
58 |
+
interaction = {
|
59 |
+
"prompt": prompt,
|
60 |
+
"full_prompt1": full_prompt1,
|
61 |
+
"full_prompt2": full_prompt2,
|
62 |
+
"response1": response1,
|
63 |
+
"response2": response2,
|
64 |
+
"vote": vote,
|
65 |
+
"model1": model1,
|
66 |
+
"formatter1": formatter1,
|
67 |
+
"model2": model2,
|
68 |
+
"formatter2": formatter2,
|
69 |
+
"timestamp": datetime.datetime.now().isoformat()
|
70 |
+
}
|
71 |
+
|
72 |
+
log_file_path = "perf_guru_log.json"
|
73 |
+
if os.path.exists(log_file_path):
|
74 |
+
with open(log_file_path, "r") as log_file:
|
75 |
+
logs = json.load(log_file)
|
76 |
+
else:
|
77 |
+
logs = []
|
78 |
+
|
79 |
+
logs.append(interaction)
|
80 |
+
|
81 |
+
# Write updated logs to file
|
82 |
+
with open(log_file_path, "w") as log_file:
|
83 |
+
json.dump(logs, log_file, indent=4)
|
84 |
+
|
85 |
+
def handle_vote(prompt, vote, response1, source1, full_prompt1, response2, source2, full_prompt2):
|
86 |
+
model1, formatter1 = source1.split(" + ")
|
87 |
+
model2, formatter2 = source2.split(" + ")
|
88 |
+
|
89 |
+
label1_class = "voted" if vote == "Vote for Response 1" else "not-voted"
|
90 |
+
label2_class = "voted" if vote == "Vote for Response 2" else "not-voted"
|
91 |
+
|
92 |
+
log_interaction(prompt, vote, response1, model1, formatter1, full_prompt1, response2, model2, formatter2, full_prompt2)
|
93 |
+
return gr.Markdown(visible=True, elem_classes=[label1_class]), gr.Markdown(visible=True, elem_classes=[label2_class]), \
|
94 |
+
gr.Button(interactive=False), gr.Button(interactive=False), gr.Button(interactive=False), gr.Button(interactive=False)
|
95 |
+
|
96 |
+
# Define the Gradio interface
|
97 |
+
with gr.Blocks(css=".not-voted p { color: black; } .voted p { color: green; } .response { padding: 25px; } .response-md { padding: 20px; }") as interface:
|
98 |
+
|
99 |
+
gr.Markdown("""# PerfGuru: Code Performance Chatbot
|
100 |
+
|
101 |
+
Welcome to PerfGuru!
|
102 |
+
|
103 |
+
This is a tool for assisting developers in identifying performance bottlenecks in their code and optimizing them using LLMs.
|
104 |
+
Upload your code files and a performance profile (if available) to get started. Then ask away!
|
105 |
+
This interface is primarily for data collecting and evaluation purposes. You will be presented outputs from two different LLMs and asked to vote on which response you find more helpful.
|
106 |
+
|
107 |
+
---""")
|
108 |
+
|
109 |
+
gr.Markdown("""## Upload Code Files and Performance Profile
|
110 |
+
|
111 |
+
You must upload at least one source code file to proceed. You can also upload a performance profile if you have one.
|
112 |
+
Currently supported formats are HPCToolkit, CProfile, and Caliper.""")
|
113 |
+
with gr.Row():
|
114 |
+
code_files = gr.File(label="Upload Code File", file_count='multiple')
|
115 |
+
|
116 |
+
with gr.Column():
|
117 |
+
profile_type = gr.Dropdown(['No Profile', 'HPCToolkit', 'CProfile', "Caliper"], value='No Profile', multiselect=False, label="Select Profile Type")
|
118 |
+
profile_file = gr.File(label="Upload Performance Profile")
|
119 |
+
|
120 |
+
gr.Markdown("---")
|
121 |
+
gr.Markdown("""## Ask a Question
|
122 |
+
|
123 |
+
Now you can ask a question about your code performance and chat with PerfGuru!
|
124 |
+
Once you receive two responses, vote on which one you found more helpful.""")
|
125 |
+
|
126 |
+
default_question = "Can you help me identify and fix performance bugs in this code?"
|
127 |
+
prompt = gr.Textbox(label="Ask a question about your code performance", value=default_question)
|
128 |
+
|
129 |
+
chat_button = gr.Button("Chat with PerfGuru", interactive=False)
|
130 |
+
with gr.Row(equal_height=True):
|
131 |
+
with gr.Column():
|
132 |
+
with gr.Accordion("Response 1", elem_classes=["response"]):
|
133 |
+
response1 = gr.Markdown(label="Response 1", visible=True, elem_classes=["response-md"])
|
134 |
+
source1 = gr.Markdown("", visible=False)
|
135 |
+
full_prompt1 = gr.Textbox("", visible=False)
|
136 |
+
with gr.Column():
|
137 |
+
with gr.Accordion("Response 2", elem_classes=["response"]):
|
138 |
+
response2 = gr.Markdown(label="Response 2", visible=True, elem_classes=["response-md"])
|
139 |
+
source2 = gr.Markdown("", visible=False)
|
140 |
+
full_prompt2 = gr.Textbox("", visible=False)
|
141 |
+
|
142 |
+
# use code_upload to toggle the status of the 'chat_button' based on whether a code file is uploaded or not
|
143 |
+
code_files.change(code_upload, inputs=[code_files], outputs=[chat_button])
|
144 |
+
|
145 |
+
with gr.Row():
|
146 |
+
vote1_button = gr.Button("Vote for Response 1", interactive=False)
|
147 |
+
vote2_button = gr.Button("Vote for Response 2", interactive=False)
|
148 |
+
tie_button = gr.Button("Vote for Tie", interactive=False)
|
149 |
+
skip_button = gr.Button("Skip", interactive=False)
|
150 |
+
|
151 |
+
vote_btns = [vote1_button, vote2_button, tie_button, skip_button]
|
152 |
+
for btn in vote_btns:
|
153 |
+
btn.click(handle_vote, inputs=[prompt, btn, response1, source1, full_prompt1, response2, source2, full_prompt2], outputs=[source1, source2, *vote_btns])
|
154 |
+
|
155 |
+
# final chat button
|
156 |
+
chat_button.click(
|
157 |
+
chat_with_llms,
|
158 |
+
inputs=[prompt, code_files, profile_file, profile_type],
|
159 |
+
outputs=[response1, source1, full_prompt1, response2, source2, full_prompt2, *vote_btns]
|
160 |
+
)
|
161 |
+
|
162 |
+
# Launch the Gradio interface
|
163 |
+
if __name__ == '__main__':
|
164 |
+
interface.launch()
|
src/profiles.py
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
""" Helpers for loading performance profiles and extracting text from them.
|
2 |
+
"""
|
3 |
+
import json
|
4 |
+
import os
|
5 |
+
from typing import Literal, Optional
|
6 |
+
|
7 |
+
import hatchet as ht
|
8 |
+
|
9 |
+
|
10 |
+
class Profile:
|
11 |
+
|
12 |
+
def __init__(self, profile_path: os.PathLike, profile_type: Literal["HPCToolkit", "CProfile", "Caliper"]):
|
13 |
+
self.gf = self._load(profile_path, profile_type)
|
14 |
+
|
15 |
+
def _load(self, profile_path: os.PathLike, profile_type: Literal["HPCToolkit", "CProfile", "Caliper"]) -> ht.GraphFrame:
|
16 |
+
if profile_type == "HPCToolkit":
|
17 |
+
return ht.GraphFrame.from_hpctoolkit(profile_path)
|
18 |
+
elif profile_type == "CProfile":
|
19 |
+
return ht.GraphFrame.from_cprofile(profile_path)
|
20 |
+
elif profile_type == "Caliper":
|
21 |
+
return ht.GraphFrame.from_caliper(profile_path)
|
22 |
+
else:
|
23 |
+
raise ValueError(f"Profile type {profile_type} not supported.")
|
24 |
+
|
25 |
+
def profile_to_tree_str(self) -> str:
|
26 |
+
return self.gf.tree()
|
src/rag.py
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
""" Techniques for formatting the prompts that are passed to the LLMs.
|
2 |
+
These need to handle 2 major tasks:
|
3 |
+
1. Taking a directory of source code and embedding it in the prompt meaningfully (and possibly concatenating it).
|
4 |
+
2. Embedding a performance profile in the prompt if available.
|
5 |
+
"""
|
6 |
+
from abc import ABC, abstractmethod
|
7 |
+
from typing import Optional, List, Mapping
|
8 |
+
from os import PathLike
|
9 |
+
from os.path import basename
|
10 |
+
import random
|
11 |
+
|
12 |
+
from profiles import Profile
|
13 |
+
|
14 |
+
class PerfGuruPromptFormatter(ABC):
|
15 |
+
|
16 |
+
def __init__(self, name: str):
|
17 |
+
self.name = name
|
18 |
+
|
19 |
+
def _read_code_files(self, code_paths: List[PathLike]) -> Mapping[PathLike, str]:
|
20 |
+
code_files = {}
|
21 |
+
for code_path in code_paths:
|
22 |
+
with open(code_path, "r") as file:
|
23 |
+
code_files[code_path] = file.read()
|
24 |
+
return code_files
|
25 |
+
|
26 |
+
def _read_profile(self, profile_path: PathLike, profile_type: str) -> Profile:
|
27 |
+
return Profile(profile_path, profile_type)
|
28 |
+
|
29 |
+
@abstractmethod
|
30 |
+
def format_prompt(self, prompt: str, code_paths: List[PathLike], profile_path: Optional[PathLike] = None, profile_type: Optional[str] = None, error_fn: Optional[callable] = None) -> str:
|
31 |
+
pass
|
32 |
+
|
33 |
+
|
34 |
+
class BasicPromptFormatter(PerfGuruPromptFormatter):
|
35 |
+
|
36 |
+
def __init__(self):
|
37 |
+
super().__init__("basic")
|
38 |
+
|
39 |
+
def format_prompt(self, prompt: str, code_paths: List[PathLike], profile_path: Optional[PathLike] = None, profile_type: Optional[str] = None, error_fn: Optional[callable] = None) -> str:
|
40 |
+
if not code_paths:
|
41 |
+
if error_fn:
|
42 |
+
error_fn("No code files provided. At least one code file must be provided.")
|
43 |
+
return None
|
44 |
+
|
45 |
+
concatenated_code = ""
|
46 |
+
code_file_contents = self._read_code_files(code_paths)
|
47 |
+
for code_path, content in code_file_contents.items():
|
48 |
+
fname = basename(code_path)
|
49 |
+
concatenated_code += f"{fname}:\n{content}\n\n"
|
50 |
+
|
51 |
+
if profile_path:
|
52 |
+
if not profile_type:
|
53 |
+
if error_fn:
|
54 |
+
error_fn("Profile type must be provided if a profile file is provided.")
|
55 |
+
return None
|
56 |
+
|
57 |
+
profile = self._read_profile(profile_path, profile_type)
|
58 |
+
profile_content = profile.profile_to_tree_str()
|
59 |
+
else:
|
60 |
+
profile_content = ""
|
61 |
+
|
62 |
+
return f"Code:\n{concatenated_code}\n\n{profile_type} Profile:\n{profile_content}\n\n{prompt}"
|
63 |
+
|
64 |
+
|
65 |
+
AVAILABLE_FORMATTERS = []
|
66 |
+
AVAILABLE_FORMATTERS.append(BasicPromptFormatter())
|
67 |
+
|
68 |
+
|
69 |
+
def select_random_formatter() -> PerfGuruPromptFormatter:
|
70 |
+
return random.choice(AVAILABLE_FORMATTERS)
|