deedrop1140 commited on
Commit
a89e75c
·
verified ·
1 Parent(s): f56efd1

Upload 25 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ ENV PYTHONDONTWRITEBYTECODE=1
6
+ ENV PYTHONUNBUFFERED=1
7
+
8
+ COPY backend/requirements.txt /app/requirements.txt
9
+
10
+ RUN pip install --no-cache-dir --upgrade pip && \
11
+ pip install --no-cache-dir -r /app/requirements.txt
12
+
13
+ COPY backend /app
14
+
15
+ EXPOSE 7860
16
+
17
+ CMD ["sh", "-c", "uvicorn app:app --host 0.0.0.0 --port 7860"]
backend/.dockerignore ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gitignore
3
+ .env
4
+ .venv
5
+ __pycache__/
6
+ *.pyc
7
+ *.pyo
8
+ *.pyd
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ .vscode/
12
+ frontend/node_modules/
13
+ frontend/dist/
14
+ instance/
15
+ uploads/
16
+ vectorstores/
17
+ database.db
backend/Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ ENV PYTHONDONTWRITEBYTECODE=1
6
+ ENV PYTHONUNBUFFERED=1
7
+
8
+ COPY backend/requirements.txt /app/requirements.txt
9
+
10
+ RUN pip install --no-cache-dir --upgrade pip && \
11
+ pip install --no-cache-dir -r /app/requirements.txt
12
+
13
+ COPY backend /app
14
+
15
+ EXPOSE 7860
16
+
17
+ CMD ["sh", "-c", "uvicorn app:app --host 0.0.0.0 --port 7860"]
backend/__init__.py ADDED
File without changes
backend/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (133 Bytes). View file
 
backend/__pycache__/app.cpython-310.pyc ADDED
Binary file (11.1 kB). View file
 
backend/__pycache__/config.cpython-310.pyc ADDED
Binary file (1.08 kB). View file
 
backend/__pycache__/lang_service.cpython-310.pyc ADDED
Binary file (2.77 kB). View file
 
backend/__pycache__/langgraph_service.cpython-310.pyc ADDED
Binary file (1.76 kB). View file
 
backend/__pycache__/models.cpython-310.pyc ADDED
Binary file (2.69 kB). View file
 
backend/app.py ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from datetime import datetime, timedelta, timezone
3
+ from typing import Generator
4
+
5
+ from fastapi import Depends, FastAPI, File, HTTPException, UploadFile, status
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
8
+ from jose import JWTError, jwt
9
+ from langchain_core.messages import AIMessage, HumanMessage
10
+ from pydantic import BaseModel
11
+ from sqlalchemy.orm import Session
12
+ from werkzeug.security import check_password_hash, generate_password_hash
13
+ from werkzeug.utils import secure_filename
14
+
15
+ try:
16
+ from .config import Config
17
+ from .lang_service import create_vectorstore, get_rag_response
18
+ from .langgraph_service import get_response
19
+ from .models import Chat, Message, SessionLocal, UploadedFile as ModelUploadedFile, User, init_db
20
+ except ImportError:
21
+ from config import Config
22
+ from lang_service import create_vectorstore, get_rag_response
23
+ from langgraph_service import get_response
24
+ from models import Chat, Message, SessionLocal, UploadedFile as ModelUploadedFile, User, init_db
25
+
26
+
27
+ app = FastAPI(title="AI Chatbot API")
28
+ security = HTTPBearer()
29
+
30
+ app.add_middleware(
31
+ CORSMiddleware,
32
+ allow_origins=Config.CORS_ORIGINS,
33
+ allow_origin_regex=Config.CORS_ORIGIN_REGEX,
34
+ allow_credentials=True,
35
+ allow_methods=["*"],
36
+ allow_headers=["*"],
37
+ )
38
+
39
+ UPLOAD_FOLDER = Config.UPLOAD_DIR
40
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
41
+ os.makedirs(Config.VECTORSTORE_DIR, exist_ok=True)
42
+ init_db()
43
+
44
+
45
+ class RegisterInput(BaseModel):
46
+ username: str
47
+ password: str
48
+
49
+
50
+ class LoginInput(BaseModel):
51
+ username: str
52
+ password: str
53
+
54
+
55
+ class TokenResponse(BaseModel):
56
+ access_token: str
57
+ token_type: str = "bearer"
58
+
59
+
60
+ class UserOut(BaseModel):
61
+ id: int
62
+ username: str
63
+
64
+
65
+ class ChatOut(BaseModel):
66
+ id: int
67
+ user_id: int
68
+ title: str
69
+ mode: str
70
+ is_pinned: bool
71
+ is_archived: bool
72
+ created_at: datetime
73
+
74
+
75
+ class MessageInput(BaseModel):
76
+ message: str
77
+
78
+
79
+ class MessageOut(BaseModel):
80
+ id: int
81
+ chat_id: int
82
+ role: str
83
+ content: str
84
+ timestamp: datetime
85
+
86
+
87
+ class UploadedFileOut(BaseModel):
88
+ id: int
89
+ chat_id: int
90
+ filename: str
91
+ filepath: str
92
+ uploaded_at: datetime
93
+
94
+
95
+ class ChatDetailOut(BaseModel):
96
+ chat: ChatOut
97
+ messages: list[MessageOut]
98
+ files: list[UploadedFileOut]
99
+
100
+
101
+ class AboutOut(BaseModel):
102
+ app_name: str
103
+ stack: list[str]
104
+
105
+
106
+ def get_db() -> Generator[Session, None, None]:
107
+ db = SessionLocal()
108
+ try:
109
+ yield db
110
+ finally:
111
+ db.close()
112
+
113
+
114
+ def create_access_token(user_id: int) -> str:
115
+ expire = datetime.now(timezone.utc) + timedelta(minutes=Config.ACCESS_TOKEN_EXPIRE_MINUTES)
116
+ payload = {"sub": str(user_id), "exp": expire}
117
+ return jwt.encode(payload, Config.JWT_SECRET_KEY, algorithm="HS256")
118
+
119
+
120
+ def get_current_user(
121
+ credentials: HTTPAuthorizationCredentials = Depends(security),
122
+ db: Session = Depends(get_db),
123
+ ) -> User:
124
+ token = credentials.credentials
125
+ try:
126
+ payload = jwt.decode(token, Config.JWT_SECRET_KEY, algorithms=["HS256"])
127
+ user_id = int(payload.get("sub"))
128
+ except (JWTError, TypeError, ValueError) as exc:
129
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") from exc
130
+
131
+ user = db.query(User).filter(User.id == user_id).first()
132
+ if not user:
133
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
134
+ return user
135
+
136
+
137
+ def chat_to_out(chat: Chat) -> ChatOut:
138
+ return ChatOut(
139
+ id=chat.id,
140
+ user_id=chat.user_id,
141
+ title=chat.title,
142
+ mode=chat.mode,
143
+ is_pinned=chat.is_pinned,
144
+ is_archived=chat.is_archived,
145
+ created_at=chat.created_at,
146
+ )
147
+
148
+
149
+ def message_to_out(message: Message) -> MessageOut:
150
+ return MessageOut(
151
+ id=message.id,
152
+ chat_id=message.chat_id,
153
+ role=message.role,
154
+ content=message.content,
155
+ timestamp=message.timestamp,
156
+ )
157
+
158
+
159
+ def file_to_out(uploaded_file: ModelUploadedFile) -> UploadedFileOut:
160
+ return UploadedFileOut(
161
+ id=uploaded_file.id,
162
+ chat_id=uploaded_file.chat_id,
163
+ filename=uploaded_file.filename,
164
+ filepath=uploaded_file.filepath,
165
+ uploaded_at=uploaded_file.uploaded_at,
166
+ )
167
+
168
+
169
+ def get_user_chat_or_404(db: Session, chat_id: int, user_id: int) -> Chat:
170
+ chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user_id).first()
171
+ if not chat:
172
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Chat not found")
173
+ return chat
174
+
175
+
176
+ @app.get("/")
177
+ def root():
178
+ return {"message": "FastAPI backend is running"}
179
+
180
+
181
+ @app.post("/api/auth/register", response_model=UserOut)
182
+ def register(payload: RegisterInput, db: Session = Depends(get_db)):
183
+ exists = db.query(User).filter(User.username == payload.username).first()
184
+ if exists:
185
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Username already exists")
186
+
187
+ user = User(username=payload.username, password=generate_password_hash(payload.password))
188
+ db.add(user)
189
+ db.commit()
190
+ db.refresh(user)
191
+
192
+ return UserOut(id=user.id, username=user.username)
193
+
194
+
195
+ @app.post("/api/auth/login", response_model=TokenResponse)
196
+ def login(payload: LoginInput, db: Session = Depends(get_db)):
197
+ user = db.query(User).filter(User.username == payload.username).first()
198
+ if not user or not check_password_hash(user.password, payload.password):
199
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
200
+
201
+ token = create_access_token(user.id)
202
+ return TokenResponse(access_token=token)
203
+
204
+
205
+ @app.get("/api/auth/me", response_model=UserOut)
206
+ def me(current_user: User = Depends(get_current_user)):
207
+ return UserOut(id=current_user.id, username=current_user.username)
208
+
209
+
210
+ @app.get("/api/chats", response_model=list[ChatOut])
211
+ def list_chats(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
212
+ chats = (
213
+ db.query(Chat)
214
+ .filter(Chat.user_id == current_user.id, Chat.is_archived.is_(False))
215
+ .order_by(Chat.is_pinned.desc(), Chat.created_at.desc())
216
+ .all()
217
+ )
218
+ return [chat_to_out(chat) for chat in chats]
219
+
220
+
221
+ @app.post("/api/chats", response_model=ChatOut)
222
+ def create_chat(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
223
+ chat = Chat(user_id=current_user.id, mode="normal")
224
+ db.add(chat)
225
+ db.commit()
226
+ db.refresh(chat)
227
+ return chat_to_out(chat)
228
+
229
+
230
+ @app.get("/api/chats/{chat_id}", response_model=ChatDetailOut)
231
+ def get_chat(chat_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
232
+ chat = get_user_chat_or_404(db, chat_id, current_user.id)
233
+
234
+ messages = db.query(Message).filter(Message.chat_id == chat_id).order_by(Message.timestamp).all()
235
+ files = db.query(ModelUploadedFile).filter(ModelUploadedFile.chat_id == chat_id).all()
236
+
237
+ return ChatDetailOut(
238
+ chat=chat_to_out(chat),
239
+ messages=[message_to_out(message) for message in messages],
240
+ files=[file_to_out(uploaded_file) for uploaded_file in files],
241
+ )
242
+
243
+
244
+ @app.post("/api/chats/{chat_id}/messages", response_model=MessageOut)
245
+ def send_message(
246
+ chat_id: int,
247
+ payload: MessageInput,
248
+ current_user: User = Depends(get_current_user),
249
+ db: Session = Depends(get_db),
250
+ ):
251
+ chat = get_user_chat_or_404(db, chat_id, current_user.id)
252
+
253
+ user_input = payload.message.strip()
254
+ if not user_input:
255
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Message cannot be empty")
256
+
257
+ if chat.title == "New Chat":
258
+ chat.title = " ".join(user_input.split()[:6])
259
+
260
+ messages = db.query(Message).filter(Message.chat_id == chat_id).order_by(Message.timestamp).all()
261
+
262
+ if chat.mode == "rag":
263
+ try:
264
+ ai_text = get_rag_response(chat_id, user_input)
265
+ except Exception:
266
+ ai_text = "Error retrieving document context."
267
+ else:
268
+ history = []
269
+ for msg in messages:
270
+ if msg.role == "user":
271
+ history.append(HumanMessage(content=msg.content))
272
+ else:
273
+ history.append(AIMessage(content=msg.content))
274
+
275
+ history.append(HumanMessage(content=user_input))
276
+ try:
277
+ ai_response = get_response(history)
278
+ ai_text = ai_response.content
279
+ except Exception:
280
+ ai_text = "AI service is not configured. Please set GROQ_API_KEY on the server."
281
+
282
+ db.add(Message(chat_id=chat_id, role="user", content=user_input))
283
+ ai_message = Message(chat_id=chat_id, role="ai", content=ai_text)
284
+ db.add(ai_message)
285
+ db.commit()
286
+ db.refresh(ai_message)
287
+
288
+ return message_to_out(ai_message)
289
+
290
+
291
+ @app.post("/api/chats/{chat_id}/upload", response_model=UploadedFileOut)
292
+ async def upload_file(
293
+ chat_id: int,
294
+ file: UploadFile = File(...),
295
+ current_user: User = Depends(get_current_user),
296
+ db: Session = Depends(get_db),
297
+ ):
298
+ chat = get_user_chat_or_404(db, chat_id, current_user.id)
299
+
300
+ filename = secure_filename(file.filename or "document.txt")
301
+ if not filename:
302
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid filename")
303
+
304
+ filepath = os.path.join(UPLOAD_FOLDER, f"{chat_id}_{filename}")
305
+
306
+ content = await file.read()
307
+ with open(filepath, "wb") as out_file:
308
+ out_file.write(content)
309
+
310
+ try:
311
+ create_vectorstore(filepath, chat_id)
312
+ except Exception as exc:
313
+ if os.path.exists(filepath):
314
+ os.remove(filepath)
315
+ raise HTTPException(
316
+ status_code=status.HTTP_400_BAD_REQUEST,
317
+ detail="Failed to process file. Upload a text-based document (txt, md, csv, json) or re-save the file as UTF-8.",
318
+ ) from exc
319
+
320
+ uploaded = ModelUploadedFile(chat_id=chat_id, filename=filename, filepath=filepath)
321
+ chat.mode = "rag"
322
+
323
+ db.add(uploaded)
324
+ db.commit()
325
+ db.refresh(uploaded)
326
+
327
+ return file_to_out(uploaded)
328
+
329
+
330
+ @app.post("/api/chats/{chat_id}/pin", response_model=ChatOut)
331
+ def pin_chat(chat_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
332
+ chat = get_user_chat_or_404(db, chat_id, current_user.id)
333
+ chat.is_pinned = not chat.is_pinned
334
+ db.commit()
335
+ db.refresh(chat)
336
+ return chat_to_out(chat)
337
+
338
+
339
+ @app.post("/api/chats/{chat_id}/archive", response_model=ChatOut)
340
+ def archive_chat(chat_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
341
+ chat = get_user_chat_or_404(db, chat_id, current_user.id)
342
+ chat.is_archived = True
343
+ db.commit()
344
+ db.refresh(chat)
345
+ return chat_to_out(chat)
346
+
347
+
348
+ @app.delete("/api/chats/{chat_id}")
349
+ def delete_chat(chat_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
350
+ chat = get_user_chat_or_404(db, chat_id, current_user.id)
351
+
352
+ db.query(Message).filter(Message.chat_id == chat_id).delete()
353
+ db.query(ModelUploadedFile).filter(ModelUploadedFile.chat_id == chat_id).delete()
354
+ db.delete(chat)
355
+ db.commit()
356
+
357
+ return {"deleted": True}
358
+
359
+
360
+ @app.get("/api/about", response_model=AboutOut)
361
+ def about():
362
+ return AboutOut(
363
+ app_name="AI Chatbot",
364
+ stack=["FastAPI", "React", "LangGraph", "FAISS", "Groq"],
365
+ )
backend/config.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from dotenv import load_dotenv
5
+
6
+
7
+ BACKEND_DIR = Path(__file__).resolve().parent
8
+ PROJECT_ROOT = BACKEND_DIR.parent
9
+
10
+ load_dotenv(PROJECT_ROOT / ".env")
11
+ load_dotenv(BACKEND_DIR / ".env")
12
+
13
+
14
+ class Config:
15
+ SECRET_KEY = os.getenv("SECRET_KEY", "change-me")
16
+ JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", SECRET_KEY)
17
+ SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", f"sqlite:///{BACKEND_DIR / 'database.db'}")
18
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
19
+ ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "10080"))
20
+ CORS_ORIGINS = [
21
+ origin.strip()
22
+ for origin in os.getenv("CORS_ORIGINS", "http://localhost:5173,http://127.0.0.1:5173").split(",")
23
+ if origin.strip()
24
+ ]
25
+ CORS_ORIGIN_REGEX = os.getenv("CORS_ORIGIN_REGEX", r"^https://.*\.vercel\.app$").strip() or None
26
+ UPLOAD_DIR = str(BACKEND_DIR / "uploads")
27
+ VECTORSTORE_DIR = str(BACKEND_DIR / "vectorstores")
backend/database.db ADDED
Binary file (57.3 kB). View file
 
backend/database.db.bak ADDED
Binary file (57.3 kB). View file
 
backend/instance/database.db ADDED
Binary file (24.6 kB). View file
 
backend/lang_service.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from langchain_community.document_loaders import PyPDFLoader
4
+ from langchain_community.document_loaders import TextLoader
5
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
6
+ from langchain_community.vectorstores import FAISS
7
+ from langchain_community.embeddings import HuggingFaceEmbeddings
8
+ from langchain_core.prompts import ChatPromptTemplate
9
+ from langchain_core.output_parsers import StrOutputParser
10
+ from langchain_core.runnables import RunnablePassthrough
11
+ from langchain_groq import ChatGroq
12
+
13
+ try:
14
+ from .config import Config
15
+ except ImportError:
16
+ from config import Config
17
+
18
+ VECTOR_STORE_PATH = Config.VECTORSTORE_DIR
19
+
20
+
21
+ # ================= CREATE VECTOR STORE =================
22
+ def create_vectorstore(filepath, chat_id):
23
+ extension = os.path.splitext(filepath)[1].lower()
24
+
25
+ if extension == ".pdf":
26
+ loader = PyPDFLoader(filepath)
27
+ else:
28
+ # Try UTF-8 first, then automatically detect compatible text encodings.
29
+ loader = TextLoader(filepath, encoding="utf-8", autodetect_encoding=True)
30
+
31
+ documents = loader.load()
32
+
33
+ if not documents or not any(doc.page_content.strip() for doc in documents):
34
+ raise ValueError("Uploaded file has no readable text content")
35
+
36
+ splitter = RecursiveCharacterTextSplitter(
37
+ chunk_size=500,
38
+ chunk_overlap=100
39
+ )
40
+
41
+ docs = splitter.split_documents(documents)
42
+
43
+ embeddings = HuggingFaceEmbeddings(
44
+ model_name="sentence-transformers/all-MiniLM-L6-v2"
45
+ )
46
+
47
+ vectorstore = FAISS.from_documents(docs, embeddings)
48
+
49
+ path = os.path.join(VECTOR_STORE_PATH, str(chat_id))
50
+ os.makedirs(path, exist_ok=True)
51
+
52
+ vectorstore.save_local(path)
53
+
54
+
55
+ # ================= GET RAG RESPONSE =================
56
+ def get_rag_response(chat_id, query):
57
+ path = os.path.join(VECTOR_STORE_PATH, str(chat_id))
58
+
59
+ if not Config.GROQ_API_KEY:
60
+ raise RuntimeError("GROQ_API_KEY is not configured")
61
+
62
+ embeddings = HuggingFaceEmbeddings(
63
+ model_name="sentence-transformers/all-MiniLM-L6-v2"
64
+ )
65
+
66
+ vectorstore = FAISS.load_local(
67
+ path,
68
+ embeddings,
69
+ allow_dangerous_deserialization=True
70
+ )
71
+
72
+ retriever = vectorstore.as_retriever()
73
+
74
+ llm = ChatGroq(
75
+ groq_api_key=Config.GROQ_API_KEY,
76
+ model="llama-3.1-8b-instant",
77
+ temperature=0
78
+ )
79
+
80
+ # 🔥 Modern RAG Chain (LCEL)
81
+ prompt = ChatPromptTemplate.from_template(
82
+ """Answer the question based only on the context below.
83
+
84
+ Context:
85
+ {context}
86
+
87
+ Question:
88
+ {question}
89
+ """
90
+ )
91
+
92
+ def format_docs(docs):
93
+ return "\n\n".join(doc.page_content for doc in docs)
94
+
95
+ rag_chain = (
96
+ {
97
+ "context": retriever | format_docs,
98
+ "question": RunnablePassthrough(),
99
+ }
100
+ | prompt
101
+ | llm
102
+ | StrOutputParser()
103
+ )
104
+
105
+ return rag_chain.invoke(query)
backend/langgraph_service.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_groq import ChatGroq
2
+ from langchain_core.messages import AIMessage, SystemMessage
3
+ from langgraph.graph import StateGraph
4
+ from typing import TypedDict, List
5
+ try:
6
+ from .config import Config
7
+ except ImportError:
8
+ from config import Config
9
+
10
+ llm = None
11
+
12
+
13
+ def get_llm():
14
+ global llm
15
+ if llm is None:
16
+ if not Config.GROQ_API_KEY:
17
+ raise RuntimeError("GROQ_API_KEY is not configured")
18
+ llm = ChatGroq(
19
+ groq_api_key=Config.GROQ_API_KEY,
20
+ model="llama-3.1-8b-instant"
21
+ )
22
+ return llm
23
+
24
+ class State(TypedDict):
25
+ messages: List
26
+
27
+
28
+ SYSTEM_PROMPT = (
29
+ "You are a helpful assistant. "
30
+ "Answer clearly and directly. "
31
+ "Do not repeat your previous response verbatim."
32
+ )
33
+
34
+
35
+ def call_model(state):
36
+ history = state["messages"]
37
+ model = get_llm()
38
+ response = model.invoke([SystemMessage(content=SYSTEM_PROMPT), *history])
39
+
40
+ last_ai_message = next(
41
+ (message for message in reversed(history) if isinstance(message, AIMessage)),
42
+ None,
43
+ )
44
+
45
+ # If the model repeats exactly, ask for a reformulated answer one time.
46
+ if last_ai_message and response.content.strip() == last_ai_message.content.strip():
47
+ response = model.invoke(
48
+ [
49
+ SystemMessage(content=SYSTEM_PROMPT),
50
+ *history,
51
+ SystemMessage(
52
+ content="Rephrase and improve the answer. Add useful new detail and avoid repetition."
53
+ ),
54
+ ]
55
+ )
56
+
57
+ return {"messages": history + [response]}
58
+
59
+ graph = StateGraph(State)
60
+ graph.add_node("chatbot", call_model)
61
+ graph.set_entry_point("chatbot")
62
+ app_graph = graph.compile()
63
+
64
+ def get_response(history):
65
+ result = app_graph.invoke({"messages": history})
66
+ return result["messages"][-1]
backend/models.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+
3
+ from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text, create_engine
4
+ from sqlalchemy.orm import declarative_base, relationship, sessionmaker
5
+
6
+ try:
7
+ from .config import Config
8
+ except ImportError:
9
+ from config import Config
10
+
11
+
12
+ DATABASE_URL = Config.SQLALCHEMY_DATABASE_URI
13
+ connect_args = {"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {}
14
+
15
+ engine = create_engine(DATABASE_URL, connect_args=connect_args)
16
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
17
+ Base = declarative_base()
18
+
19
+
20
+ class User(Base):
21
+ __tablename__ = "user"
22
+
23
+ id = Column(Integer, primary_key=True, index=True)
24
+ username = Column(String(150), unique=True, nullable=False, index=True)
25
+ password = Column(String(200), nullable=False)
26
+
27
+ chats = relationship("Chat", back_populates="user", cascade="all, delete-orphan")
28
+
29
+
30
+ class Chat(Base):
31
+ __tablename__ = "chat"
32
+
33
+ id = Column(Integer, primary_key=True, index=True)
34
+ user_id = Column(Integer, ForeignKey("user.id"), nullable=False)
35
+
36
+ title = Column(String(200), default="New Chat")
37
+ mode = Column(String(20), default="normal")
38
+
39
+ is_pinned = Column(Boolean, default=False)
40
+ is_archived = Column(Boolean, default=False)
41
+
42
+ created_at = Column(DateTime, default=datetime.utcnow)
43
+
44
+ user = relationship("User", back_populates="chats")
45
+ messages = relationship("Message", back_populates="chat", cascade="all, delete-orphan")
46
+ files = relationship("UploadedFile", back_populates="chat", cascade="all, delete-orphan")
47
+
48
+
49
+ class Message(Base):
50
+ __tablename__ = "message"
51
+
52
+ id = Column(Integer, primary_key=True, index=True)
53
+ chat_id = Column(Integer, ForeignKey("chat.id"), nullable=False, index=True)
54
+
55
+ role = Column(String(50), nullable=False)
56
+ content = Column(Text, nullable=False)
57
+ timestamp = Column(DateTime, default=datetime.utcnow)
58
+
59
+ chat = relationship("Chat", back_populates="messages")
60
+
61
+
62
+ class UploadedFile(Base):
63
+ __tablename__ = "uploaded_file"
64
+
65
+ id = Column(Integer, primary_key=True, index=True)
66
+ chat_id = Column(Integer, ForeignKey("chat.id"), nullable=False, index=True)
67
+
68
+ filename = Column(String(200), nullable=False)
69
+ filepath = Column(String(300), nullable=False)
70
+ uploaded_at = Column(DateTime, default=datetime.utcnow)
71
+
72
+ chat = relationship("Chat", back_populates="files")
73
+
74
+
75
+ def init_db():
76
+ Base.metadata.create_all(bind=engine)
backend/render.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ - type: web
3
+ name: chat-bot-api
4
+ env: docker
5
+ dockerfilePath: ./Dockerfile
6
+ autoDeploy: true
7
+ healthCheckPath: /
8
+ envVars:
9
+ - key: SECRET_KEY
10
+ sync: false
11
+ - key: JWT_SECRET_KEY
12
+ sync: false
13
+ - key: GROQ_API_KEY
14
+ sync: false
15
+ - key: ACCESS_TOKEN_EXPIRE_MINUTES
16
+ value: "10080"
17
+ - key: CORS_ORIGINS
18
+ sync: false
backend/requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ sqlalchemy
4
+ python-multipart
5
+ python-jose[cryptography]
6
+ werkzeug
7
+ python-dotenv
8
+ langchain
9
+ langgraph
10
+ langchain-groq
11
+ langchain-community
12
+ langchain-text-splitters
13
+ sentence-transformers
14
+ faiss-cpu
15
+ pypdf
backend/uploads/1_Deepak_Kushwaha_Resume.pdf ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ %PDF-1.4
2
+ %���� ReportLab Generated PDF document (opensource)
3
+ 1 0 obj
4
+ <<
5
+ /F1 2 0 R /F2 3 0 R
6
+ >>
7
+ endobj
8
+ 2 0 obj
9
+ <<
10
+ /BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
11
+ >>
12
+ endobj
13
+ 3 0 obj
14
+ <<
15
+ /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
16
+ >>
17
+ endobj
18
+ 4 0 obj
19
+ <<
20
+ /Contents 8 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 7 0 R /Resources <<
21
+ /Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
22
+ >> /Rotate 0 /Trans <<
23
+
24
+ >>
25
+ /Type /Page
26
+ >>
27
+ endobj
28
+ 5 0 obj
29
+ <<
30
+ /PageMode /UseNone /Pages 7 0 R /Type /Catalog
31
+ >>
32
+ endobj
33
+ 6 0 obj
34
+ <<
35
+ /Author (\(anonymous\)) /CreationDate (D:20260329223300+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260329223300+00'00') /Producer (ReportLab PDF Library - \(opensource\))
36
+ /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
37
+ >>
38
+ endobj
39
+ 7 0 obj
40
+ <<
41
+ /Count 1 /Kids [ 4 0 R ] /Type /Pages
42
+ >>
43
+ endobj
44
+ 8 0 obj
45
+ <<
46
+ /Filter [ /ASCII85Decode /FlateDecode ] /Length 1939
47
+ >>
48
+ stream
49
+ Gat=+9lh45&A@sBoOY_](1?/^Ng%qDD%\`d71?/#HkA/D7R87n,RlZ2r:k]B)8O7:;mk^:o.fO^h6110M_:RYY8dKh"]9bAa+H!e:MDkfmTJ_3VB`[D*rJ"]2CIBg;"sXpcGdc7$Wu<f9Y)1-o.DSOBKlRH.<SE'kp68=N/k':/B*3)<Be/6\TgQln$U2\Ni%<Rd0JuJ-ENnJPY/KbI(+HGnon+?V)sMcnF[#$5$##aPc9aG1]Hr)NOXlNp>*]%2aT<."RQZ_OD^/<acqq9.?fl)<@U_9];*i=e(8fAVrr:f^O&ED^a4QpcJmL3<[JPI+X1J'mfWqAoI?fXj'>:H?WKJ(k`MG36qNS5R^6E'(IJ!/9bp?:R5.=?s2d""Xd0J0f#uuU<11ZWlMsQi!,M'M]*V4LY7ZT42P-0'.a*GLBX":i;C#[WBu'OlAq`nD#T:=H3jk3,][[`EK8?ZP8iYg,@oSXFUDAhj+h&eEgis^kHg.InCt6bjGimbRj5.?N)pTq0;[tdM;qP&a`10H<B)(\gns.SeB-ab(``H_ch32AIR4++0m6WAV/EG)W=jD/gP#+/,)2[qlZ1h.!"qlK_nWhigs1s`4W^Y!FDI('hj?ooK'>_91`n)--ePo@\1#k';4sl[Wl[9]e9K6^Z.7d<\!m;^*.?UrlOM%L>fpS.[mlV3;<r5BcRH-g`J;=R%q_UMM-EBN]rVK:i@Q",s9/0#..`2JhB],O4VVX\g,;jND22'pR$bpB&eTYES(>VSrTg_oLA)0E)H`lbc\g.N(U)#5c[1j+B`h5SIM9f0V?>]Z57C&!tkE)j^ljWH<P8eUs^$<MVqN*^o.(9u*iUP.uB1.o8)]CFn#XqEXjn!/u4irN0>B^[BhC._["B6mb2/?(XgP#F?j$X8Y9r*5piiS=:OZPfI#L;D"SC*U,8o8R<L5)1U\F4,]1tpG.0.E41/t`B)aY85OVS\i!fSs"P:Ns"NWSDXR^84"S+HVf$KYYuIF:Z!H$VILaN':!T^qo,Co!kR)pgTpX4+NY:77-ZEOL(f([18mInp%'8K"'?q.J:\JScUZgZ-@FmLBW@B"PD>b\i0c<.#pP<$"27VEU0jIV9.c8b(D(P+a4GDPRlb!B9?4*F]a3o"$4lnLHH`M'Y4aulBM2-]1g)brA&7#lmjCC7[Hpq9ea1\F\sJ#i[][da?Ws8Tso6?]o)D0%ZqHHJoaNrQkiDKViAhroq_/p(=GKm*m28+U/!Frr3OP,CG#Xq2F:]T3:n)t!Pn1om@%aa%-B6>UWkdQX!THrK/?\*O6/IFZ_"(X5)k<tFjRLpj:`n8^lN:0N"7c!MBi)E-9_&e1e8K5ZZq^`h-.[N8Zk[9BuGjm##_<L<U)kIOFddcP7u32MN&h&1+FZO,cF%]6Gou<c[*EB!BqpJN:UeEC!,N)+:g/Jg7dL=pTJM59oj+AC9H1#<:RM'I))"VJ1HBcdGU-?a>gN/=^/MdD`S=Edl2-0%Dp=XR+WBI<7!sZSFH#n#Un>O502p0gf=LKVp:TUk@piVU@ophp1F9bo8^koQS4S74\jB8NGb1cM-<#0%@ZB9D!hhGdg)/rf[H)tKh?$=P+`g=F\_MaXs_S.$1=n_s/iYk".uQ91F2[lpNZ0pj4j.k%#]St]#2F`LMI)>UJLtDS(9'If<`A-S*J,gnCb5=(P.tbj5)]+bFJ=*a+\[\MCKElHCAq&*T#!;1)=:A>b'n2P@Whr$M-Jr/r7:+r2fMPO-d/aP]c'[]kLi2Oks[t2_1p(0Jpo>\E;`fcOB\0H^Z_Hi-:OV"VTS.f6c[:Y&g0&*&Y1+j'kN;IT4kEba$#mf_*0-),gkTH:Dn]^9j0CD@nsR]lY^_D00j1Vmusu?MOOWr/+/*`oZpNE@[_=kUpH:*o`qMODRn!V;F[o?Thccp[jful1UIiQNK;D(OHia)u~>endstream
50
+ endobj
51
+ xref
52
+ 0 9
53
+ 0000000000 65535 f
54
+ 0000000061 00000 n
55
+ 0000000102 00000 n
56
+ 0000000209 00000 n
57
+ 0000000321 00000 n
58
+ 0000000524 00000 n
59
+ 0000000592 00000 n
60
+ 0000000872 00000 n
61
+ 0000000931 00000 n
62
+ trailer
63
+ <<
64
+ /ID
65
+ [<478a1d1eebd9bba5b78c7646e29ef381><478a1d1eebd9bba5b78c7646e29ef381>]
66
+ % ReportLab generated PDF document -- digest (opensource)
67
+
68
+ /Info 6 0 R
69
+ /Root 5 0 R
70
+ /Size 9
71
+ >>
72
+ startxref
73
+ 2961
74
+ %%EOF
backend/vectorstores/1/index.faiss ADDED
Binary file (10.8 kB). View file
 
backend/vectorstores/1/index.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7dd3458e48b73fdfd88113db464a644bef16d6b6b2bed0da8f64f5feacbbf002
3
+ size 4699
backend/vectorstores/3/index.faiss ADDED
Binary file (66.1 kB). View file
 
backend/vectorstores/3/index.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4983bd2145b2cf24574513b052f73e43efdab485a01dd4c0a5e7ba574a19c780
3
+ size 19732