Girish Jeswani commited on
Commit
a9328c2
·
1 Parent(s): 36fa2f1

context fixes

Browse files
multi_llm_chatbot_backend/app/core/improved_orchestrator.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Optional, Any
2
+ from app.models.persona import Persona
3
+ from app.core.session_manager import ConversationContext, get_session_manager
4
+ from app.core.context_manager import get_context_manager
5
+ from app.llm.llm_client import LLMClient
6
+ import logging
7
+ import re
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class ImprovedChatOrchestrator:
12
+ """
13
+ Improved orchestrator with proper session management and context handling
14
+ """
15
+
16
+ def __init__(self):
17
+ self.personas: Dict[str, Persona] = {}
18
+ self.session_manager = get_session_manager()
19
+ self.context_manager = get_context_manager()
20
+
21
+ def register_persona(self, persona: Persona):
22
+ """Register a persona with the orchestrator"""
23
+ self.personas[persona.id] = persona
24
+ logger.info(f"Registered persona: {persona.id} ({persona.name})")
25
+
26
+ def get_persona(self, persona_id: str) -> Optional[Persona]:
27
+ """Get a specific persona"""
28
+ return self.personas.get(persona_id)
29
+
30
+ def list_personas(self) -> List[str]:
31
+ """List all available persona IDs"""
32
+ return list(self.personas.keys())
33
+
34
+ async def process_message(self,
35
+ user_input: str,
36
+ session_id: Optional[str] = None,
37
+ response_length: str = "medium") -> Dict[str, Any]:
38
+ """
39
+ Process a user message through the orchestration pipeline
40
+ """
41
+ try:
42
+ # Get or create session
43
+ session = self.session_manager.get_session(session_id)
44
+
45
+ # Add user message to session
46
+ session.append_message("user", user_input)
47
+
48
+ # Determine if we need clarification
49
+ needs_clarification = self._needs_clarification(session, user_input)
50
+
51
+ if needs_clarification:
52
+ # Generate clarification question
53
+ clarification = await self._generate_clarification_question(session)
54
+ session.append_message("orchestrator", clarification)
55
+
56
+ return {
57
+ "type": "clarification",
58
+ "session_id": session.session_id,
59
+ "message": clarification,
60
+ "context_summary": self.context_manager.get_context_summary(session.messages)
61
+ }
62
+
63
+ # Generate responses from all personas
64
+ responses = await self._generate_persona_responses(session, response_length)
65
+
66
+ return {
67
+ "type": "persona_responses",
68
+ "session_id": session.session_id,
69
+ "responses": responses,
70
+ "context_summary": self.context_manager.get_context_summary(session.messages)
71
+ }
72
+
73
+ except Exception as e:
74
+ logger.error(f"Error in process_message: {str(e)}")
75
+ return {
76
+ "type": "error",
77
+ "session_id": session_id,
78
+ "message": "I encountered an error processing your request. Please try again.",
79
+ "error": str(e)
80
+ }
81
+
82
+ async def chat_with_persona(self,
83
+ user_input: str,
84
+ persona_id: str,
85
+ session_id: Optional[str] = None,
86
+ response_length: str = "medium") -> Dict[str, Any]:
87
+ """
88
+ Chat with a specific persona
89
+ """
90
+ try:
91
+ if persona_id not in self.personas:
92
+ return {
93
+ "type": "error",
94
+ "message": f"Persona '{persona_id}' not found"
95
+ }
96
+
97
+ # Get or create session
98
+ session = self.session_manager.get_session(session_id)
99
+
100
+ # Add user message
101
+ session.append_message("user", user_input)
102
+
103
+ # Generate response from specific persona
104
+ persona = self.personas[persona_id]
105
+ response = await self._generate_single_persona_response(session, persona, response_length)
106
+
107
+ # Add persona response to session
108
+ session.append_message(persona_id, response['response'])
109
+
110
+ return {
111
+ "type": "single_persona_response",
112
+ "session_id": session.session_id,
113
+ "persona": response,
114
+ "context_summary": self.context_manager.get_context_summary(session.messages)
115
+ }
116
+
117
+ except Exception as e:
118
+ logger.error(f"Error in chat_with_persona: {str(e)}")
119
+ return {
120
+ "type": "error",
121
+ "session_id": session_id,
122
+ "message": "I encountered an error generating a response. Please try again.",
123
+ "error": str(e)
124
+ }
125
+
126
+ def _needs_clarification(self, session: ConversationContext, user_input: str) -> bool:
127
+ """
128
+ Determine if user input needs clarification
129
+ """
130
+ # Don't ask for clarification if this is a follow-up message
131
+ user_messages = session.get_messages_by_role("user")
132
+ if len(user_messages) > 1:
133
+ return False
134
+
135
+ # Check if input is vague
136
+ vague_patterns = [
137
+ r"i'm (not sure|unsure|confused|lost)",
138
+ r"i (don't know|dunno) (what|how|where)",
139
+ r"help me with (my|the|a) (thesis|research|phd)",
140
+ r"(what should i|how do i|where do i start)",
141
+ r"i need (help|advice|guidance)$",
142
+ r"(stuck|struggling) with",
143
+ r"(any|some) (advice|suggestions|ideas)$",
144
+ r"^(help|advice|guidance)$",
145
+ ]
146
+
147
+ user_lower = user_input.lower().strip()
148
+
149
+ for pattern in vague_patterns:
150
+ if re.search(pattern, user_lower):
151
+ return True
152
+
153
+ # Check if input is too short (likely vague)
154
+ if len(user_input.split()) < 8:
155
+ return True
156
+
157
+ return False
158
+
159
+ async def _generate_clarification_question(self, session: ConversationContext) -> str:
160
+ """
161
+ Generate a clarification question based on context
162
+ """
163
+ user_messages = session.get_messages_by_role("user")
164
+ if not user_messages:
165
+ return "What specific aspect of your PhD journey would you like guidance on?"
166
+
167
+ latest_message = user_messages[-1]['content']
168
+
169
+ # Extract what information we might be missing
170
+ missing_info = []
171
+
172
+ # Check for research area
173
+ research_keywords = ["computer science", "biology", "psychology", "physics", "chemistry",
174
+ "engineering", "literature", "history", "mathematics", "sociology"]
175
+ if not any(keyword in latest_message.lower() for keyword in research_keywords):
176
+ missing_info.append("research area")
177
+
178
+ # Check for specific question type
179
+ question_keywords = ["methodology", "theory", "writing", "analysis", "data", "literature review"]
180
+ if not any(keyword in latest_message.lower() for keyword in question_keywords):
181
+ missing_info.append("specific aspect")
182
+
183
+ # Check for academic stage
184
+ stage_keywords = ["first year", "second year", "third year", "qualifying", "dissertation",
185
+ "defense", "proposal", "coursework"]
186
+ if not any(keyword in latest_message.lower() for keyword in stage_keywords):
187
+ missing_info.append("PhD stage")
188
+
189
+ # Generate appropriate clarification question
190
+ if "research area" in missing_info:
191
+ return "What field or discipline are you studying in?"
192
+ elif "specific aspect" in missing_info:
193
+ return "What specific aspect of your research would you like guidance on?"
194
+ elif "PhD stage" in missing_info:
195
+ return "What stage of your PhD program are you currently in?"
196
+ else:
197
+ return "Could you provide more details about your specific situation or question?"
198
+
199
+ async def _generate_persona_responses(self,
200
+ session: ConversationContext,
201
+ response_length: str) -> List[Dict[str, Any]]:
202
+ """
203
+ Generate responses from all personas
204
+ """
205
+ responses = []
206
+
207
+ # Get the conversation context for personas
208
+ context_messages = session.get_recent_messages(limit=20)
209
+
210
+ for persona_id, persona in self.personas.items():
211
+ try:
212
+ response_data = await self._generate_single_persona_response(
213
+ session, persona, response_length
214
+ )
215
+ responses.append(response_data)
216
+
217
+ # Add persona response to session
218
+ session.append_message(persona_id, response_data['response'])
219
+
220
+ except Exception as e:
221
+ logger.error(f"Error generating response for persona {persona_id}: {str(e)}")
222
+ # Add fallback response
223
+ responses.append({
224
+ "persona_id": persona_id,
225
+ "persona_name": persona.name,
226
+ "response": self._get_fallback_response(persona_id),
227
+ "error": True
228
+ })
229
+
230
+ return responses
231
+
232
+ async def _generate_single_persona_response(self,
233
+ session: ConversationContext,
234
+ persona: Persona,
235
+ response_length: str) -> Dict[str, Any]:
236
+ """
237
+ Generate response from a single persona
238
+ """
239
+ try:
240
+ # Get conversation context
241
+ context_messages = session.get_recent_messages(limit=20)
242
+
243
+ # Generate response using persona
244
+ response = await persona.respond(context_messages, response_length)
245
+
246
+ # Validate response
247
+ if not self._is_valid_response(response, persona.id):
248
+ response = self._get_fallback_response(persona.id)
249
+
250
+ return {
251
+ "persona_id": persona.id,
252
+ "persona_name": persona.name,
253
+ "response": response,
254
+ "error": False
255
+ }
256
+
257
+ except Exception as e:
258
+ logger.error(f"Error in _generate_single_persona_response for {persona.id}: {str(e)}")
259
+ return {
260
+ "persona_id": persona.id,
261
+ "persona_name": persona.name,
262
+ "response": self._get_fallback_response(persona.id),
263
+ "error": True
264
+ }
265
+
266
+ def _is_valid_response(self, response: str, persona_id: str) -> bool:
267
+ """Validate response quality"""
268
+ if not response or len(response.strip()) < 10:
269
+ return False
270
+
271
+ if len(response) > 2000: # Too long
272
+ return False
273
+
274
+ # Check for AI confusion indicators
275
+ confusion_indicators = [
276
+ f"Thank you, Dr. {persona_id.title()}",
277
+ "Assistant:",
278
+ f"Dr. {persona_id.title()}:",
279
+ "excellent discussion, Assistant"
280
+ ]
281
+
282
+ return not any(indicator in response for indicator in confusion_indicators)
283
+
284
+ def _get_fallback_response(self, persona_id: str) -> str:
285
+ """Get persona-specific fallback response"""
286
+ fallbacks = {
287
+ "methodist": "Let's focus on your research methodology. What specific methodological approach are you considering?",
288
+ "theorist": "I'd like to explore the theoretical foundation of your work. What conceptual framework guides your research?",
289
+ "pragmatist": "Let's take a practical approach. What's the most pressing decision you need to make about your research right now?"
290
+ }
291
+ return fallbacks.get(persona_id, "I'd be happy to help. Could you provide more specific details about your question?")
292
+
293
+ def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
294
+ """Get information about a session"""
295
+ session = self.session_manager.get_session(session_id)
296
+ if session:
297
+ return {
298
+ "session_id": session.session_id,
299
+ "message_count": len(session.messages),
300
+ "uploaded_files": session.uploaded_files,
301
+ "created_at": session.created_at.isoformat(),
302
+ "last_accessed": session.last_accessed.isoformat(),
303
+ "context_summary": self.context_manager.get_context_summary(session.messages)
304
+ }
305
+ return None
306
+
307
+ def reset_session(self, session_id: str) -> bool:
308
+ """Reset a session (clear messages but keep metadata)"""
309
+ session = self.session_manager.get_session(session_id)
310
+ if session:
311
+ session.clear_messages()
312
+ return True
313
+ return False
314
+
315
+ def delete_session(self, session_id: str) -> bool:
316
+ """Delete a session completely"""
317
+ return self.session_manager.delete_session(session_id)
multi_llm_chatbot_backend/app/llm/improved_gemini_client.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import os
3
+ from typing import List
4
+ from app.llm.llm_client import LLMClient
5
+ from app.core.context_manager import get_context_manager
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class ImprovedGeminiClient(LLMClient):
11
+ def __init__(self, model_name: str = None):
12
+ if model_name is None:
13
+ model_name = os.getenv("GEMINI_MODEL", "gemini-2.0-flash-exp")
14
+
15
+ self.model_name = model_name
16
+ self.api_key = os.getenv("GEMINI_API_KEY")
17
+ if not self.api_key:
18
+ raise ValueError("GEMINI_API_KEY environment variable is required")
19
+
20
+ self.base_url = "https://generativelanguage.googleapis.com/v1beta/models"
21
+ self.context_manager = get_context_manager()
22
+
23
+ async def generate(self, system_prompt: str, context: List[dict], temperature: float, max_tokens: int) -> str:
24
+ """
25
+ Generate response using improved context management
26
+ """
27
+ try:
28
+ # Use context manager to prepare optimal context window
29
+ context_window = self.context_manager.prepare_context_for_llm(
30
+ messages=context,
31
+ system_prompt=system_prompt,
32
+ llm_provider="gemini"
33
+ )
34
+
35
+ logger.debug(f"Context prepared: {len(context_window.messages)} messages, "
36
+ f"~{context_window.total_tokens} tokens, truncated={context_window.truncated}")
37
+
38
+ payload = {
39
+ "contents": context_window.messages,
40
+ "generationConfig": {
41
+ "temperature": temperature,
42
+ "topK": 40,
43
+ "topP": 0.9,
44
+ "maxOutputTokens": max_tokens,
45
+ "stopSequences": ["Student:", "Question:", "\n\nStudent:", "\n\nQuestion:"]
46
+ },
47
+ "safetySettings": [
48
+ {
49
+ "category": "HARM_CATEGORY_HARASSMENT",
50
+ "threshold": "BLOCK_MEDIUM_AND_ABOVE"
51
+ },
52
+ {
53
+ "category": "HARM_CATEGORY_HATE_SPEECH",
54
+ "threshold": "BLOCK_MEDIUM_AND_ABOVE"
55
+ },
56
+ {
57
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
58
+ "threshold": "BLOCK_MEDIUM_AND_ABOVE"
59
+ },
60
+ {
61
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
62
+ "threshold": "BLOCK_MEDIUM_AND_ABOVE"
63
+ }
64
+ ]
65
+ }
66
+
67
+ url = f"{self.base_url}/{self.model_name}:generateContent?key={self.api_key}"
68
+
69
+ async with httpx.AsyncClient(timeout=30.0) as client:
70
+ response = await client.post(url, json=payload)
71
+ response.raise_for_status()
72
+
73
+ result = response.json()
74
+
75
+ if "candidates" in result and result["candidates"]:
76
+ candidate = result["candidates"][0]
77
+ if "content" in candidate and "parts" in candidate["content"]:
78
+ text = candidate["content"]["parts"][0].get("text", "")
79
+ return self._clean_response(text)
80
+
81
+ # Handle safety filter or other issues
82
+ if "promptFeedback" in result:
83
+ feedback = result["promptFeedback"]
84
+ logger.warning(f"Gemini prompt feedback: {feedback}")
85
+
86
+ return "I apologize, but I'm unable to provide a response to that query."
87
+
88
+ except httpx.TimeoutException:
89
+ logger.error("Gemini API timeout")
90
+ return "I'm experiencing a delay in processing. Please try again."
91
+ except httpx.HTTPStatusError as e:
92
+ logger.error(f"Gemini API HTTP error: {e.response.status_code} - {e.response.text}")
93
+ return "I'm having trouble accessing the AI service. Please try again."
94
+ except Exception as e:
95
+ logger.error(f"Unexpected error in Gemini client: {str(e)}")
96
+ return "I encountered an unexpected error. Please try again."
97
+
98
+ def _clean_response(self, response: str) -> str:
99
+ """Clean up response text"""
100
+ # Remove common issues
101
+ response = response.strip()
102
+
103
+ # Remove duplicate spaces and normalize
104
+ response = ' '.join(response.split())
105
+
106
+ return response
multi_llm_chatbot_backend/app/llm/improved_ollama_client.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ from typing import List
3
+ from app.llm.llm_client import LLMClient
4
+ from app.core.context_manager import get_context_manager
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class ImprovedOllamaClient(LLMClient):
10
+ def __init__(self, model_name: str = "llama3.2:1b", base_url: str = "http://localhost:11434"):
11
+ self.model_name = model_name
12
+ self.base_url = base_url
13
+ self.context_manager = get_context_manager()
14
+
15
+ async def generate(self, system_prompt: str, context: List[dict], temperature: float, max_tokens: int) -> str:
16
+ """
17
+ Generate response using improved context management
18
+ """
19
+ try:
20
+ # Use context manager to prepare optimal context
21
+ context_window = self.context_manager.prepare_context_for_llm(
22
+ messages=context,
23
+ system_prompt=system_prompt,
24
+ llm_provider="ollama"
25
+ )
26
+
27
+ logger.debug(f"Context prepared: ~{context_window.total_tokens} tokens, "
28
+ f"truncated={context_window.truncated}")
29
+
30
+ # For Ollama, context_window.messages is a formatted prompt string
31
+ formatted_prompt = context_window.messages
32
+
33
+ payload = {
34
+ "model": self.model_name,
35
+ "prompt": formatted_prompt,
36
+ "stream": False,
37
+ "options": {
38
+ "temperature": temperature,
39
+ "top_p": 0.9,
40
+ "top_k": 40,
41
+ "num_predict": max_tokens,
42
+ "repeat_penalty": 1.1,
43
+ "stop": ["\n\nStudent:", "\n\nUser:", "Question:", "Student:"]
44
+ }
45
+ }
46
+
47
+ async with httpx.AsyncClient(timeout=30.0) as client:
48
+ response = await client.post(f"{self.base_url}/api/generate", json=payload)
49
+ response.raise_for_status()
50
+
51
+ result = response.json()
52
+ text = result.get("response", "").strip()
53
+
54
+ return self._clean_response(text)
55
+
56
+ except httpx.ConnectError:
57
+ logger.error(f"Cannot connect to Ollama at {self.base_url}")
58
+ return "I'm unable to connect to the local AI service. Please ensure Ollama is running."
59
+ except httpx.TimeoutException:
60
+ logger.error("Ollama request timeout")
61
+ return "The AI service is taking too long to respond. Please try again."
62
+ except httpx.HTTPStatusError as e:
63
+ logger.error(f"Ollama HTTP error: {e.response.status_code}")
64
+ return "The AI service encountered an error. Please try again."
65
+ except Exception as e:
66
+ logger.error(f"Unexpected error in Ollama client: {str(e)}")
67
+ return "I encountered an unexpected error. Please try again."
68
+
69
+ def _clean_response(self, response: str) -> str:
70
+ """Clean up common response issues"""
71
+ # Remove common prefixes that indicate AI confusion
72
+ prefixes_to_remove = [
73
+ "Here are 2-3 sentence", "Here's an expansion", "Assistant:",
74
+ "Dr. Methodist:", "Dr. Theorist:", "Dr. Pragmatist:",
75
+ "Methodist Advisor:", "Theorist Advisor:", "Pragmatist Advisor:",
76
+ ]
77
+
78
+ for prefix in prefixes_to_remove:
79
+ if response.startswith(prefix):
80
+ response = response[len(prefix):].strip()
81
+
82
+ # Remove trailing incomplete sentences
83
+ sentences = response.split('.')
84
+ if len(sentences) > 1 and len(sentences[-1].strip()) < 10:
85
+ response = '.'.join(sentences[:-1]) + '.'
86
+
87
+ # Remove excessive academic fluff
88
+ fluff_patterns = [
89
+ "conceptual insights:", "actionable advice:", "my inquisitive student",
90
+ "excellent question", "thank you for", "assistant!"
91
+ ]
92
+
93
+ for pattern in fluff_patterns:
94
+ response = response.replace(pattern, "").strip()
95
+
96
+ # Normalize whitespace
97
+ response = ' '.join(response.split())
98
+
99
+ return response
100
+
101
+ def _is_poor_quality(self, response: str) -> bool:
102
+ """Check if response quality is poor"""
103
+ poor_indicators = [
104
+ "Thank you, Dr." in response,
105
+ "Assistant:" in response,
106
+ len(response.split()) > 150, # Too verbose
107
+ response.count("?") > 3, # Too many questions
108
+ ]
109
+ return any(poor_indicators)