Update app.py
Browse files
app.py
CHANGED
|
@@ -1,954 +1,4 @@
|
|
| 1 |
-
|
| 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)}
|