File size: 10,951 Bytes
324f9b1
b752b49
f0f7c9d
 
 
 
 
 
db840b8
 
 
 
f0f7c9d
 
 
d6ac435
 
b752b49
f0f7c9d
b752b49
 
 
f0f7c9d
b752b49
 
 
 
8b95a88
b752b49
 
 
 
f0f7c9d
 
8b95a88
b752b49
 
 
f0f7c9d
 
 
b752b49
d0c46e1
 
 
 
f0f7c9d
 
 
 
 
 
 
d0c46e1
 
f0f7c9d
 
 
 
 
 
 
d0c46e1
 
f0f7c9d
d0c46e1
8b95a88
d0c46e1
 
 
f0f7c9d
 
d0c46e1
 
 
 
 
 
 
f0f7c9d
 
d0c46e1
f0f7c9d
d0c46e1
8b95a88
d0c46e1
d6ac435
 
 
 
 
 
 
 
 
 
8b95a88
d0c46e1
 
 
 
f0f7c9d
 
 
 
 
 
8b95a88
d0c46e1
f0f7c9d
8b95a88
d0c46e1
 
 
8b95a88
d0c46e1
 
 
f0f7c9d
d0c46e1
 
8b95a88
f0f7c9d
d0c46e1
f0f7c9d
8b95a88
d0c46e1
d6ac435
f0f7c9d
d6ac435
 
 
 
 
 
f0f7c9d
d6ac435
 
 
 
f0f7c9d
d6ac435
8b95a88
d0c46e1
d6ac435
f0f7c9d
d6ac435
 
 
 
 
 
f0f7c9d
d6ac435
 
f0f7c9d
d6ac435
f0f7c9d
d6ac435
 
f0f7c9d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d0c46e1
 
f0f7c9d
8b95a88
d0c46e1
f0f7c9d
8b95a88
d0c46e1
f0f7c9d
 
d0c46e1
 
 
8b95a88
d0c46e1
f0f7c9d
 
d0c46e1
 
 
8b95a88
f0f7c9d
 
 
d0c46e1
f0f7c9d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
da39e8e
f0f7c9d
 
 
 
 
 
 
 
 
 
 
8b95a88
d0c46e1
 
f0f7c9d
b752b49
 
 
 
f0f7c9d
b752b49
8b95a88
 
f0f7c9d
 
b752b49
 
db840b8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
import os
import gradio as gr
import json
import logging
from pathlib import Path
from typing import List
from dotenv import load_dotenv

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain.chains import RetrievalQA
from langchain.schema import Document
from langchain_community.document_loaders import TextLoader, PyPDFLoader
from langchain_groq import ChatGroq

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer

# Logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class SimpleEmbeddings:
    def __init__(self):
        self.vectorizer = TfidfVectorizer(max_features=384, stop_words='english')
        self.fitted = False

    def embed_documents(self, texts: List[str]) -> List[List[float]]:
        if not self.fitted:
            self.vectorizer.fit(texts)
            self.fitted = True
        embeddings = self.vectorizer.transform(texts)
        return embeddings.toarray().tolist()

    def embed_query(self, text: str) -> List[float]:
        if not self.fitted:
            return [0.0] * 384
        embedding = self.vectorizer.transform([text])
        return embedding.toarray()[0].tolist()


class RAGAssistant:
    def __init__(self, groq_api_key: str):
        self.groq_api_key = groq_api_key
        self.embeddings = self._init_embeddings()

        self.text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=1000,
            chunk_overlap=200,
            length_function=len
        )

        self.learning_vectorstore = None
        self.code_vectorstore = None

        self.llm = ChatGroq(
            groq_api_key=groq_api_key,
            model_name="llama3-70b-8192",
            temperature=0.1
        )

        self.learning_persist_dir = "./chroma_learning_db"
        self.code_persist_dir = "./chroma_code_db"

        self._init_vector_stores()

    def _init_embeddings(self):
        try:
            from langchain_huggingface import HuggingFaceEmbeddings
            models_to_try = ["all-MiniLM-L6-v2", "paraphrase-MiniLM-L3-v2", "all-mpnet-base-v2"]
            for model_name in models_to_try:
                try:
                    embeddings = HuggingFaceEmbeddings(
                        model_name=model_name,
                        model_kwargs={'device': 'cpu'},
                        encode_kwargs={'normalize_embeddings': False}
                    )
                    return embeddings
                except Exception:
                    continue
        except ImportError:
            pass
        return SimpleEmbeddings()

    def _init_vector_stores(self):
        self.learning_vectorstore = Chroma(
            persist_directory=self.learning_persist_dir,
            embedding_function=self.embeddings,
            collection_name="learning_materials"
        )
        self.code_vectorstore = Chroma(
            persist_directory=self.code_persist_dir,
            embedding_function=self.embeddings,
            collection_name="code_documentation"
        )

    def load_documents(self, files: List[str], assistant_type: str) -> str:
        try:
            documents = []
            for file_path in files:
                if file_path.endswith('.pdf'):
                    loader = PyPDFLoader(file_path)
                else:
                    loader = TextLoader(file_path, encoding='utf-8')
                docs = loader.load()
                documents.extend(docs)

            if not documents:
                return "No documents could be loaded."

            chunks = self.text_splitter.split_documents(documents)
            for chunk in chunks:
                chunk.metadata['assistant_type'] = assistant_type

            if assistant_type == "learning":
                self.learning_vectorstore.add_documents(chunks)
                self.learning_vectorstore.persist()
            elif assistant_type == "code":
                self.code_vectorstore.add_documents(chunks)
                self.code_vectorstore.persist()

            return f"Loaded {len(chunks)} chunks from {len(documents)} documents."
        except Exception as e:
            return f"Error loading documents: {str(e)}"

    def get_learning_tutor_response(self, question: str) -> str:
        if not self.learning_vectorstore:
            return "Please upload some learning materials."
        qa_chain = RetrievalQA.from_chain_type(
            llm=self.llm,
            chain_type="stuff",
            retriever=self.learning_vectorstore.as_retriever(search_kwargs={"k": 3}),
            return_source_documents=True
        )
        result = qa_chain({"query": question})
        response = result['result']
        if result.get('source_documents'):
            response += "\n\n**Sources:**\n"
            for doc in result['source_documents'][:3]:
                response += f"- {Path(doc.metadata.get('source', 'Unknown')).name}\n"
        return response

    def get_code_helper_response(self, question: str) -> str:
        if not self.code_vectorstore:
            return "Please upload some code documentation."
        qa_chain = RetrievalQA.from_chain_type(
            llm=self.llm,
            chain_type="stuff",
            retriever=self.code_vectorstore.as_retriever(search_kwargs={"k": 3}),
            return_source_documents=True
        )
        result = qa_chain({"query": question})
        response = result['result']
        if result.get('source_documents'):
            response += "\n\n**Documentation Sources:**\n"
            for doc in result['source_documents'][:3]:
                response += f"- {Path(doc.metadata.get('source', 'Unknown')).name}\n"
        return response


# --- Evaluation Utilities ---
def evaluate_retrieval(assistant, assistant_type: str, eval_file: str, k=3):
    try:
        if not os.path.exists(eval_file):
            return f"Evaluation file {eval_file} not found."

        with open(eval_file, 'r', encoding='utf-8') as f:
            eval_data = json.load(f)

        total = len(eval_data)
        hits = 0
        mrr = 0

        for idx, item in enumerate(eval_data):
            question = item.get("question", "")
            keywords = item.get("keywords", [])
            result = assistant.get_learning_tutor_response(question) if assistant_type == "learning" else assistant.get_code_helper_response(question)
            hit = any(kw.lower() in result.lower() for kw in keywords)
            if hit:
                hits += 1
                mrr += 1 / (idx + 1)

        precision = hits / total if total else 0
        recall = precision
        mean_rr = mrr / total if total else 0

        return f"""
πŸ“Š Evaluation Summary ({assistant_type.title()} Assistant):
- Total Queries: {total}
- Precision@{k}: {precision:.2f}
- Recall@{k}: {recall:.2f}
- MRR: {mean_rr:.2f}

βš™οΈ Config:
- Retriever Top-K: {k}
- Embedding Model: {getattr(assistant.embeddings, 'model_name', 'TF-IDF (fallback)')}
- Vector Store: ChromaDB
        """
    except Exception as e:
        return f"Evaluation error: {str(e)}"


def create_gradio_interface(assistant: RAGAssistant):
    def upload_learning_files(files):
        return assistant.load_documents([f.name for f in files], "learning") if files else "No files uploaded."

    def upload_code_files(files):
        return assistant.load_documents([f.name for f in files], "code") if files else "No files uploaded."

    def learning_chat(message, history):
        if not message.strip():
            return history, ""
        response = assistant.get_learning_tutor_response(message)
        history.append((message, response))
        return history, ""

    def code_chat(message, history):
        if not message.strip():
            return history, ""
        response = assistant.get_code_helper_response(message)
        history.append((message, response))
        return history, ""

    with gr.Blocks(title="RAG-Based Learning & Code Assistant", theme=gr.themes.Soft()) as demo:
        gr.Markdown("# πŸŽ“ RAG-Based Learning & Code Assistant")

        with gr.Tabs():
            with gr.TabItem("πŸ“š Learning Tutor"):
                learning_files = gr.File(file_types=[".pdf", ".txt", ".md"], file_count="multiple")
                learning_upload_btn = gr.Button("Upload Materials")
                learning_status = gr.Textbox()
                learning_chatbot = gr.Chatbot(height=400)
                learning_input = gr.Textbox(placeholder="Ask about your learning materials")
                learning_submit = gr.Button("Ask")

                learning_upload_btn.click(upload_learning_files, [learning_files], [learning_status])
                learning_submit.click(learning_chat, [learning_input, learning_chatbot], [learning_chatbot, learning_input])
                learning_input.submit(learning_chat, [learning_input, learning_chatbot], [learning_chatbot, learning_input])

            with gr.TabItem("πŸ’» Code Documentation Helper"):
                code_files = gr.File(file_types=[".pdf", ".txt", ".md", ".py", ".js", ".json"], file_count="multiple")
                code_upload_btn = gr.Button("Upload Documentation")
                code_status = gr.Textbox()
                code_chatbot = gr.Chatbot(height=400)
                code_input = gr.Textbox(placeholder="Ask about your codebase or APIs")
                code_submit = gr.Button("Ask")

                code_upload_btn.click(upload_code_files, [code_files], [code_status])
                code_submit.click(code_chat, [code_input, code_chatbot], [code_chatbot, code_input])
                code_input.submit(code_chat, [code_input, code_chatbot], [code_chatbot, code_input])

            with gr.TabItem("πŸ“ˆ Evaluation Bench"):
                gr.Markdown("Upload a supported document (.json) containing evaluation questions and expected keywords.")
                eval_file_input = gr.File(file_types=[".json"], file_count="single")
                eval_assistant_choice = gr.Radio(["learning", "code"], label="Assistant", value="learning")
                eval_button = gr.Button("Run Evaluation")
                eval_output = gr.Textbox(lines=10)

                def run_eval(file, assistant_type):
                    if file is None:
                        return "Please upload a valid evaluation file."
                    return evaluate_retrieval(assistant, assistant_type, file.name)

                eval_button.click(run_eval, [eval_file_input, eval_assistant_choice], [eval_output])

    return demo


def main():
    load_dotenv()
    groq_api_key = os.getenv("GROQ_API_KEY")
    if not groq_api_key:
        print("Set your GROQ_API_KEY in .env file")
        return
    assistant = RAGAssistant(groq_api_key)
    demo = create_gradio_interface(assistant)
    demo.launch(server_name="0.0.0.0", server_port=7860, debug=True)


if __name__ == "__main__":
    main()