Spaces:
Sleeping
Sleeping
debug2
Browse files
app.py
CHANGED
|
@@ -17,7 +17,7 @@ from slack_bolt.oauth.async_oauth_settings import AsyncOAuthSettings
|
|
| 17 |
from slack_sdk.oauth.installation_store.async_installation_store import AsyncInstallationStore
|
| 18 |
from slack_sdk.oauth import AuthorizeUrlGenerator
|
| 19 |
from slack_sdk.oauth.installation_store.models import Installation
|
| 20 |
-
from slack_bolt.authorization import AuthorizeResult
|
| 21 |
|
| 22 |
# RAG/ML Libraries
|
| 23 |
from sentence_transformers import SentenceTransformer
|
|
@@ -32,8 +32,6 @@ from docx import Document
|
|
| 32 |
import requests
|
| 33 |
|
| 34 |
# --- Configuration and Initialization ---
|
| 35 |
-
|
| 36 |
-
# Set up logging
|
| 37 |
logging.basicConfig(level=logging.INFO)
|
| 38 |
logger = logging.getLogger(__name__)
|
| 39 |
|
|
@@ -56,7 +54,7 @@ if not all(required_vars):
|
|
| 56 |
if not SLACK_BOT_TOKEN:
|
| 57 |
raise ValueError("SLACK_BOT_TOKEN is required for single-team mode.")
|
| 58 |
|
| 59 |
-
# Set HF_TOKEN if provided
|
| 60 |
if HF_TOKEN:
|
| 61 |
try:
|
| 62 |
login(token=HF_TOKEN, add_to_git_credential=False)
|
|
@@ -67,7 +65,7 @@ if HF_TOKEN:
|
|
| 67 |
# Initialize Supabase client
|
| 68 |
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 69 |
|
| 70 |
-
# --- Supabase Async Installation Store
|
| 71 |
class SupabaseAsyncInstallationStore(AsyncInstallationStore):
|
| 72 |
def __init__(self, supabase_client):
|
| 73 |
self.supabase = supabase_client
|
|
@@ -125,67 +123,84 @@ class SupabaseAsyncInstallationStore(AsyncInstallationStore):
|
|
| 125 |
logger.error(f"Failed to delete installation for team {team_id}: {e}")
|
| 126 |
raise
|
| 127 |
|
| 128 |
-
# Initialize installation store
|
| 129 |
installation_store = None
|
| 130 |
-
USE_MULTI_TEAM = os.environ.get("USE_MULTI_TEAM", "false").lower() == "true"
|
|
|
|
| 131 |
if USE_MULTI_TEAM:
|
| 132 |
installation_store = SupabaseAsyncInstallationStore(supabase)
|
| 133 |
logger.info("Using multi-team mode with Supabase store.")
|
| 134 |
else:
|
| 135 |
logger.info("Using single-team mode with SLACK_BOT_TOKEN.")
|
| 136 |
|
| 137 |
-
# Custom authorize for multi-team
|
| 138 |
async def async_authorize(enterprise_id, team_id, user_id):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
logger.info(f"Custom authorize called for enterprise: {enterprise_id}, team: {team_id}, user: {user_id}")
|
|
|
|
| 140 |
if not USE_MULTI_TEAM:
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
installation = await installation_store.fetch_installation(team_id=team_id, enterprise_id=enterprise_id)
|
| 143 |
if installation:
|
| 144 |
-
return AuthorizeResult(
|
| 145 |
enterprise_id=enterprise_id or installation.enterprise_id,
|
| 146 |
team_id=team_id,
|
| 147 |
user_id=user_id or installation.user_id,
|
| 148 |
bot_token=installation.bot_token,
|
| 149 |
bot_user_id=installation.bot_user_id,
|
| 150 |
)
|
|
|
|
| 151 |
logger.error(f"No authorization found for team {team_id}")
|
| 152 |
return None
|
| 153 |
|
| 154 |
-
# Initialize Bolt Async App
|
| 155 |
app = AsyncApp(
|
| 156 |
signing_secret=SLACK_SIGNING_SECRET,
|
| 157 |
-
token=
|
| 158 |
-
installation_store=installation_store,
|
| 159 |
-
authorize=async_authorize
|
| 160 |
oauth_settings=AsyncOAuthSettings(
|
| 161 |
client_id=SLACK_CLIENT_ID,
|
| 162 |
client_secret=SLACK_CLIENT_SECRET,
|
| 163 |
scopes=["app_mentions:read", "files:read", "chat:write", "im:read", "im:write", "channels:read"],
|
| 164 |
redirect_uri_path="/slack/oauth/callback",
|
| 165 |
-
),
|
| 166 |
)
|
| 167 |
|
| 168 |
api = FastAPI()
|
| 169 |
|
| 170 |
# --- RAG Model Loading ---
|
| 171 |
-
# Check for GPU and set device
|
| 172 |
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 173 |
logger.info(f"Using device: {device}")
|
| 174 |
|
| 175 |
print("Loading embedding model...")
|
| 176 |
embedding_model = None
|
| 177 |
qa_pipeline = None
|
|
|
|
| 178 |
try:
|
| 179 |
embedding_model = SentenceTransformer('all-MiniLM-L6-v2', device=device)
|
| 180 |
print("Loading QA model...")
|
| 181 |
-
qa_pipeline = pipeline("question-answering", model="deepset/roberta-base-squad2", device=device)
|
| 182 |
print("Models loaded successfully!")
|
| 183 |
except Exception as e:
|
| 184 |
logger.error(f"Error loading models: {e}")
|
| 185 |
raise RuntimeError("Failed to load required ML models. Check dependencies and hardware.")
|
| 186 |
|
| 187 |
# --- Utility Functions ---
|
| 188 |
-
|
| 189 |
def download_slack_file(url: str, token: str) -> bytes:
|
| 190 |
"""Downloads a file from Slack using the private download URL and bot token."""
|
| 191 |
try:
|
|
@@ -289,7 +304,7 @@ async def answer_question(question: str, context: str) -> str:
|
|
| 289 |
if not context.strip():
|
| 290 |
return "No relevant documents found."
|
| 291 |
try:
|
| 292 |
-
MAX_CONTEXT_LEN = 4096
|
| 293 |
context_slice = context[:MAX_CONTEXT_LEN]
|
| 294 |
|
| 295 |
result = await asyncio.to_thread(
|
|
@@ -301,7 +316,6 @@ async def answer_question(question: str, context: str) -> str:
|
|
| 301 |
return "Error generating answer from context."
|
| 302 |
|
| 303 |
# --- Slack Handlers (Async) ---
|
| 304 |
-
|
| 305 |
@app.event("file_shared")
|
| 306 |
async def handle_file_shared(event, say, client):
|
| 307 |
"""Processes files shared in a channel and adds their content to the RAG knowledge base."""
|
|
@@ -317,7 +331,7 @@ async def handle_file_shared(event, say, client):
|
|
| 317 |
await say("No download URL available for the file.")
|
| 318 |
return
|
| 319 |
|
| 320 |
-
token = client.token
|
| 321 |
file_content = await asyncio.to_thread(download_slack_file, file_url, token)
|
| 322 |
|
| 323 |
text = ""
|
|
@@ -347,23 +361,11 @@ async def handle_mention(event, say, client):
|
|
| 347 |
text = event["text"]
|
| 348 |
user_query = re.sub(r'<@[A-Z0-9]+>', '', text).strip()
|
| 349 |
|
| 350 |
-
# Simple check for the presence of a file in the message for combined upload/query
|
| 351 |
-
files = event.get("files", [])
|
| 352 |
-
if files:
|
| 353 |
-
await say("I see a file attached. I'll process it first, and then answer your question.")
|
| 354 |
-
# NOTE: Handling files in app_mention is complex due to timing/race conditions.
|
| 355 |
-
# For simplicity, we delegate file processing primarily to file_shared event.
|
| 356 |
-
# This loop is kept as a fallback/redundancy but the file_shared event is more reliable.
|
| 357 |
-
# The logic here is *identical* to file_shared and is omitted for brevity but should
|
| 358 |
-
# be included if you expect users to combine uploads and queries in one message.
|
| 359 |
-
pass # The file processing logic from the original code would go here
|
| 360 |
-
|
| 361 |
if not user_query:
|
| 362 |
await say("Please ask me a question after mentioning me!")
|
| 363 |
return
|
| 364 |
|
| 365 |
try:
|
| 366 |
-
# Check if table is empty
|
| 367 |
if await is_table_empty():
|
| 368 |
await say("My knowledge base is empty. Please share some PDF or DOCX files first so I can learn!")
|
| 369 |
return
|
|
@@ -374,19 +376,15 @@ async def handle_mention(event, say, client):
|
|
| 374 |
await say("I couldn't find any relevant information in my knowledge base. Try uploading more documents.")
|
| 375 |
return
|
| 376 |
|
| 377 |
-
# Combine the content of the top documents into a single context string
|
| 378 |
context = " ".join([doc["content"] for doc in results])
|
| 379 |
answer = await answer_question(user_query, context)
|
| 380 |
|
| 381 |
-
# Format the final response
|
| 382 |
await say(f"💡 *Answer:* {answer}\n\n_I found this information by analyzing {len(results)} relevant document chunks._")
|
| 383 |
except Exception as e:
|
| 384 |
logger.error(f"Error in app_mention handler for query '{user_query}': {str(e)}")
|
| 385 |
await say(f"❌ Error answering question: {str(e)}")
|
| 386 |
|
| 387 |
# --- FastAPI Integration ---
|
| 388 |
-
|
| 389 |
-
# Initialize the Async Slack Request Handler
|
| 390 |
handler = AsyncSlackRequestHandler(app)
|
| 391 |
|
| 392 |
@api.post("/slack/events")
|
|
@@ -410,14 +408,19 @@ async def health():
|
|
| 410 |
db_status = "error"
|
| 411 |
try:
|
| 412 |
await asyncio.to_thread(
|
| 413 |
-
lambda: supabase.table("
|
| 414 |
)
|
| 415 |
db_status = "ok"
|
| 416 |
except Exception as e:
|
| 417 |
logger.error(f"Database health check failed: {e}")
|
| 418 |
|
| 419 |
models_status = "ok" if embedding_model and qa_pipeline else "error"
|
| 420 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
|
| 422 |
@api.get("/slack/install")
|
| 423 |
async def install_url():
|
|
@@ -440,36 +443,48 @@ async def oauth_callback(request: Request):
|
|
| 440 |
try:
|
| 441 |
logger.info("OAuth callback received")
|
| 442 |
response = await handler.handle(request)
|
| 443 |
-
logger.info(f"OAuth callback
|
|
|
|
| 444 |
if hasattr(response, 'status_code') and response.status_code == 200:
|
| 445 |
-
# In single-team, no need to save; for multi, it should auto-save via store
|
| 446 |
-
if USE_MULTI_TEAM:
|
| 447 |
-
logger.info("Multi-team install: Check Supabase for new data.")
|
| 448 |
return HTMLResponse(
|
| 449 |
-
content=f"<html
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
status_code=200
|
| 451 |
)
|
| 452 |
return response
|
| 453 |
except Exception as e:
|
| 454 |
logger.error(f"OAuth Callback Error: {e}")
|
| 455 |
return HTMLResponse(
|
| 456 |
-
content=f"<html
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
status_code=500
|
| 458 |
)
|
| 459 |
|
| 460 |
-
# Optional: Add a debug endpoint to check installation
|
| 461 |
@api.get("/debug/installations")
|
| 462 |
async def debug_installations():
|
| 463 |
-
"""Debug: List all installations (remove in
|
| 464 |
try:
|
| 465 |
result = await asyncio.to_thread(
|
| 466 |
lambda: supabase.table("installations").select("*").execute()
|
| 467 |
)
|
| 468 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
except Exception as e:
|
| 470 |
return {"error": str(e)}
|
| 471 |
|
| 472 |
-
|
| 473 |
if __name__ == "__main__":
|
| 474 |
port = int(os.environ.get("PORT", 7860))
|
| 475 |
uvicorn_run(api, host="0.0.0.0", port=port)
|
|
|
|
| 17 |
from slack_sdk.oauth.installation_store.async_installation_store import AsyncInstallationStore
|
| 18 |
from slack_sdk.oauth import AuthorizeUrlGenerator
|
| 19 |
from slack_sdk.oauth.installation_store.models import Installation
|
| 20 |
+
from slack_bolt.authorization import AuthorizeResult
|
| 21 |
|
| 22 |
# RAG/ML Libraries
|
| 23 |
from sentence_transformers import SentenceTransformer
|
|
|
|
| 32 |
import requests
|
| 33 |
|
| 34 |
# --- Configuration and Initialization ---
|
|
|
|
|
|
|
| 35 |
logging.basicConfig(level=logging.INFO)
|
| 36 |
logger = logging.getLogger(__name__)
|
| 37 |
|
|
|
|
| 54 |
if not SLACK_BOT_TOKEN:
|
| 55 |
raise ValueError("SLACK_BOT_TOKEN is required for single-team mode.")
|
| 56 |
|
| 57 |
+
# Set HF_TOKEN if provided
|
| 58 |
if HF_TOKEN:
|
| 59 |
try:
|
| 60 |
login(token=HF_TOKEN, add_to_git_credential=False)
|
|
|
|
| 65 |
# Initialize Supabase client
|
| 66 |
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 67 |
|
| 68 |
+
# --- Supabase Async Installation Store ---
|
| 69 |
class SupabaseAsyncInstallationStore(AsyncInstallationStore):
|
| 70 |
def __init__(self, supabase_client):
|
| 71 |
self.supabase = supabase_client
|
|
|
|
| 123 |
logger.error(f"Failed to delete installation for team {team_id}: {e}")
|
| 124 |
raise
|
| 125 |
|
| 126 |
+
# Initialize installation store
|
| 127 |
installation_store = None
|
| 128 |
+
USE_MULTI_TEAM = os.environ.get("USE_MULTI_TEAM", "false").lower() == "true"
|
| 129 |
+
|
| 130 |
if USE_MULTI_TEAM:
|
| 131 |
installation_store = SupabaseAsyncInstallationStore(supabase)
|
| 132 |
logger.info("Using multi-team mode with Supabase store.")
|
| 133 |
else:
|
| 134 |
logger.info("Using single-team mode with SLACK_BOT_TOKEN.")
|
| 135 |
|
| 136 |
+
# FIXED: Custom authorize function that works for both single and multi-team
|
| 137 |
async def async_authorize(enterprise_id, team_id, user_id):
|
| 138 |
+
"""
|
| 139 |
+
Custom authorization function.
|
| 140 |
+
For single-team: Returns a static AuthorizeResult using SLACK_BOT_TOKEN.
|
| 141 |
+
For multi-team: Fetches from installation store.
|
| 142 |
+
"""
|
| 143 |
logger.info(f"Custom authorize called for enterprise: {enterprise_id}, team: {team_id}, user: {user_id}")
|
| 144 |
+
|
| 145 |
if not USE_MULTI_TEAM:
|
| 146 |
+
# FIXED: Return AuthorizeResult for single-team mode instead of None
|
| 147 |
+
logger.info("Single-team mode: Using SLACK_BOT_TOKEN")
|
| 148 |
+
return AuthorizeResult(
|
| 149 |
+
enterprise_id=enterprise_id,
|
| 150 |
+
team_id=team_id,
|
| 151 |
+
bot_user_id=None, # Will be populated automatically by Slack SDK
|
| 152 |
+
bot_token=SLACK_BOT_TOKEN,
|
| 153 |
+
user_id=user_id,
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
# Multi-team mode: fetch from store
|
| 157 |
installation = await installation_store.fetch_installation(team_id=team_id, enterprise_id=enterprise_id)
|
| 158 |
if installation:
|
| 159 |
+
return AuthorizeResult(
|
| 160 |
enterprise_id=enterprise_id or installation.enterprise_id,
|
| 161 |
team_id=team_id,
|
| 162 |
user_id=user_id or installation.user_id,
|
| 163 |
bot_token=installation.bot_token,
|
| 164 |
bot_user_id=installation.bot_user_id,
|
| 165 |
)
|
| 166 |
+
|
| 167 |
logger.error(f"No authorization found for team {team_id}")
|
| 168 |
return None
|
| 169 |
|
| 170 |
+
# FIXED: Initialize Bolt Async App with proper configuration
|
| 171 |
app = AsyncApp(
|
| 172 |
signing_secret=SLACK_SIGNING_SECRET,
|
| 173 |
+
token=None, # Always use None when using authorize function
|
| 174 |
+
installation_store=installation_store if USE_MULTI_TEAM else None,
|
| 175 |
+
authorize=async_authorize, # Always use custom authorize
|
| 176 |
oauth_settings=AsyncOAuthSettings(
|
| 177 |
client_id=SLACK_CLIENT_ID,
|
| 178 |
client_secret=SLACK_CLIENT_SECRET,
|
| 179 |
scopes=["app_mentions:read", "files:read", "chat:write", "im:read", "im:write", "channels:read"],
|
| 180 |
redirect_uri_path="/slack/oauth/callback",
|
| 181 |
+
) if USE_MULTI_TEAM else None,
|
| 182 |
)
|
| 183 |
|
| 184 |
api = FastAPI()
|
| 185 |
|
| 186 |
# --- RAG Model Loading ---
|
|
|
|
| 187 |
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
| 188 |
logger.info(f"Using device: {device}")
|
| 189 |
|
| 190 |
print("Loading embedding model...")
|
| 191 |
embedding_model = None
|
| 192 |
qa_pipeline = None
|
| 193 |
+
|
| 194 |
try:
|
| 195 |
embedding_model = SentenceTransformer('all-MiniLM-L6-v2', device=device)
|
| 196 |
print("Loading QA model...")
|
| 197 |
+
qa_pipeline = pipeline("question-answering", model="deepset/roberta-base-squad2", device=0 if device == 'cuda' else -1)
|
| 198 |
print("Models loaded successfully!")
|
| 199 |
except Exception as e:
|
| 200 |
logger.error(f"Error loading models: {e}")
|
| 201 |
raise RuntimeError("Failed to load required ML models. Check dependencies and hardware.")
|
| 202 |
|
| 203 |
# --- Utility Functions ---
|
|
|
|
| 204 |
def download_slack_file(url: str, token: str) -> bytes:
|
| 205 |
"""Downloads a file from Slack using the private download URL and bot token."""
|
| 206 |
try:
|
|
|
|
| 304 |
if not context.strip():
|
| 305 |
return "No relevant documents found."
|
| 306 |
try:
|
| 307 |
+
MAX_CONTEXT_LEN = 4096
|
| 308 |
context_slice = context[:MAX_CONTEXT_LEN]
|
| 309 |
|
| 310 |
result = await asyncio.to_thread(
|
|
|
|
| 316 |
return "Error generating answer from context."
|
| 317 |
|
| 318 |
# --- Slack Handlers (Async) ---
|
|
|
|
| 319 |
@app.event("file_shared")
|
| 320 |
async def handle_file_shared(event, say, client):
|
| 321 |
"""Processes files shared in a channel and adds their content to the RAG knowledge base."""
|
|
|
|
| 331 |
await say("No download URL available for the file.")
|
| 332 |
return
|
| 333 |
|
| 334 |
+
token = client.token
|
| 335 |
file_content = await asyncio.to_thread(download_slack_file, file_url, token)
|
| 336 |
|
| 337 |
text = ""
|
|
|
|
| 361 |
text = event["text"]
|
| 362 |
user_query = re.sub(r'<@[A-Z0-9]+>', '', text).strip()
|
| 363 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
if not user_query:
|
| 365 |
await say("Please ask me a question after mentioning me!")
|
| 366 |
return
|
| 367 |
|
| 368 |
try:
|
|
|
|
| 369 |
if await is_table_empty():
|
| 370 |
await say("My knowledge base is empty. Please share some PDF or DOCX files first so I can learn!")
|
| 371 |
return
|
|
|
|
| 376 |
await say("I couldn't find any relevant information in my knowledge base. Try uploading more documents.")
|
| 377 |
return
|
| 378 |
|
|
|
|
| 379 |
context = " ".join([doc["content"] for doc in results])
|
| 380 |
answer = await answer_question(user_query, context)
|
| 381 |
|
|
|
|
| 382 |
await say(f"💡 *Answer:* {answer}\n\n_I found this information by analyzing {len(results)} relevant document chunks._")
|
| 383 |
except Exception as e:
|
| 384 |
logger.error(f"Error in app_mention handler for query '{user_query}': {str(e)}")
|
| 385 |
await say(f"❌ Error answering question: {str(e)}")
|
| 386 |
|
| 387 |
# --- FastAPI Integration ---
|
|
|
|
|
|
|
| 388 |
handler = AsyncSlackRequestHandler(app)
|
| 389 |
|
| 390 |
@api.post("/slack/events")
|
|
|
|
| 408 |
db_status = "error"
|
| 409 |
try:
|
| 410 |
await asyncio.to_thread(
|
| 411 |
+
lambda: supabase.table("documents").select("id").limit(1).execute()
|
| 412 |
)
|
| 413 |
db_status = "ok"
|
| 414 |
except Exception as e:
|
| 415 |
logger.error(f"Database health check failed: {e}")
|
| 416 |
|
| 417 |
models_status = "ok" if embedding_model and qa_pipeline else "error"
|
| 418 |
+
return {
|
| 419 |
+
"status": "ok" if db_status == "ok" and models_status == "ok" else "error",
|
| 420 |
+
"database": db_status,
|
| 421 |
+
"models": models_status,
|
| 422 |
+
"mode": "single-team" if not USE_MULTI_TEAM else "multi-team"
|
| 423 |
+
}
|
| 424 |
|
| 425 |
@api.get("/slack/install")
|
| 426 |
async def install_url():
|
|
|
|
| 443 |
try:
|
| 444 |
logger.info("OAuth callback received")
|
| 445 |
response = await handler.handle(request)
|
| 446 |
+
logger.info(f"OAuth callback handled")
|
| 447 |
+
|
| 448 |
if hasattr(response, 'status_code') and response.status_code == 200:
|
|
|
|
|
|
|
|
|
|
| 449 |
return HTMLResponse(
|
| 450 |
+
content=f"""<html>
|
| 451 |
+
<body>
|
| 452 |
+
<h1>Installation successful!</h1>
|
| 453 |
+
<p>You can now use the bot in your Slack workspace.</p>
|
| 454 |
+
<p>Mode: {'Single-team' if not USE_MULTI_TEAM else 'Multi-team'}</p>
|
| 455 |
+
</body>
|
| 456 |
+
</html>""",
|
| 457 |
status_code=200
|
| 458 |
)
|
| 459 |
return response
|
| 460 |
except Exception as e:
|
| 461 |
logger.error(f"OAuth Callback Error: {e}")
|
| 462 |
return HTMLResponse(
|
| 463 |
+
content=f"""<html>
|
| 464 |
+
<body>
|
| 465 |
+
<h1>Installation Failed!</h1>
|
| 466 |
+
<p>Error: {str(e)}</p>
|
| 467 |
+
<p>Check logs for details.</p>
|
| 468 |
+
</body>
|
| 469 |
+
</html>""",
|
| 470 |
status_code=500
|
| 471 |
)
|
| 472 |
|
|
|
|
| 473 |
@api.get("/debug/installations")
|
| 474 |
async def debug_installations():
|
| 475 |
+
"""Debug: List all installations (remove in production)."""
|
| 476 |
try:
|
| 477 |
result = await asyncio.to_thread(
|
| 478 |
lambda: supabase.table("installations").select("*").execute()
|
| 479 |
)
|
| 480 |
+
return {
|
| 481 |
+
"installations": result.data,
|
| 482 |
+
"mode": "multi-team" if USE_MULTI_TEAM else "single-team",
|
| 483 |
+
"count": len(result.data)
|
| 484 |
+
}
|
| 485 |
except Exception as e:
|
| 486 |
return {"error": str(e)}
|
| 487 |
|
|
|
|
| 488 |
if __name__ == "__main__":
|
| 489 |
port = int(os.environ.get("PORT", 7860))
|
| 490 |
uvicorn_run(api, host="0.0.0.0", port=port)
|