Mr-Help commited on
Commit
b057ee3
·
verified ·
1 Parent(s): f56b3f1

Upload 6 files

Browse files
Files changed (6) hide show
  1. config.py +28 -0
  2. db.py +7 -0
  3. greenapi.py +135 -0
  4. main.py +225 -0
  5. messages.py +11 -0
  6. sessions.py +58 -0
config.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ BOTS = {
7
+ "device_01": {
8
+ "instance_id": os.getenv("GREENAPI_INSTANCE_ID_1", "7105210836"),
9
+ "token": os.getenv("GREENAPI_TOKEN_1", "TOKEN1"),
10
+ "label": "ADK Bot 1",
11
+ },
12
+ "device_02": {
13
+ "instance_id": os.getenv("GREENAPI_INSTANCE_ID_2", "7105222222"),
14
+ "token": os.getenv("GREENAPI_TOKEN_2", "TOKEN2"),
15
+ "label": "ADK Bot 2",
16
+ }
17
+ }
18
+
19
+ COMPLAINT_FORM_URL = os.getenv(
20
+ "COMPLAINT_FORM_URL",
21
+ "https://mrhelp92.github.io/Complaint-form/"
22
+ )
23
+
24
+ SUPABASE_URL = os.getenv("SUPABASE_URL", "")
25
+ SUPABASE_SERVICE_ROLE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY", "")
26
+
27
+ TIMEOUT_SEC = int(os.getenv("TIMEOUT_SEC", "60"))
28
+ APP_BASE_URL = os.getenv("APP_BASE_URL", "") # optional
db.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from supabase import create_client
2
+ from config import SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY
3
+
4
+ if not SUPABASE_URL or not SUPABASE_SERVICE_ROLE_KEY:
5
+ raise ValueError("Missing SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY")
6
+
7
+ supabase = create_client(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY)
greenapi.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ from config import BOTS, TIMEOUT_SEC
3
+
4
+
5
+ def get_bot_config(device_key: str):
6
+ bot = BOTS.get(device_key)
7
+ if not bot:
8
+ raise ValueError(f"Unknown device_key: {device_key}")
9
+ return bot
10
+
11
+
12
+ def get_device_key_by_instance_id(instance_id: str):
13
+ for device_key, bot in BOTS.items():
14
+ if str(bot.get("instance_id")) == str(instance_id):
15
+ return device_key
16
+ return None
17
+
18
+
19
+ def build_greenapi_base(instance_id: str) -> str:
20
+ return f"https://7105.api.greenapi.com/waInstance{instance_id}"
21
+
22
+
23
+ def get_greenapi_urls(device_key: str):
24
+ bot = get_bot_config(device_key)
25
+
26
+ instance_id = bot["instance_id"]
27
+ token = bot["token"]
28
+
29
+ base = build_greenapi_base(instance_id)
30
+
31
+ return {
32
+ "text": f"{base}/sendMessage/{token}",
33
+ "reply_buttons": f"{base}/sendInteractiveButtonsReply/{token}",
34
+ "url_buttons": f"{base}/sendInteractiveButtons/{token}",
35
+ }
36
+
37
+
38
+ def send_text_message(device_key: str, chat_id: str, message: str):
39
+ urls = get_greenapi_urls(device_key)
40
+
41
+ payload = {
42
+ "chatId": chat_id,
43
+ "message": message
44
+ }
45
+
46
+ headers = {"Content-Type": "application/json"}
47
+
48
+ resp = requests.post(
49
+ urls["text"],
50
+ json=payload,
51
+ headers=headers,
52
+ timeout=TIMEOUT_SEC
53
+ )
54
+ return resp.status_code, (resp.text or "")[:1500]
55
+
56
+
57
+ def send_reply_buttons(device_key: str, chat_id: str, header: str, body: str, footer: str, buttons: list):
58
+ urls = get_greenapi_urls(device_key)
59
+
60
+ payload = {
61
+ "chatId": chat_id,
62
+ "header": header,
63
+ "body": body,
64
+ "footer": footer,
65
+ "buttons": buttons
66
+ }
67
+
68
+ resp = requests.post(
69
+ urls["reply_buttons"],
70
+ json=payload,
71
+ timeout=TIMEOUT_SEC
72
+ )
73
+ return resp.status_code, (resp.text or "")[:1500]
74
+
75
+
76
+ def send_url_button(device_key: str, chat_id: str, header: str, body: str, footer: str, url: str, button_text: str = "فتح الرابط"):
77
+ urls = get_greenapi_urls(device_key)
78
+
79
+ payload = {
80
+ "chatId": chat_id,
81
+ "header": header,
82
+ "body": body,
83
+ "footer": footer,
84
+ "buttons": [
85
+ {
86
+ "type": "url",
87
+ "buttonId": "1",
88
+ "buttonText": button_text,
89
+ "url": url
90
+ }
91
+ ]
92
+ }
93
+
94
+ resp = requests.post(
95
+ urls["url_buttons"],
96
+ json=payload,
97
+ timeout=TIMEOUT_SEC
98
+ )
99
+ return resp.status_code, (resp.text or "")[:1500]
100
+
101
+
102
+ def extract_text_message(msg_data: dict) -> str:
103
+ if not isinstance(msg_data, dict):
104
+ return ""
105
+
106
+ t = (msg_data.get("typeMessage") or "").strip()
107
+
108
+ if t == "textMessage":
109
+ return ((msg_data.get("textMessageData") or {}).get("textMessage") or "").strip()
110
+
111
+ if t == "extendedTextMessage":
112
+ return ((msg_data.get("extendedTextMessageData") or {}).get("text") or "").strip()
113
+
114
+ if t == "interactiveButtonsResponse":
115
+ r = msg_data.get("interactiveButtonsResponse") or {}
116
+ txt = (r.get("selectedDisplayText") or "").strip()
117
+ if txt:
118
+ return txt
119
+ return (r.get("selectedId") or "").strip()
120
+
121
+ for path in [
122
+ ("textMessageData", "textMessage"),
123
+ ("extendedTextMessageData", "text"),
124
+ ("interactiveButtonsResponse", "selectedDisplayText"),
125
+ ("interactiveButtonsResponse", "selectedId"),
126
+ ("text",),
127
+ ("message",),
128
+ ]:
129
+ cur = msg_data
130
+ for k in path:
131
+ cur = cur.get(k) if isinstance(cur, dict) else None
132
+ if isinstance(cur, str) and cur.strip():
133
+ return cur.strip()
134
+
135
+ return ""
main.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from fastapi import FastAPI, Request
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from fastapi.responses import JSONResponse
5
+
6
+ from greenapi import (
7
+ extract_text_message,
8
+ get_device_key_by_instance_id,
9
+ send_text_message,
10
+ send_url_button,
11
+ )
12
+
13
+ from engine.conversation_engine import process_message
14
+ from messages import log_message
15
+
16
+ from handoff.sales import create_sales_handoff
17
+ from handoff.support import create_support_handoff
18
+
19
+ app = FastAPI()
20
+
21
+ app.add_middleware(
22
+ CORSMiddleware,
23
+ allow_origins=["*"],
24
+ allow_credentials=False,
25
+ allow_methods=["POST", "OPTIONS"],
26
+ allow_headers=["*"],
27
+ )
28
+
29
+
30
+ @app.get("/health")
31
+ async def health():
32
+ return {"ok": True, "service": "up"}
33
+
34
+
35
+ @app.post("/receive")
36
+ async def webhook_receiver(req: Request):
37
+ try:
38
+ body = await req.json()
39
+ except Exception:
40
+ raw = await req.body()
41
+ print("===== RECEIVED RAW =====")
42
+ print(raw)
43
+ print("========================")
44
+ return {"ok": True}
45
+
46
+ print("===== RECEIVED JSON =====")
47
+ print(json.dumps(body, ensure_ascii=False, indent=2))
48
+ print("=========================")
49
+
50
+ # نتعامل فقط مع الرسائل الواردة
51
+ if body.get("typeWebhook") != "incomingMessageReceived":
52
+ return {"ok": True, "ignored": True}
53
+
54
+ instance_data = body.get("instanceData") or {}
55
+ sender_data = body.get("senderData") or {}
56
+ msg_data = body.get("messageData") or {}
57
+
58
+ # مهم: ده اللي هنستخدمه لتحديد أي bot/device استقبل الرسالة
59
+ instance_id = str(instance_data.get("idInstance") or "")
60
+ chat_id = sender_data.get("chatId")
61
+ text_message = extract_text_message(msg_data)
62
+
63
+ print(">>> instance_id:", instance_id)
64
+ print(">>> chat_id:", chat_id)
65
+ print(">>> text_message:", text_message)
66
+
67
+ if not instance_id or not chat_id or not text_message:
68
+ return {"ok": True, "skipped": True}
69
+
70
+ device_key = get_device_key_by_instance_id(instance_id)
71
+ if not device_key:
72
+ print(">>> Unknown instance_id:", instance_id)
73
+ return {
74
+ "ok": True,
75
+ "skipped": True,
76
+ "reason": "unknown_instance_id"
77
+ }
78
+
79
+ customer_phone = chat_id.replace("@c.us", "")
80
+ bot_number = device_key
81
+
82
+ # سجل الرسالة الواردة
83
+ try:
84
+ log_message(customer_phone, bot_number, "in", text_message)
85
+ except Exception as e:
86
+ print(">>> failed to log incoming message:", str(e))
87
+
88
+ # شغّل الـ engine
89
+ try:
90
+ result = process_message(
91
+ bot_number=bot_number,
92
+ customer_phone=customer_phone,
93
+ text=text_message
94
+ )
95
+ except Exception as e:
96
+ print(">>> engine error:", str(e))
97
+
98
+ fallback_reply = "تمام، حصلت مشكلة بسيطة. ممكن تعيد إرسال رسالتك مرة تانية؟"
99
+ try:
100
+ send_text_message(device_key, chat_id, fallback_reply)
101
+ log_message(customer_phone, bot_number, "out", fallback_reply)
102
+ except Exception as send_err:
103
+ print(">>> failed to send fallback:", str(send_err))
104
+
105
+ return JSONResponse(
106
+ status_code=200,
107
+ content={"ok": True, "error": str(e), "fallback_sent": True}
108
+ )
109
+
110
+ reply = (result.get("reply") or "").strip()
111
+ action = result.get("action")
112
+
113
+ # 1) لو فيه URL button
114
+ if action and action.get("type") == "url_button":
115
+ try:
116
+ status, txt = send_url_button(
117
+ device_key=device_key,
118
+ chat_id=chat_id,
119
+ header=action.get("header", "مرحباً بك"),
120
+ body=action.get("body", reply),
121
+ footer=action.get("footer", ""),
122
+ url=action.get("url"),
123
+ button_text=action.get("button_text", "فتح الرابط")
124
+ )
125
+ print(">>> sent url button:", status, txt)
126
+
127
+ try:
128
+ log_message(customer_phone, bot_number, "out", action.get("body", reply))
129
+ except Exception as e:
130
+ print(">>> failed to log outgoing url button body:", str(e))
131
+
132
+ except Exception as e:
133
+ print(">>> failed to send url button:", str(e))
134
+ # fallback text
135
+ fallback_reply = action.get("body", reply) or "تمام، افتح الرابط من فضلك."
136
+ try:
137
+ send_text_message(device_key, chat_id, fallback_reply)
138
+ log_message(customer_phone, bot_number, "out", fallback_reply)
139
+ except Exception as send_err:
140
+ print(">>> failed to send url fallback:", str(send_err))
141
+
142
+ # 2) غير كده ابعت text
143
+ else:
144
+ try:
145
+ status, txt = send_text_message(device_key, chat_id, reply)
146
+ print(">>> sent text:", status, txt)
147
+
148
+ try:
149
+ log_message(customer_phone, bot_number, "out", reply)
150
+ except Exception as e:
151
+ print(">>> failed to log outgoing text:", str(e))
152
+
153
+ except Exception as e:
154
+ print(">>> failed to send text:", str(e))
155
+
156
+ # 3) لو فيه handoff action
157
+ if action and action.get("type") == "handoff":
158
+ department = action.get("department")
159
+ summary = action.get("summary", "")
160
+ metadata = result.get("flow_data", {})
161
+
162
+ try:
163
+ if department == "sales":
164
+ create_sales_handoff(
165
+ customer_phone=customer_phone,
166
+ bot_number=bot_number,
167
+ summary=summary,
168
+ metadata=metadata
169
+ )
170
+
171
+ elif department == "support":
172
+ create_support_handoff(
173
+ customer_phone=customer_phone,
174
+ bot_number=bot_number,
175
+ summary=summary,
176
+ metadata=metadata
177
+ )
178
+
179
+ except Exception as e:
180
+ print(">>> failed to create handoff:", str(e))
181
+
182
+ return {
183
+ "ok": True,
184
+ "device_key": device_key,
185
+ "customer_phone": customer_phone,
186
+ "state": result.get("next_state"),
187
+ "has_action": bool(action),
188
+ }
189
+
190
+
191
+ @app.post("/complaints/callback")
192
+ async def complaints_callback(req: Request):
193
+ """
194
+ متوقع أن خدمة الشكاوى تبعت:
195
+ {
196
+ "uid": "device_01__2010xxxxxxx",
197
+ "complaint_number": "CMP-1001",
198
+ "status": "submitted"
199
+ }
200
+ """
201
+ try:
202
+ payload = await req.json()
203
+ except Exception:
204
+ return JSONResponse(status_code=400, content={"ok": False, "error": "invalid json"})
205
+
206
+ print("===== COMPLAINT CALLBACK =====")
207
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
208
+ print("==============================")
209
+
210
+ uid = payload.get("uid", "")
211
+ complaint_number = payload.get("complaint_number", "")
212
+ status = payload.get("status", "")
213
+
214
+ # دي حالياً مجرد نقطة استقبال
215
+ # لاحقاً هنستخدم uid عشان:
216
+ # - نحدد customer_phone + bot_number
217
+ # - نبعث للعميل رسالة تأكيد فيها رقم الشكوى
218
+ # - نسجل الشكوى في جدول complaints أو handoff_requests
219
+
220
+ return {
221
+ "ok": True,
222
+ "uid": uid,
223
+ "complaint_number": complaint_number,
224
+ "status": status
225
+ }
messages.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from db import supabase
2
+
3
+
4
+ def log_message(customer_phone: str, bot_number: str, direction: str, message: str):
5
+ payload = {
6
+ "customer_phone": customer_phone,
7
+ "bot_number": bot_number,
8
+ "direction": direction,
9
+ "message": message,
10
+ }
11
+ return supabase.table("chat_messages").insert(payload).execute()
sessions.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timezone
2
+ from db import supabase
3
+
4
+
5
+ def utc_now():
6
+ return datetime.now(timezone.utc).isoformat()
7
+
8
+
9
+ def get_session(customer_phone: str, bot_number: str):
10
+ res = (
11
+ supabase.table("chat_sessions")
12
+ .select("*")
13
+ .eq("customer_phone", customer_phone)
14
+ .eq("bot_number", bot_number)
15
+ .limit(1)
16
+ .execute()
17
+ )
18
+
19
+ rows = res.data or []
20
+ if not rows:
21
+ return None
22
+ return rows[0]
23
+
24
+
25
+ def upsert_session(customer_phone: str, bot_number: str, current_state: str, flow_type=None, flow_data=None, last_message=None):
26
+ payload = {
27
+ "customer_phone": customer_phone,
28
+ "bot_number": bot_number,
29
+ "current_state": current_state,
30
+ "flow_type": flow_type,
31
+ "flow_data": flow_data or {},
32
+ "last_message": last_message,
33
+ "updated_at": utc_now(),
34
+ }
35
+
36
+ return (
37
+ supabase.table("chat_sessions")
38
+ .upsert(payload, on_conflict="customer_phone,bot_number")
39
+ .execute()
40
+ )
41
+
42
+
43
+ def clear_session(customer_phone: str, bot_number: str):
44
+ payload = {
45
+ "customer_phone": customer_phone,
46
+ "bot_number": bot_number,
47
+ "current_state": "START",
48
+ "flow_type": None,
49
+ "flow_data": {},
50
+ "last_message": None,
51
+ "updated_at": utc_now(),
52
+ }
53
+
54
+ return (
55
+ supabase.table("chat_sessions")
56
+ .upsert(payload, on_conflict="customer_phone,bot_number")
57
+ .execute()
58
+ )