KrishnaCosmic commited on
Commit
570cbf7
·
1 Parent(s): 1e8a2b1

Add missing API endpoints for contributor, messaging, and RAG

Browse files
Files changed (4) hide show
  1. main.py +5 -1
  2. requirements.txt +3 -1
  3. routes/__init__.py +1 -0
  4. routes/data_routes.py +451 -0
main.py CHANGED
@@ -53,12 +53,16 @@ app = FastAPI(
53
  # CORS configuration
54
  app.add_middleware(
55
  CORSMiddleware,
56
- allow_origins=os.getenv("CORS_ORIGINS", "http://localhost:3000,http://localhost:5173").split(","),
57
  allow_credentials=True,
58
  allow_methods=["*"],
59
  allow_headers=["*"],
60
  )
61
 
 
 
 
 
62
 
63
  # =============================================================================
64
  # Request Models (matching original service expectations)
 
53
  # CORS configuration
54
  app.add_middleware(
55
  CORSMiddleware,
56
+ allow_origins=os.getenv("CORS_ORIGINS", "http://localhost:3000,http://localhost:5173,https://open-triage.vercel.app,https://opentriage.onrender.com").split(","),
57
  allow_credentials=True,
58
  allow_methods=["*"],
59
  allow_headers=["*"],
60
  )
61
 
62
+ # Import and include data routes (contributor, messaging, auth)
63
+ from routes.data_routes import router as data_router
64
+ app.include_router(data_router)
65
+
66
 
67
  # =============================================================================
68
  # Request Models (matching original service expectations)
requirements.txt CHANGED
@@ -39,4 +39,6 @@ uuid>=1.30
39
 
40
  # Async utilities
41
  asyncio-throttle>=1.0.2
42
- pyspark
 
 
 
39
 
40
  # Async utilities
41
  asyncio-throttle>=1.0.2
42
+
43
+ # JWT Authentication
44
+ PyJWT>=2.8.0
routes/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Routes package for AI Engine."""
routes/data_routes.py ADDED
@@ -0,0 +1,451 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Data Routes for AI Engine
3
+
4
+ These routes handle data operations (contributor dashboard, messaging, auth)
5
+ that require MongoDB access. They are added here since the ai-engine is
6
+ what's deployed on Hugging Face Spaces.
7
+ """
8
+
9
+ import logging
10
+ from datetime import datetime, timezone
11
+ from typing import List, Optional, Dict, Any
12
+ from fastapi import APIRouter, HTTPException, Depends, Header
13
+ from pydantic import BaseModel
14
+ import jwt
15
+ import os
16
+
17
+ from config.database import db
18
+ from config.settings import settings
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ router = APIRouter()
23
+
24
+ # =============================================================================
25
+ # Auth Helpers
26
+ # =============================================================================
27
+
28
+ async def get_current_user(authorization: str = Header(None)) -> dict:
29
+ """Extract and verify JWT token from Authorization header."""
30
+ if not authorization or not authorization.startswith("Bearer "):
31
+ raise HTTPException(status_code=401, detail="Not authenticated")
32
+
33
+ token = authorization[7:] # Remove "Bearer "
34
+
35
+ try:
36
+ payload = jwt.decode(token, settings.JWT_SECRET, algorithms=["HS256"])
37
+ user_id = payload.get("user_id")
38
+
39
+ if not user_id:
40
+ raise HTTPException(status_code=401, detail="Invalid token")
41
+
42
+ # Fetch user from database
43
+ user = await db.users.find_one({"id": user_id}, {"_id": 0})
44
+ if not user:
45
+ raise HTTPException(status_code=401, detail="User not found")
46
+
47
+ return user
48
+ except jwt.ExpiredSignatureError:
49
+ raise HTTPException(status_code=401, detail="Token expired")
50
+ except jwt.InvalidTokenError:
51
+ raise HTTPException(status_code=401, detail="Invalid token")
52
+
53
+
54
+ def create_jwt_token(user_id: str, role: str = None) -> str:
55
+ """Create a JWT token for a user."""
56
+ import time
57
+ payload = {
58
+ "user_id": user_id,
59
+ "role": role,
60
+ "exp": int(time.time()) + 30 * 24 * 60 * 60 # 30 days
61
+ }
62
+ return jwt.encode(payload, settings.JWT_SECRET, algorithm="HS256")
63
+
64
+
65
+ # =============================================================================
66
+ # Request Models
67
+ # =============================================================================
68
+
69
+ class SelectRoleRequest(BaseModel):
70
+ role: str
71
+
72
+
73
+ class SendMessageRequest(BaseModel):
74
+ receiver_id: str
75
+ content: str
76
+
77
+
78
+ # =============================================================================
79
+ # Auth Routes
80
+ # =============================================================================
81
+
82
+ @router.get("/api/auth/me")
83
+ async def get_current_user_info(user: dict = Depends(get_current_user)):
84
+ """Get current authenticated user information."""
85
+ return {
86
+ "id": user.get("id"),
87
+ "username": user.get("username"),
88
+ "avatarUrl": user.get("avatarUrl"),
89
+ "role": user.get("role"),
90
+ "githubId": user.get("githubId"),
91
+ }
92
+
93
+
94
+ @router.post("/api/auth/select-role")
95
+ async def select_role(request: SelectRoleRequest, user: dict = Depends(get_current_user)):
96
+ """Select user role (MAINTAINER or CONTRIBUTOR)."""
97
+ role = request.role.upper()
98
+ if role not in ["MAINTAINER", "CONTRIBUTOR"]:
99
+ raise HTTPException(status_code=400, detail="Invalid role. Must be MAINTAINER or CONTRIBUTOR")
100
+
101
+ # Update user role in database
102
+ await db.users.update_one(
103
+ {"id": user["id"]},
104
+ {"$set": {"role": role, "updatedAt": datetime.now(timezone.utc).isoformat()}}
105
+ )
106
+
107
+ # Generate new token with updated role
108
+ new_token = create_jwt_token(user["id"], role)
109
+
110
+ return {
111
+ "success": True,
112
+ "role": role,
113
+ "token": new_token,
114
+ }
115
+
116
+
117
+ # =============================================================================
118
+ # Contributor Routes
119
+ # =============================================================================
120
+
121
+ @router.get("/api/contributor/my-issues")
122
+ async def get_my_issues(
123
+ page: int = 1,
124
+ limit: int = 10,
125
+ user: dict = Depends(get_current_user)
126
+ ):
127
+ """Get paginated issues and PRs created by the contributor."""
128
+ page = max(1, page)
129
+ limit = min(max(1, limit), 50)
130
+ skip = (page - 1) * limit
131
+
132
+ # Get total count
133
+ total = await db.issues.count_documents({"authorName": user["username"]})
134
+
135
+ # Get paginated issues
136
+ issues = await db.issues.find(
137
+ {"authorName": user["username"]},
138
+ {"_id": 0}
139
+ ).sort("createdAt", -1).skip(skip).limit(limit).to_list(limit)
140
+
141
+ # Enrich with triage data
142
+ for issue in issues:
143
+ triage = await db.triage_data.find_one({"issueId": issue.get("id")}, {"_id": 0})
144
+ issue["triage"] = triage
145
+
146
+ total_pages = (total + limit - 1) // limit
147
+
148
+ return {
149
+ "items": issues,
150
+ "total": total,
151
+ "page": page,
152
+ "pages": total_pages,
153
+ "limit": limit
154
+ }
155
+
156
+
157
+ @router.get("/api/contributor/dashboard-summary")
158
+ async def get_contributor_dashboard_summary(user: dict = Depends(get_current_user)):
159
+ """Get dashboard summary statistics for contributor."""
160
+ all_items = await db.issues.find(
161
+ {"authorName": user["username"]},
162
+ {"_id": 0}
163
+ ).to_list(1000)
164
+
165
+ total_contributions = len(all_items)
166
+ prs = [item for item in all_items if item.get("isPR")]
167
+ issues = [item for item in all_items if not item.get("isPR")]
168
+
169
+ open_prs = len([pr for pr in prs if pr.get("state") == "open"])
170
+ merged_prs = len([pr for pr in prs if pr.get("state") == "closed"])
171
+
172
+ open_issues = len([issue for issue in issues if issue.get("state") == "open"])
173
+ closed_issues = len([issue for issue in issues if issue.get("state") == "closed"])
174
+
175
+ unique_repos = len(set(item.get("repoName", "") for item in all_items if item.get("repoName")))
176
+
177
+ return {
178
+ "totalContributions": total_contributions,
179
+ "totalPRs": len(prs),
180
+ "openPRs": open_prs,
181
+ "mergedPRs": merged_prs,
182
+ "totalIssues": len(issues),
183
+ "openIssues": open_issues,
184
+ "closedIssues": closed_issues,
185
+ "repositoriesContributed": unique_repos
186
+ }
187
+
188
+
189
+ @router.get("/api/contributor/issues/{issue_id}/comments")
190
+ async def get_issue_comments(issue_id: str, user: dict = Depends(get_current_user)):
191
+ """Get comments for a specific issue from GitHub."""
192
+ from services.github_service import github_service
193
+
194
+ # Find the issue
195
+ issue = await db.issues.find_one({"id": issue_id}, {"_id": 0})
196
+ if not issue:
197
+ raise HTTPException(status_code=404, detail="Issue not found")
198
+
199
+ if not issue.get("owner") or not issue.get("repo") or not issue.get("number"):
200
+ raise HTTPException(status_code=400, detail="Issue missing GitHub metadata")
201
+
202
+ # Get user's GitHub token
203
+ user_doc = await db.users.find_one({"id": user["id"]}, {"_id": 0})
204
+ github_token = user_doc.get("githubAccessToken") if user_doc else None
205
+
206
+ if not github_token:
207
+ # Return empty comments if no token
208
+ return {"issueId": issue_id, "comments": []}
209
+
210
+ try:
211
+ comments = await github_service.fetch_issue_comments(
212
+ github_access_token=github_token,
213
+ owner=issue["owner"],
214
+ repo=issue["repo"],
215
+ issue_number=issue["number"]
216
+ )
217
+ return {"issueId": issue_id, "comments": comments}
218
+ except Exception as e:
219
+ logger.error(f"Error fetching comments: {e}")
220
+ return {"issueId": issue_id, "comments": [], "error": str(e)}
221
+
222
+
223
+ @router.post("/api/contributor/claim-issue")
224
+ async def claim_issue(issueId: str = None, user: dict = Depends(get_current_user)):
225
+ """Claim an issue to work on."""
226
+ # Stub implementation
227
+ return {
228
+ "message": "Issue claim registered",
229
+ "issueId": issueId,
230
+ "claimedAt": datetime.now(timezone.utc).isoformat()
231
+ }
232
+
233
+
234
+ @router.get("/api/contributor/my-claimed-issues")
235
+ async def get_my_claimed_issues(user: dict = Depends(get_current_user)):
236
+ """Get all issues claimed by the current user."""
237
+ return {"claims": [], "count": 0}
238
+
239
+
240
+ # =============================================================================
241
+ # Messaging Routes
242
+ # =============================================================================
243
+
244
+ @router.get("/api/messaging/unread-count")
245
+ async def get_unread_count(user: dict = Depends(get_current_user)):
246
+ """Get count of unread messages for the current user."""
247
+ user_ids = [user.get("id"), user.get("username")]
248
+ user_ids = [i for i in user_ids if i]
249
+
250
+ count = await db.messages.count_documents({
251
+ "receiver_id": {"$in": user_ids},
252
+ "read": False
253
+ })
254
+
255
+ return {"count": count}
256
+
257
+
258
+ @router.get("/api/messaging/conversations")
259
+ async def get_conversations(user: dict = Depends(get_current_user)):
260
+ """Get list of all conversation contacts with last message preview."""
261
+ user_ids = [user.get("id"), user.get("username")]
262
+ user_ids = [i for i in user_ids if i]
263
+
264
+ # Get all unique users we've messaged with
265
+ pipeline = [
266
+ {
267
+ "$match": {
268
+ "$or": [
269
+ {"sender_id": {"$in": user_ids}},
270
+ {"receiver_id": {"$in": user_ids}}
271
+ ]
272
+ }
273
+ },
274
+ {"$sort": {"timestamp": -1}},
275
+ {
276
+ "$group": {
277
+ "_id": {
278
+ "$cond": [
279
+ {"$in": ["$sender_id", user_ids]},
280
+ "$receiver_id",
281
+ "$sender_id"
282
+ ]
283
+ },
284
+ "last_message": {"$first": "$content"},
285
+ "last_timestamp": {"$first": "$timestamp"},
286
+ "unread_count": {
287
+ "$sum": {
288
+ "$cond": [
289
+ {"$and": [
290
+ {"$in": ["$receiver_id", user_ids]},
291
+ {"$eq": ["$read", False]}
292
+ ]},
293
+ 1, 0
294
+ ]
295
+ }
296
+ }
297
+ }
298
+ },
299
+ {"$sort": {"last_timestamp": -1}}
300
+ ]
301
+
302
+ results = await db.messages.aggregate(pipeline).to_list(length=50)
303
+
304
+ conversations = []
305
+ for r in results:
306
+ other_user_id = r["_id"]
307
+ user_info = await db.users.find_one(
308
+ {"$or": [{"id": other_user_id}, {"username": other_user_id}]},
309
+ {"_id": 0, "id": 1, "username": 1, "avatarUrl": 1}
310
+ )
311
+
312
+ conversations.append({
313
+ "user_id": user_info.get("id") if user_info else other_user_id,
314
+ "username": user_info.get("username") if user_info else str(other_user_id),
315
+ "avatar_url": user_info.get("avatarUrl") if user_info else None,
316
+ "last_message": (r.get("last_message", "") or "")[:50],
317
+ "last_timestamp": r.get("last_timestamp"),
318
+ "unread_count": r.get("unread_count", 0)
319
+ })
320
+
321
+ return {"conversations": conversations}
322
+
323
+
324
+ @router.get("/api/messaging/history/{other_user_id}")
325
+ async def get_chat_history(other_user_id: str, user: dict = Depends(get_current_user)):
326
+ """Get chat history with a specific user."""
327
+ user_ids = [user.get("id"), user.get("username")]
328
+ user_ids = [i for i in user_ids if i]
329
+
330
+ other_ids = [other_user_id]
331
+ other_user = await db.users.find_one(
332
+ {"$or": [{"id": other_user_id}, {"username": other_user_id}]},
333
+ {"_id": 0, "id": 1, "username": 1}
334
+ )
335
+ if other_user:
336
+ other_ids.extend([other_user.get("id"), other_user.get("username")])
337
+ other_ids = [i for i in set(other_ids) if i]
338
+
339
+ messages = await db.messages.find({
340
+ "$or": [
341
+ {"sender_id": {"$in": user_ids}, "receiver_id": {"$in": other_ids}},
342
+ {"sender_id": {"$in": other_ids}, "receiver_id": {"$in": user_ids}}
343
+ ]
344
+ }, {"_id": 0}).sort("timestamp", 1).to_list(length=1000)
345
+
346
+ return messages
347
+
348
+
349
+ @router.post("/api/messaging/send")
350
+ async def send_message(request: SendMessageRequest, user: dict = Depends(get_current_user)):
351
+ """Send a message to another user."""
352
+ import uuid
353
+
354
+ message = {
355
+ "id": str(uuid.uuid4()),
356
+ "sender_id": user["id"],
357
+ "receiver_id": request.receiver_id,
358
+ "content": request.content,
359
+ "timestamp": datetime.now(timezone.utc).isoformat(),
360
+ "read": False
361
+ }
362
+
363
+ await db.messages.insert_one(message)
364
+ del message["_id"] if "_id" in message else None
365
+
366
+ return message
367
+
368
+
369
+ @router.post("/api/messaging/mark-read/{other_user_id}")
370
+ async def mark_messages_read(other_user_id: str, user: dict = Depends(get_current_user)):
371
+ """Mark all messages from a specific user as read."""
372
+ user_ids = [user.get("id"), user.get("username")]
373
+ user_ids = [i for i in user_ids if i]
374
+
375
+ other_ids = [other_user_id]
376
+ other_user = await db.users.find_one(
377
+ {"$or": [{"id": other_user_id}, {"username": other_user_id}]}
378
+ )
379
+ if other_user:
380
+ other_ids.extend([other_user.get("id"), other_user.get("username")])
381
+ other_ids = [i for i in set(other_ids) if i]
382
+
383
+ result = await db.messages.update_many(
384
+ {
385
+ "sender_id": {"$in": other_ids},
386
+ "receiver_id": {"$in": user_ids},
387
+ "read": False
388
+ },
389
+ {"$set": {"read": True}}
390
+ )
391
+
392
+ return {"marked_read": result.modified_count}
393
+
394
+
395
+ # =============================================================================
396
+ # Maintainer Routes
397
+ # =============================================================================
398
+
399
+ @router.get("/api/maintainer/dashboard-summary")
400
+ async def get_maintainer_dashboard_summary(user: dict = Depends(get_current_user)):
401
+ """Get dashboard summary for maintainers."""
402
+ if user.get("role", "").upper() not in ["MAINTAINER"]:
403
+ raise HTTPException(status_code=403, detail="Maintainer access required")
404
+
405
+ # Get user's repositories
406
+ repos = await db.repositories.find({"userId": user["id"]}, {"_id": 0}).to_list(100)
407
+ repo_ids = [r.get("id") for r in repos]
408
+
409
+ if not repo_ids:
410
+ return {
411
+ "openIssues": 0,
412
+ "openPRs": 0,
413
+ "triaged": 0,
414
+ "untriaged": 0,
415
+ "repositoriesCount": 0,
416
+ "repositories": []
417
+ }
418
+
419
+ # Count issues
420
+ open_issues = await db.issues.count_documents({
421
+ "repoId": {"$in": repo_ids},
422
+ "state": "open",
423
+ "isPR": False
424
+ })
425
+
426
+ open_prs = await db.issues.count_documents({
427
+ "repoId": {"$in": repo_ids},
428
+ "state": "open",
429
+ "isPR": True
430
+ })
431
+
432
+ # Count triaged
433
+ all_open_issues = await db.issues.find({
434
+ "repoId": {"$in": repo_ids},
435
+ "state": "open"
436
+ }, {"id": 1}).to_list(1000)
437
+
438
+ triaged = 0
439
+ for issue in all_open_issues:
440
+ has_triage = await db.triage_data.find_one({"issueId": issue["id"]})
441
+ if has_triage:
442
+ triaged += 1
443
+
444
+ return {
445
+ "openIssues": open_issues,
446
+ "openPRs": open_prs,
447
+ "triaged": triaged,
448
+ "untriaged": open_issues - triaged,
449
+ "repositoriesCount": len(repos),
450
+ "repositories": repos
451
+ }