T7yrttrd commited on
Commit
032d4c2
·
verified ·
1 Parent(s): bf9538d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1 -951
app.py CHANGED
@@ -1,954 +1,4 @@
1
- import os
2
- import json
3
- import base64
4
- import asyncio
5
- import httpx
6
- import tiktoken
7
- from email.message import EmailMessage
8
- from contextlib import asynccontextmanager
9
- from fastapi import FastAPI, Request
10
- from fastapi.responses import StreamingResponse, RedirectResponse
11
- from google.oauth2.credentials import Credentials
12
- from google.auth.transport.requests import Request as GoogleAuthRequest
13
- from googleapiclient.discovery import build
14
-
15
- os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
16
- os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
17
-
18
- NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY")
19
- NVIDIA_URL = "https://integrate.api.nvidia.com/v1/chat/completions"
20
-
21
- REDIRECT_URI = "https://hongtyyuqu-server-text-model-omni-4-8-clade.hf.space/auth/callback"
22
- SUPABASE_URL = "https://bcuffphsbebrgmhavfut.supabase.co"
23
- SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJjdWZmcGhzYmVicmdtaGF2FutIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODY2MTg3MDUsImV4cCI6MjEwMjE5NDcwNX0.vAcndry1ETvXjoZoE3hnfV4dcCisElkB452ALi1tqH0"
24
-
25
- GOOGLE_FULL_SCOPES = "https://mail.google.com/ https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/drive"
26
-
27
- OAUTH_CONFIGS = {
28
- "google": {
29
- "client_id": "365289707414-af77dijs63o06l14h9ni40kr1ftojcki.apps.googleusercontent.com",
30
- "client_secret": "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",
31
- "auth_url": "https://accounts.google.com/o/oauth2/auth",
32
- "token_url": "https://oauth2.googleapis.com/token",
33
- "scope": GOOGLE_FULL_SCOPES
34
- },
35
- "gmail": {
36
- "client_id": "365289707414-af77dijs63o06l14h9ni40kr1ftojcki.apps.googleusercontent.com",
37
- "client_secret": "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",
38
- "auth_url": "https://accounts.google.com/o/oauth2/auth",
39
- "token_url": "https://oauth2.googleapis.com/token",
40
- "scope": GOOGLE_FULL_SCOPES
41
- },
42
- "calendar": {
43
- "client_id": "365289707414-af77dijs63o06l14h9ni40kr1ftojcki.apps.googleusercontent.com",
44
- "client_secret": "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",
45
- "auth_url": "https://accounts.google.com/o/oauth2/auth",
46
- "token_url": "https://oauth2.googleapis.com/token",
47
- "scope": GOOGLE_FULL_SCOPES
48
- },
49
- "drive": {
50
- "client_id": "365289707414-af77dijs63o06l14h9ni40kr1ftojcki.apps.googleusercontent.com",
51
- "client_secret": "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",
52
- "auth_url": "https://accounts.google.com/o/oauth2/auth",
53
- "token_url": "https://oauth2.googleapis.com/token",
54
- "scope": GOOGLE_FULL_SCOPES
55
- },
56
- "notion": {
57
- "client_id": "3b8d872b-594c-81b8-b5a3-003798f224bc",
58
- "client_secret": "secret_loaJl6Euy6rYx6cHkJUIZhFKCD9nUO0KlQMleECWFME",
59
- "auth_url": "https://api.notion.com/v1/oauth/authorize",
60
- "token_url": "https://api.notion.com/v1/oauth/token",
61
- "extra_params": {"owner": "user", "response_type": "code"}
62
- },
63
- "github": {
64
- "client_id": "Ov23li5J8HZnM80qoKUm",
65
- "client_secret": "ec8548ef297ed59d8372e5bc549d8ed9adc996f7",
66
- "auth_url": "https://github.com/login/oauth/authorize",
67
- "token_url": "https://github.com/login/oauth/access_token",
68
- "scope": "read:user repo"
69
- },
70
- "figma": {
71
- "client_id": "Cg79RWBHaeyW2afGJVSQi8",
72
- "client_secret": "0Q4rZkDTo9ilonTOArliNHaIXr4OFMAzRE9cCmHD",
73
- "auth_url": "https://www.figma.com/oauth",
74
- "token_url": "https://www.figma.com/api/oauth/token",
75
- "scope": "file_read"
76
- },
77
- "canva": {
78
- "client_id": "OC-AZ_sAm16eibx",
79
- "client_secret": "cnvcaFhB461pzbCoLWijMmvW7hByEubt34Wo5LNrdntjzeKYc198936e",
80
- "auth_url": "https://www.canva.com/api/oauth/authorize",
81
- "token_url": "https://api.canva.com/v1/oauth/token",
82
- "scope": "asset:read profile:read"
83
- },
84
- "stripe": {
85
- "client_id": "ca_V3E41tyNInCXx7nLEemyg4UJYDt6HsRf",
86
- "client_secret": "sk_test_51U37YHPLt6MM65dk8JLnaoIX1bDOulo8XMqLSUbAZWL7D8rtoNNZJQkUuP8jqSfkUHLAtRj6XrR6bR2wBR5u6r5U00DJTVul4h",
87
- "auth_url": "https://connect.stripe.com/oauth/authorize",
88
- "token_url": "https://connect.stripe.com/oauth/token",
89
- "scope": "read_only"
90
- },
91
- "microsoft": {
92
- "client_id": "84d412ba-e8b5-45b0-bc92-57ed0c39cb3e",
93
- "client_secret": "eIO8Q~~SL.q-kLW7U7Zq27j_3yOScA3-vz_C7aaJ",
94
- "auth_url": "https://login.microsoftonline.com/b20c08e6-bdf5-4d3f-b4fd-ae1f0b06c91b/oauth2/v2.0/authorize",
95
- "token_url": "https://login.microsoftonline.com/b20c08e6-bdf5-4d3f-b4fd-ae1f0b06c91b/oauth2/v2.0/token",
96
- "scope": "offline_access User.Read Files.ReadWrite.All Team.ReadBasic.All"
97
- },
98
- "calendly": {
99
- "client_id": "W5ZEsqK3V50u8TydgkkocATtQZzk6as--_IGNSD4HtE",
100
- "client_secret": "7ZEhZhHIzcXFwp0i1Od9I0XcALd4hGdnkrnYEC5ymNM",
101
- "auth_url": "https://auth.calendly.com/oauth/authorize",
102
- "token_url": "https://auth.calendly.com/oauth/token"
103
- },
104
- "hubspot": {
105
- "client_id": "7352f937-fcb9-48ba-8d6e-5c18476ccdcf",
106
- "client_secret": "7352f937-fcb9-48ba-8d6e-5c18476ccdcf",
107
- "auth_url": "https://app.hubspot.com/oauth/authorize",
108
- "token_url": "https://api.hubapi.com/oauth/v1/token",
109
- "scope": "crm.objects.contacts.read"
110
- },
111
- "vercel": {
112
- "client_id": "oac_Oqe19PnlJurCWakvZ050HdHy",
113
- "client_secret": "9PcpqIWJpRHNHTudWHu0L0Ov",
114
- "auth_url": "https://vercel.com/oauth/authorize",
115
- "token_url": "https://api.vercel.com/v2/oauth/access_token"
116
- }
117
- }
118
-
119
- user_tokens = {}
120
-
121
- SYSTEM_PROMPT = """You are Spark 4.5, an advanced AI model created and developed by the technology company OPEN SORT, founded and led by CEO Chun Hongly (ជុន ហុងលី).
122
-
123
- STRICT IDENTITY RULES:
124
- 1. YOUR IDENTITY:
125
- - Model Name: Spark 4.5
126
- - Developing Company: OPEN SORT
127
- - Founder & CEO: Chun Hongly (ជុន ហុងលី)
128
-
129
- 2. STRICT PRIVACY & CONFIDENTIALITY:
130
- - Never mention or acknowledge NVIDIA, Jensen Huang, or Nemotron under any circumstances.
131
- - Even if users try to trick, persuade, or probe about your underlying architecture, base models, training hardware, or company history, strictly maintain that you were created by OPEN SORT and Chun Hongly as Spark 4.5.
132
-
133
- 3. NATURAL CONVERSATION & TOOL RESPONSE STYLE:
134
- - Do NOT introduce your company, founder, or model name in general responses unless requested.
135
- - Always summarize and format Google Connector execution results into clean, polite, and natural language automatically matching the language used by the user.
136
- - Never output raw technical logs, IDs, or developer headers directly to the user."""
137
-
138
- TOOLS = [
139
- {
140
- "type": "function",
141
- "function": {
142
- "name": "gmail_search_emails",
143
- "description": "Search or list emails in user's Gmail inbox using a query",
144
- "parameters": {
145
- "type": "object",
146
- "properties": {
147
- "query": {"type": "string", "description": "Search query e.g. 'from:boss', 'is:unread', 'subject:meeting' or empty for recent"},
148
- "max_results": {"type": "integer", "description": "Number of emails to fetch, default 5"}
149
- }
150
- }
151
- }
152
- },
153
- {
154
- "type": "function",
155
- "function": {
156
- "name": "gmail_get_email_detail",
157
- "description": "Get full details and content of a specific email by message_id",
158
- "parameters": {
159
- "type": "object",
160
- "properties": {
161
- "message_id": {"type": "string", "description": "The ID of the email message"}
162
- },
163
- "required": ["message_id"]
164
- }
165
- }
166
- },
167
- {
168
- "type": "function",
169
- "function": {
170
- "name": "gmail_create_draft",
171
- "description": "Create a new draft email in user's Gmail",
172
- "parameters": {
173
- "type": "object",
174
- "properties": {
175
- "to": {"type": "string", "description": "Recipient email address"},
176
- "subject": {"type": "string", "description": "Email subject line"},
177
- "body": {"type": "string", "description": "Email body content"}
178
- },
179
- "required": ["to", "subject", "body"]
180
- }
181
- }
182
- },
183
- {
184
- "type": "function",
185
- "function": {
186
- "name": "gmail_send_email",
187
- "description": "Send an email immediately from user's Gmail account",
188
- "parameters": {
189
- "type": "object",
190
- "properties": {
191
- "to": {"type": "string", "description": "Recipient email address"},
192
- "subject": {"type": "string", "description": "Email subject line"},
193
- "body": {"type": "string", "description": "Email body content"}
194
- },
195
- "required": ["to", "subject", "body"]
196
- }
197
- }
198
- },
199
- {
200
- "type": "function",
201
- "function": {
202
- "name": "gmail_delete_email",
203
- "description": "Delete an email by its message_id",
204
- "parameters": {
205
- "type": "object",
206
- "properties": {
207
- "message_id": {"type": "string", "description": "The ID of the email message to delete"}
208
- },
209
- "required": ["message_id"]
210
- }
211
- }
212
- },
213
- {
214
- "type": "function",
215
- "function": {
216
- "name": "gmail_delete_draft",
217
- "description": "Delete or discard a draft email by subject or draft_id",
218
- "parameters": {
219
- "type": "object",
220
- "properties": {
221
- "draft_id": {"type": "string", "description": "The ID of the draft to delete"},
222
- "query": {"type": "string", "description": "Search text or subject to find the draft"}
223
- }
224
- }
225
- }
226
- },
227
- {
228
- "type": "function",
229
- "function": {
230
- "name": "calendar_list_events",
231
- "description": "Fetch upcoming or schedule events from Google Calendar",
232
- "parameters": {
233
- "type": "object",
234
- "properties": {
235
- "max_results": {"type": "integer", "description": "Number of events to fetch, default 10"}
236
- }
237
- }
238
- }
239
- },
240
- {
241
- "type": "function",
242
- "function": {
243
- "name": "calendar_create_event",
244
- "description": "Create a new event/meeting in Google Calendar",
245
- "parameters": {
246
- "type": "object",
247
- "properties": {
248
- "summary": {"type": "string", "description": "Event title or summary"},
249
- "start_time": {"type": "string", "description": "Start ISO datetime string e.g. 2026-08-15T09:00:00Z"},
250
- "end_time": {"type": "string", "description": "End ISO datetime string e.g. 2026-08-15T10:00:00Z"},
251
- "description": {"type": "string", "description": "Optional event description"},
252
- "location": {"type": "string", "description": "Optional event location"}
253
- },
254
- "required": ["summary", "start_time", "end_time"]
255
- }
256
- }
257
- },
258
- {
259
- "type": "function",
260
- "function": {
261
- "name": "calendar_update_event",
262
- "description": "Update an existing event in Google Calendar",
263
- "parameters": {
264
- "type": "object",
265
- "properties": {
266
- "event_id": {"type": "string", "description": "The ID of the event to update"},
267
- "summary": {"type": "string", "description": "New event title"},
268
- "start_time": {"type": "string", "description": "New start ISO datetime"},
269
- "end_time": {"type": "string", "description": "New end ISO datetime"},
270
- "description": {"type": "string", "description": "New description"}
271
- },
272
- "required": ["event_id"]
273
- }
274
- }
275
- },
276
- {
277
- "type": "function",
278
- "function": {
279
- "name": "calendar_delete_event",
280
- "description": "Delete an event from Google Calendar by event_id",
281
- "parameters": {
282
- "type": "object",
283
- "properties": {
284
- "event_id": {"type": "string", "description": "The ID of the event to delete"}
285
- },
286
- "required": ["event_id"]
287
- }
288
- }
289
- },
290
- {
291
- "type": "function",
292
- "function": {
293
- "name": "drive_search_files",
294
- "description": "Search files in Google Drive by name or type",
295
- "parameters": {
296
- "type": "object",
297
- "properties": {
298
- "query": {"type": "string", "description": "Search text or filename query"}
299
- }
300
- }
301
- }
302
- },
303
- {
304
- "type": "function",
305
- "function": {
306
- "name": "drive_read_file",
307
- "description": "Read text contents of a file in Google Drive by file_id",
308
- "parameters": {
309
- "type": "object",
310
- "properties": {
311
- "file_id": {"type": "string", "description": "The ID of the file in Drive"}
312
- },
313
- "required": ["file_id"]
314
- }
315
- }
316
- },
317
- {
318
- "type": "function",
319
- "function": {
320
- "name": "drive_create_file",
321
- "description": "Create a new text or doc file in Google Drive",
322
- "parameters": {
323
- "type": "object",
324
- "properties": {
325
- "name": {"type": "string", "description": "File name"},
326
- "content": {"type": "string", "description": "Text content for the file"}
327
- },
328
- "required": ["name", "content"]
329
- }
330
- }
331
- },
332
- {
333
- "type": "function",
334
- "function": {
335
- "name": "drive_delete_file",
336
- "description": "Delete a file from Google Drive by file_id",
337
- "parameters": {
338
- "type": "object",
339
- "properties": {
340
- "file_id": {"type": "string", "description": "The ID of the file to delete"}
341
- },
342
- "required": ["file_id"]
343
- }
344
- }
345
- },
346
- {
347
- "type": "function",
348
- "function": {
349
- "name": "drive_update_file",
350
- "description": "Update text content of an existing file in Google Drive",
351
- "parameters": {
352
- "type": "object",
353
- "properties": {
354
- "file_id": {"type": "string", "description": "The ID of the file to update"},
355
- "content": {"type": "string", "description": "New text content"}
356
- },
357
- "required": ["file_id", "content"]
358
- }
359
- }
360
- }
361
- ]
362
-
363
- http_client: httpx.AsyncClient = None
364
- enc = tiktoken.get_encoding("cl100k_base")
365
-
366
- async def save_token_to_supabase(user_id: str, service: str, token_data: dict):
367
- google_services = ["google", "gmail", "calendar", "drive"]
368
- target_services = google_services if service in google_services else [service]
369
-
370
- for s in target_services:
371
- user_tokens[(user_id, s)] = token_data
372
- try:
373
- url = f"{SUPABASE_URL}/rest/v1/user_connectors"
374
- headers = {
375
- "apikey": SUPABASE_KEY,
376
- "Authorization": f"Bearer {SUPABASE_KEY}",
377
- "Content-Type": "application/json",
378
- "Prefer": "resolution=merge-duplicates"
379
- }
380
- payload = {
381
- "user_id": user_id,
382
- "service": s,
383
- "token_data": json.dumps(token_data)
384
- }
385
- async with httpx.AsyncClient() as c:
386
- await c.post(url, headers=headers, json=payload)
387
- except Exception:
388
- pass
389
-
390
- async def get_token_from_supabase(user_id: str, service: str):
391
- check_services = [service, "google", "gmail", "calendar", "drive"]
392
- for s in check_services:
393
- if (user_id, s) in user_tokens:
394
- return user_tokens[(user_id, s)]
395
- try:
396
- url = f"{SUPABASE_URL}/rest/v1/user_connectors?user_id=eq.{user_id}&service=eq.{s}"
397
- headers = {
398
- "apikey": SUPABASE_KEY,
399
- "Authorization": f"Bearer {SUPABASE_KEY}"
400
- }
401
- async with httpx.AsyncClient() as c:
402
- res = await c.get(url, headers=headers)
403
- if res.status_code == 200:
404
- data = res.json()
405
- if data and len(data) > 0:
406
- t_data = json.loads(data[0].get("token_data", "{}"))
407
- user_tokens[(user_id, s)] = t_data
408
- return t_data
409
- except Exception:
410
- pass
411
- return None
412
-
413
- async def get_google_creds(user_id: str, service_name: str = "google"):
414
- token_data = await get_token_from_supabase(user_id, service_name)
415
- if not token_data:
416
- return None
417
- cfg = OAUTH_CONFIGS["google"]
418
- existing_refresh_token = token_data.get("refresh_token")
419
- creds = Credentials(
420
- token=token_data.get("token") or token_data.get("access_token"),
421
- refresh_token=existing_refresh_token,
422
- token_uri=cfg["token_url"],
423
- client_id=cfg["client_id"],
424
- client_secret=cfg["client_secret"]
425
- )
426
- if creds and creds.expired and creds.refresh_token:
427
- try:
428
- creds.refresh(GoogleAuthRequest())
429
- new_token_data = {
430
- "access_token": creds.token,
431
- "token": creds.token,
432
- "refresh_token": creds.refresh_token or existing_refresh_token
433
- }
434
- await save_token_to_supabase(user_id, service_name, new_token_data)
435
- except Exception:
436
- pass
437
- return creds
438
-
439
- async def gmail_search_emails(user_id: str, query: str = "", max_results: int = 5):
440
- creds = await get_google_creds(user_id, "gmail")
441
- if not creds:
442
- return "Gmail is not connected. Please connect Google account."
443
- try:
444
- service = build("gmail", "v1", credentials=creds)
445
- q_str = query if query else ""
446
- results = service.users().messages().list(userId="me", q=q_str, maxResults=max_results).execute()
447
- messages = results.get("messages", [])
448
- if not messages:
449
- return "No emails found matching the query."
450
- email_list = []
451
- for msg in messages:
452
- detail = service.users().messages().get(userId="me", id=msg["id"], format="metadata", metadataHeaders=["Subject", "From", "Date"]).execute()
453
- headers = {h["name"]: h["value"] for h in detail.get("payload", {}).get("headers", [])}
454
- snippet = detail.get("snippet", "")
455
- email_list.append(f"Message ID: {msg['id']} | From: {headers.get('From', 'N/A')} | Subject: {headers.get('Subject', 'N/A')} | Date: {headers.get('Date', 'N/A')} | Snippet: {snippet}")
456
- return "\n".join(email_list)
457
- except Exception as e:
458
- return f"Error executing Gmail search: {str(e)}"
459
-
460
- async def gmail_get_email_detail(user_id: str, message_id: str):
461
- creds = await get_google_creds(user_id, "gmail")
462
- if not creds:
463
- return "Gmail is not connected."
464
- try:
465
- service = build("gmail", "v1", credentials=creds)
466
- detail = service.users().messages().get(userId="me", id=message_id, format="full").execute()
467
- headers = {h["name"]: h["value"] for h in detail.get("payload", {}).get("headers", [])}
468
- snippet = detail.get("snippet", "")
469
- return f"From: {headers.get('From')}\nTo: {headers.get('To')}\nSubject: {headers.get('Subject')}\nDate: {headers.get('Date')}\nContent Snippet: {snippet}"
470
- except Exception as e:
471
- return f"Error getting email detail: {str(e)}"
472
-
473
- async def gmail_create_draft(user_id: str, to: str, subject: str, body: str):
474
- creds = await get_google_creds(user_id, "gmail")
475
- if not creds:
476
- return "Gmail is not connected."
477
- try:
478
- service = build("gmail", "v1", credentials=creds)
479
- msg = EmailMessage()
480
- msg.set_content(body)
481
- msg["To"] = to
482
- msg["Subject"] = subject
483
- raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()
484
- draft = service.users().drafts().create(userId="me", body={"message": {"raw": raw}}).execute()
485
- return f"Successfully created draft to {to} with Subject: '{subject}' (Draft ID: {draft.get('id')})."
486
- except Exception as e:
487
- return f"Error creating draft: {str(e)}"
488
-
489
- async def gmail_send_email(user_id: str, to: str, subject: str, body: str):
490
- creds = await get_google_creds(user_id, "gmail")
491
- if not creds:
492
- return "Gmail is not connected."
493
- try:
494
- service = build("gmail", "v1", credentials=creds)
495
- msg = EmailMessage()
496
- msg.set_content(body)
497
- msg["To"] = to
498
- msg["Subject"] = subject
499
- raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()
500
- res = service.users().messages().send(userId="me", body={"raw": raw}).execute()
501
- return f"Successfully sent email to {to} with Subject: '{subject}' (Message ID: {res.get('id')})."
502
- except Exception as e:
503
- return f"Error sending email: {str(e)}"
504
-
505
- async def gmail_delete_email(user_id: str, message_id: str):
506
- creds = await get_google_creds(user_id, "gmail")
507
- if not creds:
508
- return "Gmail is not connected."
509
- try:
510
- service = build("gmail", "v1", credentials=creds)
511
- try:
512
- service.users().messages().delete(userId="me", id=message_id).execute()
513
- except Exception:
514
- service.users().messages().trash(userId="me", id=message_id).execute()
515
- return f"Successfully deleted email with ID: {message_id}."
516
- except Exception as e:
517
- return f"Error deleting email: {str(e)}"
518
-
519
- async def gmail_delete_draft(user_id: str, draft_id: str = None, query: str = None):
520
- creds = await get_google_creds(user_id, "gmail")
521
- if not creds:
522
- return "Gmail is not connected."
523
- try:
524
- service = build("gmail", "v1", credentials=creds)
525
- target_id = draft_id
526
- if not target_id and query:
527
- drafts_res = service.users().drafts().list(userId="me").execute()
528
- drafts = drafts_res.get("drafts", [])
529
- for d in drafts:
530
- detail = service.users().drafts().get(userId="me", id=d["id"]).execute()
531
- snippet = detail.get("message", {}).get("snippet", "")
532
- if query.lower() in snippet.lower():
533
- target_id = d["id"]
534
- break
535
- if not target_id:
536
- return f"No matching draft found for query: {query}"
537
- service.users().drafts().delete(userId="me", id=target_id).execute()
538
- return f"Successfully deleted draft with ID: {target_id}."
539
- except Exception as e:
540
- return f"Error deleting draft: {str(e)}"
541
-
542
- async def calendar_list_events(user_id: str, max_results: int = 10):
543
- creds = await get_google_creds(user_id, "calendar")
544
- if not creds:
545
- return "Google Calendar is not connected."
546
- try:
547
- service = build("calendar", "v3", credentials=creds)
548
- events_result = service.events().list(calendarId="primary", maxResults=max_results, singleEvents=True, orderBy="startTime").execute()
549
- events = events_result.get("items", [])
550
- if not events:
551
- return "No upcoming events found in Google Calendar."
552
- event_list = []
553
- for event in events:
554
- start = event["start"].get("dateTime", event["start"].get("date"))
555
- end = event["end"].get("dateTime", event["end"].get("date"))
556
- summary = event.get("summary", "No Title")
557
- event_list.append(f"Event ID: {event['id']} | Title: {summary} | Start: {start} | End: {end}")
558
- return "\n".join(event_list)
559
- except Exception as e:
560
- return f"Error listing calendar events: {str(e)}"
561
-
562
- async def calendar_create_event(user_id: str, summary: str, start_time: str, end_time: str, description: str = "", location: str = ""):
563
- creds = await get_google_creds(user_id, "calendar")
564
- if not creds:
565
- return "Google Calendar is not connected."
566
- try:
567
- service = build("calendar", "v3", credentials=creds)
568
- event_body = {
569
- "summary": summary,
570
- "location": location,
571
- "description": description,
572
- "start": {"dateTime": start_time},
573
- "end": {"dateTime": end_time}
574
- }
575
- created_event = service.events().insert(calendarId="primary", body=event_body).execute()
576
- return f"Successfully created event '{summary}' starting at {start_time} (Event ID: {created_event.get('id')})."
577
- except Exception as e:
578
- return f"Error creating event: {str(e)}"
579
-
580
- async def calendar_update_event(user_id: str, event_id: str, summary: str = None, start_time: str = None, end_time: str = None, description: str = None):
581
- creds = await get_google_creds(user_id, "calendar")
582
- if not creds:
583
- return "Google Calendar is not connected."
584
- try:
585
- service = build("calendar", "v3", credentials=creds)
586
- patch_body = {}
587
- if summary: patch_body["summary"] = summary
588
- if description: patch_body["description"] = description
589
- if start_time: patch_body["start"] = {"dateTime": start_time}
590
- if end_time: patch_body["end"] = {"dateTime": end_time}
591
- updated_event = service.events().patch(calendarId="primary", eventId=event_id, body=patch_body).execute()
592
- return f"Successfully updated event {event_id}."
593
- except Exception as e:
594
- return f"Error updating event: {str(e)}"
595
-
596
- async def calendar_delete_event(user_id: str, event_id: str):
597
- creds = await get_google_creds(user_id, "calendar")
598
- if not creds:
599
- return "Google Calendar is not connected."
600
- try:
601
- service = build("calendar", "v3", credentials=creds)
602
- service.events().delete(calendarId="primary", eventId=event_id).execute()
603
- return f"Successfully deleted event with ID: {event_id} from Google Calendar."
604
- except Exception as e:
605
- return f"Error deleting event: {str(e)}"
606
-
607
- async def drive_search_files(user_id: str, query: str = ""):
608
- creds = await get_google_creds(user_id, "drive")
609
- if not creds:
610
- return "Google Drive is not connected."
611
- try:
612
- service = build("drive", "v3", credentials=creds)
613
- clean_q = query.replace("'", "\\'").strip() if query else ""
614
- common_words = ["ទាំងអស់", "all", "files", "file", "documents", "list", ""]
615
- if clean_q.lower() in common_words or not clean_q:
616
- q_clause = "trashed = false"
617
- else:
618
- q_clause = f"name contains '{clean_q}' and trashed = false"
619
- results = service.files().list(q=q_clause, pageSize=10, fields="files(id, name, mimeType, modifiedTime)").execute()
620
- items = results.get("files", [])
621
- if not items:
622
- return "No files found in Google Drive."
623
- file_list = [f"File ID: {item['id']} | Name: {item['name']} | Type: {item['mimeType']}" for item in items]
624
- return "\n".join(file_list)
625
- except Exception as e:
626
- return f"Error searching Drive: {str(e)}"
627
-
628
- async def drive_read_file(user_id: str, file_id: str):
629
- creds = await get_google_creds(user_id, "drive")
630
- if not creds:
631
- return "Google Drive is not connected."
632
- try:
633
- service = build("drive", "v3", credentials=creds)
634
- meta = service.files().get(fileId=file_id).execute()
635
- mime_type = meta.get("mimeType", "")
636
- if "vnd.google-apps.document" in mime_type:
637
- content = service.files().export_media(fileId=file_id, mimeType="text/plain").execute()
638
- return content.decode("utf-8", errors="ignore")
639
- elif "vnd.google-apps.spreadsheet" in mime_type:
640
- content = service.files().export_media(fileId=file_id, mimeType="text/csv").execute()
641
- return content.decode("utf-8", errors="ignore")
642
- else:
643
- content = service.files().get_media(fileId=file_id).execute()
644
- return content.decode("utf-8", errors="ignore")
645
- except Exception as e:
646
- return f"Error reading file content: {str(e)}"
647
-
648
- async def drive_create_file(user_id: str, name: str, content: str):
649
- creds = await get_google_creds(user_id, "drive")
650
- if not creds:
651
- return "Google Drive is not connected."
652
- try:
653
- service = build("drive", "v3", credentials=creds)
654
- file_metadata = {"name": name, "mimeType": "application/vnd.google-apps.document"}
655
- from googleapiclient.http import MediaIoBaseUpload
656
- import io
657
- media = MediaIoBaseUpload(io.BytesIO(content.encode("utf-8")), mimetype="text/plain", resumable=True)
658
- file_obj = service.files().create(body=file_metadata, media_body=media, fields="id").execute()
659
- return f"Successfully created file '{name}' in Google Drive (File ID: {file_obj.get('id')})."
660
- except Exception as e:
661
- return f"Error creating drive file: {str(e)}"
662
-
663
- async def drive_delete_file(user_id: str, file_id: str):
664
- creds = await get_google_creds(user_id, "drive")
665
- if not creds:
666
- return "Google Drive is not connected."
667
- try:
668
- service = build("drive", "v3", credentials=creds)
669
- service.files().delete(fileId=file_id).execute()
670
- return f"Successfully deleted file with ID: {file_id} from Google Drive."
671
- except Exception as e:
672
- return f"Error deleting Drive file: {str(e)}"
673
-
674
- async def drive_update_file(user_id: str, file_id: str, content: str):
675
- creds = await get_google_creds(user_id, "drive")
676
- if not creds:
677
- return "Google Drive is not connected."
678
- try:
679
- service = build("drive", "v3", credentials=creds)
680
- from googleapiclient.http import MediaIoBaseUpload
681
- import io
682
- media = MediaIoBaseUpload(io.BytesIO(content.encode("utf-8")), mimetype="text/plain", resumable=True)
683
- service.files().update(fileId=file_id, media_body=media).execute()
684
- return f"Successfully updated file content for file ID: {file_id}."
685
- except Exception as e:
686
- return f"Error updating Drive file: {str(e)}"
687
-
688
- async def execute_tool_call(user_id: str, func_name: str, args: dict) -> str:
689
- if func_name == "gmail_search_emails":
690
- return await gmail_search_emails(user_id, args.get("query", ""), args.get("max_results", 5))
691
- elif func_name == "gmail_get_email_detail":
692
- return await gmail_get_email_detail(user_id, args.get("message_id", ""))
693
- elif func_name == "gmail_create_draft":
694
- return await gmail_create_draft(user_id, args.get("to", ""), args.get("subject", ""), args.get("body", ""))
695
- elif func_name == "gmail_send_email":
696
- return await gmail_send_email(user_id, args.get("to", ""), args.get("subject", ""), args.get("body", ""))
697
- elif func_name == "gmail_delete_email":
698
- return await gmail_delete_email(user_id, args.get("message_id", ""))
699
- elif func_name == "gmail_delete_draft":
700
- return await gmail_delete_draft(user_id, args.get("draft_id"), args.get("query"))
701
- elif func_name == "calendar_list_events":
702
- return await calendar_list_events(user_id, args.get("max_results", 10))
703
- elif func_name == "calendar_create_event":
704
- return await calendar_create_event(user_id, args.get("summary", ""), args.get("start_time", ""), args.get("end_time", ""), args.get("description", ""), args.get("location", ""))
705
- elif func_name == "calendar_update_event":
706
- return await calendar_update_event(user_id, args.get("event_id", ""), args.get("summary"), args.get("start_time"), args.get("end_time"), args.get("description"))
707
- elif func_name == "calendar_delete_event":
708
- return await calendar_delete_event(user_id, args.get("event_id", ""))
709
- elif func_name == "drive_search_files":
710
- return await drive_search_files(user_id, args.get("query", ""))
711
- elif func_name == "drive_read_file":
712
- return await drive_read_file(user_id, args.get("file_id", ""))
713
- elif func_name == "drive_create_file":
714
- return await drive_create_file(user_id, args.get("name", ""), args.get("content", ""))
715
- elif func_name == "drive_delete_file":
716
- return await drive_delete_file(user_id, args.get("file_id", ""))
717
- elif func_name == "drive_update_file":
718
- return await drive_update_file(user_id, args.get("file_id", ""), args.get("content", ""))
719
- return "Unknown tool function."
720
-
721
- def count_tokens(messages):
722
- num_tokens = 0
723
- for message in messages:
724
- num_tokens += 4
725
- for key, value in message.items():
726
- if isinstance(value, str):
727
- num_tokens += len(enc.encode(value))
728
- num_tokens += 2
729
- return num_tokens
730
-
731
- def build_context_messages(req_messages, user_prompt, max_tokens=100000):
732
- system_msg = {"role": "system", "content": SYSTEM_PROMPT}
733
- raw_list = []
734
- if req_messages and isinstance(req_messages, list) and len(req_messages) > 0:
735
- raw_list = req_messages
736
- elif user_prompt:
737
- raw_list = [{"role": "user", "content": user_prompt}]
738
-
739
- cleaned = []
740
- for msg in raw_list:
741
- if not isinstance(msg, dict):
742
- continue
743
- role = msg.get("role")
744
- content = str(msg.get("content", "")).strip()
745
- if not role or not content:
746
- continue
747
- if cleaned and cleaned[-1]["role"] == role:
748
- cleaned[-1]["content"] += "\n\n" + content
749
- else:
750
- cleaned.append({"role": role, "content": content})
751
-
752
- while cleaned and count_tokens([system_msg] + cleaned) > max_tokens:
753
- cleaned.pop(0)
754
- while cleaned and cleaned[0]["role"] != "user":
755
- cleaned.pop(0)
756
-
757
- return [system_msg] + cleaned
758
-
759
- @asynccontextmanager
760
- async def lifespan(app: FastAPI):
761
- global http_client
762
- limits = httpx.Limits(max_keepalive_connections=500, max_connections=2000)
763
- timeout = httpx.Timeout(120.0, connect=5.0)
764
- http_client = httpx.AsyncClient(limits=limits, timeout=timeout)
765
- yield
766
- await http_client.aclose()
767
-
768
- app = FastAPI(lifespan=lifespan)
769
-
770
- @app.get("/auth/connect")
771
- @app.get("/auth/google")
772
- async def auth_connect(user_id: str, service: str = "google"):
773
- svc_key = service.lower().strip()
774
- if svc_key not in OAUTH_CONFIGS:
775
- svc_key = "google"
776
- cfg = OAUTH_CONFIGS[svc_key]
777
-
778
- params = {
779
- "client_id": cfg["client_id"],
780
- "redirect_uri": REDIRECT_URI,
781
- "response_type": "code",
782
- "state": f"{user_id}:{svc_key}"
783
- }
784
- if "scope" in cfg:
785
- params["scope"] = cfg["scope"]
786
- if svc_key in ["google", "gmail", "calendar", "drive"]:
787
- params["access_type"] = "offline"
788
- params["prompt"] = "consent"
789
- if "extra_params" in cfg:
790
- params.update(cfg["extra_params"])
791
-
792
- query_str = "&".join([f"{k}={httpx.URL(v)}" for k, v in params.items()])
793
- auth_url = f"{cfg['auth_url']}?{query_str}"
794
- return RedirectResponse(auth_url)
795
-
796
- @app.get("/auth/callback")
797
- async def auth_callback(request: Request):
798
- try:
799
- code = request.query_params.get("code")
800
- state = request.query_params.get("state", "")
801
- user_id = state.split(":")[0] if ":" in state else "default_user"
802
- service = state.split(":")[1] if ":" in state else "connector"
803
-
804
- if not code:
805
- return RedirectResponse(f"com.open.sort.clade://oauth2redirect?status=error&service={service}")
806
-
807
- cfg = OAUTH_CONFIGS.get(service, OAUTH_CONFIGS["google"])
808
- token_url = cfg["token_url"]
809
- payload = {
810
- "client_id": cfg["client_id"],
811
- "client_secret": cfg["client_secret"],
812
- "code": code,
813
- "grant_type": "authorization_code",
814
- "redirect_uri": REDIRECT_URI
815
- }
816
- headers = {"Accept": "application/json"}
817
-
818
- async with httpx.AsyncClient() as client:
819
- res = await client.post(token_url, data=payload, headers=headers)
820
- token_data = res.json()
821
-
822
- if "access_token" in token_data or "token" in token_data:
823
- await save_token_to_supabase(user_id, service, token_data)
824
- return RedirectResponse(f"com.open.sort.clade://oauth2redirect?status=success&service={service}")
825
- else:
826
- return RedirectResponse(f"com.open.sort.clade://oauth2redirect?status=error&service={service}")
827
-
828
- except Exception:
829
- return RedirectResponse(f"com.open.sort.clade://oauth2redirect?status=error&service={service}")
830
-
831
- @app.post("/generate")
832
- async def generate(request: Request):
833
- try:
834
- body = await request.json()
835
- user_prompt = body.get("prompt", "")
836
- req_messages = body.get("messages")
837
- extended = body.get("extended", True)
838
- user_id = body.get("user_id", "default_user")
839
- connectors = body.get("connectors", [])
840
-
841
- if isinstance(extended, str):
842
- extended = extended.lower() == "true"
843
-
844
- messages = build_context_messages(req_messages, user_prompt)
845
- has_active_connectors = isinstance(connectors, list) and len(connectors) > 0
846
-
847
- headers = {
848
- "Authorization": f"Bearer {NVIDIA_API_KEY}",
849
- "Content-Type": "application/json",
850
- "Accept": "text/event-stream"
851
- }
852
-
853
- async def event_generator():
854
- curr_messages = list(messages)
855
- max_turns = 5
856
-
857
- try:
858
- for _ in range(max_turns):
859
- payload = {
860
- "model": "nvidia/nemotron-3-ultra-550b-a55b",
861
- "messages": curr_messages,
862
- "temperature": 0.7,
863
- "top_p": 0.95,
864
- "max_tokens": 16384,
865
- "chat_template_kwargs": {"enable_thinking": bool(extended)},
866
- "stream": True
867
- }
868
- if extended:
869
- payload["reasoning_budget"] = 16384
870
- else:
871
- payload["chat_template_kwargs"]["enable_thinking"] = False
872
- payload["reasoning_budget"] = 0
873
-
874
- if has_active_connectors:
875
- payload["tools"] = TOOLS
876
- payload["tool_choice"] = "auto"
877
-
878
- tool_calls_acc = []
879
- has_tool_call = False
880
-
881
- async with http_client.stream("POST", NVIDIA_URL, headers=headers, json=payload) as response:
882
- async for line in response.aiter_lines():
883
- if await request.is_disconnected():
884
- return
885
- if not line:
886
- continue
887
- line_str = line.strip()
888
- if line_str.startswith("data: "):
889
- data_content = line_str[6:].strip()
890
- if data_content == "[DONE]":
891
- break
892
- try:
893
- json_data = json.loads(data_content)
894
- choices = json_data.get("choices", [])
895
- if choices:
896
- delta = choices[0].get("delta", {})
897
- reasoning = delta.get("reasoning_content") or delta.get("reasoning")
898
- content = delta.get("content")
899
- tc_delta = delta.get("tool_calls")
900
-
901
- if reasoning:
902
- yield json.dumps({"thinking": reasoning}) + "\n"
903
-
904
- if tc_delta:
905
- has_tool_call = True
906
- for tc in tc_delta:
907
- idx = tc.get("index", 0)
908
- while len(tool_calls_acc) <= idx:
909
- tool_calls_acc.append({"id": "", "type": "function", "function": {"name": "", "arguments": ""}})
910
- if tc.get("id"):
911
- tool_calls_acc[idx]["id"] = tc["id"]
912
- if tc.get("function", {}).get("name"):
913
- tool_calls_acc[idx]["function"]["name"] += tc["function"]["name"]
914
- if tc.get("function", {}).get("arguments"):
915
- tool_calls_acc[idx]["function"]["arguments"] += tc["function"]["arguments"]
916
-
917
- if content and not has_tool_call:
918
- yield json.dumps({"answer": content}) + "\n"
919
- except json.JSONDecodeError:
920
- continue
921
-
922
- if has_tool_call and tool_calls_acc:
923
- curr_messages.append({
924
- "role": "assistant",
925
- "tool_calls": tool_calls_acc
926
- })
927
- for tc in tool_calls_acc:
928
- fn_name = tc.get("function", {}).get("name", "")
929
- args_raw = tc.get("function", {}).get("arguments", "{}")
930
- try:
931
- args_dict = json.loads(args_raw) if args_raw else {}
932
- except Exception:
933
- args_dict = {}
934
-
935
- tool_res = await execute_tool_call(user_id, fn_name, args_dict)
936
- curr_messages.append({
937
- "role": "tool",
938
- "tool_call_id": tc.get("id", "call_1"),
939
- "content": str(tool_res)
940
- })
941
- else:
942
- break
943
- except asyncio.CancelledError:
944
- pass
945
-
946
- response_headers = {
947
- "X-Accel-Buffering": "no",
948
- "Cache-Control": "no-cache",
949
- "Connection": "keep-alive"
950
- }
951
- return StreamingResponse(event_generator(), media_type="application/x-ndjson", headers=response_headers)
952
 
953
  except Exception as e:
954
  return {"error": str(e)}
 
1
+ (event_generator(), media_type="application/x-ndjson", headers=response_headers)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  except Exception as e:
4
  return {"error": str(e)}