zenaight commited on
Commit
90f252d
·
1 Parent(s): f547cc1

simplified and broken up code

Browse files
Files changed (8) hide show
  1. README.md +23 -1
  2. __init__.py +2 -0
  3. ai_chat.py +63 -0
  4. api_routes.py +87 -0
  5. config.py +30 -0
  6. database.py +83 -0
  7. main.py +31 -247
  8. 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 os
2
- from fastapi import FastAPI, Request, HTTPException, Query
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
- # --- App and Config ---
18
- app = FastAPI()
19
- VERIFY_TOKEN = os.getenv("VERIFY_TOKEN", "")
20
- PHONE_NUMBER_ID = os.getenv("PHONE_NUMBER_ID", "")
21
- WHATSAPP_API_TOKEN = os.getenv("WHATSAPP_API_TOKEN", "")
22
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
23
- SUPABASE_URL = os.getenv("SUPABASE_URL", "")
24
- SUPABASE_KEY = os.getenv("SUPABASE_KEY", "")
25
 
26
- # --- Supabase Client Setup ---
27
- supabase: Client = None
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
- class ChatState(TypedDict):
126
- user_message: str
127
- response: str
128
- user_info: dict
129
 
130
- graph = StateGraph(ChatState)
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
- ai_result = await chat_graph.ainvoke({
175
- "user_message": user_message,
176
- "user_info": user_info
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
- # --- WhatsApp Sender ---
189
- async def send_whatsapp_message(wa_id: str, message: str):
190
- url = f"https://graph.facebook.com/v18.0/{PHONE_NUMBER_ID}/messages"
191
- headers = {
192
- "Authorization": f"Bearer {WHATSAPP_API_TOKEN}",
193
- "Content-Type": "application/json"
194
- }
195
- payload = {
196
- "messaging_product": "whatsapp",
197
- "to": wa_id,
198
- "type": "text",
199
- "text": {"body": message}
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