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.
- {quantumchecker-0.4.0 → quantumchecker-1.0.0}/PKG-INFO +52 -52
- quantumchecker-1.0.0/QuantumCheck/__init__.py +33 -0
- quantumchecker-1.0.0/QuantumCheck/config.py +101 -0
- quantumchecker-1.0.0/QuantumCheck/evaluator.py +130 -0
- quantumchecker-1.0.0/QuantumCheck/evaluators/__init__.py +6 -0
- quantumchecker-1.0.0/QuantumCheck/evaluators/base.py +44 -0
- quantumchecker-1.0.0/QuantumCheck/evaluators/powerbi.py +162 -0
- quantumchecker-1.0.0/QuantumCheck/evaluators/python.py +19 -0
- quantumchecker-1.0.0/QuantumCheck/evaluators/sql.py +19 -0
- quantumchecker-1.0.0/QuantumCheck/evaluators/ssis.py +54 -0
- quantumchecker-1.0.0/QuantumCheck/exceptions.py +21 -0
- quantumchecker-1.0.0/QuantumCheck/main.py +2 -0
- quantumchecker-1.0.0/QuantumCheck/model.py +142 -0
- quantumchecker-1.0.0/QuantumCheck/parsers/__init__.py +7 -0
- quantumchecker-1.0.0/QuantumCheck/parsers/base.py +94 -0
- quantumchecker-1.0.0/QuantumCheck/parsers/lesson.py +69 -0
- quantumchecker-1.0.0/QuantumCheck/parsers/powerbi.py +198 -0
- quantumchecker-1.0.0/QuantumCheck/parsers/python.py +12 -0
- quantumchecker-1.0.0/QuantumCheck/parsers/sql.py +12 -0
- quantumchecker-1.0.0/QuantumCheck/parsers/ssis.py +203 -0
- quantumchecker-1.0.0/QuantumCheck/powerbi_evaluator.py +4 -0
- quantumchecker-1.0.0/QuantumCheck/powerbi_mentor.py +58 -0
- quantumchecker-1.0.0/QuantumCheck/prompts/__init__.py +6 -0
- quantumchecker-1.0.0/QuantumCheck/prompts/powerbi.py +44 -0
- quantumchecker-1.0.0/QuantumCheck/prompts/python.py +28 -0
- quantumchecker-1.0.0/QuantumCheck/prompts/sql.py +39 -0
- quantumchecker-1.0.0/QuantumCheck/prompts/ssis.py +31 -0
- quantumchecker-1.0.0/QuantumCheck/prompts.py +5 -0
- quantumchecker-1.0.0/QuantumCheck/python_evaluator.py +2 -0
- quantumchecker-1.0.0/QuantumCheck/python_mentor.py +36 -0
- quantumchecker-1.0.0/QuantumCheck/quantum_check.py +46 -0
- quantumchecker-1.0.0/QuantumCheck/sql_evaluator.py +2 -0
- quantumchecker-1.0.0/QuantumCheck/sql_mentor.py +36 -0
- quantumchecker-1.0.0/QuantumCheck/ssis_evaluator.py +2 -0
- quantumchecker-1.0.0/QuantumCheck/ssis_mentor.py +36 -0
- {quantumchecker-0.4.0 → quantumchecker-1.0.0}/QuantumChecker.egg-info/PKG-INFO +52 -52
- quantumchecker-1.0.0/QuantumChecker.egg-info/SOURCES.txt +47 -0
- quantumchecker-1.0.0/QuantumChecker.egg-info/requires.txt +6 -0
- {quantumchecker-0.4.0 → quantumchecker-1.0.0}/setup.cfg +4 -4
- {quantumchecker-0.4.0 → quantumchecker-1.0.0}/setup.py +28 -28
- {quantumchecker-0.4.0 → quantumchecker-1.0.0}/tests/test2.py +4 -4
- quantumchecker-1.0.0/tests/test_comprehensive.py +1340 -0
- quantumchecker-1.0.0/tests/test_live.py +257 -0
- quantumchecker-1.0.0/tests/test_refactored.py +51 -0
- quantumchecker-1.0.0/tests/test_simple.py +50 -0
- quantumchecker-0.4.0/QuantumCheck/__init__.py +0 -1
- quantumchecker-0.4.0/QuantumCheck/main.py +0 -138
- quantumchecker-0.4.0/QuantumCheck/powerbi_evaluator.py +0 -403
- quantumchecker-0.4.0/QuantumCheck/prompts.py +0 -185
- quantumchecker-0.4.0/QuantumCheck/python_evaluator.py +0 -213
- quantumchecker-0.4.0/QuantumCheck/sql_evaluator.py +0 -213
- quantumchecker-0.4.0/QuantumCheck/ssis_evaluator.py +0 -386
- quantumchecker-0.4.0/QuantumChecker.egg-info/SOURCES.txt +0 -17
- quantumchecker-0.4.0/QuantumChecker.egg-info/requires.txt +0 -6
- {quantumchecker-0.4.0 → quantumchecker-1.0.0}/QuantumChecker.egg-info/dependency_links.txt +0 -0
- {quantumchecker-0.4.0 → quantumchecker-1.0.0}/QuantumChecker.egg-info/top_level.txt +0 -0
- {quantumchecker-0.4.0 → quantumchecker-1.0.0}/README.md +0 -0
- {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
|
-
Summary:
|
|
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.
|
|
11
|
-
Description-Content-Type: text/markdown
|
|
12
|
-
Requires-Dist:
|
|
13
|
-
Requires-Dist:
|
|
14
|
-
Requires-Dist:
|
|
15
|
-
|
|
16
|
-
Requires-Dist:
|
|
17
|
-
|
|
18
|
-
Dynamic: author
|
|
19
|
-
Dynamic:
|
|
20
|
-
Dynamic:
|
|
21
|
-
Dynamic: description
|
|
22
|
-
Dynamic:
|
|
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,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))
|