Daniel Nichols commited on
Commit
8ab167c
0 Parent(s):

initial commit

Browse files
Files changed (6) hide show
  1. .gitignore +5 -0
  2. requirements.txt +4 -0
  3. src/models.py +99 -0
  4. src/perfguru.py +164 -0
  5. src/profiles.py +26 -0
  6. 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)