QuantumChecker 0.4.0__tar.gz → 1.0.0__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 (58) hide show
  1. {quantumchecker-0.4.0 → quantumchecker-1.0.0}/PKG-INFO +52 -52
  2. quantumchecker-1.0.0/QuantumCheck/__init__.py +33 -0
  3. quantumchecker-1.0.0/QuantumCheck/config.py +101 -0
  4. quantumchecker-1.0.0/QuantumCheck/evaluator.py +130 -0
  5. quantumchecker-1.0.0/QuantumCheck/evaluators/__init__.py +6 -0
  6. quantumchecker-1.0.0/QuantumCheck/evaluators/base.py +44 -0
  7. quantumchecker-1.0.0/QuantumCheck/evaluators/powerbi.py +162 -0
  8. quantumchecker-1.0.0/QuantumCheck/evaluators/python.py +19 -0
  9. quantumchecker-1.0.0/QuantumCheck/evaluators/sql.py +19 -0
  10. quantumchecker-1.0.0/QuantumCheck/evaluators/ssis.py +54 -0
  11. quantumchecker-1.0.0/QuantumCheck/exceptions.py +21 -0
  12. quantumchecker-1.0.0/QuantumCheck/main.py +2 -0
  13. quantumchecker-1.0.0/QuantumCheck/model.py +142 -0
  14. quantumchecker-1.0.0/QuantumCheck/parsers/__init__.py +7 -0
  15. quantumchecker-1.0.0/QuantumCheck/parsers/base.py +94 -0
  16. quantumchecker-1.0.0/QuantumCheck/parsers/lesson.py +69 -0
  17. quantumchecker-1.0.0/QuantumCheck/parsers/powerbi.py +198 -0
  18. quantumchecker-1.0.0/QuantumCheck/parsers/python.py +12 -0
  19. quantumchecker-1.0.0/QuantumCheck/parsers/sql.py +12 -0
  20. quantumchecker-1.0.0/QuantumCheck/parsers/ssis.py +203 -0
  21. quantumchecker-1.0.0/QuantumCheck/powerbi_evaluator.py +4 -0
  22. quantumchecker-1.0.0/QuantumCheck/powerbi_mentor.py +58 -0
  23. quantumchecker-1.0.0/QuantumCheck/prompts/__init__.py +6 -0
  24. quantumchecker-1.0.0/QuantumCheck/prompts/powerbi.py +44 -0
  25. quantumchecker-1.0.0/QuantumCheck/prompts/python.py +28 -0
  26. quantumchecker-1.0.0/QuantumCheck/prompts/sql.py +39 -0
  27. quantumchecker-1.0.0/QuantumCheck/prompts/ssis.py +31 -0
  28. quantumchecker-1.0.0/QuantumCheck/prompts.py +5 -0
  29. quantumchecker-1.0.0/QuantumCheck/python_evaluator.py +2 -0
  30. quantumchecker-1.0.0/QuantumCheck/python_mentor.py +36 -0
  31. quantumchecker-1.0.0/QuantumCheck/quantum_check.py +46 -0
  32. quantumchecker-1.0.0/QuantumCheck/sql_evaluator.py +2 -0
  33. quantumchecker-1.0.0/QuantumCheck/sql_mentor.py +36 -0
  34. quantumchecker-1.0.0/QuantumCheck/ssis_evaluator.py +2 -0
  35. quantumchecker-1.0.0/QuantumCheck/ssis_mentor.py +36 -0
  36. {quantumchecker-0.4.0 → quantumchecker-1.0.0}/QuantumChecker.egg-info/PKG-INFO +52 -52
  37. quantumchecker-1.0.0/QuantumChecker.egg-info/SOURCES.txt +47 -0
  38. quantumchecker-1.0.0/QuantumChecker.egg-info/requires.txt +6 -0
  39. {quantumchecker-0.4.0 → quantumchecker-1.0.0}/setup.cfg +4 -4
  40. {quantumchecker-0.4.0 → quantumchecker-1.0.0}/setup.py +28 -28
  41. {quantumchecker-0.4.0 → quantumchecker-1.0.0}/tests/test2.py +4 -4
  42. quantumchecker-1.0.0/tests/test_comprehensive.py +1340 -0
  43. quantumchecker-1.0.0/tests/test_live.py +257 -0
  44. quantumchecker-1.0.0/tests/test_refactored.py +51 -0
  45. quantumchecker-1.0.0/tests/test_simple.py +50 -0
  46. quantumchecker-0.4.0/QuantumCheck/__init__.py +0 -1
  47. quantumchecker-0.4.0/QuantumCheck/main.py +0 -138
  48. quantumchecker-0.4.0/QuantumCheck/powerbi_evaluator.py +0 -403
  49. quantumchecker-0.4.0/QuantumCheck/prompts.py +0 -185
  50. quantumchecker-0.4.0/QuantumCheck/python_evaluator.py +0 -213
  51. quantumchecker-0.4.0/QuantumCheck/sql_evaluator.py +0 -213
  52. quantumchecker-0.4.0/QuantumCheck/ssis_evaluator.py +0 -386
  53. quantumchecker-0.4.0/QuantumChecker.egg-info/SOURCES.txt +0 -17
  54. quantumchecker-0.4.0/QuantumChecker.egg-info/requires.txt +0 -6
  55. {quantumchecker-0.4.0 → quantumchecker-1.0.0}/QuantumChecker.egg-info/dependency_links.txt +0 -0
  56. {quantumchecker-0.4.0 → quantumchecker-1.0.0}/QuantumChecker.egg-info/top_level.txt +0 -0
  57. {quantumchecker-0.4.0 → quantumchecker-1.0.0}/README.md +0 -0
  58. {quantumchecker-0.4.0 → quantumchecker-1.0.0}/tests/test.py +0 -0
@@ -1,52 +1,52 @@
1
- Metadata-Version: 2.4
2
- Name: QuantumChecker
3
- Version: 0.4.0
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
- Sample usage:
28
- ```
29
- import asyncio
30
- from your_evaluator_module import HomeworkEvaluator
31
-
32
- async def main():
33
- evaluator = HomeworkEvaluator()
34
- question_content = """
35
- Q1: What is a Python list? Explain with an example.
36
-
37
- Q2: Write an SQL query to select all records from a table named 'students'.
38
- """
39
- answer_path = "sample_submissions/student1_answer.py"
40
- question_type = "python"
41
-
42
- result = await evaluator.evaluate_from_content(
43
- question_content=question_content,
44
- answer_path=answer_path,
45
- api_key="your_api_key",
46
- question_type=question_type
47
- )
48
- print(result)
49
-
50
- if __name__ == "__main__":
51
- asyncio.run(main())
52
- ```
1
+ Metadata-Version: 2.4
2
+ Name: QuantumChecker
3
+ Version: 1.0.0
4
+ Summary: AI-powered homework evaluation for Python, SQL, Power BI, 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.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: google-genai>=1.0.0
13
+ Requires-Dist: Pillow>=10.0.0
14
+ Requires-Dist: PyPDF2>=3.0.1
15
+ Provides-Extra: pdf
16
+ Requires-Dist: pdf2image>=1.16.3; extra == "pdf"
17
+ Dynamic: author
18
+ Dynamic: author-email
19
+ Dynamic: classifier
20
+ Dynamic: description
21
+ Dynamic: description-content-type
22
+ Dynamic: provides-extra
23
+ Dynamic: requires-dist
24
+ Dynamic: requires-python
25
+ Dynamic: summary
26
+
27
+ Sample usage:
28
+ ```
29
+ import asyncio
30
+ from your_evaluator_module import HomeworkEvaluator
31
+
32
+ async def main():
33
+ evaluator = HomeworkEvaluator()
34
+ question_content = """
35
+ Q1: What is a Python list? Explain with an example.
36
+
37
+ Q2: Write an SQL query to select all records from a table named 'students'.
38
+ """
39
+ answer_path = "sample_submissions/student1_answer.py"
40
+ question_type = "python"
41
+
42
+ result = await evaluator.evaluate_from_content(
43
+ question_content=question_content,
44
+ answer_path=answer_path,
45
+ api_key="your_api_key",
46
+ question_type=question_type
47
+ )
48
+ print(result)
49
+
50
+ if __name__ == "__main__":
51
+ asyncio.run(main())
52
+ ```
@@ -0,0 +1,33 @@
1
+ """QuantumCheck - AI-powered homework evaluation package."""
2
+
3
+ from .quantum_check import QuantumCheck
4
+ from .evaluator import HomeworkEvaluator
5
+ from .model import GeminiModel
6
+ from .powerbi_mentor import PowerBIMentor
7
+ from .python_mentor import PythonMentor
8
+ from .sql_mentor import SQLMentor
9
+ from .ssis_mentor import SSISMentor
10
+ from .parsers.lesson import parse_lesson_questions
11
+ from .exceptions import (
12
+ QuantumCheckError,
13
+ ParsingError,
14
+ EvaluationError,
15
+ ProcessingError,
16
+ ModelError,
17
+ )
18
+
19
+ __all__ = [
20
+ "QuantumCheck",
21
+ "HomeworkEvaluator",
22
+ "GeminiModel",
23
+ "PowerBIMentor",
24
+ "PythonMentor",
25
+ "SQLMentor",
26
+ "SSISMentor",
27
+ "parse_lesson_questions",
28
+ "QuantumCheckError",
29
+ "ParsingError",
30
+ "EvaluationError",
31
+ "ProcessingError",
32
+ "ModelError",
33
+ ]
@@ -0,0 +1,101 @@
1
+ """Centralized configuration and constants."""
2
+
3
+ DEFAULT_MODEL_NAME = "gemini-2.5-flash"
4
+ DEFAULT_TEMPERATURE = 0.3
5
+ RATE_LIMIT_SECONDS = 0 # Set >0 only for free-tier keys
6
+
7
+ # Retry settings for transient API errors (429, 503, etc.)
8
+ RETRY_MAX_ATTEMPTS = 5
9
+ RETRY_BASE_DELAY = 1.0 # seconds
10
+ RETRY_MAX_DELAY = 30.0 # seconds
11
+ RETRY_BACKOFF_FACTOR = 2 # exponential multiplier
12
+ OFF_TOPIC_SCORE = 20
13
+ MAX_SCORE = 100
14
+ BASELINE_SSIS_SCORE = 60
15
+ PDF_MAX_PAGES = 3
16
+ IMAGE_MAX_SIZE = (1024, 1024)
17
+
18
+ # Legacy DAX vs visual weights (kept for backward compat, no longer used internally)
19
+ POWERBI_DAX_WEIGHT = 0.7
20
+ POWERBI_VISUAL_WEIGHT = 0.3
21
+
22
+ # File extension to evaluator type mapping
23
+ EXTENSION_TO_TYPE = {
24
+ ".py": "python",
25
+ ".ipynb": "python",
26
+ ".pyw": "python",
27
+ ".pyi": "python",
28
+ ".pyx": "python",
29
+ ".pxd": "python",
30
+ ".pyd": "python",
31
+ ".so": "python",
32
+ ".sql": "sql",
33
+ ".pbit": "powerbi",
34
+ ".pdf": "powerbi",
35
+ ".dtsx": "ssis",
36
+ ".txt": "text",
37
+ ".md": "text",
38
+ }
39
+
40
+ PYTHON_EXTENSIONS = (".py", ".ipynb", ".pyw", ".pyi", ".pyx", ".pxd", ".pyd", ".so")
41
+
42
+ # JSON schema for structured model output
43
+ EVALUATION_SCHEMA = {
44
+ "type": "OBJECT",
45
+ "properties": {
46
+ "score": {
47
+ "type": "INTEGER",
48
+ "description": "Overall score out of 100",
49
+ },
50
+ "feedback": {
51
+ "type": "STRING",
52
+ "description": "Detailed feedback including strengths and areas for improvement",
53
+ },
54
+ "issues": {
55
+ "type": "ARRAY",
56
+ "items": {"type": "STRING"},
57
+ "description": "Specific issues found in the submission",
58
+ },
59
+ "recommendations": {
60
+ "type": "ARRAY",
61
+ "items": {"type": "STRING"},
62
+ "description": "Actionable recommendations for improvement",
63
+ },
64
+ },
65
+ "required": ["score", "feedback", "issues", "recommendations"],
66
+ }
67
+
68
+ VISUAL_EVALUATION_SCHEMA = {
69
+ "type": "OBJECT",
70
+ "properties": {
71
+ "score": {
72
+ "type": "INTEGER",
73
+ "description": "Visual evaluation score out of 100",
74
+ },
75
+ "feedback": {
76
+ "type": "STRING",
77
+ "description": "Feedback on visual quality",
78
+ },
79
+ "issues": {
80
+ "type": "ARRAY",
81
+ "items": {"type": "STRING"},
82
+ "description": "Visual issues found",
83
+ },
84
+ "recommendations": {
85
+ "type": "ARRAY",
86
+ "items": {"type": "STRING"},
87
+ "description": "Recommendations for visual improvement",
88
+ },
89
+ },
90
+ "required": ["score", "feedback", "issues", "recommendations"],
91
+ }
92
+
93
+
94
+ def empty_result(feedback="", score=0):
95
+ """Return a consistently shaped empty evaluation result."""
96
+ return {
97
+ "score": score,
98
+ "feedback": feedback,
99
+ "issues": [],
100
+ "recommendations": [],
101
+ }
@@ -0,0 +1,130 @@
1
+ """HomeworkEvaluator - main entry point for the QuantumCheck package."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import os
6
+ import shutil
7
+ import zipfile
8
+ from datetime import datetime
9
+ from typing import Dict, List, Optional
10
+
11
+ from .config import EXTENSION_TO_TYPE, RATE_LIMIT_SECONDS, empty_result
12
+ from .evaluators import PythonEvaluator, SQLEvaluator, PowerBIEvaluator, SSISEvaluator
13
+ from .model import GeminiModel
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class HomeworkEvaluator:
19
+ """Evaluate homework submissions across Python, SQL, Power BI, and SSIS."""
20
+
21
+ _EVALUATOR_MAP = {
22
+ "python": PythonEvaluator,
23
+ "sql": SQLEvaluator,
24
+ "powerbi": PowerBIEvaluator,
25
+ "ssis": SSISEvaluator,
26
+ }
27
+
28
+ def __init__(
29
+ self,
30
+ api_key=None,
31
+ model_name="gemini-2.5-flash",
32
+ vertexai=False,
33
+ project_id=None,
34
+ location=None,
35
+ log_level=logging.INFO,
36
+ rate_limit=RATE_LIMIT_SECONDS,
37
+ ):
38
+ logging.basicConfig(level=log_level)
39
+ self._rate_limit_seconds = rate_limit
40
+ self._last_request_time = None
41
+ self._lock = asyncio.Lock() if rate_limit > 0 else None
42
+
43
+ self._model = GeminiModel(
44
+ api_key=api_key,
45
+ model_name=model_name,
46
+ vertexai=vertexai,
47
+ project_id=project_id,
48
+ location=location,
49
+ )
50
+
51
+ async def evaluate_from_content(
52
+ self,
53
+ question_content,
54
+ answer_path,
55
+ question_type,
56
+ api_key=None,
57
+ ):
58
+ """Evaluate a student submission asynchronously."""
59
+ if self._lock:
60
+ async with self._lock:
61
+ await self._rate_limit()
62
+
63
+ questions = self._parse_questions(question_content)
64
+ if isinstance(questions, dict):
65
+ return questions
66
+
67
+ answer_path = answer_path.strip()
68
+ if not os.path.exists(answer_path):
69
+ return empty_result(feedback="Answer file not found: {}".format(answer_path))
70
+
71
+ eval_type = self._resolve_type(question_type, answer_path)
72
+ evaluator_cls = self._EVALUATOR_MAP.get(eval_type, PythonEvaluator)
73
+ evaluator = evaluator_cls(self._model)
74
+
75
+ temp_dir = "temp_extract_{}_{}".format(os.getpid(), id(asyncio.current_task()))
76
+ try:
77
+ result = evaluator.evaluate(
78
+ questions, answer_path,
79
+ temp_dir=temp_dir
80
+ )
81
+ return {
82
+ "score": result.get("score", 0),
83
+ "feedback": result.get("feedback", "No feedback provided"),
84
+ "issues": result.get("issues", []),
85
+ "recommendations": result.get("recommendations", []),
86
+ **{k: v for k, v in result.items()
87
+ if k not in ("score", "feedback", "issues", "recommendations")},
88
+ }
89
+ except Exception as exc:
90
+ logger.error("Evaluation failed: %s", exc)
91
+ return empty_result(feedback="Evaluation failed: {}".format(exc))
92
+ finally:
93
+ if os.path.exists(temp_dir):
94
+ shutil.rmtree(temp_dir, ignore_errors=True)
95
+
96
+ async def _rate_limit(self):
97
+ if self._rate_limit_seconds <= 0:
98
+ return
99
+ now = datetime.now()
100
+ if self._last_request_time:
101
+ elapsed = (now - self._last_request_time).total_seconds()
102
+ if elapsed < self._rate_limit_seconds:
103
+ await asyncio.sleep(self._rate_limit_seconds - elapsed)
104
+ self._last_request_time = datetime.now()
105
+
106
+ @staticmethod
107
+ def _parse_questions(content):
108
+ questions = [q.strip() for q in content.split("\n\n") if q.strip()]
109
+ if not questions:
110
+ return empty_result(feedback="No valid questions found in content")
111
+ return questions
112
+
113
+ @staticmethod
114
+ def _resolve_type(question_type, answer_path):
115
+ if question_type in ("python", "sql", "powerbi", "ssis"):
116
+ return question_type
117
+ _, ext = os.path.splitext(answer_path)
118
+ ext = ext.lower()
119
+ if ext == ".zip":
120
+ try:
121
+ with zipfile.ZipFile(answer_path, "r") as zf:
122
+ extensions = {os.path.splitext(n)[1].lower() for n in zf.namelist()}
123
+ types = [EXTENSION_TO_TYPE.get(e, "text") for e in extensions if e]
124
+ for t in ("python", "sql", "powerbi", "ssis"):
125
+ if t in types:
126
+ return t
127
+ except zipfile.BadZipFile:
128
+ pass
129
+ return "text"
130
+ return EXTENSION_TO_TYPE.get(ext, "text")
@@ -0,0 +1,6 @@
1
+ """Homework evaluators."""
2
+
3
+ from .python import PythonEvaluator
4
+ from .sql import SQLEvaluator
5
+ from .ssis import SSISEvaluator
6
+ from .powerbi import PowerBIEvaluator
@@ -0,0 +1,44 @@
1
+ """Base evaluator with shared parse-combine-evaluate pattern."""
2
+
3
+ import logging
4
+ import os
5
+ from abc import ABC, abstractmethod
6
+ from typing import Dict, List
7
+
8
+ from ..config import empty_result
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class BaseEvaluator(ABC):
14
+ """Template-method evaluator.
15
+
16
+ Subclasses implement ``_parse_answers`` and ``_build_prompt``.
17
+ The ``evaluate`` method orchestrates: parse -> combine -> call model.
18
+ """
19
+
20
+ def __init__(self, model):
21
+ self.model = model
22
+
23
+ def evaluate(self, questions: List[str], answer_path: str, temp_dir: str = None) -> Dict:
24
+ try:
25
+ answers = self._parse_answers(answer_path, temp_dir)
26
+
27
+ combined_questions = "\n".join(q.strip() for q in questions if q.strip())
28
+ combined_answers = "\n".join(a.strip() for a in answers if a.strip())
29
+ raw = "Questions:\n{}\n\nAnswers:\n{}".format(combined_questions, combined_answers)
30
+
31
+ prompt = self._build_prompt(raw)
32
+ return self.model.evaluate(prompt)
33
+
34
+ except Exception as exc:
35
+ logger.error("Evaluation failed for %s: %s", answer_path, exc)
36
+ return empty_result(feedback="Evaluation failed: {}".format(exc))
37
+
38
+ @abstractmethod
39
+ def _parse_answers(self, answer_path: str, temp_dir: str = None) -> List[str]:
40
+ """Return a list of answer strings from the submission file."""
41
+
42
+ @abstractmethod
43
+ def _build_prompt(self, combined_content: str) -> str:
44
+ """Wrap combined Q&A content in the appropriate prompt template."""
@@ -0,0 +1,162 @@
1
+ """Power BI homework evaluator - combines DAX, visual, and written evaluations."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ from typing import Dict, List, Optional
7
+
8
+ from ..config import empty_result
9
+ from ..parsers.powerbi import PowerBIProcessor
10
+ from ..prompts.powerbi import build_powerbi_dax_prompt, POWERBI_VISUAL_PROMPT, POWERBI_WRITTEN_PROMPT
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class PowerBIEvaluator:
16
+ """Evaluates Power BI submissions across three sections."""
17
+
18
+ def __init__(self, model):
19
+ self.model = model
20
+ self._processor = PowerBIProcessor()
21
+
22
+ def evaluate(self, questions, answer_path, temp_dir="temp_extract",
23
+ dax_question=None, visual_question=None, written_question=None):
24
+ """Evaluate a Power BI submission (legacy interface)."""
25
+ output_images = os.path.join(temp_dir, "outputimages")
26
+ combined = "\n\n".join(q.strip() for q in questions if q.strip())
27
+ if dax_question is None and visual_question is None and written_question is None:
28
+ dax_q = combined
29
+ visual_q = combined
30
+ written_q = combined
31
+ all_from_legacy = True
32
+ else:
33
+ dax_q = dax_question
34
+ visual_q = visual_question
35
+ written_q = written_question
36
+ all_from_legacy = False
37
+ try:
38
+ pbit_path, pdf_path, txt_path = self._processor.resolve_paths(answer_path, temp_dir)
39
+ return self._run_sections(
40
+ dax_q, visual_q, written_q,
41
+ pbit_path, pdf_path, txt_path,
42
+ questions, output_images, all_from_legacy,
43
+ )
44
+ except Exception as exc:
45
+ logger.error("PowerBI evaluation failed: %s", exc)
46
+ result = empty_result(feedback="Error processing file: {}".format(exc))
47
+ result["dax_score"] = 0
48
+ result["visual_score"] = 0
49
+ result["written_score"] = 0
50
+ return result
51
+ finally:
52
+ self._processor._cleanup(temp_dir, output_images)
53
+
54
+ def evaluate_all(self, answer_path, dax_question=None, visual_question=None,
55
+ written_question=None, temp_dir="temp_extract"):
56
+ """Evaluate with per-section questions. None = skip section."""
57
+ output_images = os.path.join(temp_dir, "outputimages")
58
+ try:
59
+ pbit_path, pdf_path, txt_path = self._processor.resolve_paths(answer_path, temp_dir)
60
+ return self._run_sections(
61
+ dax_question, visual_question, written_question,
62
+ pbit_path, pdf_path, txt_path,
63
+ [], output_images, False,
64
+ )
65
+ except Exception as exc:
66
+ logger.error("PowerBI evaluation failed: %s", exc)
67
+ result = empty_result(feedback="Error processing file: {}".format(exc))
68
+ result["dax_score"] = 0
69
+ result["visual_score"] = 0
70
+ result["written_score"] = 0
71
+ return result
72
+ finally:
73
+ self._processor._cleanup(temp_dir, output_images)
74
+
75
+ def _run_sections(self, dax_q, visual_q, written_q,
76
+ pbit_path, pdf_path, txt_path,
77
+ raw_questions, output_images, all_from_legacy):
78
+ sections = []
79
+ all_issues = []
80
+ all_recommendations = []
81
+ dax_score = 0
82
+ visual_score = 0
83
+ written_score = 0
84
+
85
+ if dax_q is not None:
86
+ if pbit_path:
87
+ r = self._evaluate_dax(raw_questions or [dax_q], pbit_path)
88
+ dax_score = r["score"]
89
+ sections.append(("DAX", dax_score, r["feedback"]))
90
+ all_issues.extend(r.get("issues", []))
91
+ all_recommendations.extend(r.get("recommendations", []))
92
+ else:
93
+ sections.append(("DAX", 0, "No .pbit file provided for DAX evaluation."))
94
+ all_issues.append("No .pbit file provided for DAX evaluation")
95
+
96
+ if visual_q is not None:
97
+ if pdf_path:
98
+ r = self._evaluate_visuals(visual_q, pdf_path, output_images)
99
+ visual_score = r["score"]
100
+ sections.append(("Visual", visual_score, r["feedback"]))
101
+ all_issues.extend("Visual: {}".format(i) for i in r.get("issues", []))
102
+ all_recommendations.extend(r.get("recommendations", []))
103
+ else:
104
+ sections.append(("Visual", 0, "No .pdf file provided for visual evaluation."))
105
+ all_issues.append("No .pdf file provided for visual evaluation")
106
+
107
+ if written_q is not None:
108
+ if txt_path:
109
+ r = self._evaluate_written(written_q, txt_path)
110
+ written_score = r["score"]
111
+ sections.append(("Written", written_score, r["feedback"]))
112
+ all_issues.extend(r.get("issues", []))
113
+ all_recommendations.extend(r.get("recommendations", []))
114
+ else:
115
+ sections.append(("Written", 0, "No .txt file provided for written evaluation."))
116
+ all_issues.append("No .txt file provided for written evaluation")
117
+
118
+ if not sections:
119
+ return {
120
+ "score": 0,
121
+ "feedback": "No evaluations were completed.",
122
+ "issues": [],
123
+ "recommendations": [],
124
+ "dax_score": 0,
125
+ "visual_score": 0,
126
+ "written_score": 0,
127
+ }
128
+
129
+ overall_score = int(sum(s[1] for s in sections) / len(sections))
130
+ feedback_parts = []
131
+ for name, score, fb in sections:
132
+ feedback_parts.append("{} (Score: {}/100):\n{}".format(name, score, fb))
133
+ combined_feedback = "\n\n".join(feedback_parts)
134
+
135
+ return {
136
+ "score": overall_score,
137
+ "feedback": combined_feedback,
138
+ "issues": all_issues,
139
+ "recommendations": all_recommendations,
140
+ "dax_score": dax_score,
141
+ "visual_score": visual_score,
142
+ "written_score": written_score,
143
+ }
144
+
145
+ def _evaluate_dax(self, questions, pbit_path):
146
+ data_model = self._processor.extract_datamodel(pbit_path)
147
+ summary = self._processor.extract_model_summary(data_model)
148
+ combined = "\n\n".join(
149
+ "Question {}:\n{}\n\nAnswer {}:\n{}\n".format(i, q, i, json.dumps(summary))
150
+ for i, q in enumerate(questions, 1)
151
+ )
152
+ return self.model.evaluate(build_powerbi_dax_prompt(combined))
153
+
154
+ def _evaluate_visuals(self, question, pdf_path, output_dir):
155
+ image_paths = self._processor.pdf_to_images(pdf_path, output_dir=output_dir)
156
+ prompt = POWERBI_VISUAL_PROMPT.format(question=question)
157
+ return self.model.evaluate_with_images(prompt, image_paths)
158
+
159
+ def _evaluate_written(self, question, txt_path):
160
+ answer_text = self._processor.read_text_file(txt_path)
161
+ prompt = POWERBI_WRITTEN_PROMPT.format(question=question, answer=answer_text)
162
+ return self.model.evaluate(prompt)
@@ -0,0 +1,19 @@
1
+ """Python homework evaluator."""
2
+
3
+ import os
4
+ from typing import List
5
+
6
+ from .base import BaseEvaluator
7
+ from ..parsers.python import PythonParser
8
+ from ..prompts.python import build_python_prompt
9
+
10
+
11
+ class PythonEvaluator(BaseEvaluator):
12
+ _parser = PythonParser()
13
+
14
+ def _parse_answers(self, answer_path: str, temp_dir: str = None) -> List[str]:
15
+ td = temp_dir or "temp_python_{}".format(os.getpid())
16
+ return self._parser.parse(answer_path, temp_dir=td)
17
+
18
+ def _build_prompt(self, combined_content: str) -> str:
19
+ return build_python_prompt(combined_content)
@@ -0,0 +1,19 @@
1
+ """SQL homework evaluator."""
2
+
3
+ import os
4
+ from typing import List
5
+
6
+ from .base import BaseEvaluator
7
+ from ..parsers.sql import SQLParser
8
+ from ..prompts.sql import build_sql_prompt
9
+
10
+
11
+ class SQLEvaluator(BaseEvaluator):
12
+ _parser = SQLParser()
13
+
14
+ def _parse_answers(self, answer_path: str, temp_dir: str = None) -> List[str]:
15
+ td = temp_dir or "temp_sql_{}".format(os.getpid())
16
+ return self._parser.parse(answer_path, temp_dir=td)
17
+
18
+ def _build_prompt(self, combined_content: str) -> str:
19
+ return build_sql_prompt(combined_content)
@@ -0,0 +1,54 @@
1
+ """SSIS homework evaluator."""
2
+
3
+ import logging
4
+ import os
5
+ from typing import Dict, List
6
+
7
+ from ..config import empty_result
8
+ from ..parsers.ssis import SSISParser
9
+ from ..prompts.ssis import build_ssis_prompt
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class SSISEvaluator:
15
+ """Evaluates SSIS .dtsx submissions.
16
+
17
+ Unlike the text-based evaluators, SSIS requires structured XML parsing,
18
+ so it does not extend BaseEvaluator.
19
+ """
20
+
21
+ def __init__(self, model):
22
+ self.model = model
23
+ self._parser = SSISParser()
24
+
25
+ def evaluate(self, questions: List[str], answer_path: str, temp_dir: str = None) -> Dict:
26
+ try:
27
+ td = temp_dir or "temp_ssis_{}".format(os.getpid())
28
+ parsed = self._parser.parse(answer_path, temp_dir=td)
29
+
30
+ answers = parsed.get("text_answers", [])
31
+ structured = parsed.get("structured_data", {"issues": []})
32
+
33
+ # Align answer count with questions
34
+ if not answers:
35
+ answers = ["No valid SSIS components found"] * len(questions)
36
+ elif len(answers) < len(questions):
37
+ answers = [answers[i % len(answers)] for i in range(len(questions))]
38
+ elif len(answers) > len(questions):
39
+ answers = answers[: len(questions)]
40
+
41
+ combined_q = "\n".join(q.strip() for q in questions if q.strip())
42
+ combined_a = "\n".join(a.strip() for a in answers if a.strip())
43
+ issues = structured.get("issues", [])
44
+ raw = "Questions:\n{}\n\nAnswers:\n{}\n\nIssues:\n{}".format(
45
+ combined_q, combined_a, ", ".join(issues) if issues else "None"
46
+ )
47
+
48
+ result = self.model.evaluate(build_ssis_prompt(raw))
49
+ result["issues"] = result.get("issues", []) + issues
50
+ return result
51
+
52
+ except Exception as exc:
53
+ logger.error("SSIS evaluation failed: %s", exc)
54
+ return empty_result(feedback="Evaluation failed: {}".format(exc))