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.
- ciol_astro_vqa-0.0.1/LICENSE +27 -0
- ciol_astro_vqa-0.0.1/PKG-INFO +24 -0
- ciol_astro_vqa-0.0.1/ciol_astro_vqa.egg-info/PKG-INFO +24 -0
- ciol_astro_vqa-0.0.1/ciol_astro_vqa.egg-info/SOURCES.txt +21 -0
- ciol_astro_vqa-0.0.1/ciol_astro_vqa.egg-info/dependency_links.txt +1 -0
- ciol_astro_vqa-0.0.1/ciol_astro_vqa.egg-info/requires.txt +14 -0
- ciol_astro_vqa-0.0.1/ciol_astro_vqa.egg-info/top_level.txt +2 -0
- ciol_astro_vqa-0.0.1/ciol_astrovqa_package/__init__.py +21 -0
- ciol_astro_vqa-0.0.1/ciol_astrovqa_package/evaluator.py +56 -0
- ciol_astro_vqa-0.0.1/ciol_astrovqa_package/evaluator_structured_signal_extraction.py +180 -0
- ciol_astro_vqa-0.0.1/ciol_astrovqa_package/holistic_harness.py +321 -0
- ciol_astro_vqa-0.0.1/ciol_astrovqa_package/initialize_benchmark.py +189 -0
- ciol_astro_vqa-0.0.1/ciol_astrovqa_package/prompt_variation.py +117 -0
- ciol_astro_vqa-0.0.1/ciol_astrovqa_package/utils.py +135 -0
- ciol_astro_vqa-0.0.1/pyproject.toml +41 -0
- ciol_astro_vqa-0.0.1/setup.cfg +4 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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 = ["."]
|