AlokBharadwaj commited on
Commit
8fcbdf9
·
1 Parent(s): 9abc4d1

Organisation

Browse files
Files changed (4) hide show
  1. app.py +5 -80
  2. utils.py → src/entailment.py +0 -92
  3. src/generation.py +330 -0
  4. src/rendering.py +65 -0
app.py CHANGED
@@ -1,88 +1,13 @@
1
  import gradio as gr
2
- import math
3
- import os
4
- import numpy as np
5
- from huggingface_hub import InferenceClient
6
 
7
- client = InferenceClient(token=os.environ.get("HF_TOKEN"))
 
 
 
8
 
9
- def hex_to_rgb(hex_code):
10
- r = int(hex_code[0:2], 16)
11
- g = int(hex_code[2:4], 16)
12
- b = int(hex_code[4:6], 16)
13
- return r, g, b
14
 
15
- def prob_to_color(avg_prob):
16
- norm_prob = np.clip(avg_prob, 0.0, 1.0)
17
- r = int((1 - norm_prob) * 255)
18
- g = int(norm_prob * 255)
19
- b = 0
20
- return f"{r:02x}{g:02x}{b:02x}"
21
 
22
- def tokens_to_html(logprobs_content):
23
- spans = ""
24
- for token in logprobs_content:
25
- prob = math.exp(token.logprob)
26
- hex_color = prob_to_color(prob)
27
- r, g, b = hex_to_rgb(hex_color)
28
- brightness = (r * 299 + g * 587 + b * 114) / 1000
29
- text_color = "black" if brightness > 128 else "white"
30
- token_text = token.token.replace(" ", "&nbsp;").replace("<", "&lt;").replace(">", "&gt;")
31
- tooltip_text = f"p={prob:.3f} logprob={token.logprob:.3f}"
32
- spans += (
33
- f'<span '
34
- f'data-tooltip="{tooltip_text}" '
35
- f'style="background-color:#{hex_color}; color:{text_color}; '
36
- f'padding:2px 4px; border-radius:3px; margin:1px; cursor:pointer;">'
37
- f'{token_text}</span>'
38
- )
39
- return f"<p style='font-family: monospace; font-size: 1.1em; line-height: 2em;'>{spans}</p>"
40
-
41
- LEGEND_HTML = """
42
- <div style='margin:10px 0; font-family:monospace;'>
43
- <b>Token confidence:</b>&nbsp;
44
- <span style='background:#ff0000; color:white; padding:4px 8px; border-radius:3px;'>Low (0%)</span>&nbsp;
45
- <span style='background:#7f7f00; color:white; padding:4px 8px; border-radius:3px;'>Moderate (50%)</span>&nbsp;
46
- <span style='background:#00ff00; color:black; padding:4px 8px; border-radius:3px;'>High (100%)</span>
47
- </div>
48
- """
49
-
50
- TOOLTIP_CSS = """
51
- span[data-tooltip] {
52
- position: relative;
53
- }
54
- span[data-tooltip]:hover::after {
55
- content: attr(data-tooltip);
56
- position: absolute;
57
- bottom: 130%;
58
- left: 50%;
59
- transform: translateX(-50%);
60
- background: #333;
61
- color: white;
62
- padding: 4px 8px;
63
- border-radius: 4px;
64
- font-size: 0.75em;
65
- white-space: nowrap;
66
- z-index: 9999;
67
- pointer-events: none;
68
- }
69
- """
70
-
71
- def query(user_input, model, max_tokens):
72
- if not user_input.strip():
73
- return "<p>Please enter a question.</p>"
74
- try:
75
- response = client.chat.completions.create(
76
- model=model,
77
- messages=[{"role": "user", "content": user_input}],
78
- max_tokens=int(max_tokens),
79
- logprobs=True,
80
- top_logprobs=1
81
- )
82
- print(f"QUERY: {user_input}")
83
- return tokens_to_html(response.choices[0].logprobs.content)
84
- except Exception as e:
85
- return f"<p style='color:red;'>Error: {str(e)}</p>"
86
 
87
  with gr.Blocks(title="Probabot", css=TOOLTIP_CSS) as demo:
88
  gr.Markdown("# 🤖🔍 Probabot")
 
1
  import gradio as gr
 
 
 
 
2
 
3
+ from src.rendering import (
4
+ LEGEND_HTML,
5
+ TOOLTIP_CSS
6
+ )
7
 
8
+ from src.generation import query
 
 
 
 
9
 
 
 
 
 
 
 
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  with gr.Blocks(title="Probabot", css=TOOLTIP_CSS) as demo:
13
  gr.Markdown("# 🤖🔍 Probabot")
utils.py → src/entailment.py RENAMED
@@ -1,18 +1,9 @@
1
  import os
2
- import logging
3
  import numpy as np
4
  import torch
5
  import torch.nn.functional as F
6
  from transformers import AutoModelForSequenceClassification, AutoTokenizer
7
 
8
- import math
9
-
10
- import json
11
- import pandas as pd
12
- import matplotlib.pyplot as plt
13
- from matplotlib.cm import get_cmap
14
- import random
15
-
16
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
17
 
18
 
@@ -97,87 +88,4 @@ def get_semantic_ids(strings_list, model, strict_entailment=False, example=None)
97
  return semantic_ids
98
 
99
 
100
- '''
101
- LLM CHECKS ENTAILMENT
102
- '''
103
- def LLM_get_semantic_ids(strings_list, sub_question, MODEL_NAME, strict_entailment=False):
104
- def LLM_check_implication(text1, text2):
105
- '''
106
- LLM checks if text1 implies text2. Returns 2 if entailment, 1 if neutral and 0 if contradiction.
107
- '''
108
- try:
109
- client = genai.Client()
110
- except Exception as e:
111
- print(f"Error when initializing client: {e}")
112
- if not os.environ.get("GEMINI_API_KEY"):
113
- print("ERROR: GEMINI_API_KEY not found in environment.")
114
- return
115
-
116
- # the prompt below was copied from semantic_uncertainty/semantic_uncertainty/uncertainty/uncertainty_measures/semantic_entropy.py
117
- prompt = f"""We are evaluating answers to the question \"{sub_question}\"\n"""
118
- prompt += "Here are two possible answers:\n"
119
- prompt += f"Possible Answer 1: {text1}\nPossible Answer 2: {text2}\n"
120
- prompt += "Does Possible Answer 1 semantically entail Possible Answer 2? Respond with entailment, contradiction, or neutral, nothing else. """
121
-
122
- try:
123
- response = client.models.generate_content(
124
- model=MODEL_NAME,
125
- contents=prompt,
126
- config=GenerateContentConfig(
127
- temperature = 0.7 # temperature = 0 -> models behave deterministically. temperature > 0 -> model randomly choses from top tokens
128
- )
129
- )
130
- entailment_answer = response.lower()[:30]
131
- if 'entailment' in entailment_answer:
132
- return 2
133
- elif 'neutral' in entailment_answer:
134
- return 1
135
- elif 'contradiction' in entailment_answer:
136
- return 0
137
- else:
138
- raise ValueError(
139
- f"Unexpected model output: {entailment_answer}. Answer can only be 'entailment, 'neutral' or 'contradiction'. "
140
- )
141
-
142
- except Exception as e:
143
- raise RuntimeError(
144
- f"API call failed for prompt: {prompt}"
145
- ) from e
146
-
147
- def LLM_are_equivalent(text1, text2):
148
- '''
149
- Returns True if text1 and text2 are semantically identical.
150
- Two strings text1 and text2 are semantically equivalent (with strict_entailment=False) when
151
- their implications are either [1, 2], [2, 1] or [2, 2]. With strict_entailment=True, then only [2, 2] means they are equivalent
152
- '''
153
- try:
154
- i1 = LLM_check_implication(text1, text2)
155
- except ValueError as e:
156
- print(f"LLM failed in checking implication of \n'{text1}' --> '{text2}'. \nThe error that was raised is: \n{e}")
157
-
158
- try:
159
- i2 = LLM_check_implication(text2, text1)
160
- except ValueError as e:
161
- print(f"LLM failed in checking implication of \n'{text2}' --> '{text1}'. \nThe error that was raised is: \n{e}")
162
-
163
- if strict_entailment:
164
- return (i1 == 2) and (i2 == 2)
165
- else:
166
- implications = [i1, i2]
167
- return (0 not in implications) and (implications != [1, 1])
168
-
169
- semantic_ids = [-1] * len(strings_list)
170
- next_id = 0
171
-
172
- # give each string in strings_list a semnatic_id
173
- for i, s1 in enumerate(strings_list):
174
- if semantic_ids[i] == -1:
175
- semantic_ids[i] = next_id
176
- for j in range(i + 1, len(strings_list)):
177
- if LLM_are_equivalent(s1, strings_list[j]):
178
- semantic_ids[j] = next_id # give the string the same semantic id if it is equivalent with an other string in strings_list
179
- next_id += 1
180
-
181
- return semantic_ids
182
-
183
 
 
1
  import os
 
2
  import numpy as np
3
  import torch
4
  import torch.nn.functional as F
5
  from transformers import AutoModelForSequenceClassification, AutoTokenizer
6
 
 
 
 
 
 
 
 
 
7
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
8
 
9
 
 
88
  return semantic_ids
89
 
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
src/generation.py ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ import re
4
+ import json
5
+ import time
6
+
7
+ def query_model(system_prompt, prompt, model_name, max_tokens, client=None):
8
+ """
9
+ Parameters
10
+ ----------
11
+ system_prompt : str — the system prompt
12
+ prompt : str — the input text
13
+ model_name : str — "gpt2", "llama-1b", or "llama-8b"
14
+ max_tokens : int — how many tokens to generate (capped at 200 by proxy)
15
+ client : str — the proxy URL returned by setup_client()
16
+
17
+ Returns
18
+ -------
19
+ dict with keys:
20
+ "answer" : str — the generated text
21
+ "logprobs" : dict — {token: logprob}
22
+ "token_probs" : dict — {token: probability}
23
+ """
24
+ if client is None:
25
+ client = "https://cryotud-nb4170-llm-proxy.hf.space"
26
+
27
+ # Also include SYSTEM PROMPT!
28
+ response = requests.post(
29
+ f"{client}/generate",
30
+ json={
31
+ "system_prompt": system_prompt,
32
+ "prompt": prompt,
33
+ "model": model_name,
34
+ "max_tokens": max_tokens,
35
+ },
36
+ timeout=60,
37
+ )
38
+
39
+ response.raise_for_status()
40
+ return response.json()
41
+
42
+ ## HIGH LEVEL FUNCTIONS ##
43
+ def generate_long_answer(question, model_name, max_tokens, client):
44
+ '''
45
+ Generates a longer answer of three sentences to the provided main question.
46
+ Returns a string that contains the answer.
47
+ '''
48
+
49
+ prompt = question + '\nAnswer in exactly 3 sentences. Do not use bullet points in your answer.'
50
+ response = query_model('', prompt, model_name, max_tokens, client)
51
+ answer = response['answer']
52
+ return answer
53
+
54
+ ## LEVEL BELOW ##
55
+
56
+ def split_answers_into_json_strings(answer, model_name, max_tokens, client, verbose=True):
57
+ """
58
+ Makes 1 API call to split the answer into sentences, then 1 API call per sentence
59
+ to extract sentence parts and factoids
60
+ Returns raw parsed JSON
61
+ """
62
+
63
+ sys_prompt_1 = """"You are a strict text parser.
64
+ You ONLY output valid JSON arrays of strings.
65
+ No markdown, no explanations.
66
+ Example Input:
67
+ Text: Paris is the capital of France. The Eiffel Tower is in Paris. The Louvre is a famous museum in Paris.
68
+ Example Output:
69
+ ["Paris is the capital of France.", "The Eiffel Tower is in Paris.", "The Louvre is a famous museum in Paris."]
70
+ """
71
+
72
+ user_prompt_1 = f"""Split the following text into individual sentences.
73
+ Rules:
74
+ 1. Output a maximum of 3 sentences.
75
+ 2. Respond ONLY with a valid JSON array of strings.
76
+
77
+ Text: "{answer}"
78
+
79
+ Example Output:
80
+ ["Sentence 1.", "Sentence 2.", "Sentence 3."]
81
+ """
82
+
83
+
84
+ if verbose:
85
+ print("\n" + "=" * 80)
86
+ print("SENTENCE SPLITTING (JSON)")
87
+ print("=" * 80)
88
+
89
+ # First API call: split the sentences using LLM
90
+ try:
91
+ res_sentences = query_model(sys_prompt_1, user_prompt_1, model_name, max_tokens, client)
92
+ raw_sentences = res_sentences.get('answer', '').strip()
93
+ clean_json_str = re.sub(r'```json|```', '', raw_sentences).strip() # safety cleaner to process potential "```json" in answer
94
+ sentences = json.loads(clean_json_str)
95
+ print(f"Successfully extracted {len(sentences)} sentences via JSON.")
96
+ except Exception as e:
97
+ print(f"❌ Error during sentence splitting: {e}")
98
+ return [], []
99
+
100
+ ##### FACTOID DECOMPOSITION #####
101
+ if verbose:
102
+ print("\n" + "=" * 80)
103
+ print("FACTOID EXTRACTION (JSON)")
104
+ print("=" * 80)
105
+
106
+ master_json_output = []
107
+
108
+ sys_prompt_2 = """You are a strict data extraction tool.
109
+ You ONLY output valid JSON arrays of objects.
110
+ You NEVER invent external information.
111
+
112
+ EXAMPLE INPUT:
113
+ Sentence: "Marie Curie was born in Warsaw, and she later moved to Paris to study."
114
+
115
+ EXAMPLE OUTPUT:
116
+ [
117
+ {{
118
+ "sentence_part": "Marie Curie was born in Warsaw, ",
119
+ "factoids": ["Marie Curie was born in Warsaw"]
120
+ }},
121
+ {{
122
+ "sentence_part": "and she later moved to Paris to study.",
123
+ "factoids": ["Marie Curie moved to Paris", "Marie Curie moved to Paris for her studies"]
124
+ }}
125
+ ]
126
+ """
127
+
128
+ # API calls 2-4: split into 2 sentence parts, and split those into 2 factoids
129
+ for i, sentence in enumerate(sentences, 1):
130
+ print(f"Processing Sentence {i}/{len(sentences)}: '{sentence}'")
131
+
132
+ user_prompt_2 = f"""Decompose the given sentence into consecutive 'sentence parts' and extract factoids.
133
+
134
+ RULES:
135
+ 1. Min 1, max 2 sentence parts total.
136
+ 2. EXACT COPY: The 'sentence part' MUST be an exact, unaltered substring copied directly from the original sentence.
137
+ 3. Min 1, max 2 brief factoids per sentence part.
138
+ 4. STRICT: ONLY extract facts explicitly stated.
139
+ 5. Respond ONLY with a valid JSON array.
140
+ 6. Be exact in the factoids that you generate: provide names of the people/places/occurences that you mention.
141
+
142
+ EXAMPLE INPUT:
143
+ Sentence: "Marie Curie was born in Warsaw, and she later moved to Paris to study."
144
+
145
+ EXAMPLE OUTPUT:
146
+ [
147
+ {{
148
+ "sentence_part": "Marie Curie was born in Warsaw, ",
149
+ "factoids": ["Marie Curie was born in Warsaw"]
150
+ }},
151
+ {{
152
+ "sentence_part": "and she later moved to Paris to study.",
153
+ "factoids": ["Marie Curie moved to Paris", "Marie Curie moved to Paris for her studies"]
154
+ }}
155
+ ]
156
+
157
+ ACTUAL INPUT:
158
+ Sentence: "{sentence}"
159
+ """
160
+
161
+ try:
162
+ res_factoids = query_model(sys_prompt_2, user_prompt_2, model_name, max_tokens, client)
163
+ raw_output = res_factoids.get('answer', '').strip()
164
+ clean_output = re.sub(r'```json|```', '', raw_output).strip() # again safety check to remove additional json stuff
165
+
166
+ # Parse the JSON and append to master list
167
+ parsed_sentence_data = json.loads(clean_output) # json.loads() parses the json string and puts the contents into a dictionary
168
+ master_json_output.append(parsed_sentence_data)
169
+
170
+ except Exception as e:
171
+ print(f"❌ Error parsing JSON for sentence {i}: {e}")
172
+ master_json_output.append([]) # Append empty list on failure to keep index aligned
173
+
174
+ return master_json_output, sentences
175
+
176
+ def parse_json_to_dictionaries(master_json_output, original_sentences):
177
+ """
178
+ Takes the raw JSON output from the LLM pipeline, applies Python code with safety limits,
179
+ and puts it into the designated dictionaries
180
+ """
181
+ sentence_part_to_factoids = {}
182
+ factoid_to_sentence_part = {}
183
+ sentence_parts = []
184
+ factoids_list = []
185
+
186
+ for sentence_data, original_sentence in zip(master_json_output, original_sentences):
187
+ constrained_parts = sentence_data[:2] # ensure there are only two sentence parts
188
+
189
+ for item in constrained_parts:
190
+ s_part = item.get("sentence_part", "").strip() # get the sentence part
191
+ extracted_factoids = item.get("factoids", []) # get the corresponding factoids
192
+
193
+ if not s_part:
194
+ continue
195
+
196
+ if s_part not in original_sentence:
197
+ print(f"⚠️ WARNING: Paraphrase detected! \nExpected substring of: '{original_sentence}' \nGot: '{s_part}'")
198
+
199
+ constrained_factoids = [f.strip() for f in extracted_factoids if f.strip()][:2] # strip of whitespace & ensure there are only two factoids
200
+
201
+ # print warning if a sentence part has no extracted factiods
202
+ if len(constrained_factoids) == 0:
203
+ print(f"⚠️ Notice: No valid factoids extracted for sentence part: '{s_part}'")
204
+ continue
205
+
206
+ # store the sentence part and the factoids in the corresponding datastructures
207
+ sentence_parts.append(s_part)
208
+ sentence_part_to_factoids[s_part] = constrained_factoids
209
+
210
+ for factoid in constrained_factoids:
211
+ factoids_list.append(factoid)
212
+ factoid_to_sentence_part[factoid] = s_part
213
+
214
+ return sentence_part_to_factoids, factoid_to_sentence_part, sentence_parts, factoids_list
215
+
216
+
217
+ ## LOW LEVEL FUNCTIONS ##
218
+ def generate_Q_questions(factoid, original_answer, Q, model_name, max_tokens, client, enable_sleep=True):
219
+ '''
220
+ Generates Q questions that might have generated the factoid, using the original answer for context.
221
+
222
+ Args:
223
+ - factoid: string, the current factoid to turn into questions
224
+ - original_answer: string, the original full text
225
+ - Q: int, number of questions to generate
226
+ - model_name: string, name of the model
227
+ - max_tokens: int
228
+ - client: the proxy URL
229
+ - enable_sleep: boolean, enables sleep function if rate limit is hit
230
+
231
+ Returns:
232
+ - questions: a list of exactly Q question strings
233
+ '''
234
+
235
+
236
+ sys_prompt = """You are a precise data generator. You ONLY output valid JSON arrays of strings. No explanations, no markdown, no conversational text."""
237
+
238
+ user_prompt = f"""Based on the Original Text, generate exactly {Q} different questions that are directly answered by the provided factoid.
239
+
240
+ Original Text: "{original_answer}"
241
+
242
+ Factoid to target: "{factoid}"
243
+
244
+ Rules:
245
+ 1. Generate exactly {Q} questions.
246
+ 2. Vary the phrasing of the questions.
247
+ 3. Be specific: ALWAYS include the names of the objects, people, or places in your questions so they can be understood without seeing the original text.
248
+ 4. Do not ask questions about information that is NOT in the factoid.
249
+ 5. Respond ONLY with a valid JSON array of strings.
250
+
251
+ Example Input:
252
+ "Marie Curie moved to Paris for her studies"
253
+
254
+ Example Output:
255
+ ["Why did Marie Curie move to Paris?", "Where did Marie Curie move to for her studies?"]
256
+ """
257
+
258
+ # Retry loop: query the model and if the rate limit is hit, wait for 20 secs with a maximum of 3 retries
259
+ max_retries = 3
260
+ for attempt in range(max_retries):
261
+ try:
262
+ response = query_model(sys_prompt, user_prompt, model_name, max_tokens, client)
263
+ raw_output = response.get('answer', '').strip() # extract and strip the answer
264
+ clean_json_str = re.sub(r'```json|```', '', raw_output).strip() # remove potential json mark
265
+ questions = json.loads(clean_json_str) # load the answer into a dictionary
266
+ constrained_questions = [str(q).strip() for q in questions][:Q] # strip and ensure that there are max Q questions
267
+ return constrained_questions
268
+
269
+ except Exception as e:
270
+ error_str = str(e).lower()
271
+
272
+ # if the rate limit is hit, sleep for 20 secs
273
+ if enable_sleep and ('429' in error_str or 'rate limit' in error_str or 'too many requests' in error_str):
274
+ wait_time = 20 # Wait 20 seconds before trying again
275
+ print(f"⏳ Rate limit hit! Sleeping for {wait_time}s... (Attempt {attempt+1}/{max_retries})")
276
+ time.sleep(wait_time)
277
+ else:
278
+ print(f"❌ ERROR: API Error for factoid '{factoid[:30]}...': {e}")
279
+ if attempt == max_retries - 1 or not enable_sleep:
280
+ return []
281
+ return []
282
+
283
+ def generate_M_answers(question, M, model_name, max_tokens, client, enable_sleep=True):
284
+ '''
285
+ Provides M independent answers to the current question to measure model uncertainty.
286
+
287
+ Args:
288
+ - question: string, the question to answer
289
+ - M: int, number of times to ask the question
290
+ - model_name: string, name of the model
291
+ - max_tokens: int
292
+ - client: the proxy URL
293
+ - enable_sleep: boolean, enables sleep function if rate limit is hit
294
+
295
+ Returns:
296
+ - sub_answers: list of M answer strings
297
+ '''
298
+
299
+ sys_prompt = "You are a highly concise question-answering system. You ONLY output the direct answer entity. No full sentences, no conversational text."
300
+ user_prompt = f"""Question: {question}
301
+
302
+ Rules:
303
+ 1. Do NOT answer in a full sentence.
304
+ 2. Answer with as few words as possible (e.g., only a name, place, number, or thing).
305
+ 3. If multiple names/places/things are applicable, provide a EXACTLY 1 item.
306
+ 4. Provide ONLY the answer and absolutely nothing else.
307
+ """
308
+ sub_answers = []
309
+
310
+ for i in range(M):
311
+ # Again enable 3 retries if rate limit is hit
312
+ max_retries = 3
313
+ for attempt in range(max_retries):
314
+ try:
315
+ response = query_model(sys_prompt, user_prompt, model_name, max_tokens, client)
316
+ raw_answer = response.get('answer', '').strip()
317
+ sub_answers.append(raw_answer)
318
+ break # If a response is generated, break out of the retry loop and go to the next 'M'
319
+ except Exception as e:
320
+ error_str = str(e).lower()
321
+ if enable_sleep and ('429' in error_str or 'rate limit' in error_str or 'too many requests' in error_str):
322
+ wait_time = 20
323
+ print(f"⏳ Rate limit hit! Sleeping for {wait_time}s... (Attempt {attempt+1}/{max_retries})")
324
+ time.sleep(wait_time)
325
+ else:
326
+ print(f"❌ Error during API call {i+1} for question: {e}")
327
+ if attempt == max_retries - 1 or not enable_sleep:
328
+ sub_answers.append("ERROR")
329
+ break
330
+ return sub_answers
src/rendering.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ LEGEND_HTML = """
4
+ <div style='margin:10px 0; font-family:monospace;'>
5
+ <b>Token confidence:</b>&nbsp;
6
+ <span style='background:#ff0000; color:white; padding:4px 8px; border-radius:3px;'>Low (0%)</span>&nbsp;
7
+ <span style='background:#7f7f00; color:white; padding:4px 8px; border-radius:3px;'>Moderate (50%)</span>&nbsp;
8
+ <span style='background:#00ff00; color:black; padding:4px 8px; border-radius:3px;'>High (100%)</span>
9
+ </div>
10
+ """
11
+
12
+ TOOLTIP_CSS = """
13
+ span[data-tooltip] {
14
+ position: relative;
15
+ }
16
+ span[data-tooltip]:hover::after {
17
+ content: attr(data-tooltip);
18
+ position: absolute;
19
+ bottom: 130%;
20
+ left: 50%;
21
+ transform: translateX(-50%);
22
+ background: #333;
23
+ color: white;
24
+ padding: 4px 8px;
25
+ border-radius: 4px;
26
+ font-size: 0.75em;
27
+ white-space: nowrap;
28
+ z-index: 9999;
29
+ pointer-events: none;
30
+ }
31
+ """
32
+
33
+ def hex_to_rgb(hex_code):
34
+ r = int(hex_code[0:2], 16)
35
+ g = int(hex_code[2:4], 16)
36
+ b = int(hex_code[4:6], 16)
37
+ return r, g, b
38
+
39
+ def prob_to_color(avg_prob):
40
+ import numpy as np
41
+ norm_prob = np.clip(avg_prob, 0.0, 1.0)
42
+ r = int((1 - norm_prob) * 255)
43
+ g = int(norm_prob * 255)
44
+ b = 0
45
+ return f"{r:02x}{g:02x}{b:02x}"
46
+
47
+ def tokens_to_html(logprobs_content):
48
+ import math
49
+ spans = ""
50
+ for token in logprobs_content:
51
+ prob = math.exp(token.logprob)
52
+ hex_color = prob_to_color(prob)
53
+ r, g, b = hex_to_rgb(hex_color)
54
+ brightness = (r * 299 + g * 587 + b * 114) / 1000
55
+ text_color = "black" if brightness > 128 else "white"
56
+ token_text = token.token.replace(" ", "&nbsp;").replace("<", "&lt;").replace(">", "&gt;")
57
+ tooltip_text = f"p={prob:.3f} logprob={token.logprob:.3f}"
58
+ spans += (
59
+ f'<span '
60
+ f'data-tooltip="{tooltip_text}" '
61
+ f'style="background-color:#{hex_color}; color:{text_color}; '
62
+ f'padding:2px 4px; border-radius:3px; margin:1px; cursor:pointer;">'
63
+ f'{token_text}</span>'
64
+ )
65
+ return f"<p style='font-family: monospace; font-size: 1.1em; line-height: 2em;'>{spans}</p>"