diff --git a/pxy_meta_pages/services.py b/pxy_meta_pages/services.py index 9af12c8..3b71658 100644 --- a/pxy_meta_pages/services.py +++ b/pxy_meta_pages/services.py @@ -1,8 +1,9 @@ import requests -from pxy_openai.assistants import OpenAIAssistant as OpenAIService # Import the assistant service import logging -from .models import FacebookPageAssistant 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__) @@ -10,264 +11,196 @@ logger = logging.getLogger(__name__) class FacebookService: """ - A service to interact with the Facebook Graph API. + 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.facebook_api_version = facebook_api_version - self.base_url = f"https://graph.facebook.com/{self.facebook_api_version}" - self.neo4j_db = Neo4jDatabase() # Initialize Neo4j connection + self.base_url = f"https://graph.facebook.com/{facebook_api_version}" + self.neo4j_db = Neo4jDatabase() - def get_system_user_id(self): - """ - Retrieves the system user ID using the user access token. - """ - try: - return '122106889202727657' - except requests.exceptions.RequestException as e: - logger.error(f"Error fetching system user ID: {e}") - return None + # ─── Workflows / Transactional Methods ──────────────────────────────────── - def get_page_access_token(self, page_id): + def post_comment_on_share(self, page_id, post_id, message="", sender_id=None): """ - Retrieves the Page Access Token for a specific Page ID. + 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 """ - url = f"{self.base_url}/122106889202727657/accounts?access_token={self.user_access_token}" - try: - response = requests.get(url) - response.raise_for_status() - data = response.json() - - if "data" in data: - for page in data["data"]: - if page.get("id") == str(page_id): - page_name = page.get("name", "Unknown") - access_token = page.get("access_token", "No Token") - logger.info(f"Retrieved access token for page {page_id}: {page_name}") - return access_token - logger.error(f"Error: Page ID {page_id} not found.") - else: - logger.error("Error: Unexpected response format from Facebook API.") - except requests.exceptions.RequestException as e: - logger.error(f"Error fetching Page Access Token: {e}") - return None - - def post_comment_on_share(self, page_id, post_id, message, sender_id=None): - """ - Posts a comment on a shared post using the Facebook API. - Fetches post details (description, parent_id) to improve the comment. - If parent_id exists, posts the same comment on the original post. - """ - # Retrieve the Page Access Token dynamically - page_access_token = self.get_page_access_token(page_id) - if not page_access_token: - logger.error(f"Unable to retrieve access token for page ID: {page_id}") + token = self._get_page_token(page_id) + if not token: return None - # Fetch post details (description, parent_id) - post_details = self.get_post_details(post_id, page_access_token) - if not post_details: - logger.error(f"Failed to retrieve post details for post ID: {post_id}") + details = self._get_post_details(post_id, token) + if not details: return None - description = post_details.get("parent_message", None) - parent_id = post_details.get("parent_id", None) - author_page_id = post_details.get("parent_from_id", None) - - - # Fetch the appropriate OpenAI assistant for the page - try: - page_assistant = FacebookPageAssistant.objects.get(page_id=page_id) - openai_assistant_model = page_assistant.assistant - logger.info(f"Using assistant '{openai_assistant_model.name}' for page '{page_assistant.page_name}'") - except ObjectDoesNotExist: - logger.error(f"No assistant configured for page ID: {page_id}") + assistant_name = self._get_assistant_name(page_id) + if not assistant_name: return None - # Generate a meaningful comment based on available data - if not message or message.strip() == "": - if description: - prompt = ( - f"Dr. Dr. Ekaropolus previously said: '{description}'. " - "Based on this, write an insightful response in the most appropriate language that engages people in scientific discussion." - ) - else: - prompt = "Say something truly inspiring about science, a fact or idea that will amaze people." + prompt = self._build_share_prompt(message, details["parent_message"]) + bot_resp = self._generate_ai_reply(assistant_name, prompt) - else: - if description: - prompt = ( - f"Dr. Dr. Ekaropolus previously said: '{message}', " - f"and the shared post describes: '{description}'. " - "Combine these thoughts into an engaging, fun, and insightful response in the most appropriate language." - ) - else: - prompt = f"Dr. Dr. Ekaropolus said: '{message}'. Expand on this with an insightful scientific thought." + # 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) - openai_service = OpenAIService(name=openai_assistant_model.name) - bot_response = openai_service.handle_message(prompt) - - sender_id = author_page_id - # Post a comment on the shared post - shared_comment_response = self._post_facebook_comment(post_id, bot_response, page_access_token, sender_id) - - # If the comment on the shared post was successful, store in Neo4j - if shared_comment_response: - self.neo4j_db.store_interaction( - user_id=f"fb_bot_{page_id}", - bot_id=f"fb_bot_{page_id}", - user_message=description if description else "Shared post comment", - bot_response=bot_response, - platform="Facebook" - ) - - # If parent_id exists and the first comment was successful, post the same comment on the original post - if parent_id and shared_comment_response: - logger.info(f"Also commenting on the original post: {parent_id}") - original_comment_response = self._post_facebook_comment(parent_id, bot_response, page_access_token) - - # If the comment on the original post was successful, store in Neo4j - if original_comment_response: - self.neo4j_db.store_interaction( - user_id=f"fb_bot_{page_id}", - bot_id=f"fb_bot_{page_id}", - user_message=description if description else "Original post comment", - bot_response=bot_response, - platform="Facebook (Original Post)" - ) - - return shared_comment_response - - - def get_post_details(self, post_id, access_token): - """ - Retrieves details of a post, including: - - message (the post’s own text) - - description (from attachments) - - parent_id (if it’s a share) - - from_id (author of THIS post) - - parent_from_id (author of the ORIGINAL post, if shared) - - parent_message (text of the ORIGINAL post, if shared) - """ - try: - # 1st call: get this post’s text, description, parent_id, and author - fields = ( - "message," - "attachments.limit(10){description,media,media_type,target,url}," - "parent_id,from" - ) - url = f"{self.base_url}/{post_id}?fields={fields}&access_token={access_token}" - resp = requests.get(url) - resp.raise_for_status() - data = resp.json() - - message = data.get("message", "") - attachments = data.get("attachments", {}).get("data", [{}]) - description = attachments[0].get("description", "") if attachments else "" - parent_id = data.get("parent_id") - from_id = data.get("from", {}).get("id") - - # Defaults if there is no parent - parent_from_id = None - parent_message = None - - # 2nd call: if this is a share, fetch the ORIGINAL post’s author and text - if parent_id: - parent_fields = "from,message" - parent_url = f"{self.base_url}/{parent_id}?fields={parent_fields}&access_token={access_token}" - p_resp = requests.get(parent_url) - p_resp.raise_for_status() - p_data = p_resp.json() - parent_from_id = p_data.get("from", {}).get("id") - parent_message = p_data.get("message", "") - - return { - "message": message, - "description": description, - "parent_id": parent_id, - "from_id": from_id, - "parent_from_id": parent_from_id, - "parent_message": parent_message, - } - - except requests.exceptions.RequestException as e: - logger.error(f"Failed to fetch post details for {post_id}: {e}") - return {} - - - - - def _post_facebook_comment(self, post_id, message, access_token, sender_id=None): - """ - Helper function to post a comment to a specific post. - """ - # Build the “mention” link (your Page) - prefix = f"https://www.facebook.com/{sender_id}" if sender_id else "" - - # WhatsApp CTA link - whatsapp_link = "https://wa.me/447887147696" - - # Assemble the final message with labels and spacing - formatted = "\n\n".join(filter(None, [ - prefix, - message.strip(), - f"📱 Chat with us on WhatsApp: {whatsapp_link}" - ])) - - url = f"{self.base_url}/{post_id}/comments" - payload = {"message": formatted, "access_token": access_token} - try: - response = requests.post(url, data=payload) - response.raise_for_status() - logger.info(f"Posted a comment on post ID: {post_id}") - return response.json() - except requests.exceptions.RequestException as e: - logger.error(f"Failed to comment on post ID: {post_id}. Error: {e}") - return None + # 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): """ - Replies to a specific comment using the Facebook API and OpenAI Assistant. + 1. Fetch page token & assistant + 2. Generate AI reply + 3. Post reply + log """ - # Retrieve the Page Access Token dynamically - page_access_token = self.get_page_access_token(page_id) - if not page_access_token: - logger.error(f"Unable to retrieve access token for page ID: {page_id}") + token = self._get_page_token(page_id) + if not token: return None - # Fetch the appropriate OpenAI assistant for the page + 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: - page_assistant = FacebookPageAssistant.objects.get(page_id=page_id) - openai_assistant_model = page_assistant.assistant - logger.info(f"Using assistant '{openai_assistant_model.name}' for page '{page_assistant.page_name}'") + 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 ID: {page_id}") + logger.error(f"No assistant configured for page {page_id}") return None - # Use a default message if the received message is empty - if not message or message.strip() == "": - message = "Thank you for sharing this comment! What do you think about it?" - openai_service = OpenAIService(name=openai_assistant_model.name) # Pass the model's name to the service - bot_response = openai_service.handle_message(message) - - # Send the response to Facebook - url = f"{self.base_url}/{comment_id}/comments" - payload = {"message": bot_response, "access_token": page_access_token} + def _get_post_details(self, post_id, token): + """ + Returns: + message, description, parent_id, from_id, + parent_from_id, parent_message + """ try: - response = requests.post(url, data=payload) - response.raise_for_status() - logger.info(f"Replied to comment ID: {comment_id}") - # Store the interaction in Neo4j - self.neo4j_db.store_interaction( - user_id=f"fb_user_{comment_id}", - bot_id=f"fb_bot_{page_id}", - user_message=message, - bot_response=bot_response, - platform="Facebook" - ) - return response.json() - except requests.exceptions.RequestException as e: - logger.error(f"Failed to reply to comment ID: {comment_id}. Error: {e}") + # 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}")