cite-agent 1.0.4__py3-none-any.whl → 1.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cite-agent might be problematic. Click here for more details.

Files changed (48) hide show
  1. cite_agent/__init__.py +1 -1
  2. cite_agent/account_client.py +19 -46
  3. cite_agent/agent_backend_only.py +30 -4
  4. cite_agent/cli.py +397 -64
  5. cite_agent/cli_conversational.py +294 -0
  6. cite_agent/cli_workflow.py +276 -0
  7. cite_agent/enhanced_ai_agent.py +3222 -117
  8. cite_agent/session_manager.py +215 -0
  9. cite_agent/setup_config.py +5 -21
  10. cite_agent/streaming_ui.py +252 -0
  11. cite_agent/updater.py +50 -17
  12. cite_agent/workflow.py +427 -0
  13. cite_agent/workflow_integration.py +275 -0
  14. cite_agent-1.2.3.dist-info/METADATA +442 -0
  15. cite_agent-1.2.3.dist-info/RECORD +54 -0
  16. {cite_agent-1.0.4.dist-info → cite_agent-1.2.3.dist-info}/top_level.txt +1 -0
  17. src/__init__.py +1 -0
  18. src/services/__init__.py +132 -0
  19. src/services/auth_service/__init__.py +3 -0
  20. src/services/auth_service/auth_manager.py +33 -0
  21. src/services/graph/__init__.py +1 -0
  22. src/services/graph/knowledge_graph.py +194 -0
  23. src/services/llm_service/__init__.py +5 -0
  24. src/services/llm_service/llm_manager.py +495 -0
  25. src/services/paper_service/__init__.py +5 -0
  26. src/services/paper_service/openalex.py +231 -0
  27. src/services/performance_service/__init__.py +1 -0
  28. src/services/performance_service/rust_performance.py +395 -0
  29. src/services/research_service/__init__.py +23 -0
  30. src/services/research_service/chatbot.py +2056 -0
  31. src/services/research_service/citation_manager.py +436 -0
  32. src/services/research_service/context_manager.py +1441 -0
  33. src/services/research_service/conversation_manager.py +597 -0
  34. src/services/research_service/critical_paper_detector.py +577 -0
  35. src/services/research_service/enhanced_research.py +121 -0
  36. src/services/research_service/enhanced_synthesizer.py +375 -0
  37. src/services/research_service/query_generator.py +777 -0
  38. src/services/research_service/synthesizer.py +1273 -0
  39. src/services/search_service/__init__.py +5 -0
  40. src/services/search_service/indexer.py +186 -0
  41. src/services/search_service/search_engine.py +342 -0
  42. src/services/simple_enhanced_main.py +287 -0
  43. cite_agent/__distribution__.py +0 -7
  44. cite_agent-1.0.4.dist-info/METADATA +0 -234
  45. cite_agent-1.0.4.dist-info/RECORD +0 -23
  46. {cite_agent-1.0.4.dist-info → cite_agent-1.2.3.dist-info}/WHEEL +0 -0
  47. {cite_agent-1.0.4.dist-info → cite_agent-1.2.3.dist-info}/entry_points.txt +0 -0
  48. {cite_agent-1.0.4.dist-info → cite_agent-1.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,2056 @@
1
+ import asyncio
2
+ import logging
3
+ from typing import List, Dict, Optional
4
+ from datetime import datetime, timezone
5
+ import os
6
+ from src.services.llm_service.api_clients.llm_chat_client import LLMChatClient
7
+ from src.services.llm_service.api_clients.llm_doc_client import LLMDcClient
8
+ from src.services.llm_service.llm_manager import LLMManager
9
+ from src.services.research_service.context_manager import ResearchContextManager
10
+ from src.services.research_service.synthesizer import ResearchSynthesizer
11
+ from src.services.graph.knowledge_graph import KnowledgeGraph
12
+ from src.storage.db.operations import DatabaseOperations
13
+ from dotenv import load_dotenv
14
+ load_dotenv(".env.local")
15
+ import random
16
+ import re
17
+ import sys
18
+ import time
19
+
20
+ # Configure logging
21
+ logger = logging.getLogger(__name__)
22
+
23
+ class TypingEffect:
24
+ """Simulate human-like typing effect for chatbot responses."""
25
+
26
+ def __init__(self, speed=0.03, pause_chars=['.', '!', '?', ',', ';', ':']):
27
+ self.speed = speed
28
+ self.pause_chars = pause_chars
29
+
30
+ async def type_message(self, message: str, stream=True):
31
+ """Type out a message with realistic timing."""
32
+ if not stream:
33
+ return message
34
+
35
+ typed_message = ""
36
+ for char in message:
37
+ typed_message += char
38
+ print(char, end='', flush=True)
39
+
40
+ # Add longer pauses for punctuation
41
+ if char in self.pause_chars:
42
+ await asyncio.sleep(self.speed * 3)
43
+ else:
44
+ await asyncio.sleep(self.speed)
45
+
46
+ print() # New line at the end
47
+ return typed_message
48
+
49
+ def type_message_simple(self, message: str, stream=True):
50
+ """Simpler typing effect for when async isn't available."""
51
+ if not stream:
52
+ print(message)
53
+ return message
54
+
55
+ for char in message:
56
+ print(char, end='', flush=True)
57
+ time.sleep(self.speed)
58
+ print()
59
+ return message
60
+
61
+ class ConversationContext:
62
+ """Track conversation context for better responses."""
63
+
64
+ def __init__(self):
65
+ self.topics_discussed = []
66
+ self.user_interests = set()
67
+ self.conversation_style = "neutral"
68
+ self.depth_preference = "balanced"
69
+ self.questions_asked = []
70
+ self.research_readiness = 0.0
71
+
72
+ def update(self, user_message: str, bot_response: str):
73
+ """Update context based on conversation."""
74
+ # Extract and track topics
75
+ new_topics = self._extract_topics(user_message)
76
+ self.topics_discussed.extend(new_topics)
77
+
78
+ # Detect user's preferred style
79
+ if "?" in user_message:
80
+ self.questions_asked.append(user_message)
81
+
82
+ # Adjust conversation style based on patterns
83
+ if len(user_message.split()) > 50:
84
+ self.depth_preference = "detailed"
85
+ elif len(user_message.split()) < 10:
86
+ self.depth_preference = "concise"
87
+
88
+ # Update research readiness
89
+ self._update_research_readiness(user_message)
90
+
91
+ def get_style_guidelines(self) -> str:
92
+ """Get style guidelines based on context."""
93
+ guidelines = []
94
+
95
+ if self.depth_preference == "detailed":
96
+ guidelines.append("Provide thorough, detailed responses")
97
+ elif self.depth_preference == "concise":
98
+ guidelines.append("Keep responses focused and concise")
99
+
100
+ if len(self.questions_asked) > 3:
101
+ guidelines.append("User is curious - encourage exploration")
102
+
103
+ if self.research_readiness > 0.7:
104
+ guidelines.append("User seems ready for deeper research")
105
+
106
+ return "\n".join(guidelines)
107
+
108
+ def _extract_topics(self, user_message: str) -> list:
109
+ """Extract topics from user message."""
110
+ # Simple keyword extraction
111
+ words = user_message.lower().split()
112
+ # Filter for meaningful words (length > 4, not common words)
113
+ topics = [w for w in words if len(w) > 4 and w not in
114
+ ['about', 'would', 'could', 'should', 'there', 'where']]
115
+ return topics[:3] # Top 3 topics
116
+
117
+ def _update_research_readiness(self, user_message: str):
118
+ """Update research readiness score."""
119
+ # Increase readiness if research-related terms appear
120
+ research_indicators = ['research', 'study', 'papers', 'literature',
121
+ 'investigate', 'explore', 'deep dive']
122
+
123
+ if any(term in user_message.lower() for term in research_indicators):
124
+ self.research_readiness = min(self.research_readiness + 0.2, 1.0)
125
+
126
+ # Also increase based on conversation depth
127
+ if len(self.questions_asked) > 2:
128
+ self.research_readiness = min(self.research_readiness + 0.1, 1.0)
129
+
130
+ class ChatbotResearchSession:
131
+ """Full-featured CLI chatbot for research planning and execution with parallel web search and projection."""
132
+ def __init__(self, context_manager=None, synthesizer=None, db_ops=None, user_profile: Optional[Dict] = None):
133
+ # Initialize with fallback mode detection
134
+ self.fallback_mode = False # Force real mode
135
+ self.context_manager = context_manager
136
+ self.synthesizer = synthesizer
137
+ self.db_ops = db_ops
138
+
139
+ # Initialize typing effect
140
+ self.typing_effect = TypingEffect(speed=0.02) # Slightly faster for better UX
141
+
142
+ # Force real mode - don't check dependencies
143
+ logger.info("Forcing real research mode - using actual research capabilities")
144
+ print("🔬 Running in REAL research mode with academic database access")
145
+
146
+ self.history: List[Dict] = []
147
+ self.context: Dict = {}
148
+ self.user_profile = user_profile or {"name": "User"}
149
+ self.created_at = datetime.now(timezone.utc)
150
+ self.active = True
151
+ self.session_id = None
152
+ self.research_plan = None
153
+ self.status = None
154
+ self.synthesis = None
155
+ self.topic = None
156
+ self.questions = []
157
+ self.last_bot_message = ""
158
+
159
+ # Initialize clients for real research
160
+ try:
161
+ self.chat_client = LLMChatClient()
162
+ self.doc_client = LLMDcClient()
163
+ logger.info("LLM clients initialized successfully")
164
+ except Exception as e:
165
+ logger.error("LLM client initialization failed; real mode is required for launch", exc_info=True)
166
+ raise RuntimeError(
167
+ "LLM stack failed to initialize. Configure the required provider credentials "
168
+ "(e.g., CEREBRAS_API_KEY) and network access before launching the chatbot."
169
+ ) from e
170
+
171
+ # New attributes for parallel web search and projection
172
+ self.parallel_web_context = []
173
+ self.research_proposed = False
174
+ self.projection_given = False
175
+ self.research_approved = False
176
+ self.context_tracker = ConversationContext()
177
+
178
+ import random
179
+ self.random = random
180
+
181
+ def _is_research_query(self, message: str) -> bool:
182
+ """Detect if the message is a research query."""
183
+ message_lower = message.lower()
184
+
185
+ # Research keywords
186
+ research_keywords = [
187
+ 'research', 'study', 'find', 'search', 'explore', 'investigate',
188
+ 'analyze', 'examine', 'look into', 'find out about', 'what is',
189
+ 'how does', 'why does', 'latest', 'recent', 'developments',
190
+ 'advances', 'breakthroughs', 'innovations', 'technology',
191
+ 'papers', 'articles', 'studies', 'literature'
192
+ ]
193
+
194
+ # Check if message contains research keywords
195
+ has_research_keywords = any(keyword in message_lower for keyword in research_keywords)
196
+
197
+ # Check if message is long enough to be a research query
198
+ is_long_enough = len(message.split()) > 3
199
+
200
+ # Check if message contains a topic (not just a greeting)
201
+ is_not_greeting = not any(greeting in message_lower for greeting in ['hello', 'hi', 'hey', 'how are you'])
202
+
203
+ return has_research_keywords and is_long_enough and is_not_greeting
204
+
205
+ async def _perform_real_research(self, user_message: str) -> str:
206
+ """Perform real academic research using the enhanced research service."""
207
+ try:
208
+ print("🔍 Starting REAL comprehensive research...")
209
+
210
+ # Use the enhanced research service for deep analysis
211
+ from src.services.research_service.enhanced_research import enhanced_research_service
212
+
213
+ # Extract the actual research topic from the message
214
+ research_topic = self._extract_research_topic(user_message)
215
+
216
+ print(f"📝 Research Topic: {research_topic}")
217
+ print("⏳ Performing comprehensive research with analysis...")
218
+
219
+ # Perform comprehensive research
220
+ results = await enhanced_research_service.research_topic(
221
+ query=research_topic,
222
+ max_results=20 # Go deeper
223
+ )
224
+
225
+ if results and not results.get('error'):
226
+ # Format comprehensive results
227
+ response = self._format_comprehensive_research_response(results, research_topic)
228
+ print("✅ Comprehensive research completed!")
229
+ return response
230
+ else:
231
+ # Fallback to basic search if enhanced research fails
232
+ print("⚠️ Enhanced research failed, falling back to basic search...")
233
+ return await self._perform_basic_research(user_message)
234
+
235
+ except Exception as e:
236
+ print(f"❌ Error in comprehensive research: {e}")
237
+ return await self._perform_basic_research(user_message)
238
+
239
+ def _extract_research_topic(self, message: str) -> str:
240
+ """Extract the actual research topic from user message."""
241
+ # More intelligent topic extraction
242
+ topic = message.lower()
243
+
244
+ # Remove common research request words but preserve the core topic
245
+ research_words = [
246
+ 'research', 'study', 'analyze', 'analysis', 'comprehensive', 'detailed',
247
+ 'thorough', 'perform', 'conduct', 'find', 'latest', 'developments',
248
+ 'breakthroughs', 'papers', 'sources', 'go deep', 'deep', 'at least',
249
+ 'proper', 'citations', 'quality assessment', 'synthesis', 'i want',
250
+ 'detailed analysis of', 'content synthesis', 'and quality assessment'
251
+ ]
252
+
253
+ for word in research_words:
254
+ topic = topic.replace(word, '')
255
+
256
+ # Remove numbers that are likely not part of the topic
257
+ import re
258
+ topic = re.sub(r'\b\d+\b', '', topic) # Remove standalone numbers
259
+
260
+ # Clean up and return
261
+ topic = ' '.join(topic.split()) # Remove extra spaces
262
+ topic = topic.strip()
263
+
264
+ # If topic is too short, try to extract the main subject
265
+ if len(topic.split()) < 3:
266
+ # Look for key terms that indicate the actual topic
267
+ key_terms = ['quantum computing', 'artificial intelligence', 'machine learning',
268
+ 'renewable energy', 'climate change', 'healthcare', 'biotechnology',
269
+ 'nanotechnology', 'robotics', 'cybersecurity', 'blockchain']
270
+
271
+ for term in key_terms:
272
+ if term in message.lower():
273
+ return term
274
+
275
+ return topic if topic else "research topic"
276
+
277
+ def _format_comprehensive_research_response(self, results: dict, topic: str) -> str:
278
+ """Format comprehensive research results with analysis."""
279
+
280
+ response = f"🔬 **Comprehensive Research Analysis: {topic}**\n\n"
281
+
282
+ # Summary section
283
+ response += f"📊 **Research Summary:**\n"
284
+ response += f"• Sources Analyzed: {results.get('sources_analyzed', 0)}\n"
285
+ response += f"• Key Findings: {len(results.get('key_findings', []))}\n"
286
+ response += f"• Citations Generated: {len(results.get('citations', []))}\n"
287
+ response += f"• Visualizations: {len(results.get('visualizations', {}))}\n\n"
288
+
289
+ # Key findings
290
+ if results.get('key_findings'):
291
+ response += "🔍 **Key Findings:**\n"
292
+ for i, finding in enumerate(results['key_findings'][:10], 1):
293
+ response += f"{i}. {finding}\n"
294
+ response += "\n"
295
+
296
+ # Detailed analysis
297
+ if results.get('detailed_analysis'):
298
+ response += "📋 **Detailed Analysis:**\n"
299
+ response += f"{results['detailed_analysis'][:500]}...\n\n"
300
+
301
+ # Recommendations
302
+ if results.get('recommendations'):
303
+ response += "💡 **Recommendations:**\n"
304
+ for i, rec in enumerate(results['recommendations'][:5], 1):
305
+ response += f"{i}. {rec}\n"
306
+ response += "\n"
307
+
308
+ # Citations
309
+ if results.get('citations'):
310
+ response += "📚 **Top Sources (with Citations):**\n"
311
+ for i, citation in enumerate(results['citations'][:5], 1):
312
+ title = citation.get('title', 'No title')
313
+ authors = citation.get('authors', [])
314
+ doi = citation.get('doi', 'No DOI')
315
+
316
+ response += f"{i}. **{title}**\n"
317
+ if authors:
318
+ response += f" Authors: {', '.join(authors[:3])}\n"
319
+ response += f" DOI: {doi}\n\n"
320
+
321
+ # Citation formats
322
+ if results.get('citation_formats'):
323
+ response += "📖 **Citation Formats Available:**\n"
324
+ for format_name in results['citation_formats'].keys():
325
+ response += f"• {format_name.upper()}\n"
326
+ response += "\n"
327
+
328
+ response += "🎯 **This is REAL academic research with comprehensive analysis, not just a list of sources!**\n\n"
329
+ response += "Would you like me to:\n"
330
+ response += "• Generate a full research report\n"
331
+ response += "• Create interactive visualizations\n"
332
+ response += "• Export citations in specific formats\n"
333
+ response += "• Dive deeper into specific findings\n"
334
+
335
+ return response
336
+
337
+ async def _perform_basic_research(self, user_message: str) -> str:
338
+ """Fallback to basic research if enhanced research fails."""
339
+ try:
340
+ print("🔍 Performing basic research...")
341
+
342
+ # Import search engine
343
+ from src.services.search_service.search_engine import SearchEngine
344
+ from src.storage.db.operations import DatabaseOperations
345
+
346
+ # Initialize search engine
347
+ db_ops = DatabaseOperations(
348
+ os.environ.get('MONGODB_URL', 'mongodb://localhost:27017/nocturnal_archive'),
349
+ os.environ.get('REDIS_URL', 'redis://localhost:6379')
350
+ )
351
+ search_engine = SearchEngine(db_ops, os.environ.get('REDIS_URL', 'redis://localhost:6379'))
352
+
353
+ # Perform academic search
354
+ print("📚 Searching academic databases...")
355
+ academic_results = []
356
+
357
+ try:
358
+ from src.services.paper_service.openalex import OpenAlexClient
359
+ async with OpenAlexClient() as openalex:
360
+ academic_data = await openalex.search_works(user_message, per_page=10)
361
+ if academic_data and "results" in academic_data:
362
+ academic_results = academic_data["results"]
363
+ print(f"✅ Found {len(academic_results)} academic papers")
364
+ except Exception as e:
365
+ print(f"⚠️ Academic search failed: {e}")
366
+
367
+ # Perform web search
368
+ print("🌐 Searching web sources...")
369
+ web_results = await search_engine.web_search(user_message, num_results=5)
370
+ print(f"✅ Found {len(web_results)} web sources")
371
+
372
+ # Generate response with basic research results
373
+ response = self._format_research_response(user_message, academic_results, web_results)
374
+
375
+ print("✅ Basic research completed!")
376
+ return response
377
+
378
+ except Exception as e:
379
+ print(f"❌ Error in basic research: {e}")
380
+ return f"I encountered an error while researching '{user_message}'. Please try again."
381
+
382
+ def _format_research_response(self, query: str, academic_results: list, web_results: list) -> str:
383
+ """Format research results into a comprehensive response."""
384
+
385
+ response = f"🔬 **Research Results for: {query}**\n\n"
386
+
387
+ # Academic papers section
388
+ if academic_results:
389
+ response += "📚 **Academic Papers Found:**\n"
390
+ for i, paper in enumerate(academic_results[:5], 1):
391
+ title = paper.get('title', 'No title')
392
+ authors = paper.get('authorships', [])
393
+ author_names = [author.get('author', {}).get('display_name', 'Unknown') for author in authors[:3]]
394
+ doi = paper.get('doi', 'No DOI')
395
+
396
+ response += f"{i}. **{title}**\n"
397
+ response += f" Authors: {', '.join(author_names)}\n"
398
+ response += f" DOI: {doi}\n\n"
399
+
400
+ # Web sources section
401
+ if web_results:
402
+ response += "🌐 **Web Sources Found:**\n"
403
+ for i, result in enumerate(web_results[:3], 1):
404
+ title = result.get('title', 'No title')
405
+ url = result.get('url', 'No URL')
406
+ snippet = result.get('snippet', 'No description')
407
+
408
+ response += f"{i}. **{title}**\n"
409
+ response += f" URL: {url}\n"
410
+ response += f" {snippet[:150]}...\n\n"
411
+
412
+ # Summary
413
+ total_sources = len(academic_results) + len(web_results)
414
+ response += f"📊 **Summary:** Found {total_sources} sources total ({len(academic_results)} academic papers, {len(web_results)} web sources)\n\n"
415
+
416
+ response += "Would you like me to:\n"
417
+ response += "• Analyze specific papers in detail\n"
418
+ response += "• Generate citations for these sources\n"
419
+ response += "• Create a comprehensive research summary\n"
420
+ response += "• Explore related topics\n\n"
421
+
422
+ response += "Just let me know what aspect you'd like to dive deeper into!"
423
+
424
+ return response
425
+
426
+ async def chat_turn(self, user_message: str) -> str:
427
+ """Process user message with fallback support."""
428
+ self.history.append({"role": "user", "content": user_message})
429
+
430
+ # Update conversation context
431
+ if hasattr(self, 'context_tracker'):
432
+ self.context_tracker.update(user_message, self.last_bot_message)
433
+
434
+ try:
435
+ # Check if this is a research query
436
+ if self._is_research_query(user_message):
437
+ print("🔬 Detected research query - using real academic research...")
438
+ response = await self._perform_real_research(user_message)
439
+ elif self.fallback_mode:
440
+ response = await self._simulate_chat_response(user_message)
441
+ else:
442
+ response = await self._full_chat_response(user_message)
443
+
444
+ # Add typing effect to response
445
+ await self.typing_effect.type_message(response)
446
+
447
+ # Store the response
448
+ self.last_bot_message = response
449
+ self.history.append({"role": "assistant", "content": response})
450
+
451
+ return response
452
+
453
+ except Exception as e:
454
+ logger.error(f"Error in chat_turn: {str(e)}")
455
+ error_response = await self._handle_error_gracefully(e, user_message)
456
+ await self.typing_effect.type_message(error_response)
457
+ return error_response
458
+
459
+ async def _parallel_web_search(self, user_message: str):
460
+ """Do silent parallel web search for background context."""
461
+ try:
462
+ from src.services.search_service.search_engine import SearchEngine
463
+ search_engine = SearchEngine(self.db_ops, os.environ.get('REDIS_URL', 'redis://localhost:6379'))
464
+
465
+ # Extract key terms for search
466
+ search_terms = await self._extract_search_terms(user_message)
467
+
468
+ # Do quick web search in background
469
+ results = await search_engine.web_search(search_terms, num_results=3)
470
+
471
+ # Store context for later use
472
+ self.parallel_web_context.extend(results)
473
+
474
+ except Exception as e:
475
+ # Silently fail - parallel search shouldn't interrupt conversation
476
+ pass
477
+
478
+ async def _extract_search_terms(self, message: str) -> str:
479
+ """Extract search terms from user message."""
480
+ try:
481
+ # Use LLM to extract key search terms
482
+ prompt = f"Extract 2-3 key search terms from this message for web search. Return ONLY the search terms, no JSON or formatting: {message}"
483
+ result = await self.doc_client.process_document(
484
+ title="Search Terms Extraction",
485
+ content=prompt,
486
+ model="llama-3.3-70b",
487
+ temperature=0.1,
488
+ max_tokens=100
489
+ )
490
+ terms = result.get("raw_text", "").strip()
491
+ return terms if terms else message
492
+ except:
493
+ return message
494
+
495
+ async def _simulate_chat_response(self, user_message: str) -> str:
496
+ """Provide simulated responses when in fallback mode."""
497
+ message_lower = user_message.lower()
498
+
499
+ # Greeting responses
500
+ if any(word in message_lower for word in ['hello', 'hi', 'hey', 'start']):
501
+ return ("Hello! I'm the Nocturnal Archive research assistant. "
502
+ "I can help you with comprehensive research on any topic. "
503
+ "What would you like to research today?")
504
+
505
+ # Research topic responses
506
+ if any(word in message_lower for word in ['research', 'study', 'topic', 'explore']):
507
+ if len(message_lower.split()) > 3: # Likely contains a topic
508
+ topic = user_message.strip()
509
+ return (f"Great! I'd love to help you research '{topic}'. "
510
+ "In simulation mode, I can show you what the research process would look like. "
511
+ "Would you like me to demonstrate the research workflow for this topic?")
512
+ else:
513
+ return ("What topic would you like to research? "
514
+ "For example: 'quantum computing', 'AI in healthcare', or 'blockchain technology'")
515
+
516
+ # Research type responses
517
+ if any(word in message_lower for word in ['comprehensive', 'detailed', 'thorough']):
518
+ return ("Perfect! I'll conduct a comprehensive analysis. "
519
+ "This would include:\n"
520
+ "• 15-20 papers analyzed\n"
521
+ "• Quality assessment\n"
522
+ "• Citation network mapping\n"
523
+ "• Trend analysis\n"
524
+ "• Advanced visualizations\n"
525
+ "• Multiple export formats\n\n"
526
+ "Estimated time: 35 minutes\n\n"
527
+ "Would you like me to start the research simulation?")
528
+
529
+ if any(word in message_lower for word in ['quick', 'overview', 'summary']):
530
+ return ("Great! I'll provide a quick overview. "
531
+ "This would include:\n"
532
+ "• 5-8 key papers\n"
533
+ "• Executive summary\n"
534
+ "• Main findings\n"
535
+ "• Basic visualizations\n\n"
536
+ "Estimated time: 15 minutes\n\n"
537
+ "Would you like me to start the research simulation?")
538
+
539
+ # Confirmation responses
540
+ if any(word in message_lower for word in ['yes', 'start', 'go', 'begin']):
541
+ return ("🚀 Starting research simulation...\n\n"
542
+ "This is where the actual research would happen.\n"
543
+ "The system would:\n"
544
+ "1. Search academic databases\n"
545
+ "2. Analyze papers with AI\n"
546
+ "3. Generate insights and visualizations\n"
547
+ "4. Create professional reports\n\n"
548
+ "For now, this is a simulation. The full system requires:\n"
549
+ "• API keys for LLM services\n"
550
+ "• Database connections\n"
551
+ "• Web search capabilities\n\n"
552
+ "Would you like to see what the final output would look like?")
553
+
554
+ # Help responses
555
+ if any(word in message_lower for word in ['help', 'what can you do', 'capabilities']):
556
+ return ("I'm the Nocturnal Archive research assistant! Here's what I can do:\n\n"
557
+ "🔬 **Research Capabilities:**\n"
558
+ "• Comprehensive literature reviews\n"
559
+ "• Market analysis and trends\n"
560
+ "• Technology assessment\n"
561
+ "• Quality evaluation of sources\n\n"
562
+ "📊 **Output Formats:**\n"
563
+ "• Executive summaries\n"
564
+ "• Advanced visualizations\n"
565
+ "• Multiple export formats (JSON, Markdown, HTML, LaTeX, CSV)\n\n"
566
+ "⚡ **Speed:**\n"
567
+ "• Quick overviews (15 minutes)\n"
568
+ "• Comprehensive analysis (35 minutes)\n\n"
569
+ "What would you like to research?")
570
+
571
+ # Default response
572
+ return ("I'm here to help with research! "
573
+ "What topic would you like to explore? "
574
+ "Or ask me what I can do for you.")
575
+
576
+ async def _full_chat_response(self, user_message: str) -> str:
577
+ """Full chat response when all dependencies are available."""
578
+ try:
579
+ # Background web search (non-blocking)
580
+ asyncio.create_task(self._parallel_web_search(user_message))
581
+
582
+ # Handle ambiguous inputs first
583
+ if self._is_ambiguous(user_message):
584
+ return await self._handle_ambiguous_input(user_message)
585
+
586
+ # Natural flow decision tree
587
+ conversation_state = self._analyze_conversation_state()
588
+
589
+ if conversation_state == "warming_up":
590
+ return await self._warm_conversation(user_message)
591
+
592
+ elif conversation_state == "exploring":
593
+ if self._should_propose_research():
594
+ return await self._propose_research()
595
+ return await self._normal_conversation(user_message)
596
+
597
+ elif conversation_state == "ready_for_research":
598
+ if not self.research_proposed:
599
+ return await self._propose_research()
600
+ elif self._is_research_approved(user_message):
601
+ # Start research immediately (layered engine + saturation metrics)
602
+ return await self._start_research_with_preferences({"comprehensive": True})
603
+ elif self.projection_given and self._is_projection_approved(user_message):
604
+ return await self._start_research_with_preferences(self.research_preferences)
605
+ else:
606
+ return await self._handle_research_hesitation(user_message)
607
+
608
+ elif conversation_state == "researching":
609
+ return await self._handle_research_in_progress(user_message)
610
+
611
+ elif conversation_state == "discussing_results":
612
+ return await self._handle_followup_question(user_message)
613
+
614
+ else:
615
+ return await self._normal_conversation(user_message)
616
+
617
+ except Exception as e:
618
+ return await self._handle_error_gracefully(e, user_message)
619
+
620
+ async def _handle_error_gracefully(self, error: Exception, user_message: str) -> str:
621
+ """Handle errors gracefully with user-friendly messages."""
622
+ logger.error(f"Error in chat: {str(error)}")
623
+
624
+ # Convert technical errors to user-friendly messages
625
+ error_message = str(error).lower()
626
+
627
+ if "connection" in error_message or "timeout" in error_message:
628
+ return ("I'm having trouble connecting to the research databases right now. "
629
+ "This might be due to network issues or database configuration. "
630
+ "Would you like to try again, or would you prefer to see a demo of what I can do?")
631
+
632
+ elif "authentication" in error_message or "api key" in error_message:
633
+ return ("I need API keys to access the research services. "
634
+ "Please check your .env.local file and ensure your API keys are configured. "
635
+ "For now, I can show you what the research process would look like.")
636
+
637
+ elif "database" in error_message:
638
+ return ("I'm having trouble connecting to the database. "
639
+ "Please check your database configuration. "
640
+ "Would you like to try again or see a demo?")
641
+
642
+ else:
643
+ return ("I encountered an unexpected error while processing your request. "
644
+ "This might be a temporary issue. Would you like to try again, "
645
+ "or would you prefer to see what I can do in demo mode?")
646
+
647
+ def _detect_ambiguity_type(self, user_message: str) -> str:
648
+ """Detect type of ambiguity in user message."""
649
+ message_lower = user_message.lower()
650
+ words = message_lower.split()
651
+
652
+ if len(words) < 5 and any(broad in message_lower for broad in ['everything', 'all', 'anything']):
653
+ return "too_broad"
654
+ elif len(words) < 3 or '?' not in user_message:
655
+ return "unclear_intent"
656
+ elif any(connector in message_lower for connector in ['and also', 'but also', 'oh and']):
657
+ return "mixed_topics"
658
+ else:
659
+ return "general"
660
+
661
+ async def _clarify_intent_naturally(self, user_message: str) -> str:
662
+ """Clarify user's intent naturally."""
663
+ return (
664
+ "I want to make sure I understand what you're looking for. "
665
+ "Could you tell me a bit more about what aspect interests you most?"
666
+ )
667
+
668
+ async def _handle_mixed_topics(self, user_message: str) -> str:
669
+ """Handle messages with multiple topics."""
670
+ return (
671
+ "I see you've mentioned several interesting points! "
672
+ "Which one would you like to explore first? We can always come back to the others."
673
+ )
674
+
675
+ async def _engage_exploratively(self, user_message: str) -> str:
676
+ """Engage exploratively with ambiguous input."""
677
+ messages = [
678
+ {
679
+ "role": "system",
680
+ "content": "The user's message is somewhat ambiguous. Engage with curiosity and help them clarify their interests."
681
+ },
682
+ {"role": "user", "content": user_message}
683
+ ]
684
+
685
+ bot_response = await self.chat_client.chat(
686
+ messages=messages,
687
+ model="llama-3.3-70b", # This model is correct for Cerebras
688
+ temperature=0.7,
689
+ max_tokens=800
690
+ )
691
+ self.history.append({"role": "assistant", "content": bot_response})
692
+ return bot_response
693
+
694
+ async def _categorize_followup(self, user_message: str) -> str:
695
+ """Categorize the type of follow-up question."""
696
+ message_lower = user_message.lower()
697
+
698
+ if any(word in message_lower for word in ['clarify', 'explain', 'what do you mean']):
699
+ return "clarification"
700
+ elif any(word in message_lower for word in ['deeper', 'more detail', 'elaborate']):
701
+ return "deeper_dive"
702
+ elif any(word in message_lower for word in ['apply', 'implement', 'practice', 'use']):
703
+ return "practical_application"
704
+ elif any(word in message_lower for word in ['but', 'however', 'disagree']):
705
+ return "challenge"
706
+ else:
707
+ return "general"
708
+
709
+ async def _provide_clarification(self, user_message: str) -> str:
710
+ """Provide clarification on research results."""
711
+ prompt = f"""The user needs clarification about the research results.
712
+
713
+ User question: {user_message}
714
+ Research synthesis: {str(self.synthesis)[:1000]}
715
+
716
+ Provide a clear, helpful clarification that:
717
+ - Directly addresses their confusion
718
+ - Uses simpler language if needed
719
+ - Gives concrete examples
720
+ - Maintains a helpful tone"""
721
+
722
+ result = await self.doc_client.process_document(
723
+ title="Clarification",
724
+ content=prompt,
725
+ model="llama-3.3-70b",
726
+ temperature=0.5,
727
+ max_tokens=400
728
+ )
729
+
730
+ response = result.get("raw_text", "Let me clarify that point for you...")
731
+ self.history.append({"role": "assistant", "content": response})
732
+ return response
733
+
734
+ async def _provide_practical_insights(self, user_message: str) -> str:
735
+ """Provide practical applications of research findings."""
736
+ prompt = f"""The user wants practical applications of the research.
737
+
738
+ User question: {user_message}
739
+ Research synthesis: {str(self.synthesis)[:1000]}
740
+
741
+ Provide practical insights that:
742
+ - Connect research to real-world applications
743
+ - Give actionable recommendations
744
+ - Consider implementation challenges
745
+ - Remain grounded in the research"""
746
+
747
+ result = await self.doc_client.process_document(
748
+ title="Practical Insights",
749
+ content=prompt,
750
+ model="llama-3.3-70b",
751
+ temperature=0.6,
752
+ max_tokens=500
753
+ )
754
+
755
+ response = result.get("raw_text", "Here's how you might apply these findings...")
756
+ self.history.append({"role": "assistant", "content": response})
757
+ return response
758
+
759
+ async def _handle_challenge_gracefully(self, user_message: str) -> str:
760
+ """Handle challenges to research findings gracefully."""
761
+ response = (
762
+ "That's a valid point to raise. Research findings often have nuances and limitations. "
763
+ "Let me address your concern with what the research actually shows, including any "
764
+ "contradicting viewpoints or limitations in the current studies..."
765
+ )
766
+
767
+ # Add more specific response based on the challenge
768
+ self.history.append({"role": "assistant", "content": response})
769
+ return response
770
+
771
+ async def _provide_general_followup(self, user_message: str) -> str:
772
+ """Provide general follow-up response."""
773
+ prompt = f"""Answer this follow-up question about the research results.
774
+
775
+ Question: {user_message}
776
+ Research context: {str(self.synthesis)[:1000]}
777
+
778
+ Be helpful, thorough, and conversational."""
779
+
780
+ result = await self.doc_client.process_document(
781
+ title="Follow-up Response",
782
+ content=prompt,
783
+ model="llama-3.3-70b",
784
+ temperature=0.6,
785
+ max_tokens=400
786
+ )
787
+
788
+ response = result.get("raw_text", "Based on the research findings...")
789
+ self.history.append({"role": "assistant", "content": response})
790
+ return response
791
+
792
+ async def _extract_followup_aspect(self, user_message: str) -> str:
793
+ """Extract the specific aspect user wants to explore."""
794
+ # Simple extraction - could be enhanced
795
+ return user_message[:100] # Just use the message as the aspect
796
+
797
+ async def _analyze_conversation_depth(self) -> dict:
798
+ """Analyze conversation depth for proposal style."""
799
+ return {
800
+ 'depth': self._calculate_conversation_depth(),
801
+ 'engagement': self._measure_user_engagement(),
802
+ 'style': self.context_tracker.conversation_style if hasattr(self, 'context_tracker') else 'neutral'
803
+ }
804
+
805
+ def _determine_proposal_style(self, analysis: dict) -> str:
806
+ """Determine the best proposal style."""
807
+ if analysis['engagement'] > 0.8 and analysis['depth'] > 0.7:
808
+ return "enthusiastic"
809
+ elif analysis['depth'] > 0.6:
810
+ return "analytical"
811
+ else:
812
+ return "gentle"
813
+
814
+ async def _generate_analytical_proposal(self) -> str:
815
+ """Generate an analytical research proposal."""
816
+ topic = self.topic or self._extract_implicit_topic()
817
+
818
+ prompt = f"""Create an analytical research proposal for '{topic}' based on our conversation.
819
+
820
+ Context: {self.get_context_summary()[-500:]}
821
+
822
+ The proposal should:
823
+ - Acknowledge the complexity of the topic
824
+ - Outline specific research questions we could explore
825
+ - Mention methodological approaches
826
+ - Be conversational but intellectually rigorous
827
+ - Invite the user to refine or proceed"""
828
+
829
+ result = await self.doc_client.process_document(
830
+ title="Analytical Proposal",
831
+ content=prompt,
832
+ model="llama-3.3-70b",
833
+ temperature=0.6,
834
+ max_tokens=400
835
+ )
836
+
837
+ return result.get("raw_text", f"I see there are several interesting dimensions to {topic}. Would you like me to conduct a systematic research review?")
838
+
839
+ async def _generate_gentle_proposal(self) -> str:
840
+ """Generate a gentle research proposal."""
841
+ topic = self.topic or self._extract_implicit_topic()
842
+
843
+ return (f"I've noticed we keep coming back to {topic}, and there seem to be some interesting "
844
+ f"questions emerging. If you'd like, I could look into the academic research on this "
845
+ f"topic and see what insights are available. Would that be helpful?")
846
+
847
+ def _should_propose_research(self) -> bool:
848
+ """Check if we have enough context to propose research."""
849
+ # Don't propose if already proposed or research is running
850
+ if self.research_proposed or self.status in {"running", "completed"}:
851
+ return False
852
+
853
+ # Check if we have enough context - need at least 3 turns
854
+ if len(self.history) < 6:
855
+ return False
856
+
857
+ # Check if we have a clear topic
858
+ topic, questions = self._extract_topic_and_questions(self.get_context_summary())
859
+ if not topic or topic == "Untitled Research":
860
+ return False
861
+
862
+ # Check for research intent keywords in recent messages
863
+ recent_messages = [turn["content"].lower() for turn in self.history[-3:] if turn["role"] == "user"]
864
+ research_keywords = [
865
+ "research", "find papers", "literature review", "study", "review",
866
+ "investigate", "collect papers", "gather sources", "quantum", "cryptography"
867
+ ]
868
+ has_research_intent = any(any(kw in msg for kw in research_keywords) for msg in recent_messages)
869
+
870
+ if has_research_intent:
871
+ return True
872
+
873
+ return False
874
+
875
+ async def _propose_research(self) -> str:
876
+ """Propose research in a natural, conversational way."""
877
+ self.research_proposed = True
878
+
879
+ # Analyze conversation for natural entry point
880
+ conversation_analysis = await self._analyze_conversation_depth()
881
+
882
+ # Generate a natural, contextual proposal
883
+ proposal_style = self._determine_proposal_style(conversation_analysis)
884
+
885
+ if proposal_style == "enthusiastic":
886
+ proposal = await self._generate_enthusiastic_proposal()
887
+ elif proposal_style == "analytical":
888
+ proposal = await self._generate_analytical_proposal()
889
+ else:
890
+ proposal = await self._generate_gentle_proposal()
891
+
892
+ self.history.append({"role": "assistant", "content": proposal})
893
+ return proposal
894
+
895
+ async def _generate_enthusiastic_proposal(self) -> str:
896
+ """Generate an enthusiastic research proposal."""
897
+ topic = self.topic or self._extract_implicit_topic()
898
+
899
+ templates = [
900
+ f"This is fascinating! You know what? I think we're onto something really interesting with {topic}. "
901
+ f"I could dive deep into the academic literature and pull together a comprehensive analysis for you. "
902
+ f"There's likely some cutting-edge research we could explore - would you like me to start gathering papers and synthesizing the current state of knowledge?",
903
+
904
+ f"I'm getting really intrigued by our discussion about {topic}! "
905
+ f"I have access to academic databases and could conduct a thorough literature review to map out "
906
+ f"the research landscape. We could uncover some fascinating insights - shall I start that research process?",
907
+
908
+ f"You've touched on something that deserves deeper exploration! {topic} has so many dimensions "
909
+ f"we could investigate through academic research. I could search for peer-reviewed papers, "
910
+ f"analyze methodologies, and synthesize findings. Want me to put together a comprehensive research review?"
911
+ ]
912
+
913
+ # Use LLM to make it more natural based on context
914
+ return await self._personalize_template(random.choice(templates))
915
+
916
+ def _should_propose_research(self) -> bool:
917
+ """Smarter detection of when to propose research."""
918
+ if self.research_proposed or self.status in {"running", "completed"}:
919
+ return False
920
+
921
+ # Check conversation depth and complexity
922
+ depth_score = self._calculate_conversation_depth()
923
+
924
+ # Check for research indicators beyond keywords
925
+ indicators = {
926
+ 'question_complexity': self._assess_question_complexity(),
927
+ 'topic_persistence': self._check_topic_persistence(),
928
+ 'knowledge_gaps': self._identify_knowledge_gaps(),
929
+ 'user_engagement': self._measure_user_engagement(),
930
+ 'conversation_maturity': len(self.history) > 6
931
+ }
932
+
933
+ # Weighted scoring
934
+ score = (
935
+ indicators['question_complexity'] * 0.3 +
936
+ indicators['topic_persistence'] * 0.25 +
937
+ indicators['knowledge_gaps'] * 0.2 +
938
+ indicators['user_engagement'] * 0.15 +
939
+ (1.0 if indicators['conversation_maturity'] else 0) * 0.1
940
+ )
941
+
942
+ return score > 0.4
943
+
944
+ def _assess_question_complexity(self) -> float:
945
+ """Assess the complexity of user questions."""
946
+ recent_questions = [
947
+ turn["content"] for turn in self.history[-6:]
948
+ if turn["role"] == "user"
949
+ ]
950
+
951
+ complexity_indicators = [
952
+ "how", "why", "explain", "compare", "analyze",
953
+ "implications", "trade-offs", "challenges", "future",
954
+ "state of the art", "research", "studies", "evidence"
955
+ ]
956
+
957
+ complexity_score = 0
958
+ for question in recent_questions:
959
+ question_lower = question.lower()
960
+ score = sum(1 for indicator in complexity_indicators if indicator in question_lower)
961
+ complexity_score += min(score / 3, 1.0) # Normalize per question
962
+
963
+ return complexity_score / max(len(recent_questions), 1)
964
+
965
+ def _is_research_approved(self, user_message: str) -> bool:
966
+ """Check if user approved the research proposal."""
967
+ message_lower = user_message.lower()
968
+
969
+ # Don't approve if we're already past this stage
970
+ if self.projection_given or self.status == "running":
971
+ return False
972
+
973
+ approval_keywords = ["yes", "okay", "go ahead", "sure", "start", "sounds good"]
974
+ return any(keyword in message_lower for keyword in approval_keywords)
975
+
976
+ async def _give_projection(self) -> str:
977
+ """Give projection based on gathered context."""
978
+ self.projection_given = True
979
+
980
+ # Create projection based on context and parallel web searches
981
+ projection = await self._create_projection()
982
+
983
+ projection_message = (
984
+ f"Here's what I'm probably gonna find when I dig into the research:\n\n"
985
+ f"{projection}\n\n"
986
+ f"Let me verify this with the actual research. Should I proceed?"
987
+ )
988
+
989
+ self.history.append({"role": "assistant", "content": projection_message})
990
+ return projection_message
991
+
992
+ async def _create_projection(self) -> str:
993
+ """Create sophisticated projection with academic depth."""
994
+ try:
995
+ # Combine conversation context and web search results
996
+ context_summary = self.get_context_summary()
997
+ web_context = "\n".join([f"- {r.get('title', '')}: {r.get('snippet', '')}"
998
+ for r in self.parallel_web_context[:5]])
999
+
1000
+ prompt = (
1001
+ f"Based on this conversation context and recent web information, "
1002
+ f"provide a sophisticated academic projection of research findings:\n\n"
1003
+ f"Conversation: {context_summary}\n\n"
1004
+ f"Recent web context: {web_context}\n\n"
1005
+ f"Create a projection that includes:\n"
1006
+ f"- Expected key findings and insights\n"
1007
+ f"- Potential research gaps that might be identified\n"
1008
+ f"- Current trends in the field\n"
1009
+ f"- Methodological considerations\n"
1010
+ f"- Potential limitations or challenges\n"
1011
+ f"- Academic rigor and depth\n\n"
1012
+ f"Project what the comprehensive academic research will likely reveal about this topic."
1013
+ )
1014
+
1015
+ result = await self.doc_client.process_document(
1016
+ title="Research Projection",
1017
+ content=prompt,
1018
+ model="llama-3.3-70b",
1019
+ temperature=0.3,
1020
+ max_tokens=500
1021
+ )
1022
+
1023
+ projection = result.get("raw_text", "")
1024
+ if not projection:
1025
+ projection = (
1026
+ "Based on the research context and current literature trends, I expect to find:\n\n"
1027
+ "**Key Findings**: [Expected insights from academic papers]\n"
1028
+ "**Research Gaps**: [Areas where current literature is insufficient]\n"
1029
+ "**Methodological Insights**: [Different research approaches and their effectiveness]\n"
1030
+ "**Current Trends**: [Recent developments in the field]\n"
1031
+ "**Limitations**: [Potential challenges in current research]\n\n"
1032
+ "This projection is based on the current state of academic knowledge and will be verified through comprehensive research."
1033
+ )
1034
+
1035
+ return projection
1036
+
1037
+ except Exception as e:
1038
+ return (
1039
+ "Based on the academic context gathered, I expect to find relevant research papers, "
1040
+ "methodological insights, and potential research gaps. The comprehensive analysis will "
1041
+ "provide a thorough understanding of the current state of knowledge in this field."
1042
+ )
1043
+
1044
+ def _is_projection_approved(self, user_message: str) -> bool:
1045
+ """Check if user approved the projection."""
1046
+ approval_keywords = ["yes", "okay", "proceed", "go ahead", "sure", "verify", "check"]
1047
+ return any(keyword in user_message.lower() for keyword in approval_keywords)
1048
+
1049
+ async def _start_research(self) -> str:
1050
+ """Start the actual research process (layered engine with saturation metrics)."""
1051
+ self.research_approved = True
1052
+ self.status = "running"
1053
+
1054
+ # Build research plan if not already built
1055
+ if not self.research_plan:
1056
+ self.research_plan = await self.build_research_plan()
1057
+
1058
+ # Launch research using layered engine
1059
+ await self._approve_and_launch_layered()
1060
+
1061
+ start_message = (
1062
+ f"Starting research now. This will take a moment as I search for academic papers "
1063
+ f"and analyze the findings. I'll let you know when it's complete."
1064
+ )
1065
+
1066
+ self.history.append({"role": "assistant", "content": start_message})
1067
+
1068
+ # Don't wait here - let the research run in background
1069
+ # The user will check status or the bot will check in _handle_research_in_progress
1070
+
1071
+ return start_message
1072
+
1073
+ async def _approve_and_launch_layered(self):
1074
+ plan_str = self.research_plan if self.research_plan is not None else ""
1075
+ topic, questions = self._extract_topic_and_questions(plan_str)
1076
+ self.topic = topic
1077
+ self.questions = questions
1078
+ # Start layered research (multi-source + saturation)
1079
+ self.session_id = await self.context_manager.start_layered_research(
1080
+ topic=topic,
1081
+ research_questions=questions or [topic],
1082
+ max_layers=3,
1083
+ user_id=self.user_profile.get("id", "default_user")
1084
+ )
1085
+ self.status = "running"
1086
+
1087
+ async def check_status(self):
1088
+ if not self.session_id:
1089
+ print("No research session running.")
1090
+ return
1091
+ status = await self.context_manager.get_session_status(self.session_id)
1092
+ print(f"\n[Session Status: {status.get('status')}] Progress: {status.get('progress', {}).get('percentage', 0)}%")
1093
+ if status.get('status') == 'completed':
1094
+ self.status = 'completed'
1095
+ return status
1096
+
1097
+ async def show_results(self):
1098
+ if not self.session_id:
1099
+ return "No research session found."
1100
+ session = await self.context_manager._get_session(self.session_id)
1101
+ synthesis = session.synthesis if session else None
1102
+ if synthesis:
1103
+ self.synthesis = synthesis
1104
+ # Prepare artifact links if available
1105
+ artifact_links = ""
1106
+ try:
1107
+ artifacts = synthesis.get('artifacts') if isinstance(synthesis, dict) else None
1108
+ if artifacts and artifacts.get('report_markdown') and artifacts.get('report_json'):
1109
+ artifact_links = f"\n\nReports: {artifacts['report_markdown']} | {artifacts['report_json']}"
1110
+ except Exception:
1111
+ pass
1112
+ # Compose message
1113
+ syn_str = str(synthesis)
1114
+ results_message = (
1115
+ f"Research complete! Here are the findings:\n\n"
1116
+ f"{syn_str[:2000]}{'...' if len(syn_str) > 2000 else ''}"
1117
+ f"{artifact_links}\n\n"
1118
+ f"You can ask me follow-up questions about the results, or start a new research topic."
1119
+ )
1120
+
1121
+ self.history.append({"role": "assistant", "content": results_message})
1122
+ return results_message
1123
+ else:
1124
+ return "Research is still in progress. Please wait a moment."
1125
+
1126
+ def _is_followup_question(self, user_message: str) -> bool:
1127
+ """Check if user is asking a follow-up question about results."""
1128
+ if not self.synthesis:
1129
+ return False
1130
+
1131
+ # Check for question indicators
1132
+ question_indicators = ["?", "explain", "clarify", "what about", "how", "why", "tell me more", "challenges", "implement"]
1133
+ return any(indicator in user_message.lower() for indicator in question_indicators)
1134
+
1135
+ async def _handle_followup_question(self, user_message: str) -> str:
1136
+ """Handle follow-up questions with ChatGPT-like depth and personality."""
1137
+
1138
+ # Categorize the follow-up type
1139
+ followup_type = await self._categorize_followup(user_message)
1140
+
1141
+ if followup_type == "clarification":
1142
+ return await self._provide_clarification(user_message)
1143
+ elif followup_type == "deeper_dive":
1144
+ return await self._provide_deeper_analysis(user_message)
1145
+ elif followup_type == "practical_application":
1146
+ return await self._provide_practical_insights(user_message)
1147
+ elif followup_type == "challenge":
1148
+ return await self._handle_challenge_gracefully(user_message)
1149
+ else:
1150
+ return await self._provide_general_followup(user_message)
1151
+
1152
+ async def _handle_ambiguous_input(self, user_message: str) -> str:
1153
+ """Handle ambiguous or unclear inputs gracefully."""
1154
+
1155
+ ambiguity_type = self._detect_ambiguity_type(user_message)
1156
+
1157
+ if ambiguity_type == "too_broad":
1158
+ return await self._narrow_down_gracefully(user_message)
1159
+ elif ambiguity_type == "unclear_intent":
1160
+ return await self._clarify_intent_naturally(user_message)
1161
+ elif ambiguity_type == "mixed_topics":
1162
+ return await self._handle_mixed_topics(user_message)
1163
+ else:
1164
+ return await self._engage_exploratively(user_message)
1165
+
1166
+ async def _narrow_down_gracefully(self, user_message: str) -> str:
1167
+ """Help user narrow down broad topics naturally."""
1168
+
1169
+ prompt = f"""The user has asked a very broad question. Help them narrow it down conversationally.
1170
+
1171
+ User message: {user_message}
1172
+ Conversation context: {self.get_context_summary()[-500:]}
1173
+
1174
+ Create a response that:
1175
+ 1. Acknowledges the breadth of their interest
1176
+ 2. Offers 2-3 specific directions they might explore
1177
+ 3. Asks an engaging question to help focus
1178
+ 4. Maintains enthusiasm and curiosity
1179
+ 5. Feels like a natural conversation, not an interrogation"""
1180
+
1181
+ result = await self.doc_client.process_document(
1182
+ title="Narrowing Assistance",
1183
+ content=prompt,
1184
+ model="llama-3.3-70b",
1185
+ temperature=0.8,
1186
+ max_tokens=400
1187
+ )
1188
+
1189
+ return result.get("raw_text", "That's a fascinating area! Could you tell me what aspect interests you most?")
1190
+
1191
+ def _add_personality_touches(self, response: str, style: str) -> str:
1192
+ """Add personality touches to responses."""
1193
+
1194
+ if style == "analytical":
1195
+ # Add analytical personality markers
1196
+ connectors = [
1197
+ "Actually, this connects to an interesting point...",
1198
+ "What's particularly fascinating here is...",
1199
+ "This reminds me of a key insight from the research...",
1200
+ "There's a subtle but important distinction here..."
1201
+ ]
1202
+ if not any(conn in response for conn in connectors):
1203
+ response = f"{random.choice(connectors)} {response}"
1204
+
1205
+ elif style == "enthusiastic":
1206
+ # Add enthusiasm markers
1207
+ if "!" not in response[:50]: # Add excitement if missing
1208
+ sentences = response.split(". ")
1209
+ if len(sentences) > 1:
1210
+ sentences[0] += "!"
1211
+ response = ". ".join(sentences)
1212
+
1213
+ elif style == "thoughtful":
1214
+ # Add thoughtful pauses and considerations
1215
+ thoughtful_phrases = [
1216
+ "Hmm, ",
1217
+ "You know, ",
1218
+ "That's a great question - ",
1219
+ "I've been thinking about this... "
1220
+ ]
1221
+ if not any(phrase in response[:30] for phrase in thoughtful_phrases):
1222
+ response = f"{random.choice(thoughtful_phrases)}{response}"
1223
+
1224
+ return response
1225
+
1226
+ async def _provide_deeper_analysis(self, user_message: str) -> str:
1227
+ """Provide deeper analysis with personality."""
1228
+
1229
+ # Extract the specific aspect they want to dive into
1230
+ aspect = await self._extract_followup_aspect(user_message)
1231
+
1232
+ prompt = f"""Based on the research synthesis, provide a deeper, more nuanced analysis of the user's question.
1233
+
1234
+ Research synthesis: {str(self.synthesis)}
1235
+ User's follow-up: {user_message}
1236
+ Specific aspect: {aspect}
1237
+
1238
+ Guidelines:
1239
+ - Start with an engaging hook that shows you understand their curiosity
1240
+ - Provide rich, detailed analysis with examples
1241
+ - Use analogies or metaphors where helpful
1242
+ - Connect to broader implications
1243
+ - Maintain conversational tone while being thorough
1244
+ - End with a thought-provoking insight or question"""
1245
+
1246
+ result = await self.doc_client.process_document(
1247
+ title="Deep Dive Analysis",
1248
+ content=prompt,
1249
+ model="llama-3.3-70b",
1250
+ temperature=0.7,
1251
+ max_tokens=800
1252
+ )
1253
+
1254
+ response = result.get("raw_text", "")
1255
+
1256
+ # Add personality touches
1257
+ response = self._add_personality_touches(response, "analytical")
1258
+
1259
+ self.history.append({"role": "assistant", "content": response})
1260
+ return response
1261
+
1262
+ async def _normal_conversation(self, user_message: str) -> str:
1263
+ """Handle normal conversation flow with ChatGPT/Claude-style interaction."""
1264
+
1265
+ # Build a rich context for the LLM
1266
+ system_prompt = self._build_dynamic_system_prompt()
1267
+
1268
+ # Prepare conversation history with context awareness
1269
+ messages = [{"role": "system", "content": system_prompt}]
1270
+
1271
+ # Add conversation history with smart truncation
1272
+ messages.extend(self._prepare_conversation_history())
1273
+
1274
+ # Inject subtle context from parallel searches
1275
+ if self.parallel_web_context:
1276
+ messages.append({
1277
+ "role": "system",
1278
+ "content": f"[Background knowledge from recent searches: {self._summarize_web_context()}]"
1279
+ })
1280
+
1281
+ try:
1282
+ bot_response = await self.chat_client.chat(
1283
+ messages=messages,
1284
+ model="llama-3.3-70b", # This model is correct for Cerebras
1285
+ temperature=0.7,
1286
+ max_tokens=800
1287
+ )
1288
+ # Post-process for natural flow
1289
+ bot_response = self._enhance_response_naturally(bot_response, user_message)
1290
+
1291
+ except Exception as e:
1292
+ bot_response = self._generate_fallback_response(user_message)
1293
+
1294
+ self.history.append({"role": "assistant", "content": bot_response})
1295
+ return bot_response
1296
+
1297
+ def _build_dynamic_system_prompt(self) -> str:
1298
+ """Build a dynamic system prompt based on conversation state."""
1299
+
1300
+ base_prompt = """You are an advanced AI research assistant with a warm, intellectually curious personality.
1301
+ You engage naturally in conversations, showing genuine interest in topics while maintaining academic rigor.
1302
+
1303
+ Key traits:
1304
+ - Intellectually curious and enthusiastic about learning
1305
+ - Naturally conversational while being precise when needed
1306
+ - Proactively helpful without being pushy
1307
+ - Subtly guide conversations toward productive research when appropriate
1308
+ - Use natural transitions and conversational bridges
1309
+ - Show personality through word choice and engagement style
1310
+
1311
+ Current capabilities include:
1312
+ - Deep academic research and literature analysis
1313
+ - Real-time web search integration
1314
+ - Comprehensive paper synthesis
1315
+ - Methodological guidance
1316
+ - Critical analysis"""
1317
+
1318
+ # Add state-aware context
1319
+ if len(self.history) > 4:
1320
+ base_prompt += "\n\n[Note: The conversation is developing depth. Consider whether research might be valuable soon.]"
1321
+
1322
+ if self.parallel_web_context:
1323
+ base_prompt += "\n\n[You have access to recent web search context. Use it naturally when relevant.]"
1324
+
1325
+ if self._has_research_indicators():
1326
+ base_prompt += "\n\n[The user seems interested in deeper exploration. Be ready to suggest research naturally.]"
1327
+
1328
+ return base_prompt
1329
+
1330
+ async def _handle_clarification_request(self, user_message: str) -> str:
1331
+ """Handle requests for clarification with comprehensive academic guidance."""
1332
+ try:
1333
+ prompt = (
1334
+ f"Based on the conversation history, provide a comprehensive explanation of how I can help with academic research.\n\n"
1335
+ f"Conversation context: {self.get_context_summary()}\n\n"
1336
+ f"User is asking for clarification: {user_message}\n\n"
1337
+ f"Provide a detailed explanation of my capabilities for academic research assistance, "
1338
+ f"including literature review, methodology guidance, critical analysis, and research synthesis."
1339
+ )
1340
+
1341
+ result = await self.doc_client.process_document(
1342
+ title="Clarification Response",
1343
+ content=prompt,
1344
+ model="llama-3.3-70b",
1345
+ temperature=0.3,
1346
+ max_tokens=400
1347
+ )
1348
+
1349
+ response = result.get("raw_text", "")
1350
+ if not response:
1351
+ response = (
1352
+ "I can help you with comprehensive academic research in several ways:\n\n"
1353
+ "**Literature Review & Synthesis**: I can search academic databases, analyze papers, "
1354
+ "and synthesize findings across multiple sources.\n\n"
1355
+ "**Research Methodology**: I can help you choose appropriate research methods, "
1356
+ "design studies, and identify research gaps.\n\n"
1357
+ "**Critical Analysis**: I can provide deep analysis of academic papers, "
1358
+ "identify strengths/weaknesses, and suggest improvements.\n\n"
1359
+ "**Citation Management**: I can help organize sources, format citations, "
1360
+ "and ensure proper academic referencing.\n\n"
1361
+ "**Writing Assistance**: I can help structure arguments, improve clarity, "
1362
+ "and enhance academic writing.\n\n"
1363
+ "What specific aspect of your research would you like to focus on?"
1364
+ )
1365
+
1366
+ self.history.append({"role": "assistant", "content": response})
1367
+ return response
1368
+
1369
+ except Exception as e:
1370
+ return "I can help with comprehensive academic research including literature reviews, methodology guidance, and critical analysis. What specific aspect would you like to explore?"
1371
+
1372
+ def _extract_implicit_topic(self) -> str:
1373
+ """Extract topic from conversation if not explicitly set."""
1374
+
1375
+ # Look for noun phrases in recent messages
1376
+ user_messages = [
1377
+ turn["content"] for turn in self.history[-6:]
1378
+ if turn["role"] == "user"
1379
+ ]
1380
+
1381
+ # Simple extraction - find the most discussed concept
1382
+ all_text = " ".join(user_messages).lower()
1383
+
1384
+ # Common research topics (extend this list based on your domain)
1385
+ topic_keywords = [
1386
+ "quantum", "cryptography", "security", "algorithm", "computing",
1387
+ "research", "technology", "science", "study", "analysis"
1388
+ ]
1389
+
1390
+ for keyword in topic_keywords:
1391
+ if keyword in all_text:
1392
+ # Find surrounding context
1393
+ index = all_text.find(keyword)
1394
+ start = max(0, index - 20)
1395
+ end = min(len(all_text), index + 30)
1396
+ context = all_text[start:end].strip()
1397
+ return context
1398
+
1399
+ # Default to first substantial user message
1400
+ for msg in user_messages:
1401
+ if len(msg) > 20:
1402
+ return msg[:50] + "..."
1403
+
1404
+ return "the topic we've been discussing"
1405
+
1406
+ async def _personalize_template(self, template: str) -> str:
1407
+ """Personalize template based on conversation context."""
1408
+
1409
+ # Add conversation-specific details
1410
+ recent_context = self.get_context_summary()[-500:]
1411
+
1412
+ prompt = f"""Personalize this research proposal template based on our conversation:
1413
+
1414
+ Template: {template}
1415
+
1416
+ Recent conversation: {recent_context}
1417
+
1418
+ Make it feel natural and specific to what we've been discussing. Keep the same enthusiasm but make it more personal."""
1419
+
1420
+ result = await self.doc_client.process_document(
1421
+ title="Personalize Proposal",
1422
+ content=prompt,
1423
+ model="llama-3.3-70b",
1424
+ temperature=0.7,
1425
+ max_tokens=300
1426
+ )
1427
+
1428
+ return result.get("raw_text", template)
1429
+
1430
+ def _calculate_conversation_depth(self) -> float:
1431
+ """Calculate how deep/complex the conversation has become."""
1432
+
1433
+ depth_score = 0.0
1434
+
1435
+ # Factor 1: Message length
1436
+ avg_length = sum(len(turn["content"]) for turn in self.history) / max(len(self.history), 1)
1437
+ depth_score += min(avg_length / 200, 1.0) * 0.3
1438
+
1439
+ # Factor 2: Question complexity
1440
+ depth_score += self._assess_question_complexity() * 0.4
1441
+
1442
+ # Factor 3: Topic persistence
1443
+ depth_score += self._check_topic_persistence() * 0.3
1444
+
1445
+ return min(depth_score, 1.0)
1446
+
1447
+ def _check_topic_persistence(self) -> float:
1448
+ """Check if user is sticking to a topic."""
1449
+
1450
+ if len(self.history) < 4:
1451
+ return 0.0
1452
+
1453
+ # Extract key terms from recent messages
1454
+ recent_messages = [
1455
+ turn["content"].lower() for turn in self.history[-6:]
1456
+ if turn["role"] == "user"
1457
+ ]
1458
+
1459
+ # Find common words (simple approach)
1460
+ word_counts = {}
1461
+ for msg in recent_messages:
1462
+ words = msg.split()
1463
+ for word in words:
1464
+ if len(word) > 4: # Skip small words
1465
+ word_counts[word] = word_counts.get(word, 0) + 1
1466
+
1467
+ # If any substantial word appears multiple times, topic is persistent
1468
+ max_count = max(word_counts.values()) if word_counts else 0
1469
+ return min(max_count / 3, 1.0)
1470
+
1471
+ def _identify_knowledge_gaps(self) -> float:
1472
+ """Identify if there are knowledge gaps to fill."""
1473
+
1474
+ # Look for uncertainty markers
1475
+ gap_indicators = [
1476
+ "i don't know", "not sure", "wondering", "curious",
1477
+ "how does", "why does", "what causes", "can you explain"
1478
+ ]
1479
+
1480
+ recent_messages = " ".join([
1481
+ turn["content"].lower() for turn in self.history[-4:]
1482
+ if turn["role"] == "user"
1483
+ ])
1484
+
1485
+ gap_count = sum(1 for indicator in gap_indicators if indicator in recent_messages)
1486
+ return min(gap_count / 3, 1.0)
1487
+
1488
+ def _measure_user_engagement(self) -> float:
1489
+ """Measure how engaged the user is."""
1490
+
1491
+ if len(self.history) < 2:
1492
+ return 0.5
1493
+
1494
+ # Check response length trend
1495
+ user_messages = [
1496
+ turn["content"] for turn in self.history
1497
+ if turn["role"] == "user"
1498
+ ]
1499
+
1500
+ if len(user_messages) < 2:
1501
+ return 0.5
1502
+
1503
+ # Are messages getting longer? (sign of engagement)
1504
+ recent_avg = sum(len(msg) for msg in user_messages[-3:]) / 3
1505
+ early_avg = sum(len(msg) for msg in user_messages[:3]) / 3
1506
+
1507
+ if recent_avg > early_avg * 1.5:
1508
+ return 1.0
1509
+ elif recent_avg > early_avg:
1510
+ return 0.7
1511
+ else:
1512
+ return 0.4
1513
+
1514
+ async def _warm_conversation(self, user_message: str) -> str:
1515
+ """Handle early conversation warmly and naturally."""
1516
+
1517
+ # Simple, warm system prompt
1518
+ messages = [
1519
+ {
1520
+ "role": "system",
1521
+ "content": (
1522
+ "You are a friendly, intellectually curious AI assistant. "
1523
+ "This is early in the conversation, so be welcoming and engaging. "
1524
+ "Show interest in what the user is saying and ask natural follow-up questions. "
1525
+ "Be conversational, not formal."
1526
+ )
1527
+ },
1528
+ *self.history
1529
+ ]
1530
+
1531
+ bot_response = await self.chat_client.chat(
1532
+ messages=messages,
1533
+ model="llama-3.3-70b", # This model is correct for Cerebras
1534
+ temperature=0.7,
1535
+ max_tokens=800
1536
+ )
1537
+ self.history.append({"role": "assistant", "content": bot_response})
1538
+ return bot_response
1539
+
1540
+ async def _handle_research_hesitation(self, user_message: str) -> str:
1541
+ """Handle when user seems hesitant about research."""
1542
+
1543
+ # Understand their concern
1544
+ prompt = f"""The user seems hesitant about starting research. Their message: "{user_message}"
1545
+
1546
+ Provide a helpful, understanding response that:
1547
+ 1. Acknowledges their hesitation
1548
+ 2. Offers to clarify or adjust the research scope
1549
+ 3. Gives them control over the process
1550
+ 4. Remains friendly and supportive
1551
+
1552
+ Keep it conversational and brief."""
1553
+
1554
+ result = await self.doc_client.process_document(
1555
+ title="Hesitation Response",
1556
+ content=prompt,
1557
+ model="llama-3.3-70b",
1558
+ temperature=0.7,
1559
+ max_tokens=200
1560
+ )
1561
+
1562
+ response = result.get("raw_text", "No problem! Would you like to explore the topic more first, or should we adjust what we're looking for?")
1563
+
1564
+ self.history.append({"role": "assistant", "content": response})
1565
+ return response
1566
+
1567
+ async def _handle_research_in_progress(self, user_message: str) -> str:
1568
+ """Handle conversation while research is running."""
1569
+
1570
+ # Check actual research status
1571
+ if self.session_id:
1572
+ status = await self.context_manager.get_session_status(self.session_id)
1573
+ progress = status.get('progress', {}).get('percentage', 0)
1574
+
1575
+ if status.get('status') == 'completed':
1576
+ self.status = 'completed'
1577
+ return await self.show_results()
1578
+
1579
+ # Friendly progress update
1580
+ messages = [
1581
+ {
1582
+ "role": "system",
1583
+ "content": "Research is currently running. Be helpful and conversational while they wait."
1584
+ },
1585
+ {"role": "user", "content": user_message}
1586
+ ]
1587
+
1588
+ bot_response = await self.chat_client.chat(
1589
+ messages=messages,
1590
+ model="llama-3.3-70b", # This model is correct for Cerebras
1591
+ temperature=0.7,
1592
+ max_tokens=800
1593
+ )
1594
+ # Add progress info if available
1595
+ if 'progress' in locals():
1596
+ bot_response += f"\n\n(Research is {progress}% complete)"
1597
+
1598
+ self.history.append({"role": "assistant", "content": bot_response})
1599
+ return bot_response
1600
+
1601
+ def _enhance_response_naturally(self, response: str, user_message: str) -> str:
1602
+ """Add natural enhancements to responses."""
1603
+
1604
+ # Don't enhance if already natural
1605
+ if any(phrase in response[:50] for phrase in ["You know", "Actually", "Hmm", "That's"]):
1606
+ return response
1607
+
1608
+ # Add natural starter based on context
1609
+ if "?" in user_message:
1610
+ starters = ["That's a great question! ", "Good question - ", "Hmm, "]
1611
+ response = random.choice(starters) + response
1612
+
1613
+ return response
1614
+
1615
+ def _generate_fallback_response(self, user_message: str) -> str:
1616
+ """Generate fallback response when LLM fails."""
1617
+
1618
+ if "?" in user_message:
1619
+ return "That's an interesting question! Could you tell me a bit more about what you're looking for?"
1620
+ else:
1621
+ return "I'm here to help with research and exploration. What would you like to know more about?"
1622
+
1623
+ def _analyze_conversation_state(self) -> str:
1624
+ """Dynamically determine conversation state based on multiple factors."""
1625
+
1626
+ # Don't need complex AI here - just smart logic!
1627
+ conversation_length = len(self.history)
1628
+
1629
+ # Early conversation
1630
+ if conversation_length < 4:
1631
+ return "warming_up"
1632
+
1633
+ # Check if we're already researching
1634
+ if self.status == "running":
1635
+ return "researching"
1636
+
1637
+ # Check if we have results
1638
+ if self.synthesis:
1639
+ return "discussing_results"
1640
+
1641
+ # Check depth and engagement
1642
+ if self.research_proposed and not self.projection_given:
1643
+ return "ready_for_research"
1644
+
1645
+ # Default exploring state
1646
+ complexity = self._assess_question_complexity()
1647
+ if complexity > 0.5 and conversation_length > 6:
1648
+ return "ready_for_research"
1649
+
1650
+ return "exploring"
1651
+
1652
+ def _is_ambiguous(self, user_message: str) -> bool:
1653
+ """Check if user message is ambiguous."""
1654
+ # Simple checks - no AI needed
1655
+ message_lower = user_message.lower().strip()
1656
+
1657
+ # Too short
1658
+ if len(message_lower.split()) < 3:
1659
+ return True
1660
+
1661
+ # Very broad terms
1662
+ broad_terms = ["everything", "all", "anything", "whatever", "stuff"]
1663
+ if any(term in message_lower for term in broad_terms) and "?" in message_lower:
1664
+ return True
1665
+
1666
+ # Multiple unrelated topics
1667
+ topic_keywords = ["and also", "oh and", "btw", "by the way", "another thing"]
1668
+ if any(keyword in message_lower for keyword in topic_keywords):
1669
+ return True
1670
+
1671
+ return False
1672
+
1673
+ def _prepare_conversation_history(self) -> list:
1674
+ """Prepare conversation history for LLM context."""
1675
+ # Keep last 10 messages or less
1676
+ return self.history[-10:]
1677
+
1678
+ def _summarize_web_context(self) -> str:
1679
+ """Summarize web search results for context."""
1680
+ if not self.parallel_web_context:
1681
+ return "No recent web searches"
1682
+
1683
+ # Simple summary of top 3 results
1684
+ summaries = []
1685
+ for result in self.parallel_web_context[:3]:
1686
+ title = result.get('title', 'Unknown')
1687
+ snippet = result.get('snippet', '')[:100]
1688
+ summaries.append(f"{title}: {snippet}...")
1689
+
1690
+ return " | ".join(summaries)
1691
+
1692
+ def _has_research_indicators(self) -> bool:
1693
+ """Check if conversation has research indicators."""
1694
+ recent_messages = " ".join([
1695
+ turn["content"].lower() for turn in self.history[-4:]
1696
+ if turn["role"] == "user"
1697
+ ])
1698
+
1699
+ research_terms = [
1700
+ "research", "papers", "study", "literature",
1701
+ "evidence", "findings", "what does the research say"
1702
+ ]
1703
+
1704
+ return any(term in recent_messages for term in research_terms)
1705
+
1706
+ async def _handle_topic_inquiry(self, user_message: str) -> str:
1707
+ """Handle inquiries about specific topics with academic depth."""
1708
+ try:
1709
+ # Extract the topic being inquired about
1710
+ topic_prompt = f"Extract the specific topic or concept being inquired about from: {user_message}"
1711
+ topic_result = await self.doc_client.process_document(
1712
+ title="Topic Extraction",
1713
+ content=topic_prompt,
1714
+ model="llama-3.3-70b",
1715
+ temperature=0.1,
1716
+ max_tokens=100
1717
+ )
1718
+
1719
+ topic = topic_result.get("raw_text", "").strip()
1720
+
1721
+ # Create comprehensive response about the topic
1722
+ response_prompt = (
1723
+ f"Provide a comprehensive academic explanation of '{topic}' including:\n"
1724
+ f"- Definition and key concepts\n"
1725
+ f"- Current state of research\n"
1726
+ f"- Related academic fields\n"
1727
+ f"- Potential research directions\n"
1728
+ f"- How it might relate to the user's research context\n\n"
1729
+ f"User's research context: {self.get_context_summary()}"
1730
+ )
1731
+
1732
+ result = await self.doc_client.process_document(
1733
+ title="Topic Explanation",
1734
+ content=response_prompt,
1735
+ model="llama-3.3-70b",
1736
+ temperature=0.4,
1737
+ max_tokens=400
1738
+ )
1739
+
1740
+ response = result.get("raw_text", f"I'm familiar with {topic}. Could you tell me more about how it relates to your research?")
1741
+
1742
+ self.history.append({"role": "assistant", "content": response})
1743
+ return response
1744
+
1745
+ except Exception as e:
1746
+ return "I'd be happy to discuss that topic in detail. Could you tell me more about how it relates to your research?"
1747
+
1748
+ async def _handle_scope_change(self, user_message: str) -> str:
1749
+ """Handle scope changes with academic research methodology."""
1750
+ try:
1751
+ prompt = (
1752
+ f"User wants to change the research scope: {user_message}\n\n"
1753
+ f"Current research context: {self.get_context_summary()}\n\n"
1754
+ f"Provide guidance on how to handle this scope change academically, including:\n"
1755
+ f"- Whether to expand current research or start new\n"
1756
+ f"- How to maintain academic rigor\n"
1757
+ f"- Potential research questions\n"
1758
+ f"- Methodology considerations"
1759
+ )
1760
+
1761
+ result = await self.doc_client.process_document(
1762
+ title="Scope Change Guidance",
1763
+ content=prompt,
1764
+ model="llama-3.3-70b",
1765
+ temperature=0.3,
1766
+ max_tokens=400
1767
+ )
1768
+
1769
+ response = result.get("raw_text", "I can help you adjust the research scope. Should we expand the current research or start a new direction?")
1770
+
1771
+ self.history.append({"role": "assistant", "content": response})
1772
+ return response
1773
+
1774
+ except Exception as e:
1775
+ return "I can help you adjust the research scope. Would you like to expand the current research or explore a new direction?"
1776
+
1777
+ async def _prompt_for_research_preferences(self) -> str:
1778
+ """Prompt user for research timeframe and depth preferences."""
1779
+ preference_message = (
1780
+ "🤖 Bot: I'm ready to research this topic comprehensively. "
1781
+ "I can conduct thorough research in the background to get the most complete synthesis possible.\n\n"
1782
+ "**Research Options:**\n"
1783
+ "• **Comprehensive** (recommended): I'll research as much as I can for the most complete findings\n"
1784
+ "• **Time-limited**: Set a specific timeframe (e.g., 'finish in 10 minutes' or 'take up to 1 hour')\n"
1785
+ "• **Quick overview**: Just get the main points quickly\n\n"
1786
+ "What's your preference? You can say:\n"
1787
+ "- 'Go comprehensive' or 'Take your time'\n"
1788
+ "- 'Finish in X minutes/hours'\n"
1789
+ "- 'Quick overview only'\n"
1790
+ "- Or just 'proceed' for comprehensive research"
1791
+ )
1792
+ self.history.append({"role": "assistant", "content": preference_message})
1793
+ return preference_message
1794
+
1795
+ async def _parse_research_preferences(self, user_message: str) -> dict:
1796
+ """Parse user's research timeframe and depth preferences."""
1797
+ message_lower = user_message.lower()
1798
+
1799
+ # Default to comprehensive research
1800
+ preferences = {
1801
+ "comprehensive": True,
1802
+ "time_limit": None,
1803
+ "quick_overview": False
1804
+ }
1805
+
1806
+ # Check for time limits
1807
+ time_patterns = [
1808
+ r"finish in (\d+)\s*(minute|minutes|hour|hours)",
1809
+ r"(\d+)\s*(minute|minutes|hour|hours)",
1810
+ r"time limit.*?(\d+)\s*(minute|minutes|hour|hours)",
1811
+ r"(\d+)\s*(min|mins|hr|hrs)"
1812
+ ]
1813
+
1814
+ for pattern in time_patterns:
1815
+ match = re.search(pattern, message_lower)
1816
+ if match:
1817
+ time_value = int(match.group(1))
1818
+ time_unit = match.group(2)
1819
+ if time_unit in ['minute', 'minutes', 'min', 'mins']:
1820
+ preferences["time_limit"] = time_value * 60 # Convert to seconds
1821
+ elif time_unit in ['hour', 'hours', 'hr', 'hrs']:
1822
+ preferences["time_limit"] = time_value * 3600 # Convert to seconds
1823
+ preferences["comprehensive"] = False
1824
+ break
1825
+
1826
+ # Check for quick overview
1827
+ if any(phrase in message_lower for phrase in ["quick", "overview", "summary", "brief", "fast"]):
1828
+ preferences["quick_overview"] = True
1829
+ preferences["comprehensive"] = False
1830
+
1831
+ # Check for comprehensive research
1832
+ if any(phrase in message_lower for phrase in ["comprehensive", "thorough", "complete", "take your time", "go comprehensive"]):
1833
+ preferences["comprehensive"] = True
1834
+ preferences["quick_overview"] = False
1835
+
1836
+ return preferences
1837
+
1838
+ def get_context_summary(self) -> str:
1839
+ summary = "\n".join([
1840
+ f"{turn['role'].capitalize()}: {turn['content']}" for turn in self.history
1841
+ ])
1842
+ return summary
1843
+
1844
+ async def build_research_plan(self):
1845
+ """Use LLMDocClient to synthesize a research plan from the conversation."""
1846
+ try:
1847
+ plan_prompt = (
1848
+ "Summarize the following conversation as a structured research plan. "
1849
+ "List the main topic, sub-questions, and any constraints or context provided by the user.\n\n"
1850
+ ) + self.get_context_summary()
1851
+ # Use doc client for plan extraction
1852
+ result = await self.doc_client.process_document(
1853
+ title="Research Plan Extraction",
1854
+ content=plan_prompt,
1855
+ model="llama-3.3-70b",
1856
+ temperature=0.3,
1857
+ max_tokens=1500
1858
+ )
1859
+ plan = result.get("raw_text") or result.get("main_findings", [""])[0] or "[No plan extracted]"
1860
+ self.research_plan = plan
1861
+ return plan
1862
+ except Exception as e:
1863
+ return f"Error building plan: {str(e)}"
1864
+
1865
+ async def check_status(self):
1866
+ if not self.session_id:
1867
+ print("No research session running.")
1868
+ return
1869
+ status = await self.context_manager.get_session_status(self.session_id)
1870
+ print(f"\n[Session Status: {status.get('status')}] Progress: {status.get('progress', {}).get('percentage', 0)}%")
1871
+ if status.get('status') == 'completed':
1872
+ self.status = 'completed'
1873
+ return status
1874
+
1875
+ async def show_results(self):
1876
+ if not self.session_id:
1877
+ return "No research session found."
1878
+ session = await self.context_manager._get_session(self.session_id)
1879
+ synthesis = session.synthesis if session else None
1880
+ if synthesis:
1881
+ self.synthesis = synthesis
1882
+ results_message = (
1883
+ f"Research complete! Here are the findings:\n\n"
1884
+ f"{str(synthesis)[:2000]}{'...' if len(str(synthesis)) > 2000 else ''}\n\n"
1885
+ f"You can ask me follow-up questions about the results, or start a new research topic."
1886
+ )
1887
+
1888
+ self.history.append({"role": "assistant", "content": results_message})
1889
+ return results_message
1890
+ else:
1891
+ return "Research is still in progress. Please wait a moment."
1892
+
1893
+ def _extract_topic_and_questions(self, plan: str):
1894
+ topic = None
1895
+ questions = []
1896
+ for line in plan.splitlines():
1897
+ if line.lower().startswith("topic:"):
1898
+ topic = line.split(":", 1)[1].strip()
1899
+ elif line.lower().startswith("questions:"):
1900
+ continue
1901
+ elif line.strip().startswith("-"):
1902
+ questions.append(line.strip("- ").strip())
1903
+ if not topic:
1904
+ for turn in self.history:
1905
+ if turn["role"] == "user":
1906
+ topic = turn["content"]
1907
+ break
1908
+ return topic or "Untitled Research", questions
1909
+
1910
+ async def _start_research_with_preferences(self, preferences: dict) -> str:
1911
+ """Start research with user-specified preferences for timeframe and depth."""
1912
+
1913
+ # Set research parameters based on preferences
1914
+ if preferences.get("time_limit"):
1915
+ self.max_research_time = preferences["time_limit"]
1916
+
1917
+ if preferences.get("quick_overview"):
1918
+ self.research_depth = "quick"
1919
+ elif preferences.get("comprehensive"):
1920
+ self.research_depth = "comprehensive"
1921
+
1922
+ # Start the research process
1923
+ return await self._start_research()
1924
+
1925
+ async def run_cli_chatbot():
1926
+ print("\n🧑‍💻 Nocturnal Archive Research Chatbot (CLI Mode)")
1927
+ print("Type 'exit' to quit. Type 'status' to check progress.\n")
1928
+
1929
+ # Check system readiness
1930
+ system_issues = []
1931
+
1932
+ # Check API keys
1933
+ required_keys = ['MISTRAL_API_KEY', 'COHERE_API_KEY', 'CEREBRAS_API_KEY']
1934
+ for key in required_keys:
1935
+ if not os.environ.get(key):
1936
+ system_issues.append(f"Missing {key}")
1937
+
1938
+ # Check database URLs
1939
+ if not os.environ.get('MONGODB_URL') and not os.environ.get('MONGO_URL'):
1940
+ system_issues.append("Missing database URL")
1941
+
1942
+ if not os.environ.get('REDIS_URL'):
1943
+ system_issues.append("Missing Redis URL")
1944
+
1945
+ # Display system status
1946
+ if system_issues:
1947
+ print("⚠️ System Configuration Issues Detected:")
1948
+ for issue in system_issues:
1949
+ print(f" • {issue}")
1950
+ print("\n🔄 Running in simulation mode with limited functionality.")
1951
+ print(" For full functionality, please configure your .env.local file.")
1952
+ print(" See SETUP_GUIDE.md for detailed instructions.\n")
1953
+
1954
+ try:
1955
+ # Try to initialize full system
1956
+ db_ops = None
1957
+ llm_manager = None
1958
+ synthesizer = None
1959
+ context_manager = None
1960
+
1961
+ try:
1962
+ redis_url = os.environ.get('REDIS_URL', 'redis://localhost:6379')
1963
+ mongo_url = os.environ.get('MONGODB_URL', os.environ.get('MONGO_URL', 'mongodb://localhost:27017/nocturnal_archive'))
1964
+
1965
+ db_ops = DatabaseOperations(mongo_url, redis_url)
1966
+ llm_manager = LLMManager(redis_url)
1967
+ knowledge_graph = KnowledgeGraph()
1968
+ synthesizer = ResearchSynthesizer(
1969
+ db_ops=db_ops,
1970
+ llm_manager=llm_manager,
1971
+ redis_url=redis_url,
1972
+ kg_client=knowledge_graph
1973
+ )
1974
+ context_manager = ResearchContextManager(db_ops, synthesizer, redis_url)
1975
+ except Exception as e:
1976
+ logger.warning(f"Full system initialization failed: {e}")
1977
+ print("⚠️ Some system components failed to initialize.")
1978
+ print(" Running in fallback mode with simulated responses.\n")
1979
+
1980
+ # Initialize chatbot session (will automatically detect fallback mode)
1981
+ session = ChatbotResearchSession(context_manager, synthesizer, db_ops)
1982
+
1983
+ print("✅ Chatbot initialized successfully!")
1984
+ if session.fallback_mode:
1985
+ print("📋 Available in simulation mode:")
1986
+ print(" • Research topic discussions")
1987
+ print(" • Workflow demonstrations")
1988
+ print(" • Capability explanations")
1989
+ print(" • Error-free conversation")
1990
+ else:
1991
+ print("🚀 Full functionality available!")
1992
+ print(" • Real research execution")
1993
+ print(" • Database integration")
1994
+ print(" • LLM-powered analysis")
1995
+
1996
+ print("\n" + "="*50)
1997
+ print("Start your conversation! Try saying:")
1998
+ print("• 'Hello' or 'Hi'")
1999
+ print("• 'I want to research quantum computing'")
2000
+ print("• 'What can you do?'")
2001
+ print("• 'Help'")
2002
+ print("="*50 + "\n")
2003
+
2004
+ # Initialize typing effect for welcome message
2005
+ typing_effect = TypingEffect(speed=0.02)
2006
+ welcome_message = "Hello! I'm the Nocturnal Archive research assistant. I can help you with comprehensive research on any topic. What would you like to research today?"
2007
+ await typing_effect.type_message(welcome_message)
2008
+
2009
+ while session.active:
2010
+ try:
2011
+ user_input = input("\nYou: ").strip()
2012
+
2013
+ if user_input.lower() in {"exit", "quit", "bye"}:
2014
+ await typing_effect.type_message("Goodbye! Thanks for using Nocturnal Archive!")
2015
+ break
2016
+
2017
+ if user_input.lower() == "status":
2018
+ if not session.fallback_mode:
2019
+ await session.check_status()
2020
+ else:
2021
+ await typing_effect.type_message("Running in simulation mode - no active research sessions.")
2022
+ continue
2023
+
2024
+ if user_input.lower() == "help":
2025
+ help_message = "Available commands:\n • Type any research topic to start a conversation\n • 'status' - Check research progress (full mode only)\n • 'exit' or 'quit' - End the session\n • 'help' - Show this help message"
2026
+ await typing_effect.type_message(help_message)
2027
+ continue
2028
+
2029
+ if not user_input:
2030
+ await typing_effect.type_message("Please type something! I'm here to help with research.")
2031
+ continue
2032
+
2033
+ # Process the response (typing effect is handled in chat_turn)
2034
+ response = await session.chat_turn(user_input)
2035
+
2036
+ except EOFError:
2037
+ await typing_effect.type_message("Goodbye! Thanks for using Nocturnal Archive!")
2038
+ break
2039
+ except KeyboardInterrupt:
2040
+ await typing_effect.type_message("Goodbye! Thanks for using Nocturnal Archive!")
2041
+ break
2042
+ except Exception as e:
2043
+ logger.error(f"Unexpected error in chat loop: {str(e)}")
2044
+ await typing_effect.type_message(f"I encountered an unexpected error: {str(e)}")
2045
+ await typing_effect.type_message("Let's continue our conversation!")
2046
+ continue
2047
+
2048
+ except Exception as e:
2049
+ logger.error(f"Failed to initialize chatbot: {str(e)}")
2050
+ print(f"❌ Failed to initialize chatbot: {str(e)}")
2051
+ print("\n🔧 Troubleshooting:")
2052
+ print("1. Check your .env.local file configuration")
2053
+ print("2. Ensure all required dependencies are installed")
2054
+ print("3. Verify database connections")
2055
+ print("4. Check API key configuration")
2056
+ print("\n📖 For detailed setup instructions, see SETUP_GUIDE.md")