Sumkh commited on
Commit
885a9dd
·
verified ·
1 Parent(s): ae5bdae
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ general_db/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
37
+ mcq_db/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.9-slim
3
+
4
+ # Install system dependencies
5
+ RUN apt-get update && apt-get install -y wget && rm -rf /var/lib/apt/lists/*
6
+
7
+ # Set working directory
8
+ WORKDIR /app
9
+
10
+ # Copy the requirements file and install dependencies
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy the rest of the code
15
+ COPY . .
16
+
17
+ # Expose the port for Gradio (Spaces expects the app on port 7860)
18
+ EXPOSE 7860
19
+
20
+ # Start vLLM in the background and then the Gradio app
21
+ CMD bash -c "wget -O /tmp/tool_chat_template_llama3.1_json.jinja https://github.com/vllm-project/vllm/raw/refs/heads/main/examples/tool_chat_template_llama3.1_json.jinja && \
22
+ vllm.entrypoints.openai.api_server \
23
+ --model unsloth/llama-3-8b-Instruct-bnb-4bit \
24
+ --enable-auto-tool-choice \
25
+ --tool-call-parser llama3_json \
26
+ --chat-template /tmp/tool_chat_template_llama3.1_json.jinja \
27
+ --quantization bitsandbytes \
28
+ --load-format bitsandbytes \
29
+ --dtype half \
30
+ --max-model-len 8192 \
31
+ --download-dir models/vllm > vllm.log 2>&1 & \
32
+ python app.py"
app.py ADDED
@@ -0,0 +1,1012 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from io import StringIO
2
+ import sys
3
+
4
+ import os
5
+ from huggingface_hub import login
6
+ import gradio as gr
7
+ import json
8
+ import csv
9
+ import hashlib
10
+ import uuid
11
+ import logging
12
+ from typing import Annotated, List, Dict, Sequence, TypedDict
13
+
14
+ # LangChain & related imports
15
+ from langchain_core.runnables import RunnableConfig
16
+ from langchain_core.tools import tool, StructuredTool
17
+ from pydantic import BaseModel, Field
18
+
19
+ from langchain_huggingface import HuggingFaceEmbeddings
20
+ from langchain_chroma import Chroma
21
+ from langchain_core.documents import Document
22
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
23
+ from langchain.retrievers import EnsembleRetriever
24
+
25
+ # Extraction for Documents
26
+ from langchain_docling.loader import ExportType
27
+ from langchain_docling import DoclingLoader
28
+ from docling.chunking import HybridChunker
29
+
30
+ # Extraction for HTML
31
+ from langchain_community.document_loaders import WebBaseLoader
32
+ from urllib.parse import urlparse
33
+
34
+ from langchain_groq import ChatGroq
35
+ from langchain_openai import ChatOpenAI
36
+ from langgraph.prebuilt import InjectedStore
37
+ from langgraph.store.base import BaseStore
38
+ from langgraph.store.memory import InMemoryStore
39
+ from langgraph.checkpoint.memory import MemorySaver
40
+ from langchain.embeddings import init_embeddings
41
+ from langgraph.graph import StateGraph
42
+ from langgraph.graph.message import add_messages
43
+ from langgraph.prebuilt import ToolNode, tools_condition
44
+ from langchain_core.messages import (
45
+ SystemMessage,
46
+ AIMessage,
47
+ HumanMessage,
48
+ BaseMessage,
49
+ ToolMessage,
50
+ )
51
+
52
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
53
+ logger = logging.getLogger(__name__)
54
+
55
+ # Suppress all library logs at or below WARNING for user experience:
56
+ logging.disable(logging.WARNING)
57
+
58
+
59
+ HF_TOKEN = os.getenv("HF_TOKEN") # Read from environment variable
60
+ if HF_TOKEN:
61
+ login(token=HF_TOKEN) # Log in to Hugging Face Hub
62
+ else:
63
+ print("Warning: HF_TOKEN not found in environment variables.")
64
+
65
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY") # Read from environment variable
66
+ if not GROQ_API_KEY:
67
+ print("Warning: GROQ_API_KEY not found in environment variables.")
68
+
69
+ EMBED_MODEL_ID = "sentence-transformers/all-MiniLM-L6-v2"
70
+
71
+ # =============================================================================
72
+ # Document Extraction Functions
73
+ # =============================================================================
74
+
75
+ def extract_documents(doc_path: str) -> List[str]:
76
+ """
77
+ Recursively collects all file paths from folder 'doc_path'.
78
+ Used by ExtractDocument.load_files() to find documents to parse.
79
+ """
80
+ extracted_docs = []
81
+
82
+ for root, _, files in os.walk(doc_path):
83
+ for file in files:
84
+ file_path = os.path.join(root, file)
85
+ extracted_docs.append(file_path)
86
+ return extracted_docs
87
+
88
+
89
+ def _generate_uuid(page_content: str) -> str:
90
+ """Generate a UUID for a chunk of text using MD5 hashing."""
91
+ md5_hash = hashlib.md5(page_content.encode()).hexdigest()
92
+ return str(uuid.UUID(md5_hash[0:32]))
93
+
94
+
95
+ def load_file(file_path: str) -> List[Document]:
96
+ """
97
+ Load a file from the given path and return a list of Document objects.
98
+ """
99
+ _documents = []
100
+
101
+ # Load the file and extract the text chunks
102
+ try:
103
+ loader = DoclingLoader(
104
+ file_path = file_path,
105
+ export_type = ExportType.DOC_CHUNKS,
106
+ chunker = HybridChunker(tokenizer=EMBED_MODEL_ID),
107
+ )
108
+ docs = loader.load()
109
+ logger.info(f"Total parsed doc-chunks: {len(docs)} from Source: {file_path}")
110
+
111
+ for d in docs:
112
+ # Tag each document's chunk with the source file and a unique ID
113
+ doc = Document(
114
+ page_content=d.page_content,
115
+ metadata={
116
+ "source": file_path,
117
+ "doc_id": _generate_uuid(d.page_content),
118
+ "source_type": "file",
119
+ }
120
+ )
121
+ _documents.append(doc)
122
+ logger.info(f"Total generated LangChain document chunks: {len(_documents)}\n.")
123
+
124
+ except Exception as e:
125
+ logger.error(f"Error loading file: {file_path}. Exception: {e}\n.")
126
+
127
+ return _documents
128
+
129
+
130
+ # Define function to load documents from a folder
131
+ def load_files_from_folder(doc_path: str) -> List[Document]:
132
+ """
133
+ Load documents from the given folder path and return a list of Document objects.
134
+ """
135
+ _documents = []
136
+ # Extract all files path from the given folder
137
+ extracted_docs = extract_documents(doc_path)
138
+
139
+ # Iterate through each document and extract the text chunks
140
+ for file_path in extracted_docs:
141
+ _documents.extend(load_file(file_path))
142
+
143
+ return _documents
144
+
145
+ # =============================================================================
146
+ # Load structured data in csv file to LangChain Document format
147
+ def load_mcq_csvfiles(file_path: str) -> List[Document]:
148
+ """
149
+ Load structured data in mcq csv file from the given file path and return a list of Document object.
150
+ Expected format: each row of csv is comma separated into "mcq_number", "mcq_type", "text_content"
151
+ """
152
+ _documents = []
153
+
154
+ # iterate through each csv file and load each row into _dict_per_question format
155
+ # Ensure we process only CSV files
156
+ if not file_path.endswith(".csv"):
157
+ return _documents # Skip non-CSV files
158
+ try:
159
+ # Open and read the CSV file
160
+ with open(file_path, mode='r', encoding='utf-8') as file:
161
+ reader = csv.DictReader(file)
162
+ for row in reader:
163
+ # Ensure required columns exist in the row
164
+ if not all(k in row for k in ["mcq_number", "mcq_type", "text_content"]): # Ensure required columns exist and exclude header
165
+ logger.error(f"Skipping row due to missing fields: {row}")
166
+ continue
167
+ # Tag each row of csv is comma separated into "mcq_number", "mcq_type", "text_content"
168
+ doc = Document(
169
+ page_content = row["text_content"], # text_content segment is separated by "|"
170
+ metadata={
171
+ "source": f"{file_path}_{row['mcq_number']}", # file_path + mcq_number
172
+ "doc_id": _generate_uuid(f"{file_path}_{row['mcq_number']}"), # Unique ID
173
+ "source_type": row["mcq_type"], # MCQ type
174
+ }
175
+ )
176
+ _documents.append(doc)
177
+ logger.info(f"Successfully loaded {len(_documents)} LangChain document chunks from {file_path}.")
178
+
179
+ except Exception as e:
180
+ logger.error(f"Error loading file: {file_path}. Exception: {e}\n.")
181
+
182
+ return _documents
183
+
184
+ # Define function to load documents from a folder for structured data in csv file
185
+ def load_files_from_folder_mcq(doc_path: str) -> List[Document]:
186
+ """
187
+ Load mcq csv file from the given folder path and return a list of Document objects.
188
+ """
189
+ _documents = []
190
+ # Extract all files path from the given folder
191
+ extracted_docs = [
192
+ os.path.join(doc_path, file) for file in os.listdir(doc_path)
193
+ if file.endswith(".csv") # Process only CSV files
194
+ ]
195
+
196
+ # Iterate through each document and extract the text chunks
197
+ for file_path in extracted_docs:
198
+ _documents.extend(load_mcq_csvfiles(file_path))
199
+
200
+ return _documents
201
+
202
+
203
+ # =============================================================================
204
+ # Website Extraction Functions
205
+ # =============================================================================
206
+ def _generate_uuid(page_content: str) -> str:
207
+ """Generate a UUID for a chunk of text using MD5 hashing."""
208
+ md5_hash = hashlib.md5(page_content.encode()).hexdigest()
209
+ return str(uuid.UUID(md5_hash[0:32]))
210
+
211
+ def ensure_scheme(url):
212
+ parsed_url = urlparse(url)
213
+ if not parsed_url.scheme:
214
+ return 'http://' + url # Default to http, or use 'https://' if preferred
215
+ return url
216
+
217
+ def extract_html(url: List[str]) -> List[Document]:
218
+ if isinstance(url, str):
219
+ url = [url]
220
+ """
221
+ Extracts text from the HTML content of web pages listed in 'web_path'.
222
+ Returns a list of LangChain 'Document' objects.
223
+ """
224
+ # Ensure all URLs have a scheme
225
+ web_paths = [ensure_scheme(u) for u in url]
226
+
227
+ loader = WebBaseLoader(web_paths)
228
+ loader.requests_per_second = 1
229
+ docs = loader.load()
230
+
231
+ # Iterate through each document, clean the content, removing excessive line return and store it in a LangChain Document
232
+ _documents = []
233
+ for doc in docs:
234
+ # Clean the concent
235
+ doc.page_content = doc.page_content.strip()
236
+ doc.page_content = doc.page_content.replace("\n", " ")
237
+ doc.page_content = doc.page_content.replace("\r", " ")
238
+ doc.page_content = doc.page_content.replace("\t", " ")
239
+ doc.page_content = doc.page_content.replace(" ", " ")
240
+ doc.page_content = doc.page_content.replace(" ", " ")
241
+
242
+ # Store it in a LangChain Document
243
+ web_doc = Document(
244
+ page_content=doc.page_content,
245
+ metadata={
246
+ "source": doc.metadata.get("source"),
247
+ "doc_id": _generate_uuid(doc.page_content),
248
+ "source_type": "web"
249
+ }
250
+ )
251
+ _documents.append(web_doc)
252
+ return _documents
253
+
254
+ # =============================================================================
255
+ # Vector Store Initialisation
256
+ # =============================================================================
257
+
258
+ embedding_model = HuggingFaceEmbeddings(model_name=EMBED_MODEL_ID)
259
+
260
+ # Initialise vector stores
261
+ general_vs = Chroma(
262
+ collection_name="general_vstore",
263
+ embedding_function=embedding_model,
264
+ persist_directory="./general_db"
265
+ )
266
+
267
+ mcq_vs = Chroma(
268
+ collection_name="mcq_vstore",
269
+ embedding_function=embedding_model,
270
+ persist_directory="./mcq_db"
271
+ )
272
+
273
+ in_memory_vs = Chroma(
274
+ collection_name="in_memory_vstore",
275
+ embedding_function=embedding_model
276
+ )
277
+
278
+ # Split the documents into smaller chunks for better embedding coverage
279
+ def split_text_into_chunks(docs: List[Document]) -> List[Document]:
280
+ """
281
+ Splits a list of Documents into smaller text chunks using
282
+ RecursiveCharacterTextSplitter while preserving metadata.
283
+ Returns a list of Document objects.
284
+ """
285
+ if not docs:
286
+ return []
287
+ splitter = RecursiveCharacterTextSplitter(
288
+ chunk_size=1000, # Split into chunks of 1000 characters
289
+ chunk_overlap=200, # Overlap by 200 characters
290
+ add_start_index=True
291
+ )
292
+ chunked_docs = splitter.split_documents(docs)
293
+ return chunked_docs # List of Document objects
294
+
295
+
296
+ # =============================================================================
297
+ # Retrieval Tools
298
+ # =============================================================================
299
+
300
+ # Define a simple similarity search retrieval tool on msq_vs
301
+ class MCQRetrievalTool(BaseModel):
302
+ input: str = Field(..., title="input", description="Search topic.")
303
+ k: int = Field(2, title="Number of Results", description="The number of results to retrieve.")
304
+
305
+ def mcq_retriever(input: str, k: int = 2) -> List[str]:
306
+ # Retrieve the top k most similar mcq question documents from the vector store
307
+ docs_func = mcq_vs.as_retriever(
308
+ search_type="similarity",
309
+ search_kwargs={
310
+ 'k': k,
311
+ 'filter':{"source_type": "mcq_question"}
312
+ },
313
+ )
314
+ docs_qns = docs_func.invoke(input, k=k)
315
+
316
+ # Extract the document IDs from the retrieved documents
317
+ doc_ids = [d.metadata.get("doc_id") for d in docs_qns if "doc_id" in d.metadata]
318
+
319
+ # Retrieve full documents based on the doc_ids
320
+ docs = mcq_vs.get(where = {'doc_id': {"$in":doc_ids}})
321
+
322
+ qns_list = {}
323
+ for i, d in enumerate(docs['metadatas']):
324
+ qns_list[d['source'] + " " + d['source_type']] = docs['documents'][i]
325
+
326
+ return qns_list
327
+
328
+ # Create a StructuredTool from the function
329
+ mcq_retriever_tool = StructuredTool.from_function(
330
+ func = mcq_retriever,
331
+ name = "MCQ Retrieval Tool",
332
+ description = (
333
+ """
334
+ Use this tool to retrieve MCQ questions set when Human asks to generate a quiz related to a topic.
335
+ DO NOT GIVE THE ANSWERS to Human before Human has answered all the questions.
336
+
337
+ If Human give answers for questions you do not know, SAY you do not have the questions for the answer
338
+ and ASK if the Human want you to generate a new quiz and then SAVE THE QUIZ with Summary Tool before ending the conversation.
339
+
340
+
341
+ Input must be a JSON string with the schema:
342
+ - input (str): The search topic to retrieve MCQ questions set related to the topic.
343
+ - k (int): Number of question set to retrieve.
344
+ Example usage: input='What is AI?', k=5
345
+
346
+ Returns:
347
+ - A dict of MCQ questions:
348
+ Key: 'metadata of question' e.g. './Documents/mcq/mcq.csv_Qn31 mcq_question' with suffix ['question', 'answer', 'answer_reason', 'options', 'wrong_options_reason']
349
+ Value: Text Content
350
+
351
+ """
352
+ ),
353
+ args_schema = MCQRetrievalTool,
354
+ response_format="content",
355
+ return_direct = False, # Return the response as a list of strings
356
+ verbose = False # To log tool's progress
357
+ )
358
+
359
+ # -----------------------------------------------------------------------------
360
+
361
+ # Retrieve more documents with higher diversity using MMR (Maximal Marginal Relevance) from the general vector store
362
+ # Useful if the dataset has many similar documents
363
+ class GenRetrievalTool(BaseModel):
364
+ input: str = Field(..., title="input", description="User query.")
365
+ k: int = Field(2, title="Number of Results", description="The number of results to retrieve.")
366
+
367
+ def gen_retriever(input: str, k: int = 2) -> List[str]:
368
+ # Use retriever of vector store to retrieve documents
369
+ docs_func = general_vs.as_retriever(
370
+ search_type="mmr",
371
+ search_kwargs = {'k': k, 'lambda_mult': 0.25}
372
+ )
373
+ docs = docs_func.invoke(input, k=k)
374
+ return [d.page_content for d in docs]
375
+
376
+ # Create a StructuredTool from the function
377
+ general_retriever_tool = StructuredTool.from_function(
378
+ func = gen_retriever,
379
+ name = "Assistant References Retrieval Tool",
380
+ description = (
381
+ """
382
+ Use this tool to retrieve reference information from Assistant reference database for Human queries related to a topic or
383
+ and when Human asked to generate guides to learn or study about a topic.
384
+
385
+ Input must be a JSON string with the schema:
386
+ - input (str): The user query.
387
+ - k (int): Number of results to retrieve.
388
+ Example usage: input='What is AI?', k=5
389
+ Returns:
390
+ - A list of retrieved document's content string.
391
+ """
392
+ ),
393
+ args_schema = GenRetrievalTool,
394
+ response_format="content",
395
+ return_direct = False, # Return the content of the documents
396
+ verbose = False # To log tool's progress
397
+ )
398
+
399
+ # -----------------------------------------------------------------------------
400
+
401
+ # Retrieve more documents with higher diversity using MMR (Maximal Marginal Relevance) from the in-memory vector store
402
+ # Query in-memory vector store only
403
+ class InMemoryRetrievalTool(BaseModel):
404
+ input: str = Field(..., title="input", description="User query.")
405
+ k: int = Field(2, title="Number of Results", description="The number of results to retrieve.")
406
+
407
+ def in_memory_retriever(input: str, k: int = 2) -> List[str]:
408
+ # Use retriever of vector store to retrieve documents
409
+ docs_func = in_memory_vs.as_retriever(
410
+ search_type="mmr",
411
+ search_kwargs = {'k': k, 'lambda_mult': 0.25}
412
+ )
413
+ docs = docs_func.invoke(input, k=k)
414
+ return [d.page_content for d in docs]
415
+
416
+ # Create a StructuredTool from the function
417
+ in_memory_retriever_tool = StructuredTool.from_function(
418
+ func = in_memory_retriever,
419
+ name = "In-Memory Retrieval Tool",
420
+ description = (
421
+ """
422
+ Use this tool when Human ask Assistant to retrieve information from documents that Human has uploaded.
423
+
424
+ Input must be a JSON string with the schema:
425
+ - input (str): The user query.
426
+ - k (int): Number of results to retrieve.
427
+ """
428
+ ),
429
+ args_schema = InMemoryRetrievalTool,
430
+ response_format="content",
431
+ return_direct = False, # Whether to return the tool’s output directly
432
+ verbose = False # To log tool's progress
433
+ )
434
+
435
+ # -----------------------------------------------------------------------------
436
+
437
+ # Web Extraction Tool
438
+ class WebExtractionRequest(BaseModel):
439
+ input: str = Field(..., title="input", description="Search text.")
440
+ url: str = Field(
441
+ ...,
442
+ title="url",
443
+ description="Web URL(s) to extract content from. If multiple URLs, separate them with a comma."
444
+ )
445
+ k: int = Field(5, title="Number of Results", description="The number of results to retrieve.")
446
+
447
+ # Extract content from a web URL, load into in_memory_vstore
448
+ def extract_web_path_tool(input: str, url: str, k: int = 5) -> List[str]:
449
+ if isinstance(url, str):
450
+ url = [url]
451
+ """
452
+ Extract content from the web URLs based on user's input.
453
+ Args:
454
+ - input: The input text to search for.
455
+ - url: URLs to extract content from.
456
+ - k: Number of results to retrieve.
457
+ Returns:
458
+ - A list of retrieved document's content string.
459
+ """
460
+ # Extract content from the web
461
+ html_docs = extract_html(url)
462
+ if not html_docs:
463
+ return f"No content extracted from {url}."
464
+
465
+ # Split the documents into smaller chunks for better embedding coverage
466
+ chunked_texts = split_text_into_chunks(html_docs)
467
+ in_memory_vs.add_documents(chunked_texts) # Add the chunked texts to the in-memory vector store
468
+
469
+ # Extract content from the in-memory vector store
470
+ # Use retriever of vector store to retrieve documents
471
+ docs_func = in_memory_vs.as_retriever(
472
+ search_type="mmr",
473
+ search_kwargs={
474
+ 'k': k,
475
+ 'lambda_mult': 0.25,
476
+ 'filter':{"source": {"$in": url}}
477
+ },
478
+ )
479
+ docs = docs_func.invoke(input, k=k)
480
+ return [d.page_content for d in docs]
481
+
482
+ # Create a StructuredTool from the function
483
+ web_extraction_tool = StructuredTool.from_function(
484
+ func = extract_web_path_tool,
485
+ name = "Web Extraction Tool",
486
+ description = (
487
+ "Assistant should use this tool to extract content from web URLs based on user's input, "
488
+ "Web extraction is initially load into database and then return k: Number of results to retrieve"
489
+ ),
490
+ args_schema = WebExtractionRequest,
491
+ return_direct = False, # Whether to return the tool’s output directly
492
+ verbose = False # To log tool's progress
493
+ )
494
+
495
+ # -----------------------------------------------------------------------------
496
+
497
+ # Ensemble Retrieval from General and In-Memory Vector Stores
498
+ class EnsembleRetrievalTool(BaseModel):
499
+ input: str = Field(..., title="input", description="User query.")
500
+ k: int = Field(5, title="Number of Results", description="Number of results.")
501
+
502
+ def ensemble_retriever(input: str, k: int = 5) -> List[str]:
503
+ # Use retriever of vector store to retrieve documents
504
+ general_retrieval = general_vs.as_retriever(
505
+ search_type="mmr",
506
+ search_kwargs = {'k': k, 'lambda_mult': 0.25}
507
+ )
508
+ in_memory_retrieval = in_memory_vs.as_retriever(
509
+ search_type="mmr",
510
+ search_kwargs = {'k': k, 'lambda_mult': 0.25}
511
+ )
512
+
513
+ ensemble_retriever = EnsembleRetriever(
514
+ retrievers=[general_retrieval, in_memory_retrieval],
515
+ weights=[0.5, 0.5]
516
+ )
517
+ docs = ensemble_retriever.invoke(input)
518
+ return [d.page_content for d in docs]
519
+
520
+ # Create a StructuredTool from the function
521
+ ensemble_retriever_tool = StructuredTool.from_function(
522
+ func = ensemble_retriever,
523
+ name = "Ensemble Retriever Tool",
524
+ description = (
525
+ """
526
+ Use this tool to retrieve information from reference database and
527
+ extraction of documents that Human has uploaded.
528
+
529
+ Input must be a JSON string with the schema:
530
+ - input (str): The user query.
531
+ - k (int): Number of results to retrieve.
532
+ """
533
+ ),
534
+ args_schema = EnsembleRetrievalTool,
535
+ response_format="content",
536
+ return_direct = False
537
+ )
538
+
539
+
540
+ ###############################################################################
541
+ # LLM Model Setup
542
+ ###############################################################################
543
+
544
+ TEMPERATURE = 0.5
545
+ # model = ChatOpenAI(
546
+ # model="unsloth/llama-3-8b-Instruct-bnb-4bit",
547
+ # temperature=TEMPERATURE,
548
+ # timeout=None,
549
+ # max_retries=2,
550
+ # api_key="not_required",
551
+ # base_url="http://localhost:8000/v1", # Use the VLLM instance URL
552
+ # verbose=True
553
+ # )
554
+
555
+ model = ChatGroq(
556
+ model_name="deepseek-r1-distill-llama-70b",
557
+ temperature=TEMPERATURE,
558
+ api_key=GROQ_API_KEY,
559
+ verbose=True
560
+ )
561
+
562
+ ###############################################################################
563
+ # 1. Initialize memory + config
564
+ ###############################################################################
565
+ in_memory_store = InMemoryStore(
566
+ index={
567
+ "embed": init_embeddings("huggingface:sentence-transformers/all-MiniLM-L6-v2"),
568
+ "dims": 384, # Embedding dimensions
569
+ }
570
+ )
571
+
572
+ # A memory saver to checkpoint conversation states
573
+ checkpointer = MemorySaver()
574
+
575
+ # Initialize config with user & thread info
576
+ config = {}
577
+ config["configurable"] = {
578
+ "user_id": "user_1",
579
+ "thread_id": 0,
580
+ }
581
+
582
+ ###############################################################################
583
+ # 2. Define MessagesState
584
+ ###############################################################################
585
+ class MessagesState(TypedDict):
586
+ """The state of the agent.
587
+
588
+ The key 'messages' uses add_messages as a reducer,
589
+ so each time this state is updated, new messages are appended.
590
+ # See https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers
591
+ """
592
+ messages: Annotated[Sequence[BaseMessage], add_messages]
593
+
594
+
595
+ ###############################################################################
596
+ # 3. Memory Tools
597
+ ###############################################################################
598
+ def save_memory(summary_text: str, *, config: RunnableConfig, store: BaseStore) -> str:
599
+ """Save the given memory for the current user and return the key."""
600
+ user_id = config.get("configurable", {}).get("user_id")
601
+ thread_id = config.get("configurable", {}).get("thread_id")
602
+ namespace = (user_id, "memories")
603
+ memory_id = thread_id
604
+ store.put(namespace, memory_id, {"memory": summary_text})
605
+ return f"Saved to memory key: {memory_id}"
606
+
607
+ def update_memory(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
608
+ # Extract the messages list from the event, handling potential missing key
609
+ messages = state["messages"]
610
+ # Convert LangChain messages to dictionaries before storing
611
+ messages_dict = [{"role": msg.type, "content": msg.content} for msg in messages]
612
+
613
+ # Get the user id from the config
614
+ user_id = config.get("configurable", {}).get("user_id")
615
+ thread_id = config.get("configurable", {}).get("thread_id")
616
+ # Namespace the memory
617
+ namespace = (user_id, "memories")
618
+ # Create a new memory ID
619
+ memory_id = f"{thread_id}"
620
+ store.put(namespace, memory_id, {"memory": messages_dict})
621
+ return f"Saved to memory key: {memory_id}"
622
+
623
+
624
+ # Define a Pydantic schema for the save_memory tool (if needed elsewhere)
625
+ # https://langchain-ai.github.io/langgraphjs/reference/classes/checkpoint.InMemoryStore.html
626
+ class RecallMemory(BaseModel):
627
+ query_text: str = Field(..., title="Search Text", description="The text to search from memories for similar records.")
628
+ k: int = Field(5, title="Number of Results", description="Number of results to retrieve.")
629
+
630
+ def recall_memory(query_text: str, k: int = 5) -> str:
631
+ """Retrieve user memories from in_memory_store."""
632
+ user_id = config.get("configurable", {}).get("user_id")
633
+ memories = [
634
+ m.value["memory"] for m in in_memory_store.search((user_id, "memories"), query=query_text, limit=k)
635
+ if "memory" in m.value
636
+ ]
637
+ return f"User memories: {memories}"
638
+
639
+ # Create a StructuredTool from the function
640
+ recall_memory_tool = StructuredTool.from_function(
641
+ func=recall_memory,
642
+ name="Recall Memory Tool",
643
+ description="""
644
+ Retrieve memories relevant to the user's query.
645
+ """,
646
+ args_schema=RecallMemory,
647
+ response_format="content",
648
+ return_direct=False,
649
+ verbose=False
650
+ )
651
+
652
+ ###############################################################################
653
+ # 4. Summarize Node (using StructuredTool)
654
+ ###############################################################################
655
+ # Define a Pydantic schema for the Summary tool
656
+ class SummariseConversation(BaseModel):
657
+ summary_text: str = Field(..., title="text", description="Write a summary of entire conversation here")
658
+
659
+ def summarise_node(summary_text: str):
660
+ """
661
+ Final node that summarizes the entire conversation for the current thread,
662
+ saves it in memory, increments the thread_id, and ends the conversation.
663
+ Returns a confirmation string.
664
+ """
665
+ user_id = config["configurable"]["user_id"]
666
+ current_thread_id = config["configurable"]["thread_id"]
667
+ new_thread_id = str(int(current_thread_id) + 1)
668
+
669
+ # Prepare configuration for saving memory with updated thread id
670
+ config_for_saving = {
671
+ "configurable": {
672
+ "user_id": user_id,
673
+ "thread_id": new_thread_id
674
+ }
675
+ }
676
+ key = save_memory(summary_text, config=config_for_saving, store=in_memory_store)
677
+ #return f"Summary saved under key: {key}"
678
+
679
+ # Create a StructuredTool from the function (this wraps summarise_node)
680
+ summarise_tool = StructuredTool.from_function(
681
+ func=summarise_node,
682
+ name="Summary Tool",
683
+ description="""
684
+ Summarize the current conversation in no more than
685
+ 1000 words. Also retain any unanswered quiz questions along with
686
+ your internal answers so the next conversation thread can continue.
687
+ Do not reveal solutions to the user yet. Use this tool to save
688
+ the current conversation to memory and then end the conversation.
689
+ """,
690
+ args_schema=SummariseConversation,
691
+ response_format="content",
692
+ return_direct=False,
693
+ verbose=True
694
+ )
695
+
696
+ def call_summary(state: MessagesState, config: RunnableConfig):
697
+ # Convert message dicts to HumanMessage instances if needed.
698
+ system_message="""
699
+ Summarize the current conversation in no more than
700
+ 1000 words. Also retain any unanswered quiz questions along with
701
+ your internal answers.
702
+ """
703
+ messages = []
704
+ for m in state["messages"]:
705
+ if isinstance(m, dict):
706
+ # Use role from dict (defaulting to 'user' if missing)
707
+ messages.append(AIMessage(content=system_message, role=m.get("role", "assistant")))
708
+ else:
709
+ messages.append(m)
710
+
711
+ summaries = llm_with_tools.invoke(messages)
712
+
713
+ summary_content = summaries.content
714
+
715
+ # Call Tool Manually
716
+ message_with_single_tool_call = AIMessage(
717
+ content="",
718
+ tool_calls=[
719
+ {
720
+ "name": "Summary Tool",
721
+ "args": {"summary_text": summary_content},
722
+ "id": "tool_call_id",
723
+ "type": "tool_call",
724
+ }
725
+ ],
726
+ )
727
+
728
+ tool_node.invoke({"messages": [message_with_single_tool_call]})
729
+
730
+
731
+ ###############################################################################
732
+ # 5. Build the Graph
733
+ ###############################################################################
734
+ graph_builder = StateGraph(MessagesState)
735
+
736
+ # Use the built-in ToolNode from langgraph that calls any declared tools.
737
+ tools = [
738
+ mcq_retriever_tool,
739
+ web_extraction_tool,
740
+ ensemble_retriever_tool,
741
+ general_retriever_tool,
742
+ in_memory_retriever_tool,
743
+ recall_memory_tool,
744
+ summarise_tool,
745
+ ]
746
+
747
+ tool_node = ToolNode(tools=tools)
748
+ #end_node = ToolNode(tools=[summarise_tool])
749
+
750
+ # Wrap your model with tools
751
+ llm_with_tools = model.bind_tools(tools)
752
+
753
+ ###############################################################################
754
+ # 6. The agent's main node: call_model
755
+ ###############################################################################
756
+ def call_model(state: MessagesState, config: RunnableConfig):
757
+ """
758
+ The main agent node that calls the LLM with the user + system messages.
759
+ Since our vLLM chat wrapper expects a list of BaseMessage objects,
760
+ we convert any dict messages to HumanMessage objects.
761
+ If the LLM requests a tool call, we'll route to the 'tools' node next
762
+ (depending on the condition).
763
+ """
764
+ # Convert message dicts to HumanMessage instances if needed.
765
+ messages = []
766
+ for m in state["messages"]:
767
+ if isinstance(m, dict):
768
+ # Use role from dict (defaulting to 'user' if missing)
769
+ messages.append(HumanMessage(content=m.get("content", ""), role=m.get("role", "user")))
770
+ else:
771
+ messages.append(m)
772
+
773
+ # Invoke the LLM (with tools) using the converted messages.
774
+ response = llm_with_tools.invoke(messages)
775
+
776
+ return {"messages": [response]}
777
+
778
+
779
+
780
+ def call_summary(state: MessagesState, config: RunnableConfig):
781
+ # Convert message dicts to HumanMessage instances if needed.
782
+ system_message="""
783
+ Summarize the current conversation in no more than
784
+ 1000 words. Also retain any unanswered quiz questions along with
785
+ your internal answers.
786
+ """
787
+ messages = []
788
+ for m in state["messages"]:
789
+ if isinstance(m, dict):
790
+ # Use role from dict (defaulting to 'user' if missing)
791
+ messages.append(AIMessage(content=system_message, role=m.get("role", "assistant")))
792
+ else:
793
+ messages.append(m)
794
+
795
+ summaries = llm_with_tools.invoke(messages)
796
+
797
+ summary_content = summaries.content
798
+
799
+ # Call Tool Manually
800
+ message_with_single_tool_call = AIMessage(
801
+ content="",
802
+ tool_calls=[
803
+ {
804
+ "name": "Summary Tool",
805
+ "args": {"summary_text": summary_content},
806
+ "id": "tool_call_id",
807
+ "type": "tool_call",
808
+ }
809
+ ],
810
+ )
811
+
812
+ tool_node.invoke({"messages": [message_with_single_tool_call]})
813
+
814
+ ###############################################################################
815
+ # 7. Add Nodes & Edges, Then Compile
816
+ ###############################################################################
817
+ graph_builder.add_node("agent", call_model)
818
+ graph_builder.add_node("tools", tool_node)
819
+ #graph_builder.add_node("summary", call_summary)
820
+
821
+ # Entry point
822
+ graph_builder.set_entry_point("agent")
823
+
824
+ # def custom_tools_condition(llm_output: dict) -> str:
825
+ # """Return which node to go to next based on the LLM output."""
826
+
827
+ # # The LLM's JSON might have a field like {"name": "Recall Memory Tool", "arguments": {...}}.
828
+ # tool_name = llm_output.get("name", None)
829
+
830
+ # # If the LLM calls "Summary Tool", jump directly to the 'summary' node
831
+ # if tool_name == "Summary Tool":
832
+ # return "summary"
833
+
834
+ # # If the LLM calls any other recognized tool, go to 'tools'
835
+ # valid_tool_names = [t.name for t in tools] # all tools in the main tool_node
836
+ # if tool_name in valid_tool_names:
837
+ # return "tools"
838
+
839
+ # # If there's no recognized tool name, assume we're done => go to summary
840
+ # return "__end__"
841
+
842
+ # graph_builder.add_conditional_edges(
843
+ # "agent",
844
+ # custom_tools_condition,
845
+ # {
846
+ # "tools": "tools",
847
+ # "summary": "summary",
848
+ # "__end__": "summary",
849
+ # }
850
+ # )
851
+
852
+ # If LLM requests a tool, go to "tools", otherwise go to "summary"
853
+ graph_builder.add_conditional_edges("agent", tools_condition)
854
+ #graph_builder.add_conditional_edges("agent", tools_condition, {"tools": "tools", "__end__": "summary"})
855
+ #graph_builder.add_conditional_edges("agent", lambda llm_output: "tools" if llm_output.get("name", None) in [t.name for t in tools] else "summary", {"tools": "tools", "__end__": "summary"}
856
+
857
+ # If we used a tool, return to the agent for final answer or more tools
858
+ graph_builder.add_edge("tools", "agent")
859
+ #graph_builder.add_edge("agent", "summary")
860
+ #graph_builder.set_finish_point("summary")
861
+
862
+ # Compile the graph with checkpointing and persistent store
863
+ graph = graph_builder.compile(checkpointer=checkpointer, store=in_memory_store)
864
+
865
+ #from langgraph.prebuilt import create_react_agent
866
+ #graph = create_react_agent(llm_with_tools, tools=tool_node, checkpointer=checkpointer, store=in_memory_store)
867
+
868
+ #from IPython.display import Image, display
869
+ #display(Image(graph.get_graph().draw_mermaid_png()))
870
+
871
+
872
+ ########################################
873
+ # Gradio Chatbot Application
874
+ ########################################
875
+
876
+ import gradio as gr
877
+ from gradio import ChatMessage
878
+
879
+ system_prompt = "You are a helpful Assistant. Always use the tools {tools}."
880
+
881
+ ########################################
882
+ # Upload_documents
883
+ ########################################
884
+
885
+ def upload_documents(file_list: List):
886
+ """
887
+ Load documents into in-memory vector store.
888
+ """
889
+ _documents = []
890
+
891
+ for doc_path in file_list:
892
+ _documents.extend(load_file(doc_path))
893
+
894
+ # Split the documents into smaller chunks for better embedding coverage
895
+ splitter = RecursiveCharacterTextSplitter(
896
+ chunk_size=300, # Split into chunks of 512 characters
897
+ chunk_overlap=50, # Overlap by 50 characters
898
+ add_start_index=True
899
+ )
900
+ chunked_texts = splitter.split_documents(_documents)
901
+ in_memory_vs.add_documents(chunked_texts)
902
+ return f"Uploaded {len(file_list)} documents into in-memory vector store."
903
+
904
+
905
+ ########################################
906
+ # Submit_queries (ChatInterface Function)
907
+ ########################################
908
+ def submit_queries(message, _messages):
909
+ """
910
+ - message: dict with {"text": ..., "files": [...]}
911
+ - history: list of ChatMessage
912
+ """
913
+ _messages=[]
914
+ user_text = message.get("text", "")
915
+ user_files = message.get("files", [])
916
+
917
+ # Process user-uploaded files
918
+ if user_files:
919
+ for file_obj in user_files:
920
+ _messages.append(ChatMessage(role="user", content=f"Uploaded file: {file_obj}"))
921
+ upload_response = upload_documents(user_files)
922
+ _messages.append(ChatMessage(role="assistant", content=upload_response))
923
+ yield _messages
924
+ return # Exit early since we don't need to process text or call the LLM
925
+
926
+ # Append user text if present
927
+ if user_text:
928
+ events = graph.stream(
929
+ {
930
+ "messages": [
931
+ {"role": "system", "content": system_prompt},
932
+ {"role": "user", "content": user_text},
933
+ ]
934
+ },
935
+ config,
936
+ stream_mode="values"
937
+ )
938
+
939
+ for event in events:
940
+ response = event["messages"][-1]
941
+ if isinstance(response, AIMessage):
942
+ if "tool_calls" in response.additional_kwargs:
943
+ _messages.append(
944
+ ChatMessage(role="assistant",
945
+ content=str(response.tool_calls[0]["args"]),
946
+ metadata={"title": str(response.tool_calls[0]["name"]),
947
+ "id": config["configurable"]["thread_id"]
948
+ }
949
+ ))
950
+ yield _messages
951
+ else:
952
+ _messages.append(ChatMessage(role="assistant",
953
+ content=response.content,
954
+ metadata={"id": config["configurable"]["thread_id"]
955
+ }
956
+ ))
957
+ yield _messages
958
+ return _messages
959
+
960
+
961
+
962
+
963
+ ########################################
964
+ # 3) Save Chat History
965
+ ########################################
966
+ CHAT_HISTORY_FILE = "chat_history.json"
967
+
968
+ def save_chat_history(history):
969
+ """
970
+ Saves the chat history into a JSON file.
971
+ """
972
+ session_history = [
973
+ {
974
+ "role": "user" if msg.is_user else "assistant",
975
+ "content": msg.content
976
+ }
977
+ for msg in history
978
+ ]
979
+ with open(CHAT_HISTORY_FILE, "w", encoding="utf-8") as f:
980
+ json.dump(session_history, f, ensure_ascii=False, indent=4)
981
+
982
+
983
+ ########################################
984
+ # 6) Main Gradio Interface
985
+ ########################################
986
+ with gr.Blocks(theme="ocean") as AI_Tutor:
987
+ gr.Markdown("# AI Tutor Chatbot (Gradio App)")
988
+
989
+ # Primary Chat Interface
990
+ chat_interface = gr.ChatInterface(
991
+ fn=submit_queries,
992
+ type="messages",
993
+ chatbot=gr.Chatbot(
994
+ label="Chat Window",
995
+ height=500
996
+ ),
997
+ textbox=gr.MultimodalTextbox(
998
+ file_count="multiple",
999
+ file_types=None,
1000
+ sources="upload",
1001
+ label="Type your query here:",
1002
+ placeholder="Enter your question...",
1003
+ ),
1004
+ title="AI Tutor Chatbot",
1005
+ description="Ask me anything about Artificial Intelligence!",
1006
+ multimodal=True,
1007
+ save_history=True,
1008
+ )
1009
+
1010
+
1011
+ if __name__ == "__main__":
1012
+ AI_Tutor.launch(server_name="0.0.0.0", server_port=7860)
examples/tool_chat_template_llama3.1_json.jinja ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{- bos_token }}
2
+ {%- if custom_tools is defined %}
3
+ {%- set tools = custom_tools %}
4
+ {%- endif %}
5
+ {%- if not tools_in_user_message is defined %}
6
+ {#- Llama 3.1 doesn't pass all tests if the tools are in the system prompt #}
7
+ {%- set tools_in_user_message = true %}
8
+ {%- endif %}
9
+ {%- if not date_string is defined %}
10
+ {%- if strftime_now is defined %}
11
+ {%- set date_string = strftime_now("%d %b %Y") %}
12
+ {%- else %}
13
+ {%- set date_string = "26 Jul 2024" %}
14
+ {%- endif %}
15
+ {%- endif %}
16
+ {%- if not tools is defined %}
17
+ {%- set tools = none %}
18
+ {%- endif %}
19
+
20
+ {#- This block extracts the system message, so we can slot it into the right place. #}
21
+ {%- if messages[0]['role'] == 'system' %}
22
+ {%- if messages[0]['content'] is string %}
23
+ {%- set system_message = messages[0]['content']|trim %}
24
+ {%- else %}
25
+ {%- set system_message = messages[0]['content'][0]['text']|trim %}
26
+ {%- endif %}
27
+ {%- set messages = messages[1:] %}
28
+ {%- else %}
29
+ {%- if tools is not none %}
30
+ {%- set system_message = "You are a helpful assistant with tool calling capabilities. Only reply with a tool call if the function exists in the library provided by the user. If it doesn't exist, just reply directly in natural language. When you receive a tool call response, use the output to format an answer to the original user question." %}
31
+ {%- else %}
32
+ {%- set system_message = "" %}
33
+ {%- endif %}
34
+ {%- endif %}
35
+
36
+ {#- System message #}
37
+ {{- "<|start_header_id|>system<|end_header_id|>\n\n" }}
38
+ {%- if tools is not none %}
39
+ {{- "Environment: ipython\n" }}
40
+ {%- endif %}
41
+ {{- "Cutting Knowledge Date: December 2023\n" }}
42
+ {{- "Today Date: " + date_string + "\n\n" }}
43
+ {%- if tools is not none and not tools_in_user_message %}
44
+ {{- "You have access to the following functions. To call a function, please respond with JSON for a function call. " }}
45
+ {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}. ' }}
46
+ {{- "Do not use variables.\n\n" }}
47
+ {%- for t in tools %}
48
+ {{- t | tojson(indent=4) }}
49
+ {{- "\n\n" }}
50
+ {%- endfor %}
51
+ {%- endif %}
52
+ {{- system_message }}
53
+ {{- "<|eot_id|>" }}
54
+
55
+ {#- Custom tools are passed in a user message with some extra guidance #}
56
+ {%- if tools_in_user_message and not tools is none %}
57
+ {#- Extract the first user message so we can plug it in here #}
58
+ {%- if messages | length != 0 %}
59
+ {%- if messages[0]['content'] is string %}
60
+ {%- set first_user_message = messages[0]['content']|trim %}
61
+ {%- else %}
62
+ {%- set first_user_message = messages[0]['content'] | selectattr('type', 'equalto', 'text') | map(attribute='text') | map('trim') | join('\n') %}
63
+ {%- endif %}
64
+ {%- set messages = messages[1:] %}
65
+ {%- else %}
66
+ {{- raise_exception("Cannot put tools in the first user message when there's no first user message!") }}
67
+ {%- endif %}
68
+ {{- '<|start_header_id|>user<|end_header_id|>\n\n' -}}
69
+ {{- "Given the following functions, please respond with a JSON for a function call " }}
70
+ {{- "with its proper arguments that best answers the given prompt.\n\n" }}
71
+ {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}. ' }}
72
+ {{- "Do not use variables.\n\n" }}
73
+ {%- for t in tools %}
74
+ {{- t | tojson(indent=4) }}
75
+ {{- "\n\n" }}
76
+ {%- endfor %}
77
+ {{- first_user_message + "<|eot_id|>"}}
78
+ {%- endif %}
79
+
80
+ {%- for message in messages %}
81
+ {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %}
82
+ {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' }}
83
+ {%- if message['content'] is string %}
84
+ {{- message['content'] | trim}}
85
+ {%- else %}
86
+ {%- for content in message['content'] %}
87
+ {%- if content['type'] == 'text' %}
88
+ {{- content['text'] | trim }}
89
+ {%- endif %}
90
+ {%- endfor %}
91
+ {%- endif %}
92
+ {{- '<|eot_id|>' }}
93
+ {%- elif 'tool_calls' in message %}
94
+ {%- if not message.tool_calls|length == 1 %}
95
+ {{- raise_exception("This model only supports single tool-calls at once!") }}
96
+ {%- endif %}
97
+ {%- set tool_call = message.tool_calls[0].function %}
98
+ {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' -}}
99
+ {{- '{"name": "' + tool_call.name + '", ' }}
100
+ {{- '"parameters": ' }}
101
+ {{- tool_call.arguments | tojson }}
102
+ {{- "}" }}
103
+ {{- "<|eot_id|>" }}
104
+ {%- elif message.role == "tool" or message.role == "ipython" %}
105
+ {{- "<|start_header_id|>ipython<|end_header_id|>\n\n" }}
106
+ {%- if message.content is string %}
107
+ {{- { "output": message.content } | tojson }}
108
+ {%- else %}
109
+ {%- for content in message['content'] %}
110
+ {%- if content['type'] == 'text' %}
111
+ {{- { "output": content['text'] } | tojson }}
112
+ {%- endif %}
113
+ {%- endfor %}
114
+ {%- endif %}
115
+ {{- "<|eot_id|>" }}
116
+ {%- endif %}
117
+ {%- endfor %}
118
+ {%- if add_generation_prompt %}
119
+ {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }}
120
+ {%- endif %}
general_db/0ab351f4-2d03-423f-bbf2-093d3b8eba80/data_level0.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d3c9fd302f000d7790aa403c2d0d8fec363fe46f30b07d53020b6e33b22435a9
3
+ size 1676000
general_db/0ab351f4-2d03-423f-bbf2-093d3b8eba80/header.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e87a1dc8bcae6f2c4bea6d5dd5005454d4dace8637dae29bff3c037ea771411e
3
+ size 100
general_db/0ab351f4-2d03-423f-bbf2-093d3b8eba80/length.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fc19b1997119425765295aeab72d76faa6927d4f83985d328c26f20468d6cc76
3
+ size 4000
general_db/0ab351f4-2d03-423f-bbf2-093d3b8eba80/link_lists.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
3
+ size 0
general_db/chroma.sqlite3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:543300e2fd260eefddb8b47fafb95314b9dca6b7c565eda394a15a551c42091b
3
+ size 5664768
mcq_db/chroma.sqlite3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:046f91c882d2eb2e61d8001824f85d287f675f2064ef296425c885e3ed129767
3
+ size 2412544
mcq_db/d897d22c-91fc-4db5-8a6b-aacbd9c5f57d/data_level0.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:226bd35bb7473e1124546d05e350139f64c2e50e79938f62af59b5d724168095
3
+ size 1676000
mcq_db/d897d22c-91fc-4db5-8a6b-aacbd9c5f57d/header.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e87a1dc8bcae6f2c4bea6d5dd5005454d4dace8637dae29bff3c037ea771411e
3
+ size 100
mcq_db/d897d22c-91fc-4db5-8a6b-aacbd9c5f57d/length.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d77fefa79382cc0bd9a9284e6890c944e7298a6adc9b9dd7521783833fbc237c
3
+ size 4000
mcq_db/d897d22c-91fc-4db5-8a6b-aacbd9c5f57d/link_lists.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
3
+ size 0
requirements.txt ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ requests
3
+ langchain-groq
4
+ langchain-openai
5
+ torch
6
+ vllm
7
+ accelerate
8
+ bitsandbytes
9
+
10
+ # LangChain and related dependencies
11
+ langchain
12
+ langchain-core
13
+ langchain-text-splitters
14
+ langchain-community
15
+ langgraph
16
+ chromadb
17
+ langchain-chroma
18
+ #langsmith
19
+
20
+ # Document processing
21
+ docling
22
+ langchain-docling
23
+
24
+ # Local LLM and other utilities
25
+ #llama-cpp-python
26
+ langchain_huggingface
27
+ transformers
28
+ sentence_transformers
29
+ huggingface_hub
30
+