QuantumChecker 0.2.7__tar.gz → 0.2.8__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.
- quantumchecker-0.2.8/PKG-INFO +138 -0
- quantumchecker-0.2.8/QuantumCheck/main.py +188 -0
- {quantumchecker-0.2.7 → quantumchecker-0.2.8}/QuantumCheck/powerbi_evaluator.py +37 -40
- {quantumchecker-0.2.7 → quantumchecker-0.2.8}/QuantumCheck/prompts.py +41 -61
- quantumchecker-0.2.8/QuantumCheck/python_evaluator.py +194 -0
- quantumchecker-0.2.8/QuantumCheck/sql_evaluator.py +196 -0
- quantumchecker-0.2.8/QuantumCheck/ssis_evaluator.py +292 -0
- quantumchecker-0.2.8/QuantumChecker.egg-info/PKG-INFO +138 -0
- quantumchecker-0.2.8/README.md +112 -0
- {quantumchecker-0.2.7 → quantumchecker-0.2.8}/setup.py +1 -1
- quantumchecker-0.2.8/tests/test.py +388 -0
- quantumchecker-0.2.7/PKG-INFO +0 -34
- quantumchecker-0.2.7/QuantumCheck/main.py +0 -125
- quantumchecker-0.2.7/QuantumCheck/python_evaluator.py +0 -95
- quantumchecker-0.2.7/QuantumCheck/sql_evaluator.py +0 -97
- quantumchecker-0.2.7/QuantumCheck/ssis_evaluator.py +0 -136
- quantumchecker-0.2.7/QuantumChecker.egg-info/PKG-INFO +0 -34
- quantumchecker-0.2.7/README.md +0 -8
- quantumchecker-0.2.7/tests/test.py +0 -31
- {quantumchecker-0.2.7 → quantumchecker-0.2.8}/QuantumCheck/__init__.py +0 -0
- {quantumchecker-0.2.7 → quantumchecker-0.2.8}/QuantumChecker.egg-info/SOURCES.txt +0 -0
- {quantumchecker-0.2.7 → quantumchecker-0.2.8}/QuantumChecker.egg-info/dependency_links.txt +0 -0
- {quantumchecker-0.2.7 → quantumchecker-0.2.8}/QuantumChecker.egg-info/requires.txt +0 -0
- {quantumchecker-0.2.7 → quantumchecker-0.2.8}/QuantumChecker.egg-info/top_level.txt +0 -0
- {quantumchecker-0.2.7 → quantumchecker-0.2.8}/setup.cfg +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: QuantumChecker
|
|
3
|
+
Version: 0.2.8
|
|
4
|
+
Summary: A package to evaluate homework submissions in Python, SQL, PowerBI, and SSIS.
|
|
5
|
+
Author: Qobiljon
|
|
6
|
+
Author-email: qobiljonkhayrullayev@gmail.com
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.6
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: requests>=2.31.0
|
|
13
|
+
Requires-Dist: tenacity>=8.2.3
|
|
14
|
+
Requires-Dist: pdf2image>=1.16.3
|
|
15
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
16
|
+
Requires-Dist: Pillow>=10.0.0
|
|
17
|
+
Requires-Dist: PyPDF2>=3.0.1
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: author-email
|
|
20
|
+
Dynamic: classifier
|
|
21
|
+
Dynamic: description
|
|
22
|
+
Dynamic: description-content-type
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
Dynamic: summary
|
|
26
|
+
|
|
27
|
+
# 📘 HomeworkEvaluator
|
|
28
|
+
|
|
29
|
+
The **HomeworkEvaluator** is a Python-based evaluation engine designed to automatically assess student assignments across different technologies including Python, SQL, Power BI, and SSIS. It uses AI to parse and evaluate student-submitted answers against a set of markdown-formatted questions.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## ✨ Features
|
|
34
|
+
|
|
35
|
+
- Supports multiple file types:
|
|
36
|
+
- `.py` → Python
|
|
37
|
+
- `.sql` → SQL
|
|
38
|
+
- `.zip` → Power BI
|
|
39
|
+
- `.dtsx` / `.DTSX` → SSIS
|
|
40
|
+
- `.txt` / `.md` → Plain Text
|
|
41
|
+
- Smart evaluator routing based on file extension.
|
|
42
|
+
- AI-powered feedback generation and scoring.
|
|
43
|
+
- Logging for each evaluation by file type and timestamp.
|
|
44
|
+
- Automatic fallback to backup API keys when quota is exceeded.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 📦 Folder Structure
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
.
|
|
52
|
+
├── homework_evaluator/
|
|
53
|
+
│ ├── homework_evaluator.py # Main evaluator class
|
|
54
|
+
│ ├── python_evaluator.py # Python evaluator logic
|
|
55
|
+
│ ├── sql_evaluator.py # SQL evaluator logic
|
|
56
|
+
│ ├── powerbi_evaluator.py # Power BI evaluator logic
|
|
57
|
+
│ ├── ssis_evaluator.py # SSIS evaluator logic
|
|
58
|
+
│ └── logs/ # Log files categorized by type and timestamp
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 🔧 Installation
|
|
64
|
+
|
|
65
|
+
Clone this repository and install the necessary dependencies:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/yourusername/homework-evaluator.git
|
|
69
|
+
cd homework-evaluator
|
|
70
|
+
pip install -r requirements.txt
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 🧠 Usage
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from homework_evaluator import HomeworkEvaluator
|
|
79
|
+
|
|
80
|
+
evaluator = HomeworkEvaluator()
|
|
81
|
+
|
|
82
|
+
result = evaluator.evaluate_from_content(
|
|
83
|
+
question_content=markdown_questions,
|
|
84
|
+
answer_path="/path/to/answer/file.py",
|
|
85
|
+
api_key="your-main-api-key",
|
|
86
|
+
backup_api_keys=["backup-key-1", "backup-key-2"]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
print(result["mark"]) # e.g., 85
|
|
90
|
+
print(result["feedback"]) # Structured feedback
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 🗂️ Question Format
|
|
96
|
+
|
|
97
|
+
The evaluator expects `question_content` as a Markdown-formatted string where each question is separated by a **double newline** (`\n\n`). Example:
|
|
98
|
+
|
|
99
|
+
```markdown
|
|
100
|
+
### Q1
|
|
101
|
+
Write a Python function to reverse a string.
|
|
102
|
+
|
|
103
|
+
### Q2
|
|
104
|
+
Explain the purpose of list comprehensions in Python.
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 🛠️ Logging
|
|
110
|
+
|
|
111
|
+
All evaluations are logged under the `logs/` directory, grouped by file type and timestamp.
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
logs/
|
|
115
|
+
├── python/
|
|
116
|
+
│ └── evaluation_2025-05-26_14-00-00.log
|
|
117
|
+
├── sql/
|
|
118
|
+
│ └── evaluation_2025-05-26_14-10-12.log
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 🧪 Exception Handling
|
|
124
|
+
|
|
125
|
+
- If a file is not found or questions are malformed, an informative error is raised.
|
|
126
|
+
- If the API quota is exceeded (429 errors or rate limits), it retries using backup keys.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 📄 License
|
|
131
|
+
|
|
132
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 🤝 Contributing
|
|
137
|
+
|
|
138
|
+
Pull requests are welcome. For major changes, please open an issue first to discuss your ideas.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import zipfile
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import List, Dict, Optional
|
|
6
|
+
from .python_evaluator import PythonEvaluator
|
|
7
|
+
from .sql_evaluator import SQLEvaluator
|
|
8
|
+
from .powerbi_evaluator import PowerBIEvaluator
|
|
9
|
+
from .ssis_evaluator import SSISEvaluator
|
|
10
|
+
from pprint import pprint
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
class HomeworkEvaluator:
|
|
15
|
+
EXTENSION_TO_TYPE = {
|
|
16
|
+
".py": "python",
|
|
17
|
+
".sql": "sql",
|
|
18
|
+
".pbit": "powerbi",
|
|
19
|
+
".pdf": "powerbi",
|
|
20
|
+
".dtsx": "ssis",
|
|
21
|
+
".DTSX": "ssis",
|
|
22
|
+
".txt": "text",
|
|
23
|
+
".md": "text"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def _setup_logger(self, file_type: str) -> logging.Logger:
|
|
27
|
+
base_log_dir = os.path.join(os.path.dirname(__file__), "logs")
|
|
28
|
+
type_log_dir = os.path.join(base_log_dir, file_type)
|
|
29
|
+
os.makedirs(type_log_dir, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
32
|
+
log_file_path = os.path.join(type_log_dir, f"evaluation_{timestamp}.log")
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(f"{file_type}_{timestamp}")
|
|
35
|
+
logger.setLevel(logging.INFO)
|
|
36
|
+
|
|
37
|
+
if not logger.handlers:
|
|
38
|
+
file_handler = logging.FileHandler(log_file_path, encoding="utf-8")
|
|
39
|
+
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
|
40
|
+
file_handler.setFormatter(formatter)
|
|
41
|
+
logger.addHandler(file_handler)
|
|
42
|
+
|
|
43
|
+
return logger
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def parse_questions(md_content: str) -> List[str]:
|
|
47
|
+
questions = [q.strip() for q in md_content.strip().split("\n\n") if q.strip()]
|
|
48
|
+
if not questions:
|
|
49
|
+
logger.error("No valid questions found in the question content")
|
|
50
|
+
raise ValueError("No valid questions found in the question content")
|
|
51
|
+
logger.info("Parsed %d questions from content", len(questions))
|
|
52
|
+
return questions
|
|
53
|
+
|
|
54
|
+
def _detect_zip_content_type(self, zip_path: str) -> str:
|
|
55
|
+
"""Determine the file type based on ZIP contents."""
|
|
56
|
+
try:
|
|
57
|
+
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
|
58
|
+
files = [f for f in zip_ref.namelist() if not f.startswith('__MACOSX/')]
|
|
59
|
+
extensions = {os.path.splitext(f)[1].lower() for f in files if os.path.splitext(f)[1]}
|
|
60
|
+
|
|
61
|
+
if not extensions:
|
|
62
|
+
logger.warning("No valid files found in ZIP: %s", zip_path)
|
|
63
|
+
return "text"
|
|
64
|
+
|
|
65
|
+
# Check for specific file types in order of priority: sql, powerbi, ssis, python
|
|
66
|
+
for ext in [".sql", ".pbit", ".pdf", ".dtsx", ".DTSX", ".py"]:
|
|
67
|
+
if ext in extensions and ext in self.EXTENSION_TO_TYPE:
|
|
68
|
+
file_type = self.EXTENSION_TO_TYPE[ext]
|
|
69
|
+
logger.info("Detected file type: %s from extension: %s in ZIP: %s", file_type, ext, zip_path)
|
|
70
|
+
return file_type
|
|
71
|
+
|
|
72
|
+
# Fallback to text if only .txt or .md are present
|
|
73
|
+
if extensions.issubset({".txt", ".md"}):
|
|
74
|
+
logger.info("Defaulting to text type for ZIP contents with extensions: %s", extensions)
|
|
75
|
+
return "text"
|
|
76
|
+
|
|
77
|
+
logger.warning("No recognized specific file types in ZIP: %s, extensions: %s", zip_path, extensions)
|
|
78
|
+
return "text"
|
|
79
|
+
except zipfile.BadZipFile:
|
|
80
|
+
logger.error("Invalid ZIP file: %s", zip_path)
|
|
81
|
+
return "text"
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error("Error inspecting ZIP file %s: %s", zip_path, str(e))
|
|
84
|
+
return "text"
|
|
85
|
+
|
|
86
|
+
def evaluate_from_content(
|
|
87
|
+
self,
|
|
88
|
+
question_content: str,
|
|
89
|
+
answer_path: str,
|
|
90
|
+
api_key: str,
|
|
91
|
+
backup_api_keys: Optional[List[str]] = None,
|
|
92
|
+
) -> Dict[str, any]:
|
|
93
|
+
if backup_api_keys is None:
|
|
94
|
+
backup_api_keys = []
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
questions = self.parse_questions(question_content)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error("Failed to parse question content: %s", str(e))
|
|
100
|
+
return {
|
|
101
|
+
"score": 0,
|
|
102
|
+
"feedback": f"Error parsing question content: {str(e)}",
|
|
103
|
+
"issues": [str(e)],
|
|
104
|
+
"recommendations": []
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
answer_path = answer_path.strip()
|
|
108
|
+
_, ext = os.path.splitext(answer_path)
|
|
109
|
+
ext = ext.lower()
|
|
110
|
+
|
|
111
|
+
# Determine file type
|
|
112
|
+
if ext == ".zip":
|
|
113
|
+
file_type = self._detect_zip_content_type(answer_path)
|
|
114
|
+
else:
|
|
115
|
+
file_type = self.EXTENSION_TO_TYPE.get(ext, "text")
|
|
116
|
+
|
|
117
|
+
eval_logger = self._setup_logger(file_type)
|
|
118
|
+
eval_logger.info("Processing answer_path: %s", answer_path)
|
|
119
|
+
eval_logger.info("Extracted extension: %s", ext)
|
|
120
|
+
eval_logger.info("Detected file type: %s for file: %s", file_type, answer_path)
|
|
121
|
+
pprint(f"Processing {len(questions)} questions for file type: {file_type}")
|
|
122
|
+
|
|
123
|
+
if not os.path.exists(answer_path):
|
|
124
|
+
eval_logger.error("Answer file not found: %s", answer_path)
|
|
125
|
+
return {
|
|
126
|
+
"score": 0,
|
|
127
|
+
"feedback": f"Answer file not found: {answer_path}",
|
|
128
|
+
"issues": [f"Answer file not found: {answer_path}"],
|
|
129
|
+
"recommendations": []
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
def create_evaluator(ftype, key):
|
|
133
|
+
if ftype == "python":
|
|
134
|
+
eval_logger.info("Using PythonEvaluator for file type: %s", ftype)
|
|
135
|
+
return PythonEvaluator(key)
|
|
136
|
+
elif ftype == "sql":
|
|
137
|
+
eval_logger.info("Using SQLEvaluator for file type: %s", ftype)
|
|
138
|
+
return SQLEvaluator(key)
|
|
139
|
+
elif ftype == "powerbi":
|
|
140
|
+
eval_logger.info("Using PowerBIEvaluator for file type: %s", ftype)
|
|
141
|
+
return PowerBIEvaluator(key)
|
|
142
|
+
elif ftype == "ssis":
|
|
143
|
+
eval_logger.info("Using SSISEvaluator for file type: %s", ftype)
|
|
144
|
+
return SSISEvaluator(key)
|
|
145
|
+
else:
|
|
146
|
+
eval_logger.warning("Unknown file type %s, defaulting to PythonEvaluator", ftype)
|
|
147
|
+
return PythonEvaluator(key)
|
|
148
|
+
|
|
149
|
+
keys_to_try = [api_key] + backup_api_keys[:5]
|
|
150
|
+
|
|
151
|
+
last_exception = None
|
|
152
|
+
for i, key in enumerate(keys_to_try):
|
|
153
|
+
evaluator = create_evaluator(file_type, key)
|
|
154
|
+
try:
|
|
155
|
+
evaluation = evaluator.evaluate(questions, answer_path)
|
|
156
|
+
eval_logger.info(f"Evaluation complete with API key #{i + 1}: Score = {evaluation.get('score')}")
|
|
157
|
+
return {
|
|
158
|
+
"score": evaluation.get("score", 0),
|
|
159
|
+
"feedback": evaluation.get("feedback", "No feedback provided"),
|
|
160
|
+
"issues": evaluation.get("issues", []),
|
|
161
|
+
"recommendations": evaluation.get("recommendations", [])
|
|
162
|
+
}
|
|
163
|
+
except Exception as e:
|
|
164
|
+
error_msg = str(e).lower()
|
|
165
|
+
if (
|
|
166
|
+
"429" in error_msg
|
|
167
|
+
or "rate limit" in error_msg
|
|
168
|
+
or "quota exceeded" in error_msg
|
|
169
|
+
or "daily limit exceeded" in error_msg
|
|
170
|
+
or "quota" in error_msg
|
|
171
|
+
):
|
|
172
|
+
eval_logger.warning(f"API key #{i + 1} limited or quota exceeded. Trying next key if available.")
|
|
173
|
+
last_exception = e
|
|
174
|
+
continue
|
|
175
|
+
else:
|
|
176
|
+
eval_logger.error(f"Evaluation failed with API key #{i + 1}: %s", str(e))
|
|
177
|
+
return {
|
|
178
|
+
"score": 0,
|
|
179
|
+
"feedback": f"Evaluation failed: {str(e)}",
|
|
180
|
+
"issues": [str(e)],
|
|
181
|
+
"recommendations": []
|
|
182
|
+
}
|
|
183
|
+
else:
|
|
184
|
+
eval_logger.error("All API keys exhausted and evaluation failed.")
|
|
185
|
+
return {
|
|
186
|
+
"score": 0,
|
|
187
|
+
"feedback": f"All API keys exhausted: {str(last_exception) if last_exception else 'Unknown error'}",
|
|
188
|
+
}
|
|
@@ -15,7 +15,6 @@ import io
|
|
|
15
15
|
import base64
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
# Placeholder for prompts.py content
|
|
19
18
|
def prompt_text_powerbi(combined_content: str) -> str:
|
|
20
19
|
return f"""
|
|
21
20
|
Evaluate the following Power BI DAX question-answer pairs for correctness, clarity, and appropriateness.
|
|
@@ -46,19 +45,20 @@ class GeminiFlashModel:
|
|
|
46
45
|
self.model_name = model_name
|
|
47
46
|
self.endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent"
|
|
48
47
|
|
|
49
|
-
@retry(
|
|
50
|
-
|
|
48
|
+
@retry(
|
|
49
|
+
stop=stop_after_attempt(3),
|
|
50
|
+
wait=wait_exponential(min=4, max=10),
|
|
51
|
+
retry=retry_if_exception_type((requests.exceptions.RequestException,))
|
|
52
|
+
)
|
|
51
53
|
def evaluate(self, question_answer_pairs: List[Dict[str, str]]) -> Dict[str, any]:
|
|
52
54
|
logger.info("Starting evaluation of %d Power BI question-answer pairs", len(question_answer_pairs))
|
|
53
55
|
combined_content = "\n\n".join(
|
|
54
56
|
f"Question {i}:\n{qa['question']}\n\nAnswer {i}:\n{qa['answer']}\n"
|
|
55
57
|
for i, qa in enumerate(question_answer_pairs, 1)
|
|
56
58
|
)
|
|
57
|
-
|
|
58
59
|
headers = {"Content-Type": "application/json"}
|
|
59
60
|
data = {"contents": [{"parts": [{"text": prompt_text_powerbi(combined_content)}]}]}
|
|
60
61
|
response = requests.post(f"{self.endpoint}?key={self.api_key}", headers=headers, json=data)
|
|
61
|
-
|
|
62
62
|
if response.status_code != 200:
|
|
63
63
|
logger.error("API request failed: Status %d, Response: %s", response.status_code, response.text)
|
|
64
64
|
raise Exception(f"API call failed: {response.status_code} - {response.text}")
|
|
@@ -69,8 +69,11 @@ class GeminiFlashModel:
|
|
|
69
69
|
generated_text = response_data["candidates"][0]["content"]["parts"][0]["text"]
|
|
70
70
|
return self._parse_response(generated_text)
|
|
71
71
|
|
|
72
|
-
@retry(
|
|
73
|
-
|
|
72
|
+
@retry(
|
|
73
|
+
stop=stop_after_attempt(3),
|
|
74
|
+
wait=wait_exponential(min=4, max=10),
|
|
75
|
+
retry=retry_if_exception_type((requests.exceptions.RequestException,))
|
|
76
|
+
)
|
|
74
77
|
def evaluate_visuals(self, question: str, image_folder: str) -> Dict[str, any]:
|
|
75
78
|
folder_path = Path(image_folder)
|
|
76
79
|
images = list(folder_path.glob("*.png"))[:3]
|
|
@@ -80,12 +83,12 @@ class GeminiFlashModel:
|
|
|
80
83
|
"Evaluate the Power BI report visuals based on the provided task. The visuals are professional dashboards designed for enterprise use.\n\n"
|
|
81
84
|
f"Task: {question}\n\n"
|
|
82
85
|
f"Screenshots: {[str(img.name) for img in images]}\n\n"
|
|
83
|
-
"Evaluate based on the following criteria, assigning a score out of 100
|
|
86
|
+
"Evaluate based on the following criteria, assigning a score out of 100:\n"
|
|
84
87
|
"- Clarity (30%): Are visuals clear, with readable labels, titles, and legends?\n"
|
|
85
88
|
"- Appropriateness (30%): Are chart types (e.g., bar, line, pie) suitable for the data and task?\n"
|
|
86
89
|
"- Color Usage (20%): Are colors consistent, accessible, and visually appealing? Consider contrast and colorblind accessibility.\n"
|
|
87
90
|
"- Interactivity (20%): Do visible slicers, filters, or tooltips enhance usability and data exploration?\n\n"
|
|
88
|
-
"Provide a score
|
|
91
|
+
"Provide a score for overall quality, considering the enterprise context. Avoid overly harsh penalties for minor issues.\n"
|
|
89
92
|
"Provide concise, supportive feedback for beginners, highlighting strengths and areas for improvement.\n\n"
|
|
90
93
|
"Structure the response as:\n"
|
|
91
94
|
"Score: [SCORE]/100\n"
|
|
@@ -231,9 +234,7 @@ class PowerBIProcessor:
|
|
|
231
234
|
measures.append({
|
|
232
235
|
"Table": table["name"],
|
|
233
236
|
"Name": measure["name"],
|
|
234
|
-
"Expression": " ".join(measure.get("expression", "")) if isinstance(measure.get("expression"),
|
|
235
|
-
list) else measure.get(
|
|
236
|
-
"expression", ""),
|
|
237
|
+
"Expression": " ".join(measure.get("expression", "")) if isinstance(measure.get("expression"), list) else measure.get("expression", ""),
|
|
237
238
|
"FormatString": measure.get("formatString", "")
|
|
238
239
|
})
|
|
239
240
|
return measures
|
|
@@ -242,19 +243,31 @@ class PowerBIProcessor:
|
|
|
242
243
|
def _get_tables_and_columns(tables: List[Dict]) -> List[Dict]:
|
|
243
244
|
table_info = []
|
|
244
245
|
for table in tables:
|
|
245
|
-
columns = [
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
246
|
+
columns = [
|
|
247
|
+
{
|
|
248
|
+
"Column Name": col["name"],
|
|
249
|
+
"Data Type": col.get("dataType", "Unknown"),
|
|
250
|
+
"Source Column": col.get("sourceColumn", "N/A"),
|
|
251
|
+
"Calculated": col.get("type") == "calculated"
|
|
252
|
+
}
|
|
253
|
+
for col in table.get("columns", [])
|
|
254
|
+
]
|
|
255
|
+
expressions = [part["source"]["expression"] for part in table.get("partitions", []) if part["source"].get("expression")]
|
|
250
256
|
table_info.append({"Table Name": table["name"], "Columns": columns, "Expressions": expressions})
|
|
251
257
|
return table_info
|
|
252
258
|
|
|
253
259
|
@staticmethod
|
|
254
260
|
def _get_relationships(relationships: List[Dict]) -> List[Dict]:
|
|
255
|
-
return [
|
|
256
|
-
|
|
257
|
-
|
|
261
|
+
return [
|
|
262
|
+
{
|
|
263
|
+
"From Table": rel["fromTable"],
|
|
264
|
+
"From Column": rel["fromColumn"],
|
|
265
|
+
"To Table": rel["toTable"],
|
|
266
|
+
"To Column": rel["toColumn"],
|
|
267
|
+
"Join Behavior": rel.get("joinOnDateBehavior", "N/A")
|
|
268
|
+
}
|
|
269
|
+
for rel in relationships
|
|
270
|
+
]
|
|
258
271
|
|
|
259
272
|
@staticmethod
|
|
260
273
|
def _cleanup(*paths: str):
|
|
@@ -279,8 +292,6 @@ class PowerBIEvaluator:
|
|
|
279
292
|
extract_path = os.path.join(os.path.dirname(answer_path), "temp_extract")
|
|
280
293
|
pbit_path = None
|
|
281
294
|
pdf_path = None
|
|
282
|
-
|
|
283
|
-
# Handle input file type
|
|
284
295
|
if ext == ".zip":
|
|
285
296
|
pbit_path, pdf_path = self.processor.extract_zip(answer_path, extract_path)
|
|
286
297
|
elif ext == ".pbit":
|
|
@@ -296,57 +307,43 @@ class PowerBIEvaluator:
|
|
|
296
307
|
"dax_score": 0,
|
|
297
308
|
"visual_score": 0
|
|
298
309
|
}
|
|
299
|
-
|
|
300
310
|
try:
|
|
301
|
-
# Extract and process the data model from .pbit
|
|
302
311
|
data_model = self.processor.extract_datamodel(pbit_path)
|
|
303
312
|
model_data = self.processor.extract_model_data(data_model)
|
|
304
313
|
answers = [json.dumps(model_data)] * len(questions)
|
|
305
314
|
dax_result = self.model.evaluate([{"question": q, "answer": a} for q, a in zip(questions, answers)])
|
|
306
|
-
|
|
307
|
-
# Initialize result with DAX evaluation
|
|
308
315
|
result = {
|
|
309
316
|
"score": 0,
|
|
310
317
|
"feedback": f"DAX Feedback:\n{dax_result['feedback']}",
|
|
311
318
|
"issues": dax_result["issues"],
|
|
312
319
|
"recommendations": dax_result["recommendations"],
|
|
313
|
-
"dax_score": dax_result["score"],
|
|
314
|
-
"visual_score": 0
|
|
320
|
+
"dax_score": dax_result["score"],
|
|
321
|
+
"visual_score": 0
|
|
315
322
|
}
|
|
316
|
-
|
|
317
|
-
# Process PDF and evaluate visuals if present
|
|
318
323
|
if pdf_path:
|
|
319
324
|
try:
|
|
320
325
|
self.processor.process_pdf(pdf_path)
|
|
321
326
|
visual_result = self.model.evaluate_visuals(questions[0], "outputimages")
|
|
322
|
-
# Apply 70% DAX, 30% visuals scoring
|
|
323
327
|
result["score"] = int(0.7 * dax_result["score"] + 0.3 * visual_result["score"])
|
|
324
|
-
result["visual_score"] = visual_result["score"]
|
|
328
|
+
result["visual_score"] = visual_result["score"]
|
|
325
329
|
result["feedback"] += f"\n\nVisual Feedback:\n{visual_result['feedback']}"
|
|
326
330
|
result["issues"].extend([f"Visual: {i}" for i in visual_result.get("issues", [])])
|
|
327
331
|
result["recommendations"].extend(visual_result.get("recommendations", []))
|
|
328
332
|
except ProcessingError as e:
|
|
329
333
|
logger.warning("Failed to process PDF, proceeding with DAX evaluation only: %s", str(e))
|
|
330
|
-
# Use DAX score only, weighted at 100% if no visuals
|
|
331
334
|
result["score"] = dax_result["score"]
|
|
332
335
|
result["issues"].append(f"Visual evaluation skipped: {str(e)}")
|
|
333
|
-
result["recommendations"].append(
|
|
334
|
-
"Ensure a valid PDF is provided for visual evaluation if intended")
|
|
336
|
+
result["recommendations"].append("Ensure a valid PDF is provided for visual evaluation if intended")
|
|
335
337
|
else:
|
|
336
|
-
# No PDF provided, use DAX score only
|
|
337
338
|
result["score"] = dax_result["score"]
|
|
338
339
|
result["feedback"] += "\n\nVisual Feedback:\nNo visuals provided for evaluation."
|
|
339
340
|
result["issues"].append("No PDF provided for visual evaluation")
|
|
340
341
|
result["recommendations"].append("Include a PDF with report visuals for complete evaluation")
|
|
341
|
-
|
|
342
|
-
# Print scores with text labels to terminal
|
|
343
342
|
logger.info("[DAX] Score: %d/100", result["dax_score"])
|
|
344
343
|
logger.info("[Visual] Score: %d/100", result["visual_score"])
|
|
345
344
|
logger.info("[Final] Score (70%% DAX, 30%% Visuals): %d/100", result["score"])
|
|
346
|
-
|
|
347
345
|
return result
|
|
348
346
|
finally:
|
|
349
|
-
# Cleanup temporary files and directories
|
|
350
347
|
self.processor._cleanup(extract_path, "outputimages")
|
|
351
348
|
except Exception as e:
|
|
352
349
|
logger.exception("Failed to evaluate Power BI file %s: %s", answer_path, str(e))
|