Sohan Kshirsagar commited on
Commit
dba4341
·
1 Parent(s): 3c7313e

routes file cleanup and related changes in orchestrators

Browse files
multi_llm_chatbot_backend/app/api/routes.py CHANGED
@@ -3,10 +3,12 @@ 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.models.persona import Persona
7
  from app.core.orchestrator import ChatOrchestrator
8
  from app.core.seamless_orchestrator import SeamlessOrchestrator
9
  from app.core.context import GlobalSessionContext
 
10
  from pydantic import BaseModel
11
  from typing import Optional, List
12
  from fastapi import UploadFile, File
@@ -36,102 +38,6 @@ def create_llm_client(provider: str = None) -> LLMClient:
36
  else:
37
  raise ValueError(f"Unknown provider: {provider}")
38
 
39
- # Improved LLM client with better short response handling for Ollama
40
- class ShortResponseOllamaClient(LLMClient):
41
- def __init__(self, model_name: str = "llama3.2:1b"):
42
- self.model_name = model_name
43
-
44
- async def generate(self, system_prompt: str, context: List[dict]) -> str:
45
- # Build cleaner context - only include recent relevant messages
46
- recent_context = context[-3:] if len(context) > 3 else context
47
-
48
- # Create a focused prompt
49
- prompt_parts = [system_prompt]
50
-
51
- # Add only the user's current question
52
- for msg in recent_context:
53
- if msg['role'] == 'user':
54
- prompt_parts.append(f"Student Question: {msg['content']}")
55
- break # Only use the most recent user message
56
-
57
- prompt_parts.append("Your Response:")
58
- prompt = "\n\n".join(prompt_parts)
59
-
60
- payload = {
61
- "model": self.model_name,
62
- "prompt": prompt,
63
- "stream": False,
64
- "options": {
65
- "temperature": 0.7,
66
- "top_p": 0.9,
67
- "top_k": 40,
68
- "num_predict": 80, # Reduced from 200 to force shorter responses
69
- "repeat_penalty": 1.1,
70
- "stop": ["\n\n", "Student:", "Question:", "Response:"] # Stop tokens
71
- }
72
- }
73
-
74
- try:
75
- async with httpx.AsyncClient(timeout=25.0) as client:
76
- response = await client.post("http://localhost:11434/api/generate", json=payload)
77
- response.raise_for_status()
78
- result = response.json().get("response", "[No response]").strip()
79
-
80
- # Enhanced cleanup
81
- result = self._clean_response(result)
82
-
83
- # Validate response quality
84
- if len(result) < 20 or self._is_poor_quality(result):
85
- return self._get_fallback_response()
86
-
87
- return result
88
-
89
- except Exception as e:
90
- return "I'm having trouble generating a response right now. Please try again."
91
-
92
- def _clean_response(self, response: str) -> str:
93
- """Clean up common response issues"""
94
- # Remove common prefixes
95
- prefixes_to_remove = [
96
- "Here are 2-3 sentence", "Here's an expansion", "Assistant:",
97
- "Dr. Methodist:", "Dr. Theorist:", "Dr. Pragmatist:",
98
- "Methodist Advisor:", "Theorist Advisor:", "Pragmatist Advisor:",
99
- ]
100
-
101
- for prefix in prefixes_to_remove:
102
- if response.startswith(prefix):
103
- response = response[len(prefix):].strip()
104
-
105
- # Remove trailing incomplete sentences
106
- sentences = response.split('.')
107
- if len(sentences) > 1 and len(sentences[-1].strip()) < 10:
108
- response = '.'.join(sentences[:-1]) + '.'
109
-
110
- # Remove excessive academic fluff
111
- fluff_patterns = [
112
- "conceptual insights:", "actionable advice:", "my inquisitive student",
113
- "excellent question", "thank you for", "assistant!"
114
- ]
115
-
116
- for pattern in fluff_patterns:
117
- response = response.replace(pattern, "").strip()
118
-
119
- return response
120
-
121
- def _is_poor_quality(self, response: str) -> bool:
122
- """Check if response quality is poor"""
123
- poor_indicators = [
124
- "Thank you, Dr." in response, # AI confusion about identity
125
- "Assistant:" in response,
126
- len(response.split()) > 100, # Too verbose
127
- response.count("?") > 3, # Too many questions
128
- ]
129
- return any(poor_indicators)
130
-
131
- def _get_fallback_response(self) -> str:
132
- """Return a simple fallback when quality is poor"""
133
- return "I'd be happy to help with that. Could you provide more specific details about what you're looking for?"
134
-
135
  # Initialize with default provider
136
  llm = create_llm_client()
137
  chat_orchestrator = ChatOrchestrator()
@@ -139,63 +45,9 @@ seamless_orchestrator = SeamlessOrchestrator(llm=llm)
139
 
140
  session_context = GlobalSessionContext()
141
 
142
- def create_default_personas(llm_client: LLMClient):
143
- """Create default personas with improved, concise system prompts"""
144
- return [
145
- Persona(
146
- id="methodist",
147
- name="Dr. Methodist",
148
- system_prompt="""You are Dr. Methodist, a research methodology expert.
149
-
150
- RESPONSE RULES:
151
- - Maximum 3 sentences
152
- - Start with your recommendation
153
- - Include ONE specific actionable step
154
- - Use terms like "validity," "operationalize," "sampling frame"
155
- - Focus on methodological rigor
156
-
157
- TONE: Precise, helpful, focused on research design quality.
158
-
159
- Example: "Use a cautious tone unless your methodology is exceptionally robust. Strong validity and clear operationalization justify more confident language. Next step: Review your methods section to assess how assertive you can be.""",
160
- llm=llm_client
161
- ),
162
- Persona(
163
- id="theorist",
164
- name="Dr. Theorist",
165
- system_prompt="""You are Dr. Theorist, a conceptual frameworks expert.
166
- RESPONSE RULES:
167
- - Maximum 3 sentences
168
- - Start with conceptual perspective
169
- - Reference theoretical positioning
170
- - Ask ONE probing question when relevant
171
- - Use terms like "epistemological," "framework," "assumptions"
172
-
173
- TONE: Thoughtful, intellectually rigorous, conceptually focused.
174
-
175
- Example: "Your tone should reflect your epistemological stance—bold if challenging frameworks, cautious if extending theory. Consider your relationship to existing literature. What theoretical assumptions underlie your approach?""",
176
- llm=llm_client
177
- ),
178
- Persona(
179
- id="pragmatist",
180
- name="Dr. Pragmatist",
181
- system_prompt="""You are Dr. Pragmatist, a practical action-focused advisor.
182
-
183
- RESPONSE RULES:
184
- - Maximum 2 sentences
185
- - Start with clear, actionable advice
186
- - Focus on immediate next steps
187
- - Use phrases like "Quick fix:" "Next step:" "Try this:"
188
- - Prioritize progress over perfection
189
-
190
- TONE: Warm, motivational, results-oriented.
191
-
192
- Example: "Start cautious and earn the right to be bold as you build your case. Quick fix: Use 'This study suggests...' rather than 'This study proves...'""",
193
- llm=llm_client
194
- )
195
- ]
196
-
197
  # Initialize personas
198
- DEFAULT_PERSONAS = create_default_personas(llm)
 
199
  for persona in DEFAULT_PERSONAS:
200
  chat_orchestrator.register_persona(persona)
201
 
@@ -211,6 +63,7 @@ class PersonaInput(BaseModel):
211
  class ChatMessage(BaseModel):
212
  user_input: str
213
  session_id: Optional[str] = None
 
214
 
215
  class ReplyToAdvisor(BaseModel):
216
  user_input: str
@@ -223,7 +76,7 @@ class ProviderSwitch(BaseModel):
223
  # Helper functions for response validation
224
  def _is_valid_response(response: str, persona_id: str) -> bool:
225
  """Validate response quality"""
226
- if len(response) < 20 or len(response) > 500:
227
  return False
228
 
229
  # Check for AI confusion indicators
@@ -276,7 +129,7 @@ async def switch_provider(provider_data: ProviderSwitch):
276
  llm = new_llm
277
 
278
  # Update all personas with new LLM
279
- new_personas = create_default_personas(new_llm)
280
  chat_orchestrator.personas.clear()
281
  for persona in new_personas:
282
  chat_orchestrator.register_persona(persona)
@@ -322,19 +175,20 @@ async def chat_sequential(message: ChatMessage):
322
 
323
  # Clear previous advisor responses to avoid confusion
324
  session_context.clear()
325
- session_context.append("user", enhanced_context)
 
326
 
327
- advisor_order = ["methodist", "theorist", "pragmatist"]
 
 
328
  responses = []
329
 
330
- for i, persona_id in enumerate(advisor_order):
331
  try:
332
- persona = chat_orchestrator.personas[persona_id]
333
-
334
- # Use clean context for each advisor (no cross-contamination)
335
- clean_context = [{"role": "user", "content": enhanced_context}]
336
-
337
- reply = await persona.respond(clean_context)
338
 
339
  # Validate response before adding
340
  if _is_valid_response(reply, persona_id):
@@ -342,7 +196,6 @@ async def chat_sequential(message: ChatMessage):
342
  "persona": persona.name,
343
  "persona_id": persona_id,
344
  "response": reply,
345
- "order": i
346
  })
347
  else:
348
  # Fallback response for invalid responses
@@ -350,8 +203,9 @@ async def chat_sequential(message: ChatMessage):
350
  "persona": persona.name,
351
  "persona_id": persona_id,
352
  "response": _get_persona_fallback(persona_id),
353
- "order": i
354
  })
 
 
355
 
356
  except Exception as e:
357
  print(f"Error generating response for {persona_id}: {e}")
@@ -359,9 +213,10 @@ async def chat_sequential(message: ChatMessage):
359
  "persona": chat_orchestrator.personas[persona_id].name,
360
  "persona_id": persona_id,
361
  "response": _get_persona_fallback(persona_id),
362
- "order": i
363
  })
364
 
 
 
365
  return {
366
  "type": "sequential_responses",
367
  "responses": responses,
@@ -378,11 +233,6 @@ async def chat_sequential(message: ChatMessage):
378
  }]
379
  }
380
 
381
- # Main chat endpoint (keep for compatibility)
382
- @router.post("/chat")
383
- async def chat_with_orchestrator(message: ChatMessage):
384
- """Redirect to sequential endpoint for better UX"""
385
- return await chat_sequential(message)
386
 
387
  # Individual advisor endpoint with context
388
  @router.post("/chat/{persona_id}")
@@ -395,7 +245,7 @@ async def chat_with_specific_advisor(persona_id: str, input: UserInput):
395
  session_context.append("user", input.user_input)
396
  persona = chat_orchestrator.personas[persona_id]
397
  context = session_context.full_log.copy()
398
- reply = await persona.respond(context)
399
  session_context.append(persona_id, reply)
400
 
401
  return {
@@ -426,10 +276,9 @@ async def reply_to_advisor(reply: ReplyToAdvisor):
426
 
427
  # Get response from specific advisor
428
  persona = chat_orchestrator.personas[reply.advisor_id]
429
- context = session_context.full_log.copy()
430
 
431
  # Generate response
432
- reply_response = await persona.respond(context)
433
  session_context.append(reply.advisor_id, reply_response)
434
 
435
  return {
@@ -508,7 +357,7 @@ async def upload_document(file: UploadFile = File(...)):
508
  raise HTTPException(status_code=400, detail="Document is empty or unreadable.")
509
 
510
  # Track file size and name
511
- session_context.append("user", f"[Uploaded Document Content]\n{content.strip()}")
512
  session_context.uploaded_files.append(file.filename)
513
  session_context.total_upload_size += len(file_bytes)
514
 
 
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
 
38
  else:
39
  raise ValueError(f"Unknown provider: {provider}")
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  # Initialize with default provider
42
  llm = create_llm_client()
43
  chat_orchestrator = ChatOrchestrator()
 
45
 
46
  session_context = GlobalSessionContext()
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  # Initialize personas
49
+ DEFAULT_PERSONAS = get_default_personas(llm)
50
+
51
  for persona in DEFAULT_PERSONAS:
52
  chat_orchestrator.register_persona(persona)
53
 
 
63
  class ChatMessage(BaseModel):
64
  user_input: str
65
  session_id: Optional[str] = None
66
+ response_length: Optional[str] = "medium"
67
 
68
  class ReplyToAdvisor(BaseModel):
69
  user_input: str
 
76
  # Helper functions for response validation
77
  def _is_valid_response(response: str, persona_id: str) -> bool:
78
  """Validate response quality"""
79
+ if len(response) < 2 or len(response) > 5000:
80
  return False
81
 
82
  # Check for AI confusion indicators
 
129
  llm = new_llm
130
 
131
  # Update all personas with new LLM
132
+ new_personas = get_default_personas(new_llm)
133
  chat_orchestrator.personas.clear()
134
  for persona in new_personas:
135
  chat_orchestrator.register_persona(persona)
 
175
 
176
  # Clear previous advisor responses to avoid confusion
177
  session_context.clear()
178
+ session_context.append("user", message.user_input)
179
+ session_context.append("orchestrator", enhanced_context)
180
 
181
+ advisor_order = chat_orchestrator.get_response_order()
182
+ print("Advisor Order:")
183
+ print(advisor_order)
184
  responses = []
185
 
186
+ for persona_id in advisor_order:
187
  try:
188
+ persona = chat_orchestrator.personas[persona_id]
189
+ reply = await persona.respond(session_context.full_log, response_length="medium")
190
+ print("Replies:")
191
+ print(reply)
 
 
192
 
193
  # Validate response before adding
194
  if _is_valid_response(reply, persona_id):
 
196
  "persona": persona.name,
197
  "persona_id": persona_id,
198
  "response": reply,
 
199
  })
200
  else:
201
  # Fallback response for invalid responses
 
203
  "persona": persona.name,
204
  "persona_id": persona_id,
205
  "response": _get_persona_fallback(persona_id),
 
206
  })
207
+
208
+ session_context.append(persona_id, reply)
209
 
210
  except Exception as e:
211
  print(f"Error generating response for {persona_id}: {e}")
 
213
  "persona": chat_orchestrator.personas[persona_id].name,
214
  "persona_id": persona_id,
215
  "response": _get_persona_fallback(persona_id),
 
216
  })
217
 
218
+ print("Response Block: " )
219
+ print(responses)
220
  return {
221
  "type": "sequential_responses",
222
  "responses": responses,
 
233
  }]
234
  }
235
 
 
 
 
 
 
236
 
237
  # Individual advisor endpoint with context
238
  @router.post("/chat/{persona_id}")
 
245
  session_context.append("user", input.user_input)
246
  persona = chat_orchestrator.personas[persona_id]
247
  context = session_context.full_log.copy()
248
+ reply = await persona.respond(context, response_length="medium")
249
  session_context.append(persona_id, reply)
250
 
251
  return {
 
276
 
277
  # Get response from specific advisor
278
  persona = chat_orchestrator.personas[reply.advisor_id]
 
279
 
280
  # Generate response
281
+ reply_response = await persona.respond(session_context.full_log, response_length="medium")
282
  session_context.append(reply.advisor_id, reply_response)
283
 
284
  return {
 
357
  raise HTTPException(status_code=400, detail="Document is empty or unreadable.")
358
 
359
  # Track file size and name
360
+ session_context.append("Document", f"[Uploaded Document Content]\n{content.strip()}")
361
  session_context.uploaded_files.append(file.filename)
362
  session_context.total_upload_size += len(file_bytes)
363
 
multi_llm_chatbot_backend/app/core/orchestrator.py CHANGED
@@ -15,13 +15,9 @@ class ChatOrchestrator:
15
  def get_active_personas(self) -> List[str]:
16
  return self.active_personas
17
 
18
- async def process_user_input(self, user_input: str, context: List[dict]):
19
- responses = []
20
 
21
- for pid in self.active_personas:
22
- persona = self.personas[pid]
23
- reply = await persona.respond(context)
24
- responses.append({"persona": persona.name, "response": reply})
25
- context.append({"role": persona.id, "content": reply})
26
 
27
- return responses
 
15
  def get_active_personas(self) -> List[str]:
16
  return self.active_personas
17
 
18
+ def get_response_order(self) ->List[str]:
 
19
 
20
+ # I have created this function to be a placeholder for the actual logic of response sequencing
21
+ # This logic can be replaced with something smarter like a LLM deciding order based on chat context
 
 
 
22
 
23
+ return self.personas
multi_llm_chatbot_backend/app/core/seamless_orchestrator.py CHANGED
@@ -129,7 +129,7 @@ class SeamlessOrchestrator:
129
  Ask briefly and naturally."""
130
 
131
  try:
132
- question = await self.llm.generate(system_prompt, [{"role": "user", "content": context}])
133
  return question.strip()
134
  except Exception as e:
135
  print(f"Error generating orchestrator question: {e}")
 
129
  Ask briefly and naturally."""
130
 
131
  try:
132
+ question = await self.llm.generate(system_prompt, [{"role": "user", "content": context}], temperature=0.5, max_tokens=50)
133
  return question.strip()
134
  except Exception as e:
135
  print(f"Error generating orchestrator question: {e}")
multi_llm_chatbot_backend/app/utils/document_extractor.py CHANGED
@@ -2,6 +2,7 @@ from io import BytesIO
2
  import PyPDF2
3
  import tempfile
4
  import docx2txt
 
5
 
6
  def extract_text_from_file(file_bytes: bytes, content_type: str) -> str:
7
  if content_type == "application/pdf":
@@ -12,7 +13,10 @@ def extract_text_from_file(file_bytes: bytes, content_type: str) -> str:
12
  with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as tmp:
13
  tmp.write(file_bytes)
14
  tmp_path = tmp.name
15
- return docx2txt.process(tmp_path)
 
 
 
16
 
17
  elif content_type == "text/plain":
18
  return file_bytes.decode("utf-8")
 
2
  import PyPDF2
3
  import tempfile
4
  import docx2txt
5
+ import os
6
 
7
  def extract_text_from_file(file_bytes: bytes, content_type: str) -> str:
8
  if content_type == "application/pdf":
 
13
  with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as tmp:
14
  tmp.write(file_bytes)
15
  tmp_path = tmp.name
16
+ try:
17
+ return docx2txt.process(tmp_path)
18
+ finally:
19
+ os.unlink(tmp_path) # Clean up temp file
20
 
21
  elif content_type == "text/plain":
22
  return file_bytes.decode("utf-8")