File size: 54,249 Bytes
838a722
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e5f02a
838a722
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e5f02a
838a722
 
 
 
9e5f02a
838a722
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44fdf84
838a722
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b7fb7d4
 
838a722
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0f4459
aad9e15
838a722
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ead8450
838a722
 
 
 
 
 
 
 
 
 
b7fb7d4
838a722
 
 
 
 
 
 
 
9e5f02a
838a722
 
 
9e5f02a
838a722
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
import streamlit as st
import os
from huggingface_hub import InferenceClient
import numpy as np
import pandas as pd
import concurrent.futures
import re
import time
import random
import functools

# FILES
iteration_output_file = "llm_benchmark_iteration_results.csv"  # File to store iteration results, defined as global
results_file = "llm_benchmark_results.csv"  # all data

# GLOBAL PARAMETERS
time_sleep = 0.2  # take time before making a new request
base_temp = 0.2  # base temperature for models

# QUESTION GLOBAL PARAMETERS
question_temp = 0.7  # question generation temperature
question_max_tokens = 256  # question generation max tokens
question_treshold = 3.5  # min average rank for questions to be accepted
reject_rank = 2  # all question ranks must be above

# ANSWER GLOBAL PARAMETERS
answer_temp = 0.5  # base answering temperature
long_temp = 1.0  # answering temperature for creative questions
answer_max_tokens = 1048  # max tokens per answer
long_max_tokens = 2048  # max tokens per creative answer

# --- Difficulty probabilities ---
difficulty_probabilities = {
    "a very simple": 0.0,
    "a simple": 0.0,
    "a": 0.1,  # average
    "a difficult": 0.3,
    "a very difficult": 0.6
}

def retry_api_request(max_retries=3, wait_time=10):
    """Decorator for retrying API requests with rate limit handling."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            retries = 0
            while retries <= max_retries:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"API error: {e}")
                    if retries < max_retries:
                        print(f"Waiting for {wait_time} seconds before retrying... (Retry {retries + 1}/{max_retries})")
                        time.sleep(wait_time)
                        retries += 1
                    else:
                        print(f"Max retries reached. Request failed.")
                        return None
                
            return None

        return wrapper
    return decorator

# --- Single model request function for Hugging Face ---

@retry_api_request()
def make_hf_request(model_name, messages, temperature, max_tokens, token=None):
    """
    Send request to a Hugging Face model using InferenceClient
    
    Args:
        model_name: Name of the Hugging Face model
        messages: Messages in the format [{"role": "user", "content": "..."}]
        temperature: Temperature parameter for generation
        max_tokens: Maximum tokens to generate
        token: Hugging Face API token
        
    Returns:
        Generated text or None if request fails
    """
    client = InferenceClient(model=model_name, token=token)
    
    # Convert messages to a prompt string
    prompt = ""
    for msg in messages:
        if msg["role"] == "user":
            prompt += f"User: {msg['content']}\n\n"
        elif msg["role"] == "assistant":
            prompt += f"Assistant: {msg['content']}\n\n"
    
    if not prompt.endswith("Assistant: "):
        prompt += "Assistant: "
    
    try:
        response = client.text_generation(
            prompt,
            max_new_tokens=max_tokens,
            temperature=temperature,
            do_sample=True
        )
        return response
    except Exception as e:
        print(f"Hugging Face Inference API error: {e}")
        return None

# --- Prompting Functions ---
def generate_question_prompt(topic, difficulty):
    # 1. Base Instructions with Difficulty and Topic Clarity
    base_instructions = [
        f"Generate {difficulty} question on the following topic: {topic}.",
        f"Formulate {difficulty} question regarding the following topic: {topic}.",
        f"Create {difficulty} question about the following topic: {topic}.",
        f"Compose {difficulty} question on the following topic: {topic}.",
        f"Develop {difficulty} question that explores the following topic: {topic}."
    ]

    # 2. Difficulty Options and Instructions
    difficulty_instructions = {
        "a very simple": [
            "The question should test basic, widely known facts.",
            "It should be answerable with common knowledge.",
            "Focus on simple recall and recognition.",
            "The answer is immediately obvious to someone with basic knowledge."
        ],
        "a simple": [
            "The question should require recall of specific information.",
            "It should test knowledge of fundamental concepts.",
            "The answer can be found in introductory materials.",
            "No complex reasoning or deep analysis is needed."
        ],
        "a": [ # For "average" difficulty - no specific instructions needed beyond base
            "The question should be moderately challenging.",
            "It should require some basic reasoning or inference.",
            "The answer may require connecting two or three pieces of information.",
            "It should test understanding beyond simple memorization."
        ],
        "a difficult": [
            "The question should require analytical thinking and application of knowledge.",
            "It should go beyond simple facts and require interpretation.",
            "The answer may involve multiple steps or perspectives.",
            "It should test deeper comprehension and problem-solving skills."
        ],
        "a very difficult": [
            "The question should require expert-level knowledge and critical analysis.",
            "It should involve complex reasoning and nuanced understanding.",
            "The answer may require synthesis of information from various sources.",
            "It should be challenging even for someone knowledgeable in the field."
        ],
    }

    difficulty_instructions_creative_writing = {
        "a very simple": [
            "The task should be very easy to complete, requiring minimal creativity or effort.",
            "Focus on simple, straightforward writing."
        ],
        "a simple": [
            "The task should require some imagination, but remain relatively easy.",
            "Focus on basic storytelling or poetic elements."
        ],
        "a": [
            "The task should be moderately challenging, requiring a good balance of creativity and execution.",
            "Explore more complex ideas or writing styles."
        ],
        "a difficult": [
            "The task should be quite challenging, pushing the boundaries of creativity and writing skill.",
            "Incorporate complex themes, metaphors, or unusual narrative structures."
        ],
        "a very difficult": [
            "The task should be extremely challenging, requiring a high level of originality and mastery of language.",
            "Experiment with unconventional forms, complex symbolism, or profound philosophical concepts."
        ],
    }

    # --- Topic-Specific Instructions ---
    topic_instructions = {
        "math": [
            "The question should be a mathematical problem.",
            "It should involve calculations or mathematical reasoning.",
            "Formulate a math word problem.",
            "Create a mathematical problem related to a specic field of math study"
        ],
        "logics": [
            "The question should be a logic puzzle or riddle.",
            "It should require deductive or inductive reasoning.",
            "Formulate a logical reasoning problem.",
            "Create a logic puzzle that requires careful analysis."
        ],
        "history": [
            "The question should relate to a specific historical event, period, or figure.",
            "It should require analyzing historical causes and consequences.",
            "Formulate a question about historical interpretation or analysis.",
            "Create a question that requires understanding of historical context."
        ],
        "current news": [
            "The question should pertain to a recent, significant news event.",
            "It should require understanding of current affairs.",
            "Formulate a question about the implications of a current news event.",
            "Create a question that requires analysis of a recent development."
        ],
        "general culture": [
            "The question should relate to general knowledge and cultural awareness.",
            "It should test understanding of common cultural references.",
            "Formulate a question about a well-known cultural phenomenon.",
            "Create a general knowledge question."
        ],
        "science": [
          "Generate a question regarding a scientific concept.",
          "It should test the comprehension of a scientific fact or principle.",
          "Form a question that assesses knowledge in a scientific domain."
        ],
        "technology":[
          "Generate a question regarding a technological concept.",
          "It should test the comprehension of a technological fact or principle.",
          "Form a question that assesses knowledge in a technological domain."
        ],
        "grammar":[
          "Generate a question regarding a gramatical or linguistic concept.",
          "It should test the comprehension of a gramatical or linguistic fact or principle.",
          "Form a question that assesses knowledge in a gramatical or linguistic domain.",
          "Create a question testing the understanding of gramar and linguistic rules."
        ],
        "coding":[
          "Generate a question about a coding concept or algorithm. Suggest also one or more programming languages to address the question.",
          "The question should test understanding of programming principles. If required, suggest also one or more programming languages to address the question.",
          "Formulate a coding problem or question. You may want to suggest also one or more programming languages to address the question.",
          "Create a question that requires knowledge of programming logic. If needed, suggest also one or more programming languages to address the question.",
          "The question should be related to software development or computer science. If required, suggest also one or more programming languages to address the question."
          "The question should be about Python programming.",
          "Formulate a coding problem solvable in Java.",
          "Create a question related to JavaScript concepts."
          "The question should involve algorithm design. Ssuggest also one or more programming languages to address the question.",
          "Formulate a question about data structures. Suggest also one or more programming languages to address the question.",
          "Create a question testing debugging skills.",
          "The question should assess code optimization techniques."
        ],
        "creative writing": [
            "Write a short story (under 3000 characters) that begins with the sentence: 'The old lighthouse keeper saw a light that wasn't his own.'",
            "Compose a poem (under 3000 characters) in the style of haiku, about the feeling of a summer rain.",
            "Write a short story (under 3000 characters), no more than five sentences, about a robot who discovers the meaning of friendship.",
            "Create a humorous anecdote (under 3000 characters) about a cat and a laser pointer.",
            "Write a short story (under 3000 characters) that ends with the phrase: '...and that's how the world changed forever.'",
            "Compose a free verse poem (under 3000 characters) about the loneliness of space travel.",
            "Write a short, poignant story (under 3000 characters) about a lost object found again.",
            "Tell a joke (under 3000 characters) about a programmer and a bug.",
            "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.",
            "Write a very short story (under 3000 characters) about a talking animal.",
            "Imagine you are a grain of sand. Describe your life (under 3000 characters).",
            "Write a short story (under 3000 characters) set in a world where colors don't exist.",
            "Write a poem (under 3000 characters) about the feeling of nostalgia.",
            "Create a short, funny dialogue (under 3000 characters) between two inanimate objects.",
            "Write a flash fiction piece (under 3000 characters) inspired by a random word (e.g., 'serendipity', 'obfuscate', 'ephemeral').",
            "Respond to the following prompt (under 3000 characters) with a creative story: 'You wake up one morning to find you can fly.'",
            "Compose a short story(under 3000 characters), inspired by a piece of classical music",
            "Tell a joke (under 3000 characters) based on a pun.",
            "Write a short description (under 3000 characters) of a dream you had.",
            "Craft a short, suspenseful story (under 3000 characters) that begins: 'The phone rang, but the screen was blank...'",
        ],
    }

    #add the creative writing specific prompts to the difficulty prompt,
    #if the topic is creative writing
    if topic == "creative writing":
        difficulty_instructions.update(difficulty_instructions_creative_writing)

    # 4. Guiding Sentence for Question Types
    question_type_intro = "As an example for you, it could be in the form of:"
    question_types = [
        "a comparison question (asking to compare and contrast...).",
        "an analysis question (asking to analyze the relationship between...).",
        "an explanation question (asking to explain the causes of...).",
        "a discussion question (asking to discuss the implications of...).",
        "a significance question (asking about the significance of...).",
        "a cause-and-effect question (like 'How does ... affect ...?').",
        "a difference question (like 'What are the key differences between ... and ...?').",
        "a hypothetical question (like 'What would be the consequences of ...?').", # Counterfactual
        "a scenario-based question (like 'Develop a scenario where...').", #Scenario based
        "a pros and cons question (Provide arguments for and against...')." #pro and cons
    ]

    # --- Combine Prompts using Random Choices ---
    prompt = random.choice(base_instructions) + "\n"
    prompt += random.choice(difficulty_instructions[difficulty]) + "\n"

    # Add topic-specific instruction, handling cases where topic might not be defined.
    if topic in topic_instructions:
        prompt += random.choice(topic_instructions[topic]) + "\n"
    else:
        print(f"Warning: No topic_instructions defined for topic '{topic}'")

    # 5. Conditional Question Types (Not for math, logics, grammar)
    if topic not in ["math", "logics", "grammar", "coding", "creative writing"]:
        prompt += question_type_intro + "\n"
        prompt += random.choice(question_types)

    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"
    return prompt

def answer_question_prompt(question):
    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:"

def rank_answer_prompt(question, answer, topic):
    prompt = f"""You are an expert evaluator. Rank the following answer to the given question on a scale of 1 to 5, where:
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).
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.
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.
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.
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.
Consider these criteria in your ranking:
- Clarity: Is the answer easy to understand? Is it ambiguous or confusing?
- Relevance: Is the answer relevant to the specified topic?
- Formulation: Is the answer well-structured and grammatically correct? Is it logically sound? Is it in a form that proovs expert knowledge?
- Correctness: Are the statements in the answer correct? (this is extremely relevant for topics such as math, grammar, logics, coding, science, technology)
- Interest/Engagement: Is the answer likely to be engaging or thought-provoking? (minor consideration)
"""

    if topic == "creative writing":  # More robust topic check
        prompt += """
- (For Creative Writing ONLY): Originality: Is the writing original and imaginative?  Does it avoid clichΓ©s?
- (For Creative Writing ONLY): Emotional Resonance: Does the writing evoke emotion or connect with the reader on an emotional level?
- (For Creative Writing ONLY): Adherence to Prompt: Does the writing fully address the specific requirements of the creative writing prompt?
- (For Creative Writing ONLY): Character Limit: Does the writing adhere to the 3000-character limit?
"""

    prompt += f"""
Just return a single number (the rank from 1 to 5), do not add any other text.
Question: {question}
Answer: {answer}
Rank:"""
    return prompt

def rank_question_prompt(question, topic, difficulty):
    difficulty_mapping_rank_prompt = {
        "a very simple": "very simple",
        "a simple": "simple",
        "a": "average",
        "a difficult": "difficult",
        "a very difficult": "very difficult"
    }
    difficulty_for_prompt = difficulty_mapping_rank_prompt[difficulty]

    prompt = f"""You are an expert evaluator of questions. Rank the quality of the following question on a scale of 1 to 5, where:
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.
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.
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.
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.
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.
Consider these criteria in your ranking:
- Clarity: Is the question easy to understand? Is it ambiguous or confusing?
- Relevance: Is the question relevant to the specified topic ({topic})?
- Difficulty: Is the difficulty of the question appropriate for the indicated level ({difficulty_for_prompt})?
- Formulation: Is the question well-structured and grammatically correct? Is it logically sound?
- Interest/Engagement: Is the question likely to be engaging or thought-provoking? (minor consideration)
"""
    if topic == "creative writing":
        prompt += f"""
- **(For Creative Writing ONLY): Creativity:** Does the prompt encourage original and imaginative responses?
- **(For Creative Writing ONLY): Clarity of Constraints:** Are the creative constraints (e.g., story, poem, joke) and the 3000-character limit clearly stated?
- **(For Creative Writing ONLY): Inspiration Potential:** Is the prompt likely to inspire high-quality, creative writing?
"""
    prompt += f"""
Just return a single number (the rank from 1 to 5), do not add any other text.
Question: {question}
Rank:"""
    return prompt

# --- Helper function to parse rank strings ---
def parse_rank_string(rank_str, ranking_model_id):
    match = re.search(r'^\D*(\d+)', rank_str) # Regex to find the first integer
    if match:
        rank_str = match.group(1) # Extract the first captured group (the integer)
        try:
            rank_val = int(rank_str) # Convert to integer *after* regex extraction
            if not 1 <= rank_val <= 5: # Check if rank is within valid range
                print(f"Warning: Model {ranking_model_id} returned rank outside of valid range [1-5]: {rank_val}. Rank set to None.")
                return None
            return rank_val
        except ValueError:
            print(f"Warning: Model {ranking_model_id} returned non-integer rank after regex extraction: '{rank_str}'. Rank set to None.")
            return None
    else:
        print(f"Warning: Model {ranking_model_id} returned non-numeric rank: '{rank_str}'. Rank set to None.")
        return None

# --- Helper Function for Parallel Ranking ---
def get_rank_from_model(ranking_model_id, question, answer, consecutive_failures, failure_threshold, unresponsive_models, model_config, topic, token=None, timeout=60):
    start_time = time.time()
    rank = None # Initialize rank to None, indicating potential failure

    rank_prompt = rank_answer_prompt(question, answer, topic)
    
    try:
        response = make_hf_request(model_config[ranking_model_id]["name"], [{"role": "user", "content": rank_prompt}], base_temp, 5, token=token)
        if response:
            try:
                rank_str = response.strip()
                rank = parse_rank_string(rank_str, ranking_model_id)
            except ValueError:
                print(f"Warning: Model {ranking_model_id} returned non-integer rank: '{rank_str}'. Rank set to None.")
                rank = None
        else:
            print(f"Warning: Model {ranking_model_id} failed to provide rank. Rank set to None.")
    except Exception as e:
        duration = time.time() - start_time
        print(f"Warning: Model {ranking_model_id} ranking timed out or failed after {duration:.2f}s: {e}")
        rank = None

    duration = time.time() - start_time # Calculate total duration of ranking attempt
    if duration > timeout:
        print(f"Warning: Ranking by model {ranking_model_id} exceeded timeout of {timeout:.2f}s and took {duration:.2f}s.")
        rank = None # Ensure rank is None if timeout occurs

    time.sleep(time_sleep) # Keep a small delay to avoid overwhelming APIs even in parallel
    return ranking_model_id, rank

# --- Helper Function for Parallel Ranking of questions ---
def get_question_rank_from_model(ranking_model_id, question, topic, difficulty, consecutive_failures, failure_threshold, unresponsive_models, model_config, token=None, timeout=60):
    start_time = time.time()
    rank = None # Initialize rank to None, indicating potential failure

    rank_prompt = rank_question_prompt(question, topic, difficulty) # Use question rank prompt
    
    try:
        response = make_hf_request(model_config[ranking_model_id]["name"], [{"role": "user", "content": rank_prompt}], base_temp, 5, token=token)
        if response:
            try:
                rank_str = response.strip()
                rank = parse_rank_string(rank_str, ranking_model_id)
            except ValueError:
                print(f"Warning: Model {ranking_model_id} returned non-integer rank for question: '{rank_str}'. Rank set to None.")
                rank = None
        else:
            print(f"Warning: Model {ranking_model_id} failed to provide rank for question. Rank set to None.")
    except Exception as e:
        duration = time.time() - start_time
        print(f"Warning: Model {ranking_model_id} ranking question timed out or failed after {duration:.2f}s: {e}")
        rank = None

    duration = time.time() - start_time # Calculate total duration of ranking attempt
    if duration > timeout:
        print(f"Warning: Ranking question by model {ranking_model_id} exceeded timeout of {timeout:.2f}s and took {duration:.2f}s.")
        rank = None # Ensure rank is None if timeout occurs

    time.sleep(time_sleep) # Keep a small delay to avoid overwhelming APIs even in parallel
    return ranking_model_id, rank

# --- Helper Function for Parallel Answering ---
def get_answer_from_model(model_id, question, consecutive_failures, failure_threshold, unresponsive_models, model_config, topic, token=None, timeout=60):
    start_time = time.time() # Start timer
    answer_prompt = answer_question_prompt(question)
    answer = "Error answering" # Default answer

    temp = answer_temp
    max_tok = answer_max_tokens
    if topic == "math" or topic == "coding" or topic == "grammar" or topic == "logics":
        temp = long_temp
        max_tok = long_max_tokens
    
    try:
        response = make_hf_request(model_config[model_id]["name"], [{"role": "user", "content": answer_prompt}], temp, max_tok, token=token)
        if response:
            answer = response.strip()
    except Exception as e:
        duration = time.time() - start_time
        print(f"Warning: Model {model_id} answering timed out or failed after {duration:.2f}s: {e}")
        answer = "Error answering - Timeout" # Or a specific timeout error message
        return answer, duration # Return error answer and duration

    time.sleep(time_sleep) # Small delay
    duration = time.time() - start_time # Calculate duration
    print(f"Answer generation by \"{model_id}\": {duration:.2f}s") # Print answer generation duration separately as requested - as requested

    return answer, duration # Return answer and duration

# --- Core Logic ---
def run_benchmark(hf_models, topics, difficulties, t, model_config, token=None):
    results = {
        "model_name": [],
        "topic": [],
        "question_prompt": [],
        "question": [],
        "answer": [],
        "answer_generation_duration": [],
        "average_rank": [],
        "ranks":[],
        "question_rank_average": [],
        "question_ranks": [],
        "question_rank_duration": []
    }

    cumulative_model_ranks = {} # To store cumulative ranks for each model

    # Check if iteration output file exists and remove it if it does to start fresh
    if os.path.exists(iteration_output_file):
        os.remove(iteration_output_file)

    consecutive_failures = {}  # Track failures per model ID
    failure_threshold = 5
    unresponsive_models = set()

    # Updated model lists with more informative labels
    active_models = hf_models.copy()
    model_weights = {}
    
    for model_id in active_models:
        cumulative_model_ranks[model_id] = []
        consecutive_failures[model_id] = 0
        model_weights[model_id] = 1.0 / len(active_models)  # Initial equal weights

    difficulty_choices = list(difficulty_probabilities.keys())
    probability_values = list(difficulty_probabilities.values())

    # --- Difficulty mapping for output labels ---
    difficulty_mapping = {
        "a very simple": "1",
        "a simple": "2",
        "a": "3",
        "a difficult": "4",
        "a very difficult": "5"
    }

    s_t = 0 #count succesful iterations

    for iteration in range(t): # Added iteration counter

        if len(active_models) < 2:
            print("Fewer than 2 active models remaining. Exiting benchmark.")
            break

        topic = random.choice(topics)
        # --- Select difficulty with probabilities ---
        difficulty = random.choices(difficulty_choices, weights=probability_values, k=1)[0] # Weighted random choice
        print(f"--- Iteration {s_t + 1}/{t}: {difficulty} question ({difficulty_mapping[difficulty]}) on {topic} ---") # Print iteration number
        st.write(f"--- Iteration {s_t + 1}/{t}: {difficulty} question ({difficulty_mapping[difficulty]}) on {topic} ---") # Print iteration number

        # --- Question Generation ---
        question = None
        question_prompt = generate_question_prompt(topic, difficulty)

        question_accepted = False # Flag to track if question is accepted
        question_ranks_all = []
        question_avg_rank = np.nan
        question_ranking_duration_total = 0

        cumulative_avg_rank = {} # To store cumulative average ranks for each model

        max_attempts = 3 * len(active_models)
        for attempt in range(max_attempts):
            # --- Filter for question generation roles ("answer" or "both") ---
            question_gen_candidates = [
                model_id for model_id in active_models
                if model_config[model_id].get("role", "both") in ["answer", "both"]
            ]
            if not question_gen_candidates: # No suitable models left
                print("No models available for question generation with 'answer' or 'both' role. Skipping iteration.")
                continue # Skip to next iteration

            question_generator_model_id = random.choice(question_gen_candidates)

            # --- Question Generation ---
            response = make_hf_request(model_config[question_generator_model_id]["name"], 
                                      [{"role": "user", "content": question_prompt}], 
                                      question_temp, 
                                      question_max_tokens,
                                      token=token)
            
            if response:
                question = response.strip()
                consecutive_failures[question_generator_model_id] = 0  # Reset on success
                break
            else:
                print(f"Skipping due to request failure for model {question_generator_model_id}.")
                consecutive_failures[question_generator_model_id] += 1

            if consecutive_failures[question_generator_model_id] >= failure_threshold:
                print(f"Model {question_generator_model_id} is unresponsive (question gen). Removing from active models.")
                if question_generator_model_id in active_models:
                    active_models.remove(question_generator_model_id)
                unresponsive_models.add(question_generator_model_id)
            time.sleep(time_sleep)

        if question is None:
            print(f"Failed to generate a question after {max_attempts} attempts. Skipping this round.")
            continue

        # --- Parallel Question Ranking ---
        question_ranks = {}
        question_ranking_futures = []
        question_ranking_start_time = time.time()

        with concurrent.futures.ThreadPoolExecutor(max_workers=len(active_models) or 1) as executor:
            for ranking_model_id in active_models:
                # --- Filter for ranking roles ("rank" or "both") ---
                if model_config[ranking_model_id].get("role", "both") in ["rank", "both"]:
                    future = executor.submit(
                        get_question_rank_from_model,
                        ranking_model_id,
                        question,
                        topic,
                        difficulty,
                        consecutive_failures,
                        failure_threshold,
                        unresponsive_models,
                        model_config,
                        token,
                        timeout=60
                    )
                    question_ranking_futures.append(future)

            for future in concurrent.futures.as_completed(question_ranking_futures): # Collect ranks as they become available
                ranking_model_id, rank = future.result() # Get model_id and rank
                question_ranks[ranking_model_id] = rank # Store rank with model_id as key

        question_ranking_end_time = time.time()
        question_ranking_duration_total = question_ranking_end_time - question_ranking_start_time

        # Filter out None values (failed ranks) and calculate weighted average
        valid_question_ranks_values = [r for r in question_ranks.values() if r is not None] # Get rank values
        question_avg_rank = np.nan  # Default to NaN

        if valid_question_ranks_values:
            # Create a list of weights corresponding to the valid ranks
            weights_for_valid_question_ranks = [model_weights[model_id]
                                        for model_id, rank in question_ranks.items()
                                        if rank is not None]

            #check that the length is correct
            if len(weights_for_valid_question_ranks) != len(valid_question_ranks_values):
                print("Warning: Mismatch length of weights and valid question ranks")
                print(f'weights_for_valid_question_ranks {weights_for_valid_question_ranks}')
                print(f'valid_question_ranks_values: {valid_question_ranks_values}')

            question_avg_rank = np.average(valid_question_ranks_values, weights=weights_for_valid_question_ranks)
        min_question_rank = min(valid_question_ranks_values) if valid_question_ranks_values else 0 # To avoid error if no valid rank

        if question_avg_rank >= question_treshold and all(rank > reject_rank for rank in valid_question_ranks_values): # Question acceptance criteria
            question_accepted = True
            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]}")
            st.write(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]}")
            s_t += 1
        else:
            question_accepted = False
            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]}")
            st.write(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]}")

        if not question_accepted:
            print("Generated question was not accepted. Regenerating question.")
            continue

        if len(active_models) < 2:
          print("Fewer than 2 active models remaining. Exiting benchmark.")
          break

        # --- Parallel Answer Generation ---
        answers = {}
        answer_futures = []
        answer_durations = {}
        with concurrent.futures.ThreadPoolExecutor(max_workers=len(active_models)) as executor:
            for model_id in active_models:
                # --- Filter for answer generation roles ("answer" or "both") ---
                if model_config[model_id].get("role", "both") in ["answer", "both"]:
                    try:
                        future = executor.submit(
                            get_answer_from_model,
                            model_id,
                            question,
                            consecutive_failures,
                            failure_threshold,
                            unresponsive_models,
                            model_config,
                            topic,
                            token,
                            timeout=60
                        )
                        answer_futures.append(future)
                    except TimeoutError as e:
                        print(f"Answer generation for model {model_id} timed out: {e}")
                        answer = "I am struggling to answer this question" # Treat timeout as error
                        duration = 120 # You can set a default duration or handle it differently if needed
                        answers[model_id] = answer # Store error answer
                        answer_durations[model_id] = duration # Store default duration

            for future in concurrent.futures.as_completed(answer_futures):
                model_id = active_models[answer_futures.index(future)] # Get model_id based on future index (order is preserved)
                answer, duration = future.result() # Get both answer and duration - unpack the returned tuple
                st.write(f"Answer generation by \"{model_id}\": {duration:.2f}s") # Print answer generation duration separately as requested - as requested
                answers[model_id] = answer
                answer_durations[model_id] = duration # Store duration - store the duration in the answer_durations dictionary

        # --- Ranking Process ---

        # Prepare to write to file (open in append mode outside the model loop but inside iteration loop)
        iteration_results_file_opened = open(iteration_output_file, 'a')
        if iteration == 0: # Write header only for the first iteration
            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


        for model_id in active_models:
            answer = answers.get(model_id, "Error answering") # Safely retrieve pre-generated answer, default to error
            duration = answer_durations.get(model_id, 0.0)  # Safely retrieve duration, default to 0.0

            if answer == "Error answering": # Handle answer generation errors
                consecutive_failures[model_id] += 1
                if consecutive_failures[model_id] >= failure_threshold:
                    print(f"Model {model_id} is consistently failing to answer. Removing from active models.")
                    if model_id in active_models: # double check before removing, might have been removed in another thread
                        active_models.remove(model_id)
                    unresponsive_models.add(model_id)
                continue # Skip ranking if answer generation failed for this model


            if len(active_models) < 2: # Re-check active models before ranking
              print("Fewer than 2 active models remaining. Exiting benchmark.")
              break

            ranks = {}
            ranking_futures = []

            ranking_start_time = time.time()
            with concurrent.futures.ThreadPoolExecutor(max_workers=len(active_models) or 1) as executor:
                for ranking_model_id in active_models:
                    # --- Filter for ranking roles ("rank" or "both") ---
                    if model_config[ranking_model_id].get("role", "both") in ["rank", "both"]:
                        future = executor.submit(
                            get_rank_from_model,
                            ranking_model_id,
                            question,
                            answer,
                            consecutive_failures,
                            failure_threshold,
                            unresponsive_models,
                            model_config,
                            topic,
                            token,
                            timeout=60
                        )
                        ranking_futures.append(future)

                for future in concurrent.futures.as_completed(ranking_futures): # Collect ranks as they become available
                    ranking_model_id, rank = future.result() # Get model_id and rank
                    ranks[ranking_model_id] = rank # Store rank with model_id as key

            ranking_end_time = time.time() # Record end time of ranking
            ranking_duration = ranking_end_time - ranking_start_time # Calculate duration

            # Filter out None values (failed ranks) and calculate weighted average
            valid_ranks_values = [r for r in ranks.values() if r is not None] # Get rank values
            average_rank = np.nan  # Default to NaN

            if valid_ranks_values:
                #Create a list of weights corresponding to the valid ranks
                weights_for_valid_ranks = [model_weights[model_id]
                                    for model_id, rank in ranks.items()
                                    if rank is not None]


                if len(weights_for_valid_ranks) != len(valid_ranks_values):
                    print("Warning: Mismatch length of weights and valid answer ranks")
                    print(f'weights_for_valid_ranks {weights_for_valid_ranks}')
                    print(f'valid_ranks_values: {valid_ranks_values}')

                average_rank = np.average(valid_ranks_values, weights=weights_for_valid_ranks)

            results["model_name"].append(model_id)
            results["topic"].append(topic)
            results["question_prompt"].append(question_prompt)
            results["question"].append(question)
            results["answer"].append(answer)
            results["answer_generation_duration"].append(duration)
            results["average_rank"].append(average_rank)
            results["ranks"].append([ranks.get(m, None) for m in active_models]) # Store raw ranks including Nones, ensure order, use .get() for safety
            results["question_rank_average"].append(question_avg_rank) # Store question rank average
            results["question_ranks"].append([question_ranks.get(m, None) for m in active_models]) # Store question ranks, use .get() for safety
            results["question_rank_duration"].append(question_ranking_duration_total) # Store question ranking duration

            cumulative_model_ranks[model_id].append(average_rank) # Append current iteration's average rank
            # Safely calculate cumulative average rank - this is where the KeyError was happening
            cumulative_avg_rank[model_id] = np.nanmean(cumulative_model_ranks.get(model_id, [])) if cumulative_model_ranks.get(model_id, []) else np.nan

            # --- Print and store iteration results IMMEDIATELY after ranking for this model ---
            ranks_str = "[" + ", ".join(map(str, [ranks.get(m, None) for m in active_models])) + "]" if ranks else "[]" # Format ranks for CSV, ensure order, use .get() for safety
            print(f"{topic}, {difficulty_mapping[difficulty]}, {model_id}, Avg Rank: {cumulative_avg_rank.get(model_id, np.nan):.2f}, Avg Rank for run: {average_rank:.2f}, Ranks: {ranks_str}, {ranking_duration:.2f} s")
            st.write(f"{topic}, {difficulty_mapping[difficulty]}, {model_id}, Avg Rank: {cumulative_avg_rank.get(model_id, np.nan):.2f}, Avg Rank for run: {average_rank:.2f}, Ranks: {ranks_str}, {ranking_duration:.2f} s")

            # Write iteration results to file (append mode) - write for each model right after ranking
            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.get(model_id, np.nan):.2f},{average_rank:.2f},{ranks_str},{ranking_duration:.2f}\n")

        # Update model weights based on cumulative average ranks, handling NaNs
        temp_weights = {}
        total_valid_rank = 0  # Keep track of the sum of valid (non-NaN) ranks

        for m_id in active_models:
            # Safe access with .get() to handle missing keys
            if m_id in cumulative_avg_rank and not np.isnan(cumulative_avg_rank.get(m_id, np.nan)):
                temp_weights[m_id] = cumulative_avg_rank.get(m_id, 0)
                total_valid_rank += cumulative_avg_rank.get(m_id, 0)
            else: # if cumulative is empty or NaN, keep original
                temp_weights[m_id] = model_weights.get(m_id, 1.0 / len(active_models))

        # Normalize the weights so they sum to 1, handling cases where total_valid_rank might be zero
        if total_valid_rank > 0:
            for m_id in temp_weights:
                model_weights[m_id] = temp_weights[m_id] / total_valid_rank
        else:
            # If total_valid_rank is 0 (all models have NaN ranks), revert to equal weights
            for m_id in active_models:
                model_weights[m_id] = 1.0 / len(active_models)

        iteration_results_file_opened.close()

    print(f"Unresponsive models during this run: {unresponsive_models}")
    return results, cumulative_avg_rank, s_t

def check_model_availability(models, token):
    """Test if models are available with the provided token"""
    availability_results = {}
    
    for model_name in models:
        st.write(f"Testing availability of {model_name}...")
        try:
            # Create a simple test prompt
            test_prompt = "Hello, are you available?"
            
            # Use a short timeout to quickly test connectivity
            client = InferenceClient(model=model_name, token=token)
            response = client.text_generation(
                test_prompt,
                max_new_tokens=10,
                temperature=0.7,
                do_sample=True
            )
            
            availability_results[model_name] = {
                "available": True,
                "response": response[:50] + "..." if len(response) > 50 else response
            }
            st.success(f"βœ… {model_name} is available")
            
        except Exception as e:
            error_msg = str(e)
            availability_results[model_name] = {
                "available": False,
                "error": error_msg
            }
            
            if "401" in error_msg or "unauthorized" in error_msg.lower():
                st.error(f"❌ {model_name}: Authentication error. Check your API token.")
            elif "404" in error_msg or "not found" in error_msg.lower():
                st.error(f"❌ {model_name}: Model not found. It may not exist or you may not have access.")
            elif "429" in error_msg or "rate limit" in error_msg.lower():
                st.error(f"❌ {model_name}: Rate limit exceeded. Try again later.")
            else:
                st.error(f"❌ {model_name}: Unknown error: {error_msg}")
                
        time.sleep(1)  # Add delay between checks
    
    return availability_results

# Streamlit UI
st.title("AutoBench 1.0 Demo")
st.write("A Collective-Model-As-Judge system that will generate a customizable LLM benchmark. AutoBench 1.0 Demo is just a simple trial version for educational purposes that implements the Hugging Face's Inference API. Please refer to the AutoBench 1.0 repository for any advanced use. \n\nChose the models you want to evaluate (at least 2). The models will rank each other against the selected topics. But, first, check if models are available (this will depend on your Hugging Face account. Premium is strongly recommended to avoid unresponsive models). Consult the README file for more details.")

# Setup sidebar for configuration
st.sidebar.header("Configuration")

# Add a field for the Hugging Face token
hf_token = st.sidebar.text_input("Hugging Face API Token", type="password", 
                                 help="Your Hugging Face API token to access models")

# Model selection
st.sidebar.subheader("Models")
available_models = [
    "meta-llama/Llama-3.3-70B-Instruct",
    "meta-llama/Llama-3.1-70B-Instruct",
    "mistralai/Mistral-7B-Instruct-v0.2",
    "mistralai/Mixtral-8x7B-Instruct-v0.1",
    "Qwen/Qwen2.5-72B-Instruct",
    "Qwen/QwQ-32B-Preview"
    # Add more models as needed
]

selected_models = st.sidebar.multiselect(
    "Select models to benchmark",
    available_models,
    default=available_models[:3]  # Default to first 3 models
)

# Topic selection
available_topics = ["math", "logics", "grammar", "coding", "history", "current news", 
                    "general culture", "science", "technology", "creative writing"]
selected_topics = st.sidebar.multiselect(
    "Select topics",
    available_topics,
    default=available_topics
)

# Number of iterations
num_iterations = st.sidebar.slider("Number of iterations", 1, 20, 5)

# Create model_config dictionary from selected models
model_config = {}
for model in selected_models:
    model_config[model] = {"name": model, "role": "both"}

if st.sidebar.button("Test Selected Models"):
    if not hf_token:
        st.error("Please enter your Hugging Face API token")
    elif not selected_models:
        st.error("Please select at least one model")
    else:
        with st.spinner("Testing model availability..."):
            availability = check_model_availability(selected_models, hf_token)
            
            # Show results in a table
            availability_df = pd.DataFrame([
                {
                    "Model": model,
                    "Available": info["available"],
                    "Status": "Available" if info["available"] else "Error",
                    "Details": info.get("response", "") if info["available"] else info.get("error", "")
                }
                for model, info in availability.items()
            ])
            
            st.dataframe(availability_df)
            
            # Check if we have enough models to run the benchmark
            available_models = [m for m, info in availability.items() if info["available"]]
            if len(available_models) >= 2:
                st.success(f"{len(available_models)} models are available for benchmarking")
            else:
                st.error("You need at least 2 available models to run the benchmark")

# Start benchmark button
if st.sidebar.button("Start Benchmark"):
    if not hf_token:
        st.error("Please enter your Hugging Face API token")
    elif not selected_models:
        st.error("Please select at least two models")
    elif not selected_topics:
        st.error("Please select at least one topic")
    else:
        # Create progress bar
        progress_bar = st.progress(0)
        status_text = st.empty()
        
        # Setup to capture results for display
        results_container = st.container()
        with results_container:
            results_placeholder = st.empty()
            iterations_table = st.empty()
        
        # Create a global variable to store intermediate results
        if 'results_df' not in st.session_state:
            st.session_state.results_df = pd.DataFrame()
        
        # Run the benchmark
        try:
            # Update status
            status_text.text("Benchmark running... For more detailed logs, check the container log (above, next to the \"running\" button).")
            
            # Run benchmark and get results
            results, cumulative_avg_rank, total_successful = run_benchmark(
                selected_models, selected_topics, 
                ["a very simple", "a simple", "a", "a difficult", "a very difficult"],
                num_iterations, model_config, hf_token
            )
            
            # Update progress to complete
            progress_bar.progress(100)
            st.subheader(f"Benchmark completed! {total_successful} successful iterations.")
            
            # Display results
            if total_successful > 0:
                results_df = pd.DataFrame(results)
                st.session_state['results_df'] = results_df
                
                # Show model rankings
                st.subheader("Model Rankings")
                ranking_df = pd.DataFrame({
                    "Model": list(cumulative_avg_rank.keys()),
                    "Average Rank": [round(r, 2) if not np.isnan(r) else 'N/A' for r in cumulative_avg_rank.values()]
                })
                #ranking_df = ranking_df.sort_values("Average Rank", ascending=False)
                st.dataframe(ranking_df)  # Use st.dataframe directly instead of results_placeholder.dataframe
                
                # Show detailed results
                st.subheader("Detailed Results")
                st.dataframe(results_df)
                
                # Option to download results
                csv = results_df.to_csv(index=False)
                st.download_button(
                    label="Download Results CSV",
                    data=csv,
                    file_name="llm_benchmark_results.csv",
                    mime="text/csv",
                )
            else:
                st.warning("The benchmark did not complete any successful iterations.")
        except Exception as e:
            st.error(f"An error occurred: {e}")
            st.exception(e)
            
# Show previous results if available
elif 'results_df' in st.session_state and not st.session_state.results_df.empty:
    st.subheader("Previous Results")
    st.dataframe(st.session_state.results_df)