Dobin Yim commited on
Commit
9046e9c
Β·
1 Parent(s): 74aa61e

Final App V0

Browse files
Files changed (7) hide show
  1. .env +2 -0
  2. Dockerfile +11 -0
  3. Excel Review.pdf +0 -0
  4. chainlit.md +14 -0
  5. final.py +333 -0
  6. requirements.txt +24 -0
  7. uploads/fall23.zip +3 -0
.env ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ OPENAI_API_KEY=sk-proj-Ag0GaxKAAre2MFgXdFUWT3BlbkFJEAR66bo4a45j4Sa5DwML
2
+ PYTHONPATH=.
Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11
2
+ RUN useradd -m -u 1000 user
3
+ USER user
4
+ ENV HOME=/home/user \
5
+ PATH=/home/user/.local/bin:$PATH
6
+ WORKDIR $HOME/app
7
+ COPY --chown=user . $HOME/app
8
+ COPY ./requirements.txt ~/app/requirements.txt
9
+ RUN pip install -r requirements.txt
10
+ COPY . .
11
+ CMD ["chainlit", "run", "final.py", "--port", "7860"]
Excel Review.pdf ADDED
Binary file (46.6 kB). View file
 
chainlit.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Final Project πŸš€πŸ€–
2
+
3
+ Hi there, Developer! πŸ‘‹ We're excited to have you on board. Chainlit is a powerful tool designed to help you prototype, debug and share applications built on top of LLMs.
4
+
5
+ ## Useful Links πŸ”—
6
+
7
+ - **Documentation:** Get started with our comprehensive [Chainlit Documentation](https://docs.chainlit.io) πŸ“š
8
+ - **Discord Community:** Join our friendly [Chainlit Discord](https://discord.gg/k73SQ3FyUh) to ask questions, share your projects, and connect with other developers! πŸ’¬
9
+
10
+ We can't wait to see what you create with Chainlit! Happy coding! πŸ’»πŸ˜Š
11
+
12
+ ## Welcome screen
13
+
14
+ To modify the welcome screen, edit the `chainlit.md` file at the root of your project. If you do not want a welcome screen, just leave this file empty.
final.py ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """AIE3final.py
3
+ ______
4
+ Automated Grading System for AIE3 Final Project
5
+ ______
6
+ """
7
+
8
+ # Import necessary libraries
9
+ import logging
10
+ import sys
11
+ import os
12
+ import re
13
+ import zipfile
14
+ import tempfile
15
+ from typing import List, Dict, Tuple
16
+ from dotenv import load_dotenv
17
+ from langchain_community.document_loaders import PyMuPDFLoader
18
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
19
+ from langchain.schema import Document
20
+ from langchain_core.messages import AIMessage
21
+ from langchain_openai import OpenAIEmbeddings
22
+ from sentence_transformers import SentenceTransformer
23
+ from qdrant_client import QdrantClient
24
+ from qdrant_client.models import VectorParams, Distance, PointStruct, ScoredPoint
25
+ from docx import Document as DocxDocument
26
+ from transformers import AutoModelForCausalLM, AutoTokenizer
27
+ import torch
28
+ import getpass
29
+ from langchain_core.prompts import ChatPromptTemplate
30
+ from langchain_openai import ChatOpenAI
31
+ import openai
32
+ import json
33
+ import numpy as np
34
+ from sklearn.metrics.pairwise import cosine_similarity
35
+ import chainlit as cl
36
+ import asyncio
37
+
38
+ # Load environment variables
39
+ load_dotenv()
40
+ OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
41
+ openai.api_key = OPENAI_API_KEY
42
+
43
+ # Set up logging
44
+ logging.basicConfig(level=logging.INFO)
45
+ logger = logging.getLogger(__name__)
46
+
47
+ # Define constants
48
+ REFERENCE_DOCUMENT_PATH = './Excel Review.pdf'
49
+ UPLOAD_FOLDER = './uploads'
50
+
51
+ # Ensure the upload folder exists
52
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
53
+
54
+ def unzip_file(file_path: str, output_dir: str):
55
+ with zipfile.ZipFile(file_path, 'r') as zip_ref:
56
+ for member in zip_ref.namelist():
57
+ if not member.startswith('__MACOSX/'):
58
+ zip_ref.extract(member, output_dir)
59
+
60
+ def read_pdf(file_path: str) -> List[Document]:
61
+ loader = PyMuPDFLoader(file_path)
62
+ return loader.load()
63
+
64
+ def read_docx(file_path: str) -> Document:
65
+ doc = DocxDocument(file_path)
66
+ text = "\n".join([p.text for p in doc.paragraphs])
67
+ return Document(page_content=text, metadata={"source": file_path})
68
+
69
+ def read_files_from_directory(directory: str) -> List[Document]:
70
+ documents = []
71
+ for root, _, files in os.walk(directory):
72
+ for file in files:
73
+ file_path = os.path.join(root, file)
74
+ if os.path.basename(file_path).startswith('~$'):
75
+ continue # Skip temporary files
76
+ if file_path.endswith('.docx'):
77
+ documents.append(read_docx(file_path))
78
+ elif file_path.endswith('.pdf'):
79
+ documents.extend(read_pdf(file_path))
80
+ return documents
81
+
82
+ def extract_json(message: AIMessage) -> List[dict]:
83
+ text = message.content
84
+ pattern = r"```json(.*?)```"
85
+ matches = re.findall(pattern, text, re.DOTALL)
86
+ try:
87
+ return [json.loads(match.strip()) for match in matches]
88
+ except Exception:
89
+ raise ValueError(f"Failed to parse: {message}")
90
+
91
+ qa_chat_model = ChatOpenAI(
92
+ model="gpt-4o-mini",
93
+ temperature=0
94
+ )
95
+
96
+ ref_prompt = f"""
97
+ You are given a reference documents. The document contains a mix of instructions, guides, questions, and answers.
98
+ Your task is to go through the reference document and extract questions and answers from the document step-by-step.
99
+ Use the keyword 'Question #' to identify the start of each question.
100
+ Retain the following words until the 'Answer:' as the question.
101
+ Use the keyword 'Answer:' to identify the start of each answer.
102
+ Retain the follwing words until the 'Question:' as the answer, until the end of the document.
103
+ Remove any white spaces such as carriage returns.
104
+ Return the question-answer pairs as a key-value pair as Dict type.
105
+ ---
106
+
107
+ Reference Document Content:
108
+ {{source}}
109
+
110
+ Please extract the question-answer pairs and return them as JSON.
111
+ """
112
+
113
+ ref_prompt_template = ChatPromptTemplate.from_template(ref_prompt)
114
+ ref_generation_chain = ref_prompt_template | qa_chat_model
115
+
116
+ student_prompt = f"""
117
+ You are given a student assignment document. The document may contain a mix of instructions, guides, questions, and answers.
118
+ Your task is to go through the student document and extract answers to questions from the document step-by-step.
119
+ Use the reference document as a guide.
120
+ Use the keyword 'Question #' to identify each question.
121
+ Then for its associated values, search the student document for the answer.
122
+ If you do not see any answer in the student document, return 'No answer found'.
123
+ Do not make up any answer.
124
+ Remove any white spaces such as carriage returns.
125
+ Return the original question and the student answer pairs as a key-value pair as Dict type.
126
+ ---
127
+
128
+ Reference Content:
129
+ {{source}}
130
+
131
+ Student Content:
132
+ {{student}}
133
+
134
+ Please extract the question-answer pairs and return them as JSON.
135
+ """
136
+
137
+ student_prompt_template = ChatPromptTemplate.from_template(student_prompt)
138
+ student_response_chain = student_prompt_template | qa_chat_model
139
+
140
+ def split_documents(documents: List[Document]) -> List[Document]:
141
+ text_splitter = RecursiveCharacterTextSplitter(
142
+ chunk_size=500,
143
+ chunk_overlap=100,
144
+ length_function=len,
145
+ is_separator_regex=False
146
+ )
147
+ split_docs = text_splitter.split_documents(documents)
148
+ total_tokens = sum(len(doc.page_content) for doc in split_docs) # Approximate token count
149
+ return split_docs, total_tokens
150
+
151
+ def generate_embeddings(docs: List[Document]) -> List[List[float]]:
152
+ embeddings_model = OpenAIEmbeddings(model="text-embedding-3-small")
153
+ embeddings = embeddings_model.embed_documents([doc.page_content for doc in docs])
154
+ total_tokens = sum(len(doc.page_content) for doc in docs) # Approximate token count
155
+ return embeddings, total_tokens
156
+
157
+ def prepare_files():
158
+ unzip_file('./uploads/fall23_small.zip', './temp')
159
+ documents = read_files_from_directory('./temp/fall23_small')
160
+ reference_document = read_pdf(REFERENCE_DOCUMENT_PATH)
161
+ return documents, reference_document
162
+
163
+ def process_student(documents, reference):
164
+ test_doc = documents[0]
165
+ student_result = student_response_chain.invoke({"source": reference.keys(),"student": test_doc })
166
+ student_gen_tokens = student_result.usage_metadata["total_tokens"]
167
+ student_result = dict(extract_json(student_result)[0])
168
+ return student_result, student_gen_tokens
169
+
170
+ def process_reference(reference_document):
171
+ result = ref_generation_chain.invoke({"source": reference_document})
172
+ ref_gen_tokens = result.usage_metadata["total_tokens"]
173
+ reference = dict(extract_json(result)[0])
174
+
175
+ answers = {}
176
+ for key in reference:
177
+ if key.startswith('Question'):
178
+ question_number = key.split('#')[1]
179
+ answer_key = f'Answer #{question_number}'
180
+ answers[key] = reference[answer_key]
181
+
182
+ return reference, answers, ref_gen_tokens
183
+
184
+ def split_docs(answers, student_result):
185
+ split_reference_docs, ref_tokens = {}, 0
186
+ split_student_docs, student_tokens = {}, 0
187
+ for key, value in answers.items():
188
+ split_docs, tokens = split_documents([Document(page_content=value)])
189
+ split_reference_docs[key] = split_docs
190
+ ref_tokens += tokens
191
+
192
+ for key, value in student_result.items():
193
+ split_docs, tokens = split_documents([Document(page_content=value)])
194
+ split_student_docs[key] = split_docs
195
+ student_tokens += tokens
196
+
197
+ reference_embeddings = {key: generate_embeddings(value)[0] for key, value in split_reference_docs.items()}
198
+ student_embeddings = {key: generate_embeddings(value)[0] for key, value in split_student_docs.items()}
199
+
200
+ return reference_embeddings, student_embeddings, ref_tokens, student_tokens
201
+
202
+ def compute_cosine_similarity(reference_embeddings: dict, student_embeddings: dict) -> float:
203
+ similarity_results = {}
204
+ for key in reference_embeddings.keys():
205
+ if key not in student_embeddings:
206
+ similarity_results[key] = 0
207
+ continue
208
+ reference_vector = np.array(reference_embeddings[key]).reshape(1, -1)
209
+ student_vector = np.array(student_embeddings[key]).reshape(1, -1)
210
+ if reference_vector.shape[1] != student_vector.shape[1]:
211
+ min_dim = min(reference_vector.shape[1], student_vector.shape[1])
212
+ reference_vector = reference_vector[:, :min_dim]
213
+ student_vector = student_vector[:, :min_dim]
214
+ similarity = cosine_similarity(reference_vector, student_vector)[0][0]
215
+ similarity_results[key] = similarity
216
+
217
+ total_similarity = sum(similarity_results.values())
218
+ num_questions = len(similarity_results)
219
+ average_similarity = total_similarity / num_questions if num_questions else 0
220
+
221
+ return average_similarity
222
+
223
+
224
+ def llm_similarity(answers, student_result):
225
+ score_prompt = f"""
226
+ You are given two dictionaries representing instructor solution and student answers.
227
+ Your task is to go through each question to grade the correctness of student answer.
228
+ Use the keyword 'Question #' to identify each question.
229
+ Then for its associated values, compare student answer against the instructor answer.
230
+ If the instructor answer has numerical values, check to make sure the student answer has the same number,
231
+ whether it is expressed in numbers or text.
232
+ If you do not see any answer in the student answer, assign score 0 for that answer.
233
+ For student answer that is similar to instructor, assign a full score of 1.
234
+ If the student answer is similar enough, assign a partial score of 0.5.
235
+ Otherwise, assign a score of 0.
236
+ Return the original question and the student score pairs as a key-value pair as Dict type.
237
+ ---
238
+
239
+ Reference Content:
240
+ {{source}}
241
+
242
+ Student Content:
243
+ {{student}}
244
+
245
+ Please extract the question-answer pairs and return them as JSON.
246
+ """
247
+
248
+ score_prompt_template = ChatPromptTemplate.from_template(score_prompt)
249
+ student_score_chain = score_prompt_template | qa_chat_model
250
+
251
+ student_score = student_score_chain.invoke({"source": answers, "student": student_result })
252
+ llm_score_tokens = student_score.usage_metadata["total_tokens"]
253
+ student_score = dict(extract_json(student_score)[0])
254
+
255
+ total_score = sum(student_score.values())
256
+ num_questions = len(student_score)
257
+ average_score = total_score / num_questions if num_questions else 0
258
+
259
+ return average_score, llm_score_tokens
260
+
261
+ def process_data() -> Tuple[float, float, int, int, int]:
262
+ documents, reference_document = prepare_files()
263
+ reference, answers, ref_gen_tokens = process_reference(reference_document)
264
+ student_result, student_gen_tokens = process_student(documents, reference)
265
+ reference_embeddings, student_embeddings, ref_tokens, student_tokens = split_docs(answers, student_result)
266
+ student_total_tokens = student_gen_tokens + student_tokens
267
+ ref_total_tokens = ref_gen_tokens + ref_tokens
268
+
269
+ average_similarity = compute_cosine_similarity(reference_embeddings, student_embeddings)
270
+ average_score, llm_score_tokens = llm_similarity(answers, student_result)
271
+ llm_total_tokens = ref_gen_tokens + student_gen_tokens + llm_score_tokens
272
+
273
+ return average_similarity, average_score, ref_total_tokens, student_total_tokens, llm_total_tokens
274
+
275
+ async def process_grading():
276
+ average_similarity, average_score, ref_total_tokens, student_total_tokens, llm_total_tokens = process_data()
277
+
278
+ await cl.Message(content=f"Total tokens used for reference documents: {ref_total_tokens}").send()
279
+ await cl.Message(content=f"Total tokens used for student documents: {student_total_tokens}").send()
280
+ await cl.Message(content=f"Total tokens used by LLM: {llm_total_tokens}").send()
281
+ await cl.Message(content=f"Score: {average_similarity}").send()
282
+ await cl.Message(content=f"Average Score: {average_score}").send()
283
+
284
+ @cl.on_chat_start
285
+ async def start_chat():
286
+ await cl.Message(content="Do you want to proceed with the grading? (yes/no)").send()
287
+
288
+
289
+
290
+ # Define a global flag to track the processing state
291
+ user_wants_to_continue = False
292
+
293
+ @cl.on_message
294
+ async def on_message(message: cl.Message):
295
+ global user_wants_to_continue
296
+
297
+ if message.content.lower() == 'yes' and not user_wants_to_continue:
298
+ # Start processing
299
+ processing_message = cl.Message(content="Processing files...")
300
+ await processing_message.send() # Send the message immediately
301
+ await asyncio.sleep(0.5) # Short delay to ensure the message is displayed
302
+ await process_grading()
303
+
304
+ # Ask user if they want to continue after processing is done
305
+ user_wants_to_continue = True
306
+ await cl.Message(content="Do you want to continue? (yes/no)").send()
307
+
308
+ elif user_wants_to_continue:
309
+ if message.content.lower() == 'yes':
310
+ user_wants_to_continue = False # Reset the flag
311
+ await cl.Message(content="Restarting the app...").send()
312
+ await asyncio.sleep(1) # Give time for the message to be sent
313
+ python = sys.executable
314
+ os.execl(python, python, *sys.argv) # Restart the app
315
+
316
+ elif message.content.lower() == 'no':
317
+ user_wants_to_continue = False # Reset the flag
318
+ await cl.Message(content="Okay, thank you for using the grading app. Restarting...").send()
319
+ await asyncio.sleep(1) # Give time for the message to be sent
320
+ python = sys.executable
321
+ os.execl(python, python, *sys.argv) # Restart the app
322
+
323
+ else:
324
+ await cl.Message(content="Invalid response. Please type 'yes' or 'no'.").send()
325
+
326
+ elif message.content.lower() == 'no':
327
+ await cl.Message(content="Okay, thank you for using the grading app. Restarting...").send()
328
+ await asyncio.sleep(1) # Give time for the message to be sent
329
+ python = sys.executable
330
+ os.execl(python, python, *sys.argv) # Restart the app
331
+
332
+ else:
333
+ await cl.Message(content="Please type 'yes' to start processing or 'no' to exit.").send()
requirements.txt ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ chainlit
2
+ langchain
3
+ langchain-core
4
+ langgraph
5
+ langchain-community
6
+ langchain_huggingface
7
+ langchain_openai
8
+ langchain-text-splitters
9
+ peft
10
+ bitsandbytes
11
+ accelerate
12
+ qdrant-client
13
+ python-dotenv
14
+ pymupdf
15
+ huggingface_hub
16
+ pandas
17
+ sentence-transformers
18
+ python-docx
19
+ docx2pdf
20
+ python-dotenv
21
+ transformers
22
+ torch
23
+
24
+
uploads/fall23.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f83797e21c0153a03ef19ff14315ffff3de8730560610bd7b96e2eb066518930
3
+ size 2092284