Spaces:
Sleeping
Sleeping
initial commit
Browse files- .gitignore +3 -0
- README.md +18 -0
- __pycache__/chroma_utils.cpython-311.pyc +0 -0
- __pycache__/db_utils.cpython-311.pyc +0 -0
- __pycache__/langchain_utils.cpython-311.pyc +0 -0
- __pycache__/main.cpython-311.pyc +0 -0
- __pycache__/pinecone_utilis.cpython-311.pyc +0 -0
- __pycache__/pydantic_models.cpython-311.pyc +0 -0
- app.py +5 -0
- backend/main.py +0 -0
- backend/utilis.py +57 -0
- db_utils.py +75 -0
- langchain_utils.py +43 -0
- main.py +76 -0
- pinecone_utilis.py +84 -0
- prompt_templates.py +64 -0
- pydantic_models.py +25 -0
- requirements.txt +10 -0
- ui.py +61 -0
.gitignore
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
venv
|
2 |
+
InternTaskGenAI.pdf
|
3 |
+
.env
|
README.md
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# smart-research-assistant
|
2 |
+
|
3 |
+
## TODO:
|
4 |
+
- [] pinecone utilis
|
5 |
+
- [] RAG component using langchain/langgraph
|
6 |
+
- [] database setup
|
7 |
+
- [] backend using FastAPI
|
8 |
+
- [] frontend using streamlit
|
9 |
+
## Deliverabilities
|
10 |
+
- Answer questions that require comprehension and inference
|
11 |
+
- Pose logic-based questions to users and evaluate their responses
|
12 |
+
- Justify every answer with a reference from the document
|
13 |
+
### Functional Requirements
|
14 |
+
- Input file (pdf/txt)
|
15 |
+
- 2 modes (a) Ask anything (b) challenge me
|
16 |
+
- Auto summary after document upload
|
17 |
+
- Streamlit + FastAPI (current stack)
|
18 |
+
- Bonus features i.e. state management and context highlighting
|
__pycache__/chroma_utils.cpython-311.pyc
ADDED
Binary file (3.46 kB). View file
|
|
__pycache__/db_utils.cpython-311.pyc
ADDED
Binary file (4.78 kB). View file
|
|
__pycache__/langchain_utils.cpython-311.pyc
ADDED
Binary file (2.51 kB). View file
|
|
__pycache__/main.cpython-311.pyc
ADDED
Binary file (5.47 kB). View file
|
|
__pycache__/pinecone_utilis.cpython-311.pyc
ADDED
Binary file (4.81 kB). View file
|
|
__pycache__/pydantic_models.cpython-311.pyc
ADDED
Binary file (1.99 kB). View file
|
|
app.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pinecone_utilis import create_pinecone_vectorstore,load_and_split_document, index_document_to_pinecone
|
2 |
+
|
3 |
+
file_path="InternTaskGenAI.pdf"
|
4 |
+
|
5 |
+
print(index_document_to_pinecone(file_path=file_path, file_id=1))
|
backend/main.py
ADDED
File without changes
|
backend/utilis.py
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langchain.document_loaders import PyPDFLoader
|
2 |
+
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
3 |
+
from pinecone import Pinecone, ServerlessSpec
|
4 |
+
from langchain_core.prompts import PromptTemplate
|
5 |
+
from langchain_core.output_parsers import StrOutputParser
|
6 |
+
from operator import itemgetter
|
7 |
+
|
8 |
+
class RAG:
|
9 |
+
def load_split_file(self, file_path):
|
10 |
+
loader = PyPDFLoader(file_path)
|
11 |
+
pages = loader.load_and_split()
|
12 |
+
text_splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=10)
|
13 |
+
docs = text_splitter.split_documents(pages)
|
14 |
+
return docs
|
15 |
+
|
16 |
+
def create_index(self, index_name, PINECONE_API_KEY):
|
17 |
+
pc = Pinecone(api_key=PINECONE_API_KEY)
|
18 |
+
if index_name in pc.list_indexes().names():
|
19 |
+
pc.delete_index(index_name) # To avoid any conflicts in retrieval
|
20 |
+
pc.create_index(
|
21 |
+
name=index_name,
|
22 |
+
dimension=384,
|
23 |
+
metric='cosine',
|
24 |
+
spec=ServerlessSpec(
|
25 |
+
cloud="aws",
|
26 |
+
region="us-east-1"
|
27 |
+
)
|
28 |
+
)
|
29 |
+
return index_name
|
30 |
+
|
31 |
+
def final_response(self, index, question, model):
|
32 |
+
retriever = index.as_retriever()
|
33 |
+
parser = StrOutputParser()
|
34 |
+
template = """
|
35 |
+
You must provide an answer based strictly on the context below.
|
36 |
+
The answer is highly likely to be found within the given context, so analyze it thoroughly before responding.
|
37 |
+
Only if there is absolutely no relevant information, respond with "I don't know".
|
38 |
+
Do not make things up.
|
39 |
+
|
40 |
+
Context: {context}
|
41 |
+
|
42 |
+
Question: {question}
|
43 |
+
"""
|
44 |
+
prompt = PromptTemplate.from_template(template)
|
45 |
+
prompt.format(context="Here is some context", question="Here is a question")
|
46 |
+
|
47 |
+
chain = (
|
48 |
+
{
|
49 |
+
"context": itemgetter("question") | retriever,
|
50 |
+
"question": itemgetter("question"),
|
51 |
+
}
|
52 |
+
| prompt
|
53 |
+
| model
|
54 |
+
| parser
|
55 |
+
)
|
56 |
+
matching_results = index.similarity_search(question, k=2)
|
57 |
+
return f"Answer: {chain.invoke({'question': question})}", matching_results
|
db_utils.py
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sqlite3
|
2 |
+
from datetime import datetime
|
3 |
+
|
4 |
+
DB_NAME = "rag_app.db"
|
5 |
+
|
6 |
+
def get_db_connection():
|
7 |
+
conn = sqlite3.connect(DB_NAME)
|
8 |
+
conn.row_factory = sqlite3.Row
|
9 |
+
return conn
|
10 |
+
|
11 |
+
def create_application_logs():
|
12 |
+
conn = get_db_connection()
|
13 |
+
conn.execute('''CREATE TABLE IF NOT EXISTS application_logs
|
14 |
+
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
15 |
+
session_id TEXT,
|
16 |
+
user_query TEXT,
|
17 |
+
gpt_response TEXT,
|
18 |
+
model TEXT,
|
19 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
|
20 |
+
conn.close()
|
21 |
+
|
22 |
+
def create_document_store():
|
23 |
+
conn = get_db_connection()
|
24 |
+
conn.execute('''CREATE TABLE IF NOT EXISTS document_store
|
25 |
+
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
26 |
+
filename TEXT,
|
27 |
+
upload_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
|
28 |
+
conn.close()
|
29 |
+
|
30 |
+
def insert_application_logs(session_id, user_query, gpt_response, model):
|
31 |
+
conn = get_db_connection()
|
32 |
+
conn.execute('INSERT INTO application_logs (session_id, user_query, gpt_response, model) VALUES (?, ?, ?, ?)',
|
33 |
+
(session_id, user_query, gpt_response, model))
|
34 |
+
conn.commit()
|
35 |
+
conn.close()
|
36 |
+
|
37 |
+
def get_chat_history(session_id):
|
38 |
+
conn = get_db_connection()
|
39 |
+
cursor = conn.cursor()
|
40 |
+
cursor.execute('SELECT user_query, gpt_response FROM application_logs WHERE session_id = ? ORDER BY created_at', (session_id,))
|
41 |
+
messages = []
|
42 |
+
for row in cursor.fetchall():
|
43 |
+
messages.extend([
|
44 |
+
{"role": "human", "content": row['user_query']},
|
45 |
+
{"role": "ai", "content": row['gpt_response']}
|
46 |
+
])
|
47 |
+
conn.close()
|
48 |
+
return messages
|
49 |
+
def insert_document_record(filename):
|
50 |
+
conn = get_db_connection()
|
51 |
+
cursor = conn.cursor()
|
52 |
+
cursor.execute('INSERT INTO document_store (filename) VALUES (?)', (filename,))
|
53 |
+
file_id = cursor.lastrowid
|
54 |
+
conn.commit()
|
55 |
+
conn.close()
|
56 |
+
return file_id
|
57 |
+
|
58 |
+
def delete_document_record(file_id):
|
59 |
+
conn = get_db_connection()
|
60 |
+
conn.execute('DELETE FROM document_store WHERE id = ?', (file_id,))
|
61 |
+
conn.commit()
|
62 |
+
conn.close()
|
63 |
+
return True
|
64 |
+
|
65 |
+
def get_all_documents():
|
66 |
+
conn = get_db_connection()
|
67 |
+
cursor = conn.cursor()
|
68 |
+
cursor.execute('SELECT id, filename, upload_timestamp FROM document_store ORDER BY upload_timestamp DESC')
|
69 |
+
documents = cursor.fetchall()
|
70 |
+
conn.close()
|
71 |
+
return [dict(doc) for doc in documents]
|
72 |
+
|
73 |
+
# Initialize the database tables
|
74 |
+
create_application_logs()
|
75 |
+
create_document_store()
|
langchain_utils.py
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langchain_openai import ChatOpenAI
|
2 |
+
from langchain_core.output_parsers import StrOutputParser
|
3 |
+
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
4 |
+
from langchain.chains import create_history_aware_retriever, create_retrieval_chain
|
5 |
+
from langchain.chains.combine_documents import create_stuff_documents_chain
|
6 |
+
from typing import List
|
7 |
+
from typing_extensions import List, TypedDict
|
8 |
+
from langchain_core.documents import Document
|
9 |
+
import os
|
10 |
+
from pinecone_utilis import vectorstore
|
11 |
+
from dotenv import load_dotenv
|
12 |
+
load_dotenv()
|
13 |
+
OPENAI_API_KEY=os.getenv("OPENAI_API_KEY")
|
14 |
+
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
|
15 |
+
|
16 |
+
output_parser = StrOutputParser()
|
17 |
+
|
18 |
+
contextualize_q_system_prompt = (
|
19 |
+
"Given a chat history and the latest user question "
|
20 |
+
"which might reference context in the chat history, "
|
21 |
+
"formulate a standalone question which can be understood "
|
22 |
+
"without the chat history. Do NOT answer the question, "
|
23 |
+
"just reformulate it if needed and otherwise return it as is."
|
24 |
+
)
|
25 |
+
|
26 |
+
contextualize_q_prompt = ChatPromptTemplate.from_messages([
|
27 |
+
("system", contextualize_q_system_prompt),
|
28 |
+
MessagesPlaceholder("chat_history"),
|
29 |
+
("human", "{input}"),
|
30 |
+
])
|
31 |
+
|
32 |
+
qa_prompt = ChatPromptTemplate.from_messages([
|
33 |
+
("system", "You are a helpful AI assistant. Use the following context to answer the user's question."),
|
34 |
+
("system", "Context: {context}"),
|
35 |
+
MessagesPlaceholder(variable_name="chat_history"),
|
36 |
+
("human", "{input}")
|
37 |
+
])
|
38 |
+
|
39 |
+
class State(TypedDict):
|
40 |
+
messages: List[BaseMessage]
|
41 |
+
|
42 |
+
|
43 |
+
|
main.py
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException
|
2 |
+
from pydantic_models import QueryInput, QueryResponse, DocumentInfo, DeleteFileRequest
|
3 |
+
from langchain_utils import get_rag_chain
|
4 |
+
from db_utils import insert_application_logs, get_chat_history, get_all_documents, insert_document_record, delete_document_record
|
5 |
+
from chroma_utils import index_document_to_chroma, delete_doc_from_chroma
|
6 |
+
import os
|
7 |
+
import uuid
|
8 |
+
import logging
|
9 |
+
import shutil
|
10 |
+
|
11 |
+
# Set up logging
|
12 |
+
logging.basicConfig(filename='app.log', level=logging.INFO)
|
13 |
+
|
14 |
+
# Initialize FastAPI app
|
15 |
+
app = FastAPI()
|
16 |
+
|
17 |
+
@app.post("/chat", response_model=QueryResponse)
|
18 |
+
def chat(query_input: QueryInput):
|
19 |
+
session_id = query_input.session_id or str(uuid.uuid4())
|
20 |
+
logging.info(f"Session ID: {session_id}, User Query: {query_input.question}, Model: {query_input.model.value}")
|
21 |
+
|
22 |
+
chat_history = get_chat_history(session_id)
|
23 |
+
rag_chain = get_rag_chain(query_input.model.value)
|
24 |
+
answer = rag_chain.invoke({
|
25 |
+
"input": query_input.question,
|
26 |
+
"chat_history": chat_history
|
27 |
+
})['answer']
|
28 |
+
|
29 |
+
insert_application_logs(session_id, query_input.question, answer, query_input.model.value)
|
30 |
+
logging.info(f"Session ID: {session_id}, AI Response: {answer}")
|
31 |
+
return QueryResponse(answer=answer, session_id=session_id, model=query_input.model)
|
32 |
+
|
33 |
+
|
34 |
+
@app.post("/upload-doc")
|
35 |
+
def upload_and_index_document(file: UploadFile = File(...)):
|
36 |
+
allowed_extensions = ['.pdf', '.docx', '.html']
|
37 |
+
file_extension = os.path.splitext(file.filename)[1].lower()
|
38 |
+
|
39 |
+
if file_extension not in allowed_extensions:
|
40 |
+
raise HTTPException(status_code=400, detail=f"Unsupported file type. Allowed types are: {', '.join(allowed_extensions)}")
|
41 |
+
|
42 |
+
temp_file_path = f"temp_{file.filename}"
|
43 |
+
|
44 |
+
try:
|
45 |
+
# Save the uploaded file to a temporary file
|
46 |
+
with open(temp_file_path, "wb") as buffer:
|
47 |
+
shutil.copyfileobj(file.file, buffer)
|
48 |
+
|
49 |
+
file_id = insert_document_record(file.filename)
|
50 |
+
success = index_document_to_chroma(temp_file_path, file_id)
|
51 |
+
|
52 |
+
if success:
|
53 |
+
return {"message": f"File {file.filename} has been successfully uploaded and indexed.", "file_id": file_id}
|
54 |
+
else:
|
55 |
+
delete_document_record(file_id)
|
56 |
+
raise HTTPException(status_code=500, detail=f"Failed to index {file.filename}.")
|
57 |
+
finally:
|
58 |
+
if os.path.exists(temp_file_path):
|
59 |
+
os.remove(temp_file_path)
|
60 |
+
|
61 |
+
@app.get("/list-docs", response_model=list[DocumentInfo])
|
62 |
+
def list_documents():
|
63 |
+
return get_all_documents()
|
64 |
+
|
65 |
+
@app.post("/delete-doc")
|
66 |
+
def delete_document(request: DeleteFileRequest):
|
67 |
+
chroma_delete_success = delete_doc_from_chroma(request.file_id)
|
68 |
+
|
69 |
+
if chroma_delete_success:
|
70 |
+
db_delete_success = delete_document_record(request.file_id)
|
71 |
+
if db_delete_success:
|
72 |
+
return {"message": f"Successfully deleted document with file_id {request.file_id} from the system."}
|
73 |
+
else:
|
74 |
+
return {"error": f"Deleted from Chroma but failed to delete document with file_id {request.file_id} from the database."}
|
75 |
+
else:
|
76 |
+
return {"error": f"Failed to delete document with file_id {request.file_id} from Chroma."}
|
pinecone_utilis.py
ADDED
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langchain_community.document_loaders import PyPDFLoader, TextLoader
|
2 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
3 |
+
from langchain_openai import OpenAIEmbeddings
|
4 |
+
from pinecone import Pinecone, ServerlessSpec
|
5 |
+
from langchain_pinecone import PineconeVectorStore
|
6 |
+
from typing import List
|
7 |
+
from langchain_core.documents import Document
|
8 |
+
import os
|
9 |
+
from dotenv import load_dotenv
|
10 |
+
load_dotenv()
|
11 |
+
|
12 |
+
# API keys
|
13 |
+
PINECONE_API_KEY=os.getenv("PINECONE_API_KEY")
|
14 |
+
print(f"Pinecone API Key: {PINECONE_API_KEY}")
|
15 |
+
OPENAI_API_KEY=os.getenv("OPENAI_API_KEY")
|
16 |
+
|
17 |
+
|
18 |
+
|
19 |
+
# text splitter and embedding function
|
20 |
+
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200, length_function=len)
|
21 |
+
embeddings = OpenAIEmbeddings(model="text-embedding-3-large", dimensions=1024, api_key=OPENAI_API_KEY)
|
22 |
+
|
23 |
+
# Pinecone vector store
|
24 |
+
pc = Pinecone(api_key="pcsk_55CAuv_4KzXk8TYtxsauVoxwBsqqTgxWYmNJEEYrCpVxPqHcYbeS8njTWjZ14xCMEbksPS")
|
25 |
+
|
26 |
+
def load_and_split_document(file_path: str) -> List[Document]:
|
27 |
+
if file_path.endswith('.pdf'):
|
28 |
+
loader = PyPDFLoader(file_path)
|
29 |
+
elif file_path.endswith('.txt'):
|
30 |
+
loader = TextLoader(file_path)
|
31 |
+
else:
|
32 |
+
raise ValueError(f"Unsupported file type: {file_path}")
|
33 |
+
|
34 |
+
documents = loader.load()
|
35 |
+
return text_splitter.split_documents(documents)
|
36 |
+
|
37 |
+
INDEX_NAME = "smart-research-assistant"
|
38 |
+
|
39 |
+
def create_pinecone_vectorstore()-> PineconeVectorStore:
|
40 |
+
try:
|
41 |
+
if not pc.has_index(INDEX_NAME):
|
42 |
+
pc.create_index(
|
43 |
+
name=INDEX_NAME,
|
44 |
+
dimension=1024,
|
45 |
+
metric="cosine",
|
46 |
+
spec=ServerlessSpec(cloud="aws", region="us-east-1")
|
47 |
+
)
|
48 |
+
|
49 |
+
index = pc.Index(INDEX_NAME)
|
50 |
+
return PineconeVectorStore(index=index, embedding=embeddings)
|
51 |
+
|
52 |
+
except Exception as e:
|
53 |
+
print(f"Index initialization failed: {e}")
|
54 |
+
raise
|
55 |
+
|
56 |
+
|
57 |
+
vectorstore=create_pinecone_vectorstore()
|
58 |
+
|
59 |
+
def index_document_to_pinecone(file_path: str, file_id: int) -> bool:
|
60 |
+
try:
|
61 |
+
splits = load_and_split_document(file_path)
|
62 |
+
|
63 |
+
# Add metadata to each split
|
64 |
+
for split in splits:
|
65 |
+
split.metadata['file_id'] = file_id
|
66 |
+
|
67 |
+
vectorstore.add_documents(splits)
|
68 |
+
return True
|
69 |
+
except Exception as e:
|
70 |
+
print(f"Error indexing document: {e}")
|
71 |
+
return False
|
72 |
+
|
73 |
+
def delete_doc_from_pinecone(file_id: int):
|
74 |
+
try:
|
75 |
+
docs = vectorstore.get(where={"file_id": file_id})
|
76 |
+
print(f"Found {len(docs['ids'])} document chunks for file_id {file_id}")
|
77 |
+
|
78 |
+
vectorstore._collection.delete(where={"file_id": file_id})
|
79 |
+
print(f"Deleted all documents with file_id {file_id}")
|
80 |
+
|
81 |
+
return True
|
82 |
+
except Exception as e:
|
83 |
+
print(f"Error deleting document with file_id {file_id} from Chroma: {str(e)}")
|
84 |
+
return False
|
prompt_templates.py
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 1. Auto-summarization prompt template
|
2 |
+
AUTO_SUMMARY_TEMPLATE = """
|
3 |
+
Summarize the following document in no more than 150 words. Focus on the main points and key findings. Do not include information not present in the document.
|
4 |
+
|
5 |
+
DOCUMENT:
|
6 |
+
{document}
|
7 |
+
|
8 |
+
SUMMARY:
|
9 |
+
"""
|
10 |
+
|
11 |
+
# 2. Question answering prompt template
|
12 |
+
QA_PROMPT_TEMPLATE = """
|
13 |
+
Answer the following question based only on the provided document. Your answer must be grounded in the document and include a specific reference to the text that supports your answer.
|
14 |
+
|
15 |
+
Document:
|
16 |
+
{document}
|
17 |
+
|
18 |
+
Question:
|
19 |
+
{question}
|
20 |
+
|
21 |
+
Answer:
|
22 |
+
"""
|
23 |
+
|
24 |
+
# 3. Logic-based question generation prompt template
|
25 |
+
LOGIC_QUESTION_GENERATION_TEMPLATE = """
|
26 |
+
Generate three logic-based or comprehension-focused questions about the following document. Each question should require understanding or reasoning about the document content, not just simple recall. Provide each question on a new line.
|
27 |
+
|
28 |
+
Document:
|
29 |
+
{document}
|
30 |
+
|
31 |
+
Questions:
|
32 |
+
"""
|
33 |
+
|
34 |
+
# 4. Answer evaluation prompt template
|
35 |
+
ANSWER_EVALUATION_TEMPLATE = """
|
36 |
+
Evaluate the following user answer to the question, using only the provided document as the source of truth. State whether the answer is correct or not, and provide a brief justification referencing the document.
|
37 |
+
|
38 |
+
Document:
|
39 |
+
{document}
|
40 |
+
|
41 |
+
Question:
|
42 |
+
{question}
|
43 |
+
|
44 |
+
User Answer:
|
45 |
+
{user_answer}
|
46 |
+
|
47 |
+
Evaluation:
|
48 |
+
"""
|
49 |
+
|
50 |
+
# 5. For memory/follow-up: Chat prompt template
|
51 |
+
CHAT_PROMPT_TEMPLATE = """
|
52 |
+
The following is a conversation between a user and an AI assistant about a document. The assistant answers questions and provides justifications based on the document. Use the conversation history and the document to answer the new question.
|
53 |
+
|
54 |
+
Document:
|
55 |
+
{document}
|
56 |
+
|
57 |
+
Conversation History:
|
58 |
+
{history}
|
59 |
+
|
60 |
+
Question:
|
61 |
+
{question}
|
62 |
+
|
63 |
+
Answer:
|
64 |
+
"""
|
pydantic_models.py
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic import BaseModel, Field
|
2 |
+
from enum import Enum
|
3 |
+
from datetime import datetime
|
4 |
+
|
5 |
+
class ModelName(str, Enum):
|
6 |
+
GPT4_O = "gpt-4o"
|
7 |
+
GPT4_O_MINI = "gpt-4o-mini"
|
8 |
+
|
9 |
+
class QueryInput(BaseModel):
|
10 |
+
question: str
|
11 |
+
session_id: str = Field(default=None)
|
12 |
+
model: ModelName = Field(default=ModelName.GPT4_O_MINI)
|
13 |
+
|
14 |
+
class QueryResponse(BaseModel):
|
15 |
+
answer: str
|
16 |
+
session_id: str
|
17 |
+
model: ModelName
|
18 |
+
|
19 |
+
class DocumentInfo(BaseModel):
|
20 |
+
id: int
|
21 |
+
filename: str
|
22 |
+
upload_timestamp: datetime
|
23 |
+
|
24 |
+
class DeleteFileRequest(BaseModel):
|
25 |
+
file_id: int
|
requirements.txt
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
langchain
|
2 |
+
langchain-openai
|
3 |
+
langchain-core
|
4 |
+
langchain_community
|
5 |
+
docx2txt
|
6 |
+
pypdf
|
7 |
+
langchain_chroma
|
8 |
+
python-multipart
|
9 |
+
fastapi
|
10 |
+
uvicorn
|
ui.py
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import requests
|
3 |
+
|
4 |
+
# Set the FastAPI backend URL
|
5 |
+
FASTAPI_URL = "http://localhost:8000"
|
6 |
+
|
7 |
+
st.title("Smart Research Assistant")
|
8 |
+
|
9 |
+
# Document Upload Section
|
10 |
+
uploaded_file = st.file_uploader("Upload a PDF or TXT document", type=["pdf", "txt"])
|
11 |
+
if uploaded_file:
|
12 |
+
files = {"file": uploaded_file}
|
13 |
+
response = requests.post(f"{FASTAPI_URL}/upload-doc", files=files)
|
14 |
+
if response.status_code == 200:
|
15 |
+
result = response.json()
|
16 |
+
st.success(result["message"])
|
17 |
+
file_id = result["file_id"]
|
18 |
+
# Here you would also call a summary endpoint if implemented
|
19 |
+
# For demo, assume summary is returned in the upload response
|
20 |
+
# st.write("Summary: ", result.get("summary", "Summary not available"))
|
21 |
+
else:
|
22 |
+
st.error(f"Error: {response.json().get('detail', 'Unknown error')}")
|
23 |
+
|
24 |
+
# List Documents (optional)
|
25 |
+
if st.button("List Documents"):
|
26 |
+
response = requests.get(f"{FASTAPI_URL}/list-docs")
|
27 |
+
if response.status_code == 200:
|
28 |
+
documents = response.json()
|
29 |
+
st.write("Available Documents:")
|
30 |
+
for doc in documents:
|
31 |
+
st.write(f"- {doc['filename']} (ID: {doc['file_id']})")
|
32 |
+
else:
|
33 |
+
st.error("Failed to list documents")
|
34 |
+
|
35 |
+
# Interaction Modes
|
36 |
+
mode = st.radio("Choose Mode", ["Ask Anything", "Challenge Me"])
|
37 |
+
|
38 |
+
if mode == "Ask Anything":
|
39 |
+
question = st.text_input("Ask a question about the document")
|
40 |
+
if question and st.button("Submit"):
|
41 |
+
payload = {
|
42 |
+
"question": question,
|
43 |
+
"session_id": "user123", # Replace with actual session management
|
44 |
+
"model": "default" # Replace with your model selection
|
45 |
+
}
|
46 |
+
response = requests.post(f"{FASTAPI_URL}/chat", json=payload)
|
47 |
+
if response.status_code == 200:
|
48 |
+
result = response.json()
|
49 |
+
st.write("Answer:", result["answer"])
|
50 |
+
# If your backend returns a source snippet, display it:
|
51 |
+
# st.write("Source:", result.get("source", ""))
|
52 |
+
else:
|
53 |
+
st.error("Failed to get answer")
|
54 |
+
|
55 |
+
# elif mode == "Challenge Me":
|
56 |
+
# if st.button("Generate Challenge Questions"):
|
57 |
+
|
58 |
+
# # Assume your backend has a `/generate-questions` endpoint
|
59 |
+
# # response = requests.post(f"{FASTAPI_URL}/generate-questions", json={"file_id": file_id})
|
60 |
+
# # if response.status_code == 200:
|
61 |
+
# # questions
|