Spaces:
Running
Running
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 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
#
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 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 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
-
#
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
-
#
|
| 140 |
-
|
| 141 |
-
|
| 142 |
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
canvas.last_chat_processed = datetime.utcnow()
|
| 150 |
-
canvas.last_updated = datetime.utcnow()
|
| 151 |
|
| 152 |
-
|
| 153 |
-
await self._save_canvas(canvas)
|
| 154 |
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 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
|
| 268 |
try:
|
| 269 |
canvas = await self.get_or_create_canvas(user_id)
|
| 270 |
|
| 271 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 298 |
}
|
| 299 |
|
| 300 |
async def export_canvas_for_printing(self, user_id: str) -> Dict:
|
| 301 |
-
"""Export canvas in a
|
| 302 |
try:
|
| 303 |
canvas = await self.get_or_create_canvas(user_id)
|
| 304 |
|
| 305 |
-
#
|
| 306 |
-
|
| 307 |
for section_key, section in canvas.sections.items():
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 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":
|
| 341 |
"metadata": {
|
| 342 |
-
"canvas_id": str(canvas.id),
|
| 343 |
"created_at": canvas.created_at,
|
| 344 |
-
"
|
|
|
|
| 345 |
}
|
| 346 |
}
|
| 347 |
|
| 348 |
except Exception as e:
|
| 349 |
-
logger.error(f"Error exporting canvas for printing: {e}")
|
| 350 |
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
|
| 352 |
-
#
|
| 353 |
-
|
| 354 |
|
| 355 |
def get_canvas_manager() -> CanvasManager:
|
| 356 |
-
"""Get
|
| 357 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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())
|