Girish Jeswani commited on
Commit
8498958
·
1 Parent(s): 62d4bb4

update model and chat fetch

Browse files
multi_llm_chatbot_backend/app/core/canvas_manager.py CHANGED
@@ -2,6 +2,8 @@ import logging
2
  from typing import Dict, List, Optional
3
  from datetime import datetime, timedelta
4
  from bson import ObjectId
 
 
5
 
6
  from app.models.phd_canvas import PhdCanvas, CanvasInsight, UpdateCanvasRequest
7
  from app.core.canvas_analysis import CanvasAnalysisService
@@ -15,6 +17,8 @@ class CanvasManager:
15
  def __init__(self):
16
  self.analysis_service = CanvasAnalysisService(llm_client=llm)
17
  self._db = None
 
 
18
 
19
  def get_database(self):
20
  """Lazy database connection to avoid circular imports"""
@@ -56,113 +60,154 @@ class CanvasManager:
56
 
57
  async def update_canvas(self, user_id: str, request: UpdateCanvasRequest) -> PhdCanvas:
58
  """Update canvas with latest insights from chat sessions"""
59
- try:
60
- db = self.get_database()
61
- canvas = await self.get_or_create_canvas(user_id)
62
-
63
- logger.info(f"Updating canvas for user {user_id}, force_full={request.force_full_update}")
64
-
65
- # IMPORTANT: Auto-detect if this should be a full update for first-time canvas
66
- is_first_time_update = (
67
- canvas.last_chat_processed is None and
68
- canvas.total_insights == 0 and
69
- not request.force_full_update
70
- )
71
-
72
- if is_first_time_update:
73
- logger.info(f"Auto-detecting first-time canvas update for user {user_id}. Converting to full update.")
74
- request.force_full_update = True
75
-
76
- # Determine which chats to process
77
- if request.force_full_update:
78
- # Process all chats
79
- chat_sessions = await self._get_all_user_chat_sessions(user_id)
80
- logger.info(f"Force full update: processing {len(chat_sessions)} total chat sessions")
81
- else:
82
- # Process only chats created/updated after last canvas update
83
- chat_sessions = await self._get_new_chat_sessions(user_id, canvas.last_chat_processed)
84
- logger.info(f"Incremental update: processing {len(chat_sessions)} new chat sessions since {canvas.last_chat_processed}")
85
-
86
- if not chat_sessions:
87
- logger.info("No new chat sessions to process")
88
- return canvas
89
-
90
- # Filter chat sessions if specific ones requested
91
- if request.include_chat_sessions:
92
- chat_sessions = [
93
- chat for chat in chat_sessions
94
- if str(chat["_id"]) in request.include_chat_sessions
95
- ]
96
- logger.info(f"Filtered to {len(chat_sessions)} specifically requested chat sessions")
97
-
98
- # Process each chat session for insights
99
- all_new_insights = []
100
- processed_chat_ids = []
101
-
102
- for chat_session in chat_sessions:
103
- try:
104
- chat_id = str(chat_session["_id"])
105
- messages = chat_session.get("messages", [])
106
-
107
- if not messages:
108
- continue
109
-
110
- logger.info(f"Processing chat {chat_id} with {len(messages)} messages")
111
-
112
- # Extract insights from this chat session
113
- session_insights = await self.analysis_service.extract_insights_from_messages(
114
- messages, chat_id
115
- )
116
-
117
- if session_insights:
118
- all_new_insights.extend(session_insights)
119
- processed_chat_ids.append(chat_id)
120
- logger.info(f"Extracted {len(session_insights)} insights from chat {chat_id}")
121
-
122
- except Exception as e:
123
- logger.error(f"Error processing chat session {chat_session.get('_id')}: {e}")
124
- continue
125
-
126
- if all_new_insights:
127
- # Categorize insights by section
128
- categorized_insights = self.analysis_service.categorize_insights(all_new_insights)
129
 
130
- # Update canvas sections
131
- sections_updated = 0
132
- for section_key, insights in categorized_insights.items():
133
- if request.exclude_sections and section_key in request.exclude_sections:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  continue
 
 
 
 
135
 
136
- # Prioritize insights before adding
137
- prioritized_insights = self.analysis_service.prioritize_insights(insights)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
- # Limit insights per section to avoid overwhelming canvas
140
- max_insights_per_section = 10
141
- limited_insights = prioritized_insights[:max_insights_per_section]
142
 
143
- if limited_insights:
144
- canvas.update_section(section_key, limited_insights)
145
- sections_updated += 1
146
- logger.info(f"Updated section '{section_key}' with {len(limited_insights)} insights")
147
-
148
- # Update canvas metadata
149
- canvas.last_chat_processed = datetime.utcnow()
150
- canvas.last_updated = datetime.utcnow()
151
 
152
- # Save updated canvas to database
153
- await self._save_canvas(canvas)
154
 
155
- logger.info(f"Canvas update completed: {len(all_new_insights)} new insights, {sections_updated} sections updated")
156
- else:
157
- logger.info("No insights extracted from chat sessions")
158
-
159
- return canvas
160
-
161
- except Exception as e:
162
- logger.error(f"Error updating canvas for user {user_id}: {e}")
163
- import traceback
164
- logger.error(f"Full traceback: {traceback.format_exc()}")
165
- raise
166
 
167
  async def _get_all_user_chat_sessions(self, user_id: str) -> List[Dict]:
168
  """Get all chat sessions for a user"""
@@ -264,94 +309,100 @@ class CanvasManager:
264
  return False
265
 
266
  async def get_canvas_stats(self, user_id: str) -> Dict:
267
- """Get statistics about user's canvas"""
268
  try:
269
  canvas = await self.get_or_create_canvas(user_id)
270
 
271
- stats = {
 
 
 
 
 
 
 
 
 
 
272
  "total_insights": canvas.total_insights,
273
  "total_sections": len(canvas.sections),
274
  "last_updated": canvas.last_updated,
275
  "last_chat_processed": canvas.last_chat_processed,
276
  "created_at": canvas.created_at,
277
  "auto_update": canvas.auto_update,
278
- "sections_breakdown": {}
279
  }
280
 
281
- # Add breakdown by section
282
- for section_key, section in canvas.sections.items():
283
- stats["sections_breakdown"][section_key] = {
284
- "title": section.title,
285
- "insight_count": len(section.insights),
286
- "last_updated": section.updated_at,
287
- "priority": section.priority
288
- }
289
-
290
- return stats
291
-
292
  except Exception as e:
293
  logger.error(f"Error getting canvas stats for user {user_id}: {e}")
294
  return {
295
  "total_insights": 0,
296
  "total_sections": 0,
297
- "error": str(e)
298
  }
299
 
300
  async def export_canvas_for_printing(self, user_id: str) -> Dict:
301
- """Export canvas in a print-optimized format"""
302
  try:
303
  canvas = await self.get_or_create_canvas(user_id)
304
 
305
- # Sort sections by priority and insight count
306
- sorted_sections = []
307
  for section_key, section in canvas.sections.items():
308
- if section.insights: # Only include sections with insights
309
- # Sort insights by confidence score
310
- sorted_insights = sorted(
311
- section.insights,
312
- key=lambda x: x.confidence_score,
313
- reverse=True
314
- )
315
-
316
- sorted_sections.append({
317
- "key": section_key,
318
- "title": section.title,
319
- "description": section.description,
320
- "insights": [
321
- {
322
- "content": insight.content,
323
- "source": insight.source_persona.title(),
324
- "confidence": round(insight.confidence_score, 2)
325
- }
326
- for insight in sorted_insights[:8] # Limit for printing
327
- ],
328
- "insight_count": len(section.insights),
329
- "priority": section.priority
330
- })
331
-
332
- # Sort sections by priority, then by insight count
333
- sorted_sections.sort(key=lambda x: (x["priority"], -x["insight_count"]))
334
 
335
  return {
336
- "user_id": user_id,
337
  "generated_at": datetime.utcnow(),
338
  "total_insights": canvas.total_insights,
339
  "last_updated": canvas.last_updated,
340
- "sections": sorted_sections,
341
  "metadata": {
342
- "canvas_id": str(canvas.id),
343
  "created_at": canvas.created_at,
344
- "version": "1.0"
 
345
  }
346
  }
347
 
348
  except Exception as e:
349
- logger.error(f"Error exporting canvas for printing: {e}")
350
  raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
 
352
- # Global canvas manager instance
353
- canvas_manager = CanvasManager()
354
 
355
  def get_canvas_manager() -> CanvasManager:
356
- """Get the global canvas manager instance"""
357
- return canvas_manager
 
 
 
 
2
  from typing import Dict, List, Optional
3
  from datetime import datetime, timedelta
4
  from bson import ObjectId
5
+ import asyncio
6
+ import hashlib
7
 
8
  from app.models.phd_canvas import PhdCanvas, CanvasInsight, UpdateCanvasRequest
9
  from app.core.canvas_analysis import CanvasAnalysisService
 
17
  def __init__(self):
18
  self.analysis_service = CanvasAnalysisService(llm_client=llm)
19
  self._db = None
20
+ # Add lock dictionary to prevent concurrent updates for the same user
21
+ self._update_locks = {}
22
 
23
  def get_database(self):
24
  """Lazy database connection to avoid circular imports"""
 
60
 
61
  async def update_canvas(self, user_id: str, request: UpdateCanvasRequest) -> PhdCanvas:
62
  """Update canvas with latest insights from chat sessions"""
63
+
64
+ # Get or create a lock for this user
65
+ if user_id not in self._update_locks:
66
+ self._update_locks[user_id] = asyncio.Lock()
67
+
68
+ # Check if an update is already in progress
69
+ if self._update_locks[user_id].locked():
70
+ logger.warning(f"Canvas update already in progress for user {user_id}, skipping duplicate request")
71
+ # Return current canvas without updating
72
+ return await self.get_or_create_canvas(user_id)
73
+
74
+ # Acquire lock to prevent concurrent updates
75
+ async with self._update_locks[user_id]:
76
+ try:
77
+ db = self.get_database()
78
+ canvas = await self.get_or_create_canvas(user_id)
79
+
80
+ logger.info(f"Updating canvas for user {user_id}, force_full={request.force_full_update}")
81
+
82
+ # IMPORTANT: Auto-detect if this should be a full update for first-time canvas
83
+ is_first_time_update = (
84
+ canvas.last_chat_processed is None and
85
+ canvas.total_insights == 0 and
86
+ not request.force_full_update
87
+ )
88
+
89
+ if is_first_time_update:
90
+ logger.info(f"Auto-detecting first-time canvas update for user {user_id}. Converting to full update.")
91
+ request.force_full_update = True
92
+
93
+ # Store the timestamp BEFORE we start processing
94
+ update_started_at = datetime.utcnow()
95
+
96
+ # Determine which chats to process
97
+ if request.force_full_update:
98
+ # Process all chats
99
+ chat_sessions = await self._get_all_user_chat_sessions(user_id)
100
+ logger.info(f"Force full update: processing {len(chat_sessions)} total chat sessions")
101
+ else:
102
+ # Process only chats created/updated after last canvas update
103
+ chat_sessions = await self._get_new_chat_sessions(user_id, canvas.last_chat_processed)
104
+ logger.info(f"Incremental update: processing {len(chat_sessions)} new chat sessions since {canvas.last_chat_processed}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
+ if not chat_sessions:
107
+ logger.info("No new chat sessions to process")
108
+ return canvas
109
+
110
+ # Filter chat sessions if specific ones requested
111
+ if request.include_chat_sessions:
112
+ chat_sessions = [
113
+ chat for chat in chat_sessions
114
+ if str(chat["_id"]) in request.include_chat_sessions
115
+ ]
116
+ logger.info(f"Filtered to {len(chat_sessions)} specifically requested chat sessions")
117
+
118
+ # Track processed chat+message combinations to prevent duplicates
119
+ processed_sources = set()
120
+
121
+ # Get existing processed sources from canvas
122
+ for section in canvas.sections.values():
123
+ for insight in section.insights:
124
+ if insight.source_chat_session and insight.source_message_id:
125
+ processed_sources.add((insight.source_chat_session, insight.source_message_id))
126
+
127
+ # Process each chat session for insights
128
+ all_new_insights = []
129
+ processed_chat_ids = []
130
+
131
+ for chat_session in chat_sessions:
132
+ try:
133
+ chat_id = str(chat_session["_id"])
134
+ messages = chat_session.get("messages", [])
135
+
136
+ if not messages:
137
+ continue
138
+
139
+ # Check if we've already processed these messages
140
+ messages_to_process = []
141
+ for msg in messages:
142
+ msg_id = msg.get('id', '')
143
+ if (chat_id, msg_id) not in processed_sources:
144
+ messages_to_process.append(msg)
145
+
146
+ if not messages_to_process:
147
+ logger.info(f"All messages from chat {chat_id} already processed, skipping")
148
+ continue
149
+
150
+ logger.info(f"Processing {len(messages_to_process)} new messages from chat {chat_id}")
151
+
152
+ # Extract insights from new messages only
153
+ session_insights = await self.analysis_service.extract_insights_from_messages(
154
+ messages_to_process, chat_id
155
+ )
156
+
157
+ if session_insights:
158
+ all_new_insights.extend(session_insights)
159
+ processed_chat_ids.append(chat_id)
160
+ logger.info(f"Extracted {len(session_insights)} insights from chat {chat_id}")
161
+
162
+ except Exception as e:
163
+ logger.error(f"Error processing chat session {chat_session.get('_id')}: {e}")
164
  continue
165
+
166
+ if all_new_insights:
167
+ # Categorize insights by section
168
+ categorized_insights = self.analysis_service.categorize_insights(all_new_insights)
169
 
170
+ # Update canvas sections
171
+ sections_updated = 0
172
+ for section_key, insights in categorized_insights.items():
173
+ if request.exclude_sections and section_key in request.exclude_sections:
174
+ continue
175
+
176
+ # Prioritize insights before adding
177
+ prioritized_insights = self.analysis_service.prioritize_insights(insights)
178
+
179
+ # Limit insights per section to avoid overwhelming canvas
180
+ max_insights_per_section = 10
181
+ limited_insights = prioritized_insights[:max_insights_per_section]
182
+
183
+ if limited_insights:
184
+ canvas.update_section(section_key, limited_insights)
185
+ sections_updated += 1
186
+ logger.info(f"Updated section '{section_key}' with {len(limited_insights)} insights")
187
 
188
+ # Update canvas metadata with the timestamp from BEFORE processing
189
+ canvas.last_chat_processed = update_started_at
190
+ canvas.last_updated = datetime.utcnow()
191
 
192
+ # Save updated canvas to database
193
+ await self._save_canvas(canvas)
194
+
195
+ logger.info(f"Canvas update completed: {len(all_new_insights)} new insights, {sections_updated} sections updated")
196
+ else:
197
+ logger.info("No insights extracted from chat sessions")
 
 
198
 
199
+ return canvas
 
200
 
201
+ except Exception as e:
202
+ logger.error(f"Error updating canvas for user {user_id}: {e}")
203
+ import traceback
204
+ logger.error(f"Full traceback: {traceback.format_exc()}")
205
+ raise
206
+ finally:
207
+ # Clean up lock if no longer needed
208
+ if user_id in self._update_locks and not self._update_locks[user_id].locked():
209
+ # Remove lock after some time to prevent memory buildup
210
+ pass # Keep lock for potential future use
 
211
 
212
  async def _get_all_user_chat_sessions(self, user_id: str) -> List[Dict]:
213
  """Get all chat sessions for a user"""
 
309
  return False
310
 
311
  async def get_canvas_stats(self, user_id: str) -> Dict:
312
+ """Get statistics about the user's PhD Canvas"""
313
  try:
314
  canvas = await self.get_or_create_canvas(user_id)
315
 
316
+ # Calculate section breakdown
317
+ sections_breakdown = {}
318
+ for section_key, section in canvas.sections.items():
319
+ sections_breakdown[section_key] = {
320
+ "title": section.title,
321
+ "insight_count": len(section.insights),
322
+ "priority": section.priority,
323
+ "last_updated": section.updated_at
324
+ }
325
+
326
+ return {
327
  "total_insights": canvas.total_insights,
328
  "total_sections": len(canvas.sections),
329
  "last_updated": canvas.last_updated,
330
  "last_chat_processed": canvas.last_chat_processed,
331
  "created_at": canvas.created_at,
332
  "auto_update": canvas.auto_update,
333
+ "sections_breakdown": sections_breakdown
334
  }
335
 
 
 
 
 
 
 
 
 
 
 
 
336
  except Exception as e:
337
  logger.error(f"Error getting canvas stats for user {user_id}: {e}")
338
  return {
339
  "total_insights": 0,
340
  "total_sections": 0,
341
+ "sections_breakdown": {}
342
  }
343
 
344
  async def export_canvas_for_printing(self, user_id: str) -> Dict:
345
+ """Export canvas in a format optimized for printing"""
346
  try:
347
  canvas = await self.get_or_create_canvas(user_id)
348
 
349
+ # Format sections for printing
350
+ sections = []
351
  for section_key, section in canvas.sections.items():
352
+ formatted_section = {
353
+ "title": section.title,
354
+ "description": section.description,
355
+ "insights": [
356
+ {
357
+ "content": insight.content,
358
+ "source": insight.source_persona,
359
+ "confidence": insight.confidence_score
360
+ }
361
+ for insight in section.insights[:5] # Limit to top 5 for printing
362
+ ]
363
+ }
364
+ sections.append(formatted_section)
 
 
 
 
 
 
 
 
 
 
 
 
 
365
 
366
  return {
367
+ "user_id": str(canvas.user_id),
368
  "generated_at": datetime.utcnow(),
369
  "total_insights": canvas.total_insights,
370
  "last_updated": canvas.last_updated,
371
+ "sections": sections,
372
  "metadata": {
 
373
  "created_at": canvas.created_at,
374
+ "last_chat_processed": canvas.last_chat_processed,
375
+ "print_optimized": True
376
  }
377
  }
378
 
379
  except Exception as e:
380
+ logger.error(f"Error exporting canvas for printing for user {user_id}: {e}")
381
  raise
382
+
383
+ async def toggle_auto_update(self, user_id: str, enabled: bool) -> bool:
384
+ """Toggle auto-update setting for a canvas"""
385
+ try:
386
+ db = self.get_database()
387
+ user_object_id = ObjectId(user_id)
388
+
389
+ result = await db.phd_canvases.update_one(
390
+ {"user_id": user_object_id},
391
+ {"$set": {"auto_update": enabled}}
392
+ )
393
+
394
+ return result.modified_count > 0
395
+
396
+ except Exception as e:
397
+ logger.error(f"Error toggling auto-update for user {user_id}: {e}")
398
+ return False
399
 
400
+ # Singleton instance
401
+ _canvas_manager_instance = None
402
 
403
  def get_canvas_manager() -> CanvasManager:
404
+ """Get singleton instance of CanvasManager"""
405
+ global _canvas_manager_instance
406
+ if _canvas_manager_instance is None:
407
+ _canvas_manager_instance = CanvasManager()
408
+ return _canvas_manager_instance
multi_llm_chatbot_backend/app/models/phd_canvas.py CHANGED
@@ -3,6 +3,9 @@ from typing import Dict, List, Optional, Any
3
  from datetime import datetime
4
  from bson import ObjectId
5
  from app.models.user import PyObjectId
 
 
 
6
 
7
  class CanvasInsight(BaseModel):
8
  """Individual insight extracted from chat messages"""
@@ -53,13 +56,45 @@ class PhdCanvas(BaseModel):
53
  description=self._get_section_description(section_key)
54
  )
55
 
56
- # Add new insights and remove duplicates
57
- existing_contents = {insight.content for insight in self.sections[section_key].insights}
58
- new_insights = [insight for insight in insights if insight.content not in existing_contents]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- self.sections[section_key].insights.extend(new_insights)
61
- self.sections[section_key].updated_at = datetime.utcnow()
62
- self.last_updated = datetime.utcnow()
 
 
 
 
63
 
64
  # Update total insights count
65
  self.total_insights = sum(len(section.insights) for section in self.sections.values())
 
3
  from datetime import datetime
4
  from bson import ObjectId
5
  from app.models.user import PyObjectId
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
 
10
  class CanvasInsight(BaseModel):
11
  """Individual insight extracted from chat messages"""
 
56
  description=self._get_section_description(section_key)
57
  )
58
 
59
+ existing_insights_map = {
60
+ insight.content.strip().lower(): insight
61
+ for insight in self.sections[section_key].insights
62
+ }
63
+
64
+ # Also track existing chat session + message combinations
65
+ existing_sources = {
66
+ (insight.source_chat_session, insight.source_message_id)
67
+ for insight in self.sections[section_key].insights
68
+ if insight.source_chat_session and insight.source_message_id
69
+ }
70
+
71
+ new_insights = []
72
+ for insight in insights:
73
+ # Normalize content for comparison
74
+ normalized_content = insight.content.strip().lower()
75
+
76
+ # Check if this exact content already exists
77
+ if normalized_content in existing_insights_map:
78
+ logger.debug(f"Skipping duplicate insight: {insight.content[:50]}...")
79
+ continue
80
+
81
+ # Check if this source was already processed
82
+ if insight.source_chat_session and insight.source_message_id:
83
+ source_key = (insight.source_chat_session, insight.source_message_id)
84
+ if source_key in existing_sources:
85
+ logger.debug(f"Skipping already processed source: {source_key}")
86
+ continue
87
+
88
+ # This is genuinely new
89
+ new_insights.append(insight)
90
 
91
+ if new_insights:
92
+ logger.info(f"Adding {len(new_insights)} new insights to section '{section_key}'")
93
+ self.sections[section_key].insights.extend(new_insights)
94
+ self.sections[section_key].updated_at = datetime.utcnow()
95
+ self.last_updated = datetime.utcnow()
96
+ else:
97
+ logger.info(f"No new insights to add to section '{section_key}' (all {len(insights)} were duplicates)")
98
 
99
  # Update total insights count
100
  self.total_insights = sum(len(section.insights) for section in self.sections.values())