Girish Jeswani commited on
Commit
5ff4a89
·
1 Parent(s): e285e4c

remove redundant code

Browse files
multi_llm_chatbot_backend/app/api/routes_old.py DELETED
@@ -1,404 +0,0 @@
1
- import os
2
- from fastapi import APIRouter, Body, HTTPException
3
- import httpx
4
- from app.llm.llm_client import LLMClient
5
- from app.llm.gemini_client import GeminiClient
6
- from app.llm.short_ollama_client import ShortResponseOllamaClient
7
- from app.models.persona import Persona
8
- from app.core.orchestrator import ChatOrchestrator
9
- from app.core.seamless_orchestrator import SeamlessOrchestrator
10
- from app.core.context import GlobalSessionContext
11
- from app.models.default_personas import get_default_personas
12
- from pydantic import BaseModel
13
- from typing import Optional, List
14
- from fastapi import UploadFile, File
15
- from app.utils.document_extractor import extract_text_from_file
16
- from app.core.orchestrator import answer_with_persona_context
17
- from app.utils.chroma_client import add_persona_doc
18
- import hashlib
19
- from app.utils.file_limits import is_within_upload_limit
20
-
21
- router = APIRouter()
22
-
23
- # Provider management
24
- current_provider = "gemini"
25
- available_providers = ["ollama", "gemini"]
26
-
27
- def create_llm_client(provider: str = None) -> LLMClient:
28
- """Create LLM client based on provider"""
29
- if provider is None:
30
- provider = current_provider
31
-
32
- if provider == "gemini":
33
- try:
34
- return GeminiClient(model_name=os.getenv("GEMINI_MODEL"))
35
- except ValueError as e:
36
- # Fallback to Ollama if Gemini API key is not available
37
- print(f"Gemini API key not found, falling back to Ollama: {e}")
38
- return ShortResponseOllamaClient(model_name="llama3.2:1b")
39
- elif provider == "ollama":
40
- return ShortResponseOllamaClient(model_name="llama3.2:1b")
41
- else:
42
- raise ValueError(f"Unknown provider: {provider}")
43
-
44
- # Initialize with default provider
45
- llm = create_llm_client()
46
- chat_orchestrator = ChatOrchestrator()
47
- seamless_orchestrator = SeamlessOrchestrator(llm=llm)
48
-
49
- session_context = GlobalSessionContext()
50
-
51
- # Initialize personas
52
- DEFAULT_PERSONAS = get_default_personas(llm)
53
-
54
- for persona in DEFAULT_PERSONAS:
55
- chat_orchestrator.register_persona(persona)
56
-
57
- # Data models
58
- class UserInput(BaseModel):
59
- user_input: str
60
-
61
- class PersonaInput(BaseModel):
62
- id: str
63
- name: str
64
- system_prompt: str
65
-
66
- class ChatMessage(BaseModel):
67
- user_input: str
68
- session_id: Optional[str] = None
69
- response_length: Optional[str] = "medium"
70
-
71
- class ReplyToAdvisor(BaseModel):
72
- user_input: str
73
- advisor_id: str
74
- original_message_id: Optional[str] = None
75
-
76
- class ProviderSwitch(BaseModel):
77
- provider: str
78
-
79
- # Helper functions for response validation
80
- def _is_valid_response(response: str, persona_id: str) -> bool:
81
- """Validate response quality"""
82
- if len(response) < 2 or len(response) > 5000:
83
- return False
84
-
85
- # Check for AI confusion indicators
86
- confusion_indicators = [
87
- f"Thank you, Dr. {persona_id.title()}",
88
- "Assistant:",
89
- f"Dr. {persona_id.title()} Advisor:",
90
- "excellent discussion, Assistant"
91
- ]
92
-
93
- return not any(indicator in response for indicator in confusion_indicators)
94
-
95
- def _get_persona_fallback(persona_id: str) -> str:
96
- """Get persona-specific fallback responses"""
97
- fallbacks = {
98
- "methodologist": "Focus on ensuring your methodology aligns with your research question. What specific method are you considering?",
99
- "theorist": "Consider the theoretical framework underlying your approach. What assumptions guide your thinking?",
100
- "pragmatist": "Let's break this down into actionable steps. What's the most important thing you need to decide today?"
101
- }
102
- return fallbacks.get(persona_id, "I'd be happy to help. Could you provide more details?")
103
-
104
- # Provider management endpoints
105
- @router.get("/current-provider")
106
- async def get_current_provider():
107
- return {
108
- "current_provider": current_provider,
109
- "available_providers": available_providers,
110
- "model_info": {
111
- "name": llm.model_name if hasattr(llm, 'model_name') else "gemini-2.0-flash",
112
- "provider": current_provider
113
- }
114
- }
115
-
116
- @router.post("/switch-provider")
117
- async def switch_provider(provider_data: ProviderSwitch):
118
- global current_provider, llm
119
-
120
- if provider_data.provider not in available_providers:
121
- raise HTTPException(
122
- status_code=400,
123
- detail=f"Unknown provider: {provider_data.provider}. Available: {available_providers}"
124
- )
125
-
126
- try:
127
- # Update current provider
128
- current_provider = provider_data.provider
129
-
130
- # Create new LLM client
131
- new_llm = create_llm_client(current_provider)
132
- llm = new_llm
133
-
134
- # Update all personas with new LLM
135
- new_personas = get_default_personas(new_llm)
136
- chat_orchestrator.personas.clear()
137
- for persona in new_personas:
138
- chat_orchestrator.register_persona(persona)
139
-
140
- # Update seamless orchestrator
141
- seamless_orchestrator.llm = new_llm
142
-
143
- return {
144
- "message": f"Successfully switched to {current_provider}",
145
- "current_provider": current_provider,
146
- "model_info": {
147
- "name": new_llm.model_name if hasattr(new_llm, 'model_name') else "gemini-2.0-flash",
148
- "provider": current_provider
149
- }
150
- }
151
-
152
- except Exception as e:
153
- raise HTTPException(
154
- status_code=500,
155
- detail=f"Failed to switch to {provider_data.provider}: {str(e)}"
156
- )
157
-
158
- # Sequential advisor responses endpoint
159
- @router.post("/chat-sequential")
160
- async def chat_sequential(message: ChatMessage):
161
- """Generate advisor responses with improved quality controls"""
162
-
163
- try:
164
- orchestrator_result = await seamless_orchestrator.process_message(message.user_input)
165
-
166
- if orchestrator_result["status"] == "orchestrator_asking":
167
- return {
168
- "type": "orchestrator_question",
169
- "responses": [{
170
- "persona": "PhD Advisor Assistant",
171
- "response": orchestrator_result["orchestrator_question"]
172
- }],
173
- "collected_info": orchestrator_result["collected_info"]
174
- }
175
-
176
- elif orchestrator_result["status"] == "ready_for_advisors":
177
- enhanced_context = orchestrator_result["enhanced_context"]
178
-
179
- # Clear previous advisor responses to avoid confusion
180
- session_context.clear()
181
- session_context.append("user", message.user_input)
182
- session_context.append("orchestrator", enhanced_context)
183
-
184
- advisor_order = chat_orchestrator.get_response_order()
185
- print("Advisor Order:")
186
- print(advisor_order)
187
- responses = []
188
-
189
- for persona_id in advisor_order:
190
- try:
191
- persona = chat_orchestrator.personas[persona_id]
192
- reply = await persona.respond(session_context.full_log, response_length="medium")
193
- print("Replies:")
194
- print(reply)
195
-
196
- # Validate response before adding
197
- if _is_valid_response(reply, persona_id):
198
- responses.append({
199
- "persona": persona.name,
200
- "persona_id": persona_id,
201
- "response": reply,
202
- })
203
- else:
204
- # Fallback response for invalid responses
205
- responses.append({
206
- "persona": persona.name,
207
- "persona_id": persona_id,
208
- "response": _get_persona_fallback(persona_id),
209
- })
210
-
211
- session_context.append(persona_id, reply)
212
-
213
- except Exception as e:
214
- print(f"Error generating response for {persona_id}: {e}")
215
- responses.append({
216
- "persona": chat_orchestrator.personas[persona_id].name,
217
- "persona_id": persona_id,
218
- "response": _get_persona_fallback(persona_id),
219
- })
220
-
221
- print("Response Block: " )
222
- print(responses)
223
- return {
224
- "type": "sequential_responses",
225
- "responses": responses,
226
- "collected_info": orchestrator_result["collected_info"]
227
- }
228
-
229
- except Exception as e:
230
- print(f"Error in chat_sequential: {e}")
231
- return {
232
- "type": "error",
233
- "responses": [{
234
- "persona": "System",
235
- "response": "I'm having trouble processing your request. Could you please try again?"
236
- }]
237
- }
238
-
239
-
240
- # Individual advisor endpoint with context
241
- @router.post("/chat/{persona_id}")
242
- async def chat_with_specific_advisor(persona_id: str, input: UserInput):
243
- """Chat with a specific advisor"""
244
- try:
245
- if persona_id not in chat_orchestrator.personas:
246
- raise HTTPException(status_code=404, detail=f"Persona '{persona_id}' not found")
247
-
248
- session_context.append("user", input.user_input)
249
- persona = chat_orchestrator.personas[persona_id]
250
- context = session_context.full_log.copy()
251
- reply = await persona.respond(context, response_length="medium")
252
- session_context.append(persona_id, reply)
253
-
254
- return {
255
- "persona": persona.name,
256
- "persona_id": persona_id,
257
- "response": reply
258
- }
259
- except HTTPException:
260
- raise
261
- except Exception as e:
262
- print(f"Error in chat_with_specific_advisor: {e}")
263
- return {
264
- "persona": "System",
265
- "response": "I'm having trouble generating a response right now. Please try again."
266
- }
267
-
268
- # Reply to specific advisor endpoint
269
- @router.post("/reply-to-advisor")
270
- async def reply_to_advisor(reply: ReplyToAdvisor):
271
- """Reply to a specific advisor and get response only from that advisor"""
272
-
273
- try:
274
- if reply.advisor_id not in chat_orchestrator.personas:
275
- raise HTTPException(status_code=404, detail=f"Advisor '{reply.advisor_id}' not found")
276
-
277
- # Add user reply to context
278
- session_context.append("user", reply.user_input)
279
-
280
- # Get response from specific advisor
281
- persona = chat_orchestrator.personas[reply.advisor_id]
282
-
283
- # Generate response
284
- reply_response = await persona.respond(session_context.full_log, response_length="medium")
285
- session_context.append(reply.advisor_id, reply_response)
286
-
287
- return {
288
- "type": "advisor_reply",
289
- "persona": persona.name,
290
- "persona_id": reply.advisor_id,
291
- "response": reply_response,
292
- "original_message_id": reply.original_message_id
293
- }
294
-
295
- except HTTPException:
296
- raise
297
- except Exception as e:
298
- print(f"Error in reply_to_advisor: {e}")
299
- return {
300
- "type": "error",
301
- "persona": "System",
302
- "response": "I'm having trouble generating a reply right now. Please try again."
303
- }
304
-
305
- # Reset session
306
- @router.post("/reset-session")
307
- async def reset_session():
308
- try:
309
- seamless_orchestrator.reset()
310
- session_context.clear()
311
- return {"status": "reset", "message": "Session reset successfully"}
312
- except Exception as e:
313
- print(f"Error resetting session: {e}")
314
- return {"status": "error", "message": "Failed to reset session"}
315
-
316
- # Context inspection
317
- @router.get("/context")
318
- def get_context():
319
- return session_context.full_log
320
-
321
- # Legacy model switching endpoint (now redirects to provider switching)
322
- @router.post("/switch-model")
323
- async def switch_model(model_name: str = Body(...)):
324
- # For backward compatibility, try to map model names to providers
325
- if "gemini" in model_name.lower():
326
- return await switch_provider(ProviderSwitch(provider="gemini"))
327
- else:
328
- return await switch_provider(ProviderSwitch(provider="ollama"))
329
-
330
- @router.get("/current-model")
331
- async def get_current_model():
332
- # For backward compatibility
333
- model_name = llm.model_name if hasattr(llm, 'model_name') else "gemini-2.0-flash"
334
- return {
335
- "model": model_name,
336
- "provider": current_provider
337
- }
338
-
339
- @router.post("/upload-document")
340
- async def upload_document(file: UploadFile = File(...)):
341
- # Validate file type
342
- if file.content_type not in [
343
- "application/pdf",
344
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
345
- "text/plain"
346
- ]:
347
- raise HTTPException(status_code=400, detail="Unsupported file type.")
348
-
349
- try:
350
- # Read file bytes
351
- file_bytes = await file.read()
352
-
353
- # Check file size limit
354
- if not is_within_upload_limit("default", file_bytes, session_context):
355
- raise HTTPException(status_code=400, detail="Upload exceeds session document size limit (10 MB).")
356
-
357
- # Extract and validate text
358
- content = extract_text_from_file(file_bytes, file.content_type)
359
- if not content.strip():
360
- raise HTTPException(status_code=400, detail="Document is empty or unreadable.")
361
-
362
- # Track file size and name
363
- session_context.append("Document", f"[Uploaded Document Content]\n{content.strip()}")
364
- session_context.uploaded_files.append(file.filename)
365
- session_context.total_upload_size += len(file_bytes)
366
-
367
- return {"message": "Document uploaded and added to context successfully."}
368
-
369
- except Exception as e:
370
- raise HTTPException(status_code=500, detail=f"Error processing document: {str(e)}")
371
-
372
- # Debug endpoint
373
- @router.get("/debug/personas")
374
- async def debug_personas():
375
- return {
376
- "personas": {
377
- pid: {
378
- "name": persona.name,
379
- "prompt": persona.system_prompt[:100] + "..."
380
- } for pid, persona in chat_orchestrator.personas.items()
381
- },
382
- "context_length": len(session_context.full_log),
383
- "current_provider": current_provider
384
- }
385
-
386
- class PersonaQuery(BaseModel):
387
- question: str
388
- persona: str
389
-
390
- @router.post("/ask/")
391
- async def ask_question(query: PersonaQuery):
392
- response = await answer_with_persona_context(query.question, query.persona)
393
-
394
- # Store Q&A in vector DB
395
- combined_text = f"Q: {query.question}\nA: {response}"
396
- doc_id = hashlib.md5(combined_text.encode()).hexdigest() # Create a unique doc ID
397
-
398
- add_persona_doc(combined_text, query.persona, doc_id)
399
-
400
- return {"response": response}
401
-
402
- @router.get("/uploaded-files")
403
- def get_uploaded_filenames():
404
- return {"files": session_context.uploaded_files}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
multi_llm_chatbot_backend/app/core/orchestrator.py DELETED
@@ -1,41 +0,0 @@
1
- from app.models.persona import Persona
2
- from typing import List
3
- from app.utils.chroma_client import query_persona_knowledge
4
- from app.llm.gemini_client import GeminiClient
5
- from app.llm.llm_client import LLMClient
6
-
7
- class ChatOrchestrator:
8
- def __init__(self):
9
- self.personas: dict[str, Persona] = {}
10
- self.active_personas: List[str] = [] # renamed for clarity
11
-
12
- def register_persona(self, persona: Persona):
13
- self.personas[persona.id] = persona
14
-
15
- def set_active_personas(self, ids: List[str]):
16
- self.active_personas = [pid for pid in ids if pid in self.personas]
17
-
18
- def get_active_personas(self) -> List[str]:
19
- return self.active_personas
20
-
21
- def get_response_order(self) ->List[str]:
22
-
23
- # I have created this function to be a placeholder for the actual logic of response sequencing
24
- # This logic can be replaced with something smarter like a LLM deciding order based on chat context
25
-
26
- return self.personas
27
-
28
-
29
-
30
- async def answer_with_persona_context(question: str, persona: str) -> str:
31
- llm: LLMClient = GeminiClient()
32
- context_chunks = query_persona_knowledge(question, persona)
33
- context = "\n".join(context_chunks) if context_chunks else "No relevant info found."
34
-
35
- system_prompt = f"You are a helpful assistant responding as a {persona}."
36
- message_context = [
37
- {"role": "system", "content": system_prompt},
38
- {"role": "user", "content": f"Context:\n{context}\n\nQuestion:\n{question}"}
39
- ]
40
-
41
- return await llm.generate(system_prompt, message_context)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
multi_llm_chatbot_backend/app/llm/gemini_client.py DELETED
@@ -1,155 +0,0 @@
1
- import httpx
2
- import os
3
- from typing import List
4
- from app.llm.llm_client import LLMClient
5
-
6
- class GeminiClient(LLMClient):
7
- def __init__(self, model_name: str = None):
8
- if model_name is None:
9
- model_name = os.getenv("GEMINI_MODEL", "gemini-2.0-flash-exp")
10
-
11
- self.model_name = model_name
12
- self.api_key = os.getenv("GEMINI_API_KEY")
13
- print("DEBUG: GEMINI_API_KEY =", os.getenv("GEMINI_API_KEY"))
14
- if not self.api_key:
15
- raise ValueError("GEMINI_API_KEY environment variable is required")
16
-
17
- self.base_url = "https://generativelanguage.googleapis.com/v1beta/models"
18
-
19
- async def generate(self, system_prompt: str, context: List[dict], temperature: float, max_tokens: int) -> str:
20
- # Build focused context - only include recent relevant messages
21
- recent_context = context[-3:] if len(context) > 3 else context
22
-
23
- # Format messages for Gemini API
24
- formatted_messages = []
25
-
26
- # Add system instruction as initial conversation
27
- if system_prompt:
28
- formatted_messages.append({
29
- "role": "user",
30
- "parts": [{"text": system_prompt}]
31
- })
32
- formatted_messages.append({
33
- "role": "model",
34
- "parts": [{"text": "I understand. I'll follow these instructions."}]
35
- })
36
-
37
- # Add only the user's current question
38
- for msg in recent_context:
39
- if msg['role'] == 'user':
40
- formatted_messages.append({
41
- "role": "user",
42
- "parts": [{"text": msg['content']}]
43
- })
44
- break # Only use most recent user message
45
-
46
- payload = {
47
- "contents": formatted_messages,
48
- "generationConfig": {
49
- "temperature": temperature,
50
- "topK": 40,
51
- "topP": 0.9,
52
- "maxOutputTokens": max_tokens, # Reduced from default to ensure concise responses
53
- "stopSequences": ["Student:", "Question:", "\n\nStudent:", "\n\nQuestion:"]
54
- },
55
- "safetySettings": [
56
- {
57
- "category": "HARM_CATEGORY_HARASSMENT",
58
- "threshold": "BLOCK_MEDIUM_AND_ABOVE"
59
- },
60
- {
61
- "category": "HARM_CATEGORY_HATE_SPEECH",
62
- "threshold": "BLOCK_MEDIUM_AND_ABOVE"
63
- },
64
- {
65
- "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
66
- "threshold": "BLOCK_MEDIUM_AND_ABOVE"
67
- },
68
- {
69
- "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
70
- "threshold": "BLOCK_MEDIUM_AND_ABOVE"
71
- }
72
- ]
73
- }
74
-
75
- try:
76
- url = f"{self.base_url}/{self.model_name}:generateContent?key={self.api_key}"
77
-
78
- async with httpx.AsyncClient(timeout=30.0) as client:
79
- response = await client.post(url, json=payload)
80
- response.raise_for_status()
81
-
82
- result = response.json()
83
-
84
- # Extract text from Gemini response
85
- if "candidates" in result and len(result["candidates"]) > 0:
86
- candidate = result["candidates"][0]
87
- if "content" in candidate and "parts" in candidate["content"]:
88
- parts = candidate["content"]["parts"]
89
- if len(parts) > 0 and "text" in parts[0]:
90
- text = parts[0]["text"].strip()
91
-
92
- # Clean up response
93
- text = self._clean_response(text)
94
-
95
- # Validate response quality
96
- if len(text) < 20 or self._is_poor_quality(text):
97
- print("Poor Response Quality from Gemini")
98
- return self._get_fallback_response()
99
-
100
- return text
101
-
102
- print("Candidate Issue")
103
- return self._get_fallback_response()
104
-
105
- except httpx.HTTPError as e:
106
- print(f"HTTP error calling Gemini API: {e}")
107
- return "I'm having trouble connecting to generate a response. Please try again."
108
- except Exception as e:
109
- print(f"Error calling Gemini API: {e}")
110
- return "I'm having trouble generating a response right now. Please try again."
111
-
112
- def _clean_response(self, response: str) -> str:
113
- """Clean up Gemini responses"""
114
- # Remove common prefixes that indicate AI confusion
115
- prefixes_to_remove = [
116
- "Dr. Methodologist:", "Dr. Theorist:", "Dr. Pragmatist:",
117
- "Methodologist Advisor:", "Theorist Advisor:", "Pragmatist Advisor:",
118
- "As Dr.", "Here's my response:", "Response:", "Assistant:",
119
- "Here are 2-3 sentence", "Here's an expansion of the advice:",
120
- ]
121
-
122
- for prefix in prefixes_to_remove:
123
- if response.startswith(prefix):
124
- response = response[len(prefix):].strip()
125
-
126
- # Remove excessive academic fluff
127
- fluff_patterns = [
128
- "conceptual insights:", "actionable advice:", "my inquisitive student",
129
- "excellent question", "thank you for", "assistant!", "excellent discussion"
130
- ]
131
-
132
- for pattern in fluff_patterns:
133
- response = response.replace(pattern, "").strip()
134
-
135
- # Remove trailing incomplete sentences
136
- sentences = response.split('.')
137
- if len(sentences) > 1 and len(sentences[-1].strip()) < 10:
138
- response = '.'.join(sentences[:-1]) + '.'
139
-
140
- return response
141
-
142
- def _is_poor_quality(self, response: str) -> bool:
143
- """Check if response quality is poor"""
144
- poor_indicators = [
145
- "Thank you, Dr." in response, # AI confusion about identity
146
- "Assistant:" in response,
147
- len(response.split()) > 700, # Too verbose
148
- response.count("?") > 3, # Too many questions
149
- "excellent discussion, Assistant" in response,
150
- ]
151
- return any(poor_indicators)
152
-
153
- def _get_fallback_response(self) -> str:
154
- """Return a simple fallback when quality is poor"""
155
- return "I'd be happy to help with that. Could you provide more specific details about what you're looking for?"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
multi_llm_chatbot_backend/app/llm/mistral_client.py DELETED
@@ -1,27 +0,0 @@
1
- import httpx
2
- from typing import List
3
- from app.llm.llm_client import LLMClient
4
-
5
- OLLAMA_URL = "http://localhost:11434/api/generate"
6
-
7
- class MistralClient(LLMClient):
8
- async def generate(self, system_prompt: str, context: List[dict], temperature: float, max_tokens: int) -> str:
9
- # Flatten context into a string
10
- formatted = "\n".join(f"{msg['role'].capitalize()}: {msg['content']}" for msg in context)
11
- full_prompt = f"{system_prompt}\n\n{formatted}\n\nAssistant:"
12
-
13
- payload = {
14
- "model": "mistral",
15
- "prompt": full_prompt,
16
- "temperature": temperature,
17
- "num_predict": max_tokens,
18
- "stream": False
19
- }
20
-
21
- try:
22
- async with httpx.AsyncClient(timeout=60.0) as client:
23
- response = await client.post(OLLAMA_URL, json=payload)
24
- response.raise_for_status()
25
- return response.json().get("response", "[No response]")
26
- except httpx.HTTPError as e:
27
- return f"[Error from Mistral: {str(e)}]"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
multi_llm_chatbot_backend/app/llm/ollama_client.py DELETED
@@ -1,34 +0,0 @@
1
- import httpx
2
- from typing import List
3
- from app.llm.llm_client import LLMClient
4
-
5
- OLLAMA_URL = "http://localhost:11434/api/generate"
6
-
7
- class OllamaClient(LLMClient):
8
- def __init__(self, model_name: str = "llama3.2:1b"):
9
- self.model_name = model_name
10
-
11
- async def generate(self, system_prompt: str, context: List[dict], temperature: float, max_tokens: int) -> str:
12
- # Flatten context into a string
13
- formatted = "\n".join(f"{msg['role'].capitalize()}: {msg['content']}" for msg in context)
14
- full_prompt = f"{system_prompt}\n\n{formatted}\n\nAssistant:"
15
-
16
- payload = {
17
- "model": self.model_name,
18
- "prompt": full_prompt,
19
- "stream": False,
20
- "options": {
21
- "temperature": temperature,
22
- "top_p": 0.9,
23
- "top_k": 40,
24
- "num_predict": max_tokens, # Limit response length for faster generation
25
- }
26
- }
27
-
28
- try:
29
- async with httpx.AsyncClient(timeout=30.0) as client: # Reduced timeout
30
- response = await client.post(OLLAMA_URL, json=payload)
31
- response.raise_for_status()
32
- return response.json().get("response", "[No response]")
33
- except httpx.HTTPError as e:
34
- return f"[Error from {self.model_name}: {str(e)}]"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
multi_llm_chatbot_backend/app/llm/short_ollama_client.py DELETED
@@ -1,101 +0,0 @@
1
- import httpx
2
- from typing import List
3
- from app.llm.llm_client import LLMClient
4
-
5
- OLLAMA_URL = "http://localhost:11434/api/generate"
6
-
7
- # Improved LLM client with better short response handling for Ollama
8
- class ShortResponseOllamaClient(LLMClient):
9
- def __init__(self, model_name: str = "llama3.2:1b"):
10
- self.model_name = model_name
11
-
12
- async def generate(self, system_prompt: str, context: List[dict], temperature: float, max_tokens: int) -> str:
13
- # Build cleaner context - only include recent relevant messages
14
- recent_context = context[-3:] if len(context) > 3 else context
15
-
16
- # Create a focused prompt
17
- prompt_parts = [system_prompt]
18
-
19
- # Add only the user's current question
20
- for msg in recent_context:
21
- if msg['role'] == 'user':
22
- prompt_parts.append(f"Student Question: {msg['content']}")
23
- break # Only use the most recent user message
24
-
25
- prompt_parts.append("Your Response:")
26
- prompt = "\n\n".join(prompt_parts)
27
-
28
- payload = {
29
- "model": self.model_name,
30
- "prompt": prompt,
31
- "stream": False,
32
- "options": {
33
- "temperature": temperature,
34
- "top_p": 0.9,
35
- "top_k": 40,
36
- "num_predict": max_tokens, # Reduced from 200 to force shorter responses
37
- "repeat_penalty": 1.1,
38
- "stop": ["\n\n", "Student:", "Question:", "Response:"] # Stop tokens
39
- }
40
- }
41
-
42
- try:
43
- async with httpx.AsyncClient(timeout=25.0) as client:
44
- response = await client.post("http://localhost:11434/api/generate", json=payload)
45
- response.raise_for_status()
46
- result = response.json().get("response", "[No response]").strip()
47
-
48
- # Enhanced cleanup
49
- result = self._clean_response(result)
50
-
51
- # Validate response quality
52
- if len(result) < 20 or self._is_poor_quality(result):
53
- return self._get_fallback_response()
54
-
55
- return result
56
-
57
- except Exception as e:
58
- return "I'm having trouble generating a response right now. Please try again."
59
-
60
- def _clean_response(self, response: str) -> str:
61
- """Clean up common response issues"""
62
- # Remove common prefixes
63
- prefixes_to_remove = [
64
- "Here are 2-3 sentence", "Here's an expansion", "Assistant:",
65
- "Dr. Methodologist:", "Dr. Theorist:", "Dr. Pragmatist:",
66
- "Methodologist Advisor:", "Theorist Advisor:", "Pragmatist Advisor:",
67
- ]
68
-
69
- for prefix in prefixes_to_remove:
70
- if response.startswith(prefix):
71
- response = response[len(prefix):].strip()
72
-
73
- # Remove trailing incomplete sentences
74
- sentences = response.split('.')
75
- if len(sentences) > 1 and len(sentences[-1].strip()) < 10:
76
- response = '.'.join(sentences[:-1]) + '.'
77
-
78
- # Remove excessive academic fluff
79
- fluff_patterns = [
80
- "conceptual insights:", "actionable advice:", "my inquisitive student",
81
- "excellent question", "thank you for", "assistant!"
82
- ]
83
-
84
- for pattern in fluff_patterns:
85
- response = response.replace(pattern, "").strip()
86
-
87
- return response
88
-
89
- def _is_poor_quality(self, response: str) -> bool:
90
- """Check if response quality is poor"""
91
- poor_indicators = [
92
- "Thank you, Dr." in response, # AI confusion about identity
93
- "Assistant:" in response,
94
- len(response.split()) > 100, # Too verbose
95
- response.count("?") > 3, # Too many questions
96
- ]
97
- return any(poor_indicators)
98
-
99
- def _get_fallback_response(self) -> str:
100
- """Return a simple fallback when quality is poor"""
101
- return "I'd be happy to help with that. Could you provide more specific details about what you're looking for?"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
phd-advisor-frontend/README.md DELETED
@@ -1,70 +0,0 @@
1
- # Getting Started with Create React App
2
-
3
- This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4
-
5
- ## Available Scripts
6
-
7
- In the project directory, you can run:
8
-
9
- ### `npm start`
10
-
11
- Runs the app in the development mode.\
12
- Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
13
-
14
- The page will reload when you make changes.\
15
- You may also see any lint errors in the console.
16
-
17
- ### `npm test`
18
-
19
- Launches the test runner in the interactive watch mode.\
20
- See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21
-
22
- ### `npm run build`
23
-
24
- Builds the app for production to the `build` folder.\
25
- It correctly bundles React in production mode and optimizes the build for the best performance.
26
-
27
- The build is minified and the filenames include the hashes.\
28
- Your app is ready to be deployed!
29
-
30
- See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31
-
32
- ### `npm run eject`
33
-
34
- **Note: this is a one-way operation. Once you `eject`, you can't go back!**
35
-
36
- If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37
-
38
- Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
39
-
40
- You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
41
-
42
- ## Learn More
43
-
44
- You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45
-
46
- To learn React, check out the [React documentation](https://reactjs.org/).
47
-
48
- ### Code Splitting
49
-
50
- This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51
-
52
- ### Analyzing the Bundle Size
53
-
54
- This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55
-
56
- ### Making a Progressive Web App
57
-
58
- This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59
-
60
- ### Advanced Configuration
61
-
62
- This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63
-
64
- ### Deployment
65
-
66
- This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67
-
68
- ### `npm run build` fails to minify
69
-
70
- This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)