ADFMentor 0.3.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.
- adfmentor-0.3.0/ADFMentor.egg-info/PKG-INFO +370 -0
- adfmentor-0.3.0/ADFMentor.egg-info/SOURCES.txt +20 -0
- adfmentor-0.3.0/ADFMentor.egg-info/dependency_links.txt +1 -0
- adfmentor-0.3.0/ADFMentor.egg-info/requires.txt +2 -0
- adfmentor-0.3.0/ADFMentor.egg-info/top_level.txt +3 -0
- adfmentor-0.3.0/ADFmentor/__init__.py +3 -0
- adfmentor-0.3.0/ADFmentor/core.py +121 -0
- adfmentor-0.3.0/ADFmentor/models/__init__.py +9 -0
- adfmentor-0.3.0/ADFmentor/models/gemini.py +80 -0
- adfmentor-0.3.0/ADFmentor/models/model.py +63 -0
- adfmentor-0.3.0/ADFmentor/utils/__init__.py +20 -0
- adfmentor-0.3.0/ADFmentor/utils/checker.py +61 -0
- adfmentor-0.3.0/ADFmentor/utils/extractor.py +56 -0
- adfmentor-0.3.0/ADFmentor/utils/path_handler.py +28 -0
- adfmentor-0.3.0/ADFmentor/utils/processor.py +461 -0
- adfmentor-0.3.0/ADFmentor/utils/question_parser.py +41 -0
- adfmentor-0.3.0/LICENSE +21 -0
- adfmentor-0.3.0/PKG-INFO +370 -0
- adfmentor-0.3.0/README.md +349 -0
- adfmentor-0.3.0/pyproject.toml +34 -0
- adfmentor-0.3.0/setup.cfg +4 -0
- adfmentor-0.3.0/tests/test.py +71 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ADFMentor
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Parse Azure Data Factory project files and evaluate them using AI models.
|
|
5
|
+
Author-email: Qobiljon Xayrullayev <qobiljonkhayrullayev@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: google-genai>=1.0.0
|
|
19
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
ADFMentor
|
|
23
|
+
=========
|
|
24
|
+
|
|
25
|
+
A Python package for parsing Azure Data Factory (ADF) project files and evaluating them using AI models like Google Gemini.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- 🏗️ **ADF Processing**: Parse ADF pipeline, dataset, linked service, trigger, and dataflow JSON files
|
|
30
|
+
- **Pipeline** — Parse ADF pipeline JSON files (activities, dependencies, parameters)
|
|
31
|
+
- **Written** — Evaluate text-based answers about ADF concepts
|
|
32
|
+
- 📝 **Detailed Reports**: Generate comprehensive grading reports from ADF project structures
|
|
33
|
+
- 📦 **ZIP Support**: Automatically handles ZIP file submissions — no manual extraction needed
|
|
34
|
+
- 🗂️ **Flexible Inputs**: Accepts a directory, a ZIP, or a single file (`.json`, `.txt`)
|
|
35
|
+
- 🔍 **Auto File Discovery**: Locates ADF resource folders (`pipeline/`, `dataset/`, `linkedService/`, etc.)
|
|
36
|
+
- ⚠️ **Graceful Missing-File Scoring**: Missing required files yield a `0` score and clear feedback
|
|
37
|
+
- 🧩 **Lesson Question Parser**: Parse structured lesson text into `pipeline` and `text` question blocks
|
|
38
|
+
- 🔧 **Easy Integration**: Simple API for evaluating student assignments and projects
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
### From Source
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/yourusername/ADFMentor.git
|
|
46
|
+
cd ADFMentor
|
|
47
|
+
pip install -e .
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Using pip (when published)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install ADFMentor
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from ADFmentor import ADFMentor
|
|
60
|
+
|
|
61
|
+
# Initialize with your Gemini API key
|
|
62
|
+
mentor = ADFMentor(api_key="your-api-key")
|
|
63
|
+
|
|
64
|
+
# Evaluate a full submission (pipeline + written)
|
|
65
|
+
# Works with directories, ZIP files, or single files (.json/.txt)
|
|
66
|
+
questions = """
|
|
67
|
+
PIPELINE: Create an ADF pipeline to copy data from Blob Storage to SQL Database
|
|
68
|
+
TEXT: Explain your pipeline design choices
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
prompts = {
|
|
72
|
+
"pipeline": "Evaluate pipeline structure, activities, and best practices",
|
|
73
|
+
"text": "Evaluate clarity and reasoning",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
result = mentor.evaluate_all(
|
|
77
|
+
answer_path="path/to/submission/", # or "submission.zip"
|
|
78
|
+
questions=questions,
|
|
79
|
+
prompts=prompts,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
print(f"Score: {result['score']}/100")
|
|
83
|
+
print(f"Feedback:\n{result['feedback']}")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Package Structure
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
ADFMentor/
|
|
90
|
+
├── __init__.py # Main package entry point
|
|
91
|
+
├── core.py # ADFMentor class with evaluation methods
|
|
92
|
+
├── models/ # AI model wrappers
|
|
93
|
+
│ ├── __init__.py
|
|
94
|
+
│ ├── model.py # Abstract base model
|
|
95
|
+
│ └── gemini.py # Google Gemini implementation
|
|
96
|
+
└── utils/ # Utility functions
|
|
97
|
+
├── __init__.py
|
|
98
|
+
├── processor.py # ADF JSON parsing and report generation
|
|
99
|
+
├── checker.py # File discovery helpers
|
|
100
|
+
├── extractor.py # ZIP extraction utilities
|
|
101
|
+
└── question_parser.py # Lesson question parser helpers
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Core Components
|
|
105
|
+
|
|
106
|
+
### ADFMentor Class
|
|
107
|
+
|
|
108
|
+
The main class provides a single evaluation method:
|
|
109
|
+
|
|
110
|
+
- **`evaluate_all(answer_path, questions, prompts)`**: Evaluates pipeline structure and written answers together and returns an overall score and combined feedback
|
|
111
|
+
|
|
112
|
+
Notes:
|
|
113
|
+
- `answer_path` can be a directory, ZIP file, or a single submission file (`.json`, `.txt`).
|
|
114
|
+
- `questions` and `prompts` must include `pipeline` and `text` keys. A section is skipped if its question is set to `None`.
|
|
115
|
+
|
|
116
|
+
### ADF Processor
|
|
117
|
+
|
|
118
|
+
`ADFMentor.utils.processor` provides functions for processing ADF projects:
|
|
119
|
+
|
|
120
|
+
#### `parse_adf_json(json_path)`
|
|
121
|
+
Reads and parses a single ADF resource JSON file.
|
|
122
|
+
|
|
123
|
+
#### `discover_adf_resources(directory)`
|
|
124
|
+
Scans a directory for ADF resource folders:
|
|
125
|
+
- `pipeline/` — Pipeline definitions
|
|
126
|
+
- `dataset/` — Dataset definitions
|
|
127
|
+
- `linkedService/` — Linked service definitions
|
|
128
|
+
- `trigger/` — Trigger definitions
|
|
129
|
+
- `dataflow/` — Dataflow definitions
|
|
130
|
+
|
|
131
|
+
#### `extract_grading_info(resources)`
|
|
132
|
+
Extracts key elements for grading:
|
|
133
|
+
- Pipelines: activities, dependencies, parameters, variables
|
|
134
|
+
- Datasets: type, linked service reference, schema, location
|
|
135
|
+
- Linked Services: type, connection details (sanitized)
|
|
136
|
+
- Triggers: type, schedule, pipeline references
|
|
137
|
+
- Dataflows: sources, sinks, transformations
|
|
138
|
+
|
|
139
|
+
#### `generate_grading_report(grading_info)`
|
|
140
|
+
Formats extracted information into a readable text report.
|
|
141
|
+
|
|
142
|
+
#### `analyze_adf(adf_path)`
|
|
143
|
+
Convenience function that chains all steps above.
|
|
144
|
+
|
|
145
|
+
### AI Models
|
|
146
|
+
|
|
147
|
+
#### Gemini Model
|
|
148
|
+
`ADFMentor.models.gemini.Gemini`
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from ADFmentor.models import Gemini
|
|
152
|
+
|
|
153
|
+
model = Gemini(api_key="your-api-key", model_name="gemini-2.0-flash-exp")
|
|
154
|
+
|
|
155
|
+
# Evaluate text-based answers
|
|
156
|
+
result = model.evaluate(
|
|
157
|
+
question="What are ADF linked services?",
|
|
158
|
+
answer="Linked services are connection strings...",
|
|
159
|
+
prompt="Evaluate for accuracy and completeness"
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Response Format:**
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"score": 85,
|
|
167
|
+
"feedback": "Strong implementation with minor issues..."
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Configuration
|
|
172
|
+
|
|
173
|
+
### API Key Setup
|
|
174
|
+
|
|
175
|
+
Create a `.env` file in your project root:
|
|
176
|
+
|
|
177
|
+
```env
|
|
178
|
+
API_KEY=your_gemini_api_key_here
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Load it in your code:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from dotenv import load_dotenv
|
|
185
|
+
import os
|
|
186
|
+
|
|
187
|
+
load_dotenv()
|
|
188
|
+
api_key = os.getenv("API_KEY")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Detailed Usage Examples
|
|
192
|
+
|
|
193
|
+
### 1. Analyze an ADF Project
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from ADFmentor.utils import analyze_adf
|
|
197
|
+
|
|
198
|
+
# Generate a detailed report from an ADF project directory
|
|
199
|
+
report = analyze_adf("path/to/adf-project/")
|
|
200
|
+
print(report)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Sample Output:**
|
|
204
|
+
```
|
|
205
|
+
============================================================
|
|
206
|
+
AZURE DATA FACTORY PROJECT REPORT
|
|
207
|
+
============================================================
|
|
208
|
+
|
|
209
|
+
PIPELINES:
|
|
210
|
+
- CopyBlobToSQL
|
|
211
|
+
Parameters: inputPath, outputTable
|
|
212
|
+
Activities (3):
|
|
213
|
+
• LookupSource (type: Lookup)
|
|
214
|
+
• CopyData (type: Copy)
|
|
215
|
+
depends on: LookupSource [Succeeded]
|
|
216
|
+
source: BlobSource
|
|
217
|
+
sink: SqlSink
|
|
218
|
+
• StoredProcedure (type: SqlServerStoredProcedure)
|
|
219
|
+
depends on: CopyData [Succeeded]
|
|
220
|
+
|
|
221
|
+
DATASETS:
|
|
222
|
+
- BlobInput (type: DelimitedText)
|
|
223
|
+
linked service: AzureBlobStorage
|
|
224
|
+
location: type: AzureBlobStorageLocation, folder: input
|
|
225
|
+
- SqlOutput (type: AzureSqlTable)
|
|
226
|
+
linked service: AzureSqlDatabase
|
|
227
|
+
table: dbo.SalesData
|
|
228
|
+
|
|
229
|
+
LINKED SERVICES:
|
|
230
|
+
- AzureBlobStorage (type: AzureBlobStorage)
|
|
231
|
+
- AzureSqlDatabase (type: AzureSqlDatabase)
|
|
232
|
+
|
|
233
|
+
TRIGGERS:
|
|
234
|
+
- DailyTrigger (type: ScheduleTrigger)
|
|
235
|
+
schedule: every 1 Day
|
|
236
|
+
pipelines: CopyBlobToSQL
|
|
237
|
+
|
|
238
|
+
DATAFLOWS:
|
|
239
|
+
none
|
|
240
|
+
|
|
241
|
+
SUMMARY:
|
|
242
|
+
- total_pipelines: 1
|
|
243
|
+
- total_activities: 3
|
|
244
|
+
- total_datasets: 2
|
|
245
|
+
- total_linked_services: 2
|
|
246
|
+
- total_triggers: 1
|
|
247
|
+
- total_dataflows: 0
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### 2. Complete Evaluation Pipeline
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
from ADFmentor import ADFMentor
|
|
254
|
+
|
|
255
|
+
mentor = ADFMentor(api_key="your-api-key")
|
|
256
|
+
|
|
257
|
+
# Define questions and prompts for each evaluation type
|
|
258
|
+
questions = {
|
|
259
|
+
"pipeline": "Create a pipeline to copy data from Blob to SQL with error handling",
|
|
260
|
+
"text": "Explain your pipeline design choices"
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
prompts = {
|
|
264
|
+
"pipeline": "Evaluate pipeline structure, activities, error handling, and best practices",
|
|
265
|
+
"text": "Evaluate clarity, justification, and understanding"
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
# Evaluate all aspects
|
|
269
|
+
result = mentor.evaluate_all(
|
|
270
|
+
answer_path="path/to/student/submission/",
|
|
271
|
+
questions=questions,
|
|
272
|
+
prompts=prompts
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
print(f"Overall Score: {result['score']}/100")
|
|
276
|
+
print(f"Feedback:\n{result['feedback']}")
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 3. Parse Lesson Questions
|
|
280
|
+
|
|
281
|
+
If your lesson content uses codes like `"TEXT001"`, `"PIPELINE002"`, you can parse it into question blocks:
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
from ADFmentor.utils.question_parser import parse_lesson_questions
|
|
285
|
+
|
|
286
|
+
lesson_text = """
|
|
287
|
+
"TEXT001"
|
|
288
|
+
1. Explain the purpose of linked services in ADF.
|
|
289
|
+
|
|
290
|
+
"PIPELINE002"
|
|
291
|
+
2. Create a pipeline to copy data from Blob Storage to SQL Database.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
questions = parse_lesson_questions(lesson_text)
|
|
295
|
+
# -> {"text": "1. ...", "pipeline": "2. ..."}
|
|
296
|
+
|
|
297
|
+
# Map to the evaluate_all() schema
|
|
298
|
+
questions = {
|
|
299
|
+
"pipeline": questions["pipeline"],
|
|
300
|
+
"text": questions["text"],
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### 4. Skipping an Evaluation Section
|
|
305
|
+
|
|
306
|
+
To skip a section, set its question to `None` (the key must still exist):
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
questions = {
|
|
310
|
+
"pipeline": "Create a copy pipeline with parameterized paths",
|
|
311
|
+
"text": None,
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
prompts = {
|
|
315
|
+
"pipeline": "Evaluate pipeline structure and best practices",
|
|
316
|
+
"text": "Evaluate clarity, justification, and understanding",
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## ADF Project Structure
|
|
321
|
+
|
|
322
|
+
ADFMentor expects submissions to follow the standard ADF project structure:
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
adf-project/
|
|
326
|
+
├── pipeline/ # Pipeline JSON definitions
|
|
327
|
+
│ └── CopyPipeline.json
|
|
328
|
+
├── dataset/ # Dataset JSON definitions
|
|
329
|
+
│ ├── BlobInput.json
|
|
330
|
+
│ └── SqlOutput.json
|
|
331
|
+
├── linkedService/ # Linked service definitions
|
|
332
|
+
│ ├── BlobStorage.json
|
|
333
|
+
│ └── SqlDatabase.json
|
|
334
|
+
├── trigger/ # Trigger definitions
|
|
335
|
+
│ └── DailyTrigger.json
|
|
336
|
+
└── dataflow/ # Dataflow definitions (optional)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Each JSON file follows the standard ADF resource format with `name`, `type`, and `properties` fields.
|
|
340
|
+
|
|
341
|
+
## Development
|
|
342
|
+
|
|
343
|
+
### Running Tests
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
# Run integration tests
|
|
347
|
+
python tests/test.py
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Project Dependencies
|
|
351
|
+
|
|
352
|
+
Core:
|
|
353
|
+
- `google-genai>=1.0.0` - Google Gemini API client
|
|
354
|
+
- `python-dotenv>=1.0.0` - Environment variable management
|
|
355
|
+
|
|
356
|
+
Optional:
|
|
357
|
+
- `google-cloud-aiplatform>=1.0.0` - For Vertex AI support
|
|
358
|
+
|
|
359
|
+
## Requirements
|
|
360
|
+
|
|
361
|
+
- Python 3.9 or higher
|
|
362
|
+
- Google Gemini API key (get one at [Google AI Studio](https://makersuite.google.com/app/apikey))
|
|
363
|
+
|
|
364
|
+
## Contributing
|
|
365
|
+
|
|
366
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
367
|
+
|
|
368
|
+
## License
|
|
369
|
+
|
|
370
|
+
MIT License - see LICENSE file for details
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
ADFMentor.egg-info/PKG-INFO
|
|
5
|
+
ADFMentor.egg-info/SOURCES.txt
|
|
6
|
+
ADFMentor.egg-info/dependency_links.txt
|
|
7
|
+
ADFMentor.egg-info/requires.txt
|
|
8
|
+
ADFMentor.egg-info/top_level.txt
|
|
9
|
+
ADFmentor/__init__.py
|
|
10
|
+
ADFmentor/core.py
|
|
11
|
+
ADFmentor/models/__init__.py
|
|
12
|
+
ADFmentor/models/gemini.py
|
|
13
|
+
ADFmentor/models/model.py
|
|
14
|
+
ADFmentor/utils/__init__.py
|
|
15
|
+
ADFmentor/utils/checker.py
|
|
16
|
+
ADFmentor/utils/extractor.py
|
|
17
|
+
ADFmentor/utils/path_handler.py
|
|
18
|
+
ADFmentor/utils/processor.py
|
|
19
|
+
ADFmentor/utils/question_parser.py
|
|
20
|
+
tests/test.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from .models.gemini import Gemini
|
|
7
|
+
from ADFmentor.utils.processor import analyze_adf
|
|
8
|
+
from ADFmentor.utils.checker import get_file_by_type, is_adf_project
|
|
9
|
+
from .utils.path_handler import prepare_answer_path
|
|
10
|
+
from .utils.question_parser import parse_lesson_questions
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ADFMentor:
|
|
14
|
+
def __init__(self, api_key: str, model_name: str = "gemini-2.0-flash-exp", model: Optional[Gemini] = None):
|
|
15
|
+
if model is None:
|
|
16
|
+
model = Gemini(api_key=api_key, model_name=model_name)
|
|
17
|
+
self.model = model
|
|
18
|
+
|
|
19
|
+
def _evaluate_pipeline_from_path(self, working_path: str, question: Optional[str], prompt: str) -> Optional[
|
|
20
|
+
Dict[str, Any]]:
|
|
21
|
+
if question is None:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
path = Path(working_path)
|
|
25
|
+
|
|
26
|
+
if path.is_file() and path.suffix.lower() == ".json":
|
|
27
|
+
adf_path = path
|
|
28
|
+
elif path.is_dir():
|
|
29
|
+
if is_adf_project(str(path)):
|
|
30
|
+
adf_path = path
|
|
31
|
+
else:
|
|
32
|
+
json_file = get_file_by_type(working_path, ".json")
|
|
33
|
+
if not json_file:
|
|
34
|
+
return {
|
|
35
|
+
"score": 0,
|
|
36
|
+
"feedback": (
|
|
37
|
+
"Unable to evaluate submission.\n\n"
|
|
38
|
+
"The assignment includes ADF pipeline questions, but no ADF project files (JSON) were found. "
|
|
39
|
+
"Please ensure your submission includes all required components and resubmit."
|
|
40
|
+
),
|
|
41
|
+
}
|
|
42
|
+
# Use the whole directory so analyze_adf can rglob all .json files
|
|
43
|
+
adf_path = path
|
|
44
|
+
else:
|
|
45
|
+
return {
|
|
46
|
+
"score": 0,
|
|
47
|
+
"feedback": (
|
|
48
|
+
"Unable to evaluate submission.\n\n"
|
|
49
|
+
"The assignment includes ADF pipeline questions, but no ADF project files were found. "
|
|
50
|
+
"Please ensure your submission includes all required components and resubmit."
|
|
51
|
+
),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return self.model.evaluate(
|
|
55
|
+
question=question,
|
|
56
|
+
answer=analyze_adf(str(adf_path)),
|
|
57
|
+
prompt=prompt,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _evaluate_write_from_path(self, working_path: str, question: Optional[str], prompt: str) -> Optional[
|
|
61
|
+
Dict[str, Any]]:
|
|
62
|
+
if question is None:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
path = Path(working_path)
|
|
66
|
+
|
|
67
|
+
if path.is_file() and path.suffix.lower() == ".txt":
|
|
68
|
+
txt_path = path
|
|
69
|
+
else:
|
|
70
|
+
txt_file = get_file_by_type(working_path, ".txt")
|
|
71
|
+
if not txt_file:
|
|
72
|
+
return {
|
|
73
|
+
"score": 0,
|
|
74
|
+
"feedback": (
|
|
75
|
+
"Unable to evaluate submission.\n\n"
|
|
76
|
+
"The assignment includes written type questions, but no written type response (TXT file) was found. "
|
|
77
|
+
"Please ensure your submission includes all required components and resubmit."
|
|
78
|
+
),
|
|
79
|
+
}
|
|
80
|
+
txt_path = path / txt_file
|
|
81
|
+
|
|
82
|
+
text_answer = txt_path.read_text(encoding="utf-8")
|
|
83
|
+
|
|
84
|
+
return self.model.evaluate(
|
|
85
|
+
question=question,
|
|
86
|
+
answer=text_answer,
|
|
87
|
+
prompt=prompt,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def evaluate_all(self, answer_path: str, questions: str, prompts: Dict[str, str]) -> Dict[str, Any]:
|
|
91
|
+
working_path = prepare_answer_path(answer_path)
|
|
92
|
+
|
|
93
|
+
questions = parse_lesson_questions(questions)
|
|
94
|
+
|
|
95
|
+
results = {
|
|
96
|
+
"pipeline": self._evaluate_pipeline_from_path(working_path, questions.get("pipeline"), prompts.get("pipeline")),
|
|
97
|
+
"text": self._evaluate_write_from_path(working_path, questions.get("text"), prompts.get("text")),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
scores = []
|
|
101
|
+
feedback_parts = []
|
|
102
|
+
|
|
103
|
+
for key, value in results.items():
|
|
104
|
+
if value is not None:
|
|
105
|
+
scores.append(value['score'])
|
|
106
|
+
feedback_parts.append(
|
|
107
|
+
f"{'=' * 70}\n"
|
|
108
|
+
f"{key.upper()} EVALUATION\n"
|
|
109
|
+
f"{'=' * 70}\n"
|
|
110
|
+
f"Score: {value['score']}/100\n\n"
|
|
111
|
+
f"{value['feedback']}\n"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
avg_score = sum(scores) / len(scores) if scores else 0
|
|
115
|
+
|
|
116
|
+
summary = {
|
|
117
|
+
'score': round(avg_score, 2),
|
|
118
|
+
'feedback': '\n'.join(feedback_parts) if feedback_parts else "No evaluations completed."
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return summary
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, Union
|
|
4
|
+
from google import genai
|
|
5
|
+
from google.genai import types
|
|
6
|
+
from .model import Model, build_content
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Gemini(Model):
|
|
11
|
+
"""Google Gemini model wrapper for ADFMentor evaluations.
|
|
12
|
+
|
|
13
|
+
Uses the Google Gemini API to evaluate student submissions with
|
|
14
|
+
structured JSON responses containing scores and feedback.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
client: Google Gemini API client
|
|
18
|
+
model_name: Name of the Gemini model to use
|
|
19
|
+
response_schema: JSON schema for structured responses
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, api_key: str, model_name: str = "gemini-2.0-flash-exp"):
|
|
23
|
+
"""Initialize the Gemini model.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
api_key: Your Google Gemini API key
|
|
27
|
+
model_name: Model to use (default: gemini-2.0-flash-exp)
|
|
28
|
+
"""
|
|
29
|
+
super().__init__()
|
|
30
|
+
self.client = genai.Client(api_key=api_key)
|
|
31
|
+
self.model_name = model_name
|
|
32
|
+
|
|
33
|
+
self.response_schema = types.GenerateContentConfig(
|
|
34
|
+
response_mime_type="application/json",
|
|
35
|
+
response_schema={
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"score": {
|
|
39
|
+
"type": "number",
|
|
40
|
+
"description": "The numerical score for the evaluation"
|
|
41
|
+
},
|
|
42
|
+
"feedback": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "Detailed feedback explaining the score"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"required": ["score", "feedback"]
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def evaluate(self, question: str, answer: str, prompt: str) -> Dict[str, Any]:
|
|
52
|
+
"""Evaluate a text-based answer.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
question: The assignment question
|
|
56
|
+
answer: The student's answer
|
|
57
|
+
prompt: Evaluation criteria and instructions
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Dictionary with 'score' (int, 0-100) and 'feedback' (str)
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: If the model doesn't return valid JSON or missing fields
|
|
64
|
+
"""
|
|
65
|
+
response = self.client.models.generate_content(
|
|
66
|
+
model=self.model_name,
|
|
67
|
+
contents=build_content(question, answer, prompt),
|
|
68
|
+
config=self.response_schema,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
text = (response.text or "").strip()
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
result = json.loads(text)
|
|
75
|
+
|
|
76
|
+
if "score" not in result or "feedback" not in result:
|
|
77
|
+
raise ValueError(f"Missing required fields (score, feedback) in response: {result}")
|
|
78
|
+
return result
|
|
79
|
+
except json.JSONDecodeError as e:
|
|
80
|
+
raise ValueError(f"Model did not return valid JSON.\nRaw output:\n{text}") from e
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Abstract base model for ADFMentor evaluators."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def build_content(question: str, answer: str, prompt: str) -> str:
|
|
8
|
+
"""Build the evaluation prompt for text-based evaluations.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
question: The assignment question
|
|
12
|
+
answer: The student's answer
|
|
13
|
+
prompt: Evaluation instructions
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Formatted prompt string
|
|
17
|
+
"""
|
|
18
|
+
return f"""
|
|
19
|
+
Instruction:
|
|
20
|
+
{prompt.strip()}
|
|
21
|
+
|
|
22
|
+
Question:
|
|
23
|
+
{question.strip()}
|
|
24
|
+
|
|
25
|
+
Answer:
|
|
26
|
+
{answer.strip()}
|
|
27
|
+
|
|
28
|
+
Return ONLY valid JSON in the following format.
|
|
29
|
+
DO NOT add explanations, markdown, or extra text.
|
|
30
|
+
DO NOT wrap in ```.
|
|
31
|
+
|
|
32
|
+
JSON schema:
|
|
33
|
+
{{
|
|
34
|
+
"score": number (0-100),
|
|
35
|
+
"feedback": string
|
|
36
|
+
}}
|
|
37
|
+
""".strip()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Model(ABC):
|
|
41
|
+
"""Abstract base class for AI model evaluators.
|
|
42
|
+
|
|
43
|
+
All model implementations must inherit from this class and implement
|
|
44
|
+
the evaluate method.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self):
|
|
48
|
+
"""Initialize the model."""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def evaluate(self, question: str, answer: str, prompt: str) -> Dict[str, Any]:
|
|
53
|
+
"""Evaluate an answer to a question using the AI model.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
question: The question or assignment prompt
|
|
57
|
+
answer: The student's answer or solution
|
|
58
|
+
prompt: Evaluation criteria and instructions for the model
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dictionary with 'score' (0-100) and 'feedback' (string)
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Utilities package for ADFMentor.
|
|
2
|
+
|
|
3
|
+
This package contains utility functions for processing Azure Data Factory
|
|
4
|
+
files and extracting metadata.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .processor import analyze_adf, parse_adf_json, extract_grading_info, generate_grading_report, discover_adf_resources
|
|
8
|
+
from .checker import get_file_by_type, is_adf_project
|
|
9
|
+
from .extractor import extract_zip_to_temp
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"analyze_adf",
|
|
13
|
+
"parse_adf_json",
|
|
14
|
+
"discover_adf_resources",
|
|
15
|
+
"extract_grading_info",
|
|
16
|
+
"generate_grading_report",
|
|
17
|
+
"get_file_by_type",
|
|
18
|
+
"is_adf_project",
|
|
19
|
+
"extract_zip_to_temp",
|
|
20
|
+
]
|