Spaces:
Sleeping
Sleeping
Tripagra commited on
Commit ·
e3cbe6c
1
Parent(s): bdf0486
Fix Vercel deployment: Use proper serverless handler and streamline dependencies
Browse files- api/ask.py +89 -84
- requirements.txt +1 -11
- vercel.json +1 -6
api/ask.py
CHANGED
|
@@ -3,10 +3,8 @@ Vercel Serverless Function for handling customer support questions.
|
|
| 3 |
Endpoint: POST /api/ask
|
| 4 |
"""
|
| 5 |
|
| 6 |
-
from
|
| 7 |
-
|
| 8 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
-
from pydantic import BaseModel
|
| 10 |
import sys
|
| 11 |
import os
|
| 12 |
|
|
@@ -19,16 +17,17 @@ from src.llm_client import LLMClient
|
|
| 19 |
from src.config import Settings
|
| 20 |
|
| 21 |
# Initialize components (cached across invocations)
|
| 22 |
-
settings =
|
| 23 |
rag_system = None
|
| 24 |
llm_client = None
|
| 25 |
escalation_engine = None
|
| 26 |
|
| 27 |
def initialize_components():
|
| 28 |
"""Initialize RAG system, LLM client, and escalation engine."""
|
| 29 |
-
global rag_system, llm_client, escalation_engine
|
| 30 |
|
| 31 |
if rag_system is None:
|
|
|
|
| 32 |
articles_dir = os.path.join(
|
| 33 |
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
| 34 |
"data",
|
|
@@ -41,88 +40,94 @@ def initialize_components():
|
|
| 41 |
relevance_threshold=settings.relevance_threshold
|
| 42 |
)
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
# Configure CORS
|
| 47 |
-
app.add_middleware(
|
| 48 |
-
CORSMiddleware,
|
| 49 |
-
allow_origins=["*"], # In production, restrict to your domain
|
| 50 |
-
allow_credentials=True,
|
| 51 |
-
allow_methods=["*"],
|
| 52 |
-
allow_headers=["*"],
|
| 53 |
-
)
|
| 54 |
-
|
| 55 |
-
class QuestionRequest(BaseModel):
|
| 56 |
-
question: str
|
| 57 |
-
|
| 58 |
-
class QuestionResponse(BaseModel):
|
| 59 |
-
response_type: str
|
| 60 |
-
message: str
|
| 61 |
-
confidence_explanation: str
|
| 62 |
-
sources: list = None
|
| 63 |
-
|
| 64 |
-
@app.post("/api/ask", response_model=QuestionResponse)
|
| 65 |
-
async def ask_question(request: QuestionRequest):
|
| 66 |
-
"""
|
| 67 |
-
Process customer question and return answer or escalation.
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
-
|
| 98 |
-
"response_type":
|
| 99 |
-
"message":
|
| 100 |
-
"confidence_explanation":
|
| 101 |
-
"sources":
|
| 102 |
}
|
| 103 |
|
| 104 |
-
|
| 105 |
-
# Log error and return escalation response
|
| 106 |
-
print(f"Error processing question: {str(e)}")
|
| 107 |
-
return JSONResponse(
|
| 108 |
-
status_code=500,
|
| 109 |
-
content={
|
| 110 |
-
"response_type": "escalation",
|
| 111 |
-
"message": "I'm experiencing technical difficulties. Let me connect you with a human agent.",
|
| 112 |
-
"confidence_explanation": f"System error occurred",
|
| 113 |
-
"sources": None
|
| 114 |
-
}
|
| 115 |
-
)
|
| 116 |
-
|
| 117 |
-
@app.get("/api/health")
|
| 118 |
-
async def health_check():
|
| 119 |
-
"""Health check endpoint."""
|
| 120 |
-
return {
|
| 121 |
-
"status": "healthy",
|
| 122 |
-
"vector_store_loaded": rag_system is not None
|
| 123 |
-
}
|
| 124 |
|
| 125 |
-
# Vercel serverless function handler
|
| 126 |
-
def handler(request: Request):
|
| 127 |
-
"""Main handler for Vercel serverless function."""
|
| 128 |
-
return app(request)
|
|
|
|
| 3 |
Endpoint: POST /api/ask
|
| 4 |
"""
|
| 5 |
|
| 6 |
+
from http.server import BaseHTTPRequestHandler
|
| 7 |
+
import json
|
|
|
|
|
|
|
| 8 |
import sys
|
| 9 |
import os
|
| 10 |
|
|
|
|
| 17 |
from src.config import Settings
|
| 18 |
|
| 19 |
# Initialize components (cached across invocations)
|
| 20 |
+
settings = None
|
| 21 |
rag_system = None
|
| 22 |
llm_client = None
|
| 23 |
escalation_engine = None
|
| 24 |
|
| 25 |
def initialize_components():
|
| 26 |
"""Initialize RAG system, LLM client, and escalation engine."""
|
| 27 |
+
global settings, rag_system, llm_client, escalation_engine
|
| 28 |
|
| 29 |
if rag_system is None:
|
| 30 |
+
settings = Settings()
|
| 31 |
articles_dir = os.path.join(
|
| 32 |
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
| 33 |
"data",
|
|
|
|
| 40 |
relevance_threshold=settings.relevance_threshold
|
| 41 |
)
|
| 42 |
|
| 43 |
+
class handler(BaseHTTPRequestHandler):
|
| 44 |
+
"""Vercel serverless function handler."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
+
def do_POST(self):
|
| 47 |
+
"""Handle POST requests."""
|
| 48 |
+
try:
|
| 49 |
+
# Initialize components on first request
|
| 50 |
+
initialize_components()
|
| 51 |
+
|
| 52 |
+
# Read request body
|
| 53 |
+
content_length = int(self.headers.get('Content-Length', 0))
|
| 54 |
+
body = self.rfile.read(content_length).decode('utf-8')
|
| 55 |
+
|
| 56 |
+
# Parse JSON
|
| 57 |
+
try:
|
| 58 |
+
data = json.loads(body)
|
| 59 |
+
question = data.get('question', '').strip()
|
| 60 |
+
except json.JSONDecodeError:
|
| 61 |
+
self.send_error_response(400, "Invalid JSON")
|
| 62 |
+
return
|
| 63 |
+
|
| 64 |
+
# Validate question
|
| 65 |
+
if not question:
|
| 66 |
+
self.send_error_response(400, "Question cannot be empty")
|
| 67 |
+
return
|
| 68 |
+
|
| 69 |
+
# Retrieve relevant chunks
|
| 70 |
+
chunks = rag_system.retrieve(question, top_k=settings.top_k_chunks)
|
| 71 |
+
|
| 72 |
+
# Decide whether to answer or escalate
|
| 73 |
+
decision = escalation_engine.process_question(question, chunks)
|
| 74 |
+
|
| 75 |
+
# Send success response
|
| 76 |
+
self.send_response(200)
|
| 77 |
+
self.send_header('Content-Type', 'application/json')
|
| 78 |
+
self.send_header('Access-Control-Allow-Origin', '*')
|
| 79 |
+
self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
|
| 80 |
+
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
| 81 |
+
self.end_headers()
|
| 82 |
+
|
| 83 |
+
response = {
|
| 84 |
+
"response_type": decision.action,
|
| 85 |
+
"message": decision.message,
|
| 86 |
+
"confidence_explanation": decision.confidence_explanation,
|
| 87 |
+
"sources": decision.sources
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
self.wfile.write(json.dumps(response).encode('utf-8'))
|
| 91 |
+
|
| 92 |
+
except Exception as e:
|
| 93 |
+
print(f"Error processing question: {str(e)}")
|
| 94 |
+
self.send_error_response(500, "Internal server error")
|
| 95 |
+
|
| 96 |
+
def do_OPTIONS(self):
|
| 97 |
+
"""Handle OPTIONS requests for CORS."""
|
| 98 |
+
self.send_response(200)
|
| 99 |
+
self.send_header('Access-Control-Allow-Origin', '*')
|
| 100 |
+
self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
|
| 101 |
+
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
| 102 |
+
self.end_headers()
|
| 103 |
+
|
| 104 |
+
def do_GET(self):
|
| 105 |
+
"""Handle GET requests (health check)."""
|
| 106 |
+
self.send_response(200)
|
| 107 |
+
self.send_header('Content-Type', 'application/json')
|
| 108 |
+
self.send_header('Access-Control-Allow-Origin', '*')
|
| 109 |
+
self.end_headers()
|
| 110 |
|
| 111 |
+
response = {
|
| 112 |
+
"status": "healthy",
|
| 113 |
+
"vector_store_loaded": rag_system is not None
|
| 114 |
+
}
|
| 115 |
|
| 116 |
+
self.wfile.write(json.dumps(response).encode('utf-8'))
|
| 117 |
+
|
| 118 |
+
def send_error_response(self, status_code, message):
|
| 119 |
+
"""Send error response."""
|
| 120 |
+
self.send_response(status_code)
|
| 121 |
+
self.send_header('Content-Type', 'application/json')
|
| 122 |
+
self.send_header('Access-Control-Allow-Origin', '*')
|
| 123 |
+
self.end_headers()
|
| 124 |
|
| 125 |
+
response = {
|
| 126 |
+
"response_type": "error",
|
| 127 |
+
"message": message,
|
| 128 |
+
"confidence_explanation": "Request error",
|
| 129 |
+
"sources": None
|
| 130 |
}
|
| 131 |
|
| 132 |
+
self.wfile.write(json.dumps(response).encode('utf-8'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -1,6 +1,4 @@
|
|
| 1 |
-
#
|
| 2 |
-
fastapi==0.104.1
|
| 3 |
-
uvicorn[standard]==0.24.0
|
| 4 |
pydantic==2.5.0
|
| 5 |
pydantic-settings==2.1.0
|
| 6 |
python-dotenv==1.0.0
|
|
@@ -13,11 +11,3 @@ numpy==1.24.3
|
|
| 13 |
# LLM API clients
|
| 14 |
google-generativeai==0.3.2
|
| 15 |
|
| 16 |
-
# Testing dependencies
|
| 17 |
-
pytest==7.4.3
|
| 18 |
-
pytest-asyncio==0.21.1
|
| 19 |
-
hypothesis==6.92.1
|
| 20 |
-
httpx==0.25.2
|
| 21 |
-
|
| 22 |
-
# CORS support
|
| 23 |
-
python-multipart==0.0.6
|
|
|
|
| 1 |
+
# Core dependencies
|
|
|
|
|
|
|
| 2 |
pydantic==2.5.0
|
| 3 |
pydantic-settings==2.1.0
|
| 4 |
python-dotenv==1.0.0
|
|
|
|
| 11 |
# LLM API clients
|
| 12 |
google-generativeai==0.3.2
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vercel.json
CHANGED
|
@@ -1,10 +1,5 @@
|
|
| 1 |
{
|
| 2 |
"buildCommand": "cd frontend && npm ci && npm run build",
|
| 3 |
"outputDirectory": "frontend/dist",
|
| 4 |
-
"installCommand": "pip install -r requirements.txt"
|
| 5 |
-
"functions": {
|
| 6 |
-
"api/**/*.py": {
|
| 7 |
-
"runtime": "@vercel/python@4.3.0"
|
| 8 |
-
}
|
| 9 |
-
}
|
| 10 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"buildCommand": "cd frontend && npm ci && npm run build",
|
| 3 |
"outputDirectory": "frontend/dist",
|
| 4 |
+
"installCommand": "pip install --upgrade pip && pip install -r requirements.txt"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
}
|