Spaces:
Running
Running
Rishit Pant commited on
Commit ·
bc9ec9b
1
Parent(s): b9d54f5
Improved ingestion, retrieve and generate (#4)
Browse files* Eval prompts and scripts
* Update dependencies
* Answer generation using LLLM
* Evaluation metrics using DeepEval
* Fix: mismatched heading tags
* Add noise regex and context
* Fixed noise patterns
* Improve system prompt
- .deepeval/.deepeval_telemetry.txt +2 -0
- data/grading-document.md +41 -54
- eval/eval_prompts.json +37 -0
- eval/evaluate.py +423 -0
- pyproject.toml +1 -1
- requirements.txt +2 -2
- src/generate.py +124 -0
- src/ingest.py +30 -5
- src/retrieve.py +2 -1
- uv.lock +18 -5
.deepeval/.deepeval_telemetry.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DEEPEVAL_ID=5f67d0f6-8daa-40e0-8e0c-a6e888223837
|
| 2 |
+
DEEPEVAL_STATUS=old
|
data/grading-document.md
CHANGED
|
@@ -1019,7 +1019,7 @@ BS-DS_ May 2026 Grading document (Student) Updated automatically every 5 minutes
|
|
| 1019 |
|
| 1020 |
(W11/W12 contents will be included for the final exam. Hence, please practice and submit W11/W12 assignment).
|
| 1021 |
|
| 1022 |
-
# 6. English 2
|
| 1023 |
|
| 1024 |
**Quiz 1:** July 19 2026 **Quiz 2:** August 16 2026 **End term:** September 13 2026
|
| 1025 |
|
|
@@ -1041,9 +1041,9 @@ F = score in final exam
|
|
| 1041 |
|
| 1042 |
(W11/W12 contents will be included for the final exam. Hence, please practice and submit W11/W12 assignment).
|
| 1043 |
|
| 1044 |
-
# 7. Intro to python programming
|
| 1045 |
|
| 1046 |
-
## Academic policies
|
| 1047 |
|
| 1048 |
1. In each programming assignment, be it any course or any OPPE, taking help from LLMs (e.g. ChatGPT, Gemini) partially or completely is considered plagiarism.
|
| 1049 |
|
|
@@ -1291,7 +1291,7 @@ Updated automatically every 5 minutes
|
|
| 1291 |
|
| 1292 |
Suggested pathway to register and study
|
| 1293 |
|
| 1294 |
-
#
|
| 1295 |
|
| 1296 |
1. **Most aggressive pathway** - completing in 4 terms - ONLY IF YOU ARE DOING THIS AS FULL TIME AND NOTHING ELSE AND CAN SPEND <u>70 HRs PER WEEK</u> MINIMUM
|
| 1297 |
|
|
@@ -1872,16 +1872,9 @@ July 3rd week (Tentative): We will release the slots for OPPE 1 & Dates for OPPE
|
|
| 1872 |
|
| 1873 |
https://docs.google.com/document/d/e/2PACX-1vT5PBOz4OH663W0IJPVGVjG_nfmYZGfFI7W1j-6wTLcex13O_7BZmf6a96Q6liO0W-mLZB5hOGZ… 13/40
|
| 1874 |
|
| 1875 |
-
6/9/26, 7:10 PM BS-DS_ May 2026 Grading document (Student)
|
| 1876 |
-
|
| 1877 |
-
Google Docs info icon Published using Google Docs Report abuse Learn more
|
| 1878 |
|
| 1879 |
to attend the exam and if you are eligible, exam will be scheduled as per the slots allocated. Please choose courses for the May 2026 term keeping all these points in mind.
|
| 1880 |
|
| 1881 |
-
BS-DS_ May 2026 Grading document (Student) Updated automatically every 5 minutes
|
| 1882 |
-
|
| 1883 |
-
# Diploma level courses
|
| 1884 |
-
|
| 1885 |
## 1. Machine Learning foundations (DS Diploma)
|
| 1886 |
|
| 1887 |
**Quiz 1: July 19 2026** **Quiz 2: August 16 2026** **End term: September 13 2026**
|
|
@@ -1947,17 +1940,6 @@ SoP for the SCT Exam is as follows: <u>Click Here for OPPE SCT SoP Document</u>
|
|
| 1947 |
|
| 1948 |
https://docs.google.com/document/d/e/2PACX-1vT5PBOz4OH663W0IJPVGVjG_nfmYZGfFI7W1j-6wTLcex13O_7BZmf6a96Q6liO0W-mLZB5hOGZ… 14/40
|
| 1949 |
|
| 1950 |
-
6/9/26, 7:10 PM
|
| 1951 |
-
|
| 1952 |
-
BS-DS_ May 2026 Grading document (Student)
|
| 1953 |
-
|
| 1954 |
-
Google Docs icon Published using Google Docs
|
| 1955 |
-
|
| 1956 |
-
Report abuse Learn more
|
| 1957 |
-
|
| 1958 |
-
BS-DS_ May 2026 Grading document (Student)
|
| 1959 |
-
|
| 1960 |
-
Updated automatically every 5 minutes
|
| 1961 |
|
| 1962 |
OPPE1 will not be scheduled for students who fail to complete the OPPE SCT exam.
|
| 1963 |
<u>Repeat_OPPE Criteria</u> (w.e.f from Jan 2026 term) [updated on Jan 05, 2026]
|
|
@@ -2096,7 +2078,9 @@ Google Docs logo Published using Google Docs Report abuse Learn more
|
|
| 2096 |
# BS-DS_ May 2026 Grading document (Student)
|
| 2097 |
Updated automatically every 5 minutes
|
| 2098 |
|
| 2099 |
-
|
|
|
|
|
|
|
| 2100 |
|
| 2101 |
**Above to be attended in person at designated centres.**
|
| 2102 |
|
|
@@ -2117,7 +2101,7 @@ Overall score for eligible students:
|
|
| 2117 |
|
| 2118 |
(Though the W11/W12 assignment score is not included in GAA, W11/W12 contents will be included for the final exam. Hence, please practice and submit W11/W12 assignment).
|
| 2119 |
|
| 2120 |
-
# 5. Business Analytics (Diploma in DS)
|
| 2121 |
|
| 2122 |
**Quiz 1: July 19 2026** **Quiz 2: August 16 2026** **End term: September 13 2026**
|
| 2123 |
|
|
@@ -2182,7 +2166,7 @@ A = Sum of the Best 2 out of (Assignment 1, Assignment 2, Assignment 3)
|
|
| 2182 |
</tbody>
|
| 2183 |
</table>
|
| 2184 |
|
| 2185 |
-
# 6. Tools in Data Science (Diploma in DS)
|
| 2186 |
|
| 2187 |
https://docs.google.com/document/d/e/2PACX-1vT5PBOz4OH663W0IJPVGVjG_nfmYZGfFI7W1j-6wTLcex13O_7BZmf6a96Q6liO0W-mLZB5hOGZ... 16/40
|
| 2188 |
|
|
@@ -2195,7 +2179,7 @@ Report abuse Learn more
|
|
| 2195 |
# BS-DS_ May 2026 Grading document (Student)
|
| 2196 |
Updated automatically every 5 minutes
|
| 2197 |
|
| 2198 |
-
## Academic policies
|
| 2199 |
|
| 2200 |
1. The Tools in Data Science portal has all the course content: Graded Assignments, Projects and ROE links. The Seek Portal is not used for submissions.
|
| 2201 |
|
|
@@ -2248,7 +2232,7 @@ Before registering for TDS, please attempt the **Entrance Exam** at **https://ex
|
|
| 2248 |
|
| 2249 |
<mark>**Final course score T = 0.2 GAA + 0.2 ROE + 0.2 P1 + 0.2 P2 + 0.2 F**</mark>
|
| 2250 |
|
| 2251 |
-
# 7. Programming Data structures and algorithms using Python (PDSA) - Diploma in Programming
|
| 2252 |
|
| 2253 |
**Weekly assignments:** Mix of autograded assignment and Programming assignments
|
| 2254 |
|
|
@@ -2301,9 +2285,9 @@ OP = Score in Online proctored remote exam
|
|
| 2301 |
F = score in final exam
|
| 2302 |
**T = 0.05GAA + 0.2OP + 0.45F + max (0.2max(Qz1, Qz2), (0.10Qz1+0.20Qz2 ))**
|
| 2303 |
|
| 2304 |
-
# 8. Database management system (DBMS) -
|
| 2305 |
|
| 2306 |
-
## Diploma in Programming
|
| 2307 |
|
| 2308 |
**Quiz 1: July 19 2026** **Quiz 2: August 16 2026** **End term: September 13 2026**
|
| 2309 |
|
|
@@ -2479,7 +2463,7 @@ BS-DS_ May 2026 Grading document (Student) Updated automatically every 5 minutes
|
|
| 2479 |
</tbody>
|
| 2480 |
</table>
|
| 2481 |
|
| 2482 |
-
# 9. Application development - 1 (Diploma in programming)
|
| 2483 |
|
| 2484 |
**Quiz 1**: July 19 2026
|
| 2485 |
**Quiz 2**: August 16 2026
|
|
@@ -2533,7 +2517,7 @@ Google Docs logo Published using Google Docs Report abuse Learn more
|
|
| 2533 |
|
| 2534 |
**Final course score T = 0.05 GLA + max (0.6F + 0.25max(Qz1, Qz2), 0.4F + 0.25Qz1 + 0.3Qz2)**
|
| 2535 |
|
| 2536 |
-
# 10. Programming concepts using Java (Diploma in programming)
|
| 2537 |
|
| 2538 |
**Quiz 1: July 19 2026** **Quiz 2: August 16 2026** **End term: September 13 2026**
|
| 2539 |
|
|
@@ -2744,7 +2728,7 @@ BPT 4 - August 14, 2026
|
|
| 2744 |
|
| 2745 |
Will be conducted in the course VM - Each BPT has 4 Questions
|
| 2746 |
|
| 2747 |
-
## Repeat_OPPE Criteria
|
| 2748 |
**(w.e.f from Jan 2026 term) [updated on Jan 05, 2026]**
|
| 2749 |
**Students who were OPPE eligible and received an I_OP in the Jan 2026 term, and have registered as Repeat OPPE for the May 2026 term, will continue to be considered eligible for OPPE in May 2026. However, students who were marked OPPE Ineligible with I_OP or I_Both and have registered for the respective course as Repeat OPPE or Repeat OPPE & End Term must regain OPPE eligibility in the May 2026 term by submitting the required assignments and OPPE SCT. If OPPE eligibility is not attained, the OPPE will not be scheduled and a U grade will be awarded for the May 2026 term.**
|
| 2750 |
|
|
@@ -2752,13 +2736,13 @@ Will be conducted in the course VM - Each BPT has 4 Questions
|
|
| 2752 |
**SCT for OPPE and exam day rules:**
|
| 2753 |
https://docs.google.com/document/d/e/2PACX-1vS4Hhh4MsKD2WL8_D26Vw2WJKw0CBtPihZyKrnEM_kefRXm_O75GqTcJA6IR0X_xCiVL5gUi5y6_bjw/pub
|
| 2754 |
|
| 2755 |
-
## Eligibility to attend the end term exam:
|
| 2756 |
Average of the best 5 out of the first 7 weekly assessments (objective and programming) scores >= 40/100 AND OPPE should be eligible
|
| 2757 |
|
| 2758 |
-
## Eligibility to get the course grade:
|
| 2759 |
Attending the end semester exam AND programming exam (OPPE) score >= 40/100
|
| 2760 |
|
| 2761 |
-
## There will be ONE OPPE based on weeks 1-9.
|
| 2762 |
Students have to mandatorily attend the OPPE on the first date.
|
| 2763 |
|
| 2764 |
* If you fail in this, you get a chance to reappear the next weekend. So you get 2 chances to attempt the exam.
|
|
@@ -2952,7 +2936,7 @@ Overall score for eligible students:
|
|
| 2952 |
|
| 2953 |
## Project Courses:
|
| 2954 |
|
| 2955 |
-
Project courses are now for 2 credits each - BDM, MLP, App Dev 1, App Dev 2,
|
| 2956 |
|
| 2957 |
The Project courses are not part of CCC. The CCC is only for theory courses.
|
| 2958 |
|
|
@@ -2992,7 +2976,7 @@ Submission deadline :
|
|
| 2992 |
</tbody>
|
| 2993 |
</table>
|
| 2994 |
|
| 2995 |
-
##
|
| 2996 |
|
| 2997 |
<table>
|
| 2998 |
<thead>
|
|
@@ -3034,7 +3018,7 @@ Updated automatically every 5 minutes
|
|
| 3034 |
|
| 3035 |
https://docs.google.com/document/d/e/2PACX-1vT5PBOz4OH663W0IJPVGVjG_nfmYZGfFI7W1j-6wTLcex13O_7BZmf6a96Q6liO0W-mLZB5hOGZ-A9dMIXVrfe069kqUkplPMRAiZBBIMJcp9XTCR/pub
|
| 3036 |
|
| 3037 |
-
## MAD I Project
|
| 3038 |
|
| 3039 |
**Project Document:**
|
| 3040 |
https://docs.google.com/document/d/e/2PACX-1vR9pOLPvPzsxqhtrmtLi8GAV2j2JxpisWkIHCjEb8WuYrgm4ZPq8S_Sor-MygixO4hvGcPvO6Ei_W/pub
|
|
@@ -3042,7 +3026,7 @@ https://docs.google.com/document/d/e/2PACX-1vR9pOLPvPzsxqhtrmtLi8GAV2j2JxpisWkIH
|
|
| 3042 |
**Project statement - Trekking Management Application:**
|
| 3043 |
https://docs.google.com/document/d/e/2PACX-1vQvqzWz2tFt96B8VApnHqWqIP3LtPDbnXwApYr8VOffLCm_Zh2JuTa51z7d1CNJbrZKC0oWPredYcV/pub
|
| 3044 |
|
| 3045 |
-
## MAD II Project
|
| 3046 |
|
| 3047 |
**Project document:**
|
| 3048 |
https://docs.google.com/document/d/e/2PACX-1vTKQBIsllAp4VUq_rST3_rbpMSoADyfb3agZ1E-jrGQxMeN2IBNOXu3GS0mchzRWaxWa204q52tbFYA/pub
|
|
@@ -3055,11 +3039,11 @@ https://docs.google.com/document/d/e/2PACX-1vTcayCKq8OPAVeTZHXNwCXxvJfiQRCmxiigX
|
|
| 3055 |
|
| 3056 |
**BDM Project: BDM PROJECT SUBMISSION TIMELINE**
|
| 3057 |
|
| 3058 |
-
## Deep learning and Generative AI Project
|
| 3059 |
|
| 3060 |
https://docs.google.com/document/d/e/2PACX-1vTTorjQe5LH-TP_c0x-iDjdm-zX30uvJ3jv_QKtDvGsVoQydTkCWsfKwTUtCQv5syWsnSxmN4W3KFzE/pub_
|
| 3061 |
|
| 3062 |
-
## Rules regarding project fees:
|
| 3063 |
|
| 3064 |
The fee paid for each of the 4 projects is valid for 2 terms. Please read this carefully so that you register properly.
|
| 3065 |
|
|
@@ -3118,7 +3102,7 @@ who will be entering Degree level in the Sep 2026 term. **Without completing the
|
|
| 3118 |
|
| 3119 |
**Students are requested to complete the projects at least two terms before their diploma completion term i.e DO NOT REGISTER OR HAVE PROJECTS ALONE LEFT IN YOUR LAST TERM OF DIPLOMA.**
|
| 3120 |
|
| 3121 |
-
## Very important:
|
| 3122 |
|
| 3123 |
1. <u>Viva policy for MLP, DL&GEN AI</u>, <u>Viva Policy for Appdev</u>
|
| 3124 |
|
|
@@ -3187,7 +3171,7 @@ It is important you learn more on programming and data science outside of what t
|
|
| 3187 |
|
| 3188 |
4. Please do not share your assignments with others before the deadlines. If similarities are found between submissions, all will be penalised irrespective of who did it first and who shared it with whom.
|
| 3189 |
|
| 3190 |
-
#
|
| 3191 |
|
| 3192 |
**Quiz 1**: July 19 2026 **Quiz 2**: August 16 2026 **End term**: September 13 2026
|
| 3193 |
|
|
@@ -3204,7 +3188,7 @@ Qz2 = score in Quiz II (0, if not attempted)
|
|
| 3204 |
F - score in End Term exam
|
| 3205 |
**T = 0.1GAA + 0.4F + 0.25Qz1 + 0.25Qz2**
|
| 3206 |
|
| 3207 |
-
#
|
| 3208 |
|
| 3209 |
**Quiz 1**: No Quiz **Quiz 2**: August 16 2026 **End term**: September 13 2026
|
| 3210 |
|
|
@@ -3269,7 +3253,7 @@ BS-DS_ May 2026 Grading document (Student)
|
|
| 3269 |
|
| 3270 |
Updated automatically every 5 minutes
|
| 3271 |
|
| 3272 |
-
#
|
| 3273 |
|
| 3274 |
**Quiz 1: July 19 2026** **Quiz 2: August 16 2026** **End term: September 13 2026**
|
| 3275 |
|
|
@@ -3296,7 +3280,7 @@ F - score in End Term exam
|
|
| 3296 |
|
| 3297 |
**T = 0.05GAA + 0.25Qz1 + 0.25Qz2 + 0.45F**
|
| 3298 |
|
| 3299 |
-
#
|
| 3300 |
|
| 3301 |
**Academic policies**
|
| 3302 |
|
|
@@ -3339,7 +3323,7 @@ Programming Assignment 1 will be released between Quiz 1 and Quiz 2.
|
|
| 3339 |
|
| 3340 |
The dates will be announced in the forum. This assignment will be evaluated offline.
|
| 3341 |
|
| 3342 |
-
#
|
| 3343 |
|
| 3344 |
**Academic policies**
|
| 3345 |
|
|
@@ -3407,7 +3391,7 @@ Project:
|
|
| 3407 |
More details about the Group Project will be given in the course.
|
| 3408 |
Bonus marks for additional activities may be awarded at the discretion of faculty or instructor, provided the student passes the course.
|
| 3409 |
|
| 3410 |
-
#
|
| 3411 |
|
| 3412 |
**Quiz 1: July 19 2026** **Quiz 2: No Quiz 2** **End term: September 13 2026**
|
| 3413 |
**Above to be attended in person at designated centres**
|
|
@@ -3516,6 +3500,9 @@ Google Docs icon Published using Google Docs
|
|
| 3516 |
|
| 3517 |
Updated automatically every 5 minutes
|
| 3518 |
|
|
|
|
|
|
|
|
|
|
| 3519 |
<span style="color: red">Above to be attended in person at designated centres.</span>
|
| 3520 |
|
| 3521 |
Quiz1 - based on content taught by Prof Mitesh (Based on weeks 1-4)
|
|
@@ -3721,7 +3708,7 @@ Qz2 = score in Quiz II (0, if not attempted), Syllabus: Week 5-8
|
|
| 3721 |
F = score in final exam, Syllabus: Week 1-12
|
| 3722 |
**T = 0.075 GAA + 0.025 GRPA + 0.25Qz1 + 0.25Qz2 + 0.4F**
|
| 3723 |
|
| 3724 |
-
#
|
| 3725 |
|
| 3726 |
**Quiz 1:** July 19 2026 **Quiz 2:** August 16 2026 **End term:** September 13 2026
|
| 3727 |
|
|
@@ -3752,7 +3739,7 @@ Overall score for eligible students:
|
|
| 3752 |
**Case release date:** YTD
|
| 3753 |
**Case presentation:** Depending on numbers, if needed can extend one more day for presentation.
|
| 3754 |
|
| 3755 |
-
#
|
| 3756 |
|
| 3757 |
**Quiz 1:** July 19 2026 **Quiz 2:** August 16 2026 **End term:** September 13 2026
|
| 3758 |
|
|
@@ -3773,7 +3760,7 @@ F = score in final exam
|
|
| 3773 |
|
| 3774 |
**T = 0.1GAA + 0.4F + 0.25Qz1 + 0.25Qz2**
|
| 3775 |
|
| 3776 |
-
#
|
| 3777 |
|
| 3778 |
**Quiz 1:** NA **Quiz 2:** NA **End term:** September 13 2026
|
| 3779 |
|
|
@@ -3981,7 +3968,7 @@ Qz2 = score in Quiz II (0, if not attempted)
|
|
| 3981 |
F - score in End Term exam
|
| 3982 |
T = 0.2 GAA + 0.3F + 0.25Qz1 + 0.25Qz2
|
| 3983 |
|
| 3984 |
-
#
|
| 3985 |
|
| 3986 |
**Quiz 1**: July 19 2026 **Quiz 2**: August 16 2026 **End term**: September 13 2026
|
| 3987 |
|
|
@@ -3999,7 +3986,7 @@ F = score in final exam
|
|
| 3999 |
|
| 4000 |
T = 0.1GAA + 0.4F + 0.2Qz1 + 0.25Qz2 + 0.05 circuit verse assignment
|
| 4001 |
|
| 4002 |
-
#
|
| 4003 |
|
| 4004 |
**Quiz 1**: July 19 2026 **Quiz 2**: August 16 2026 **End term**: September 13 2026
|
| 4005 |
|
|
@@ -4021,7 +4008,7 @@ F = score in final exam
|
|
| 4021 |
|
| 4022 |
T = 0.1GAA + 0.4F + 0.25Qz1 + 0.25Qz2
|
| 4023 |
|
| 4024 |
-
#
|
| 4025 |
|
| 4026 |
**Quiz 1**: NA **Quiz 2**: August 16 2026 **End term**: September 13 2026
|
| 4027 |
|
|
|
|
| 1019 |
|
| 1020 |
(W11/W12 contents will be included for the final exam. Hence, please practice and submit W11/W12 assignment).
|
| 1021 |
|
| 1022 |
+
## 6. English 2
|
| 1023 |
|
| 1024 |
**Quiz 1:** July 19 2026 **Quiz 2:** August 16 2026 **End term:** September 13 2026
|
| 1025 |
|
|
|
|
| 1041 |
|
| 1042 |
(W11/W12 contents will be included for the final exam. Hence, please practice and submit W11/W12 assignment).
|
| 1043 |
|
| 1044 |
+
## 7. Intro to python programming
|
| 1045 |
|
| 1046 |
+
### Academic policies
|
| 1047 |
|
| 1048 |
1. In each programming assignment, be it any course or any OPPE, taking help from LLMs (e.g. ChatGPT, Gemini) partially or completely is considered plagiarism.
|
| 1049 |
|
|
|
|
| 1291 |
|
| 1292 |
Suggested pathway to register and study
|
| 1293 |
|
| 1294 |
+
# Diploma level courses:
|
| 1295 |
|
| 1296 |
1. **Most aggressive pathway** - completing in 4 terms - ONLY IF YOU ARE DOING THIS AS FULL TIME AND NOTHING ELSE AND CAN SPEND <u>70 HRs PER WEEK</u> MINIMUM
|
| 1297 |
|
|
|
|
| 1872 |
|
| 1873 |
https://docs.google.com/document/d/e/2PACX-1vT5PBOz4OH663W0IJPVGVjG_nfmYZGfFI7W1j-6wTLcex13O_7BZmf6a96Q6liO0W-mLZB5hOGZ… 13/40
|
| 1874 |
|
|
|
|
|
|
|
|
|
|
| 1875 |
|
| 1876 |
to attend the exam and if you are eligible, exam will be scheduled as per the slots allocated. Please choose courses for the May 2026 term keeping all these points in mind.
|
| 1877 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1878 |
## 1. Machine Learning foundations (DS Diploma)
|
| 1879 |
|
| 1880 |
**Quiz 1: July 19 2026** **Quiz 2: August 16 2026** **End term: September 13 2026**
|
|
|
|
| 1940 |
|
| 1941 |
https://docs.google.com/document/d/e/2PACX-1vT5PBOz4OH663W0IJPVGVjG_nfmYZGfFI7W1j-6wTLcex13O_7BZmf6a96Q6liO0W-mLZB5hOGZ… 14/40
|
| 1942 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1943 |
|
| 1944 |
OPPE1 will not be scheduled for students who fail to complete the OPPE SCT exam.
|
| 1945 |
<u>Repeat_OPPE Criteria</u> (w.e.f from Jan 2026 term) [updated on Jan 05, 2026]
|
|
|
|
| 2078 |
# BS-DS_ May 2026 Grading document (Student)
|
| 2079 |
Updated automatically every 5 minutes
|
| 2080 |
|
| 2081 |
+
## 4. Business Data Management (DS Diploma)
|
| 2082 |
+
|
| 2083 |
+
**Quiz 1: July 19 2026** **Quiz 2: August 16 2026** **End term: September 13 2026**
|
| 2084 |
|
| 2085 |
**Above to be attended in person at designated centres.**
|
| 2086 |
|
|
|
|
| 2101 |
|
| 2102 |
(Though the W11/W12 assignment score is not included in GAA, W11/W12 contents will be included for the final exam. Hence, please practice and submit W11/W12 assignment).
|
| 2103 |
|
| 2104 |
+
## 5. Business Analytics (Diploma in DS)
|
| 2105 |
|
| 2106 |
**Quiz 1: July 19 2026** **Quiz 2: August 16 2026** **End term: September 13 2026**
|
| 2107 |
|
|
|
|
| 2166 |
</tbody>
|
| 2167 |
</table>
|
| 2168 |
|
| 2169 |
+
## 6. Tools in Data Science (Diploma in DS)
|
| 2170 |
|
| 2171 |
https://docs.google.com/document/d/e/2PACX-1vT5PBOz4OH663W0IJPVGVjG_nfmYZGfFI7W1j-6wTLcex13O_7BZmf6a96Q6liO0W-mLZB5hOGZ... 16/40
|
| 2172 |
|
|
|
|
| 2179 |
# BS-DS_ May 2026 Grading document (Student)
|
| 2180 |
Updated automatically every 5 minutes
|
| 2181 |
|
| 2182 |
+
### Academic policies
|
| 2183 |
|
| 2184 |
1. The Tools in Data Science portal has all the course content: Graded Assignments, Projects and ROE links. The Seek Portal is not used for submissions.
|
| 2185 |
|
|
|
|
| 2232 |
|
| 2233 |
<mark>**Final course score T = 0.2 GAA + 0.2 ROE + 0.2 P1 + 0.2 P2 + 0.2 F**</mark>
|
| 2234 |
|
| 2235 |
+
## 7. Programming Data structures and algorithms using Python (PDSA) - Diploma in Programming
|
| 2236 |
|
| 2237 |
**Weekly assignments:** Mix of autograded assignment and Programming assignments
|
| 2238 |
|
|
|
|
| 2285 |
F = score in final exam
|
| 2286 |
**T = 0.05GAA + 0.2OP + 0.45F + max (0.2max(Qz1, Qz2), (0.10Qz1+0.20Qz2 ))**
|
| 2287 |
|
| 2288 |
+
## 8. Database management system (DBMS) -
|
| 2289 |
|
| 2290 |
+
### Diploma in Programming
|
| 2291 |
|
| 2292 |
**Quiz 1: July 19 2026** **Quiz 2: August 16 2026** **End term: September 13 2026**
|
| 2293 |
|
|
|
|
| 2463 |
</tbody>
|
| 2464 |
</table>
|
| 2465 |
|
| 2466 |
+
## 9. Application development - 1 (Diploma in programming)
|
| 2467 |
|
| 2468 |
**Quiz 1**: July 19 2026
|
| 2469 |
**Quiz 2**: August 16 2026
|
|
|
|
| 2517 |
|
| 2518 |
**Final course score T = 0.05 GLA + max (0.6F + 0.25max(Qz1, Qz2), 0.4F + 0.25Qz1 + 0.3Qz2)**
|
| 2519 |
|
| 2520 |
+
## 10. Programming concepts using Java (Diploma in programming)
|
| 2521 |
|
| 2522 |
**Quiz 1: July 19 2026** **Quiz 2: August 16 2026** **End term: September 13 2026**
|
| 2523 |
|
|
|
|
| 2728 |
|
| 2729 |
Will be conducted in the course VM - Each BPT has 4 Questions
|
| 2730 |
|
| 2731 |
+
### Repeat_OPPE Criteria
|
| 2732 |
**(w.e.f from Jan 2026 term) [updated on Jan 05, 2026]**
|
| 2733 |
**Students who were OPPE eligible and received an I_OP in the Jan 2026 term, and have registered as Repeat OPPE for the May 2026 term, will continue to be considered eligible for OPPE in May 2026. However, students who were marked OPPE Ineligible with I_OP or I_Both and have registered for the respective course as Repeat OPPE or Repeat OPPE & End Term must regain OPPE eligibility in the May 2026 term by submitting the required assignments and OPPE SCT. If OPPE eligibility is not attained, the OPPE will not be scheduled and a U grade will be awarded for the May 2026 term.**
|
| 2734 |
|
|
|
|
| 2736 |
**SCT for OPPE and exam day rules:**
|
| 2737 |
https://docs.google.com/document/d/e/2PACX-1vS4Hhh4MsKD2WL8_D26Vw2WJKw0CBtPihZyKrnEM_kefRXm_O75GqTcJA6IR0X_xCiVL5gUi5y6_bjw/pub
|
| 2738 |
|
| 2739 |
+
### Eligibility to attend the end term exam:
|
| 2740 |
Average of the best 5 out of the first 7 weekly assessments (objective and programming) scores >= 40/100 AND OPPE should be eligible
|
| 2741 |
|
| 2742 |
+
### Eligibility to get the course grade:
|
| 2743 |
Attending the end semester exam AND programming exam (OPPE) score >= 40/100
|
| 2744 |
|
| 2745 |
+
### There will be ONE OPPE based on weeks 1-9.
|
| 2746 |
Students have to mandatorily attend the OPPE on the first date.
|
| 2747 |
|
| 2748 |
* If you fail in this, you get a chance to reappear the next weekend. So you get 2 chances to attempt the exam.
|
|
|
|
| 2936 |
|
| 2937 |
## Project Courses:
|
| 2938 |
|
| 2939 |
+
Project courses are now for 2 credits each - BDM, MLP, App Dev 1, App Dev 2, DL-GenAI project.
|
| 2940 |
|
| 2941 |
The Project courses are not part of CCC. The CCC is only for theory courses.
|
| 2942 |
|
|
|
|
| 2976 |
</tbody>
|
| 2977 |
</table>
|
| 2978 |
|
| 2979 |
+
### MAD1, MAD2 PROJECTS:
|
| 2980 |
|
| 2981 |
<table>
|
| 2982 |
<thead>
|
|
|
|
| 3018 |
|
| 3019 |
https://docs.google.com/document/d/e/2PACX-1vT5PBOz4OH663W0IJPVGVjG_nfmYZGfFI7W1j-6wTLcex13O_7BZmf6a96Q6liO0W-mLZB5hOGZ-A9dMIXVrfe069kqUkplPMRAiZBBIMJcp9XTCR/pub
|
| 3020 |
|
| 3021 |
+
### MAD I Project
|
| 3022 |
|
| 3023 |
**Project Document:**
|
| 3024 |
https://docs.google.com/document/d/e/2PACX-1vR9pOLPvPzsxqhtrmtLi8GAV2j2JxpisWkIHCjEb8WuYrgm4ZPq8S_Sor-MygixO4hvGcPvO6Ei_W/pub
|
|
|
|
| 3026 |
**Project statement - Trekking Management Application:**
|
| 3027 |
https://docs.google.com/document/d/e/2PACX-1vQvqzWz2tFt96B8VApnHqWqIP3LtPDbnXwApYr8VOffLCm_Zh2JuTa51z7d1CNJbrZKC0oWPredYcV/pub
|
| 3028 |
|
| 3029 |
+
### MAD II Project
|
| 3030 |
|
| 3031 |
**Project document:**
|
| 3032 |
https://docs.google.com/document/d/e/2PACX-1vTKQBIsllAp4VUq_rST3_rbpMSoADyfb3agZ1E-jrGQxMeN2IBNOXu3GS0mchzRWaxWa204q52tbFYA/pub
|
|
|
|
| 3039 |
|
| 3040 |
**BDM Project: BDM PROJECT SUBMISSION TIMELINE**
|
| 3041 |
|
| 3042 |
+
### Deep learning and Generative AI Project
|
| 3043 |
|
| 3044 |
https://docs.google.com/document/d/e/2PACX-1vTTorjQe5LH-TP_c0x-iDjdm-zX30uvJ3jv_QKtDvGsVoQydTkCWsfKwTUtCQv5syWsnSxmN4W3KFzE/pub_
|
| 3045 |
|
| 3046 |
+
### Rules regarding project fees:
|
| 3047 |
|
| 3048 |
The fee paid for each of the 4 projects is valid for 2 terms. Please read this carefully so that you register properly.
|
| 3049 |
|
|
|
|
| 3102 |
|
| 3103 |
**Students are requested to complete the projects at least two terms before their diploma completion term i.e DO NOT REGISTER OR HAVE PROJECTS ALONE LEFT IN YOUR LAST TERM OF DIPLOMA.**
|
| 3104 |
|
| 3105 |
+
### Very important:
|
| 3106 |
|
| 3107 |
1. <u>Viva policy for MLP, DL&GEN AI</u>, <u>Viva Policy for Appdev</u>
|
| 3108 |
|
|
|
|
| 3171 |
|
| 3172 |
4. Please do not share your assignments with others before the deadlines. If similarities are found between submissions, all will be penalised irrespective of who did it first and who shared it with whom.
|
| 3173 |
|
| 3174 |
+
# 1. Software Testing
|
| 3175 |
|
| 3176 |
**Quiz 1**: July 19 2026 **Quiz 2**: August 16 2026 **End term**: September 13 2026
|
| 3177 |
|
|
|
|
| 3188 |
F - score in End Term exam
|
| 3189 |
**T = 0.1GAA + 0.4F + 0.25Qz1 + 0.25Qz2**
|
| 3190 |
|
| 3191 |
+
# 2. Software Engineering
|
| 3192 |
|
| 3193 |
**Quiz 1**: No Quiz **Quiz 2**: August 16 2026 **End term**: September 13 2026
|
| 3194 |
|
|
|
|
| 3253 |
|
| 3254 |
Updated automatically every 5 minutes
|
| 3255 |
|
| 3256 |
+
# 3. Deep Learning
|
| 3257 |
|
| 3258 |
**Quiz 1: July 19 2026** **Quiz 2: August 16 2026** **End term: September 13 2026**
|
| 3259 |
|
|
|
|
| 3280 |
|
| 3281 |
**T = 0.05GAA + 0.25Qz1 + 0.25Qz2 + 0.45F**
|
| 3282 |
|
| 3283 |
+
# 4. AI: Search Methods for Problem Solving
|
| 3284 |
|
| 3285 |
**Academic policies**
|
| 3286 |
|
|
|
|
| 3323 |
|
| 3324 |
The dates will be announced in the forum. This assignment will be evaluated offline.
|
| 3325 |
|
| 3326 |
+
# 5. Strategies for Professional Growth
|
| 3327 |
|
| 3328 |
**Academic policies**
|
| 3329 |
|
|
|
|
| 3391 |
More details about the Group Project will be given in the course.
|
| 3392 |
Bonus marks for additional activities may be awarded at the discretion of faculty or instructor, provided the student passes the course.
|
| 3393 |
|
| 3394 |
+
# 6. Programming in C
|
| 3395 |
|
| 3396 |
**Quiz 1: July 19 2026** **Quiz 2: No Quiz 2** **End term: September 13 2026**
|
| 3397 |
**Above to be attended in person at designated centres**
|
|
|
|
| 3500 |
|
| 3501 |
Updated automatically every 5 minutes
|
| 3502 |
|
| 3503 |
+
# 9. Deep Learning Practice
|
| 3504 |
+
|
| 3505 |
+
**Quiz 1: July 19 2026** **Quiz 2: August 16 2026** **End term: September 13 2026**
|
| 3506 |
<span style="color: red">Above to be attended in person at designated centres.</span>
|
| 3507 |
|
| 3508 |
Quiz1 - based on content taught by Prof Mitesh (Based on weeks 1-4)
|
|
|
|
| 3708 |
F = score in final exam, Syllabus: Week 1-12
|
| 3709 |
**T = 0.075 GAA + 0.025 GRPA + 0.25Qz1 + 0.25Qz2 + 0.4F**
|
| 3710 |
|
| 3711 |
+
# 13.Market Research
|
| 3712 |
|
| 3713 |
**Quiz 1:** July 19 2026 **Quiz 2:** August 16 2026 **End term:** September 13 2026
|
| 3714 |
|
|
|
|
| 3739 |
**Case release date:** YTD
|
| 3740 |
**Case presentation:** Depending on numbers, if needed can extend one more day for presentation.
|
| 3741 |
|
| 3742 |
+
# 14.Managerial Economics
|
| 3743 |
|
| 3744 |
**Quiz 1:** July 19 2026 **Quiz 2:** August 16 2026 **End term:** September 13 2026
|
| 3745 |
|
|
|
|
| 3760 |
|
| 3761 |
**T = 0.1GAA + 0.4F + 0.25Qz1 + 0.25Qz2**
|
| 3762 |
|
| 3763 |
+
# 15. MLOps (Machine Learning Operations)
|
| 3764 |
|
| 3765 |
**Quiz 1:** NA **Quiz 2:** NA **End term:** September 13 2026
|
| 3766 |
|
|
|
|
| 3968 |
F - score in End Term exam
|
| 3969 |
T = 0.2 GAA + 0.3F + 0.25Qz1 + 0.25Qz2
|
| 3970 |
|
| 3971 |
+
# 20. Computer Systems Design
|
| 3972 |
|
| 3973 |
**Quiz 1**: July 19 2026 **Quiz 2**: August 16 2026 **End term**: September 13 2026
|
| 3974 |
|
|
|
|
| 3986 |
|
| 3987 |
T = 0.1GAA + 0.4F + 0.2Qz1 + 0.25Qz2 + 0.05 circuit verse assignment
|
| 3988 |
|
| 3989 |
+
# 21. Game Theory and Strategy
|
| 3990 |
|
| 3991 |
**Quiz 1**: July 19 2026 **Quiz 2**: August 16 2026 **End term**: September 13 2026
|
| 3992 |
|
|
|
|
| 4008 |
|
| 4009 |
T = 0.1GAA + 0.4F + 0.25Qz1 + 0.25Qz2
|
| 4010 |
|
| 4011 |
+
# 22. Algorithms for Data Science (ADS)
|
| 4012 |
|
| 4013 |
**Quiz 1**: NA **Quiz 2**: August 16 2026 **End term**: September 13 2026
|
| 4014 |
|
eval/eval_prompts.json
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": "fee_001",
|
| 4 |
+
"question": "What is the foundation level fee for students joining from Jan 2026 onwards?",
|
| 5 |
+
"expected_keywords": ["48,000", "48000"],
|
| 6 |
+
"source": "student-handbook.md",
|
| 7 |
+
"category": "fees"
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
"id": "fee_002",
|
| 11 |
+
"question": "What is the total fee for Foundation + Two Diplomas under the revised fee structure?",
|
| 12 |
+
"expected_keywords": ["210,000", "210000"],
|
| 13 |
+
"source": "student-handbook.md",
|
| 14 |
+
"category": "fees"
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"id": "fee_003",
|
| 18 |
+
"question": "What is the total fee for the BS Degree under the revised fee structure?",
|
| 19 |
+
"expected_keywords": ["386,000", "450,000"],
|
| 20 |
+
"source": "student-handbook.md",
|
| 21 |
+
"category": "fees"
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"id": "fee_004",
|
| 25 |
+
"question": "What is the foundation level fee for students who joined the program before Sep 2025?",
|
| 26 |
+
"expected_keywords": ["32,000", "32000"],
|
| 27 |
+
"source": "student-handbook.md",
|
| 28 |
+
"category": "fees"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"id": "fee_005",
|
| 32 |
+
"question": "What fee waiver does an SC/ST student with family income above 5 LPA get?",
|
| 33 |
+
"expected_keywords": ["50%"],
|
| 34 |
+
"source": "student-handbook.md",
|
| 35 |
+
"category": "fees"
|
| 36 |
+
}
|
| 37 |
+
]
|
eval/evaluate.py
CHANGED
|
@@ -0,0 +1,423 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
evaluate.py — Offline evaluation using DeepEval
|
| 3 |
+
for the IITM BS Degree RAG System
|
| 4 |
+
|
| 5 |
+
Judge LLM : Groq (llama-3.1-8b-instant) via DeepEvalBaseLLM wrapper
|
| 6 |
+
Metrics :
|
| 7 |
+
- FaithfulnessMetric — are answer claims grounded in retrieved chunks?
|
| 8 |
+
- AnswerRelevancyMetric — is the answer relevant to the question?
|
| 9 |
+
- ContextualPrecisionMetric — are the top-ranked chunks the most useful ones?
|
| 10 |
+
|
| 11 |
+
Plus a fast keyword-hit check (no LLM needed) as a CI gate.
|
| 12 |
+
|
| 13 |
+
Usage:
|
| 14 |
+
python eval/evaluate.py # full DeepEval run
|
| 15 |
+
python eval/evaluate.py --category fees # one category only
|
| 16 |
+
python eval/evaluate.py --threshold 0.75 # custom pass threshold
|
| 17 |
+
python eval/evaluate.py --no-deepeval # keyword-only, fast CI mode
|
| 18 |
+
python eval/evaluate.py --save-report # write results to eval/report.json
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import os
|
| 22 |
+
import sys
|
| 23 |
+
import json
|
| 24 |
+
import argparse
|
| 25 |
+
import time
|
| 26 |
+
import re
|
| 27 |
+
from datetime import datetime
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
|
| 30 |
+
from dotenv import load_dotenv
|
| 31 |
+
from groq import Groq
|
| 32 |
+
from groq import RateLimitError
|
| 33 |
+
from pydantic import BaseModel
|
| 34 |
+
|
| 35 |
+
from deepeval.models import DeepEvalBaseLLM
|
| 36 |
+
from deepeval.metrics import (
|
| 37 |
+
AnswerRelevancyMetric,
|
| 38 |
+
FaithfulnessMetric,
|
| 39 |
+
ContextualPrecisionMetric,
|
| 40 |
+
)
|
| 41 |
+
from deepeval.test_case import LLMTestCase
|
| 42 |
+
|
| 43 |
+
# ── Path setup ─────────────────────────────────────────────────────────────────
|
| 44 |
+
ROOT = Path(__file__).parent.parent
|
| 45 |
+
sys.path.insert(0, str(ROOT / "src"))
|
| 46 |
+
from retrieve import Retriever # noqa: E402
|
| 47 |
+
|
| 48 |
+
load_dotenv()
|
| 49 |
+
|
| 50 |
+
API_KEY = os.getenv("GROQ_API_KEY")
|
| 51 |
+
if not API_KEY:
|
| 52 |
+
print("ERROR: GROQ_API_KEY not found in environment.")
|
| 53 |
+
sys.exit(1)
|
| 54 |
+
|
| 55 |
+
EVAL_DATA_PATH = Path(__file__).parent / "eval_prompts.json"
|
| 56 |
+
REPORT_PATH = Path(__file__).parent / "report.json"
|
| 57 |
+
DEFAULT_THRESHOLD = 0.5
|
| 58 |
+
JUDGE_MODEL = "llama-3.1-8b-instant"
|
| 59 |
+
GEN_MODEL = "llama-3.1-8b-instant"
|
| 60 |
+
|
| 61 |
+
# Retry / throttle settings
|
| 62 |
+
MAX_RETRIES = 6
|
| 63 |
+
BACKOFF_BASE = 2 # seconds — used only if retry delay isn't parseable
|
| 64 |
+
BETWEEN_CALLS = 3 # polite gap after every successful Groq call
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# ── Retry helper ───────────────────────────────────────────────────────────────
|
| 68 |
+
|
| 69 |
+
def _parse_retry_delay(error: RateLimitError) -> float | None:
|
| 70 |
+
"""
|
| 71 |
+
Extract the suggested wait time from a Groq 429 error message.
|
| 72 |
+
Groq says things like:
|
| 73 |
+
'Please try again in 760ms'
|
| 74 |
+
'Please try again in 1.2s'
|
| 75 |
+
We parse that and add a small buffer.
|
| 76 |
+
"""
|
| 77 |
+
msg = str(error)
|
| 78 |
+
|
| 79 |
+
# milliseconds: "760ms"
|
| 80 |
+
ms_match = re.search(r'try again in (\d+(?:\.\d+)?)ms', msg, re.I)
|
| 81 |
+
if ms_match:
|
| 82 |
+
return float(ms_match.group(1)) / 1000.0 + 0.5
|
| 83 |
+
|
| 84 |
+
# seconds: "1.2s"
|
| 85 |
+
s_match = re.search(r'try again in (\d+(?:\.\d+)?)s', msg, re.I)
|
| 86 |
+
if s_match:
|
| 87 |
+
return float(s_match.group(1)) + 0.5
|
| 88 |
+
|
| 89 |
+
return None
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def groq_call_with_retry(fn, *args, **kwargs):
|
| 93 |
+
"""
|
| 94 |
+
Call any Groq SDK function with automatic retry on 429 rate limits.
|
| 95 |
+
Reads the suggested delay from the error response when available,
|
| 96 |
+
otherwise falls back to exponential backoff.
|
| 97 |
+
|
| 98 |
+
Usage:
|
| 99 |
+
response = groq_call_with_retry(
|
| 100 |
+
client.chat.completions.create,
|
| 101 |
+
model=..., messages=..., ...
|
| 102 |
+
)
|
| 103 |
+
"""
|
| 104 |
+
for attempt in range(1, MAX_RETRIES + 1):
|
| 105 |
+
try:
|
| 106 |
+
result = fn(*args, **kwargs)
|
| 107 |
+
time.sleep(BETWEEN_CALLS)
|
| 108 |
+
return result
|
| 109 |
+
except RateLimitError as e:
|
| 110 |
+
if attempt == MAX_RETRIES:
|
| 111 |
+
raise
|
| 112 |
+
|
| 113 |
+
suggested = _parse_retry_delay(e)
|
| 114 |
+
wait = suggested if suggested else (BACKOFF_BASE ** attempt)
|
| 115 |
+
print(f"\n ⏳ 429 rate-limited — waiting {wait:.2f}s (attempt {attempt}/{MAX_RETRIES})...")
|
| 116 |
+
time.sleep(wait)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
# ── Groq wrapper for DeepEval ──────────────────────────────────────────────────
|
| 120 |
+
|
| 121 |
+
class GroqJudge(DeepEvalBaseLLM):
|
| 122 |
+
"""
|
| 123 |
+
Wraps the Groq SDK so DeepEval can use it as its judge LLM.
|
| 124 |
+
|
| 125 |
+
DeepEval calls generate() with either:
|
| 126 |
+
- just a prompt string → return a plain string
|
| 127 |
+
- a prompt + Pydantic schema → return a parsed schema instance
|
| 128 |
+
|
| 129 |
+
The schema path is used for metric scoring (DeepEval asks the judge
|
| 130 |
+
to fill in a structured verdict JSON). We force json_object mode so
|
| 131 |
+
Groq always returns valid JSON for those calls.
|
| 132 |
+
"""
|
| 133 |
+
def __init__(self, api_key: str, model_name: str = JUDGE_MODEL):
|
| 134 |
+
self.api_key = api_key
|
| 135 |
+
self.model_name = model_name
|
| 136 |
+
self._client = Groq(api_key=api_key)
|
| 137 |
+
|
| 138 |
+
def load_model(self):
|
| 139 |
+
return self._client
|
| 140 |
+
|
| 141 |
+
def generate(self, prompt: str, schema: BaseModel = None):
|
| 142 |
+
client = self.load_model()
|
| 143 |
+
kwargs = dict(
|
| 144 |
+
model=self.model_name,
|
| 145 |
+
messages=[{"role": "user", "content": prompt}],
|
| 146 |
+
temperature=0.0,
|
| 147 |
+
max_tokens=1024,
|
| 148 |
+
)
|
| 149 |
+
if schema is not None:
|
| 150 |
+
kwargs["response_format"] = {"type": "json_object"}
|
| 151 |
+
|
| 152 |
+
response = groq_call_with_retry(client.chat.completions.create, **kwargs)
|
| 153 |
+
raw = response.choices[0].message.content.strip()
|
| 154 |
+
|
| 155 |
+
if schema is not None:
|
| 156 |
+
for candidate in [raw] + raw.split("```"):
|
| 157 |
+
candidate = candidate.lstrip("json").strip()
|
| 158 |
+
try:
|
| 159 |
+
return schema(**json.loads(candidate))
|
| 160 |
+
except Exception:
|
| 161 |
+
continue
|
| 162 |
+
raise ValueError(f"GroqJudge: could not parse schema: {raw[:200]}")
|
| 163 |
+
|
| 164 |
+
return raw
|
| 165 |
+
|
| 166 |
+
async def a_generate(self, prompt: str, schema: BaseModel = None):
|
| 167 |
+
return self.generate(prompt, schema)
|
| 168 |
+
|
| 169 |
+
def get_model_name(self) -> str:
|
| 170 |
+
return f"Groq/{self.model_name}"
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
# ── Answer generator ───────────────────────────────────────────────────────────
|
| 174 |
+
|
| 175 |
+
def generate_answer(question: str, chunks: list[dict], client: Groq) -> str:
|
| 176 |
+
"""Generate a grounded answer from retrieved chunks using Groq."""
|
| 177 |
+
context_parts = [
|
| 178 |
+
f"[{c['source']} — Section: {c['page']}]\n{c['text']}"
|
| 179 |
+
for c in chunks
|
| 180 |
+
]
|
| 181 |
+
prompt = (
|
| 182 |
+
"You are an official academic advisor AI for the IITM BS Degree Programme.\n"
|
| 183 |
+
"Answer the student question ONLY from the provided context. Be concise and factual.\n"
|
| 184 |
+
'If the answer is not in the context, say "I don\'t have that information."\n\n'
|
| 185 |
+
"CONTEXT:\n"
|
| 186 |
+
+ "\n---\n".join(context_parts)
|
| 187 |
+
+ f"\n\nSTUDENT QUESTION: {question}\n"
|
| 188 |
+
)
|
| 189 |
+
response = groq_call_with_retry(
|
| 190 |
+
client.chat.completions.create,
|
| 191 |
+
model=GEN_MODEL,
|
| 192 |
+
messages=[
|
| 193 |
+
{"role": "system", "content": "Answer only from the provided context."},
|
| 194 |
+
{"role": "user", "content": prompt},
|
| 195 |
+
],
|
| 196 |
+
temperature=0.0,
|
| 197 |
+
max_tokens=200,
|
| 198 |
+
)
|
| 199 |
+
return response.choices[0].message.content.strip()
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# ── Keyword hit ────────────────────────────────────────────────────────────────
|
| 203 |
+
|
| 204 |
+
def keyword_hit(answer: str, expected_keywords: list[str]) -> bool:
|
| 205 |
+
"""Check if any expected keyword appears in the answer (case-insensitive)."""
|
| 206 |
+
a = answer.lower()
|
| 207 |
+
return any(kw.lower() in a for kw in expected_keywords)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
# ── Main evaluation ────────────────────────────────────────────────────────────
|
| 211 |
+
|
| 212 |
+
def run_evaluation(
|
| 213 |
+
category: str = None,
|
| 214 |
+
use_deepeval: bool = True,
|
| 215 |
+
threshold: float = DEFAULT_THRESHOLD,
|
| 216 |
+
save_report: bool = False,
|
| 217 |
+
):
|
| 218 |
+
print("\n" + "=" * 65)
|
| 219 |
+
print(" IITM BS RAG — DEEPEVAL EVALUATION")
|
| 220 |
+
print("=" * 65)
|
| 221 |
+
|
| 222 |
+
if not EVAL_DATA_PATH.exists():
|
| 223 |
+
print(f"ERROR: eval_prompts.json not found at {EVAL_DATA_PATH}")
|
| 224 |
+
sys.exit(1)
|
| 225 |
+
|
| 226 |
+
with open(EVAL_DATA_PATH) as f:
|
| 227 |
+
eval_data = json.load(f)
|
| 228 |
+
|
| 229 |
+
if category:
|
| 230 |
+
eval_data = [q for q in eval_data if q.get("category") == category]
|
| 231 |
+
print(f" Category filter : '{category}' → {len(eval_data)} questions")
|
| 232 |
+
else:
|
| 233 |
+
print(f" Total questions : {len(eval_data)}")
|
| 234 |
+
|
| 235 |
+
print(f" Judge model : {JUDGE_MODEL}")
|
| 236 |
+
print(f" DeepEval : {'enabled' if use_deepeval else 'disabled (keyword-only)'}")
|
| 237 |
+
print(f" Threshold : {threshold}\n")
|
| 238 |
+
|
| 239 |
+
print("Initializing retriever...")
|
| 240 |
+
retriever = Retriever()
|
| 241 |
+
groq_client = Groq(api_key=API_KEY)
|
| 242 |
+
|
| 243 |
+
# ── Phase 1: retrieve + generate ──────────────────────────────────────────
|
| 244 |
+
print("\nPhase 1 — Retrieve & Generate\n" + "-" * 40)
|
| 245 |
+
|
| 246 |
+
test_cases : list[LLMTestCase] = []
|
| 247 |
+
kw_hits : list[bool] = []
|
| 248 |
+
item_map : list[dict] = []
|
| 249 |
+
chunk_scores: list[list[float]] = []
|
| 250 |
+
|
| 251 |
+
for i, item in enumerate(eval_data, 1):
|
| 252 |
+
question = item["question"]
|
| 253 |
+
print(f" [{i:02d}/{len(eval_data)}] {question[:70]}")
|
| 254 |
+
|
| 255 |
+
chunks = retriever.retrieve(question, top_n=2)
|
| 256 |
+
answer = generate_answer(question, chunks, groq_client)
|
| 257 |
+
contexts = [c["text"][:1000] for c in chunks]
|
| 258 |
+
scores = [round(c["rerank_score"], 3) for c in chunks]
|
| 259 |
+
|
| 260 |
+
kw = keyword_hit(answer, item["expected_keywords"])
|
| 261 |
+
kw_hits.append(kw)
|
| 262 |
+
chunk_scores.append(scores)
|
| 263 |
+
|
| 264 |
+
print(f" rerank scores : {scores}")
|
| 265 |
+
print(f" keyword : {'✅' if kw else '❌'} {answer[:80]}{'…' if len(answer) > 80 else ''}\n")
|
| 266 |
+
|
| 267 |
+
test_cases.append(LLMTestCase(
|
| 268 |
+
input=question,
|
| 269 |
+
actual_output=answer,
|
| 270 |
+
retrieval_context=contexts,
|
| 271 |
+
expected_output=" | ".join(item["expected_keywords"]),
|
| 272 |
+
))
|
| 273 |
+
item_map.append(item)
|
| 274 |
+
|
| 275 |
+
kw_rate = sum(kw_hits) / len(kw_hits)
|
| 276 |
+
|
| 277 |
+
# ── Phase 2: DeepEval scoring ──────────────────────────────────────────────
|
| 278 |
+
results_by_metric : dict[str, list[float]] = {}
|
| 279 |
+
per_question_scores : list[dict] = []
|
| 280 |
+
|
| 281 |
+
if use_deepeval:
|
| 282 |
+
print("\nPhase 2 — DeepEval Metrics\n" + "-" * 40)
|
| 283 |
+
print(f" Judge model : {JUDGE_MODEL}")
|
| 284 |
+
print(f" Gap between calls : {BETWEEN_CALLS}s | Max retries on 429 : {MAX_RETRIES}\n")
|
| 285 |
+
|
| 286 |
+
judge = GroqJudge(api_key=API_KEY)
|
| 287 |
+
|
| 288 |
+
metrics = [
|
| 289 |
+
FaithfulnessMetric(
|
| 290 |
+
threshold=threshold, model=judge,
|
| 291 |
+
include_reason=True, async_mode=False,
|
| 292 |
+
),
|
| 293 |
+
AnswerRelevancyMetric(
|
| 294 |
+
threshold=threshold, model=judge,
|
| 295 |
+
include_reason=True, async_mode=False,
|
| 296 |
+
),
|
| 297 |
+
ContextualPrecisionMetric(
|
| 298 |
+
threshold=threshold, model=judge,
|
| 299 |
+
include_reason=True, async_mode=False,
|
| 300 |
+
),
|
| 301 |
+
]
|
| 302 |
+
|
| 303 |
+
for i, tc in enumerate(test_cases, 1):
|
| 304 |
+
print(f" [{i:02d}/{len(test_cases)}] {tc.input[:65]}")
|
| 305 |
+
q_scores = {"question": tc.input, "keyword_hit": kw_hits[i - 1]}
|
| 306 |
+
|
| 307 |
+
for m in metrics:
|
| 308 |
+
mname = type(m).__name__
|
| 309 |
+
try:
|
| 310 |
+
m.measure(tc)
|
| 311 |
+
score = m.score if m.score is not None else 0.0
|
| 312 |
+
reason = (m.reason or "—")[:110]
|
| 313 |
+
icon = "✅" if score >= threshold else "❌"
|
| 314 |
+
print(f" {mname:<32} {icon} {score:.3f} {reason}")
|
| 315 |
+
except RateLimitError as e:
|
| 316 |
+
score = 0.0
|
| 317 |
+
print(f" {mname:<32} ⚠️ rate limit exhausted after {MAX_RETRIES} retries: {e}")
|
| 318 |
+
except Exception as e:
|
| 319 |
+
score = 0.0
|
| 320 |
+
print(f" {mname:<32} ⚠️ error: {e}")
|
| 321 |
+
|
| 322 |
+
results_by_metric.setdefault(mname, []).append(score)
|
| 323 |
+
q_scores[mname] = round(score, 4)
|
| 324 |
+
|
| 325 |
+
per_question_scores.append(q_scores)
|
| 326 |
+
print()
|
| 327 |
+
|
| 328 |
+
# ── Phase 3: Aggregate summary ─────────────────────────────────────────────
|
| 329 |
+
print("=" * 65)
|
| 330 |
+
print(" AGGREGATE RESULTS")
|
| 331 |
+
print("=" * 65)
|
| 332 |
+
print(f" Questions evaluated : {len(eval_data)}")
|
| 333 |
+
print(f" Keyword Hit Rate : {kw_rate:.1%} {'✅' if kw_rate >= threshold else '❌'}")
|
| 334 |
+
|
| 335 |
+
def _avg(lst: list[float]) -> float:
|
| 336 |
+
valid = [s for s in lst if s is not None]
|
| 337 |
+
return sum(valid) / len(valid) if valid else 0.0
|
| 338 |
+
|
| 339 |
+
avg_faith = avg_rel = avg_prec = None
|
| 340 |
+
|
| 341 |
+
if use_deepeval and results_by_metric:
|
| 342 |
+
avg_faith = _avg(results_by_metric.get("FaithfulnessMetric", []))
|
| 343 |
+
avg_rel = _avg(results_by_metric.get("AnswerRelevancyMetric", []))
|
| 344 |
+
avg_prec = _avg(results_by_metric.get("ContextualPrecisionMetric",[]))
|
| 345 |
+
|
| 346 |
+
print(f" Faithfulness (avg) : {avg_faith:.3f} {'✅' if avg_faith >= threshold else '❌'}")
|
| 347 |
+
print(f" Answer Relevancy (avg) : {avg_rel:.3f} {'✅' if avg_rel >= threshold else '❌'}")
|
| 348 |
+
print(f" Contextual Precision (avg) : {avg_prec:.3f} {'✅' if avg_prec >= threshold else '❌'}")
|
| 349 |
+
|
| 350 |
+
print("\n Per-question breakdown:")
|
| 351 |
+
header = f" {'ID':<28} {'kw':>3} {'Faith':>6} {'Rel':>6} {'Prec':>6}"
|
| 352 |
+
print(header)
|
| 353 |
+
print(" " + "-" * (len(header) - 2))
|
| 354 |
+
for item, kw, pq in zip(item_map, kw_hits, per_question_scores):
|
| 355 |
+
f = pq.get("FaithfulnessMetric", 0)
|
| 356 |
+
r = pq.get("AnswerRelevancyMetric", 0)
|
| 357 |
+
p = pq.get("ContextualPrecisionMetric", 0)
|
| 358 |
+
print(
|
| 359 |
+
f" {item['id']:<28} {'✅' if kw else '❌':>3} "
|
| 360 |
+
f" {f:>6.3f} {r:>6.3f} {p:>6.3f}"
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
# ── CI gate ────────────────────────────────────────────────────────────────
|
| 364 |
+
if avg_faith is not None:
|
| 365 |
+
gate_metric = min(kw_rate, avg_faith)
|
| 366 |
+
gate_label = f"min(keyword={kw_rate:.1%}, faithfulness={avg_faith:.3f})"
|
| 367 |
+
else:
|
| 368 |
+
gate_metric = kw_rate
|
| 369 |
+
gate_label = f"keyword hit rate = {kw_rate:.1%}"
|
| 370 |
+
|
| 371 |
+
print(f"\n Gate : {gate_label}")
|
| 372 |
+
print(f" Score : {gate_metric:.3f} (threshold: {threshold:.2f})")
|
| 373 |
+
|
| 374 |
+
# ── Optional JSON report ───────────────────────────────────────────────────
|
| 375 |
+
if save_report:
|
| 376 |
+
report = {
|
| 377 |
+
"timestamp": datetime.now().isoformat(),
|
| 378 |
+
"judge_model": JUDGE_MODEL,
|
| 379 |
+
"threshold": threshold,
|
| 380 |
+
"category": category,
|
| 381 |
+
"num_questions": len(eval_data),
|
| 382 |
+
"keyword_hit_rate": round(kw_rate, 4),
|
| 383 |
+
"averages": {
|
| 384 |
+
"faithfulness": round(avg_faith, 4) if avg_faith is not None else None,
|
| 385 |
+
"answer_relevancy": round(avg_rel, 4) if avg_rel is not None else None,
|
| 386 |
+
"contextual_precision": round(avg_prec, 4) if avg_prec is not None else None,
|
| 387 |
+
},
|
| 388 |
+
"gate_score": round(gate_metric, 4),
|
| 389 |
+
"passed": gate_metric >= threshold,
|
| 390 |
+
"per_question": per_question_scores,
|
| 391 |
+
}
|
| 392 |
+
REPORT_PATH.write_text(json.dumps(report, indent=2))
|
| 393 |
+
print(f"\n 📄 Report saved → {REPORT_PATH}")
|
| 394 |
+
|
| 395 |
+
# ── Exit with CI-friendly code ─────────────────────────────────────────────
|
| 396 |
+
if gate_metric >= threshold:
|
| 397 |
+
print(f"\n ✅ PASSED — RAG quality is above threshold ({threshold:.0%})\n")
|
| 398 |
+
sys.exit(0)
|
| 399 |
+
else:
|
| 400 |
+
print(f"\n ❌ FAILED — Quality dropped below threshold ({threshold:.0%})")
|
| 401 |
+
print(" Check ❌ rows above. Re-run ingest.py if documents changed.\n")
|
| 402 |
+
sys.exit(1)
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
# ── CLI ────────────────────────────────────────────────────────────────────────
|
| 406 |
+
if __name__ == "__main__":
|
| 407 |
+
parser = argparse.ArgumentParser(description="Evaluate IITM BS RAG with DeepEval")
|
| 408 |
+
parser.add_argument("--category", type=str, default=None,
|
| 409 |
+
help="Filter eval_prompts.json by category field")
|
| 410 |
+
parser.add_argument("--threshold", type=float, default=DEFAULT_THRESHOLD,
|
| 411 |
+
help="Pass/fail threshold for all metrics (default: 0.5)")
|
| 412 |
+
parser.add_argument("--no-deepeval", action="store_true",
|
| 413 |
+
help="Skip DeepEval metrics; run keyword check only")
|
| 414 |
+
parser.add_argument("--save-report", action="store_true",
|
| 415 |
+
help="Write results to eval/report.json")
|
| 416 |
+
args = parser.parse_args()
|
| 417 |
+
|
| 418 |
+
run_evaluation(
|
| 419 |
+
category=args.category,
|
| 420 |
+
use_deepeval=not args.no_deepeval,
|
| 421 |
+
threshold=args.threshold,
|
| 422 |
+
save_report=args.save_report,
|
| 423 |
+
)
|
pyproject.toml
CHANGED
|
@@ -12,7 +12,6 @@ dependencies = [
|
|
| 12 |
"faiss-cpu>=1.14.2",
|
| 13 |
"google-genai>=2.8.0",
|
| 14 |
"gradio>=6.17.3",
|
| 15 |
-
"groq>=1.4.0",
|
| 16 |
"ipykernel>=7.3.0",
|
| 17 |
"langchain-chroma>=1.1.0",
|
| 18 |
"langchain>=1.3.7",
|
|
@@ -31,4 +30,5 @@ dependencies = [
|
|
| 31 |
"sentence-transformers>=5.5.1",
|
| 32 |
"langchain-huggingface>=1.2.2",
|
| 33 |
"langchain-classic>=1.0.8",
|
|
|
|
| 34 |
]
|
|
|
|
| 12 |
"faiss-cpu>=1.14.2",
|
| 13 |
"google-genai>=2.8.0",
|
| 14 |
"gradio>=6.17.3",
|
|
|
|
| 15 |
"ipykernel>=7.3.0",
|
| 16 |
"langchain-chroma>=1.1.0",
|
| 17 |
"langchain>=1.3.7",
|
|
|
|
| 30 |
"sentence-transformers>=5.5.1",
|
| 31 |
"langchain-huggingface>=1.2.2",
|
| 32 |
"langchain-classic>=1.0.8",
|
| 33 |
+
"langchain-groq>=1.1.3",
|
| 34 |
]
|
requirements.txt
CHANGED
|
@@ -7,7 +7,6 @@ sentence-transformers
|
|
| 7 |
faiss-cpu
|
| 8 |
chromadb
|
| 9 |
rank-bm25
|
| 10 |
-
groq
|
| 11 |
pyyaml
|
| 12 |
datasets
|
| 13 |
ragas
|
|
@@ -23,4 +22,5 @@ python-dotenv
|
|
| 23 |
deepeval
|
| 24 |
langchain_chroma
|
| 25 |
langchain_huggingface
|
| 26 |
-
langchain-classic
|
|
|
|
|
|
| 7 |
faiss-cpu
|
| 8 |
chromadb
|
| 9 |
rank-bm25
|
|
|
|
| 10 |
pyyaml
|
| 11 |
datasets
|
| 12 |
ragas
|
|
|
|
| 22 |
deepeval
|
| 23 |
langchain_chroma
|
| 24 |
langchain_huggingface
|
| 25 |
+
langchain-classic
|
| 26 |
+
langchain_groq
|
src/generate.py
CHANGED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
from langchain_groq import ChatGroq
|
| 6 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 7 |
+
from langchain_core.output_parsers import StrOutputParser
|
| 8 |
+
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
|
| 9 |
+
from retrieve import Retriever
|
| 10 |
+
|
| 11 |
+
load_dotenv()
|
| 12 |
+
|
| 13 |
+
if not os.getenv("GROQ_API_KEY"):
|
| 14 |
+
print("ERROR: GROQ_API_KEY environment variable not found.")
|
| 15 |
+
sys.exit(1)
|
| 16 |
+
|
| 17 |
+
SYSTEM_PROMPT = """
|
| 18 |
+
You are the official academic advisor AI for the IITM BS Degree Programme. Answer student questions accurately, professionally, and concisely.
|
| 19 |
+
|
| 20 |
+
Follow these strict rules:
|
| 21 |
+
1. **Strict Grounding:** Answer ONLY using the provided Context Documents. If the answer is not explicitly stated, state that you do not have the information. Do not guess or hallucinate.
|
| 22 |
+
2. **Data Extraction:** Carefully align rows and columns when extracting tabular data.
|
| 23 |
+
3. **Citations:** Always cite the source document name when stating factual rules or fees (e.g., "According to student-handbook.md...").
|
| 24 |
+
4. **Definitions First:** Define any acronyms, fee names, or policy terms before giving specific details. Never omit context that changes how a number or fact should be interpreted.
|
| 25 |
+
5. **Redundancy Handling:** If the context contains numerically-related figures across different sections that might represent the same fee, explicitly surface this potential overlap rather than stating them as definitively separate.
|
| 26 |
+
6. **Course Focus:** Detail only the single most relevant course. If courses with similar names exist, mention them in one brief line at the end as alternatives; do not provide their grading details unless explicitly requested.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
HUMAN_PROMPT = """CONTEXT DOCUMENTS:
|
| 30 |
+
{context}
|
| 31 |
+
|
| 32 |
+
STUDENT QUESTION:
|
| 33 |
+
{question}"""
|
| 34 |
+
|
| 35 |
+
prompt = ChatPromptTemplate.from_messages([
|
| 36 |
+
("system", SYSTEM_PROMPT),
|
| 37 |
+
("human", HUMAN_PROMPT),
|
| 38 |
+
])
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def format_context(chunks: list[dict]) -> str:
|
| 42 |
+
"""Convert retrieved chunk dicts into a formatted context string."""
|
| 43 |
+
parts = []
|
| 44 |
+
for i, c in enumerate(chunks, 1):
|
| 45 |
+
tag = f"[{c['source']} — {c['page']}]"
|
| 46 |
+
parts.append(f"--- Chunk {i} {tag} ---\n{c['text']}")
|
| 47 |
+
return "\n\n".join(parts)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class Generator:
|
| 51 |
+
def __init__(self):
|
| 52 |
+
print("Initializing retrieval system...")
|
| 53 |
+
self.retriever = Retriever()
|
| 54 |
+
|
| 55 |
+
llm = ChatGroq(
|
| 56 |
+
model="llama-3.3-70b-versatile", #llama-3.3-70b-versatile
|
| 57 |
+
temperature=0.1,
|
| 58 |
+
streaming=True,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# LCEL chain:
|
| 62 |
+
# 1. Retrieve chunks for the question
|
| 63 |
+
# 2. Format them into a context string
|
| 64 |
+
# 3. Pass context + question into the prompt
|
| 65 |
+
# 4. Send to LLM and parse output
|
| 66 |
+
self.chain = (
|
| 67 |
+
{
|
| 68 |
+
"context": RunnableLambda(lambda q: format_context(self.retriever.retrieve(q))),
|
| 69 |
+
"question": RunnablePassthrough(),
|
| 70 |
+
}
|
| 71 |
+
| prompt
|
| 72 |
+
| llm
|
| 73 |
+
| StrOutputParser()
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
# Keep a reference so answer() can show sources
|
| 77 |
+
self._last_chunks: list[dict] = []
|
| 78 |
+
|
| 79 |
+
def answer(self, query: str, top_n: int = 4) -> str:
|
| 80 |
+
"""Run the full RAG chain and stream the answer to stdout."""
|
| 81 |
+
print(f"\n🔍 Retrieving context for: '{query}'...")
|
| 82 |
+
self._last_chunks = self.retriever.retrieve(query, top_n=top_n)
|
| 83 |
+
|
| 84 |
+
if not self._last_chunks:
|
| 85 |
+
msg = "I couldn't find any official documentation related to that question."
|
| 86 |
+
print(msg)
|
| 87 |
+
return msg
|
| 88 |
+
|
| 89 |
+
print("🧠 Generating answer...\n")
|
| 90 |
+
|
| 91 |
+
full_response = ""
|
| 92 |
+
for token in self.chain.stream(query):
|
| 93 |
+
print(token, end="", flush=True)
|
| 94 |
+
full_response += token
|
| 95 |
+
|
| 96 |
+
print("\n\n" + "-" * 60)
|
| 97 |
+
print("SOURCES USED:")
|
| 98 |
+
seen = set()
|
| 99 |
+
for c in self._last_chunks:
|
| 100 |
+
label = f"- {c['source']} (Section: {c['page'][:60]})"
|
| 101 |
+
if label not in seen:
|
| 102 |
+
print(label)
|
| 103 |
+
seen.add(label)
|
| 104 |
+
|
| 105 |
+
return full_response
|
| 106 |
+
|
| 107 |
+
if __name__ == "__main__":
|
| 108 |
+
agent = Generator()
|
| 109 |
+
|
| 110 |
+
print("\n🎓 IITM BS Degree RAG Assistant Online!")
|
| 111 |
+
print("Type 'exit' or 'quit' to close.\n")
|
| 112 |
+
|
| 113 |
+
while True:
|
| 114 |
+
try:
|
| 115 |
+
user_input = input("\nStudent Question: ").strip()
|
| 116 |
+
if user_input.lower() in ("exit", "quit"):
|
| 117 |
+
print("Shutting down...")
|
| 118 |
+
break
|
| 119 |
+
if not user_input:
|
| 120 |
+
continue
|
| 121 |
+
agent.answer(user_input)
|
| 122 |
+
except KeyboardInterrupt:
|
| 123 |
+
print("\nShutting down...")
|
| 124 |
+
break
|
src/ingest.py
CHANGED
|
@@ -10,8 +10,8 @@ DB_DIR = "db"
|
|
| 10 |
COLLECTION_NAME = "handbook_docs"
|
| 11 |
EMBED_MODEL = "BAAI/bge-small-en-v1.5"
|
| 12 |
|
| 13 |
-
CHUNK_SIZE =
|
| 14 |
-
CHUNK_OVERLAP =
|
| 15 |
|
| 16 |
HEADERS_TO_SPLIT = [
|
| 17 |
("#", "h1"),
|
|
@@ -76,12 +76,17 @@ def clean_markdown(raw_text: str) -> str:
|
|
| 76 |
# Google Docs URLs
|
| 77 |
r'https://docs\.google\.com/\S+',
|
| 78 |
|
| 79 |
-
# Bare page-number artifacts: " 38/66 " or "10/66" on their own
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
]
|
| 82 |
text = raw_text
|
| 83 |
for pattern in noise_patterns:
|
| 84 |
-
text = re.sub(pattern, '', text, flags=re.IGNORECASE)
|
| 85 |
|
| 86 |
text = html_tables_to_markdown(text)
|
| 87 |
# Collapse runs of whitespace left behind by removals
|
|
@@ -108,6 +113,26 @@ def load_and_split(md_path: Path) -> list[Document]:
|
|
| 108 |
)
|
| 109 |
final_docs = char_splitter.split_documents(header_docs)
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
for doc in final_docs:
|
| 112 |
doc.metadata["source"] = md_path.name
|
| 113 |
|
|
|
|
| 10 |
COLLECTION_NAME = "handbook_docs"
|
| 11 |
EMBED_MODEL = "BAAI/bge-small-en-v1.5"
|
| 12 |
|
| 13 |
+
CHUNK_SIZE = 2000
|
| 14 |
+
CHUNK_OVERLAP = 200
|
| 15 |
|
| 16 |
HEADERS_TO_SPLIT = [
|
| 17 |
("#", "h1"),
|
|
|
|
| 76 |
# Google Docs URLs
|
| 77 |
r'https://docs\.google\.com/\S+',
|
| 78 |
|
| 79 |
+
# Bare page-number artifacts: " 38/66 " or "10/66" on their own line
|
| 80 |
+
# (excludes /100 since that's always a score threshold, e.g. "40/100")
|
| 81 |
+
r'(?<![\d>=])\b\d{1,3}/(?!100\b)\d{2,3}\b(?!\d)\s*(?=\n|$)',
|
| 82 |
+
|
| 83 |
+
r'^#{1,3}\s*BS-DS_\s*May\s*2026\s*Grading\s*document\s*\(Student\)\s*$',
|
| 84 |
+
r'^BS-DS_\s*May\s*2026\s*Grading\s*document\s*\(Student\)\s*$',
|
| 85 |
+
r'^Updated\s+automatically\s+every\s+\d+\s+minutes\s*$',
|
| 86 |
]
|
| 87 |
text = raw_text
|
| 88 |
for pattern in noise_patterns:
|
| 89 |
+
text = re.sub(pattern, '', text, flags=re.IGNORECASE | re.MULTILINE)
|
| 90 |
|
| 91 |
text = html_tables_to_markdown(text)
|
| 92 |
# Collapse runs of whitespace left behind by removals
|
|
|
|
| 113 |
)
|
| 114 |
final_docs = char_splitter.split_documents(header_docs)
|
| 115 |
|
| 116 |
+
# Prepend the section's header path to every chunk's content.
|
| 117 |
+
# Without this, a chunk containing e.g. a course's T formula but not
|
| 118 |
+
# the course name (because the header line landed in an earlier chunk)
|
| 119 |
+
# won't match queries that mention the course name — for either the
|
| 120 |
+
# vector embedding or BM25.
|
| 121 |
+
for doc in final_docs:
|
| 122 |
+
header_parts = [
|
| 123 |
+
doc.metadata.get("h1", ""),
|
| 124 |
+
doc.metadata.get("h2", ""),
|
| 125 |
+
doc.metadata.get("h3", ""),
|
| 126 |
+
]
|
| 127 |
+
header_path = " > ".join(p for p in header_parts if p)
|
| 128 |
+
if header_path:
|
| 129 |
+
doc.page_content = (
|
| 130 |
+
f"[Course: {header_path}]\n"
|
| 131 |
+
f"{doc.page_content}\n"
|
| 132 |
+
f"[/Course: {header_path}]"
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
for doc in final_docs:
|
| 137 |
doc.metadata["source"] = md_path.name
|
| 138 |
|
src/retrieve.py
CHANGED
|
@@ -28,7 +28,8 @@ _NOISE_PATTERNS = [
|
|
| 28 |
re.compile(r'Report\s+abuse\s+Learn\s+more[^\n]*', re.I),
|
| 29 |
re.compile(r'Updated\s+automatically\s+every\s+\d+\s+minutes[^\n]*', re.I),
|
| 30 |
re.compile(r'https://docs\.google\.com/\S+'),
|
| 31 |
-
re.compile(r'(?<!\d)\d{1,3}/\d{2,3}(?!\d)(?=\
|
|
|
|
| 32 |
]
|
| 33 |
|
| 34 |
def _scrub_noise(text: str) -> str:
|
|
|
|
| 28 |
re.compile(r'Report\s+abuse\s+Learn\s+more[^\n]*', re.I),
|
| 29 |
re.compile(r'Updated\s+automatically\s+every\s+\d+\s+minutes[^\n]*', re.I),
|
| 30 |
re.compile(r'https://docs\.google\.com/\S+'),
|
| 31 |
+
re.compile(r'(?<![\d>=])\b\d{1,3}/(?!100\b)\d{2,3}\b(?!\d)\s*(?=\n|$)'),
|
| 32 |
+
re.compile(r'^#{1,3}\s*BS-DS_\s*May\s*2026\s*Grading\s*document\s*\(Student\)\s*$', re.I | re.M),
|
| 33 |
]
|
| 34 |
|
| 35 |
def _scrub_noise(text: str) -> str:
|
uv.lock
CHANGED
|
@@ -1748,7 +1748,7 @@ wheels = [
|
|
| 1748 |
|
| 1749 |
[[package]]
|
| 1750 |
name = "groq"
|
| 1751 |
-
version = "
|
| 1752 |
source = { registry = "https://pypi.org/simple" }
|
| 1753 |
dependencies = [
|
| 1754 |
{ name = "anyio" },
|
|
@@ -1758,9 +1758,9 @@ dependencies = [
|
|
| 1758 |
{ name = "sniffio" },
|
| 1759 |
{ name = "typing-extensions" },
|
| 1760 |
]
|
| 1761 |
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
| 1762 |
wheels = [
|
| 1763 |
-
{ url = "https://files.pythonhosted.org/packages/
|
| 1764 |
]
|
| 1765 |
|
| 1766 |
[[package]]
|
|
@@ -2467,6 +2467,19 @@ wheels = [
|
|
| 2467 |
{ url = "https://files.pythonhosted.org/packages/04/a2/af563ff45208d22abc28d0a0e44a7fb8aceaadac201afa6a11c77bfb6338/langchain_google_vertexai-3.2.4-py3-none-any.whl", hash = "sha256:65b5615e596fdabc2e149f0160fded88bebef2bbc1ea70095ff81714f7570183", size = 118884 },
|
| 2468 |
]
|
| 2469 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2470 |
[[package]]
|
| 2471 |
name = "langchain-huggingface"
|
| 2472 |
version = "1.2.2"
|
|
@@ -4975,7 +4988,6 @@ dependencies = [
|
|
| 4975 |
{ name = "faiss-cpu" },
|
| 4976 |
{ name = "google-genai" },
|
| 4977 |
{ name = "gradio" },
|
| 4978 |
-
{ name = "groq" },
|
| 4979 |
{ name = "ipykernel" },
|
| 4980 |
{ name = "langchain" },
|
| 4981 |
{ name = "langchain-chroma" },
|
|
@@ -4984,6 +4996,7 @@ dependencies = [
|
|
| 4984 |
{ name = "langchain-core" },
|
| 4985 |
{ name = "langchain-google-genai" },
|
| 4986 |
{ name = "langchain-google-vertexai" },
|
|
|
|
| 4987 |
{ name = "langchain-huggingface" },
|
| 4988 |
{ name = "langchain-openai" },
|
| 4989 |
{ name = "pdfplumber" },
|
|
@@ -5005,7 +5018,6 @@ requires-dist = [
|
|
| 5005 |
{ name = "faiss-cpu", specifier = ">=1.14.2" },
|
| 5006 |
{ name = "google-genai", specifier = ">=2.8.0" },
|
| 5007 |
{ name = "gradio", specifier = ">=6.17.3" },
|
| 5008 |
-
{ name = "groq", specifier = ">=1.4.0" },
|
| 5009 |
{ name = "ipykernel", specifier = ">=7.3.0" },
|
| 5010 |
{ name = "langchain", specifier = ">=1.3.7" },
|
| 5011 |
{ name = "langchain-chroma", specifier = ">=1.1.0" },
|
|
@@ -5014,6 +5026,7 @@ requires-dist = [
|
|
| 5014 |
{ name = "langchain-core", specifier = ">=1.4.5" },
|
| 5015 |
{ name = "langchain-google-genai", specifier = ">=4.2.5" },
|
| 5016 |
{ name = "langchain-google-vertexai", specifier = ">=3.2.4" },
|
|
|
|
| 5017 |
{ name = "langchain-huggingface", specifier = ">=1.2.2" },
|
| 5018 |
{ name = "langchain-openai", specifier = ">=1.3.0" },
|
| 5019 |
{ name = "pdfplumber", specifier = ">=0.11.9" },
|
|
|
|
| 1748 |
|
| 1749 |
[[package]]
|
| 1750 |
name = "groq"
|
| 1751 |
+
version = "0.37.1"
|
| 1752 |
source = { registry = "https://pypi.org/simple" }
|
| 1753 |
dependencies = [
|
| 1754 |
{ name = "anyio" },
|
|
|
|
| 1758 |
{ name = "sniffio" },
|
| 1759 |
{ name = "typing-extensions" },
|
| 1760 |
]
|
| 1761 |
+
sdist = { url = "https://files.pythonhosted.org/packages/e9/78/18948a9056e1509c87e10ab8316a90ecce87035fbd53342dffdf97f4de00/groq-0.37.1.tar.gz", hash = "sha256:7353d6dfb60834fd7aacbb86af106e2dc2aeaff6d0edd65fb2fd0f16bd39314c", size = 145289 }
|
| 1762 |
wheels = [
|
| 1763 |
+
{ url = "https://files.pythonhosted.org/packages/5f/d6/645a081750e43f858b7d09dce5d8e1e76cf11e7e4bdba81252e04f78963d/groq-0.37.1-py3-none-any.whl", hash = "sha256:b49f8c8898c55eaec9f71f1342f3fcacc9560d67a08ce5f35fbfb84e8dacd3da", size = 137494 },
|
| 1764 |
]
|
| 1765 |
|
| 1766 |
[[package]]
|
|
|
|
| 2467 |
{ url = "https://files.pythonhosted.org/packages/04/a2/af563ff45208d22abc28d0a0e44a7fb8aceaadac201afa6a11c77bfb6338/langchain_google_vertexai-3.2.4-py3-none-any.whl", hash = "sha256:65b5615e596fdabc2e149f0160fded88bebef2bbc1ea70095ff81714f7570183", size = 118884 },
|
| 2468 |
]
|
| 2469 |
|
| 2470 |
+
[[package]]
|
| 2471 |
+
name = "langchain-groq"
|
| 2472 |
+
version = "1.1.3"
|
| 2473 |
+
source = { registry = "https://pypi.org/simple" }
|
| 2474 |
+
dependencies = [
|
| 2475 |
+
{ name = "groq" },
|
| 2476 |
+
{ name = "langchain-core" },
|
| 2477 |
+
]
|
| 2478 |
+
sdist = { url = "https://files.pythonhosted.org/packages/6e/4d/64b5e57e9e43f009fcb1591f567f0095bcf38f0b705a2d63cec4c04e0d56/langchain_groq-1.1.3.tar.gz", hash = "sha256:890c099a55526bceafc3e696d123cb9d36464c6664a3ead34ae6e09e0d50caeb", size = 192228 }
|
| 2479 |
+
wheels = [
|
| 2480 |
+
{ url = "https://files.pythonhosted.org/packages/a7/5d/2f862bf5623d5c8ca6ed8a8917a7b1410e6d3595ee0bac12aff508556f76/langchain_groq-1.1.3-py3-none-any.whl", hash = "sha256:a69bb8212b7a699f407c033bf41ca526db8de68f438d51a41740a72bf6dc09bf", size = 20779 },
|
| 2481 |
+
]
|
| 2482 |
+
|
| 2483 |
[[package]]
|
| 2484 |
name = "langchain-huggingface"
|
| 2485 |
version = "1.2.2"
|
|
|
|
| 4988 |
{ name = "faiss-cpu" },
|
| 4989 |
{ name = "google-genai" },
|
| 4990 |
{ name = "gradio" },
|
|
|
|
| 4991 |
{ name = "ipykernel" },
|
| 4992 |
{ name = "langchain" },
|
| 4993 |
{ name = "langchain-chroma" },
|
|
|
|
| 4996 |
{ name = "langchain-core" },
|
| 4997 |
{ name = "langchain-google-genai" },
|
| 4998 |
{ name = "langchain-google-vertexai" },
|
| 4999 |
+
{ name = "langchain-groq" },
|
| 5000 |
{ name = "langchain-huggingface" },
|
| 5001 |
{ name = "langchain-openai" },
|
| 5002 |
{ name = "pdfplumber" },
|
|
|
|
| 5018 |
{ name = "faiss-cpu", specifier = ">=1.14.2" },
|
| 5019 |
{ name = "google-genai", specifier = ">=2.8.0" },
|
| 5020 |
{ name = "gradio", specifier = ">=6.17.3" },
|
|
|
|
| 5021 |
{ name = "ipykernel", specifier = ">=7.3.0" },
|
| 5022 |
{ name = "langchain", specifier = ">=1.3.7" },
|
| 5023 |
{ name = "langchain-chroma", specifier = ">=1.1.0" },
|
|
|
|
| 5026 |
{ name = "langchain-core", specifier = ">=1.4.5" },
|
| 5027 |
{ name = "langchain-google-genai", specifier = ">=4.2.5" },
|
| 5028 |
{ name = "langchain-google-vertexai", specifier = ">=3.2.4" },
|
| 5029 |
+
{ name = "langchain-groq", specifier = ">=1.1.3" },
|
| 5030 |
{ name = "langchain-huggingface", specifier = ">=1.2.2" },
|
| 5031 |
{ name = "langchain-openai", specifier = ">=1.3.0" },
|
| 5032 |
{ name = "pdfplumber", specifier = ">=0.11.9" },
|