refactoring public and private services
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
b64c7cb474
commit
6e9881f9d0
@ -1,8 +1,9 @@
|
|||||||
import requests
|
import requests
|
||||||
from pxy_openai.assistants import OpenAIAssistant as OpenAIService # Import the assistant service
|
|
||||||
import logging
|
import logging
|
||||||
from .models import FacebookPageAssistant
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
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
|
from pxy_neo4j.neo4j_connector import Neo4jDatabase
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -10,264 +11,196 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class FacebookService:
|
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"):
|
def __init__(self, user_access_token, facebook_api_version="v22.0"):
|
||||||
self.user_access_token = user_access_token
|
self.user_access_token = user_access_token
|
||||||
self.facebook_api_version = facebook_api_version
|
self.base_url = f"https://graph.facebook.com/{facebook_api_version}"
|
||||||
self.base_url = f"https://graph.facebook.com/{self.facebook_api_version}"
|
self.neo4j_db = Neo4jDatabase()
|
||||||
self.neo4j_db = Neo4jDatabase() # Initialize Neo4j connection
|
|
||||||
|
|
||||||
def get_system_user_id(self):
|
# ─── Workflows / Transactional Methods ────────────────────────────────────
|
||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
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}"
|
token = self._get_page_token(page_id)
|
||||||
try:
|
if not token:
|
||||||
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}")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Fetch post details (description, parent_id)
|
details = self._get_post_details(post_id, token)
|
||||||
post_details = self.get_post_details(post_id, page_access_token)
|
if not details:
|
||||||
if not post_details:
|
|
||||||
logger.error(f"Failed to retrieve post details for post ID: {post_id}")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
description = post_details.get("parent_message", None)
|
assistant_name = self._get_assistant_name(page_id)
|
||||||
parent_id = post_details.get("parent_id", None)
|
if not assistant_name:
|
||||||
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}")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Generate a meaningful comment based on available data
|
prompt = self._build_share_prompt(message, details["parent_message"])
|
||||||
if not message or message.strip() == "":
|
bot_resp = self._generate_ai_reply(assistant_name, prompt)
|
||||||
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."
|
|
||||||
|
|
||||||
else:
|
# Comment on shared post
|
||||||
if description:
|
mention_target = details.get("parent_from_id") or details.get("from_id")
|
||||||
prompt = (
|
shared_msg = self._format_comment(bot_resp, mention_target)
|
||||||
f"Dr. Dr. Ekaropolus previously said: '{message}', "
|
shared_resp = self._post_facebook_comment(post_id, shared_msg, token)
|
||||||
f"and the shared post describes: '{description}'. "
|
if shared_resp:
|
||||||
"Combine these thoughts into an engaging, fun, and insightful response in the most appropriate language."
|
self._log_interaction(page_id, details["parent_message"] or "Shared post", bot_resp)
|
||||||
)
|
|
||||||
else:
|
|
||||||
prompt = f"Dr. Dr. Ekaropolus said: '{message}'. Expand on this with an insightful scientific thought."
|
|
||||||
|
|
||||||
openai_service = OpenAIService(name=openai_assistant_model.name)
|
# Comment on original post
|
||||||
bot_response = openai_service.handle_message(prompt)
|
parent = details.get("parent_id")
|
||||||
|
if parent and shared_resp:
|
||||||
sender_id = author_page_id
|
orig_msg = self._format_comment(bot_resp, mention_target)
|
||||||
# Post a comment on the shared post
|
orig_resp = self._post_facebook_comment(parent, orig_msg, token)
|
||||||
shared_comment_response = self._post_facebook_comment(post_id, bot_response, page_access_token, sender_id)
|
if orig_resp:
|
||||||
|
self._log_interaction(page_id, details["parent_message"] or "Original post", bot_resp, suffix=" (Original)")
|
||||||
# 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
|
|
||||||
|
|
||||||
|
return shared_resp
|
||||||
|
|
||||||
def reply_to_comment(self, page_id, comment_id, message):
|
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
|
token = self._get_page_token(page_id)
|
||||||
page_access_token = self.get_page_access_token(page_id)
|
if not token:
|
||||||
if not page_access_token:
|
|
||||||
logger.error(f"Unable to retrieve access token for page ID: {page_id}")
|
|
||||||
return None
|
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:
|
try:
|
||||||
page_assistant = FacebookPageAssistant.objects.get(page_id=page_id)
|
res = requests.get(url); res.raise_for_status()
|
||||||
openai_assistant_model = page_assistant.assistant
|
for p in res.json().get("data", []):
|
||||||
logger.info(f"Using assistant '{openai_assistant_model.name}' for page '{page_assistant.page_name}'")
|
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:
|
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
|
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
|
def _get_post_details(self, post_id, token):
|
||||||
bot_response = openai_service.handle_message(message)
|
"""
|
||||||
|
Returns:
|
||||||
# Send the response to Facebook
|
message, description, parent_id, from_id,
|
||||||
url = f"{self.base_url}/{comment_id}/comments"
|
parent_from_id, parent_message
|
||||||
payload = {"message": bot_response, "access_token": page_access_token}
|
"""
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, data=payload)
|
# Fetch this post
|
||||||
response.raise_for_status()
|
fields = "message,attachments.limit(1){description},parent_id,from"
|
||||||
logger.info(f"Replied to comment ID: {comment_id}")
|
url1 = f"{self.base_url}/{post_id}?fields={fields}&access_token={token}"
|
||||||
# Store the interaction in Neo4j
|
d1 = requests.get(url1).json()
|
||||||
self.neo4j_db.store_interaction(
|
|
||||||
user_id=f"fb_user_{comment_id}",
|
msg = d1.get("message", "")
|
||||||
bot_id=f"fb_bot_{page_id}",
|
desc = d1.get("attachments", {}).get("data",[{}])[0].get("description","")
|
||||||
user_message=message,
|
parent = d1.get("parent_id")
|
||||||
bot_response=bot_response,
|
from_id = d1.get("from",{}).get("id")
|
||||||
platform="Facebook"
|
|
||||||
)
|
# If a share, fetch original author & text
|
||||||
return response.json()
|
p_from = None
|
||||||
except requests.exceptions.RequestException as e:
|
p_msg = None
|
||||||
logger.error(f"Failed to reply to comment ID: {comment_id}. Error: {e}")
|
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
|
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}")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user