Ananthakr1shnan commited on
Commit
141e89b
·
verified ·
1 Parent(s): 6fef50e

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +781 -723
main.py CHANGED
@@ -1,724 +1,782 @@
1
- import os
2
- import sys
3
- import json
4
- import asyncio
5
- from typing import Dict, List, Optional, Any
6
- from datetime import datetime
7
- from pathlib import Path
8
- from contextlib import asynccontextmanager
9
-
10
- # Add the project root to Python path
11
- sys.path.append(str(Path(__file__).parent))
12
-
13
- from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Request, Depends
14
- from fastapi.staticfiles import StaticFiles
15
- from fastapi.templating import Jinja2Templates
16
- from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, FileResponse
17
- from fastapi.middleware.cors import CORSMiddleware
18
- from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
19
- from pydantic import BaseModel, Field
20
- import uvicorn
21
-
22
- # Import settings and ResearchMate components
23
- from src.components.research_assistant import ResearchMate
24
- from src.components.citation_network import CitationNetworkAnalyzer
25
- from src.components.auth import AuthManager
26
-
27
- # Initialize only essential components at startup (fast components only)
28
- auth_manager = AuthManager()
29
- security = HTTPBearer(auto_error=False)
30
-
31
- # Simple settings for development
32
- class Settings:
33
- def __init__(self):
34
- self.server = type('ServerSettings', (), {
35
- 'debug': False,
36
- 'host': '0.0.0.0',
37
- 'port': int(os.environ.get('PORT', 8000))
38
- })()
39
- self.security = type('SecuritySettings', (), {
40
- 'cors_origins': ["*"],
41
- 'cors_methods': ["*"],
42
- 'cors_headers': ["*"]
43
- })()
44
-
45
- def get_static_dir(self):
46
- return "src/static"
47
-
48
- def get_templates_dir(self):
49
- return "src/templates"
50
-
51
- settings = Settings()
52
-
53
- # Initialize ResearchMate and Citation Analyzer (will be done during loading screen)
54
- research_mate = None
55
- citation_analyzer = None
56
-
57
- # Global initialization flag
58
- research_mate_initialized = False
59
- initialization_in_progress = False
60
-
61
- async def initialize_research_mate():
62
- """Initialize ResearchMate and Citation Analyzer in the background"""
63
- global research_mate, citation_analyzer, research_mate_initialized, initialization_in_progress
64
-
65
- if initialization_in_progress:
66
- return
67
-
68
- initialization_in_progress = True
69
- print("🚀 Starting ResearchMate background initialization...")
70
-
71
- try:
72
- # Run initialization in thread pool to avoid blocking
73
- import concurrent.futures
74
- with concurrent.futures.ThreadPoolExecutor() as executor:
75
- loop = asyncio.get_event_loop()
76
-
77
- print("📊 Initializing Citation Network Analyzer...")
78
- citation_analyzer = await loop.run_in_executor(executor, CitationNetworkAnalyzer)
79
- print("✅ Citation Network Analyzer initialized!")
80
-
81
- print("🧠 Initializing ResearchMate core...")
82
- research_mate = await loop.run_in_executor(executor, ResearchMate)
83
- print("✅ ResearchMate core initialized!")
84
-
85
- research_mate_initialized = True
86
- print("🎉 All components initialized successfully!")
87
- except Exception as e:
88
- print(f"❌ Failed to initialize components: {e}")
89
- print("⚠️ Server will start but some features may not work")
90
- research_mate = None
91
- citation_analyzer = None
92
- research_mate_initialized = False
93
- finally:
94
- initialization_in_progress = False
95
-
96
- # Pydantic models for API
97
- class SearchQuery(BaseModel):
98
- query: str = Field(..., description="Search query")
99
- max_results: int = Field(default=10, ge=1, le=50, description="Maximum number of results")
100
-
101
- class QuestionQuery(BaseModel):
102
- question: str = Field(..., description="Research question")
103
-
104
- class ProjectCreate(BaseModel):
105
- name: str = Field(..., description="Project name")
106
- research_question: str = Field(..., description="Research question")
107
- keywords: List[str] = Field(..., description="Keywords")
108
-
109
- class ProjectQuery(BaseModel):
110
- project_id: str = Field(..., description="Project ID")
111
- question: str = Field(..., description="Question about the project")
112
-
113
- class TrendQuery(BaseModel):
114
- topic: str = Field(..., description="Research topic")
115
-
116
- # Authentication models
117
- class LoginRequest(BaseModel):
118
- username: str = Field(..., description="Username")
119
- password: str = Field(..., description="Password")
120
-
121
- class RegisterRequest(BaseModel):
122
- username: str = Field(..., description="Username")
123
- email: str = Field(..., description="Email address")
124
- password: str = Field(..., description="Password")
125
-
126
- # Authentication dependency for API endpoints
127
- async def get_current_user_dependency(request: Request, credentials: HTTPAuthorizationCredentials = Depends(security)):
128
- user = None
129
-
130
- # Try Authorization header first
131
- if credentials:
132
- user = auth_manager.verify_token(credentials.credentials)
133
-
134
- # If no user from header, try cookie
135
- if not user:
136
- token = request.cookies.get('authToken')
137
- if token:
138
- user = auth_manager.verify_token(token)
139
-
140
- if not user:
141
- raise HTTPException(status_code=401, detail="Authentication required")
142
-
143
- return user
144
-
145
- # Authentication for web pages (checks both header and cookie)
146
- async def get_current_user_web(request: Request):
147
- """Get current user for web page requests (checks both Authorization header and cookies)"""
148
- user = None
149
-
150
- # First try Authorization header
151
- try:
152
- credentials = await security(request)
153
- if credentials:
154
- user = auth_manager.verify_token(credentials.credentials)
155
- except:
156
- pass
157
-
158
- # If no user from header, try cookie
159
- if not user:
160
- token = request.cookies.get('authToken')
161
- if token:
162
- user = auth_manager.verify_token(token)
163
-
164
- return user
165
-
166
- # Background task to clean up expired sessions
167
- async def cleanup_expired_sessions():
168
- while True:
169
- try:
170
- expired_count = auth_manager.cleanup_expired_sessions()
171
- if expired_count > 0:
172
- print(f"Cleaned up {expired_count} expired sessions")
173
- except Exception as e:
174
- print(f"Error cleaning up sessions: {e}")
175
-
176
- # Run cleanup every 30 minutes
177
- await asyncio.sleep(30 * 60)
178
-
179
- @asynccontextmanager
180
- async def lifespan(app: FastAPI):
181
- # Start ResearchMate initialization in background (non-blocking)
182
- asyncio.create_task(initialize_research_mate())
183
-
184
- # Start background cleanup task
185
- cleanup_task = asyncio.create_task(cleanup_expired_sessions())
186
-
187
- try:
188
- yield
189
- finally:
190
- cleanup_task.cancel()
191
- try:
192
- await cleanup_task
193
- except asyncio.CancelledError:
194
- pass
195
-
196
- # Initialize FastAPI app with lifespan
197
- app = FastAPI(
198
- title="ResearchMate API",
199
- description="AI Research Assistant powered by Groq Llama 3.3 70B",
200
- version="1.0.0",
201
- debug=settings.server.debug,
202
- lifespan=lifespan
203
- )
204
-
205
- # Add CORS middleware
206
- app.add_middleware(
207
- CORSMiddleware,
208
- allow_origins=settings.security.cors_origins,
209
- allow_credentials=True,
210
- allow_methods=settings.security.cors_methods,
211
- allow_headers=settings.security.cors_headers,
212
- )
213
-
214
- # Mount static files with cache control for development
215
- static_dir = Path(settings.get_static_dir())
216
- static_dir.mkdir(parents=True, exist_ok=True)
217
-
218
- # Custom static files class to add no-cache headers for development
219
- class NoCacheStaticFiles(StaticFiles):
220
- def file_response(self, full_path, stat_result, scope):
221
- response = FileResponse(
222
- path=full_path,
223
- stat_result=stat_result
224
- )
225
- # Add no-cache headers for development
226
- response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
227
- response.headers["Pragma"] = "no-cache"
228
- response.headers["Expires"] = "0"
229
- return response
230
-
231
- app.mount("/static", NoCacheStaticFiles(directory=str(static_dir)), name="static")
232
-
233
- # Templates
234
- templates_dir = Path(settings.get_templates_dir())
235
- templates_dir.mkdir(parents=True, exist_ok=True)
236
- templates = Jinja2Templates(directory=str(templates_dir))
237
-
238
- # Loading page route
239
- @app.get("/loading", response_class=HTMLResponse)
240
- async def loading_page(request: Request):
241
- return templates.TemplateResponse("loading.html", {"request": request})
242
-
243
- # Authentication routes
244
- @app.post("/api/auth/register")
245
- async def register(request: RegisterRequest):
246
- result = auth_manager.create_user(request.username, request.email, request.password)
247
- if result["success"]:
248
- return {"success": True, "message": "Account created successfully"}
249
- else:
250
- raise HTTPException(status_code=400, detail=result["error"])
251
-
252
- @app.post("/api/auth/login")
253
- async def login(request: LoginRequest):
254
- result = auth_manager.authenticate_user(request.username, request.password)
255
- if result["success"]:
256
- return {
257
- "success": True,
258
- "token": result["token"],
259
- "user_id": result["user_id"],
260
- "username": result["username"]
261
- }
262
- else:
263
- raise HTTPException(status_code=401, detail=result["error"])
264
-
265
- @app.get("/login", response_class=HTMLResponse)
266
- async def login_page(request: Request):
267
- # Check if ResearchMate is initialized
268
- global research_mate_initialized
269
- if not research_mate_initialized:
270
- return RedirectResponse(url="/loading", status_code=302)
271
-
272
- return templates.TemplateResponse("login.html", {"request": request})
273
-
274
- @app.post("/api/auth/logout")
275
- async def logout(request: Request):
276
- # Get current user to invalidate their session
277
- user = await get_current_user_web(request)
278
- if user:
279
- auth_manager.logout_user(user['user_id'])
280
-
281
- response = JSONResponse({"success": True, "message": "Logged out successfully"})
282
- response.delete_cookie("authToken", path="/")
283
- return response
284
-
285
- # Web interface routes (protected)
286
- @app.get("/", response_class=HTMLResponse)
287
- async def home(request: Request):
288
- # Check if ResearchMate is initialized first
289
- global research_mate_initialized
290
- if not research_mate_initialized:
291
- return RedirectResponse(url="/loading", status_code=302)
292
-
293
- # Check if user is authenticated
294
- user = await get_current_user_web(request)
295
- if not user:
296
- return RedirectResponse(url="/login", status_code=302)
297
- return templates.TemplateResponse("index.html", {"request": request, "user": user})
298
-
299
- @app.get("/search", response_class=HTMLResponse)
300
- async def search_page(request: Request):
301
- # Check if ResearchMate is initialized first
302
- global research_mate_initialized
303
- if not research_mate_initialized:
304
- return RedirectResponse(url="/loading", status_code=302)
305
-
306
- user = await get_current_user_web(request)
307
- if not user:
308
- return RedirectResponse(url="/login", status_code=302)
309
- return templates.TemplateResponse("search.html", {"request": request, "user": user})
310
-
311
- @app.get("/projects", response_class=HTMLResponse)
312
- async def projects_page(request: Request):
313
- user = await get_current_user_web(request)
314
- if not user:
315
- return RedirectResponse(url="/login", status_code=302)
316
- return templates.TemplateResponse("projects.html", {"request": request, "user": user})
317
-
318
- @app.get("/trends", response_class=HTMLResponse)
319
- async def trends_page(request: Request):
320
- user = await get_current_user_web(request)
321
- if not user:
322
- return RedirectResponse(url="/login", status_code=302)
323
- return templates.TemplateResponse("trends.html", {"request": request, "user": user})
324
-
325
- @app.get("/upload", response_class=HTMLResponse)
326
- async def upload_page(request: Request):
327
- user = await get_current_user_web(request)
328
- if not user:
329
- return RedirectResponse(url="/login", status_code=302)
330
- return templates.TemplateResponse("upload.html", {"request": request, "user": user})
331
-
332
- @app.get("/citation", response_class=HTMLResponse)
333
- async def citation_page(request: Request):
334
- try:
335
- if citation_analyzer is None:
336
- # If citation analyzer isn't initialized yet, show empty state
337
- summary = {"total_papers": 0, "total_citations": 0, "networks": []}
338
- else:
339
- summary = citation_analyzer.get_network_summary()
340
- return templates.TemplateResponse("citation.html", {"request": request, "summary": summary})
341
- except Exception as e:
342
- raise HTTPException(status_code=500, detail=str(e))
343
-
344
- @app.get("/test-search", response_class=HTMLResponse)
345
- async def test_search_page(request: Request):
346
- """Simple test page for debugging search"""
347
- with open("test_search.html", "r") as f:
348
- content = f.read()
349
- return HTMLResponse(content=content)
350
-
351
- # API endpoints
352
- @app.post("/api/search")
353
- async def search_papers(query: SearchQuery, current_user: dict = Depends(get_current_user_dependency)):
354
- try:
355
- if research_mate is None:
356
- raise HTTPException(status_code=503, detail="ResearchMate not initialized")
357
- rm = research_mate
358
- result = rm.search(query.query, query.max_results)
359
- if not result.get("success"):
360
- raise HTTPException(status_code=400, detail=result.get("error", "Search failed"))
361
- papers = result.get("papers", [])
362
- if papers and citation_analyzer is not None: # Only add papers if citation analyzer is ready
363
- citation_analyzer.add_papers(papers)
364
- return result
365
- except Exception as e:
366
- raise HTTPException(status_code=500, detail=str(e))
367
-
368
- @app.post("/api/ask")
369
- async def ask_question(question: QuestionQuery, current_user: dict = Depends(get_current_user_dependency)):
370
- try:
371
- if research_mate is None:
372
- raise HTTPException(status_code=503, detail="ResearchMate not initialized")
373
- rm = research_mate
374
- result = rm.ask(question.question)
375
- if not result.get("success"):
376
- raise HTTPException(status_code=400, detail=result.get("error", "Question failed"))
377
- return result
378
- except Exception as e:
379
- raise HTTPException(status_code=500, detail=str(e))
380
-
381
- @app.post("/api/upload")
382
- async def upload_pdf(file: UploadFile = File(...), current_user: dict = Depends(get_current_user_dependency)):
383
- if research_mate is None:
384
- raise HTTPException(status_code=503, detail="ResearchMate not initialized")
385
-
386
- if not file.filename.endswith('.pdf'):
387
- raise HTTPException(status_code=400, detail="Only PDF files are supported")
388
-
389
- try:
390
- # Save uploaded file
391
- upload_dir = Path("uploads")
392
- upload_dir.mkdir(exist_ok=True)
393
- file_path = upload_dir / file.filename
394
-
395
- with open(file_path, "wb") as buffer:
396
- content = await file.read()
397
- buffer.write(content)
398
-
399
- # Process PDF
400
- result = research_mate.upload_pdf(str(file_path))
401
-
402
- # Clean up file
403
- file_path.unlink()
404
-
405
- if not result.get("success"):
406
- raise HTTPException(status_code=400, detail=result.get("error", "PDF analysis failed"))
407
-
408
- return result
409
- except Exception as e:
410
- raise HTTPException(status_code=500, detail=str(e))
411
-
412
- @app.post("/api/projects")
413
- async def create_project(project: ProjectCreate, current_user: dict = Depends(get_current_user_dependency)):
414
- if research_mate is None:
415
- raise HTTPException(status_code=503, detail="ResearchMate not initialized")
416
-
417
- try:
418
- user_id = current_user.get("user_id")
419
- result = research_mate.create_project(project.name, project.research_question, project.keywords, user_id)
420
- if not result.get("success"):
421
- raise HTTPException(status_code=400, detail=result.get("error", "Project creation failed"))
422
- return result
423
- except Exception as e:
424
- raise HTTPException(status_code=500, detail=str(e))
425
-
426
- @app.get("/api/projects")
427
- async def list_projects(current_user: dict = Depends(get_current_user_dependency)):
428
- if research_mate is None:
429
- raise HTTPException(status_code=503, detail="ResearchMate not initialized")
430
-
431
- try:
432
- user_id = current_user.get("user_id")
433
- result = research_mate.list_projects(user_id)
434
- if not result.get("success"):
435
- raise HTTPException(status_code=400, detail=result.get("error", "Failed to list projects"))
436
- return result
437
- except Exception as e:
438
- raise HTTPException(status_code=500, detail=str(e))
439
-
440
- @app.get("/api/projects/{project_id}")
441
- async def get_project(project_id: str, current_user: dict = Depends(get_current_user_dependency)):
442
- if research_mate is None:
443
- raise HTTPException(status_code=503, detail="ResearchMate not initialized")
444
-
445
- try:
446
- user_id = current_user.get("user_id")
447
- result = research_mate.get_project(project_id, user_id)
448
- if not result.get("success"):
449
- raise HTTPException(status_code=404, detail=result.get("error", "Project not found"))
450
- return result
451
- except Exception as e:
452
- raise HTTPException(status_code=500, detail=str(e))
453
-
454
- @app.post("/api/projects/{project_id}/search")
455
- async def search_project_literature(project_id: str, max_papers: int = 10, current_user: dict = Depends(get_current_user_dependency)):
456
- if research_mate is None:
457
- raise HTTPException(status_code=503, detail="ResearchMate not initialized")
458
-
459
- try:
460
- user_id = current_user.get("user_id")
461
- result = research_mate.search_project_literature(project_id, max_papers, user_id)
462
- if not result.get("success"):
463
- raise HTTPException(status_code=400, detail=result.get("error", "Literature search failed"))
464
- return result
465
- except Exception as e:
466
- raise HTTPException(status_code=500, detail=str(e))
467
-
468
- @app.post("/api/projects/{project_id}/analyze")
469
- async def analyze_project(project_id: str, current_user: dict = Depends(get_current_user_dependency)):
470
- if research_mate is None:
471
- raise HTTPException(status_code=503, detail="ResearchMate not initialized")
472
-
473
- try:
474
- user_id = current_user.get("user_id")
475
- result = research_mate.analyze_project(project_id, user_id)
476
- if not result.get("success"):
477
- raise HTTPException(status_code=400, detail=result.get("error", "Project analysis failed"))
478
- return result
479
- except Exception as e:
480
- raise HTTPException(status_code=500, detail=str(e))
481
-
482
- @app.post("/api/projects/{project_id}/review")
483
- async def generate_review(project_id: str, current_user: dict = Depends(get_current_user_dependency)):
484
- if research_mate is None:
485
- raise HTTPException(status_code=503, detail="ResearchMate not initialized")
486
-
487
- try:
488
- user_id = current_user.get("user_id")
489
- result = research_mate.generate_review(project_id, user_id)
490
- if not result.get("success"):
491
- raise HTTPException(status_code=400, detail=result.get("error", "Review generation failed"))
492
- return result
493
- except Exception as e:
494
- raise HTTPException(status_code=500, detail=str(e))
495
-
496
- @app.post("/api/projects/{project_id}/ask")
497
- async def ask_project_question(project_id: str, question: QuestionQuery):
498
- if research_mate is None:
499
- raise HTTPException(status_code=503, detail="ResearchMate not initialized")
500
-
501
- try:
502
- result = research_mate.ask_project_question(project_id, question.question)
503
- if not result.get("success"):
504
- raise HTTPException(status_code=400, detail=result.get("error", "Project question failed"))
505
- return result
506
- except Exception as e:
507
- raise HTTPException(status_code=500, detail=str(e))
508
-
509
-
510
-
511
- @app.post("/api/trends")
512
- async def get_trends(trend: TrendQuery):
513
- if research_mate is None:
514
- raise HTTPException(status_code=503, detail="ResearchMate not initialized")
515
-
516
- try:
517
- result = research_mate.analyze_trends(trend.topic)
518
- if result.get("error"):
519
- raise HTTPException(status_code=400, detail=result.get("error", "Trend analysis failed"))
520
- return result
521
- except Exception as e:
522
- raise HTTPException(status_code=500, detail=str(e))
523
-
524
- @app.post("/api/trends/temporal")
525
- async def get_temporal_trends(trend: TrendQuery):
526
- """Get temporal trend analysis"""
527
- if research_mate is None:
528
- raise HTTPException(status_code=503, detail="ResearchMate not initialized")
529
-
530
- try:
531
- # Get papers for analysis
532
- papers = research_mate.search_papers(trend.topic, 50)
533
- if not papers:
534
- raise HTTPException(status_code=404, detail="No papers found for temporal analysis")
535
-
536
- # Use advanced trend monitor
537
- result = research_mate.trend_monitor.analyze_temporal_trends(papers)
538
- if result.get("error"):
539
- raise HTTPException(status_code=400, detail=result.get("error"))
540
-
541
- return {
542
- "topic": trend.topic,
543
- "temporal_analysis": result,
544
- "papers_analyzed": len(papers)
545
- }
546
- except Exception as e:
547
- raise HTTPException(status_code=500, detail=str(e))
548
-
549
- @app.post("/api/trends/gaps")
550
- async def detect_research_gaps(trend: TrendQuery):
551
- """Detect research gaps for a topic"""
552
- if research_mate is None:
553
- raise HTTPException(status_code=503, detail="ResearchMate not initialized")
554
-
555
- try:
556
- # Get papers for gap analysis
557
- papers = research_mate.search_papers(trend.topic, 50)
558
- if not papers:
559
- raise HTTPException(status_code=404, detail="No papers found for gap analysis")
560
-
561
- # Use advanced trend monitor
562
- result = research_mate.trend_monitor.detect_research_gaps(papers)
563
- if result.get("error"):
564
- raise HTTPException(status_code=400, detail=result.get("error"))
565
-
566
- return {
567
- "topic": trend.topic,
568
- "gap_analysis": result,
569
- "papers_analyzed": len(papers)
570
- }
571
- except Exception as e:
572
- raise HTTPException(status_code=500, detail=str(e))
573
-
574
- @app.get("/api/status")
575
- async def get_status(current_user: dict = Depends(get_current_user_dependency)):
576
- if research_mate is None:
577
- raise HTTPException(status_code=503, detail="ResearchMate not initialized")
578
-
579
- try:
580
- result = research_mate.get_status()
581
- # Ensure proper structure for frontend
582
- if result.get('success'):
583
- return {
584
- 'success': True,
585
- 'statistics': result.get('statistics', {
586
- 'rag_documents': 0,
587
- 'system_version': '2.0.0',
588
- 'status_check_time': datetime.now().isoformat()
589
- }),
590
- 'components': result.get('components', {})
591
- }
592
- else:
593
- return result
594
- except Exception as e:
595
- raise HTTPException(status_code=500, detail=str(e))
596
-
597
- # Initialization status endpoint
598
- @app.get("/api/init-status")
599
- async def get_init_status():
600
- """Check if ResearchMate is initialized"""
601
- global research_mate_initialized, initialization_in_progress
602
-
603
- if research_mate_initialized:
604
- status = "ready"
605
- elif initialization_in_progress:
606
- status = "initializing"
607
- else:
608
- status = "not_started"
609
-
610
- return {
611
- "initialized": research_mate_initialized,
612
- "in_progress": initialization_in_progress,
613
- "timestamp": datetime.now().isoformat(),
614
- "status": status
615
- }
616
-
617
- # Fast search endpoint that initializes on first call
618
- @app.post("/api/search-fast")
619
- async def search_papers_fast(query: SearchQuery, current_user: dict = Depends(get_current_user_dependency)):
620
- """Fast search that shows initialization progress"""
621
- try:
622
- global research_mate
623
- if research_mate is None:
624
- # Return immediate response indicating initialization
625
- return {
626
- "initializing": True,
627
- "message": "ResearchMate is initializing (this may take 30-60 seconds)...",
628
- "query": query.query,
629
- "estimated_time": "30-60 seconds"
630
- }
631
-
632
- # Use existing search
633
- result = research_mate.search(query.query, query.max_results)
634
- if not result.get("success"):
635
- raise HTTPException(status_code=400, detail=result.get("error", "Search failed"))
636
-
637
- papers = result.get("papers", [])
638
- if papers and citation_analyzer is not None:
639
- citation_analyzer.add_papers(papers)
640
-
641
- return result
642
- except Exception as e:
643
- raise HTTPException(status_code=500, detail=str(e))
644
-
645
- @app.get("/api/user/status")
646
- async def get_user_status(current_user: dict = Depends(get_current_user_dependency)):
647
- """Get current user's status and statistics"""
648
- if research_mate is None:
649
- raise HTTPException(status_code=503, detail="ResearchMate not initialized")
650
-
651
- try:
652
- user_id = current_user.get("user_id")
653
-
654
- # Get user's projects
655
- projects_result = research_mate.list_projects(user_id)
656
- if not projects_result.get("success"):
657
- raise HTTPException(status_code=400, detail="Failed to get user projects")
658
-
659
- user_projects = projects_result.get("projects", [])
660
- total_papers = sum(len(p.get('papers', [])) for p in user_projects)
661
-
662
- return {
663
- "success": True,
664
- "user_id": user_id,
665
- "username": current_user.get("username"),
666
- "statistics": {
667
- "total_projects": len(user_projects),
668
- "total_papers": total_papers,
669
- "active_projects": len([p for p in user_projects if p.get('status') == 'active'])
670
- },
671
- "last_updated": datetime.now().isoformat()
672
- }
673
- except Exception as e:
674
- raise HTTPException(status_code=500, detail=str(e))
675
-
676
- # Trigger initialization endpoint (for testing)
677
- @app.post("/api/trigger-init")
678
- async def trigger_initialization():
679
- """Manually trigger ResearchMate initialization"""
680
- if not initialization_in_progress and not research_mate_initialized:
681
- asyncio.create_task(initialize_research_mate())
682
- return {"message": "Initialization triggered"}
683
- elif initialization_in_progress:
684
- return {"message": "Initialization already in progress"}
685
- else:
686
- return {"message": "Already initialized"}
687
-
688
- # Health check endpoint
689
- @app.get("/api/health")
690
- async def health_check():
691
- """Health check endpoint"""
692
- return {"status": "ok", "timestamp": datetime.now().isoformat()}
693
-
694
- # Update the existing FastAPI app to use lifespan
695
- app.router.lifespan_context = lifespan
696
-
697
- # Startup event to ensure initialization begins immediately after server starts
698
- @app.on_event("startup")
699
- async def startup_event():
700
- """Ensure initialization starts on startup"""
701
- print("🌟 Server started, ensuring ResearchMate initialization begins...")
702
- # Give the server a moment to fully start, then trigger initialization
703
- await asyncio.sleep(1)
704
- if not initialization_in_progress and not research_mate_initialized:
705
- asyncio.create_task(initialize_research_mate())
706
-
707
- # Run the application
708
- if __name__ == "__main__":
709
- import os
710
-
711
- # Hugging Face Spaces uses port 7860
712
- port = int(os.environ.get('PORT', 7860))
713
- host = "0.0.0.0"
714
-
715
- print("Starting ResearchMate on Hugging Face Spaces...")
716
- print(f"Web Interface: http://0.0.0.0:{port}")
717
- print(f"API Documentation: http://0.0.0.0:{port}/docs")
718
-
719
- uvicorn.run(
720
- "main:app",
721
- host=host,
722
- port=port,
723
- log_level="info"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
724
  )
 
1
+ import os
2
+ import sys
3
+ import json
4
+ import asyncio
5
+ from typing import Dict, List, Optional, Any
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from contextlib import asynccontextmanager
9
+
10
+ # Add the project root to Python path
11
+ sys.path.append(str(Path(__file__).parent))
12
+
13
+ from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Request, Depends
14
+ from fastapi.staticfiles import StaticFiles
15
+ from fastapi.templating import Jinja2Templates
16
+ from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, FileResponse
17
+ from fastapi.middleware.cors import CORSMiddleware
18
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
19
+ from pydantic import BaseModel, Field
20
+ import uvicorn
21
+
22
+ # Import settings and ResearchMate components
23
+ from src.components.research_assistant import ResearchMate
24
+ from src.components.citation_network import CitationNetworkAnalyzer
25
+ from src.components.auth import AuthManager
26
+
27
+ # Initialize only essential components at startup (fast components only)
28
+ auth_manager = AuthManager()
29
+ security = HTTPBearer(auto_error=False)
30
+
31
+ # Simple settings for development
32
+ class Settings:
33
+ def __init__(self):
34
+ self.server = type('ServerSettings', (), {
35
+ 'debug': False,
36
+ 'host': '0.0.0.0',
37
+ 'port': int(os.environ.get('PORT', 8000))
38
+ })()
39
+ self.security = type('SecuritySettings', (), {
40
+ 'cors_origins': ["*"],
41
+ 'cors_methods': ["*"],
42
+ 'cors_headers': ["*"]
43
+ })()
44
+
45
+ def get_static_dir(self):
46
+ return "src/static"
47
+
48
+ def get_templates_dir(self):
49
+ return "src/templates"
50
+
51
+ settings = Settings()
52
+
53
+ # Initialize ResearchMate and Citation Analyzer (will be done during loading screen)
54
+ research_mate = None
55
+ citation_analyzer = None
56
+
57
+ # Global initialization flag
58
+ research_mate_initialized = False
59
+ initialization_in_progress = False
60
+
61
+ async def initialize_research_mate():
62
+ """Initialize ResearchMate and Citation Analyzer in the background"""
63
+ global research_mate, citation_analyzer, research_mate_initialized, initialization_in_progress
64
+
65
+ if initialization_in_progress:
66
+ return
67
+
68
+ initialization_in_progress = True
69
+ print("🚀 Starting ResearchMate background initialization...")
70
+
71
+ try:
72
+ # Run initialization in thread pool to avoid blocking
73
+ import concurrent.futures
74
+ with concurrent.futures.ThreadPoolExecutor() as executor:
75
+ loop = asyncio.get_event_loop()
76
+
77
+ print("📊 Initializing Citation Network Analyzer...")
78
+ citation_analyzer = await loop.run_in_executor(executor, CitationNetworkAnalyzer)
79
+ print("✅ Citation Network Analyzer initialized!")
80
+
81
+ print("🧠 Initializing ResearchMate core...")
82
+ research_mate = await loop.run_in_executor(executor, ResearchMate)
83
+ print("✅ ResearchMate core initialized!")
84
+
85
+ research_mate_initialized = True
86
+ print("🎉 All components initialized successfully!")
87
+ except Exception as e:
88
+ print(f"❌ Failed to initialize components: {e}")
89
+ print("⚠️ Server will start but some features may not work")
90
+ research_mate = None
91
+ citation_analyzer = None
92
+ research_mate_initialized = False
93
+ finally:
94
+ initialization_in_progress = False
95
+
96
+ # Pydantic models for API
97
+ class SearchQuery(BaseModel):
98
+ query: str = Field(..., description="Search query")
99
+ max_results: int = Field(default=10, ge=1, le=50, description="Maximum number of results")
100
+
101
+ class QuestionQuery(BaseModel):
102
+ question: str = Field(..., description="Research question")
103
+
104
+ class ProjectCreate(BaseModel):
105
+ name: str = Field(..., description="Project name")
106
+ research_question: str = Field(..., description="Research question")
107
+ keywords: List[str] = Field(..., description="Keywords")
108
+
109
+ class ProjectQuery(BaseModel):
110
+ project_id: str = Field(..., description="Project ID")
111
+ question: str = Field(..., description="Question about the project")
112
+
113
+ class TrendQuery(BaseModel):
114
+ topic: str = Field(..., description="Research topic")
115
+
116
+ # Authentication models
117
+ class LoginRequest(BaseModel):
118
+ username: str = Field(..., description="Username")
119
+ password: str = Field(..., description="Password")
120
+
121
+ class RegisterRequest(BaseModel):
122
+ username: str = Field(..., description="Username")
123
+ email: str = Field(..., description="Email address")
124
+ password: str = Field(..., description="Password")
125
+
126
+ # Authentication dependency for API endpoints
127
+ async def get_current_user_dependency(request: Request, credentials: HTTPAuthorizationCredentials = Depends(security)):
128
+ user = None
129
+
130
+ # Try Authorization header first
131
+ if credentials:
132
+ user = auth_manager.verify_token(credentials.credentials)
133
+
134
+ # If no user from header, try cookie
135
+ if not user:
136
+ token = request.cookies.get('authToken')
137
+ if token:
138
+ user = auth_manager.verify_token(token)
139
+
140
+ if not user:
141
+ raise HTTPException(status_code=401, detail="Authentication required")
142
+
143
+ return user
144
+
145
+ # Authentication for web pages (checks both header and cookie)
146
+ async def get_current_user_web(request: Request):
147
+ """Get current user for web page requests (checks both Authorization header and cookies)"""
148
+ user = None
149
+
150
+ # First try Authorization header
151
+ try:
152
+ credentials = await security(request)
153
+ if credentials:
154
+ user = auth_manager.verify_token(credentials.credentials)
155
+ except:
156
+ pass
157
+
158
+ # If no user from header, try cookie
159
+ if not user:
160
+ token = request.cookies.get('authToken')
161
+ if token:
162
+ user = auth_manager.verify_token(token)
163
+
164
+ return user
165
+
166
+ # Background task to clean up expired sessions
167
+ async def cleanup_expired_sessions():
168
+ while True:
169
+ try:
170
+ expired_count = auth_manager.cleanup_expired_sessions()
171
+ if expired_count > 0:
172
+ print(f"Cleaned up {expired_count} expired sessions")
173
+ except Exception as e:
174
+ print(f"Error cleaning up sessions: {e}")
175
+
176
+ # Run cleanup every 30 minutes
177
+ await asyncio.sleep(30 * 60)
178
+
179
+ @asynccontextmanager
180
+ async def lifespan(app: FastAPI):
181
+ # Start ResearchMate initialization in background (non-blocking)
182
+ asyncio.create_task(initialize_research_mate())
183
+
184
+ # Start background cleanup task
185
+ cleanup_task = asyncio.create_task(cleanup_expired_sessions())
186
+
187
+ try:
188
+ yield
189
+ finally:
190
+ cleanup_task.cancel()
191
+ try:
192
+ await cleanup_task
193
+ except asyncio.CancelledError:
194
+ pass
195
+
196
+ # Initialize FastAPI app with lifespan
197
+ app = FastAPI(
198
+ title="ResearchMate API",
199
+ description="AI Research Assistant powered by Groq Llama 3.3 70B",
200
+ version="1.0.0",
201
+ debug=settings.server.debug,
202
+ lifespan=lifespan
203
+ )
204
+
205
+ # Add CORS middleware
206
+ app.add_middleware(
207
+ CORSMiddleware,
208
+ allow_origins=settings.security.cors_origins,
209
+ allow_credentials=True,
210
+ allow_methods=settings.security.cors_methods,
211
+ allow_headers=settings.security.cors_headers,
212
+ )
213
+
214
+ # Mount static files with cache control for development
215
+ static_dir = Path(settings.get_static_dir())
216
+ static_dir.mkdir(parents=True, exist_ok=True)
217
+
218
+ # Custom static files class to add no-cache headers for development
219
+ class NoCacheStaticFiles(StaticFiles):
220
+ def file_response(self, full_path, stat_result, scope):
221
+ response = FileResponse(
222
+ path=full_path,
223
+ stat_result=stat_result
224
+ )
225
+ # Add no-cache headers for development
226
+ response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
227
+ response.headers["Pragma"] = "no-cache"
228
+ response.headers["Expires"] = "0"
229
+ return response
230
+
231
+ app.mount("/static", NoCacheStaticFiles(directory=str(static_dir)), name="static")
232
+
233
+ # Templates
234
+ templates_dir = Path(settings.get_templates_dir())
235
+ templates_dir.mkdir(parents=True, exist_ok=True)
236
+ templates = Jinja2Templates(directory=str(templates_dir))
237
+
238
+ # Loading page route
239
+ @app.get("/loading", response_class=HTMLResponse)
240
+ async def loading_page(request: Request):
241
+ return templates.TemplateResponse("loading.html", {"request": request})
242
+
243
+ # Authentication routes
244
+ @app.post("/api/auth/register")
245
+ async def register(request: RegisterRequest):
246
+ result = auth_manager.create_user(request.username, request.email, request.password)
247
+ if result["success"]:
248
+ return {"success": True, "message": "Account created successfully"}
249
+ else:
250
+ raise HTTPException(status_code=400, detail=result["error"])
251
+
252
+ @app.post("/api/auth/login")
253
+ async def login(request: LoginRequest):
254
+ """
255
+ Enhanced login endpoint with better error handling and debugging
256
+ """
257
+ try:
258
+ print(f"🔐 Login attempt for username: {request.username}")
259
+
260
+ # Validate input
261
+ if not request.username or not request.password:
262
+ print("❌ Missing username or password")
263
+ raise HTTPException(status_code=400, detail="Username and password are required")
264
+
265
+ # Ensure admin user exists (critical for first-time setup)
266
+ admin_result = auth_manager.create_default_admin()
267
+ print(f"👤 Admin user status: {admin_result.get('message', 'Ready')}")
268
+
269
+ # Debug: Show available users
270
+ users = auth_manager.load_users()
271
+ print(f"📊 Available users: {list(users.keys())}")
272
+
273
+ # Authenticate user
274
+ result = auth_manager.authenticate_user(request.username, request.password)
275
+
276
+ if result["success"]:
277
+ print(f"✅ Login successful for: {request.username}")
278
+ return {
279
+ "success": True,
280
+ "token": result["token"],
281
+ "user_id": result["user_id"],
282
+ "username": result["username"]
283
+ }
284
+ else:
285
+ print(f"❌ Login failed for: {request.username} - {result.get('error')}")
286
+ raise HTTPException(status_code=401, detail=result["error"])
287
+
288
+ except HTTPException:
289
+ raise
290
+ except Exception as e:
291
+ print(f"💥 Login endpoint error: {e}")
292
+ raise HTTPException(status_code=500, detail="Internal server error")
293
+
294
+ @app.get("/api/auth/debug")
295
+ async def debug_auth():
296
+ """Debug authentication status - REMOVE IN PRODUCTION"""
297
+ try:
298
+ auth_manager.debug_status()
299
+ users = auth_manager.load_users()
300
+ sessions = auth_manager.load_active_sessions()
301
+
302
+ return {
303
+ "storage_mode": "memory" if auth_manager.use_memory else "file",
304
+ "users_file_exists": auth_manager.users_file.exists() if not auth_manager.use_memory else None,
305
+ "total_users": len(users),
306
+ "active_sessions": len(sessions),
307
+ "users": list(users.keys()),
308
+ "data_dir": str(auth_manager.data_dir),
309
+ "admin_exists": "admin" in users
310
+ }
311
+ except Exception as e:
312
+ return {"error": str(e)}
313
+
314
+
315
+ @app.get("/login", response_class=HTMLResponse)
316
+ async def login_page(request: Request):
317
+ # Check if ResearchMate is initialized
318
+ global research_mate_initialized
319
+ if not research_mate_initialized:
320
+ return RedirectResponse(url="/loading", status_code=302)
321
+
322
+ return templates.TemplateResponse("login.html", {"request": request})
323
+
324
+ @app.post("/api/auth/logout")
325
+ async def logout(request: Request):
326
+ # Get current user to invalidate their session
327
+ user = await get_current_user_web(request)
328
+ if user:
329
+ auth_manager.logout_user(user['user_id'])
330
+
331
+ response = JSONResponse({"success": True, "message": "Logged out successfully"})
332
+ response.delete_cookie("authToken", path="/")
333
+ return response
334
+
335
+ # Web interface routes (protected)
336
+ @app.get("/", response_class=HTMLResponse)
337
+ async def home(request: Request):
338
+ # Check if ResearchMate is initialized first
339
+ global research_mate_initialized
340
+ if not research_mate_initialized:
341
+ return RedirectResponse(url="/loading", status_code=302)
342
+
343
+ # Check if user is authenticated
344
+ user = await get_current_user_web(request)
345
+ if not user:
346
+ return RedirectResponse(url="/login", status_code=302)
347
+ return templates.TemplateResponse("index.html", {"request": request, "user": user})
348
+
349
+ @app.get("/search", response_class=HTMLResponse)
350
+ async def search_page(request: Request):
351
+ # Check if ResearchMate is initialized first
352
+ global research_mate_initialized
353
+ if not research_mate_initialized:
354
+ return RedirectResponse(url="/loading", status_code=302)
355
+
356
+ user = await get_current_user_web(request)
357
+ if not user:
358
+ return RedirectResponse(url="/login", status_code=302)
359
+ return templates.TemplateResponse("search.html", {"request": request, "user": user})
360
+
361
+ @app.get("/projects", response_class=HTMLResponse)
362
+ async def projects_page(request: Request):
363
+ user = await get_current_user_web(request)
364
+ if not user:
365
+ return RedirectResponse(url="/login", status_code=302)
366
+ return templates.TemplateResponse("projects.html", {"request": request, "user": user})
367
+
368
+ @app.get("/trends", response_class=HTMLResponse)
369
+ async def trends_page(request: Request):
370
+ user = await get_current_user_web(request)
371
+ if not user:
372
+ return RedirectResponse(url="/login", status_code=302)
373
+ return templates.TemplateResponse("trends.html", {"request": request, "user": user})
374
+
375
+ @app.get("/upload", response_class=HTMLResponse)
376
+ async def upload_page(request: Request):
377
+ user = await get_current_user_web(request)
378
+ if not user:
379
+ return RedirectResponse(url="/login", status_code=302)
380
+ return templates.TemplateResponse("upload.html", {"request": request, "user": user})
381
+
382
+ @app.get("/citation", response_class=HTMLResponse)
383
+ async def citation_page(request: Request):
384
+ try:
385
+ if citation_analyzer is None:
386
+ # If citation analyzer isn't initialized yet, show empty state
387
+ summary = {"total_papers": 0, "total_citations": 0, "networks": []}
388
+ else:
389
+ summary = citation_analyzer.get_network_summary()
390
+ return templates.TemplateResponse("citation.html", {"request": request, "summary": summary})
391
+ except Exception as e:
392
+ raise HTTPException(status_code=500, detail=str(e))
393
+
394
+ @app.get("/test-search", response_class=HTMLResponse)
395
+ async def test_search_page(request: Request):
396
+ """Simple test page for debugging search"""
397
+ with open("test_search.html", "r") as f:
398
+ content = f.read()
399
+ return HTMLResponse(content=content)
400
+
401
+ # API endpoints
402
+ @app.post("/api/search")
403
+ async def search_papers(query: SearchQuery, current_user: dict = Depends(get_current_user_dependency)):
404
+ try:
405
+ if research_mate is None:
406
+ raise HTTPException(status_code=503, detail="ResearchMate not initialized")
407
+ rm = research_mate
408
+ result = rm.search(query.query, query.max_results)
409
+ if not result.get("success"):
410
+ raise HTTPException(status_code=400, detail=result.get("error", "Search failed"))
411
+ papers = result.get("papers", [])
412
+ if papers and citation_analyzer is not None: # Only add papers if citation analyzer is ready
413
+ citation_analyzer.add_papers(papers)
414
+ return result
415
+ except Exception as e:
416
+ raise HTTPException(status_code=500, detail=str(e))
417
+
418
+ @app.post("/api/ask")
419
+ async def ask_question(question: QuestionQuery, current_user: dict = Depends(get_current_user_dependency)):
420
+ try:
421
+ if research_mate is None:
422
+ raise HTTPException(status_code=503, detail="ResearchMate not initialized")
423
+ rm = research_mate
424
+ result = rm.ask(question.question)
425
+ if not result.get("success"):
426
+ raise HTTPException(status_code=400, detail=result.get("error", "Question failed"))
427
+ return result
428
+ except Exception as e:
429
+ raise HTTPException(status_code=500, detail=str(e))
430
+
431
+ @app.post("/api/upload")
432
+ async def upload_pdf(file: UploadFile = File(...), current_user: dict = Depends(get_current_user_dependency)):
433
+ if research_mate is None:
434
+ raise HTTPException(status_code=503, detail="ResearchMate not initialized")
435
+
436
+ if not file.filename.endswith('.pdf'):
437
+ raise HTTPException(status_code=400, detail="Only PDF files are supported")
438
+
439
+ try:
440
+ # Save uploaded file
441
+ upload_dir = Path("uploads")
442
+ upload_dir.mkdir(exist_ok=True)
443
+ file_path = upload_dir / file.filename
444
+
445
+ with open(file_path, "wb") as buffer:
446
+ content = await file.read()
447
+ buffer.write(content)
448
+
449
+ # Process PDF
450
+ result = research_mate.upload_pdf(str(file_path))
451
+
452
+ # Clean up file
453
+ file_path.unlink()
454
+
455
+ if not result.get("success"):
456
+ raise HTTPException(status_code=400, detail=result.get("error", "PDF analysis failed"))
457
+
458
+ return result
459
+ except Exception as e:
460
+ raise HTTPException(status_code=500, detail=str(e))
461
+
462
+ @app.post("/api/projects")
463
+ async def create_project(project: ProjectCreate, current_user: dict = Depends(get_current_user_dependency)):
464
+ if research_mate is None:
465
+ raise HTTPException(status_code=503, detail="ResearchMate not initialized")
466
+
467
+ try:
468
+ user_id = current_user.get("user_id")
469
+ result = research_mate.create_project(project.name, project.research_question, project.keywords, user_id)
470
+ if not result.get("success"):
471
+ raise HTTPException(status_code=400, detail=result.get("error", "Project creation failed"))
472
+ return result
473
+ except Exception as e:
474
+ raise HTTPException(status_code=500, detail=str(e))
475
+
476
+ @app.get("/api/projects")
477
+ async def list_projects(current_user: dict = Depends(get_current_user_dependency)):
478
+ if research_mate is None:
479
+ raise HTTPException(status_code=503, detail="ResearchMate not initialized")
480
+
481
+ try:
482
+ user_id = current_user.get("user_id")
483
+ result = research_mate.list_projects(user_id)
484
+ if not result.get("success"):
485
+ raise HTTPException(status_code=400, detail=result.get("error", "Failed to list projects"))
486
+ return result
487
+ except Exception as e:
488
+ raise HTTPException(status_code=500, detail=str(e))
489
+
490
+ @app.get("/api/projects/{project_id}")
491
+ async def get_project(project_id: str, current_user: dict = Depends(get_current_user_dependency)):
492
+ if research_mate is None:
493
+ raise HTTPException(status_code=503, detail="ResearchMate not initialized")
494
+
495
+ try:
496
+ user_id = current_user.get("user_id")
497
+ result = research_mate.get_project(project_id, user_id)
498
+ if not result.get("success"):
499
+ raise HTTPException(status_code=404, detail=result.get("error", "Project not found"))
500
+ return result
501
+ except Exception as e:
502
+ raise HTTPException(status_code=500, detail=str(e))
503
+
504
+ @app.post("/api/projects/{project_id}/search")
505
+ async def search_project_literature(project_id: str, max_papers: int = 10, current_user: dict = Depends(get_current_user_dependency)):
506
+ if research_mate is None:
507
+ raise HTTPException(status_code=503, detail="ResearchMate not initialized")
508
+
509
+ try:
510
+ user_id = current_user.get("user_id")
511
+ result = research_mate.search_project_literature(project_id, max_papers, user_id)
512
+ if not result.get("success"):
513
+ raise HTTPException(status_code=400, detail=result.get("error", "Literature search failed"))
514
+ return result
515
+ except Exception as e:
516
+ raise HTTPException(status_code=500, detail=str(e))
517
+
518
+ @app.post("/api/projects/{project_id}/analyze")
519
+ async def analyze_project(project_id: str, current_user: dict = Depends(get_current_user_dependency)):
520
+ if research_mate is None:
521
+ raise HTTPException(status_code=503, detail="ResearchMate not initialized")
522
+
523
+ try:
524
+ user_id = current_user.get("user_id")
525
+ result = research_mate.analyze_project(project_id, user_id)
526
+ if not result.get("success"):
527
+ raise HTTPException(status_code=400, detail=result.get("error", "Project analysis failed"))
528
+ return result
529
+ except Exception as e:
530
+ raise HTTPException(status_code=500, detail=str(e))
531
+
532
+ @app.post("/api/projects/{project_id}/review")
533
+ async def generate_review(project_id: str, current_user: dict = Depends(get_current_user_dependency)):
534
+ if research_mate is None:
535
+ raise HTTPException(status_code=503, detail="ResearchMate not initialized")
536
+
537
+ try:
538
+ user_id = current_user.get("user_id")
539
+ result = research_mate.generate_review(project_id, user_id)
540
+ if not result.get("success"):
541
+ raise HTTPException(status_code=400, detail=result.get("error", "Review generation failed"))
542
+ return result
543
+ except Exception as e:
544
+ raise HTTPException(status_code=500, detail=str(e))
545
+
546
+ @app.post("/api/projects/{project_id}/ask")
547
+ async def ask_project_question(project_id: str, question: QuestionQuery):
548
+ if research_mate is None:
549
+ raise HTTPException(status_code=503, detail="ResearchMate not initialized")
550
+
551
+ try:
552
+ result = research_mate.ask_project_question(project_id, question.question)
553
+ if not result.get("success"):
554
+ raise HTTPException(status_code=400, detail=result.get("error", "Project question failed"))
555
+ return result
556
+ except Exception as e:
557
+ raise HTTPException(status_code=500, detail=str(e))
558
+
559
+
560
+
561
+ @app.post("/api/trends")
562
+ async def get_trends(trend: TrendQuery):
563
+ if research_mate is None:
564
+ raise HTTPException(status_code=503, detail="ResearchMate not initialized")
565
+
566
+ try:
567
+ result = research_mate.analyze_trends(trend.topic)
568
+ if result.get("error"):
569
+ raise HTTPException(status_code=400, detail=result.get("error", "Trend analysis failed"))
570
+ return result
571
+ except Exception as e:
572
+ raise HTTPException(status_code=500, detail=str(e))
573
+
574
+ @app.post("/api/trends/temporal")
575
+ async def get_temporal_trends(trend: TrendQuery):
576
+ """Get temporal trend analysis"""
577
+ if research_mate is None:
578
+ raise HTTPException(status_code=503, detail="ResearchMate not initialized")
579
+
580
+ try:
581
+ # Get papers for analysis
582
+ papers = research_mate.search_papers(trend.topic, 50)
583
+ if not papers:
584
+ raise HTTPException(status_code=404, detail="No papers found for temporal analysis")
585
+
586
+ # Use advanced trend monitor
587
+ result = research_mate.trend_monitor.analyze_temporal_trends(papers)
588
+ if result.get("error"):
589
+ raise HTTPException(status_code=400, detail=result.get("error"))
590
+
591
+ return {
592
+ "topic": trend.topic,
593
+ "temporal_analysis": result,
594
+ "papers_analyzed": len(papers)
595
+ }
596
+ except Exception as e:
597
+ raise HTTPException(status_code=500, detail=str(e))
598
+
599
+ @app.post("/api/trends/gaps")
600
+ async def detect_research_gaps(trend: TrendQuery):
601
+ """Detect research gaps for a topic"""
602
+ if research_mate is None:
603
+ raise HTTPException(status_code=503, detail="ResearchMate not initialized")
604
+
605
+ try:
606
+ # Get papers for gap analysis
607
+ papers = research_mate.search_papers(trend.topic, 50)
608
+ if not papers:
609
+ raise HTTPException(status_code=404, detail="No papers found for gap analysis")
610
+
611
+ # Use advanced trend monitor
612
+ result = research_mate.trend_monitor.detect_research_gaps(papers)
613
+ if result.get("error"):
614
+ raise HTTPException(status_code=400, detail=result.get("error"))
615
+
616
+ return {
617
+ "topic": trend.topic,
618
+ "gap_analysis": result,
619
+ "papers_analyzed": len(papers)
620
+ }
621
+ except Exception as e:
622
+ raise HTTPException(status_code=500, detail=str(e))
623
+
624
+ @app.get("/api/status")
625
+ async def get_status(current_user: dict = Depends(get_current_user_dependency)):
626
+ if research_mate is None:
627
+ raise HTTPException(status_code=503, detail="ResearchMate not initialized")
628
+
629
+ try:
630
+ result = research_mate.get_status()
631
+ # Ensure proper structure for frontend
632
+ if result.get('success'):
633
+ return {
634
+ 'success': True,
635
+ 'statistics': result.get('statistics', {
636
+ 'rag_documents': 0,
637
+ 'system_version': '2.0.0',
638
+ 'status_check_time': datetime.now().isoformat()
639
+ }),
640
+ 'components': result.get('components', {})
641
+ }
642
+ else:
643
+ return result
644
+ except Exception as e:
645
+ raise HTTPException(status_code=500, detail=str(e))
646
+
647
+ # Initialization status endpoint
648
+ @app.get("/api/init-status")
649
+ async def get_init_status():
650
+ """Check if ResearchMate is initialized"""
651
+ global research_mate_initialized, initialization_in_progress
652
+
653
+ if research_mate_initialized:
654
+ status = "ready"
655
+ elif initialization_in_progress:
656
+ status = "initializing"
657
+ else:
658
+ status = "not_started"
659
+
660
+ return {
661
+ "initialized": research_mate_initialized,
662
+ "in_progress": initialization_in_progress,
663
+ "timestamp": datetime.now().isoformat(),
664
+ "status": status
665
+ }
666
+
667
+ # Fast search endpoint that initializes on first call
668
+ @app.post("/api/search-fast")
669
+ async def search_papers_fast(query: SearchQuery, current_user: dict = Depends(get_current_user_dependency)):
670
+ """Fast search that shows initialization progress"""
671
+ try:
672
+ global research_mate
673
+ if research_mate is None:
674
+ # Return immediate response indicating initialization
675
+ return {
676
+ "initializing": True,
677
+ "message": "ResearchMate is initializing (this may take 30-60 seconds)...",
678
+ "query": query.query,
679
+ "estimated_time": "30-60 seconds"
680
+ }
681
+
682
+ # Use existing search
683
+ result = research_mate.search(query.query, query.max_results)
684
+ if not result.get("success"):
685
+ raise HTTPException(status_code=400, detail=result.get("error", "Search failed"))
686
+
687
+ papers = result.get("papers", [])
688
+ if papers and citation_analyzer is not None:
689
+ citation_analyzer.add_papers(papers)
690
+
691
+ return result
692
+ except Exception as e:
693
+ raise HTTPException(status_code=500, detail=str(e))
694
+
695
+ @app.get("/api/user/status")
696
+ async def get_user_status(current_user: dict = Depends(get_current_user_dependency)):
697
+ """Get current user's status and statistics"""
698
+ if research_mate is None:
699
+ raise HTTPException(status_code=503, detail="ResearchMate not initialized")
700
+
701
+ try:
702
+ user_id = current_user.get("user_id")
703
+
704
+ # Get user's projects
705
+ projects_result = research_mate.list_projects(user_id)
706
+ if not projects_result.get("success"):
707
+ raise HTTPException(status_code=400, detail="Failed to get user projects")
708
+
709
+ user_projects = projects_result.get("projects", [])
710
+ total_papers = sum(len(p.get('papers', [])) for p in user_projects)
711
+
712
+ return {
713
+ "success": True,
714
+ "user_id": user_id,
715
+ "username": current_user.get("username"),
716
+ "statistics": {
717
+ "total_projects": len(user_projects),
718
+ "total_papers": total_papers,
719
+ "active_projects": len([p for p in user_projects if p.get('status') == 'active'])
720
+ },
721
+ "last_updated": datetime.now().isoformat()
722
+ }
723
+ except Exception as e:
724
+ raise HTTPException(status_code=500, detail=str(e))
725
+
726
+ # Trigger initialization endpoint (for testing)
727
+ @app.post("/api/trigger-init")
728
+ async def trigger_initialization():
729
+ """Manually trigger ResearchMate initialization"""
730
+ if not initialization_in_progress and not research_mate_initialized:
731
+ asyncio.create_task(initialize_research_mate())
732
+ return {"message": "Initialization triggered"}
733
+ elif initialization_in_progress:
734
+ return {"message": "Initialization already in progress"}
735
+ else:
736
+ return {"message": "Already initialized"}
737
+
738
+ # Health check endpoint
739
+ @app.get("/api/health")
740
+ async def health_check():
741
+ """Health check endpoint"""
742
+ return {"status": "ok", "timestamp": datetime.now().isoformat()}
743
+
744
+ # Update the existing FastAPI app to use lifespan
745
+ app.router.lifespan_context = lifespan
746
+
747
+ # Startup event to ensure initialization begins immediately after server starts
748
+ @app.on_event("startup")
749
+ async def startup_event():
750
+ """Ensure initialization starts on startup"""
751
+ print("🌟 Server started, ensuring ResearchMate initialization begins...")
752
+ # Give the server a moment to fully start, then trigger initialization
753
+ # Debug auth on startup
754
+ print("🔐 Checking authentication setup...")
755
+ auth_manager.debug_status()
756
+
757
+ # Ensure admin user exists
758
+ admin_result = auth_manager.create_default_admin()
759
+ print(f"👤 Admin user: {admin_result.get('message', 'Ready')}")
760
+
761
+ await asyncio.sleep(1)
762
+ if not initialization_in_progress and not research_mate_initialized:
763
+ asyncio.create_task(initialize_research_mate())
764
+
765
+ # Run the application
766
+ if __name__ == "__main__":
767
+ import os
768
+
769
+ # Hugging Face Spaces uses port 7860
770
+ port = int(os.environ.get('PORT', 7860))
771
+ host = "0.0.0.0"
772
+
773
+ print("Starting ResearchMate on Hugging Face Spaces...")
774
+ print(f"Web Interface: http://0.0.0.0:{port}")
775
+ print(f"API Documentation: http://0.0.0.0:{port}/docs")
776
+
777
+ uvicorn.run(
778
+ "main:app",
779
+ host=host,
780
+ port=port,
781
+ log_level="info"
782
  )