zenaight commited on
Commit ·
90f252d
1
Parent(s): f547cc1
simplified and broken up code
Browse files- README.md +23 -1
- __init__.py +2 -0
- ai_chat.py +63 -0
- api_routes.py +87 -0
- config.py +30 -0
- database.py +83 -0
- main.py +31 -247
- whatsapp.py +23 -0
README.md
CHANGED
|
@@ -112,9 +112,31 @@ users
|
|
| 112 |
- Optional Row Level Security (RLS) in Supabase
|
| 113 |
- Input validation and error handling
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
## Development
|
| 116 |
|
| 117 |
-
The application uses:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
- **FastAPI** for the web framework
|
| 119 |
- **LangGraph** for conversation flow management
|
| 120 |
- **OpenAI GPT-4** for AI responses
|
|
|
|
| 112 |
- Optional Row Level Security (RLS) in Supabase
|
| 113 |
- Input validation and error handling
|
| 114 |
|
| 115 |
+
## Project Structure
|
| 116 |
+
|
| 117 |
+
```
|
| 118 |
+
PropAgent/
|
| 119 |
+
├── main.py # Main application entry point
|
| 120 |
+
├── config.py # Configuration and client setup
|
| 121 |
+
├── database.py # Supabase database operations
|
| 122 |
+
├── whatsapp.py # WhatsApp API integration
|
| 123 |
+
├── ai_chat.py # LangGraph AI conversation logic
|
| 124 |
+
├── api_routes.py # FastAPI route definitions
|
| 125 |
+
├── supabase_setup.sql # Database schema setup
|
| 126 |
+
└── requirements.txt # Python dependencies
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
## Development
|
| 130 |
|
| 131 |
+
The application uses a modular architecture:
|
| 132 |
+
- **main.py** - Clean entry point focusing on core webhook logic
|
| 133 |
+
- **config.py** - Environment variables and client initialization
|
| 134 |
+
- **database.py** - All Supabase database operations
|
| 135 |
+
- **whatsapp.py** - WhatsApp Business API integration
|
| 136 |
+
- **ai_chat.py** - LangGraph conversation flow and AI processing
|
| 137 |
+
- **api_routes.py** - REST API endpoints and webhook verification
|
| 138 |
+
|
| 139 |
+
**Technologies:**
|
| 140 |
- **FastAPI** for the web framework
|
| 141 |
- **LangGraph** for conversation flow management
|
| 142 |
- **OpenAI GPT-4** for AI responses
|
__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PropAgent - WhatsApp AI Agent Package
|
| 2 |
+
__version__ = "1.0.0"
|
ai_chat.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langgraph.graph import StateGraph, END
|
| 2 |
+
from langchain_core.runnables import RunnableLambda
|
| 3 |
+
from typing import TypedDict
|
| 4 |
+
from config import llm, OPENAI_API_KEY
|
| 5 |
+
|
| 6 |
+
# Simple in-memory chat history (no deprecation warnings)
|
| 7 |
+
chat_history = []
|
| 8 |
+
|
| 9 |
+
def chat_with_memory(state):
|
| 10 |
+
"""Chat function with memory and user context"""
|
| 11 |
+
user_message = state["user_message"]
|
| 12 |
+
user_info = state.get("user_info", {})
|
| 13 |
+
|
| 14 |
+
# Add user message to history
|
| 15 |
+
chat_history.append({"role": "user", "content": user_message})
|
| 16 |
+
|
| 17 |
+
# Keep only last 6 messages for context
|
| 18 |
+
recent_messages = chat_history[-6:] if len(chat_history) > 6 else chat_history
|
| 19 |
+
|
| 20 |
+
# Add system message with user context
|
| 21 |
+
system_message = "You are a helpful and concise assistant."
|
| 22 |
+
if user_info.get("name") and user_info["name"] != "Unknown":
|
| 23 |
+
system_message += f" The user's name is {user_info['name']}."
|
| 24 |
+
|
| 25 |
+
messages = [{"role": "system", "content": system_message}] + recent_messages
|
| 26 |
+
|
| 27 |
+
try:
|
| 28 |
+
if not OPENAI_API_KEY:
|
| 29 |
+
return {"response": "Sorry, AI chat is not available. Please check your OpenAI API key configuration."}
|
| 30 |
+
|
| 31 |
+
response = llm.invoke(messages)
|
| 32 |
+
|
| 33 |
+
# Add AI response to history
|
| 34 |
+
chat_history.append({"role": "assistant", "content": response.content})
|
| 35 |
+
|
| 36 |
+
return {"response": response.content}
|
| 37 |
+
except Exception as e:
|
| 38 |
+
print(f"Error in chat_with_memory: {e}")
|
| 39 |
+
return {"response": "Sorry, something went wrong: " + str(e)}
|
| 40 |
+
|
| 41 |
+
class ChatState(TypedDict):
|
| 42 |
+
user_message: str
|
| 43 |
+
response: str
|
| 44 |
+
user_info: dict
|
| 45 |
+
|
| 46 |
+
# --- Build LangGraph ---
|
| 47 |
+
graph = StateGraph(ChatState)
|
| 48 |
+
graph.add_node("chat", RunnableLambda(chat_with_memory))
|
| 49 |
+
graph.set_entry_point("chat")
|
| 50 |
+
graph.add_edge("chat", END)
|
| 51 |
+
chat_graph = graph.compile()
|
| 52 |
+
|
| 53 |
+
async def process_message(user_message: str, user_info: dict = None):
|
| 54 |
+
"""Process a message through the AI chat system"""
|
| 55 |
+
if user_info is None:
|
| 56 |
+
user_info = {}
|
| 57 |
+
|
| 58 |
+
result = await chat_graph.ainvoke({
|
| 59 |
+
"user_message": user_message,
|
| 60 |
+
"user_info": user_info
|
| 61 |
+
})
|
| 62 |
+
|
| 63 |
+
return result["response"]
|
api_routes.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Request, HTTPException, Query
|
| 2 |
+
from fastapi.responses import PlainTextResponse, JSONResponse
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
from typing import Optional
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
from config import VERIFY_TOKEN, supabase, OPENAI_API_KEY, WHATSAPP_API_TOKEN
|
| 8 |
+
from database import get_user, list_users, update_user_name
|
| 9 |
+
from ai_chat import process_message
|
| 10 |
+
|
| 11 |
+
router = APIRouter()
|
| 12 |
+
|
| 13 |
+
# --- Webhook Verification ---
|
| 14 |
+
@router.get("/webhook")
|
| 15 |
+
def verify_webhook(
|
| 16 |
+
hub_mode: str = Query(None, alias="hub.mode"),
|
| 17 |
+
hub_token: str = Query(None, alias="hub.verify_token"),
|
| 18 |
+
hub_challenge: str = Query(None, alias="hub.challenge")
|
| 19 |
+
):
|
| 20 |
+
if hub_mode == "subscribe" and hub_token == VERIFY_TOKEN:
|
| 21 |
+
return PlainTextResponse(hub_challenge)
|
| 22 |
+
raise HTTPException(status_code=403, detail="Invalid token")
|
| 23 |
+
|
| 24 |
+
# --- Optional Direct Chat Endpoint ---
|
| 25 |
+
class ChatRequest(BaseModel):
|
| 26 |
+
message: str
|
| 27 |
+
model: Optional[str] = "gpt-4o-mini"
|
| 28 |
+
max_tokens: Optional[int] = 1500
|
| 29 |
+
|
| 30 |
+
class ChatResponse(BaseModel):
|
| 31 |
+
response: str
|
| 32 |
+
model: str
|
| 33 |
+
tokens_used: Optional[int] = None
|
| 34 |
+
|
| 35 |
+
@router.post("/chat", response_model=ChatResponse)
|
| 36 |
+
async def chat_with_ai(request: ChatRequest):
|
| 37 |
+
response = await process_message(request.message)
|
| 38 |
+
return ChatResponse(response=response, model=request.model)
|
| 39 |
+
|
| 40 |
+
# --- Health ---
|
| 41 |
+
@router.get("/ping")
|
| 42 |
+
def ping():
|
| 43 |
+
return {"pong": True}
|
| 44 |
+
|
| 45 |
+
@router.get("/health")
|
| 46 |
+
def health_check():
|
| 47 |
+
status = {
|
| 48 |
+
"api": "healthy",
|
| 49 |
+
"whatsapp": "configured" if WHATSAPP_API_TOKEN else "not configured",
|
| 50 |
+
"openai": "configured" if OPENAI_API_KEY else "not configured",
|
| 51 |
+
"supabase": "configured" if supabase else "not configured"
|
| 52 |
+
}
|
| 53 |
+
return status
|
| 54 |
+
|
| 55 |
+
# --- User Management Endpoints ---
|
| 56 |
+
@router.get("/users/{wa_id}")
|
| 57 |
+
async def get_user_endpoint(wa_id: str):
|
| 58 |
+
"""Get user by WhatsApp ID"""
|
| 59 |
+
if not supabase:
|
| 60 |
+
raise HTTPException(status_code=503, detail="Database not configured")
|
| 61 |
+
|
| 62 |
+
user = await get_user(wa_id)
|
| 63 |
+
if user:
|
| 64 |
+
return user
|
| 65 |
+
else:
|
| 66 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 67 |
+
|
| 68 |
+
@router.get("/users")
|
| 69 |
+
async def list_users_endpoint(limit: int = 50, offset: int = 0):
|
| 70 |
+
"""List all users with pagination"""
|
| 71 |
+
if not supabase:
|
| 72 |
+
raise HTTPException(status_code=503, detail="Database not configured")
|
| 73 |
+
|
| 74 |
+
result = await list_users(limit, offset)
|
| 75 |
+
return result
|
| 76 |
+
|
| 77 |
+
@router.put("/users/{wa_id}")
|
| 78 |
+
async def update_user_endpoint(wa_id: str, name: str):
|
| 79 |
+
"""Update user name"""
|
| 80 |
+
if not supabase:
|
| 81 |
+
raise HTTPException(status_code=503, detail="Database not configured")
|
| 82 |
+
|
| 83 |
+
user = await update_user_name(wa_id, name)
|
| 84 |
+
if user:
|
| 85 |
+
return user
|
| 86 |
+
else:
|
| 87 |
+
raise HTTPException(status_code=404, detail="User not found")
|
config.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from supabase import create_client, Client
|
| 3 |
+
from langchain_openai import ChatOpenAI
|
| 4 |
+
|
| 5 |
+
# --- Environment Variables ---
|
| 6 |
+
VERIFY_TOKEN = os.getenv("VERIFY_TOKEN", "")
|
| 7 |
+
PHONE_NUMBER_ID = os.getenv("PHONE_NUMBER_ID", "")
|
| 8 |
+
WHATSAPP_API_TOKEN = os.getenv("WHATSAPP_API_TOKEN", "")
|
| 9 |
+
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
|
| 10 |
+
SUPABASE_URL = os.getenv("SUPABASE_URL", "")
|
| 11 |
+
SUPABASE_KEY = os.getenv("SUPABASE_KEY", "")
|
| 12 |
+
|
| 13 |
+
# --- Supabase Client Setup ---
|
| 14 |
+
supabase: Client = None
|
| 15 |
+
if SUPABASE_URL and SUPABASE_KEY and SUPABASE_URL.strip() and SUPABASE_KEY.strip():
|
| 16 |
+
try:
|
| 17 |
+
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 18 |
+
print("Supabase client initialized successfully")
|
| 19 |
+
except Exception as e:
|
| 20 |
+
print(f"Warning: Failed to initialize Supabase client: {e}")
|
| 21 |
+
supabase = None
|
| 22 |
+
else:
|
| 23 |
+
print("Warning: Supabase credentials not set. Database functionality will be limited.")
|
| 24 |
+
|
| 25 |
+
# --- OpenAI Client Setup ---
|
| 26 |
+
llm = ChatOpenAI(openai_api_key=OPENAI_API_KEY, model="gpt-4o-mini")
|
| 27 |
+
|
| 28 |
+
# Initialize LLM only if API key is available
|
| 29 |
+
if not OPENAI_API_KEY:
|
| 30 |
+
print("Warning: OPENAI_API_KEY not set. AI functionality will be limited.")
|
database.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
from config import supabase
|
| 3 |
+
|
| 4 |
+
async def get_or_create_user(wa_id: str, name: str = None) -> dict:
|
| 5 |
+
"""Get existing user or create new one"""
|
| 6 |
+
if not supabase:
|
| 7 |
+
return {"wa_id": wa_id, "name": name or "Unknown"}
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
# Try to get existing user
|
| 11 |
+
response = supabase.table("users").select("*").eq("wa_id", wa_id).execute()
|
| 12 |
+
|
| 13 |
+
if response.data:
|
| 14 |
+
# User exists, update if name provided
|
| 15 |
+
user = response.data[0]
|
| 16 |
+
if name and name != user.get("name"):
|
| 17 |
+
supabase.table("users").update({"name": name, "updated_at": datetime.utcnow().isoformat()}).eq("wa_id", wa_id).execute()
|
| 18 |
+
user["name"] = name
|
| 19 |
+
return user
|
| 20 |
+
else:
|
| 21 |
+
# Create new user
|
| 22 |
+
new_user = {
|
| 23 |
+
"wa_id": wa_id,
|
| 24 |
+
"name": name or "Unknown",
|
| 25 |
+
"created_at": datetime.utcnow().isoformat(),
|
| 26 |
+
"updated_at": datetime.utcnow().isoformat()
|
| 27 |
+
}
|
| 28 |
+
response = supabase.table("users").insert(new_user).execute()
|
| 29 |
+
return response.data[0] if response.data else new_user
|
| 30 |
+
|
| 31 |
+
except Exception as e:
|
| 32 |
+
print(f"Error in get_or_create_user: {e}")
|
| 33 |
+
return {"wa_id": wa_id, "name": name or "Unknown"}
|
| 34 |
+
|
| 35 |
+
async def update_user_activity(wa_id: str):
|
| 36 |
+
"""Update user's last activity timestamp"""
|
| 37 |
+
if not supabase:
|
| 38 |
+
return
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
supabase.table("users").update({"updated_at": datetime.utcnow().isoformat()}).eq("wa_id", wa_id).execute()
|
| 42 |
+
except Exception as e:
|
| 43 |
+
print(f"Error updating user activity: {e}")
|
| 44 |
+
|
| 45 |
+
async def get_user(wa_id: str):
|
| 46 |
+
"""Get user by WhatsApp ID"""
|
| 47 |
+
if not supabase:
|
| 48 |
+
return None
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
response = supabase.table("users").select("*").eq("wa_id", wa_id).execute()
|
| 52 |
+
return response.data[0] if response.data else None
|
| 53 |
+
except Exception as e:
|
| 54 |
+
print(f"Error getting user: {e}")
|
| 55 |
+
return None
|
| 56 |
+
|
| 57 |
+
async def list_users(limit: int = 50, offset: int = 0):
|
| 58 |
+
"""List all users with pagination"""
|
| 59 |
+
if not supabase:
|
| 60 |
+
return {"users": [], "count": 0}
|
| 61 |
+
|
| 62 |
+
try:
|
| 63 |
+
response = supabase.table("users").select("*").range(offset, offset + limit - 1).order("created_at", desc=True).execute()
|
| 64 |
+
return {"users": response.data, "count": len(response.data)}
|
| 65 |
+
except Exception as e:
|
| 66 |
+
print(f"Error listing users: {e}")
|
| 67 |
+
return {"users": [], "count": 0}
|
| 68 |
+
|
| 69 |
+
async def update_user_name(wa_id: str, name: str):
|
| 70 |
+
"""Update user name"""
|
| 71 |
+
if not supabase:
|
| 72 |
+
return None
|
| 73 |
+
|
| 74 |
+
try:
|
| 75 |
+
response = supabase.table("users").update({
|
| 76 |
+
"name": name,
|
| 77 |
+
"updated_at": datetime.utcnow().isoformat()
|
| 78 |
+
}).eq("wa_id", wa_id).execute()
|
| 79 |
+
|
| 80 |
+
return response.data[0] if response.data else None
|
| 81 |
+
except Exception as e:
|
| 82 |
+
print(f"Error updating user: {e}")
|
| 83 |
+
return None
|
main.py
CHANGED
|
@@ -1,152 +1,23 @@
|
|
| 1 |
-
import
|
| 2 |
-
from fastapi import
|
| 3 |
-
from fastapi.responses import PlainTextResponse, JSONResponse
|
| 4 |
-
import httpx
|
| 5 |
-
from openai import OpenAI
|
| 6 |
-
from pydantic import BaseModel
|
| 7 |
-
from typing import Optional
|
| 8 |
-
from langgraph.graph import StateGraph, END
|
| 9 |
-
from langchain_core.runnables import RunnableLambda
|
| 10 |
-
from langchain_core.memory import BaseMemory
|
| 11 |
-
from langchain_core.chat_history import BaseChatMessageHistory
|
| 12 |
-
from langchain_core.messages import HumanMessage, AIMessage
|
| 13 |
-
from langchain_openai import ChatOpenAI
|
| 14 |
-
from supabase import create_client, Client
|
| 15 |
-
from datetime import datetime
|
| 16 |
|
| 17 |
-
#
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
SUPABASE_URL = os.getenv("SUPABASE_URL", "")
|
| 24 |
-
SUPABASE_KEY = os.getenv("SUPABASE_KEY", "")
|
| 25 |
|
| 26 |
-
# ---
|
| 27 |
-
|
| 28 |
-
if SUPABASE_URL and SUPABASE_KEY and SUPABASE_URL.strip() and SUPABASE_KEY.strip():
|
| 29 |
-
try:
|
| 30 |
-
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 31 |
-
print("Supabase client initialized successfully")
|
| 32 |
-
except Exception as e:
|
| 33 |
-
print(f"Warning: Failed to initialize Supabase client: {e}")
|
| 34 |
-
supabase = None
|
| 35 |
-
else:
|
| 36 |
-
print("Warning: Supabase credentials not set. Database functionality will be limited.")
|
| 37 |
-
|
| 38 |
-
# --- User Management Functions ---
|
| 39 |
-
async def get_or_create_user(wa_id: str, name: str = None) -> dict:
|
| 40 |
-
"""Get existing user or create new one"""
|
| 41 |
-
if not supabase:
|
| 42 |
-
return {"wa_id": wa_id, "name": name or "Unknown"}
|
| 43 |
-
|
| 44 |
-
try:
|
| 45 |
-
# Try to get existing user
|
| 46 |
-
response = supabase.table("users").select("*").eq("wa_id", wa_id).execute()
|
| 47 |
-
|
| 48 |
-
if response.data:
|
| 49 |
-
# User exists, update if name provided
|
| 50 |
-
user = response.data[0]
|
| 51 |
-
if name and name != user.get("name"):
|
| 52 |
-
supabase.table("users").update({"name": name, "updated_at": datetime.utcnow().isoformat()}).eq("wa_id", wa_id).execute()
|
| 53 |
-
user["name"] = name
|
| 54 |
-
return user
|
| 55 |
-
else:
|
| 56 |
-
# Create new user
|
| 57 |
-
new_user = {
|
| 58 |
-
"wa_id": wa_id,
|
| 59 |
-
"name": name or "Unknown",
|
| 60 |
-
"created_at": datetime.utcnow().isoformat(),
|
| 61 |
-
"updated_at": datetime.utcnow().isoformat()
|
| 62 |
-
}
|
| 63 |
-
response = supabase.table("users").insert(new_user).execute()
|
| 64 |
-
return response.data[0] if response.data else new_user
|
| 65 |
-
|
| 66 |
-
except Exception as e:
|
| 67 |
-
print(f"Error in get_or_create_user: {e}")
|
| 68 |
-
return {"wa_id": wa_id, "name": name or "Unknown"}
|
| 69 |
-
|
| 70 |
-
async def update_user_activity(wa_id: str):
|
| 71 |
-
"""Update user's last activity timestamp"""
|
| 72 |
-
if not supabase:
|
| 73 |
-
return
|
| 74 |
-
|
| 75 |
-
try:
|
| 76 |
-
supabase.table("users").update({"updated_at": datetime.utcnow().isoformat()}).eq("wa_id", wa_id).execute()
|
| 77 |
-
except Exception as e:
|
| 78 |
-
print(f"Error updating user activity: {e}")
|
| 79 |
-
|
| 80 |
-
# --- OpenAI Client Setup ---
|
| 81 |
-
llm = ChatOpenAI(openai_api_key=OPENAI_API_KEY, model="gpt-4o-mini")
|
| 82 |
-
|
| 83 |
-
# Simple in-memory chat history (no deprecation warnings)
|
| 84 |
-
chat_history = []
|
| 85 |
-
|
| 86 |
-
# Initialize LLM only if API key is available
|
| 87 |
-
if not OPENAI_API_KEY:
|
| 88 |
-
print("Warning: OPENAI_API_KEY not set. AI functionality will be limited.")
|
| 89 |
-
|
| 90 |
-
# --- LangGraph Setup ---
|
| 91 |
-
def chat_with_memory(state):
|
| 92 |
-
user_message = state["user_message"]
|
| 93 |
-
user_info = state.get("user_info", {})
|
| 94 |
-
|
| 95 |
-
# Add user message to history
|
| 96 |
-
chat_history.append({"role": "user", "content": user_message})
|
| 97 |
-
|
| 98 |
-
# Keep only last 6 messages for context
|
| 99 |
-
recent_messages = chat_history[-6:] if len(chat_history) > 6 else chat_history
|
| 100 |
-
|
| 101 |
-
# Add system message with user context
|
| 102 |
-
system_message = "You are a helpful and concise assistant."
|
| 103 |
-
if user_info.get("name") and user_info["name"] != "Unknown":
|
| 104 |
-
system_message += f" The user's name is {user_info['name']}."
|
| 105 |
-
|
| 106 |
-
messages = [{"role": "system", "content": system_message}] + recent_messages
|
| 107 |
-
|
| 108 |
-
try:
|
| 109 |
-
if not OPENAI_API_KEY:
|
| 110 |
-
return {"response": "Sorry, AI chat is not available. Please check your OpenAI API key configuration."}
|
| 111 |
-
|
| 112 |
-
response = llm.invoke(messages)
|
| 113 |
-
|
| 114 |
-
# Add AI response to history
|
| 115 |
-
chat_history.append({"role": "assistant", "content": response.content})
|
| 116 |
-
|
| 117 |
-
return {"response": response.content}
|
| 118 |
-
except Exception as e:
|
| 119 |
-
print(f"Error in chat_with_memory: {e}")
|
| 120 |
-
return {"response": "Sorry, something went wrong: " + str(e)}
|
| 121 |
-
|
| 122 |
-
# --- Build LangGraph ---
|
| 123 |
-
from typing import TypedDict
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
response: str
|
| 128 |
-
user_info: dict
|
| 129 |
|
| 130 |
-
|
| 131 |
-
graph.add_node("chat", RunnableLambda(chat_with_memory))
|
| 132 |
-
graph.set_entry_point("chat")
|
| 133 |
-
graph.add_edge("chat", END)
|
| 134 |
-
chat_graph = graph.compile()
|
| 135 |
-
|
| 136 |
-
# --- Webhook Verification ---
|
| 137 |
-
@app.get("/webhook")
|
| 138 |
-
def verify_webhook(
|
| 139 |
-
hub_mode: str = Query(None, alias="hub.mode"),
|
| 140 |
-
hub_token: str = Query(None, alias="hub.verify_token"),
|
| 141 |
-
hub_challenge: str = Query(None, alias="hub.challenge")
|
| 142 |
-
):
|
| 143 |
-
if hub_mode == "subscribe" and hub_token == VERIFY_TOKEN:
|
| 144 |
-
return PlainTextResponse(hub_challenge)
|
| 145 |
-
raise HTTPException(status_code=403, detail="Invalid token")
|
| 146 |
-
|
| 147 |
-
# --- Webhook Receiver ---
|
| 148 |
@app.post("/webhook")
|
| 149 |
async def receive_message(req: Request):
|
|
|
|
| 150 |
body = await req.json()
|
| 151 |
print("Incoming webhook:", body)
|
| 152 |
|
|
@@ -154,6 +25,7 @@ async def receive_message(req: Request):
|
|
| 154 |
change = body["entry"][0]["changes"][0]["value"]
|
| 155 |
contacts = change.get("contacts")
|
| 156 |
messages = change.get("messages")
|
|
|
|
| 157 |
if not contacts or not messages:
|
| 158 |
return JSONResponse({"status": "ignored", "reason": "no contacts/messages"})
|
| 159 |
|
|
@@ -171,12 +43,11 @@ async def receive_message(req: Request):
|
|
| 171 |
await update_user_activity(wa_id)
|
| 172 |
|
| 173 |
# Process with AI including user context
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
|
| 179 |
-
await send_whatsapp_message(wa_id, ai_result["response"])
|
| 180 |
return JSONResponse({"status": "replied", "user_id": wa_id})
|
| 181 |
|
| 182 |
except Exception as e:
|
|
@@ -185,102 +56,15 @@ async def receive_message(req: Request):
|
|
| 185 |
|
| 186 |
return JSONResponse({"status": "ignored"})
|
| 187 |
|
| 188 |
-
# ---
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
}
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
}
|
| 201 |
-
try:
|
| 202 |
-
resp = await httpx.post(url, headers=headers, json=payload)
|
| 203 |
-
print(f"Sent message → {resp.status_code}: {resp.text}")
|
| 204 |
-
except Exception as e:
|
| 205 |
-
print(f"Failed to send message: {e}")
|
| 206 |
-
|
| 207 |
-
# --- Optional Direct Chat Endpoint ---
|
| 208 |
-
class ChatRequest(BaseModel):
|
| 209 |
-
message: str
|
| 210 |
-
model: Optional[str] = "gpt-4o-mini"
|
| 211 |
-
max_tokens: Optional[int] = 1500
|
| 212 |
-
|
| 213 |
-
class ChatResponse(BaseModel):
|
| 214 |
-
response: str
|
| 215 |
-
model: str
|
| 216 |
-
tokens_used: Optional[int] = None
|
| 217 |
-
|
| 218 |
-
@app.post("/chat", response_model=ChatResponse)
|
| 219 |
-
async def chat_with_ai(request: ChatRequest):
|
| 220 |
-
ai_result = await chat_graph.ainvoke({"user_message": request.message})
|
| 221 |
-
return ChatResponse(response=ai_result["response"], model=request.model)
|
| 222 |
-
|
| 223 |
-
# --- Health ---
|
| 224 |
-
@app.get("/ping")
|
| 225 |
-
def ping():
|
| 226 |
-
return {"pong": True}
|
| 227 |
-
|
| 228 |
-
@app.get("/health")
|
| 229 |
-
def health_check():
|
| 230 |
-
status = {
|
| 231 |
-
"api": "healthy",
|
| 232 |
-
"whatsapp": "configured" if WHATSAPP_API_TOKEN else "not configured",
|
| 233 |
-
"openai": "configured" if OPENAI_API_KEY else "not configured",
|
| 234 |
-
"supabase": "configured" if supabase else "not configured"
|
| 235 |
-
}
|
| 236 |
-
return status
|
| 237 |
-
|
| 238 |
-
# --- User Management Endpoints ---
|
| 239 |
-
@app.get("/users/{wa_id}")
|
| 240 |
-
async def get_user(wa_id: str):
|
| 241 |
-
"""Get user by WhatsApp ID"""
|
| 242 |
-
if not supabase:
|
| 243 |
-
raise HTTPException(status_code=503, detail="Database not configured")
|
| 244 |
-
|
| 245 |
-
try:
|
| 246 |
-
response = supabase.table("users").select("*").eq("wa_id", wa_id).execute()
|
| 247 |
-
if response.data:
|
| 248 |
-
return response.data[0]
|
| 249 |
-
else:
|
| 250 |
-
raise HTTPException(status_code=404, detail="User not found")
|
| 251 |
-
except Exception as e:
|
| 252 |
-
print(f"Error getting user: {e}")
|
| 253 |
-
raise HTTPException(status_code=500, detail="Database error")
|
| 254 |
-
|
| 255 |
-
@app.get("/users")
|
| 256 |
-
async def list_users(limit: int = 50, offset: int = 0):
|
| 257 |
-
"""List all users with pagination"""
|
| 258 |
-
if not supabase:
|
| 259 |
-
raise HTTPException(status_code=503, detail="Database not configured")
|
| 260 |
-
|
| 261 |
-
try:
|
| 262 |
-
response = supabase.table("users").select("*").range(offset, offset + limit - 1).order("created_at", desc=True).execute()
|
| 263 |
-
return {"users": response.data, "count": len(response.data)}
|
| 264 |
-
except Exception as e:
|
| 265 |
-
print(f"Error listing users: {e}")
|
| 266 |
-
raise HTTPException(status_code=500, detail="Database error")
|
| 267 |
-
|
| 268 |
-
@app.put("/users/{wa_id}")
|
| 269 |
-
async def update_user(wa_id: str, name: str):
|
| 270 |
-
"""Update user name"""
|
| 271 |
-
if not supabase:
|
| 272 |
-
raise HTTPException(status_code=503, detail="Database not configured")
|
| 273 |
-
|
| 274 |
-
try:
|
| 275 |
-
response = supabase.table("users").update({
|
| 276 |
-
"name": name,
|
| 277 |
-
"updated_at": datetime.utcnow().isoformat()
|
| 278 |
-
}).eq("wa_id", wa_id).execute()
|
| 279 |
-
|
| 280 |
-
if response.data:
|
| 281 |
-
return response.data[0]
|
| 282 |
-
else:
|
| 283 |
-
raise HTTPException(status_code=404, detail="User not found")
|
| 284 |
-
except Exception as e:
|
| 285 |
-
print(f"Error updating user: {e}")
|
| 286 |
-
raise HTTPException(status_code=500, detail="Database error")
|
|
|
|
| 1 |
+
from fastapi import FastAPI, Request
|
| 2 |
+
from fastapi.responses import JSONResponse
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
+
# Import our modular components
|
| 5 |
+
from config import supabase
|
| 6 |
+
from database import get_or_create_user, update_user_activity
|
| 7 |
+
from whatsapp import send_whatsapp_message
|
| 8 |
+
from ai_chat import process_message
|
| 9 |
+
from api_routes import router
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
# --- FastAPI App Setup ---
|
| 12 |
+
app = FastAPI(title="PropAgent", description="WhatsApp AI Agent with Supabase integration")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
+
# Include all API routes
|
| 15 |
+
app.include_router(router)
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
# --- WhatsApp Webhook Handler ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
@app.post("/webhook")
|
| 19 |
async def receive_message(req: Request):
|
| 20 |
+
"""Main webhook endpoint for receiving WhatsApp messages"""
|
| 21 |
body = await req.json()
|
| 22 |
print("Incoming webhook:", body)
|
| 23 |
|
|
|
|
| 25 |
change = body["entry"][0]["changes"][0]["value"]
|
| 26 |
contacts = change.get("contacts")
|
| 27 |
messages = change.get("messages")
|
| 28 |
+
|
| 29 |
if not contacts or not messages:
|
| 30 |
return JSONResponse({"status": "ignored", "reason": "no contacts/messages"})
|
| 31 |
|
|
|
|
| 43 |
await update_user_activity(wa_id)
|
| 44 |
|
| 45 |
# Process with AI including user context
|
| 46 |
+
ai_response = await process_message(user_message, user_info)
|
| 47 |
+
|
| 48 |
+
# Send response back to WhatsApp
|
| 49 |
+
await send_whatsapp_message(wa_id, ai_response)
|
| 50 |
|
|
|
|
| 51 |
return JSONResponse({"status": "replied", "user_id": wa_id})
|
| 52 |
|
| 53 |
except Exception as e:
|
|
|
|
| 56 |
|
| 57 |
return JSONResponse({"status": "ignored"})
|
| 58 |
|
| 59 |
+
# --- Application Startup Event ---
|
| 60 |
+
@app.on_event("startup")
|
| 61 |
+
async def startup_event():
|
| 62 |
+
"""Log startup information"""
|
| 63 |
+
print("=" * 50)
|
| 64 |
+
print("PropAgent WhatsApp AI Agent Starting...")
|
| 65 |
+
print(f"Supabase: {'✅ Connected' if supabase else '❌ Not configured'}")
|
| 66 |
+
print("=" * 50)
|
| 67 |
+
|
| 68 |
+
if __name__ == "__main__":
|
| 69 |
+
import uvicorn
|
| 70 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
whatsapp.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import httpx
|
| 2 |
+
from config import PHONE_NUMBER_ID, WHATSAPP_API_TOKEN
|
| 3 |
+
|
| 4 |
+
async def send_whatsapp_message(wa_id: str, message: str):
|
| 5 |
+
"""Send a message via WhatsApp Business API"""
|
| 6 |
+
url = f"https://graph.facebook.com/v18.0/{PHONE_NUMBER_ID}/messages"
|
| 7 |
+
headers = {
|
| 8 |
+
"Authorization": f"Bearer {WHATSAPP_API_TOKEN}",
|
| 9 |
+
"Content-Type": "application/json"
|
| 10 |
+
}
|
| 11 |
+
payload = {
|
| 12 |
+
"messaging_product": "whatsapp",
|
| 13 |
+
"to": wa_id,
|
| 14 |
+
"type": "text",
|
| 15 |
+
"text": {"body": message}
|
| 16 |
+
}
|
| 17 |
+
try:
|
| 18 |
+
resp = await httpx.post(url, headers=headers, json=payload)
|
| 19 |
+
print(f"Sent message → {resp.status_code}: {resp.text}")
|
| 20 |
+
return resp.status_code == 200
|
| 21 |
+
except Exception as e:
|
| 22 |
+
print(f"Failed to send message: {e}")
|
| 23 |
+
return False
|