import os from fastapi import FastAPI, Request, HTTPException, Form import uvicorn from gradio_client import Client from fastapi.responses import Response import json import re # Import re for potential future use (e.g., parsing messages) import asyncio # Import asyncio for async operations # Connect to your hosted Gradio Space (Futuresony/Mr.Events) # This client is used by BOTH the /chat and /webhook endpoints to interact with the core chatbot try: client = Client("Futuresony/ABSA_Test_Space") print("Gradio Client for 'Futuresony/ABSA_Test_Space' initialized.") except Exception as e: print(f"Error initializing Gradio Client for 'Futuresony/Mr.Events': {e}") print("Ensure the Space name is correct and it is accessible.") client = None # Set client to None if initialization fails # Get your secure API key for THIS FastAPI application and the hosted Space from environment # Assuming the same API key (APP_API_KEY) is used for both. VALID_API_KEY = os.getenv("APP_API_KEY") # Add a print statement to confirm if the API key is loaded print(f"APP_API_KEY loaded: {'Yes' if VALID_API_KEY else 'No'}") if not VALID_API_KEY: print("Warning: APP_API_KEY secret not set. API key validation and calls to hosted space may fail.") app = FastAPI() # --- Chat Endpoint (Existing Functionality) --- @app.post("/chat") async def chat(request: Request): """ Handles chat requests via a JSON payload, validates API key, and calls the hosted Gradio chatbot with history. """ print("\n--- Received POST request at /chat ---") data = await request.json() # API Key Check for THIS FastAPI application api_key = request.headers.get("X-API-Key") # Get API key from headers print(f"API Key from header: {api_key[:4]}...") if api_key else "No API Key in header" if not VALID_API_KEY or api_key != VALID_API_KEY: print("API Key validation failed.") raise HTTPException(status_code=403, detail="Invalid API Key") print("API Key validation successful.") # Get user message user_message = data.get("message") if not user_message: print("Error: 'message' is required in the request body.") raise HTTPException(status_code=400, detail="Message is required") print(f"User message: {user_message}") # Get chat history (assuming it's sent in the request body for stateless API) # The chat_history is expected to be a list of lists: [[user_msg, bot_msg], ...] # If not provided, initialize as empty list. chat_history = data.get("chat_history", []) # print(f"Received chat history: {chat_history}") # Be cautious logging history # --- Call the hosted Gradio chatbot --- if client is None: print("Error: Gradio Client not initialized. Cannot call chatbot.") raise HTTPException(status_code=500, detail="Chatbot service not available.") try: print(f"Calling hosted Gradio Space 'Futuresony/Mr.Events' /chat endpoint from /chat...") # Note: The Gradio ChatInterface API typically expects query (current message) # and chat_history (history *before* the current turn). # Use the same VALID_API_KEY for the hosted space call result = await client.predict( # Use await because client.predict can be async query=user_message, # chat_history=chat_history, # Remove chat_history parameter api_key=VALID_API_KEY, # Pass the APP_API_KEY to the hosted space api_name="/chat" # Ensure this matches the API endpoint exposed by the hosted Gradio app ) print(f"Received raw result from hosted Space: {result}") # The result from client.predict on a ChatInterface is typically the assistant's response string assistant_response = result if not isinstance(assistant_response, str): print(f"Warning: Hosted Space returned unexpected result type: {type(assistant_response)}. Raw result: {result}") # Attempt to convert to string or handle appropriately assistant_response = str(assistant_response) print(f"Formatted assistant response: {assistant_response}") except Exception as e: print(f"Error calling hosted Gradio Space from /chat: {e}") raise HTTPException(status_code=500, detail=f"Error communicating with chatbot model: {e}") return {"response": assistant_response} # --- Twilio Webhook Endpoint --- # In-memory dictionary to store history per sender (NOT for production!) # Replace this with a persistent storage solution (database, file storage) for production. conversation_histories = {} # For production-level history management, you would initialize and interact with # a database or other persistent storage here. @app.post("/webhook") async def webhook( # Explicitly receive form data parameters expected from Twilio From: str = Form(...), # Sender's phone number Body: str = Form(...), # Message content # Twilio sends other parameters like MessageSid, To, AccountSid, etc. # You can receive them here if needed: # MessageSid: str = Form(None), # To: str = Form(None), request: Request = None # Keep request for raw access if needed ): """ Handles incoming Twilio webhook requests for new messages, processes them with the chatbot, and returns TwiML. Note: This implementation uses in-memory history (NOT for production). """ print("\n--- Received POST request at /webhook from Twilio ---") # Access the incoming message and sender number directly from Form parameters incoming_message = Body sender_number = From print(f"Parsed Incoming Message: '{incoming_message}' from {sender_number}") # --- Conversation History Management (In-Memory - NOT Persistent!) --- # In a real application, you would load/save history from a database/file. chat_history = conversation_histories.get(sender_number, []) print(f"Retrieved in-memory history for {sender_number}: {chat_history}") # --- Call Chatbot Logic --- if client is None: print("Error: Gradio Client not initialized. Cannot call chatbot from webhook.") bot_response = "Error: Chatbot service is not available." else: try: # Use the same VALID_API_KEY for the hosted space call from webhook print(f"Calling hosted Gradio Space 'Futuresony/Mr.Events' /chat endpoint from /webhook...") print(f" Query: {incoming_message}") # print(f" History: {chat_history}") # Be cautious logging history # Call the hosted chatbot with the retrieved history # Gradio client expects query (current message) and chat_history (history *before* current turn) result = await client.predict( # Use await query=incoming_message, # chat_history=chat_history, # Remove chat_history parameter api_key=VALID_API_KEY, # Pass the APP_API_KEY to the hosted space api_name="/chat" # Ensure this matches the API endpoint exposed by the hosted Gradio app ) print(f"Received raw result from hosted Space for webhook: {result}") bot_response = result if not isinstance(bot_response, str): print(f"Warning: Hosted Space returned unexpected result type for webhook: {type(bot_response)}. Raw result: {result}") bot_response = str(bot_response) print(f"Formatted chatbot response for webhook: {bot_response}") except Exception as e: print(f"Error calling hosted Gradio Space from /webhook: {e}") bot_response = f"An error occurred while processing your request: {e}" # Provide a user-friendly error message # --- Update and Store History (In-Memory - NOT Persistent!) --- # Append the current turn (user message + bot response) chat_history.append([incoming_message, bot_response]) conversation_histories[sender_number] = chat_history print(f"Updated in-memory history for {sender_number}: {conversation_histories[sender_number]}") # --- Generate TwiML Response --- # Twilio expects TwiML XML to know what to do with the message # Use f-string with triple single quotes for multi-line string to avoid conflicts with HTML-like tags twiml_response = f'''{bot_response}''' print(f"Generated TwiML response: {twiml_response}") # Return TwiML with the correct media type return Response(content=twiml_response, media_type="application/xml") if __name__ == "__main__": # When running this app.py directly (e.g., with `uvicorn app:app --reload`), # this block is executed. On Hugging Face Spaces, the environment typically # runs the FastAPI application directly without executing this block. # If you need specific initializations (like loading RAG data, initializing cache) # when running on Spaces via FastAPI directly, you might need to move them # outside this __main__ block or ensure they are called on app startup. # Example (commented out, adjust based on your needs): # from app_components import authenticate_google_sheets, load_business_info, initialize_cache # authenticate_google_sheets() # load_business_info() # initialize_cache() # cleanup_expired_cache_entries() # Optional print("Starting FastAPI application with Uvicorn...") uvicorn.run(app, host="0.0.0.0", port=7860) # HF default port