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.
Files changed (25) hide show
  1. quantumchecker-0.2.8/PKG-INFO +138 -0
  2. quantumchecker-0.2.8/QuantumCheck/main.py +188 -0
  3. {quantumchecker-0.2.7 → quantumchecker-0.2.8}/QuantumCheck/powerbi_evaluator.py +37 -40
  4. {quantumchecker-0.2.7 → quantumchecker-0.2.8}/QuantumCheck/prompts.py +41 -61
  5. quantumchecker-0.2.8/QuantumCheck/python_evaluator.py +194 -0
  6. quantumchecker-0.2.8/QuantumCheck/sql_evaluator.py +196 -0
  7. quantumchecker-0.2.8/QuantumCheck/ssis_evaluator.py +292 -0
  8. quantumchecker-0.2.8/QuantumChecker.egg-info/PKG-INFO +138 -0
  9. quantumchecker-0.2.8/README.md +112 -0
  10. {quantumchecker-0.2.7 → quantumchecker-0.2.8}/setup.py +1 -1
  11. quantumchecker-0.2.8/tests/test.py +388 -0
  12. quantumchecker-0.2.7/PKG-INFO +0 -34
  13. quantumchecker-0.2.7/QuantumCheck/main.py +0 -125
  14. quantumchecker-0.2.7/QuantumCheck/python_evaluator.py +0 -95
  15. quantumchecker-0.2.7/QuantumCheck/sql_evaluator.py +0 -97
  16. quantumchecker-0.2.7/QuantumCheck/ssis_evaluator.py +0 -136
  17. quantumchecker-0.2.7/QuantumChecker.egg-info/PKG-INFO +0 -34
  18. quantumchecker-0.2.7/README.md +0 -8
  19. quantumchecker-0.2.7/tests/test.py +0 -31
  20. {quantumchecker-0.2.7 → quantumchecker-0.2.8}/QuantumCheck/__init__.py +0 -0
  21. {quantumchecker-0.2.7 → quantumchecker-0.2.8}/QuantumChecker.egg-info/SOURCES.txt +0 -0
  22. {quantumchecker-0.2.7 → quantumchecker-0.2.8}/QuantumChecker.egg-info/dependency_links.txt +0 -0
  23. {quantumchecker-0.2.7 → quantumchecker-0.2.8}/QuantumChecker.egg-info/requires.txt +0 -0
  24. {quantumchecker-0.2.7 → quantumchecker-0.2.8}/QuantumChecker.egg-info/top_level.txt +0 -0
  25. {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(stop=stop_after_attempt(3), wait=wait_exponential(min=4, max=10),
50
- retry=retry_if_exception_type((requests.exceptions.RequestException,)))
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(stop=stop_after_attempt(3), wait=wait_exponential(min=4, max=10),
73
- retry=retry_if_exception_type((requests.exceptions.RequestException,)))
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:z\n"
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 (0-100) that reflects the overall quality, considering the enterprise context. Avoid overly harsh penalties for minor issues.\n"
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 = [{"Column Name": col["name"], "Data Type": col.get("dataType", "Unknown"),
246
- "Source Column": col.get("sourceColumn", "N/A"), "Calculated": col.get("type") == "calculated"}
247
- for col in table.get("columns", [])]
248
- expressions = [part["source"]["expression"] for part in table.get("partitions", []) if
249
- part["source"].get("expression")]
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 [{"From Table": rel["fromTable"], "From Column": rel["fromColumn"], "To Table": rel["toTable"],
256
- "To Column": rel["toColumn"], "Join Behavior": rel.get("joinOnDateBehavior", "N/A")} for rel in
257
- relationships]
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"], # Store DAX score
314
- "visual_score": 0 # Default visual score
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"] # Store visual 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))