Spaces:
Sleeping
Sleeping
Initial commit
Browse files- README.md +25 -1
- app.py +167 -0
- requirements.txt +6 -0
- styles.css +83 -0
README.md
CHANGED
@@ -11,4 +11,28 @@ license: mit
|
|
11 |
short_description: Try LLM's in a side by side comparison with prompt levers
|
12 |
---
|
13 |
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
short_description: Try LLM's in a side by side comparison with prompt levers
|
12 |
---
|
13 |
|
14 |
+
# README.md
|
15 |
+
# π§ Prompt Canvas Engine
|
16 |
+
|
17 |
+
Test different prompt phrasings across two LLMs and compare outputs side-by-side.
|
18 |
+
|
19 |
+
|
20 |
+
## Features
|
21 |
+
- Compare any two models supported by OpenRouter
|
22 |
+
- Select or customize prompts
|
23 |
+
- Tune temperature, top_p, penalties, and max_tokens
|
24 |
+
- See outputs side-by-side
|
25 |
+
- Rate model outputs
|
26 |
+
- Share publicly
|
27 |
+
|
28 |
+
|
29 |
+
## π Run locally
|
30 |
+
```bash
|
31 |
+
export OPENROUTER_API_KEY=your_key_here
|
32 |
+
python app.py
|
33 |
+
```
|
34 |
+
|
35 |
+
## π€ Deploy on Hugging Face
|
36 |
+
- Create a new **Gradio Space**
|
37 |
+
- Upload `app.py`, `requirements.txt`, and `README.md`
|
38 |
+
- Set `OPENROUTER_API_KEY` in Space secrets
|
app.py
ADDED
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import requests
|
3 |
+
import os
|
4 |
+
from dotenv import load_dotenv
|
5 |
+
|
6 |
+
load_dotenv()
|
7 |
+
|
8 |
+
API_KEY = os.getenv("OPENROUTER_API_KEY")
|
9 |
+
API_URL = "https://openrouter.ai/api/v1/chat/completions"
|
10 |
+
|
11 |
+
MODEL_METADATA = {
|
12 |
+
"mistralai/mistral-7b-instruct": "β‘ Free (Very Fast)",
|
13 |
+
"meta-llama/llama-3-70b-instruct": "β‘ Free (Very Accurate)",
|
14 |
+
"google/gemini-2.0-flash-exp:free": "β‘ Free (Fast, Token Efficient)",
|
15 |
+
|
16 |
+
"huggingfaceh4/zephyr-7b-beta": "πͺ Token-efficient (Compact, Responsive)",
|
17 |
+
"nousresearch/nous-capybara-7b": "πͺ Token-efficient (Lightweight)",
|
18 |
+
"meta-llama/llama-3-8b-instruct": "πͺ Token-efficient (Fast, Balanced)",
|
19 |
+
|
20 |
+
"openai/gpt-3.5-turbo": "π° Paid (Fast, Cheap)",
|
21 |
+
"openai/gpt-4": "π° Paid (Accurate, Expensive)",
|
22 |
+
"anthropic/claude-3-haiku": "π° Paid (Long Context, Fast)",
|
23 |
+
"cohere/command-r-plus": "π° Paid (Context-Aware)"
|
24 |
+
}
|
25 |
+
|
26 |
+
MODEL_OPTIONS = list(MODEL_METADATA.keys())
|
27 |
+
FREE_MODELS = [m for m in MODEL_OPTIONS if "β‘ Free" in MODEL_METADATA[m]]
|
28 |
+
|
29 |
+
def get_default_model2(model1):
|
30 |
+
for m in FREE_MODELS:
|
31 |
+
if m != model1:
|
32 |
+
return f"{m} {MODEL_METADATA[m]}"
|
33 |
+
return f"{FREE_MODELS[0]} {MODEL_METADATA[FREE_MODELS[0]]}"
|
34 |
+
|
35 |
+
rating_store = []
|
36 |
+
|
37 |
+
def run_dual_model(prompt, model1, model2, temperature, top_p, max_tokens, freq_penalty, pres_penalty):
|
38 |
+
outputs = []
|
39 |
+
for model in [model1, model2]:
|
40 |
+
data = {
|
41 |
+
"model": model,
|
42 |
+
"messages": [
|
43 |
+
{"role": "system", "content": "You are a helpful assistant."},
|
44 |
+
{"role": "user", "content": prompt}
|
45 |
+
],
|
46 |
+
"temperature": temperature,
|
47 |
+
"top_p": top_p,
|
48 |
+
"max_tokens": max_tokens,
|
49 |
+
"frequency_penalty": freq_penalty,
|
50 |
+
"presence_penalty": pres_penalty
|
51 |
+
}
|
52 |
+
headers = {
|
53 |
+
"Authorization": f"Bearer {API_KEY}",
|
54 |
+
"Content-Type": "application/json"
|
55 |
+
}
|
56 |
+
try:
|
57 |
+
resp = requests.post(API_URL, headers=headers, json=data)
|
58 |
+
resp_data = resp.json()
|
59 |
+
if "choices" not in resp_data:
|
60 |
+
outputs.append(f"API Error: {resp_data.get('error', 'No choices')}")
|
61 |
+
else:
|
62 |
+
outputs.append(resp_data["choices"][0]["message"]["content"].strip())
|
63 |
+
except Exception as e:
|
64 |
+
outputs.append(f"Error: {e}")
|
65 |
+
return tuple(outputs)
|
66 |
+
|
67 |
+
def record_feedback(prompt_index, rating):
|
68 |
+
rating_store.append((prompt_index, rating))
|
69 |
+
return f"Feedback saved: Output {prompt_index+1} rated {rating}"
|
70 |
+
|
71 |
+
def extract_model(raw_label):
|
72 |
+
return raw_label.split(" ")[0]
|
73 |
+
|
74 |
+
def build_combined_prompt(task, user_text):
|
75 |
+
return f"{task}\n\n{user_text}"
|
76 |
+
|
77 |
+
def update_model2_on_model1_change(m1_val):
|
78 |
+
model_key = extract_model(m1_val)
|
79 |
+
return get_default_model2(model_key)
|
80 |
+
|
81 |
+
# Load CSS from external file
|
82 |
+
def load_css():
|
83 |
+
try:
|
84 |
+
with open('styles.css', 'r') as f:
|
85 |
+
return f.read()
|
86 |
+
except FileNotFoundError:
|
87 |
+
print("Warning: styles.css not found. Using default styling.")
|
88 |
+
return ""
|
89 |
+
|
90 |
+
css = load_css()
|
91 |
+
|
92 |
+
with gr.Blocks(css=css, title="Prompt Canvas Engine") as demo:
|
93 |
+
gr.Markdown("# π§ Prompt Canvas Lab\n## **Compare prompt effectiveness across two language models.**")
|
94 |
+
|
95 |
+
with gr.Row():
|
96 |
+
model1 = gr.Dropdown(label="Model 1", choices=[f"{k} {MODEL_METADATA[k]}" for k in MODEL_OPTIONS], value="mistralai/mistral-7b-instruct β‘ Free (Very Fast)")
|
97 |
+
model2 = gr.Dropdown(label="Model 2", choices=[f"{k} {MODEL_METADATA[k]}" for k in MODEL_OPTIONS], value="meta-llama/llama-3-70b-instruct β‘ Free (Very Accurate)")
|
98 |
+
|
99 |
+
with gr.Row():
|
100 |
+
prompt_selector = gr.Dropdown(label="Prompt", choices=[
|
101 |
+
"Summarize the paragraph in 3 points.",
|
102 |
+
"Translate this sentence to French.",
|
103 |
+
"Write a one-sentence TL;DR.",
|
104 |
+
"Fix grammar and spelling mistakes.",
|
105 |
+
"Extract keywords from the text.",
|
106 |
+
"Convert this bullet list into a paragraph.",
|
107 |
+
"Write a short product description for SEO."
|
108 |
+
], value="Summarize the paragraph in 3 points.", interactive=True, scale=1)
|
109 |
+
|
110 |
+
user_input = gr.Textbox(label="Input Text", value="A qubitβshort for quantum bitβis the fundamental unit of quantum information. Unlike classical bits that exist strictly as 0 or 1, a qubit can exist in a superposition of both states simultaneously, thanks to the principles of quantum mechanics. This allows quantum computers to process complex problems with exponentially greater efficiency.", lines=3, max_lines=3, show_copy_button=True)
|
111 |
+
|
112 |
+
# Parameter controls section
|
113 |
+
with gr.Row():
|
114 |
+
gr.Markdown("## **Parameter Guide**")
|
115 |
+
|
116 |
+
with gr.Row(elem_classes="params-row"):
|
117 |
+
with gr.Column(elem_classes="param-sliders"):
|
118 |
+
temperature = gr.Slider(0.0, 1.0, value=0.7, step=0.1, label="Temperature: Controls randomness (higher = more creative)", interactive=True)
|
119 |
+
top_p = gr.Slider(0.1, 1.0, value=1.0, step=0.1, label="Top-p: Nucleus sampling (higher = more variety)", interactive=True)
|
120 |
+
max_tokens = gr.Slider(32, 1024, value=256, step=32, label="Max Tokens: Limits response length", interactive=True)
|
121 |
+
freq_penalty = gr.Slider(-2.0, 2.0, value=0.0, step=0.1, label="Frequency Penalty: Reduces repetition", interactive=True)
|
122 |
+
pres_penalty = gr.Slider(-2.0, 2.0, value=0.0, step=0.1, label="Presence Penalty: Encourages topic diversity", interactive=True)
|
123 |
+
|
124 |
+
run_btn = gr.Button("Run", elem_classes="run-btn")
|
125 |
+
|
126 |
+
with gr.Row():
|
127 |
+
out1 = gr.Textbox(label="Model 1 Output", lines=6)
|
128 |
+
out2 = gr.Textbox(label="Model 2 Output", lines=6)
|
129 |
+
|
130 |
+
with gr.Row(elem_classes="rating-row"):
|
131 |
+
rating1 = gr.Radio(choices=["π", "π"], label="Rate Model 1")
|
132 |
+
rating2 = gr.Radio(choices=["π", "π"], label="Rate Model 2")
|
133 |
+
|
134 |
+
feedback_msg = gr.Textbox(visible=False)
|
135 |
+
|
136 |
+
run_btn.click(
|
137 |
+
fn=lambda task, user_text, m1, m2, t, p, mt, fp, pp: run_dual_model(
|
138 |
+
build_combined_prompt(task, user_text),
|
139 |
+
extract_model(m1),
|
140 |
+
extract_model(m2),
|
141 |
+
t, p, mt, fp, pp),
|
142 |
+
inputs=[prompt_selector, user_input, model1, model2, temperature, top_p, max_tokens, freq_penalty, pres_penalty],
|
143 |
+
outputs=[out1, out2]
|
144 |
+
)
|
145 |
+
|
146 |
+
model1.change(update_model2_on_model1_change, inputs=model1, outputs=model2)
|
147 |
+
|
148 |
+
rating1.change(fn=record_feedback, inputs=[gr.State(0), rating1], outputs=feedback_msg)
|
149 |
+
rating2.change(fn=record_feedback, inputs=[gr.State(1), rating2], outputs=feedback_msg)
|
150 |
+
|
151 |
+
# Add shareable link section
|
152 |
+
with gr.Row():
|
153 |
+
gr.Markdown("---")
|
154 |
+
|
155 |
+
with gr.Row():
|
156 |
+
gr.Markdown("## **Share this workspace:** ")
|
157 |
+
share_link = gr.Textbox(
|
158 |
+
value="https://your-app-url.com",
|
159 |
+
label="",
|
160 |
+
interactive=False,
|
161 |
+
show_copy_button=True,
|
162 |
+
scale=4
|
163 |
+
)
|
164 |
+
gr.Markdown("*Copy this link to collaborate with your team on prompt optimization.*")
|
165 |
+
|
166 |
+
if __name__ == "__main__":
|
167 |
+
demo.launch(share=True) # This will generate a shareable link
|
requirements.txt
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# requirements.txt
|
2 |
+
gradio
|
3 |
+
requests
|
4 |
+
python-dotenv
|
5 |
+
openai
|
6 |
+
|
styles.css
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
body {
|
2 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
3 |
+
background: #f4f7fb;
|
4 |
+
}
|
5 |
+
|
6 |
+
h1, h2, label {
|
7 |
+
color: #1f2937;
|
8 |
+
font-weight: bold;
|
9 |
+
}
|
10 |
+
|
11 |
+
/* Make all form labels bold */
|
12 |
+
.gr-form label,
|
13 |
+
.gr-dropdown label,
|
14 |
+
.gr-textbox label,
|
15 |
+
.gr-slider label,
|
16 |
+
.gr-radio label {
|
17 |
+
font-weight: bold !important;
|
18 |
+
}
|
19 |
+
|
20 |
+
.gr-button {
|
21 |
+
background: linear-gradient(90deg, #4f46e5 0%, #3b82f6 100%) !important;
|
22 |
+
color: white !important;
|
23 |
+
font-weight: 600;
|
24 |
+
border-radius: 8px;
|
25 |
+
border: none;
|
26 |
+
padding: 10px 24px;
|
27 |
+
}
|
28 |
+
|
29 |
+
.run-btn {
|
30 |
+
background: #fa8072 !important;
|
31 |
+
color: white !important;
|
32 |
+
font-weight: 600;
|
33 |
+
border-radius: 8px;
|
34 |
+
border: none;
|
35 |
+
padding: 12px 32px;
|
36 |
+
font-size: 16px;
|
37 |
+
}
|
38 |
+
|
39 |
+
.gr-radio input:checked + span {
|
40 |
+
color: #2563eb !important;
|
41 |
+
font-weight: 700;
|
42 |
+
}
|
43 |
+
|
44 |
+
.gr-textbox {
|
45 |
+
background: #fff;
|
46 |
+
border: 1px solid #d1d5db;
|
47 |
+
border-radius: 6px;
|
48 |
+
max-height: 160px;
|
49 |
+
overflow-y: auto;
|
50 |
+
resize: none;
|
51 |
+
}
|
52 |
+
|
53 |
+
.gr-dropdown {
|
54 |
+
border-radius: 6px;
|
55 |
+
}
|
56 |
+
|
57 |
+
.gr-slider {
|
58 |
+
color: #2563eb;
|
59 |
+
}
|
60 |
+
|
61 |
+
.rating-row {
|
62 |
+
margin-top: 10px;
|
63 |
+
}
|
64 |
+
|
65 |
+
.params-row {
|
66 |
+
display: flex;
|
67 |
+
align-items: flex-start;
|
68 |
+
gap: 24px;
|
69 |
+
}
|
70 |
+
|
71 |
+
.param-sliders {
|
72 |
+
flex: 3;
|
73 |
+
}
|
74 |
+
|
75 |
+
.param-desc {
|
76 |
+
flex: 2;
|
77 |
+
background: #e0e7ff;
|
78 |
+
padding: 12px;
|
79 |
+
border-radius: 8px;
|
80 |
+
color: #3730a3;
|
81 |
+
font-size: 14px;
|
82 |
+
line-height: 1.4;
|
83 |
+
}
|