Sohan Kshirsagar commited on
Commit
9cef5f3
·
1 Parent(s): e169dd5

Routes refactoring

Browse files
multi_llm_chatbot_backend/app/api/{routes.py → old_routes.py} RENAMED
File without changes
multi_llm_chatbot_backend/app/api/routes/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ from .chat import router as chat_router
3
+ from .documents import router as document_router
4
+ from .sessions import router as session_router
5
+ from .provider import router as provider_router
6
+ from .debug import router as debug_router
7
+ from .root import router as root_router
8
+
9
+ router = APIRouter()
10
+ router.include_router(chat_router)
11
+ router.include_router(document_router)
12
+ router.include_router(session_router)
13
+ router.include_router(provider_router)
14
+ router.include_router(debug_router)
15
+ router.include_router(root_router)
multi_llm_chatbot_backend/app/api/routes/chat.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Request, HTTPException, Body
2
+ from app.models.persona import Persona
3
+ from app.core.session_manager import get_session_manager
4
+ from app.api.utils import get_or_create_session_for_request
5
+ from app.core.bootstrap import chat_orchestrator
6
+ from pydantic import BaseModel
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ router = APIRouter()
12
+
13
+ session_manager = get_session_manager()
14
+
15
+ # Keep all the same data models as before
16
+ class UserInput(BaseModel):
17
+ user_input: str
18
+
19
+ class ChatMessage(BaseModel):
20
+ user_input: str
21
+ session_id: str = None
22
+ response_length: str = "medium"
23
+
24
+ class ReplyToAdvisor(BaseModel):
25
+ user_input: str
26
+ advisor_id: str
27
+ original_message_id: str = None
28
+
29
+ class PersonaQuery(BaseModel):
30
+ question: str
31
+ persona: str
32
+
33
+
34
+ @router.post("/chat-sequential")
35
+ async def chat_sequential_enhanced(message: ChatMessage, request: Request):
36
+ """
37
+ Enhanced sequential chat with intelligent persona ordering.
38
+ Returns responses in the order determined by LLM-based relevance ranking.
39
+ """
40
+ try:
41
+ # Get or create session
42
+ session_id = get_or_create_session_for_request(request, message.session_id)
43
+
44
+ # Add user message to session first (needed for persona ranking)
45
+ session = session_manager.get_session(session_id)
46
+ session.append_message("user", message.user_input)
47
+
48
+ # Get intelligently ordered personas based on context
49
+ top_personas = await chat_orchestrator.get_top_personas(
50
+ session_id=session_id,
51
+ k=3 # Get top 3 most relevant personas
52
+ )
53
+
54
+ logger.info(f"Intelligent persona order for session {session_id}: {top_personas}")
55
+
56
+ # Generate responses from personas in the intelligent order
57
+ responses = []
58
+
59
+ for persona_id in top_personas:
60
+ try:
61
+ # Generate response from this persona
62
+ persona_result = await chat_orchestrator.chat_with_persona(
63
+ user_input=message.user_input,
64
+ persona_id=persona_id,
65
+ session_id=session_id,
66
+ response_length=message.response_length or "medium"
67
+ )
68
+
69
+
70
+ if "persona_name" in persona_result and "response" in persona_result:
71
+ responses.append({
72
+ "persona": persona_result["persona_name"],
73
+ "persona_id": persona_result["persona_id"],
74
+ "response": persona_result["response"]
75
+ })
76
+ elif persona_result.get("type") == "single_persona_response" and "persona" in persona_result:
77
+ persona_data = persona_result["persona"]
78
+ responses.append({
79
+ "persona": persona_data["persona_name"],
80
+ "persona_id": persona_data["persona_id"],
81
+ "response": persona_data["response"]
82
+ })
83
+ else:
84
+ # Fallback response
85
+ responses.append({
86
+ "persona": chat_orchestrator.personas[persona_id].name,
87
+ "persona_id": persona_id,
88
+ "response": "I'm having trouble processing your question right now. Please try again."
89
+ })
90
+
91
+ except Exception as e:
92
+ logger.error(f"Error generating response for persona {persona_id}: {str(e)}")
93
+ # Error fallback
94
+ responses.append({
95
+ "persona": chat_orchestrator.personas[persona_id].name,
96
+ "persona_id": persona_id,
97
+ "response": "I encountered an error while processing your question. Please try again."
98
+ })
99
+
100
+ # response format
101
+ return {
102
+ "type": "sequential_responses",
103
+ "responses": responses
104
+ }
105
+
106
+ except Exception as e:
107
+ logger.error(f"Error in enhanced sequential chat: {str(e)}")
108
+ return {
109
+ "type": "error",
110
+ "responses": [{
111
+ "persona": "System",
112
+ "response": "I'm having trouble processing your request. Could you please try again?"
113
+ }]
114
+ }
115
+
116
+ @router.post("/chat/{persona_id}")
117
+ async def chat_with_specific_advisor(persona_id: str, input: UserInput, request: Request):
118
+ """Chat with a specific advisor - SAME INTERFACE"""
119
+ try:
120
+ if persona_id not in chat_orchestrator.personas:
121
+ raise HTTPException(status_code=404, detail=f"Persona '{persona_id}' not found")
122
+
123
+ # Get session using compatibility layer
124
+ session_id = get_or_create_session_for_request(request)
125
+
126
+ # Use new orchestrator
127
+ result = await chat_orchestrator.chat_with_persona(
128
+ user_input=input.user_input,
129
+ persona_id=persona_id,
130
+ session_id=session_id
131
+ )
132
+
133
+ # FIX: Handle the actual response structure from orchestrator
134
+ if result.get("type") == "single_persona_response" and "persona" in result:
135
+ # New expected structure
136
+ persona_data = result["persona"]
137
+ return {
138
+ "persona": persona_data["persona_name"],
139
+ "persona_id": persona_data["persona_id"],
140
+ "response": persona_data["response"]
141
+ }
142
+ elif "persona_id" in result and "response" in result:
143
+ # Current actual structure from orchestrator
144
+ return {
145
+ "persona": result["persona_name"],
146
+ "persona_id": result["persona_id"],
147
+ "response": result["response"]
148
+ }
149
+ elif result.get("type") == "error" or "error" in result:
150
+ # Error handling
151
+ return {
152
+ "persona": "System",
153
+ "response": result.get("error", "I'm having trouble generating a response right now. Please try again.")
154
+ }
155
+ else:
156
+ # Fallback
157
+ return {
158
+ "persona": "System",
159
+ "response": "I'm having trouble generating a response right now. Please try again."
160
+ }
161
+
162
+ except HTTPException:
163
+ raise
164
+ except Exception as e:
165
+ logger.error(f"Error in chat_with_specific_advisor: {e}")
166
+ return {
167
+ "persona": "System",
168
+ "response": "I'm having trouble generating a response right now. Please try again."
169
+ }
170
+
171
+ # Reply to advisor endpoint (SAME INTERFACE)
172
+ @router.post("/reply-to-advisor")
173
+ async def reply_to_advisor(reply: ReplyToAdvisor, request: Request):
174
+ """Reply to a specific advisor - SAME INTERFACE"""
175
+ try:
176
+ if reply.advisor_id not in chat_orchestrator.personas:
177
+ raise HTTPException(status_code=404, detail=f"Advisor '{reply.advisor_id}' not found")
178
+
179
+ # Get session using compatibility layer
180
+ session_id = get_or_create_session_for_request(request)
181
+
182
+ # Use new orchestrator
183
+ result = await chat_orchestrator.chat_with_persona(
184
+ user_input=reply.user_input,
185
+ persona_id=reply.advisor_id,
186
+ session_id=session_id
187
+ )
188
+
189
+ if result["type"] == "single_persona_response":
190
+ persona_data = result["persona"]
191
+ return {
192
+ "type": "advisor_reply",
193
+ "persona": persona_data["persona_name"],
194
+ "persona_id": persona_data["persona_id"],
195
+ "response": persona_data["response"],
196
+ "original_message_id": reply.original_message_id
197
+ }
198
+ else:
199
+ return {
200
+ "type": "error",
201
+ "persona": "System",
202
+ "response": result.get("message", "I'm having trouble generating a reply right now. Please try again.")
203
+ }
204
+
205
+ except HTTPException:
206
+ raise
207
+ except Exception as e:
208
+ logger.error(f"Error in reply_to_advisor: {e}")
209
+ return {
210
+ "type": "error",
211
+ "persona": "System",
212
+ "response": "I'm having trouble generating a reply right now. Please try again."
213
+ }
214
+
215
+ @router.post("/chat/{persona_id}")
216
+ async def chat_with_specific_persona(persona_id: str, message: ChatMessage, request: Request):
217
+ """
218
+ Chat with a specific persona - Enhanced with RAG debugging
219
+
220
+ This endpoint helps debug RAG integration by testing individual personas
221
+ """
222
+ try:
223
+ session_id = get_or_create_session_for_request(request, message.session_id)
224
+
225
+ # Validate persona exists
226
+ if persona_id not in chat_orchestrator.personas:
227
+ available_personas = list(chat_orchestrator.personas.keys())
228
+ raise HTTPException(
229
+ status_code=400,
230
+ detail=f"Persona '{persona_id}' not found. Available: {available_personas}"
231
+ )
232
+
233
+ # Use the enhanced orchestrator method
234
+ result = await chat_orchestrator.chat_with_persona(
235
+ user_input=message.user_input,
236
+ persona_id=persona_id,
237
+ session_id=session_id,
238
+ response_length=message.response_length or "medium"
239
+ )
240
+
241
+ # Fix: Handle the response structure properly
242
+ if result.get("type") == "single_persona_response" and "persona" in result:
243
+ persona_data = result["persona"]
244
+
245
+ # Add debugging information
246
+ result["debug_info"] = {
247
+ "persona_id": persona_id,
248
+ "session_id": session_id,
249
+ "query_length": len(message.user_input),
250
+ "rag_manager_available": True,
251
+ "used_documents": persona_data.get("used_documents", False),
252
+ "chunks_used": persona_data.get("document_chunks_used", 0)
253
+ }
254
+
255
+ return result
256
+
257
+ except HTTPException:
258
+ raise
259
+ except Exception as e:
260
+ logger.error(f"Error in individual persona chat: {str(e)}")
261
+ return {
262
+ "type": "error",
263
+ "message": f"Error chatting with {persona_id}: {str(e)}",
264
+ "persona_id": persona_id
265
+ }
266
+
267
+ @router.post("/ask/")
268
+ async def ask_question(query: PersonaQuery, request: Request):
269
+ """Ask question - SAME INTERFACE"""
270
+ try:
271
+ session_id = get_or_create_session_for_request(request)
272
+
273
+ # Use the new orchestrator
274
+ result = await chat_orchestrator.chat_with_persona(
275
+ user_input=query.question,
276
+ persona_id=query.persona,
277
+ session_id=session_id
278
+ )
279
+
280
+ if result["type"] == "single_persona_response":
281
+ response_text = result["persona"]["response"]
282
+ else:
283
+ response_text = result.get("message", "I'm having trouble responding right now.")
284
+
285
+ return {"response": response_text}
286
+
287
+ except Exception as e:
288
+ logger.error(f"Error in ask endpoint: {str(e)}")
289
+ return {"response": "I encountered an error. Please try again."}
multi_llm_chatbot_backend/app/api/routes/debug.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Request, Query
2
+ from app.core.session_manager import get_session_manager
3
+ from app.core.rag_manager import get_rag_manager
4
+ from app.api.utils import get_or_create_session_for_request
5
+ from app.core.bootstrap import chat_orchestrator
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ router = APIRouter()
11
+
12
+ session_manager = get_session_manager()
13
+
14
+ @router.get("/debug/personas")
15
+ async def debug_personas(request: Request):
16
+ try:
17
+ session_id = get_or_create_session_for_request(request)
18
+ session = session_manager.get_session(session_id)
19
+ rag_manager = get_rag_manager()
20
+ rag_stats = rag_manager.get_document_stats(session_id)
21
+
22
+ return {
23
+ "personas": {
24
+ pid: {
25
+ "name": persona.name,
26
+ "prompt": persona.system_prompt[:100] + "...",
27
+ "retrieval_keywords": chat_orchestrator._get_persona_context_keywords(pid)
28
+ } for pid, persona in chat_orchestrator.personas.items()
29
+ },
30
+ "session_info": {
31
+ "context_length": len(session.messages),
32
+ "uploaded_files": session.uploaded_files,
33
+ "rag_stats": rag_stats
34
+ }
35
+ }
36
+ except Exception as e:
37
+ logger.error(f"Error in debug endpoint: {str(e)}")
38
+ return {
39
+ "personas": {},
40
+ "session_info": {"context_length": 0},
41
+ "error": str(e)
42
+ }
43
+
44
+ @router.get("/debug/ranked-personas")
45
+ async def get_ranked_personas(request: Request, k: int = Query(3, ge=1, le=10)):
46
+ try:
47
+ session_id = get_or_create_session_for_request(request)
48
+ top_personas = await chat_orchestrator.get_top_personas(session_id=session_id, k=k)
49
+ return {
50
+ "ranked_personas": top_personas,
51
+ "available_personas": list(chat_orchestrator.personas.keys()),
52
+ "session_id": session_id
53
+ }
54
+ except Exception as e:
55
+ logger.error(f"Error in /debug/ranked-personas: {e}")
56
+ return {
57
+ "ranked_personas": [],
58
+ "error": str(e)
59
+ }
60
+
61
+ @router.get("/debug/rag-status")
62
+ async def debug_rag_status(request: Request):
63
+ try:
64
+ session_id = get_or_create_session_for_request(request)
65
+ rag_manager = get_rag_manager()
66
+ session_stats = session_manager.get_session_stats(session_id)
67
+
68
+ test_search = rag_manager.search_documents(
69
+ query="test methodology research",
70
+ session_id=session_id,
71
+ persona_context="",
72
+ n_results=3
73
+ )
74
+
75
+ return {
76
+ "rag_manager_healthy": True,
77
+ "session_id": session_id,
78
+ "session_stats": session_stats.get("rag_stats", {}),
79
+ "test_search_results": len(test_search),
80
+ "test_search_details": [
81
+ {
82
+ "relevance": chunk.get("relevance_score", 0),
83
+ "distance": chunk.get("distance", "unknown"),
84
+ "text_length": len(chunk.get("text", "")),
85
+ "filename": chunk.get("metadata", {}).get("filename", "unknown")
86
+ }
87
+ for chunk in test_search[:3]
88
+ ],
89
+ "persona_keywords": {
90
+ pid: chat_orchestrator._get_persona_context_keywords(pid)
91
+ for pid in chat_orchestrator.personas.keys()
92
+ }
93
+ }
94
+
95
+ except Exception as e:
96
+ logger.error(f"Error in RAG debug: {str(e)}")
97
+ return {
98
+ "rag_manager_healthy": False,
99
+ "error": str(e),
100
+ "session_id": session_id if 'session_id' in locals() else "unknown"
101
+ }
multi_llm_chatbot_backend/app/api/routes/documents.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Request, HTTPException, UploadFile, File, Body
2
+ from fastapi import Query
3
+ from app.utils.document_extractor import extract_text_from_file
4
+ from app.core.session_manager import get_session_manager
5
+ from app.core.rag_manager import get_rag_manager
6
+ from app.api.utils import get_or_create_session_for_request
7
+ from fastapi.responses import StreamingResponse
8
+ from app.utils.chat_summary import generate_summary_from_messages, parse_summary_to_blocks
9
+ from app.utils.file_export import prepare_export_response
10
+ from app.core.session_manager import get_session_manager
11
+ from app.api.utils import get_or_create_session_for_request
12
+ from app.core.bootstrap import chat_orchestrator
13
+ import logging
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ router = APIRouter()
18
+
19
+ session_manager = get_session_manager()
20
+ get_rag_manager = get_rag_manager # avoid circular import issues
21
+
22
+
23
+ @router.post("/upload-document")
24
+ async def upload_document(file: UploadFile = File(...), request: Request = None):
25
+ try:
26
+ session_id = get_or_create_session_for_request(request)
27
+ session = session_manager.get_session(session_id)
28
+
29
+ MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
30
+ if file.size and file.size > MAX_FILE_SIZE:
31
+ raise HTTPException(status_code=413, detail="File size exceeds 10MB limit")
32
+
33
+ file_bytes = await file.read()
34
+ content = extract_text_from_file(file_bytes, file.content_type)
35
+ if not content.strip():
36
+ raise HTTPException(status_code=400, detail="Document is empty or unreadable.")
37
+
38
+ rag_manager = get_rag_manager()
39
+ file_type_map = {
40
+ "application/pdf": "pdf",
41
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
42
+ "text/plain": "txt"
43
+ }
44
+ file_type = file_type_map.get(file.content_type, "unknown")
45
+
46
+ rag_result = rag_manager.add_document(
47
+ content=content,
48
+ filename=file.filename,
49
+ session_id=session_id,
50
+ file_type=file_type
51
+ )
52
+
53
+ if not rag_result["success"]:
54
+ raise HTTPException(status_code=500, detail=f"Failed to process document: {rag_result.get('error', 'Unknown error')}")
55
+
56
+ session.uploaded_files.append(file.filename)
57
+ session.total_upload_size += len(file_bytes)
58
+
59
+ doc_metadata = rag_result.get("document_metadata", {})
60
+ doc_title = doc_metadata.get("title", file.filename)
61
+
62
+ session.append_message(
63
+ "system",
64
+ f"Document uploaded: '{doc_title}' ({file.filename}) - {rag_result['chunks_created']} sections processed, ~{rag_result['total_tokens']} tokens analyzed. You can now ask questions about this document by referencing it by name."
65
+ )
66
+
67
+ return {
68
+ "message": f"Document '{file.filename}' uploaded and processed successfully.",
69
+ "filename": file.filename,
70
+ "document_title": doc_title,
71
+ "chunks_created": rag_result['chunks_created'],
72
+ "total_tokens": rag_result['total_tokens'],
73
+ "file_type": file_type,
74
+ "can_reference_by_name": True
75
+ }
76
+
77
+ except HTTPException:
78
+ raise
79
+ except Exception as e:
80
+ logger.error(f"Error processing document upload: {str(e)}")
81
+ raise HTTPException(status_code=500, detail=f"Error processing document: {str(e)}")
82
+
83
+
84
+ @router.post("/search-documents")
85
+ async def search_documents(request: Request, query: str = Body(..., embed=True), persona: str = Body("", embed=True)):
86
+ try:
87
+ session_id = get_or_create_session_for_request(request)
88
+ rag_manager = get_rag_manager()
89
+
90
+ persona_contexts = {
91
+ "methodologist": "methodology research design analysis",
92
+ "theorist": "theory theoretical framework conceptual",
93
+ "pragmatist": "practical application implementation"
94
+ }
95
+ persona_context = persona_contexts.get(persona, "")
96
+
97
+ results = rag_manager.search_documents(
98
+ query=query,
99
+ session_id=session_id,
100
+ persona_context=persona_context,
101
+ n_results=5
102
+ )
103
+
104
+ return {
105
+ "query": query,
106
+ "persona_filter": persona,
107
+ "results_count": len(results),
108
+ "results": results
109
+ }
110
+
111
+ except Exception as e:
112
+ logger.error(f"Error searching documents: {str(e)}")
113
+ return {"query": query, "results_count": 0, "results": [], "error": str(e)}
114
+
115
+
116
+ @router.get("/document-stats")
117
+ async def get_document_stats(request: Request):
118
+ try:
119
+ session_id = get_or_create_session_for_request(request)
120
+ rag_manager = get_rag_manager()
121
+ return rag_manager.get_document_stats(session_id)
122
+ except Exception as e:
123
+ logger.error(f"Error getting document stats: {str(e)}")
124
+ return {"total_chunks": 0, "total_documents": 0, "documents": []}
125
+
126
+
127
+ @router.get("/uploaded-files")
128
+ async def get_uploaded_filenames(request: Request):
129
+ try:
130
+ session_id = get_or_create_session_for_request(request)
131
+ session = session_manager.get_session(session_id)
132
+ return {"files": session.uploaded_files}
133
+ except Exception as e:
134
+ logger.error(f"Error getting uploaded files: {str(e)}")
135
+ return {"files": []}
136
+
137
+
138
+ @router.get("/document-insights/{filename}")
139
+ async def get_document_insights(filename: str, request: Request):
140
+ try:
141
+ session_id = get_or_create_session_for_request(request)
142
+ rag_manager = get_rag_manager()
143
+ stats = rag_manager.get_document_stats(session_id)
144
+ document_info = next((doc for doc in stats.get("documents", []) if doc["filename"] == filename), None)
145
+
146
+ if not document_info:
147
+ raise HTTPException(status_code=404, detail=f"Document {filename} not found")
148
+
149
+ results = rag_manager.collection.get(
150
+ where={"session_id": session_id, "filename": filename},
151
+ limit=3,
152
+ include=["documents", "metadatas"]
153
+ )
154
+
155
+ sample_sections = []
156
+ if results["documents"]:
157
+ for doc, metadata in zip(results["documents"], results["metadatas"]):
158
+ sample_sections.append({
159
+ "section": metadata.get("document_section", "unknown"),
160
+ "content_preview": doc[:200] + "..." if len(doc) > 200 else doc,
161
+ "keywords": metadata.get("keywords", "")
162
+ })
163
+
164
+ return {
165
+ "filename": filename,
166
+ "document_title": document_info.get("title", filename),
167
+ "file_type": document_info.get("file_type", "unknown"),
168
+ "statistics": {
169
+ "total_chunks": document_info["chunks"],
170
+ "estimated_tokens": document_info["estimated_tokens"],
171
+ "sections_identified": document_info["sections"]
172
+ },
173
+ "content_analysis": {
174
+ "has_methodology": document_info.get("has_methodology", False),
175
+ "has_theory": document_info.get("has_theory", False),
176
+ "has_references": document_info.get("has_references", False)
177
+ },
178
+ "sample_sections": sample_sections
179
+ }
180
+
181
+ except HTTPException:
182
+ raise
183
+ except Exception as e:
184
+ logger.error(f"Error getting document insights: {str(e)}")
185
+ raise HTTPException(status_code=500, detail=f"Error analyzing document: {str(e)}")
186
+
187
+ @router.get("/export-chat")
188
+ async def export_chat(request: Request, format: str = Query(..., regex="^(txt|pdf|docx)$")):
189
+ try:
190
+ session_id = get_or_create_session_for_request(request)
191
+ session = session_manager.get_session(session_id)
192
+
193
+ if not session.messages:
194
+ return {"error": "No messages in this session."}
195
+
196
+ return prepare_export_response(session.messages, format)
197
+
198
+ except Exception as e:
199
+ logger.error(f"Error exporting chat: {str(e)}")
200
+ return {"error": "Failed to export chat.", "detail": str(e)}
201
+
202
+ @router.get("/chat-summary")
203
+ async def chat_summary(request: Request, format: str = Query("text", regex="^(txt|pdf|docx)$")):
204
+ try:
205
+ session_id = get_or_create_session_for_request(request)
206
+ session = session_manager.get_session(session_id)
207
+
208
+ if not session.messages:
209
+ return {"error": "No messages in this session."}
210
+
211
+ llm = next(iter(session_manager.get_session(session_id).messages), {}).get("llm")
212
+ if not llm:
213
+ llm = next(iter(session_manager.sessions.values())).messages[0].get("llm")
214
+
215
+ llm = next(iter(chat_orchestrator.personas.values())).llm
216
+ summary_text = await generate_summary_from_messages(session.messages, llm)
217
+
218
+ if format == "txt":
219
+ return prepare_export_response(summary_text, "txt", filename_prefix="chat_summary")
220
+
221
+ elif format == "docx":
222
+ return prepare_export_response(summary_text, "docx", filename_prefix="chat_summary")
223
+
224
+ elif format == "pdf":
225
+ blocks = [{"type": "heading", "text": "Chat Summary"}] + parse_summary_to_blocks(summary_text)
226
+ file_stream = prepare_export_response(summary_text, "pdf", filename_prefix="chat_summary").body_iterator
227
+ return StreamingResponse(
228
+ file_stream,
229
+ media_type="application/pdf",
230
+ headers={"Content-Disposition": "attachment; filename=chat_summary.pdf"}
231
+ )
232
+
233
+ except Exception as e:
234
+ logger.error(f"Error in chat-summary endpoint: {str(e)}")
235
+ return {"error": "Summary generation failed", "detail": str(e)}
236
+
multi_llm_chatbot_backend/app/api/routes/provider.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Body, HTTPException
2
+ from app.llm.improved_gemini_client import ImprovedGeminiClient
3
+ from app.llm.improved_ollama_client import ImprovedOllamaClient
4
+ from app.models.default_personas import get_default_personas
5
+ from app.core.bootstrap import chat_orchestrator, llm, current_provider, available_providers
6
+ from pydantic import BaseModel
7
+ import os
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ router = APIRouter()
13
+
14
+ def create_llm_client(provider: str = None):
15
+ global current_provider
16
+ if provider is None:
17
+ provider = current_provider
18
+
19
+ if provider == "gemini":
20
+ try:
21
+ return ImprovedGeminiClient(model_name=os.getenv("GEMINI_MODEL"))
22
+ except ValueError as e:
23
+ logger.warning(f"Gemini API key not found, falling back to Ollama: {e}")
24
+ return ImprovedOllamaClient(model_name="llama3.2:1b")
25
+ elif provider == "ollama":
26
+ return ImprovedOllamaClient(model_name="llama3.2:1b")
27
+ else:
28
+ raise ValueError(f"Unknown provider: {provider}")
29
+
30
+ # Initialize LLM and personas
31
+ llm = create_llm_client(current_provider)
32
+ DEFAULT_PERSONAS = get_default_personas(llm)
33
+ for persona in DEFAULT_PERSONAS:
34
+ chat_orchestrator.register_persona(persona)
35
+
36
+ class ProviderSwitch(BaseModel):
37
+ provider: str
38
+
39
+ @router.get("/current-provider")
40
+ async def get_current_provider():
41
+ return {
42
+ "current_provider": current_provider,
43
+ "available_providers": available_providers,
44
+ "model_info": {
45
+ "name": llm.model_name if hasattr(llm, 'model_name') else "gemini-2.0-flash",
46
+ "provider": current_provider
47
+ }
48
+ }
49
+
50
+ @router.post("/switch-provider")
51
+ async def switch_provider(provider_data: ProviderSwitch):
52
+ global current_provider, llm
53
+
54
+ if provider_data.provider not in available_providers:
55
+ raise HTTPException(status_code=400, detail=f"Unknown provider: {provider_data.provider}. Available: {available_providers}")
56
+
57
+ try:
58
+ current_provider = provider_data.provider
59
+ new_llm = create_llm_client(current_provider)
60
+ llm = new_llm
61
+
62
+ new_personas = get_default_personas(new_llm)
63
+ chat_orchestrator.personas.clear()
64
+ for persona in new_personas:
65
+ chat_orchestrator.register_persona(persona)
66
+
67
+ return {
68
+ "message": f"Successfully switched to {current_provider}",
69
+ "current_provider": current_provider,
70
+ "model_info": {
71
+ "name": new_llm.model_name if hasattr(new_llm, 'model_name') else "gemini-2.0-flash",
72
+ "provider": current_provider
73
+ }
74
+ }
75
+
76
+ except Exception as e:
77
+ raise HTTPException(status_code=500, detail=f"Failed to switch to {provider_data.provider}: {str(e)}")
78
+
79
+ @router.post("/switch-model")
80
+ async def switch_model(model_name: str = Body(...)):
81
+ if "gemini" in model_name.lower():
82
+ return await switch_provider(ProviderSwitch(provider="gemini"))
83
+ else:
84
+ return await switch_provider(ProviderSwitch(provider="ollama"))
85
+
86
+ @router.get("/current-model")
87
+ async def get_current_model():
88
+ model_name = llm.model_name if hasattr(llm, 'model_name') else "gemini-2.0-flash"
89
+ return {
90
+ "model": model_name,
91
+ "provider": current_provider
92
+ }
multi_llm_chatbot_backend/app/api/routes/root.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ router = APIRouter()
8
+
9
+ @router.get("/")
10
+ def root():
11
+ return {
12
+ "message": "Multi-LLM PhD Advisor Backend is up and running",
13
+ "version": "1.0.0",
14
+ "features": [
15
+ "Improved Session Management",
16
+ "Unified Context Handling",
17
+ "Ollama Support",
18
+ "Gemini API Support",
19
+ "Provider Switching"
20
+ ]
21
+ }
22
+
multi_llm_chatbot_backend/app/api/routes/sessions.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Request, HTTPException
2
+ from app.core.session_manager import get_session_manager
3
+ from app.api.utils import get_or_create_session_for_request
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ router = APIRouter()
9
+
10
+ session_manager = get_session_manager()
11
+
12
+ @router.get("/context")
13
+ async def get_context(request: Request):
14
+ try:
15
+ session_id = get_or_create_session_for_request(request)
16
+ session = session_manager.get_session(session_id)
17
+ rag_stats = session.get_rag_stats()
18
+ return {
19
+ "messages": session.messages,
20
+ "rag_info": {
21
+ "total_documents": rag_stats.get("total_documents", 0),
22
+ "total_chunks": rag_stats.get("total_chunks", 0),
23
+ "documents": rag_stats.get("documents", [])
24
+ }
25
+ }
26
+ except Exception as e:
27
+ logger.error(f"Error getting context: {str(e)}")
28
+ return {"messages": [], "rag_info": {"total_documents": 0, "total_chunks": 0}}
29
+
30
+
31
+ @router.post("/reset-session")
32
+ async def reset_session(request: Request):
33
+ try:
34
+ session_id = get_or_create_session_for_request(request)
35
+ success = session_manager.reset_session_completely(session_id)
36
+
37
+ if success:
38
+ return {"status": "reset", "message": "Session and all documents reset successfully"}
39
+ else:
40
+ return {"status": "error", "message": "Failed to reset session"}
41
+ except Exception as e:
42
+ logger.error(f"Error resetting session: {e}")
43
+ return {"status": "error", "message": "Failed to reset session"}
44
+
45
+
46
+ @router.get("/session-stats")
47
+ async def get_session_stats(request: Request):
48
+ try:
49
+ session_id = get_or_create_session_for_request(request)
50
+ stats = session_manager.get_session_stats(session_id)
51
+ return stats
52
+ except Exception as e:
53
+ logger.error(f"Error getting session stats: {str(e)}")
54
+ return {"error": str(e)}
multi_llm_chatbot_backend/app/api/utils.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from fastapi import Request
3
+ from app.core.session_manager import get_session_manager
4
+
5
+ session_manager = get_session_manager()
6
+
7
+ def get_or_create_session_for_request(request: Request, session_id_override: Optional[str] = None) -> str:
8
+ """
9
+ Get or create session for request using multiple strategies:
10
+ 1. Explicit session ID
11
+ 2. X-Session-ID header
12
+ 3. Client IP fallback
13
+ """
14
+ if session_id_override:
15
+ return session_id_override
16
+
17
+ session_header = request.headers.get("X-Session-ID")
18
+ if session_header:
19
+ return session_header
20
+
21
+ client_ip = request.client.host if request.client else "unknown"
22
+ ip_session_id = f"ip_{client_ip}"
23
+ session = session_manager.get_session(ip_session_id)
24
+ return session.session_id
multi_llm_chatbot_backend/app/core/bootstrap.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/core/bootstrap.py
2
+ import os
3
+ from app.llm.improved_gemini_client import ImprovedGeminiClient
4
+ from app.llm.improved_ollama_client import ImprovedOllamaClient
5
+ from app.core.improved_orchestrator import ImprovedChatOrchestrator
6
+ from app.models.default_personas import get_default_personas
7
+
8
+ current_provider = "gemini"
9
+ available_providers = ["ollama", "gemini"]
10
+
11
+ def create_llm_client(provider=None):
12
+ if provider is None:
13
+ provider = current_provider
14
+ if provider == "gemini":
15
+ return ImprovedGeminiClient(model_name=os.getenv("GEMINI_MODEL"))
16
+ else:
17
+ return ImprovedOllamaClient(model_name="llama3.2:1b")
18
+
19
+ llm = create_llm_client()
20
+ chat_orchestrator = ImprovedChatOrchestrator()
21
+
22
+ DEFAULT_PERSONAS = get_default_personas(llm)
23
+ for persona in DEFAULT_PERSONAS:
24
+ chat_orchestrator.register_persona(persona)