cite-agent 1.3.6__py3-none-any.whl → 1.3.7__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.
- cite_agent/__version__.py +1 -1
- cite_agent/cli.py +9 -2
- cite_agent/enhanced_ai_agent.py +332 -73
- {cite_agent-1.3.6.dist-info → cite_agent-1.3.7.dist-info}/METADATA +1 -1
- cite_agent-1.3.7.dist-info/RECORD +31 -0
- {cite_agent-1.3.6.dist-info → cite_agent-1.3.7.dist-info}/top_level.txt +0 -1
- cite_agent-1.3.6.dist-info/RECORD +0 -57
- src/__init__.py +0 -1
- src/services/__init__.py +0 -132
- src/services/auth_service/__init__.py +0 -3
- src/services/auth_service/auth_manager.py +0 -33
- src/services/graph/__init__.py +0 -1
- src/services/graph/knowledge_graph.py +0 -194
- src/services/llm_service/__init__.py +0 -5
- src/services/llm_service/llm_manager.py +0 -495
- src/services/paper_service/__init__.py +0 -5
- src/services/paper_service/openalex.py +0 -231
- src/services/performance_service/__init__.py +0 -1
- src/services/performance_service/rust_performance.py +0 -395
- src/services/research_service/__init__.py +0 -23
- src/services/research_service/chatbot.py +0 -2056
- src/services/research_service/citation_manager.py +0 -436
- src/services/research_service/context_manager.py +0 -1441
- src/services/research_service/conversation_manager.py +0 -597
- src/services/research_service/critical_paper_detector.py +0 -577
- src/services/research_service/enhanced_research.py +0 -121
- src/services/research_service/enhanced_synthesizer.py +0 -375
- src/services/research_service/query_generator.py +0 -777
- src/services/research_service/synthesizer.py +0 -1273
- src/services/search_service/__init__.py +0 -5
- src/services/search_service/indexer.py +0 -186
- src/services/search_service/search_engine.py +0 -342
- src/services/simple_enhanced_main.py +0 -287
- {cite_agent-1.3.6.dist-info → cite_agent-1.3.7.dist-info}/WHEEL +0 -0
- {cite_agent-1.3.6.dist-info → cite_agent-1.3.7.dist-info}/entry_points.txt +0 -0
- {cite_agent-1.3.6.dist-info → cite_agent-1.3.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,2056 +0,0 @@
|
|
|
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")
|