207 lines
8.1 KiB
Python
207 lines
8.1 KiB
Python
import requests
|
||
import logging
|
||
from django.core.exceptions import ObjectDoesNotExist
|
||
|
||
from .models import FacebookPageAssistant
|
||
from pxy_openai.assistants import OpenAIAssistant as OpenAIService
|
||
from pxy_neo4j.neo4j_connector import Neo4jDatabase
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class FacebookService:
|
||
"""
|
||
A service to interact with the Facebook Graph API,
|
||
generate AI replies, and log interactions.
|
||
"""
|
||
|
||
def __init__(self, user_access_token, facebook_api_version="v22.0"):
|
||
self.user_access_token = user_access_token
|
||
self.base_url = f"https://graph.facebook.com/{facebook_api_version}"
|
||
self.neo4j_db = Neo4jDatabase()
|
||
|
||
# ─── Workflows / Transactional Methods ────────────────────────────────────
|
||
|
||
def post_comment_on_share(self, page_id, post_id, message="", sender_id=None):
|
||
"""
|
||
1. Fetch page token & post details
|
||
2. Build AI prompt and get a reply
|
||
3. Comment on the shared post + log
|
||
4. Comment on the original post (if any) + log
|
||
"""
|
||
token = self._get_page_token(page_id)
|
||
if not token:
|
||
return None
|
||
|
||
details = self._get_post_details(post_id, token)
|
||
if not details:
|
||
return None
|
||
|
||
assistant_name = self._get_assistant_name(page_id)
|
||
if not assistant_name:
|
||
return None
|
||
|
||
prompt = self._build_share_prompt(message, details["parent_message"])
|
||
bot_resp = self._generate_ai_reply(assistant_name, prompt)
|
||
|
||
# Comment on shared post
|
||
mention_target = details.get("parent_from_id") or details.get("from_id")
|
||
shared_msg = self._format_comment(bot_resp, mention_target)
|
||
shared_resp = self._post_facebook_comment(post_id, shared_msg, token)
|
||
if shared_resp:
|
||
self._log_interaction(page_id, details["parent_message"] or "Shared post", bot_resp)
|
||
|
||
# Comment on original post
|
||
parent = details.get("parent_id")
|
||
if parent and shared_resp:
|
||
orig_msg = self._format_comment(bot_resp, mention_target)
|
||
orig_resp = self._post_facebook_comment(parent, orig_msg, token)
|
||
if orig_resp:
|
||
self._log_interaction(page_id, details["parent_message"] or "Original post", bot_resp, suffix=" (Original)")
|
||
|
||
return shared_resp
|
||
|
||
def reply_to_comment(self, page_id, comment_id, message):
|
||
"""
|
||
1. Fetch page token & assistant
|
||
2. Generate AI reply
|
||
3. Post reply + log
|
||
"""
|
||
token = self._get_page_token(page_id)
|
||
if not token:
|
||
return None
|
||
|
||
assistant_name = self._get_assistant_name(page_id)
|
||
if not assistant_name:
|
||
return None
|
||
|
||
incoming = message.strip() or "Thank you for your comment! What do you think about it?"
|
||
bot_resp = self._generate_ai_reply(assistant_name, incoming)
|
||
|
||
resp = self._post_facebook_comment(comment_id, bot_resp, token)
|
||
if resp:
|
||
self._log_interaction(page_id, incoming, bot_resp, user_prefix="fb_user")
|
||
return resp
|
||
|
||
# ─── Internal Helpers ────────────────────────────────────────────────────
|
||
|
||
def _get_page_token(self, page_id):
|
||
"""Fetch the Page‑access token via /me/accounts."""
|
||
url = f"{self.base_url}/me/accounts?access_token={self.user_access_token}"
|
||
try:
|
||
res = requests.get(url); res.raise_for_status()
|
||
for p in res.json().get("data", []):
|
||
if p.get("id") == str(page_id):
|
||
return p.get("access_token")
|
||
logger.error(f"Page {page_id} not found in accounts")
|
||
except Exception as e:
|
||
logger.error(f"_get_page_token error: {e}")
|
||
return None
|
||
|
||
def _get_assistant_name(self, page_id):
|
||
"""Lookup which OpenAI assistant is bound to this Facebook Page."""
|
||
try:
|
||
fa = FacebookPageAssistant.objects.get(page_id=page_id)
|
||
return fa.assistant.name
|
||
except ObjectDoesNotExist:
|
||
logger.error(f"No assistant configured for page {page_id}")
|
||
return None
|
||
|
||
def _get_post_details(self, post_id, token):
|
||
"""
|
||
Returns:
|
||
message, description, parent_id, from_id,
|
||
parent_from_id, parent_message
|
||
"""
|
||
try:
|
||
# Fetch this post
|
||
fields = "message,attachments.limit(1){description},parent_id,from"
|
||
url1 = f"{self.base_url}/{post_id}?fields={fields}&access_token={token}"
|
||
d1 = requests.get(url1).json()
|
||
|
||
msg = d1.get("message", "")
|
||
desc = d1.get("attachments", {}).get("data",[{}])[0].get("description","")
|
||
parent = d1.get("parent_id")
|
||
from_id = d1.get("from",{}).get("id")
|
||
|
||
# If a share, fetch original author & text
|
||
p_from = None
|
||
p_msg = None
|
||
if parent:
|
||
url2 = f"{self.base_url}/{parent}?fields=from,message&access_token={token}"
|
||
p = requests.get(url2).json()
|
||
p_from = p.get("from",{}).get("id")
|
||
p_msg = p.get("message","")
|
||
|
||
return {
|
||
"message": msg,
|
||
"description": desc,
|
||
"parent_id": parent,
|
||
"from_id": from_id,
|
||
"parent_from_id": p_from,
|
||
"parent_message": p_msg,
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"_get_post_details error for {post_id}: {e}")
|
||
return {}
|
||
|
||
def _build_share_prompt(self, incoming, parent_message):
|
||
"""Construct the AI prompt for a share‑comment."""
|
||
text = parent_message or incoming or ""
|
||
if not text:
|
||
return "Share an inspiring science fact."
|
||
return (
|
||
f"You are Polisplexity, an expert city‑tech consultancy bot. "
|
||
f"Read the post: \"{text}\". "
|
||
"1) Identify the urban problem. "
|
||
"2) Summarize any existing solution or say 'No solution mentioned.' "
|
||
"3) Propose a new data‑driven solution using digital twins/HPC. "
|
||
"4) Ask a question to spark discussion."
|
||
)
|
||
|
||
def _generate_ai_reply(self, assistant_name, prompt):
|
||
"""Send the prompt to OpenAI and return the response."""
|
||
try:
|
||
svc = OpenAIService(name=assistant_name)
|
||
return svc.handle_message(prompt)
|
||
except Exception as e:
|
||
logger.error(f"_generate_ai_reply error: {e}")
|
||
return ""
|
||
|
||
def _format_comment(self, message, sender_id=None):
|
||
"""Prepend a link to the sender page + WhatsApp CTA, nicely formatted."""
|
||
prefix = f"https://www.facebook.com/{sender_id}" if sender_id else ""
|
||
whatsapp = "https://wa.me/447887147696"
|
||
parts = [
|
||
prefix,
|
||
message.strip(),
|
||
f"📱 Chat on WhatsApp: {whatsapp}"
|
||
]
|
||
return "\n\n".join(p for p in parts if p)
|
||
|
||
def _post_facebook_comment(self, object_id, message, token, sender_id=None):
|
||
"""Low‑level: POST /{object}/comments"""
|
||
msg = self._format_comment(message, sender_id)
|
||
try:
|
||
url = f"{self.base_url}/{object_id}/comments"
|
||
resp = requests.post(url, data={"message": msg, "access_token": token})
|
||
resp.raise_for_status()
|
||
logger.info(f"Posted comment on {object_id}")
|
||
return resp.json()
|
||
except Exception as e:
|
||
logger.error(f"_post_facebook_comment error on {object_id}: {e}")
|
||
return None
|
||
|
||
def _log_interaction(self, page_id, user_msg, bot_msg, user_prefix="fb_bot", suffix=""):
|
||
"""Persist the interaction in Neo4j."""
|
||
try:
|
||
self.neo4j_db.store_interaction(
|
||
user_id = f"{user_prefix}_{page_id}",
|
||
bot_id = f"fb_bot_{page_id}",
|
||
user_message= user_msg,
|
||
bot_response= bot_msg,
|
||
platform = f"Facebook{suffix}"
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"_log_interaction error: {e}")
|