Girish Jeswani commited on
Commit
5542d2b
·
1 Parent(s): 634624b

fix json format bug

Browse files
multi_llm_chatbot_backend/app/core/canvas_analysis.py CHANGED
@@ -68,15 +68,32 @@ class CanvasAnalysisService:
68
  insights = []
69
 
70
  for message in messages:
71
- if message.get("type") == "assistant" and "responses" in message:
72
- # Handle multi-persona responses
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  for response in message.get("responses", []):
74
  persona_insights = await self._extract_insights_from_persona_response(
75
  response, message.get("id"), chat_session_id
76
  )
77
  insights.extend(persona_insights)
 
78
  elif message.get("role") == "assistant" and message.get("content"):
79
- # Handle single response format
80
  persona_insights = await self._extract_insights_from_content(
81
  message.get("content", ""), "assistant",
82
  message.get("id"), chat_session_id
@@ -85,13 +102,15 @@ class CanvasAnalysisService:
85
 
86
  # Remove duplicates and low-confidence insights
87
  unique_insights = self._deduplicate_insights(insights)
88
- high_confidence_insights = [i for i in unique_insights if i.confidence_score >= 0.6]
89
 
90
  logger.info(f"Extracted {len(high_confidence_insights)} insights from {len(messages)} messages")
91
  return high_confidence_insights
92
 
93
  except Exception as e:
94
  logger.error(f"Error extracting insights from messages: {e}")
 
 
95
  return []
96
 
97
  async def _extract_insights_from_persona_response(self, response: Dict, message_id: str, chat_session_id: str) -> List[CanvasInsight]:
@@ -103,7 +122,7 @@ class CanvasAnalysisService:
103
 
104
  async def _extract_insights_from_content(self, content: str, persona_id: str, message_id: str, chat_session_id: str) -> List[CanvasInsight]:
105
  """Extract actionable insights from content using LLM analysis"""
106
- if not content or len(content.strip()) < 50:
107
  return []
108
 
109
  try:
@@ -126,15 +145,23 @@ class CanvasAnalysisService:
126
  """
127
 
128
  if self.llm_client:
129
- llm_response = await self.llm_client.generate(
130
- system_prompt="You are an expert at extracting actionable PhD guidance from advisor responses.",
131
- context=[{"role": "user", "content": extraction_prompt}],
132
- temperature=0.3,
133
- max_tokens=500
134
- )
135
-
136
  try:
137
- insights_data = json.loads(llm_response.strip())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  insights = []
139
 
140
  for item in insights_data:
@@ -149,10 +176,16 @@ class CanvasAnalysisService:
149
  )
150
  insights.append(insight)
151
 
 
152
  return insights
153
 
154
- except json.JSONDecodeError:
155
- logger.warning(f"Failed to parse LLM insights response for {persona_id}")
 
 
 
 
 
156
  # Fallback to rule-based extraction
157
  return self._extract_insights_rule_based(content, persona_id, message_id, chat_session_id)
158
 
@@ -178,7 +211,7 @@ class CanvasAnalysisService:
178
 
179
  for sentence in sentences:
180
  sentence = sentence.strip()
181
- if len(sentence) < 20: # Skip very short sentences
182
  continue
183
 
184
  # Check if sentence contains actionable language
@@ -198,6 +231,7 @@ class CanvasAnalysisService:
198
  )
199
  insights.append(insight)
200
 
 
201
  return insights[:3] # Limit to 3 insights per message to avoid noise
202
 
203
  def _extract_keywords_from_sentence(self, sentence: str) -> List[str]:
@@ -226,157 +260,42 @@ class CanvasAnalysisService:
226
  return dict(categorized)
227
 
228
  def _determine_section(self, insight: CanvasInsight) -> str:
229
- """Determine which canvas section an insight belongs to"""
230
  content_lower = insight.content.lower()
231
- keywords_lower = [kw.lower() for kw in insight.keywords]
232
  all_text = content_lower + " " + " ".join(keywords_lower)
233
 
234
  # Score each section based on keyword matches
235
  section_scores = {}
 
 
 
 
236
 
237
- for section_key, section_keywords in self.section_keywords.items():
238
- score = 0
239
- for keyword in section_keywords:
240
- if keyword in all_text:
241
- score += 1
242
-
243
- # Bonus for persona-specific insights
244
- persona_bonuses = {
245
- "methodologist": {"methodology": 2, "data_analysis": 1},
246
- "theorist": {"theoretical_framework": 2, "literature_review": 1},
247
- "pragmatist": {"next_steps": 2, "research_progress": 1},
248
- "socratic": {"theoretical_framework": 1, "challenges_obstacles": 1},
249
- "motivator": {"motivation_mindset": 2, "career_development": 1},
250
- "critic": {"challenges_obstacles": 1, "writing_communication": 1},
251
- "empathetic": {"motivation_mindset": 2},
252
- "visionary": {"career_development": 1, "research_progress": 1}
253
- }
254
-
255
- if insight.source_persona in persona_bonuses:
256
- score += persona_bonuses[insight.source_persona].get(section_key, 0)
257
-
258
- section_scores[section_key] = score
259
-
260
- # Return section with highest score, default to general category
261
- best_section = max(section_scores, key=section_scores.get) if section_scores else "research_progress"
262
-
263
- # If no keywords matched, categorize by persona type
264
- if section_scores[best_section] == 0:
265
- persona_defaults = {
266
- "methodologist": "methodology",
267
- "theorist": "theoretical_framework",
268
- "pragmatist": "next_steps",
269
- "socratic": "theoretical_framework",
270
- "motivator": "motivation_mindset",
271
- "critic": "challenges_obstacles",
272
- "empathetic": "motivation_mindset",
273
- "visionary": "career_development",
274
- "storyteller": "writing_communication",
275
- "minimalist": "next_steps"
276
- }
277
- best_section = persona_defaults.get(insight.source_persona, "research_progress")
278
-
279
- return best_section
280
 
281
  def _deduplicate_insights(self, insights: List[CanvasInsight]) -> List[CanvasInsight]:
282
- """Remove duplicate insights based on semantic similarity"""
283
  if not insights:
284
- return []
285
 
286
  unique_insights = []
287
- seen_contents = set()
288
 
289
  for insight in insights:
290
  # Simple deduplication based on content similarity
291
- content_normalized = re.sub(r'\s+', ' ', insight.content.lower().strip())
292
-
293
- # Check for substantial overlap with existing insights
294
- is_duplicate = False
295
- for seen_content in seen_contents:
296
- if self._calculate_similarity(content_normalized, seen_content) > 0.8:
297
- is_duplicate = True
298
- break
299
 
300
- if not is_duplicate:
 
301
  unique_insights.append(insight)
302
- seen_contents.add(content_normalized)
303
-
304
- return unique_insights
305
-
306
- def _calculate_similarity(self, text1: str, text2: str) -> float:
307
- """Calculate simple similarity between two texts"""
308
- words1 = set(text1.split())
309
- words2 = set(text2.split())
310
 
311
- if not words1 or not words2:
312
- return 0.0
313
-
314
- intersection = words1.intersection(words2)
315
- union = words1.union(words2)
316
-
317
- return len(intersection) / len(union) if union else 0.0
318
-
319
- def prioritize_insights(self, insights: List[CanvasInsight]) -> List[CanvasInsight]:
320
- """Prioritize insights based on relevance and actionability"""
321
-
322
- def calculate_priority_score(insight: CanvasInsight) -> float:
323
- score = insight.confidence_score
324
-
325
- # Boost score for actionable language
326
- actionable_terms = ["should", "need to", "must", "plan", "goal", "deadline", "complete"]
327
- content_lower = insight.content.lower()
328
- for term in actionable_terms:
329
- if term in content_lower:
330
- score += 0.1
331
-
332
- # Boost score for specific vs generic advice
333
- specific_terms = ["chapter", "data", "analysis", "method", "theory", "timeline"]
334
- for term in specific_terms:
335
- if term in content_lower:
336
- score += 0.05
337
-
338
- # Boost score for certain personas
339
- persona_boosts = {
340
- "pragmatist": 0.1, # Action-oriented insights are valuable
341
- "methodologist": 0.08, # Methodological insights are important
342
- "critic": 0.06 # Critical feedback is valuable
343
- }
344
- score += persona_boosts.get(insight.source_persona, 0)
345
-
346
- return min(score, 1.0) # Cap at 1.0
347
-
348
- # Calculate scores and sort
349
- for insight in insights:
350
- insight.confidence_score = calculate_priority_score(insight)
351
-
352
- return sorted(insights, key=lambda x: x.confidence_score, reverse=True)
353
-
354
- async def generate_section_summary(self, section: CanvasSection) -> str:
355
- """Generate a summary for a canvas section using LLM"""
356
- if not section.insights or not self.llm_client:
357
- return f"Key insights for {section.title}"
358
-
359
- try:
360
- insights_text = "\n".join([f"- {insight.content}" for insight in section.insights])
361
-
362
- prompt = f"""
363
- Create a 1-2 sentence summary of these PhD insights for the section "{section.title}":
364
-
365
- {insights_text}
366
-
367
- The summary should be concise, professional, and suitable for sharing with an advisor.
368
- Return only the summary text, no other content.
369
- """
370
-
371
- summary = await self.llm_client.generate(
372
- system_prompt="You create concise summaries of academic insights.",
373
- context=[{"role": "user", "content": prompt}],
374
- temperature=0.2,
375
- max_tokens=100
376
- )
377
-
378
- return summary.strip()
379
-
380
- except Exception as e:
381
- logger.error(f"Error generating section summary: {e}")
382
- return f"Key insights and guidance for {section.title}"
 
68
  insights = []
69
 
70
  for message in messages:
71
+ # Handle the actual stored message format from your database
72
+ msg_type = message.get("type", "")
73
+
74
+ if msg_type == "advisor":
75
+ # This is the format used in your database
76
+ content = message.get("content", "")
77
+ persona_id = message.get("advisorName", message.get("persona", "advisor"))
78
+ message_id = message.get("id", str(message.get("_id", "")))
79
+
80
+ if content and len(content.strip()) > 20: # Only process substantial content
81
+ persona_insights = await self._extract_insights_from_content(
82
+ content, persona_id, message_id, chat_session_id
83
+ )
84
+ insights.extend(persona_insights)
85
+ logger.debug(f"Extracted {len(persona_insights)} insights from {persona_id} message")
86
+
87
+ elif msg_type == "assistant" and "responses" in message:
88
+ # Handle multi-persona responses (if this format exists)
89
  for response in message.get("responses", []):
90
  persona_insights = await self._extract_insights_from_persona_response(
91
  response, message.get("id"), chat_session_id
92
  )
93
  insights.extend(persona_insights)
94
+
95
  elif message.get("role") == "assistant" and message.get("content"):
96
+ # Handle converted message format (from export functions)
97
  persona_insights = await self._extract_insights_from_content(
98
  message.get("content", ""), "assistant",
99
  message.get("id"), chat_session_id
 
102
 
103
  # Remove duplicates and low-confidence insights
104
  unique_insights = self._deduplicate_insights(insights)
105
+ high_confidence_insights = [i for i in unique_insights if i.confidence_score >= 0.5] # Lowered threshold
106
 
107
  logger.info(f"Extracted {len(high_confidence_insights)} insights from {len(messages)} messages")
108
  return high_confidence_insights
109
 
110
  except Exception as e:
111
  logger.error(f"Error extracting insights from messages: {e}")
112
+ import traceback
113
+ logger.error(f"Full traceback: {traceback.format_exc()}")
114
  return []
115
 
116
  async def _extract_insights_from_persona_response(self, response: Dict, message_id: str, chat_session_id: str) -> List[CanvasInsight]:
 
122
 
123
  async def _extract_insights_from_content(self, content: str, persona_id: str, message_id: str, chat_session_id: str) -> List[CanvasInsight]:
124
  """Extract actionable insights from content using LLM analysis"""
125
+ if not content or len(content.strip()) < 30: # Lowered minimum length
126
  return []
127
 
128
  try:
 
145
  """
146
 
147
  if self.llm_client:
 
 
 
 
 
 
 
148
  try:
149
+ llm_response = await self.llm_client.generate(
150
+ system_prompt="You are an expert at extracting actionable PhD guidance from advisor responses.",
151
+ context=[{"role": "user", "content": extraction_prompt}],
152
+ temperature=0.3,
153
+ max_tokens=500
154
+ )
155
+
156
+ # Clean the response to extract just the JSON
157
+ llm_response = llm_response.strip()
158
+ if llm_response.startswith('```json'):
159
+ llm_response = llm_response[7:]
160
+ if llm_response.endswith('```'):
161
+ llm_response = llm_response[:-3]
162
+ llm_response = llm_response.strip()
163
+
164
+ insights_data = json.loads(llm_response)
165
  insights = []
166
 
167
  for item in insights_data:
 
176
  )
177
  insights.append(insight)
178
 
179
+ logger.debug(f"LLM extracted {len(insights)} insights from {persona_id}")
180
  return insights
181
 
182
+ except json.JSONDecodeError as je:
183
+ logger.warning(f"Failed to parse LLM insights response for {persona_id}: {je}")
184
+ logger.debug(f"Raw LLM response: {llm_response}")
185
+ # Fallback to rule-based extraction
186
+ return self._extract_insights_rule_based(content, persona_id, message_id, chat_session_id)
187
+ except Exception as llm_error:
188
+ logger.warning(f"LLM extraction failed for {persona_id}: {llm_error}")
189
  # Fallback to rule-based extraction
190
  return self._extract_insights_rule_based(content, persona_id, message_id, chat_session_id)
191
 
 
211
 
212
  for sentence in sentences:
213
  sentence = sentence.strip()
214
+ if len(sentence) < 15: # Lowered minimum length
215
  continue
216
 
217
  # Check if sentence contains actionable language
 
231
  )
232
  insights.append(insight)
233
 
234
+ logger.debug(f"Rule-based extracted {len(insights)} insights from {persona_id}")
235
  return insights[:3] # Limit to 3 insights per message to avoid noise
236
 
237
  def _extract_keywords_from_sentence(self, sentence: str) -> List[str]:
 
260
  return dict(categorized)
261
 
262
  def _determine_section(self, insight: CanvasInsight) -> str:
263
+ """Determine which section an insight belongs to"""
264
  content_lower = insight.content.lower()
265
+ keywords_lower = [k.lower() for k in insight.keywords]
266
  all_text = content_lower + " " + " ".join(keywords_lower)
267
 
268
  # Score each section based on keyword matches
269
  section_scores = {}
270
+ for section, keywords in self.section_keywords.items():
271
+ score = sum(1 for keyword in keywords if keyword in all_text)
272
+ if score > 0:
273
+ section_scores[section] = score
274
 
275
+ # Return the section with highest score, or default
276
+ if section_scores:
277
+ return max(section_scores, key=section_scores.get)
278
+ else:
279
+ return "general_notes" # Default section
280
+
281
+ def prioritize_insights(self, insights: List[CanvasInsight]) -> List[CanvasInsight]:
282
+ """Sort insights by priority (confidence score and recency)"""
283
+ return sorted(insights, key=lambda x: (x.confidence_score, x.extracted_at), reverse=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
 
285
  def _deduplicate_insights(self, insights: List[CanvasInsight]) -> List[CanvasInsight]:
286
+ """Remove duplicate or very similar insights"""
287
  if not insights:
288
+ return insights
289
 
290
  unique_insights = []
291
+ seen_content = set()
292
 
293
  for insight in insights:
294
  # Simple deduplication based on content similarity
295
+ content_key = insight.content.lower().strip()[:100] # First 100 chars
 
 
 
 
 
 
 
296
 
297
+ if content_key not in seen_content:
298
+ seen_content.add(content_key)
299
  unique_insights.append(insight)
 
 
 
 
 
 
 
 
300
 
301
+ return unique_insights