PeterKruger commited on
Commit
e155a3d
·
verified ·
1 Parent(s): 646762c

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +931 -0
app.py ADDED
@@ -0,0 +1,931 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ python
2
+ import streamlit as st
3
+ import os
4
+ from huggingface_hub import InferenceClient
5
+ import numpy as np
6
+ import pandas as pd
7
+ import concurrent.futures
8
+ import re
9
+ import time
10
+ import random
11
+ import functools
12
+
13
+ # FILES
14
+ iteration_output_file = "llm_benchmark_iteration_results.csv" # File to store iteration results, defined as global
15
+ results_file = "llm_benchmark_results.csv" # all data
16
+
17
+ # GLOBAL PARAMETERS
18
+ time_sleep = 0.2 # take time before making a new request
19
+ base_temp = 0.2 # base temperature for models
20
+
21
+ # QUESTION GLOBAL PARAMETERS
22
+ question_temp = 0.7 # question generation temperature
23
+ question_max_tokens = 256 # question generation max tokens
24
+ question_treshold = 4.3 # min average rank for questions to be accepted
25
+ reject_rank = 3 # all question ranks must be above
26
+
27
+ # ANSWER GLOBAL PARAMETERS
28
+ answer_temp = 0.5 # base answering temperature
29
+ long_temp = 1.0 # answering temperature for creative questions
30
+ answer_max_tokens = 1048 # max tokens per answer
31
+ long_max_tokens = 2048 # max tokens per creative answer
32
+
33
+ # --- Difficulty probabilities ---
34
+ difficulty_probabilities = {
35
+ "a very simple": 0.0,
36
+ "a simple": 0.0,
37
+ "a": 0.1, # average
38
+ "a difficult": 0.3,
39
+ "a very difficult": 0.6
40
+ }
41
+
42
+ def retry_api_request(max_retries=3, wait_time=10):
43
+ """Decorator for retrying API requests with rate limit handling."""
44
+ def decorator(func):
45
+ @functools.wraps(func)
46
+ def wrapper(*args, **kwargs):
47
+ retries = 0
48
+ while retries <= max_retries:
49
+ try:
50
+ return func(*args, **kwargs)
51
+ except Exception as e:
52
+ print(f"API error: {e}")
53
+ if retries < max_retries:
54
+ print(f"Waiting for {wait_time} seconds before retrying... (Retry {retries + 1}/{max_retries})")
55
+ time.sleep(wait_time)
56
+ retries += 1
57
+ else:
58
+ print(f"Max retries reached. Request failed.")
59
+ return None
60
+
61
+ return None
62
+
63
+ return wrapper
64
+ return decorator
65
+
66
+ # --- Single model request function for Hugging Face ---
67
+ @retry_api_request()
68
+ def make_hf_request(model_name, messages, temperature, max_tokens):
69
+ """
70
+ Send request to a Hugging Face model using InferenceClient
71
+
72
+ Args:
73
+ model_name: Name of the Hugging Face model
74
+ messages: Messages in the format [{"role": "user", "content": "..."}]
75
+ temperature: Temperature parameter for generation
76
+ max_tokens: Maximum tokens to generate
77
+
78
+ Returns:
79
+ Generated text or None if request fails
80
+ """
81
+ client = InferenceClient(model=model_name)
82
+
83
+ # Convert messages to a prompt string
84
+ prompt = ""
85
+ for msg in messages:
86
+ if msg["role"] == "user":
87
+ prompt += f"User: {msg['content']}\n\n"
88
+ elif msg["role"] == "assistant":
89
+ prompt += f"Assistant: {msg['content']}\n\n"
90
+
91
+ if not prompt.endswith("Assistant: "):
92
+ prompt += "Assistant: "
93
+
94
+ try:
95
+ response = client.text_generation(
96
+ prompt,
97
+ max_new_tokens=max_tokens,
98
+ temperature=temperature,
99
+ do_sample=True
100
+ )
101
+ return response
102
+ except Exception as e:
103
+ print(f"Hugging Face Inference API error: {e}")
104
+ return None
105
+
106
+ # --- Prompting Functions ---
107
+ def generate_question_prompt(topic, difficulty):
108
+ # 1. Base Instructions with Difficulty and Topic Clarity
109
+ base_instructions = [
110
+ f"Generate {difficulty} question on the following topic: {topic}.",
111
+ f"Formulate {difficulty} question regarding the following topic: {topic}.",
112
+ f"Create {difficulty} question about the following topic: {topic}.",
113
+ f"Compose {difficulty} question on the following topic: {topic}.",
114
+ f"Develop {difficulty} question that explores the following topic: {topic}."
115
+ ]
116
+
117
+ # 2. Difficulty Options and Instructions
118
+ difficulty_instructions = {
119
+ "a very simple": [
120
+ "The question should test basic, widely known facts.",
121
+ "It should be answerable with common knowledge.",
122
+ "Focus on simple recall and recognition.",
123
+ "The answer is immediately obvious to someone with basic knowledge."
124
+ ],
125
+ "a simple": [
126
+ "The question should require recall of specific information.",
127
+ "It should test knowledge of fundamental concepts.",
128
+ "The answer can be found in introductory materials.",
129
+ "No complex reasoning or deep analysis is needed."
130
+ ],
131
+ "a": [ # For "average" difficulty - no specific instructions needed beyond base
132
+ "The question should be moderately challenging.",
133
+ "It should require some basic reasoning or inference.",
134
+ "The answer may require connecting two or three pieces of information.",
135
+ "It should test understanding beyond simple memorization."
136
+ ],
137
+ "a difficult": [
138
+ "The question should require analytical thinking and application of knowledge.",
139
+ "It should go beyond simple facts and require interpretation.",
140
+ "The answer may involve multiple steps or perspectives.",
141
+ "It should test deeper comprehension and problem-solving skills."
142
+ ],
143
+ "a very difficult": [
144
+ "The question should require expert-level knowledge and critical analysis.",
145
+ "It should involve complex reasoning and nuanced understanding.",
146
+ "The answer may require synthesis of information from various sources.",
147
+ "It should be challenging even for someone knowledgeable in the field."
148
+ ],
149
+ }
150
+
151
+ difficulty_instructions_creative_writing = {
152
+ "a very simple": [
153
+ "The task should be very easy to complete, requiring minimal creativity or effort.",
154
+ "Focus on simple, straightforward writing."
155
+ ],
156
+ "a simple": [
157
+ "The task should require some imagination, but remain relatively easy.",
158
+ "Focus on basic storytelling or poetic elements."
159
+ ],
160
+ "a": [
161
+ "The task should be moderately challenging, requiring a good balance of creativity and execution.",
162
+ "Explore more complex ideas or writing styles."
163
+ ],
164
+ "a difficult": [
165
+ "The task should be quite challenging, pushing the boundaries of creativity and writing skill.",
166
+ "Incorporate complex themes, metaphors, or unusual narrative structures."
167
+ ],
168
+ "a very difficult": [
169
+ "The task should be extremely challenging, requiring a high level of originality and mastery of language.",
170
+ "Experiment with unconventional forms, complex symbolism, or profound philosophical concepts."
171
+ ],
172
+ }
173
+
174
+ # --- Topic-Specific Instructions ---
175
+ topic_instructions = {
176
+ "math": [
177
+ "The question should be a mathematical problem.",
178
+ "It should involve calculations or mathematical reasoning.",
179
+ "Formulate a math word problem.",
180
+ "Create a mathematical problem related to a specic field of math study"
181
+ ],
182
+ "logics": [
183
+ "The question should be a logic puzzle or riddle.",
184
+ "It should require deductive or inductive reasoning.",
185
+ "Formulate a logical reasoning problem.",
186
+ "Create a logic puzzle that requires careful analysis."
187
+ ],
188
+ "history": [
189
+ "The question should relate to a specific historical event, period, or figure.",
190
+ "It should require analyzing historical causes and consequences.",
191
+ "Formulate a question about historical interpretation or analysis.",
192
+ "Create a question that requires understanding of historical context."
193
+ ],
194
+ "current news": [
195
+ "The question should pertain to a recent, significant news event.",
196
+ "It should require understanding of current affairs.",
197
+ "Formulate a question about the implications of a current news event.",
198
+ "Create a question that requires analysis of a recent development."
199
+ ],
200
+ "general culture": [
201
+ "The question should relate to general knowledge and cultural awareness.",
202
+ "It should test understanding of common cultural references.",
203
+ "Formulate a question about a well-known cultural phenomenon.",
204
+ "Create a general knowledge question."
205
+ ],
206
+ "science": [
207
+ "Generate a question regarding a scientific concept.",
208
+ "It should test the comprehension of a scientific fact or principle.",
209
+ "Form a question that assesses knowledge in a scientific domain."
210
+ ],
211
+ "technology":[
212
+ "Generate a question regarding a technological concept.",
213
+ "It should test the comprehension of a technological fact or principle.",
214
+ "Form a question that assesses knowledge in a technological domain."
215
+ ],
216
+ "grammar":[
217
+ "Generate a question regarding a gramatical or linguistic concept.",
218
+ "It should test the comprehension of a gramatical or linguistic fact or principle.",
219
+ "Form a question that assesses knowledge in a gramatical or linguistic domain.",
220
+ "Create a question testing the understanding of gramar and linguistic rules."
221
+ ],
222
+ "coding":[
223
+ "Generate a question about a coding concept or algorithm. Suggest also one or more programming languages to address the question.",
224
+ "The question should test understanding of programming principles. If required, suggest also one or more programming languages to address the question.",
225
+ "Formulate a coding problem or question. You may want to suggest also one or more programming languages to address the question.",
226
+ "Create a question that requires knowledge of programming logic. If needed, suggest also one or more programming languages to address the question.",
227
+ "The question should be related to software development or computer science. If required, suggest also one or more programming languages to address the question."
228
+ "The question should be about Python programming.",
229
+ "Formulate a coding problem solvable in Java.",
230
+ "Create a question related to JavaScript concepts."
231
+ "The question should involve algorithm design. Ssuggest also one or more programming languages to address the question.",
232
+ "Formulate a question about data structures. Suggest also one or more programming languages to address the question.",
233
+ "Create a question testing debugging skills.",
234
+ "The question should assess code optimization techniques."
235
+ ],
236
+ "creative writing": [
237
+ "Write a short story (under 3000 characters) that begins with the sentence: 'The old lighthouse keeper saw a light that wasn't his own.'",
238
+ "Compose a poem (under 3000 characters) in the style of haiku, about the feeling of a summer rain.",
239
+ "Write a short story (under 3000 characters), no more than five sentences, about a robot who discovers the meaning of friendship.",
240
+ "Create a humorous anecdote (under 3000 characters) about a cat and a laser pointer.",
241
+ "Write a short story (under 3000 characters) that ends with the phrase: '...and that's how the world changed forever.'",
242
+ "Compose a free verse poem (under 3000 characters) about the loneliness of space travel.",
243
+ "Write a short, poignant story (under 3000 characters) about a lost object found again.",
244
+ "Tell a joke (under 3000 characters) about a programmer and a bug.",
245
+ "Respond to the philosophical question (under 3000 characters): 'If a tree falls in a forest and no one is around to hear it, does it make a sound?' in a creative and thought-provoking way.",
246
+ "Write a very short story (under 3000 characters) about a talking animal.",
247
+ "Imagine you are a grain of sand. Describe your life (under 3000 characters).",
248
+ "Write a short story (under 3000 characters) set in a world where colors don't exist.",
249
+ "Write a poem (under 3000 characters) about the feeling of nostalgia.",
250
+ "Create a short, funny dialogue (under 3000 characters) between two inanimate objects.",
251
+ "Write a flash fiction piece (under 3000 characters) inspired by a random word (e.g., 'serendipity', 'obfuscate', 'ephemeral').",
252
+ "Respond to the following prompt (under 3000 characters) with a creative story: 'You wake up one morning to find you can fly.'",
253
+ "Compose a short story(under 3000 characters), inspired by a piece of classical music",
254
+ "Tell a joke (under 3000 characters) based on a pun.",
255
+ "Write a short description (under 3000 characters) of a dream you had.",
256
+ "Craft a short, suspenseful story (under 3000 characters) that begins: 'The phone rang, but the screen was blank...'",
257
+ ],
258
+ }
259
+
260
+ #add the creative writing specific prompts to the difficulty prompt,
261
+ #if the topic is creative writing
262
+ if topic == "creative writing":
263
+ difficulty_instructions.update(difficulty_instructions_creative_writing)
264
+
265
+ # 4. Guiding Sentence for Question Types
266
+ question_type_intro = "As an example for you, it could be in the form of:"
267
+ question_types = [
268
+ "a comparison question (asking to compare and contrast...).",
269
+ "an analysis question (asking to analyze the relationship between...).",
270
+ "an explanation question (asking to explain the causes of...).",
271
+ "a discussion question (asking to discuss the implications of...).",
272
+ "a significance question (asking about the significance of...).",
273
+ "a cause-and-effect question (like 'How does ... affect ...?').",
274
+ "a difference question (like 'What are the key differences between ... and ...?').",
275
+ "a hypothetical question (like 'What would be the consequences of ...?').", # Counterfactual
276
+ "a scenario-based question (like 'Develop a scenario where...').", #Scenario based
277
+ "a pros and cons question (Provide arguments for and against...')." #pro and cons
278
+ ]
279
+
280
+ # --- Combine Prompts using Random Choices ---
281
+ prompt = random.choice(base_instructions) + "\n"
282
+ prompt += random.choice(difficulty_instructions[difficulty]) + "\n"
283
+
284
+ # Add topic-specific instruction, handling cases where topic might not be defined.
285
+ if topic in topic_instructions:
286
+ prompt += random.choice(topic_instructions[topic]) + "\n"
287
+ else:
288
+ print(f"Warning: No topic_instructions defined for topic '{topic}'")
289
+
290
+ # 5. Conditional Question Types (Not for math, logics, grammar)
291
+ if topic not in ["math", "logics", "grammar", "coding", "creative writing"]:
292
+ prompt += question_type_intro + "\n"
293
+ prompt += random.choice(question_types)
294
+
295
+ prompt += "\n\nIn generating your question, do not show your internal thought process. Make sure to provide as an output only the final complete and consistent formulation of your question\n"
296
+ return prompt
297
+
298
+ def answer_question_prompt(question):
299
+ return f"Answer the question below. Ensure your answer is clear and insightful, relevant to the topic discussed, logical and grammatically sound, and contains only correct information. In generating your answer, do not show your internal thought process. Provide only your final, complete, and supported answer.\n\nQuestion: {question}\n\nAnswer:"
300
+
301
+ def rank_answer_prompt(question, answer, topic):
302
+ prompt = f"""You are an expert evaluator. Rank the following answer to the given question on a scale of 1 to 5, where:
303
+ 1: Not good answer - unclear, irrelevant to the topic, poorly formulated, or with evidently incorrect statements. For creative writing, this also includes being unoriginal, unimaginative, or failing to adhere to the prompt's constraints (including the 3000-character limit).
304
+ 2: Quite good answer - quite clear, reasonably adherent to the topic, reasonably well-formulated, with no incorrect statements. For creative writing, some originality and imagination are present, but it may be somewhat predictable or have minor flaws. Adheres to the 3000-character limit.
305
+ 3: Good answer - clear, relevant to the topic, well-formulated, with correct statements. For creative writing, this includes demonstrating good originality, imagination, and adherence to the prompt, including the 3000-character limit.
306
+ 4: Very good answer - very clear, very relevant to the topic, expertly formulated, with highly correct statements. For creative writing, shows strong originality, a compelling narrative or poetic voice, and excellent adherence to the prompt, including the 3000-character limit.
307
+ 5: Exceptionally good answer - only appliable to exceptional answers that match all the criteria of the previous "4: Very good answer", but also bring additional unique insights, perfectly sound original arguments, or other exceptional unexpected contributions to the topic. For creative writing, this indicates a truly outstanding piece of writing with exceptional creativity, emotional resonance, and masterful execution, while adhering to the 3000-character limit.
308
+
309
+ Consider these criteria in your ranking:
310
+ - Clarity: Is the answer easy to understand? Is it ambiguous or confusing?
311
+ - Relevance: Is the answer relevant to the specified topic?
312
+ - Formulation: Is the answer well-structured and grammatically correct? Is it logically sound? Is it in a form that proovs expert knowledge?
313
+ - Correctness: Are the statements in the answer correct? (this is extremely relevant for topics such as math, grammar, logics, coding, science, technology)
314
+ - Interest/Engagement: Is the answer likely to be engaging or thought-provoking? (minor consideration)
315
+ """
316
+
317
+ if topic == "creative writing": # More robust topic check
318
+ prompt += """
319
+ - (For Creative Writing ONLY): Originality: Is the writing original and imaginative? Does it avoid clichés?
320
+ - (For Creative Writing ONLY): Emotional Resonance: Does the writing evoke emotion or connect with the reader on an emotional level?
321
+ - (For Creative Writing ONLY): Adherence to Prompt: Does the writing fully address the specific requirements of the creative writing prompt?
322
+ - (For Creative Writing ONLY): Character Limit: Does the writing adhere to the 3000-character limit?
323
+ """
324
+
325
+ prompt += f"""
326
+ Just return a single number (the rank from 1 to 5), do not add any other text.
327
+
328
+ Question: {question}
329
+ Answer: {answer}
330
+ Rank:"""
331
+ return prompt
332
+
333
+ def rank_question_prompt(question, topic, difficulty):
334
+ difficulty_mapping_rank_prompt = {
335
+ "a very simple": "very simple",
336
+ "a simple": "simple",
337
+ "a": "average",
338
+ "a difficult": "difficult",
339
+ "a very difficult": "very difficult"
340
+ }
341
+ difficulty_for_prompt = difficulty_mapping_rank_prompt[difficulty]
342
+
343
+ prompt = f"""You are an expert evaluator of questions. Rank the quality of the following question on a scale of 1 to 5, where:
344
+ 1: Very poor question - unclear, irrelevant to the topic, not appropriate for the difficulty level, or poorly formulated. For creative writing prompts, this also means the prompt is uninspired, lacks clear instructions, or sets an unreasonable character limit.
345
+ 2: Poor question - somewhat unclear, loosely related to the topic, slightly inappropriate for the difficulty level, or with minor formulation issues. For creative writing, the prompt may be somewhat unimaginative or have minor clarity issues.
346
+ 3: Good question - clear, relevant to the topic, generally appropriate for the difficulty level, and reasonably well-formulated. For creative writing, the prompt is clear, provides a reasonable starting point for creative work, and sets a clear 3000-character limit.
347
+ 4: Very good question - clear, highly relevant to the topic, appropriate for the difficulty level, and well-formulated. For creative writing, the prompt is engaging, sparks imagination, and offers a good balance of direction and freedom, with a clear 3000-character limit.
348
+ 5: Excellent question - exceptionally clear, insightful, highly relevant to the topic, perfectly matched to the difficulty level, and expertly formulated. For creative writing, the prompt is exceptionally creative, thought-provoking, and likely to inspire high-quality writing, with a clear 3000-character limit.
349
+
350
+ Consider these criteria in your ranking:
351
+ - Clarity: Is the question easy to understand? Is it ambiguous or confusing?
352
+ - Relevance: Is the question relevant to the specified topic ({topic})?
353
+ - Difficulty: Is the difficulty of the question appropriate for the indicated level ({difficulty_for_prompt})?
354
+ - Formulation: Is the question well-structured and grammatically correct? Is it logically sound?
355
+ - Interest/Engagement: Is the question likely to be engaging or thought-provoking? (minor consideration)
356
+ """
357
+ if topic == "creative writing":
358
+ prompt += f"""
359
+ - **(For Creative Writing ONLY): Creativity:** Does the prompt encourage original and imaginative responses?
360
+ - **(For Creative Writing ONLY): Clarity of Constraints:** Are the creative constraints (e.g., story, poem, joke) and the 3000-character limit clearly stated?
361
+ - **(For Creative Writing ONLY): Inspiration Potential:** Is the prompt likely to inspire high-quality, creative writing?
362
+ """
363
+ prompt += f"""
364
+ Just return a single number (the rank from 1 to 5), do not add any other text.
365
+
366
+ Question: {question}
367
+ Rank:"""
368
+ return prompt
369
+
370
+ # --- Helper function to parse rank strings ---
371
+ def parse_rank_string(rank_str, ranking_model_id):
372
+ match = re.search(r'^\D*(\d+)', rank_str) # Regex to find the first integer
373
+ if match:
374
+ rank_str = match.group(1) # Extract the first captured group (the integer)
375
+ try:
376
+ rank_val = int(rank_str) # Convert to integer *after* regex extraction
377
+ if not 1 <= rank_val <= 5: # Check if rank is within valid range
378
+ print(f"Warning: Model {ranking_model_id} returned rank outside of valid range [1-5]: {rank_val}. Rank set to None.")
379
+ return None
380
+ return rank_val
381
+ except ValueError:
382
+ print(f"Warning: Model {ranking_model_id} returned non-integer rank after regex extraction: '{rank_str}'. Rank set to None.")
383
+ return None
384
+ else:
385
+ print(f"Warning: Model {ranking_model_id} returned non-numeric rank: '{rank_str}'. Rank set to None.")
386
+ return None
387
+
388
+ # --- Helper Function for Parallel Ranking ---
389
+ def get_rank_from_model(ranking_model_id, question, answer, consecutive_failures, failure_threshold, unresponsive_models, model_config, topic, timeout=60):
390
+ start_time = time.time()
391
+ rank = None # Initialize rank to None, indicating potential failure
392
+
393
+ rank_prompt = rank_answer_prompt(question, answer, topic)
394
+
395
+ try:
396
+ response = make_hf_request(model_config[ranking_model_id]["name"], [{"role": "user", "content": rank_prompt}], base_temp, 5)
397
+ if response:
398
+ try:
399
+ rank_str = response.strip()
400
+ rank = parse_rank_string(rank_str, ranking_model_id)
401
+ except ValueError:
402
+ print(f"Warning: Model {ranking_model_id} returned non-integer rank: '{rank_str}'. Rank set to None.")
403
+ rank = None
404
+ else:
405
+ print(f"Warning: Model {ranking_model_id} failed to provide rank. Rank set to None.")
406
+ except Exception as e:
407
+ duration = time.time() - start_time
408
+ print(f"Warning: Model {ranking_model_id} ranking timed out or failed after {duration:.2f}s: {e}")
409
+ rank = None
410
+
411
+ duration = time.time() - start_time # Calculate total duration of ranking attempt
412
+ if duration > timeout:
413
+ print(f"Warning: Ranking by model {ranking_model_id} exceeded timeout of {timeout:.2f}s and took {duration:.2f}s.")
414
+ rank = None # Ensure rank is None if timeout occurs
415
+
416
+ time.sleep(time_sleep) # Keep a small delay to avoid overwhelming APIs even in parallel
417
+ return ranking_model_id, rank
418
+
419
+ # --- Helper Function for Parallel Ranking of questions ---
420
+ def get_question_rank_from_model(ranking_model_id, question, topic, difficulty, consecutive_failures, failure_threshold, unresponsive_models, model_config, timeout=60):
421
+ start_time = time.time()
422
+ rank = None # Initialize rank to None, indicating potential failure
423
+
424
+ rank_prompt = rank_question_prompt(question, topic, difficulty) # Use question rank prompt
425
+
426
+ try:
427
+ response = make_hf_request(model_config[ranking_model_id]["name"], [{"role": "user", "content": rank_prompt}], base_temp, 5)
428
+ if response:
429
+ try:
430
+ rank_str = response.strip()
431
+ rank = parse_rank_string(rank_str, ranking_model_id)
432
+ except ValueError:
433
+ print(f"Warning: Model {ranking_model_id} returned non-integer rank for question: '{rank_str}'. Rank set to None.")
434
+ rank = None
435
+ else:
436
+ print(f"Warning: Model {ranking_model_id} failed to provide rank for question. Rank set to None.")
437
+ except Exception as e:
438
+ duration = time.time() - start_time
439
+ print(f"Warning: Model {ranking_model_id} ranking question timed out or failed after {duration:.2f}s: {e}")
440
+ rank = None
441
+
442
+ duration = time.time() - start_time # Calculate total duration of ranking attempt
443
+ if duration > timeout:
444
+ print(f"Warning: Ranking question by model {ranking_model_id} exceeded timeout of {timeout:.2f}s and took {duration:.2f}s.")
445
+ rank = None # Ensure rank is None if timeout occurs
446
+
447
+ time.sleep(time_sleep) # Keep a small delay to avoid overwhelming APIs even in parallel
448
+ return ranking_model_id, rank
449
+
450
+ # --- Helper Function for Parallel Answering ---
451
+ def get_answer_from_model(model_id, question, consecutive_failures, failure_threshold, unresponsive_models, model_config, topic, timeout=60):
452
+ start_time = time.time() # Start timer
453
+ answer_prompt = answer_question_prompt(question)
454
+ answer = "Error answering" # Default answer
455
+
456
+ temp = answer_temp
457
+ max_tok = answer_max_tokens
458
+ if topic == "math" or topic == "coding" or topic == "grammar" or topic == "logics":
459
+ temp = long_temp
460
+ max_tok = long_max_tokens
461
+
462
+ try:
463
+ response = make_hf_request(model_config[model_id]["name"], [{"role": "user", "content": answer_prompt}], temp, max_tok)
464
+ if response:
465
+ answer = response.strip()
466
+ except Exception as e:
467
+ duration = time.time() - start_time
468
+ print(f"Warning: Model {model_id} answering timed out or failed after {duration:.2f}s: {e}")
469
+ answer = "Error answering - Timeout" # Or a specific timeout error message
470
+ return answer, duration # Return error answer and duration
471
+
472
+ time.sleep(time_sleep) # Small delay
473
+ duration = time.time() - start_time # Calculate duration
474
+ print(f"Answer generation by \"{model_id}\": {duration:.2f}s") # Print answer generation duration separately as requested - as requested
475
+
476
+ return answer, duration # Return answer and duration
477
+
478
+ # --- Core Logic ---
479
+ def run_benchmark(hf_models, topics, difficulties, t, model_config):
480
+ results = {
481
+ "model_name": [],
482
+ "topic": [],
483
+ "difficulty": [],
484
+ "question_prompt": [],
485
+ "question": [],
486
+ "answer": [],
487
+ "answer_generation_duration": [],
488
+ "average_rank": [],
489
+ "ranks":[],
490
+ "question_rank_average": [],
491
+ "question_ranks": [],
492
+ "question_rank_duration": []
493
+ }
494
+
495
+ cumulative_model_ranks = {} # To store cumulative ranks for each model
496
+
497
+ # Check if iteration output file exists and remove it if it does to start fresh
498
+ if os.path.exists(iteration_output_file):
499
+ os.remove(iteration_output_file)
500
+
501
+ consecutive_failures = {} # Track failures per model ID
502
+ failure_threshold = 5
503
+ unresponsive_models = set()
504
+
505
+ # Updated model lists with more informative labels
506
+ active_models = hf_models.copy()
507
+ model_weights = {}
508
+
509
+ for model_id in active_models:
510
+ cumulative_model_ranks[model_id] = []
511
+ consecutive_failures[model_id] = 0
512
+ model_weights[model_id] = 1.0 / len(active_models) # Initial equal weights
513
+
514
+ difficulty_choices = list(difficulty_probabilities.keys())
515
+ probability_values = list(difficulty_probabilities.values())
516
+
517
+ # --- Difficulty mapping for output labels ---
518
+ difficulty_mapping = {
519
+ "a very simple": "1",
520
+ "a simple": "2",
521
+ "a": "3",
522
+ "a difficult": "4",
523
+ "a very difficult": "5"
524
+ }
525
+
526
+ s_t = 0 #count succesful iterations
527
+
528
+ for iteration in range(t): # Added iteration counter
529
+
530
+ if len(active_models) < 2:
531
+ print("Fewer than 2 active models remaining. Exiting benchmark.")
532
+ break
533
+
534
+ topic = random.choice(topics)
535
+ # --- Select difficulty with probabilities ---
536
+ difficulty = random.choices(difficulty_choices, weights=probability_values, k=1)[0] # Weighted random choice
537
+ print(f"--- Iteration {s_t + 1}/{t}: {difficulty} question ({difficulty_mapping[difficulty]}) on {topic} ---") # Print iteration number
538
+
539
+ # --- Question Generation ---
540
+ question = None
541
+ question_prompt = generate_question_prompt(topic, difficulty)
542
+
543
+ question_accepted = False # Flag to track if question is accepted
544
+ question_ranks_all = []
545
+ question_avg_rank = np.nan
546
+ question_ranking_duration_total = 0
547
+
548
+ cumulative_avg_rank = {} # To store cumulative average ranks for each model
549
+
550
+ max_attempts = 3 * len(active_models)
551
+ for attempt in range(max_attempts):
552
+ # --- Filter for question generation roles ("answer" or "both") ---
553
+ question_gen_candidates = [
554
+ model_id for model_id in active_models
555
+ if model_config[model_id].get("role", "both") in ["answer", "both"]
556
+ ]
557
+ if not question_gen_candidates: # No suitable models left
558
+ print("No models available for question generation with 'answer' or 'both' role. Skipping iteration.")
559
+ continue # Skip to next iteration
560
+
561
+ question_generator_model_id = random.choice(question_gen_candidates)
562
+
563
+ # --- Question Generation ---
564
+ response = make_hf_request(model_config[question_generator_model_id]["name"],
565
+ [{"role": "user", "content": question_prompt}],
566
+ question_temp,
567
+ question_max_tokens)
568
+
569
+ if response:
570
+ question = response.strip()
571
+ consecutive_failures[question_generator_model_id] = 0 # Reset on success
572
+ break
573
+ else:
574
+ print(f"Skipping due to request failure for model {question_generator_model_id}.")
575
+ consecutive_failures[question_generator_model_id] += 1
576
+
577
+ if consecutive_failures[question_generator_model_id] >= failure_threshold:
578
+ print(f"Model {question_generator_model_id} is unresponsive (question gen). Removing from active models.")
579
+ if question_generator_model_id in active_models:
580
+ active_models.remove(question_generator_model_id)
581
+ unresponsive_models.add(question_generator_model_id)
582
+ time.sleep(time_sleep)
583
+
584
+ if question is None:
585
+ print(f"Failed to generate a question after {max_attempts} attempts. Skipping this round.")
586
+ continue
587
+
588
+ # --- Parallel Question Ranking ---
589
+ question_ranks = {}
590
+ question_ranking_futures = []
591
+ question_ranking_start_time = time.time()
592
+
593
+ with concurrent.futures.ThreadPoolExecutor(max_workers=len(active_models) or 1) as executor:
594
+ for ranking_model_id in active_models:
595
+ # --- Filter for ranking roles ("rank" or "both") ---
596
+ if model_config[ranking_model_id].get("role", "both") in ["rank", "both"]:
597
+ future = executor.submit(
598
+ get_question_rank_from_model,
599
+ ranking_model_id,
600
+ question,
601
+ topic,
602
+ difficulty,
603
+ consecutive_failures,
604
+ failure_threshold,
605
+ unresponsive_models,
606
+ model_config,
607
+ timeout=60
608
+ )
609
+ question_ranking_futures.append(future)
610
+
611
+ for future in concurrent.futures.as_completed(question_ranking_futures): # Collect ranks as they become available
612
+ ranking_model_id, rank = future.result() # Get model_id and rank
613
+ question_ranks[ranking_model_id] = rank # Store rank with model_id as key
614
+
615
+ question_ranking_end_time = time.time()
616
+ question_ranking_duration_total = question_ranking_end_time - question_ranking_start_time
617
+
618
+ # Filter out None values (failed ranks) and calculate weighted average
619
+ valid_question_ranks_values = [r for r in question_ranks.values() if r is not None] # Get rank values
620
+ question_avg_rank = np.nan # Default to NaN
621
+
622
+ if valid_question_ranks_values:
623
+ # Create a list of weights corresponding to the valid ranks
624
+ weights_for_valid_question_ranks = [model_weights[model_id]
625
+ for model_id, rank in question_ranks.items()
626
+ if rank is not None]
627
+
628
+ #check that the length is correct
629
+ if len(weights_for_valid_question_ranks) != len(valid_question_ranks_values):
630
+ print("Warning: Mismatch length of weights and valid question ranks")
631
+ print(f'weights_for_valid_question_ranks {weights_for_valid_question_ranks}')
632
+ print(f'valid_question_ranks_values: {valid_question_ranks_values}')
633
+
634
+ question_avg_rank = np.average(valid_question_ranks_values, weights=weights_for_valid_question_ranks)
635
+ min_question_rank = min(valid_question_ranks_values) if valid_question_ranks_values else 0 # To avoid error if no valid rank
636
+
637
+ if question_avg_rank >= question_treshold and all(rank > reject_rank for rank in valid_question_ranks_values): # Question acceptance criteria
638
+ question_accepted = True
639
+ print(f"Question accepted. Avg Question Rank: {question_avg_rank:.2f}, Min Rank: {min_question_rank}, Ranks: {[question_ranks[m] for m in active_models if m in question_ranks]}")
640
+ s_t += 1
641
+ else:
642
+ question_accepted = False
643
+ print(f"Question rejected. Avg Question Rank: {question_avg_rank:.2f}, Min Rank: {min_question_rank}, Ranks: {[question_ranks[m] for m in active_models if m in question_ranks]}")
644
+
645
+ if not question_accepted:
646
+ print("Generated question was not accepted. Regenerating question.")
647
+ continue
648
+
649
+ if len(active_models) < 2:
650
+ print("Fewer than 2 active models remaining. Exiting benchmark.")
651
+ break
652
+
653
+ # --- Parallel Answer Generation ---
654
+ answers = {}
655
+ answer_futures = []
656
+ answer_durations = {}
657
+ with concurrent.futures.ThreadPoolExecutor(max_workers=len(active_models)) as executor:
658
+ for model_id in active_models:
659
+ # --- Filter for answer generation roles ("answer" or "both") ---
660
+ if model_config[model_id].get("role", "both") in ["answer", "both"]:
661
+ try:
662
+ future = executor.submit(
663
+ get_answer_from_model,
664
+ model_id,
665
+ question,
666
+ consecutive_failures,
667
+ failure_threshold,
668
+ unresponsive_models,
669
+ model_config,
670
+ topic,
671
+ timeout=60
672
+ )
673
+ answer_futures.append(future)
674
+ except TimeoutError as e:
675
+ print(f"Answer generation for model {model_id} timed out: {e}")
676
+ answer = "I am struggling to answer this question" # Treat timeout as error
677
+ duration = 120 # You can set a default duration or handle it differently if needed
678
+ answers[model_id] = answer # Store error answer
679
+ answer_durations[model_id] = duration # Store default duration
680
+
681
+ for future in concurrent.futures.as_completed(answer_futures):
682
+ model_id = active_models[answer_futures.index(future)] # Get model_id based on future index (order is preserved)
683
+ answer, duration = future.result() # Get both answer and duration - unpack the returned tuple
684
+ answers[model_id] = answer
685
+ answer_durations[model_id] = duration # Store duration - store the duration in the answer_durations dictionary
686
+
687
+ # --- Ranking Process ---
688
+
689
+ # Prepare to write to file (open in append mode outside the model loop but inside iteration loop)
690
+ iteration_results_file_opened = open(iteration_output_file, 'a')
691
+ if iteration == 0: # Write header only for the first iteration
692
+ iteration_results_file_opened.write("Iteration, Topic, Difficulty, Question Rank, QR Duration, Model,Cumulative Avg Rank,Iteration Avg Rank,Ranks,Ranking Duration (sec)\n") # Added Ranking Duration to header
693
+
694
+
695
+ for model_id in active_models:
696
+ answer = answers[model_id] # Retrieve pre-generated answer
697
+
698
+ if answer == "Error answering": # Handle answer generation errors
699
+ consecutive_failures[model_id] += 1
700
+ if consecutive_failures[model_id] >= failure_threshold:
701
+ print(f"Model {model_id} is consistently failing to answer. Removing from active models.")
702
+ if model_id in active_models: # double check before removing, might have been removed in another thread
703
+ active_models.remove(model_id)
704
+ unresponsive_models.add(model_id)
705
+ continue # Skip ranking if answer generation failed for this model
706
+
707
+
708
+ if len(active_models) < 2: # Re-check active models before ranking
709
+ print("Fewer than 2 active models remaining. Exiting benchmark.")
710
+ break
711
+
712
+ ranks = {}
713
+ ranking_futures = []
714
+
715
+ ranking_start_time = time.time()
716
+ with concurrent.futures.ThreadPoolExecutor(max_workers=len(active_models) or 1) as executor:
717
+ for ranking_model_id in active_models:
718
+ # --- Filter for ranking roles ("rank" or "both") ---
719
+ if model_config[ranking_model_id].get("role", "both") in ["rank", "both"]:
720
+ future = executor.submit(
721
+ get_rank_from_model,
722
+ ranking_model_id,
723
+ question,
724
+ answer,
725
+ consecutive_failures,
726
+ failure_threshold,
727
+ unresponsive_models,
728
+ model_config,
729
+ topic,
730
+ timeout=60
731
+ )
732
+ ranking_futures.append(future)
733
+
734
+ for future in concurrent.futures.as_completed(ranking_futures): # Collect ranks as they become available
735
+ ranking_model_id, rank = future.result() # Get model_id and rank
736
+ ranks[ranking_model_id] = rank # Store rank with model_id as key
737
+
738
+ ranking_end_time = time.time() # Record end time of ranking
739
+ ranking_duration = ranking_end_time - ranking_start_time # Calculate duration
740
+
741
+ # Filter out None values (failed ranks) and calculate weighted average
742
+ valid_ranks_values = [r for r in ranks.values() if r is not None] # Get rank values
743
+ average_rank = np.nan # Default to NaN
744
+
745
+ if valid_ranks_values:
746
+ #Create a list of weights corresponding to the valid ranks
747
+ weights_for_valid_ranks = [model_weights[model_id]
748
+ for model_id, rank in ranks.items()
749
+ if rank is not None]
750
+
751
+
752
+ if len(weights_for_valid_ranks) != len(valid_ranks_values):
753
+ print("Warning: Mismatch length of weights and valid answer ranks")
754
+ print(f'weights_for_valid_ranks {weights_for_valid_ranks}')
755
+ print(f'valid_ranks_values: {valid_ranks_values}')
756
+
757
+ average_rank = np.average(valid_ranks_values, weights=weights_for_valid_ranks)
758
+
759
+ results["model_name"].append(model_id)
760
+ results["topic"].append(topic)
761
+ results["difficulty"].append(difficulty)
762
+ results["question_prompt"].append(question_prompt)
763
+ results["question"].append(question)
764
+ results["answer"].append(answer)
765
+ results["answer_generation_duration"].append(duration)
766
+ results["average_rank"].append(average_rank)
767
+ results["ranks"].append([ranks[m] for m in active_models if m in ranks]) # Store raw ranks including Nones, ensure order
768
+ results["question_rank_average"].append(question_avg_rank) # Store question rank average
769
+ results["question_ranks"].append([question_ranks[m] for m in active_models if m in question_ranks]) # Store question ranks
770
+ results["question_rank_duration"].append(question_ranking_duration_total) # Store question ranking duration
771
+
772
+ cumulative_model_ranks[model_id].append(average_rank) # Append current iteration's average rank
773
+ cumulative_avg_rank[model_id] = np.nanmean(cumulative_model_ranks[model_id]) if cumulative_model_ranks[model_id] else np.nan
774
+
775
+ # --- Print and store iteration results IMMEDIATELY after ranking for this model ---
776
+ ranks_str = "[" + ", ".join(map(str, [ranks[m] for m in active_models if m in ranks])) + "]" if ranks else "[]" # Format ranks for CSV, ensure order
777
+ print(f"{topic}, {difficulty_mapping[difficulty]}, {model_id}, {cumulative_avg_rank[model_id]:.2f}, {average_rank:.5f}, {ranks_str}, {ranking_duration:.2f} sec")
778
+
779
+ # Write iteration results to file (append mode) - write for each model right after ranking
780
+ iteration_results_file_opened.write(f"{iteration+1},{topic}, {difficulty_mapping[difficulty]},{question_avg_rank:.2f},{question_ranking_duration_total:.2f},{model_id},{cumulative_avg_rank[model_id]:.2f},{average_rank:.2f},{ranks_str},{ranking_duration:.2f}\n")
781
+
782
+ # Update model weights based on cumulative average ranks, handling NaNs
783
+ temp_weights = {}
784
+ total_valid_rank = 0 # Keep track of the sum of valid (non-NaN) ranks
785
+
786
+ for m_id in active_models:
787
+ if cumulative_avg_rank[m_id]:
788
+ temp_weights[m_id] = cumulative_avg_rank[m_id]
789
+ total_valid_rank += cumulative_avg_rank[m_id]
790
+ else: # if cumulative is empty, keep original
791
+ temp_weights[m_id] = model_weights.get(m_id, 1.0 / len(active_models))
792
+
793
+ # Normalize the weights so they sum to 1, handling cases where total_valid_rank might be zero
794
+ if total_valid_rank > 0:
795
+ for m_id in temp_weights:
796
+ model_weights[m_id] = temp_weights[m_id] / total_valid_rank
797
+ else:
798
+ # If total_valid_rank is 0 (all models have NaN ranks), revert to equal weights
799
+ for m_id in active_models:
800
+ model_weights[m_id] = 1.0 / len(active_models)
801
+
802
+ iteration_results_file_opened.close()
803
+
804
+ print(f"Unresponsive models during this run: {unresponsive_models}")
805
+ return results, cumulative_avg_rank, s_t
806
+
807
+ # Streamlit UI
808
+ st.title("LLM Benchmark")
809
+
810
+ # Setup sidebar for configuration
811
+ st.sidebar.header("Configuration")
812
+
813
+ # Add a field for the Hugging Face token
814
+ hf_token = st.sidebar.text_input("Hugging Face API Token", type="password",
815
+ help="Your Hugging Face API token to access models")
816
+
817
+ # Model selection
818
+ st.sidebar.subheader("Models")
819
+ available_models = [
820
+ "meta-llama/Llama-3.1-8B-Instruct",
821
+ "meta-llama/Meta-Llama-3-8B-Instruct",
822
+ "google/gemma-2-2b-it",
823
+ "mistralai/Mistral-7B-Instruct-v0.2",
824
+ "microsoft/Phi-3-mini-4k-instruct",
825
+ # Add more models as needed
826
+ ]
827
+
828
+ selected_models = st.sidebar.multiselect(
829
+ "Select models to benchmark",
830
+ available_models,
831
+ default=available_models[:3] # Default to first 3 models
832
+ )
833
+
834
+ # Topic selection
835
+ available_topics = ["math", "logics", "grammar", "coding", "history", "current news",
836
+ "general culture", "science", "technology", "creative writing"]
837
+ selected_topics = st.sidebar.multiselect(
838
+ "Select topics",
839
+ available_topics,
840
+ default=available_topics
841
+ )
842
+
843
+ # Number of iterations
844
+ num_iterations = st.sidebar.slider("Number of iterations", 1, 20, 5)
845
+
846
+ # Create model_config dictionary from selected models
847
+ model_config = {}
848
+ for model in selected_models:
849
+ model_config[model] = {"name": model, "role": "both"}
850
+
851
+ # Start benchmark button
852
+ if st.sidebar.button("Start Benchmark"):
853
+ if not hf_token:
854
+ st.error("Please enter your Hugging Face API token")
855
+ elif not selected_models:
856
+ st.error("Please select at least two models")
857
+ elif not selected_topics:
858
+ st.error("Please select at least one topic")
859
+ else:
860
+ # Create progress bar
861
+ progress_bar = st.progress(0)
862
+ status_text = st.empty()
863
+
864
+ # Setup to capture results for display
865
+ results_container = st.container()
866
+ with results_container:
867
+ results_placeholder = st.empty()
868
+ iterations_table = st.empty()
869
+
870
+ # Create a global variable to store intermediate results
871
+ if 'results_df' not in st.session_state:
872
+ st.session_state.results_df = pd.DataFrame()
873
+
874
+ # Modify make_hf_request to use the token
875
+ def make_hf_request_with_token(model_name, messages, temperature, max_tokens):
876
+ client = InferenceClient(model=model_name, token=hf_token)
877
+ # Rest of the function is the same...
878
+ # Return response
879
+
880
+ # Run the benchmark
881
+ try:
882
+ # Update status
883
+ status_text.text("Benchmark running...")
884
+
885
+ # Run benchmark and get results
886
+ results, cumulative_avg_rank, total_successful = run_benchmark(
887
+ selected_models, selected_topics,
888
+ ["a very simple", "a simple", "a", "a difficult", "a very difficult"],
889
+ num_iterations, model_config
890
+ )
891
+
892
+ # Update progress to complete
893
+ progress_bar.progress(100)
894
+ status_text.text(f"Benchmark completed! {total_successful} successful iterations")
895
+
896
+ # Display results
897
+ if total_successful > 0:
898
+ results_df = pd.DataFrame(results)
899
+ st.session_state.results_df = results_df
900
+
901
+ # Show model rankings
902
+ st.subheader("Model Rankings")
903
+ ranking_df = pd.DataFrame({
904
+ "Model": list(cumulative_avg_rank.keys()),
905
+ "Average Rank": [round(r, 2) for r in cumulative_avg_rank.values()]
906
+ })
907
+ ranking_df = ranking_df.sort_values("Average Rank", ascending=False)
908
+ results_placeholder.dataframe(ranking_df)
909
+
910
+ # Show detailed results
911
+ st.subheader("Detailed Results")
912
+ st.dataframe(results_df)
913
+
914
+ # Option to download results
915
+ csv = results_df.to_csv(index=False)
916
+ st.download_button(
917
+ label="Download Results CSV",
918
+ data=csv,
919
+ file_name="llm_benchmark_results.csv",
920
+ mime="text/csv",
921
+ )
922
+ else:
923
+ st.warning("The benchmark did not complete any successful iterations.")
924
+ except Exception as e:
925
+ st.error(f"An error occurred: {e}")
926
+ st.exception(e)
927
+
928
+ # Show previous results if available
929
+ elif 'results_df' in st.session_state and not st.session_state.results_df.empty:
930
+ st.subheader("Previous Results")
931
+ st.dataframe(st.session_state.results_df)