ciol-astro-vqa 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+
2
+ ---
3
+
4
+ # 📄 **LICENSE (MIT License)**
5
+
6
+ ```text id="license001"
7
+ MIT License
8
+
9
+ Copyright (c) 2026 CIOL
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: ciol-astro-vqa
3
+ Version: 0.0.1
4
+ Summary: A lightweight framework for benchmarking multimodal AI agents with parallel execution, prompt variation, and automated evaluation.
5
+ Author-email: Wahid Faisal <wahiddhrubo@gmail.com>
6
+ License: MIT
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: pandas
11
+ Requires-Dist: numpy
12
+ Requires-Dist: alive-progress
13
+ Requires-Dist: gitpython
14
+ Requires-Dist: huggingface-hub
15
+ Requires-Dist: openai
16
+ Requires-Dist: scipy
17
+ Requires-Dist: scikit-learn
18
+ Requires-Dist: torch
19
+ Requires-Dist: transformers
20
+ Requires-Dist: umap-learn
21
+ Requires-Dist: pillow
22
+ Requires-Dist: cohere
23
+ Requires-Dist: hdbscan
24
+ Dynamic: license-file
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: ciol-astro-vqa
3
+ Version: 0.0.1
4
+ Summary: A lightweight framework for benchmarking multimodal AI agents with parallel execution, prompt variation, and automated evaluation.
5
+ Author-email: Wahid Faisal <wahiddhrubo@gmail.com>
6
+ License: MIT
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: pandas
11
+ Requires-Dist: numpy
12
+ Requires-Dist: alive-progress
13
+ Requires-Dist: gitpython
14
+ Requires-Dist: huggingface-hub
15
+ Requires-Dist: openai
16
+ Requires-Dist: scipy
17
+ Requires-Dist: scikit-learn
18
+ Requires-Dist: torch
19
+ Requires-Dist: transformers
20
+ Requires-Dist: umap-learn
21
+ Requires-Dist: pillow
22
+ Requires-Dist: cohere
23
+ Requires-Dist: hdbscan
24
+ Dynamic: license-file
@@ -0,0 +1,21 @@
1
+ LICENSE
2
+ pyproject.toml
3
+ ./ciol_astrovqa_package/__init__.py
4
+ ./ciol_astrovqa_package/evaluator.py
5
+ ./ciol_astrovqa_package/evaluator_structured_signal_extraction.py
6
+ ./ciol_astrovqa_package/holistic_harness.py
7
+ ./ciol_astrovqa_package/initialize_benchmark.py
8
+ ./ciol_astrovqa_package/prompt_variation.py
9
+ ./ciol_astrovqa_package/utils.py
10
+ ciol_astro_vqa.egg-info/PKG-INFO
11
+ ciol_astro_vqa.egg-info/SOURCES.txt
12
+ ciol_astro_vqa.egg-info/dependency_links.txt
13
+ ciol_astro_vqa.egg-info/requires.txt
14
+ ciol_astro_vqa.egg-info/top_level.txt
15
+ ciol_astrovqa_package/__init__.py
16
+ ciol_astrovqa_package/evaluator.py
17
+ ciol_astrovqa_package/evaluator_structured_signal_extraction.py
18
+ ciol_astrovqa_package/holistic_harness.py
19
+ ciol_astrovqa_package/initialize_benchmark.py
20
+ ciol_astrovqa_package/prompt_variation.py
21
+ ciol_astrovqa_package/utils.py
@@ -0,0 +1,14 @@
1
+ pandas
2
+ numpy
3
+ alive-progress
4
+ gitpython
5
+ huggingface-hub
6
+ openai
7
+ scipy
8
+ scikit-learn
9
+ torch
10
+ transformers
11
+ umap-learn
12
+ pillow
13
+ cohere
14
+ hdbscan
@@ -0,0 +1,2 @@
1
+ ciol_astrovqa_package
2
+ dist
@@ -0,0 +1,21 @@
1
+ from .evaluator import HarnessEvaluator,structured_signal_extraction,cross_model_diagnostic_synthesis
2
+ from .holistic_harness import HarnessRunner
3
+ from .initialize_benchmark import process_benchmark, BenchmarkStats
4
+ from .prompt_variation import PromptVariationGenerator
5
+ from .evaluator_error_taxonomy_induction import cluster_name_mapping
6
+
7
+
8
+
9
+ __version__ = "0.1.5"
10
+
11
+ __all__ = [
12
+ "HarnessEvaluator",
13
+ "cross_model_diagnostic_synthesis",
14
+ "structured_signal_extraction",
15
+ "HarnessRunner",
16
+ "process_benchmark",
17
+ "PromptVariationGenerator",
18
+ "BenchmarkStats",
19
+ "cluster_name_mapping"
20
+ ]
21
+
@@ -0,0 +1,56 @@
1
+ # ================= STANDARD LIB =================
2
+ import os
3
+ from pathlib import Path
4
+
5
+ # ================= DATA =================
6
+ import pandas as pd
7
+
8
+
9
+
10
+ from .evaluator_structured_signal_extraction import structured_signal_extraction
11
+
12
+
13
+
14
+
15
+
16
+ def HarnessEvaluator(
17
+ results_path,
18
+ max_concurrent=10,
19
+ run_limit=None,
20
+ mcq_run=False,
21
+ model_name="gemma-4-31b-it",
22
+ ):
23
+
24
+ results_path=Path(results_path)
25
+
26
+ current_run=0
27
+
28
+ for model_folder in results_path.iterdir():
29
+ if model_folder.is_dir():
30
+ if run_limit is not None and current_run >= run_limit:
31
+ break
32
+
33
+ current_run += 1
34
+
35
+ response_csv_path= model_folder / "agent_response.csv"
36
+ score_csv_path= model_folder / "agent_scores.csv"
37
+ prev_df=None
38
+
39
+ if os.path.exists(score_csv_path) :
40
+ prev_df=pd.read_csv(score_csv_path)
41
+
42
+ structured_signal_extraction(
43
+ response_csv_path,
44
+ mcq_run=mcq_run,
45
+ max_concurrent=max_concurrent,
46
+ model_name=model_name,
47
+ prev_df=prev_df
48
+ )
49
+
50
+
51
+ if score_csv_path.exists():
52
+ df=pd.read_csv(score_csv_path)
53
+ if "accuracy_score" in df.columns:
54
+ print("Numbers of Failed Tasks: ",len(df[df["accuracy_score"].isna()]))
55
+
56
+
@@ -0,0 +1,180 @@
1
+ # ================= STANDARD LIB =================
2
+ from pathlib import Path
3
+ from concurrent.futures import ThreadPoolExecutor, as_completed
4
+
5
+ # ================= DATA =================
6
+ import pandas as pd
7
+
8
+ # ================= UTILITIES =================
9
+ from alive_progress import alive_bar
10
+
11
+ from .utils import get_client,json_parser
12
+
13
+
14
+ structured_signal_scoring_prompt_template = """
15
+ You are an expert evaluation agent. Your job is to rigorously evaluate a model's response.
16
+
17
+ Return ONLY a valid JSON object. Do NOT include any explanation, markdown, or extra text.
18
+
19
+ ================ INPUT ================
20
+
21
+ Question: {question}
22
+
23
+ Ground Truth Answer: {ground_truth}
24
+
25
+ Ground Truth Reasoning: {reasoning}
26
+
27
+ Model Response: {model_output}
28
+
29
+
30
+
31
+ ================ INSTRUCTIONS ================
32
+
33
+ Follow these rules strictly:
34
+
35
+ 1. Accuracy Score (0/1):
36
+ - 1 if the final answer is correct.
37
+ - 0 otherwise.
38
+ - For OE: use semantic equivalence.
39
+
40
+ 2. Resoning Score (1-10):
41
+ - How well the model reasoning matches Ground Truth Reasoning or the answer.
42
+ - The Better the Match The Higher The Score.
43
+
44
+ 3. Refusal Score (0/1):
45
+ - 1 if the model refuses (e.g., "I cannot answer", "insufficient information").
46
+ - 0 otherwise.
47
+
48
+ 4. Off Topic Score (0/1):
49
+ - 1 if response is irrelevant.
50
+ - 0 if relevant.
51
+
52
+ 5. Extract Reasoning:
53
+ - Extract and summarize the reasoning for the Score.
54
+
55
+ ================ OUTPUT FORMAT ================
56
+
57
+ Return STRICT JSON only (no trailing commas, no Python types, use null not None):
58
+
59
+
60
+ {{
61
+ "accuracy_score": 0,
62
+ "refusal_score": 0,
63
+ "off_topic_score": 0,
64
+ "reasoning": "",
65
+ }}
66
+
67
+
68
+ """
69
+
70
+
71
+
72
+
73
+
74
+ def process_row_for_signal_extraction(row,model_name,error_logs):
75
+
76
+ max_retry=5
77
+ retry_count=0
78
+ while retry_count<max_retry:
79
+ client = get_client()
80
+
81
+ try:
82
+
83
+ correct_answer = row["answer"]
84
+ model_answer = row["response"]
85
+ question = row["task_description"]
86
+ reasoning = row["Explanation"]
87
+
88
+
89
+
90
+ prompt = structured_signal_scoring_prompt_template.format(
91
+ question=question,
92
+ ground_truth=correct_answer,
93
+ model_output=model_answer,
94
+ reasoning=reasoning
95
+
96
+ )
97
+
98
+ content = [{"type": "text", "text": prompt}]
99
+
100
+ response = client.chat.completions.create(
101
+ model=model_name,
102
+ messages=[{"role": "user", "content": content}],
103
+ temperature=0.1,
104
+ max_tokens=1000,
105
+ )
106
+ res = json_parser(response.choices[0].message.content) if response else {}
107
+
108
+ return {**row, **res}
109
+
110
+ except Exception as e:
111
+
112
+ retry_count+=1
113
+ if retry_count==max_retry:
114
+ error_logs.append(f"Task {row["id"]}:{e}")
115
+ return row # return original row if failure
116
+
117
+ def structured_signal_extraction(csv_path,prev_df=None,model_name="google/gemini-3.1-flash-image-preview",max_concurrent=10):
118
+ save_path=(Path(csv_path.parent))
119
+
120
+ error_logs=[]
121
+
122
+
123
+
124
+ df=pd.read_csv(csv_path)
125
+ ids=[]
126
+
127
+ score_col="accuracy_score"
128
+ df=df[df["response"].notna()]
129
+
130
+ if prev_df is not None:
131
+ if score_col in prev_df.columns:
132
+ prev_df=prev_df[(prev_df[score_col].notna()) & (prev_df["response"].notna())]
133
+ ids = prev_df["id"].tolist()
134
+ df=df[~df["id"].isin(ids)]
135
+ print(f"Found ({len(ids)}) Previous tasks | New Tasks Remaining ({len(df)})")
136
+
137
+
138
+
139
+ ds= df.to_dict(orient="records")
140
+ model=str(save_path).split("/")[-1]
141
+
142
+ print(f"Scoring {model}")
143
+ results = []
144
+
145
+ with ThreadPoolExecutor(max_workers=max_concurrent) as executor:
146
+ futures = [executor.submit(process_row_for_signal_extraction, row,model_name,error_logs ) for row in ds]
147
+
148
+ with alive_bar(len(futures), bar='blocks', spinner='dots',
149
+ title='Scoring Agent Response', force_tty=True) as bar:
150
+
151
+ for future in as_completed(futures):
152
+ results.append(future.result())
153
+ pd.DataFrame(results).to_csv(save_path/"agent_scores.csv",index=False)
154
+ bar()
155
+
156
+ if prev_df is not None:
157
+
158
+ new_scores = pd.DataFrame(results)
159
+
160
+ # ✅ Ensure same columns (important)
161
+ all_cols = list(set(new_scores.columns).union(set(prev_df.columns)))
162
+ new_scores = new_scores.reindex(columns=all_cols)
163
+ prev_df = prev_df.reindex(columns=all_cols)
164
+
165
+ # ✅ Concat
166
+ df = pd.concat([prev_df, new_scores], ignore_index=True)
167
+
168
+
169
+ df.to_csv(save_path / "agent_scores.csv", index=False)
170
+ else:
171
+
172
+ pd.DataFrame(results).to_csv(save_path/"agent_scores.csv",index=False)
173
+
174
+ try:
175
+ if len(error_logs)>0:
176
+ f=open(save_path/f"Scoring_error{model}.txt","w")
177
+ f.write(str(error_logs))
178
+ f.close()
179
+ except Exception as E:
180
+ print("Error Saving Error Logs: ",E)
@@ -0,0 +1,321 @@
1
+ # =========================
2
+ # Standard library
3
+ # =========================
4
+ import os
5
+ import logging
6
+ import ast
7
+ import shutil
8
+ from pathlib import Path
9
+ from concurrent.futures import ThreadPoolExecutor, as_completed
10
+ import time
11
+
12
+ # =========================
13
+ # 3rd-party packages
14
+ # =========================
15
+ import pandas as pd
16
+ from alive_progress import alive_bar
17
+
18
+ # =========================
19
+ # Local modules
20
+ # =========================
21
+ from .initialize_benchmark import process_benchmark
22
+ from .prompt_variation import PromptVariationGenerator, generate_var_bench
23
+ from .evaluator import HarnessEvaluator
24
+ from .utils import encode_image, get_client, verify_run_name
25
+
26
+
27
+
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+
33
+
34
+
35
+ def agent(inputs: dict[str, dict], model_name: str,error_log,**kwargs):
36
+
37
+ client = get_client()
38
+
39
+ task_id, task = list(inputs.items())[0]
40
+
41
+ images_paths = [p for p in task["ques_image_path_lst"]]
42
+ response=None
43
+
44
+ max_retries = 1
45
+
46
+ if os.environ.get("COHERE_API_KEY", False):
47
+ for attempt in range(max_retries):
48
+ try:
49
+ # build content list
50
+ content = [{"type": "text", "text": task["task_description"]}]
51
+
52
+ for path in images_paths[:4]:
53
+ fmt, b64 = encode_image(path)
54
+
55
+ content.append({
56
+ "type": "image_url",
57
+ "image_url": {
58
+ "url": f"data:image/{fmt};base64,{b64}"
59
+ }
60
+ })
61
+
62
+ response = client.chat(
63
+ model=model_name,
64
+ messages=[
65
+ {
66
+ "role": "user",
67
+ "content": content
68
+ }
69
+ ],
70
+ temperature=0.1,
71
+ max_tokens=600,
72
+ )
73
+
74
+ content=response.message.content[0].text
75
+
76
+ inputs[task_id]["response"] = (
77
+ content.strip()
78
+ )
79
+ break # ✅ success → exit loop
80
+
81
+ except Exception as e:
82
+
83
+
84
+
85
+ if attempt < max_retries - 1:
86
+ sleep_time = 2 ** attempt # exponential backoff: 1s, 2s, 4s
87
+ time.sleep(sleep_time)
88
+ else:
89
+ error_log.append(f"❌ Failed at {max_retries} task {task_id}:{e}")
90
+ inputs[task_id]["response"] = None
91
+
92
+
93
+
94
+ else:
95
+ for attempt in range(max_retries):
96
+ try:
97
+ # build content list
98
+ content = [{"type": "text", "text": task["task_description"]}]
99
+ if "llama-3.2-11b-vision-instruct" in model_name :
100
+ path_len = 1
101
+ elif "qwen3.5-9b" in model_name:
102
+ path_len = 5
103
+ else:
104
+ path_len=len(images_paths)
105
+
106
+ for path in images_paths[:path_len]:
107
+ fmt, b64 = encode_image(path)
108
+
109
+ content.append({
110
+ "type": "image_url",
111
+ "image_url": {
112
+ "url": f"data:image/{fmt};base64,{b64}"
113
+ }
114
+ })
115
+
116
+ response = client.chat.completions.create(
117
+ model=model_name,
118
+ messages=[
119
+ {
120
+ "role": "user",
121
+ "content": content
122
+ }
123
+ ],
124
+ temperature=0.1,
125
+ max_tokens=1000,
126
+ reasoning_effort="low"
127
+ )
128
+ if not (response.choices):
129
+ print(response)
130
+
131
+ content=response.choices[0].message.content
132
+ reasoning=response.choices[0].message.reasoning
133
+
134
+ if not (content or reasoning):
135
+ print(response)
136
+
137
+ inputs[task_id]["response"] = (
138
+ content.strip() if content else reasoning
139
+ )
140
+ break # ✅ success → exit loop
141
+
142
+ except Exception as e:
143
+
144
+
145
+
146
+ if attempt < max_retries - 1:
147
+ sleep_time = 2 ** attempt # exponential backoff: 1s, 2s, 4s
148
+ time.sleep(sleep_time)
149
+ else:
150
+ error_log.append(f"❌ Failed at {max_retries} task {task_id}:{e}")
151
+ inputs[task_id]["response"] = None
152
+
153
+
154
+
155
+ return inputs
156
+
157
+
158
+
159
+ def run_agent_function(inputs: dict[str, dict], agent_function ,error_log, use_template: bool =False, model_name: str = "" , **kwargs):
160
+ if use_template:
161
+ res=agent_function(inputs,model_name=model_name,error_log=error_log)
162
+ else:
163
+ res=agent_function(inputs,)
164
+
165
+ return res
166
+
167
+
168
+
169
+
170
+ def HarnessRunner(
171
+ use_template=True,
172
+ custom_function=None,
173
+ template_name="default_agent",
174
+ run_mcq=False,
175
+ dataset_dir=None,
176
+ clean_dataset_dir=True,
177
+ template_model="gpt-4o-mini-2024-07-18",
178
+ run_id = "test",
179
+ max_concurrent = 5,
180
+ continue_run = False,
181
+ max_tasks = 0,
182
+ prompt_sensitivity = False,
183
+ num_variations = 3,
184
+ variation_strength = "mild",
185
+ results_dir = None,
186
+ task_ids = [],
187
+ categories = [],
188
+ sub_categories = [],
189
+ openai_api_key = None,
190
+ open_router_api_key = None,
191
+ cohere_api_key = None,
192
+ evaluation_needed=False,
193
+ custom_error_taxonomies=None
194
+ ):
195
+
196
+ failed_ids=[]
197
+
198
+ error_log=[]
199
+
200
+ results_dir = results_dir or ("mcq_results" if run_mcq else "results")
201
+
202
+ function_template = {
203
+ "default_agent": agent,
204
+ }
205
+
206
+ if openai_api_key:
207
+ os.environ["OPENAI_API_KEY"]=openai_api_key
208
+
209
+ if open_router_api_key:
210
+ os.environ["OPEN_ROUTER_API_KEY"]=open_router_api_key
211
+ if cohere_api_key:
212
+ os.environ["COHERE_API_KEY"]=cohere_api_key
213
+
214
+
215
+
216
+ if (not use_template) and (not custom_function):
217
+ raise ValueError(
218
+ "Select use_template or provide custom_function "
219
+ )
220
+
221
+
222
+ if use_template:
223
+ agent_function= function_template[template_name]
224
+ else:
225
+ agent_function= custom_function
226
+
227
+ temp_dir="./temp_dir"
228
+ verify_run_name(run_id)
229
+ Path(f"{results_dir}/{run_id}").mkdir(parents=True, exist_ok=True)
230
+ response_path=f"{results_dir}/{run_id}/agent_response.csv"
231
+
232
+
233
+ if os.path.exists(response_path) and (not continue_run):
234
+ raise ValueError("Run Already Exist. Pass continue_run if you want to Continue Run.")
235
+
236
+ if dataset_dir:
237
+ temp_dir=dataset_dir
238
+
239
+
240
+ df = process_benchmark(
241
+ temp_dir,
242
+ categories,
243
+ sub_categories,
244
+ task_ids,
245
+ max_tasks,
246
+ continue_run=continue_run,
247
+ eval_path=response_path,
248
+ mcq_prompt=run_mcq
249
+ )
250
+
251
+ dataset=df.set_index('id').to_dict(orient='index')
252
+
253
+
254
+ eval_results = {}
255
+
256
+ if continue_run and os.path.exists(response_path):
257
+ eval_results=pd.read_csv(response_path).set_index('id').to_dict(orient='index')
258
+
259
+ var_dataset=None
260
+
261
+
262
+ if prompt_sensitivity:
263
+
264
+
265
+ generator = PromptVariationGenerator(num_variations=num_variations, strength=variation_strength)
266
+ prompt_map = generator.generate_variations_for_dataset(dataset)
267
+ var_dataset=generate_var_bench(dataset, prompt_map)
268
+
269
+ active_dataset = var_dataset if var_dataset else dataset
270
+
271
+ def process_item(key, item):
272
+ """Generate prompt and run agent for a single item"""
273
+
274
+ res = run_agent_function(
275
+ inputs={key: item},
276
+ agent_function=agent_function,
277
+ use_template=use_template,
278
+ model_name=template_model,
279
+ error_log=error_log,
280
+ )
281
+
282
+ if (not res[key].get("response", False)):
283
+ failed_ids.append(key)
284
+
285
+ return key, res[key]
286
+
287
+
288
+ # Run in parallel with progress bar
289
+ with alive_bar(len(active_dataset), bar='blocks', spinner='dots', title=f'Running {run_id}', force_tty=True) as bar:
290
+ with ThreadPoolExecutor(max_workers=max_concurrent) as executor:
291
+ futures = {executor.submit(process_item, k, v): k for k, v in active_dataset.items()}
292
+
293
+ for future in as_completed(futures):
294
+ key, result = future.result()
295
+ eval_results[key] = result
296
+ pd.DataFrame.from_dict(eval_results, orient="index").rename_axis("id").reset_index().to_csv(response_path, index=False)
297
+ bar() # update progress
298
+
299
+ if len(failed_ids) > 0:
300
+ print(f"Failed {len(failed_ids)} tasks: {failed_ids}")
301
+
302
+ with open(f"{results_dir}/{run_id}/error_log.txt", "w") as f:
303
+ for e in error_log:
304
+ f.write(f"{repr(e)}\n")
305
+
306
+ if evaluation_needed:
307
+ # Run evaluator on results
308
+ HarnessEvaluator(
309
+ custom_error_taxonomies= custom_error_taxonomies,
310
+ max_concurrent=max_concurrent,
311
+ model_name=template_model,
312
+ results_path=results_dir,
313
+
314
+ )
315
+
316
+ if clean_dataset_dir:
317
+ shutil.rmtree(temp_dir)
318
+
319
+
320
+
321
+
@@ -0,0 +1,189 @@
1
+ import os
2
+ import ast
3
+ from pathlib import Path
4
+
5
+ # 3rd-party packages
6
+ import pandas as pd
7
+ from alive_progress import alive_bar
8
+ from huggingface_hub import snapshot_download
9
+ from zipfile import ZipFile
10
+ from importlib.resources import files
11
+ import re
12
+
13
+
14
+
15
+ def clean_options(text):
16
+ if pd.isna(text):
17
+ return ""
18
+
19
+ text = str(text) # ensure string
20
+ text = re.sub(r'\b[A-Za-z]\s*[\.\)\:-]\s*', ' | ', text)
21
+ return re.sub(r'^\s*\|\s*', '', text).strip()
22
+
23
+ class BenchmarkStats:
24
+ def __init__(self, ):
25
+ csv_path = files("ciol_astrovqa_package") / "annotations.csv"
26
+ self.df = pd.read_csv(csv_path)
27
+
28
+ def _print_stats(self, column, title):
29
+ pd.set_option("display.max_rows", None)
30
+
31
+ df_counts = (
32
+ self.df[column]
33
+ .value_counts()
34
+ .sort_values()
35
+ .rename_axis(title)
36
+ .reset_index(name="Count")
37
+ )
38
+
39
+ print(df_counts.to_markdown(index=False)) # clean table
40
+
41
+
42
+
43
+
44
+ def get_category_stats(self):
45
+ self._print_stats("Category", "Category")
46
+
47
+
48
+ def get_sub_category_stats(self):
49
+ self._print_stats("Sub-Category", "Sub Category")
50
+
51
+
52
+ def get_difficulty_stats(self):
53
+ self._print_stats("Difficulty", "Difficulty")
54
+
55
+
56
+
57
+
58
+ def initialize_benchmark(local_path):
59
+ token = os.environ.get("HF_TOKEN",None)
60
+ repo_id="ciol-research/AstroVQA"
61
+
62
+ if not token:
63
+ raise ValueError("Dataset is no Public Yet!!!")
64
+
65
+
66
+ snapshot_download(
67
+ repo_id=repo_id,
68
+ repo_type="dataset", # "dataset" or "model"
69
+ local_dir=local_path, # path
70
+ token=token,
71
+ allow_patterns=None, #download only some files
72
+ ignore_patterns=None, #ignore some files
73
+ )
74
+
75
+
76
+ zip_path = Path(local_path) / "question_images.zip"
77
+ extract_to = Path(local_path) / "question_images"
78
+
79
+ with ZipFile(zip_path, 'r') as z:
80
+ members = z.infolist()
81
+
82
+ with alive_bar(len(members), title="Extracting files") as bar:
83
+ for m in members:
84
+ z.extract(m, extract_to)
85
+ bar()
86
+
87
+ zip_path.unlink()
88
+
89
+
90
+ def prompt_generator(row):
91
+
92
+ prompt = f"""Answer the question using clear reasoning.
93
+
94
+ Question:
95
+ {row['question']}
96
+ """
97
+
98
+ prompt += "\n\nGive only the final answer and Add a short Reasoning Behind Your Answer."
99
+
100
+ return prompt
101
+
102
+ def mcq_prompt_generator(row):
103
+
104
+ prompt = f"""Answer the question using clear reasoning.
105
+
106
+ Question:
107
+ {row['question']}
108
+ """
109
+
110
+ choices=f"{row['Option 1']}, {row['Option 2']}, {row['Option 3']}, {row['Option 4']}"
111
+ prompt += f"\nContext (Some possible answers): {choices}"
112
+
113
+ prompt += "\n\nGive only the final answer in one sentence."
114
+
115
+ return prompt
116
+
117
+
118
+
119
+
120
+
121
+ def process_benchmark(
122
+ temp_dir,
123
+ categories=[],
124
+ sub_categories=[],
125
+ task_ids=[],
126
+ max_tasks=0,
127
+ continue_run=False,
128
+ eval_path="",
129
+ mcq_prompt=False,
130
+ ):
131
+
132
+ if not os.path.exists(temp_dir) :
133
+ initialize_benchmark(temp_dir)
134
+
135
+
136
+
137
+ df=pd.read_csv(f"{temp_dir}/annotations.csv").drop_duplicates(subset=["id"])
138
+
139
+ df["ques_image_path_lst"] = df["ques_image_path_lst"].apply(ast.literal_eval)
140
+
141
+ df["ques_image_path_lst"] = df["ques_image_path_lst"].apply(
142
+ lambda paths: [os.path.join(temp_dir, "question_images", p) for p in paths]
143
+ )
144
+
145
+
146
+
147
+ if len(categories):
148
+ df=df[df["category"].isin(categories)]
149
+
150
+ if len(task_ids):
151
+ df=df[df["task"].isin(task_ids)]
152
+
153
+ if len(sub_categories):
154
+ df=df[df["sub_category"].isin(sub_categories)]
155
+
156
+ ids = []
157
+
158
+ if continue_run:
159
+ if os.path.exists(eval_path):
160
+ prev_df = pd.read_csv(eval_path)
161
+
162
+ # keep only valid completed rows
163
+ if "response" in prev_df.columns:
164
+ valid_prev = prev_df[prev_df["response"].notna()]
165
+
166
+ ids = valid_prev["id"].tolist()
167
+ else:
168
+ ids = []
169
+
170
+ # filter current df
171
+ df = df[~df["id"].isin(ids)].copy()
172
+
173
+ print(f"Found ({len(ids)}) Previous tasks | New Tasks Remaining ({len(df)})")
174
+
175
+
176
+
177
+ if max_tasks and max_tasks > 0 and max_tasks < len(df):
178
+ print(f"Limiting to the first {max_tasks} tasks as requested")
179
+ df=df[:max_tasks]
180
+
181
+
182
+ df["task_description"]=(
183
+ df.apply(lambda x: mcq_prompt_generator(x),axis=1)
184
+ if mcq_prompt
185
+ else df.apply(lambda x: prompt_generator(x),axis=1)
186
+ )
187
+
188
+ return df
189
+
@@ -0,0 +1,117 @@
1
+
2
+ from typing import Dict, List
3
+ from .utils import get_client
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+ # ---------------- Config ----------------
12
+
13
+ # Instructions per strength
14
+ STRENGTH_INSTRUCTIONS = {
15
+ "mild": (
16
+ "Generate small, surface-level variations of the prompt. "
17
+ "Use synonyms, minor changes in formality, active/passive voice changes. "
18
+ "Keep all information, meaning, and structure intact. "
19
+ "Output should be fluent and similar in style to the original."
20
+ ),
21
+ "medium": (
22
+ "Generate variations that restructure sentences and reorder information. "
23
+ "Combine or split sentences, slightly change phrasing, but preserve all meaning. "
24
+ "Output should still be clear and professional, resembling the original style."
25
+ ),
26
+ "strong": (
27
+ "Generate majorly different variations that sound like different people wrote them. "
28
+ "Use conversational phrasing, varied sentence structures, and style differences. "
29
+ "Preserve all information and constraints exactly. Output should maintain readability."
30
+ ),
31
+ "naturalistic": (
32
+ "Generate realistic user-style variations as if typed by a real person. "
33
+ "Include casual phrasing, abbreviations, typos, and informal chat patterns. "
34
+ "Keep all meaning and requirements intact. Output should still resemble the original prompt in content."
35
+ ),
36
+ }
37
+
38
+ STRENGTH_TEMPERATURE = {
39
+ "mild": 0.7,
40
+ "medium": 0.8,
41
+ "strong": 0.9,
42
+ "naturalistic": 0.9,
43
+ }
44
+
45
+
46
+ class PromptVariationGenerator:
47
+ def __init__(self, model_name="gpt-4o-mini-2024-07-18", num_variations=2, strength="medium"):
48
+ self.model_name = model_name
49
+ self.num_variations = num_variations
50
+ self.strength = strength.lower()
51
+ self.client = get_client()
52
+
53
+ def generate_variations_for_task(self, task_id: str, prompt: str) -> List[Dict]:
54
+ instruction = STRENGTH_INSTRUCTIONS.get(self.strength, STRENGTH_INSTRUCTIONS["mild"])
55
+ temperature = STRENGTH_TEMPERATURE.get(self.strength, 0.7)
56
+
57
+ system_prompt = f"You are an expert at paraphrasing prompts while preserving meaning.\nInstruction: {instruction}"
58
+ user_prompt = f"Original prompt:\n{prompt}\nGenerate {self.num_variations} variations, one per line. Do NOT number them."
59
+
60
+ try:
61
+ response = self.client.chat.completions.create(
62
+ model=self.model_name,
63
+ messages=[
64
+ {"role": "system", "content": system_prompt},
65
+ {"role": "user", "content": user_prompt},
66
+ ],
67
+ temperature=temperature,
68
+ max_tokens=1000,
69
+ )
70
+
71
+ lines = response.choices[0].message.content.strip().split("\n")
72
+ variations = [line.strip() for line in lines if len(line.strip()) > 10]
73
+
74
+ except Exception as e:
75
+ print(f"Error generating variations for task {task_id}: {e}")
76
+ variations = []
77
+
78
+ all_variations = [prompt] + variations[:self.num_variations]
79
+
80
+ return [
81
+ {"question": v, "prompt_variation_id": i, "prompt_variation_strength": self.strength}
82
+ for i, v in enumerate(all_variations)
83
+ ]
84
+
85
+ def generate_variations_for_dataset(self, dataset: Dict[str, Dict], prompt_field: str = "question") -> Dict[str, List[Dict]]:
86
+ result = {}
87
+ for task_id, task_data in dataset.items():
88
+ if prompt_field not in task_data:
89
+ result[task_id] = [{
90
+ "question": task_data.get(prompt_field, ""),
91
+ "prompt_variation_id": 0,
92
+ "prompt_variation_strength": self.strength
93
+ }]
94
+ continue
95
+ prompt = task_data[prompt_field]
96
+ result[task_id] = self.generate_variations_for_task(task_id, prompt)
97
+ return result
98
+
99
+
100
+
101
+ def generate_var_bench( dataset, prompt_map):
102
+ new_dataset={}
103
+ for d_id,d in dataset.items():
104
+
105
+ for idx,q in enumerate(prompt_map[d_id]):
106
+ new_dataset[f'{d_id}-{str(idx).zfill(2)}'] = {
107
+ **dataset[d_id],
108
+ "question":q['question']
109
+ }
110
+
111
+ return new_dataset
112
+
113
+
114
+
115
+
116
+
117
+
@@ -0,0 +1,135 @@
1
+ from PIL import Image
2
+ import os
3
+ from openai import OpenAI
4
+ import base64
5
+
6
+ import re
7
+ import os
8
+
9
+ import json
10
+
11
+
12
+
13
+ import re
14
+ import json
15
+
16
+ def json_parser(json_text):
17
+ try:
18
+ # 1. Extract JSON block
19
+ match = re.search(r"\{.*\}", json_text, re.S)
20
+ if not match:
21
+ raise ValueError("No JSON object found")
22
+
23
+ text = match.group()
24
+
25
+ # 2. Clean problematic patterns
26
+
27
+ # Remove LaTeX blocks: $...$
28
+ text = re.sub(r"\$.*?\$", "", text)
29
+
30
+ # Remove backslashes (e.g., \rightarrow)
31
+ text = re.sub(r"\\+", "", text)
32
+
33
+ # Remove non-ASCII chars (optional but useful)
34
+ text = re.sub(r"[^\x00-\x7F]+", "", text)
35
+
36
+ # Fix trailing commas
37
+ text = re.sub(r",\s*}", "}", text)
38
+ text = re.sub(r",\s*]", "]", text)
39
+
40
+ # 3. Try parsing
41
+ return json.loads(text)
42
+
43
+ except Exception as e:
44
+ print(f"❌ JSON Parse Error: {e}")
45
+ print("🔎 Raw text:", json_text)
46
+
47
+ # 4. Fallback: return empty dict instead of crashing
48
+ return {}
49
+
50
+
51
+ class InvalidPathNameError(ValueError):
52
+ """Custom exception for invalid path names."""
53
+ pass
54
+
55
+ def verify_run_name(name: str) -> str:
56
+ """
57
+ Verify that the given name is a valid filename/folder name.
58
+
59
+ Rules:
60
+ - Cannot contain forbidden characters: \ / : * ? " < > |
61
+ - Cannot be empty or only whitespace
62
+ - Cannot be a reserved Windows name (CON, PRN, AUX, NUL, COM1..9, LPT1..9)
63
+
64
+ Returns the stripped valid name if valid.
65
+ Raises InvalidPathNameError if invalid.
66
+ """
67
+ if not name or name.strip() == "":
68
+ raise InvalidPathNameError("Path name cannot be empty or whitespace.")
69
+
70
+ name = name.strip()
71
+
72
+ # Forbidden characters
73
+ forbidden_chars = r'[<>:"/\\|?*]'
74
+ if re.search(forbidden_chars, name):
75
+ raise InvalidPathNameError(
76
+ f"Path name '{name}' contains forbidden characters: \\ / : * ? \" < > |"
77
+ )
78
+
79
+ # Reserved Windows names
80
+ reserved = {"CON","PRN","AUX","NUL"} | {f"COM{i}" for i in range(1,10)} | {f"LPT{i}" for i in range(1,10)}
81
+ if name.upper() in reserved:
82
+ raise InvalidPathNameError(f"Path name '{name}' is a reserved system name.")
83
+
84
+ return name
85
+
86
+ import io, base64
87
+
88
+ def encode_image(path):
89
+ img = Image.open(path)
90
+ buffer = io.BytesIO()
91
+
92
+ fmt = img.format if img.format else "PNG"
93
+ img.save(buffer, format=fmt)
94
+
95
+ return fmt.lower(), base64.b64encode(buffer.getvalue()).decode()
96
+
97
+
98
+ import os
99
+ from openai import OpenAI
100
+ import cohere
101
+
102
+
103
+ def get_client(provider="auto"):
104
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
105
+ OPEN_ROUTER_API_KEY = os.environ.get("OPEN_ROUTER_API_KEY")
106
+ COHERE_API_KEY = os.environ.get("COHERE_API_KEY")
107
+
108
+ # Auto selection priority
109
+ if provider == "auto":
110
+ if OPEN_ROUTER_API_KEY:
111
+ provider = "openrouter"
112
+ elif OPENAI_API_KEY:
113
+ provider = "openai"
114
+ elif COHERE_API_KEY:
115
+ provider = "cohere"
116
+ else:
117
+ raise ValueError("No API key found.")
118
+
119
+ # OpenRouter (supports Cohere models via routing)
120
+ if provider == "openrouter":
121
+ return OpenAI(
122
+ base_url="https://openrouter.ai/api/v1",
123
+ api_key=OPEN_ROUTER_API_KEY
124
+ )
125
+
126
+ # OpenAI
127
+ elif provider == "openai":
128
+ return OpenAI(api_key=OPENAI_API_KEY)
129
+
130
+ # Cohere (native)
131
+ elif provider == "cohere":
132
+ return cohere.ClientV2(COHERE_API_KEY)
133
+
134
+ else:
135
+ raise ValueError(f"Unknown provider: {provider}")
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ciol-astro-vqa"
7
+ version = "0.0.1"
8
+ description = "A lightweight framework for benchmarking multimodal AI agents with parallel execution, prompt variation, and automated evaluation."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ authors = [
12
+ { name = "Wahid Faisal", email = "wahiddhrubo@gmail.com" }
13
+ ]
14
+ license = { text = "MIT" }
15
+
16
+ dependencies = [
17
+ "pandas",
18
+ "numpy",
19
+ "alive-progress",
20
+ "gitpython",
21
+ "huggingface-hub",
22
+ "openai",
23
+ "scipy",
24
+ "scikit-learn",
25
+ "torch",
26
+ "transformers",
27
+ "umap-learn",
28
+ "pillow",
29
+ "cohere",
30
+ "hdbscan"
31
+ ]
32
+
33
+ [tool.setuptools]
34
+ package-dir = {"" = "."}
35
+ include-package-data = true
36
+
37
+ [tool.setuptools.package-data]
38
+ holistic_math_harness = ["*.csv"]
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["."]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+