soorajaryan007 commited on
Commit
084284b
·
verified ·
1 Parent(s): eec64ad

Create backend/main.py

Browse files
Files changed (1) hide show
  1. backend/main.py +308 -0
backend/main.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RAG + Voice + MCP Chatbot Backend - FastAPI
3
+ LangChain + LangGraph + Groq (UPDATED)
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import logging
9
+ from typing import Optional, List, TypedDict
10
+
11
+ import numpy as np
12
+ from fastapi import FastAPI, HTTPException, UploadFile, File
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from fastapi.responses import HTMLResponse, FileResponse
15
+ from pydantic import BaseModel
16
+ from dotenv import load_dotenv
17
+
18
+ # LangChain / LangGraph
19
+ from langchain_groq import ChatGroq
20
+ from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
21
+ from langgraph.graph import StateGraph, END
22
+
23
+ load_dotenv()
24
+
25
+ logging.basicConfig(level=logging.INFO)
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # =========================================================
29
+ # FastAPI App
30
+ # =========================================================
31
+ app = FastAPI(title="Voice RAG Chatbot", version="2.0.0")
32
+
33
+ app.add_middleware(
34
+ CORSMiddleware,
35
+ allow_origins=["*"],
36
+ allow_credentials=True,
37
+ allow_methods=["*"],
38
+ allow_headers=["*"],
39
+ )
40
+
41
+ # =========================================================
42
+ # RAG Engine (unchanged, your implementation is good)
43
+ # =========================================================
44
+ class RAGEngine:
45
+ def __init__(self):
46
+ self.documents: List[dict] = []
47
+ self.embeddings: Optional[np.ndarray] = None
48
+ self.model = None
49
+ self._initialized = False
50
+
51
+ def _lazy_init(self):
52
+ if self._initialized:
53
+ return
54
+ try:
55
+ from sentence_transformers import SentenceTransformer
56
+ self.model = SentenceTransformer("all-MiniLM-L6-v2")
57
+ self._initialized = True
58
+ logger.info("RAG engine initialized")
59
+ except Exception as e:
60
+ logger.error(f"RAG init error: {e}")
61
+
62
+ def add_documents(self, texts: List[str], source: str = "upload"):
63
+ self._lazy_init()
64
+ if not self.model:
65
+ return False
66
+
67
+ chunks = []
68
+ for text in texts:
69
+ for i in range(0, len(text), 450):
70
+ chunk = text[i:i + 450].strip()
71
+ if chunk:
72
+ chunks.append({"text": chunk, "source": source})
73
+
74
+ if not chunks:
75
+ return False
76
+
77
+ new_embeddings = self.model.encode([c["text"] for c in chunks])
78
+ self.documents.extend(chunks)
79
+
80
+ if self.embeddings is None:
81
+ self.embeddings = new_embeddings
82
+ else:
83
+ self.embeddings = np.vstack([self.embeddings, new_embeddings])
84
+
85
+ logger.info(f"Added {len(chunks)} chunks from {source}")
86
+ return True
87
+
88
+ def retrieve(self, query: str, top_k: int = 4) -> List[str]:
89
+ if not self.documents or self.embeddings is None:
90
+ return []
91
+
92
+ self._lazy_init()
93
+ if not self.model:
94
+ return []
95
+
96
+ q_emb = self.model.encode([query])
97
+
98
+ norms = np.linalg.norm(self.embeddings, axis=1, keepdims=True) + 1e-9
99
+ normed = self.embeddings / norms
100
+ q_norm = q_emb / (np.linalg.norm(q_emb) + 1e-9)
101
+
102
+ scores = normed @ q_norm.T
103
+ top_idx = np.argsort(scores[:, 0])[::-1][:top_k]
104
+ return [self.documents[i]["text"] for i in top_idx]
105
+
106
+ def clear(self):
107
+ self.documents = []
108
+ self.embeddings = None
109
+
110
+
111
+ rag = RAGEngine()
112
+
113
+ # =========================================================
114
+ # MCP Tool Registry
115
+ # =========================================================
116
+ class MCPToolRegistry:
117
+ def __init__(self):
118
+ self.tools = {}
119
+
120
+ def register(self, name: str, description: str, handler):
121
+ self.tools[name] = {"description": description, "handler": handler}
122
+
123
+ def get_tool_descriptions(self) -> str:
124
+ return "\n".join(
125
+ f"- {name}: {info['description']}"
126
+ for name, info in self.tools.items()
127
+ )
128
+
129
+ async def run(self, name: str, args: dict) -> str:
130
+ if name not in self.tools:
131
+ return f"Tool '{name}' not found."
132
+ try:
133
+ return str(await self.tools[name]["handler"](**args))
134
+ except Exception as e:
135
+ return f"Tool error: {e}"
136
+
137
+
138
+ mcp = MCPToolRegistry()
139
+
140
+ # ---------------- Tools ----------------
141
+ async def tool_current_time() -> str:
142
+ from datetime import datetime
143
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
144
+
145
+
146
+ async def tool_word_count(text: str = "") -> str:
147
+ return f"{len(text.split())} words, {len(text)} characters"
148
+
149
+
150
+ async def tool_search_docs(query: str = "") -> str:
151
+ results = rag.retrieve(query, top_k=3)
152
+ if not results:
153
+ return "No documents found."
154
+ return "\n---\n".join(results[:2])
155
+
156
+
157
+ mcp.register("get_time", "Get current time", tool_current_time)
158
+ mcp.register("word_count", "Count words in text", tool_word_count)
159
+ mcp.register("search_docs", "Search knowledge base", tool_search_docs)
160
+
161
+ # =========================================================
162
+ # Groq LLM via LangChain (FIXED MODEL)
163
+ # =========================================================
164
+ api_key = os.getenv("GROQ_API_KEY")
165
+ if not api_key:
166
+ raise RuntimeError("❌ GROQ_API_KEY not set")
167
+
168
+ llm = ChatGroq(
169
+ model="llama-3.1-8b-instant", # ✅ NOT deprecated
170
+ groq_api_key=api_key,
171
+ temperature=0.7,
172
+ )
173
+
174
+ # =========================================================
175
+ # LangGraph Agent
176
+ # =========================================================
177
+ class AgentState(TypedDict):
178
+ messages: List
179
+
180
+
181
+ def agent_node(state: AgentState):
182
+ response = llm.invoke(state["messages"])
183
+ return {"messages": state["messages"] + [response]}
184
+
185
+
186
+ graph = StateGraph(AgentState)
187
+ graph.add_node("agent", agent_node)
188
+ graph.set_entry_point("agent")
189
+ graph.add_edge("agent", END)
190
+
191
+ app_graph = graph.compile()
192
+
193
+ # =========================================================
194
+ # Conversation Store
195
+ # =========================================================
196
+ conversations: dict = {}
197
+
198
+ # =========================================================
199
+ # Schemas
200
+ # =========================================================
201
+ class ChatRequest(BaseModel):
202
+ message: str
203
+ session_id: str = "default"
204
+ use_rag: bool = True
205
+
206
+
207
+ class ChatResponse(BaseModel):
208
+ reply: str
209
+ sources: List[str] = []
210
+ session_id: str
211
+
212
+
213
+ # =========================================================
214
+ # Routes
215
+ # =========================================================
216
+ @app.get("/", response_class=HTMLResponse)
217
+ async def root():
218
+ return FileResponse("frontend/index.html")
219
+
220
+
221
+ @app.post("/chat", response_model=ChatResponse)
222
+ async def chat(req: ChatRequest):
223
+ session_id = req.session_id
224
+ if session_id not in conversations:
225
+ conversations[session_id] = []
226
+
227
+ history = conversations[session_id]
228
+
229
+ # -------- RAG --------
230
+ rag_context = ""
231
+ sources = []
232
+
233
+ if req.use_rag and rag.documents:
234
+ retrieved = rag.retrieve(req.message)
235
+ if retrieved:
236
+ sources = retrieved[:3]
237
+ rag_context = "\n\n[Knowledge Base]\n" + "\n---\n".join(retrieved[:3])
238
+
239
+ # -------- Build messages --------
240
+ system_text = (
241
+ "You are a helpful AI assistant.\n"
242
+ f"{rag_context}\n\n"
243
+ f"Tools available:\n{mcp.get_tool_descriptions()}"
244
+ )
245
+
246
+ lc_messages = [SystemMessage(content=system_text)]
247
+
248
+ for h in history[-10:]:
249
+ if h["role"] == "user":
250
+ lc_messages.append(HumanMessage(content=h["content"]))
251
+ else:
252
+ lc_messages.append(AIMessage(content=h["content"]))
253
+
254
+ lc_messages.append(HumanMessage(content=req.message))
255
+
256
+ # -------- LangGraph invoke --------
257
+ try:
258
+ result = app_graph.invoke({"messages": lc_messages})
259
+ reply = result["messages"][-1].content
260
+ except Exception as e:
261
+ raise HTTPException(status_code=500, detail=str(e))
262
+
263
+ # -------- Save history --------
264
+ history.append({"role": "user", "content": req.message})
265
+ history.append({"role": "assistant", "content": reply})
266
+
267
+ conversations[session_id] = history[-40:]
268
+
269
+ return ChatResponse(
270
+ reply=reply,
271
+ sources=sources,
272
+ session_id=session_id,
273
+ )
274
+
275
+
276
+ @app.post("/upload")
277
+ async def upload_document(file: UploadFile = File(...)):
278
+ content = await file.read()
279
+ filename = file.filename or "document"
280
+ texts = []
281
+
282
+ try:
283
+ if filename.endswith(".pdf"):
284
+ import PyPDF2, io
285
+ reader = PyPDF2.PdfReader(io.BytesIO(content))
286
+ for page in reader.pages:
287
+ t = page.extract_text()
288
+ if t:
289
+ texts.append(t)
290
+ else:
291
+ texts.append(content.decode("utf-8", errors="ignore"))
292
+ except Exception as e:
293
+ raise HTTPException(status_code=400, detail=str(e))
294
+
295
+ success = rag.add_documents(texts, source=filename)
296
+ if not success:
297
+ raise HTTPException(status_code=500, detail="Indexing failed")
298
+
299
+ return {"message": f"Indexed {filename}", "chunks": len(rag.documents)}
300
+
301
+
302
+ @app.get("/status")
303
+ async def status():
304
+ return {
305
+ "status": "online",
306
+ "rag_chunks": len(rag.documents),
307
+ "model": "llama-3.1-8b-instant",
308
+ }