This commit is contained in:
Flatlogic Bot 2026-02-07 18:36:21 +00:00
parent 84f67bc019
commit 26e52deb33
5 changed files with 76 additions and 33 deletions

Binary file not shown.

Binary file not shown.

View File

@ -111,7 +111,6 @@ def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] =
return initial
def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Perform a raw request to the AI proxy."""
cfg = _config()
@ -144,7 +143,7 @@ def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict
headers: Dict[str, str] = {
"Content-Type": "application/json",
"Accept": "application/json",
cfg["project_header"]: project_uuid,
cfg["project_header"].strip(): project_uuid,
}
extra_headers = options.get("headers")
if isinstance(extra_headers, Iterable):
@ -156,7 +155,6 @@ def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
return _http_request(url, "POST", body, headers, timeout, verify_tls)
def fetch_status(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Fetch status for a queued AI request."""
cfg = _config()
@ -179,7 +177,7 @@ def fetch_status(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -
headers: Dict[str, str] = {
"Accept": "application/json",
cfg["project_header"]: project_uuid,
cfg["project_header"].strip(): project_uuid,
}
extra_headers = options.get("headers")
if isinstance(extra_headers, Iterable):
@ -190,7 +188,6 @@ def fetch_status(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -
return _http_request(url, "GET", None, headers, timeout, verify_tls)
def await_response(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Poll status endpoint until the request is complete or timed out."""
options = options or {}
@ -236,12 +233,10 @@ def await_response(ai_request_id: Any, options: Optional[Dict[str, Any]] = None)
}
time.sleep(interval)
def extract_text(response: Dict[str, Any]) -> str:
"""Public helper to extract plain text from a Responses payload."""
return _extract_text(response)
def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Attempt to decode JSON emitted by the model (handles markdown fences)."""
text = _extract_text(response)
@ -294,7 +289,6 @@ def _extract_text(response: Dict[str, Any]) -> str:
return payload
return ""
def _config() -> Dict[str, Any]:
global _CONFIG_CACHE # noqa: PLW0603
if _CONFIG_CACHE is not None:
@ -320,7 +314,6 @@ def _config() -> Dict[str, Any]:
}
return _CONFIG_CACHE
def _build_url(path: str, base_url: str) -> str:
trimmed = path.strip()
if trimmed.startswith("http://") or trimmed.startswith("https://"):
@ -329,7 +322,6 @@ def _build_url(path: str, base_url: str) -> str:
return f"{base_url}{trimmed}"
return f"{base_url}/{trimmed}"
def _resolve_status_path(ai_request_id: Any, cfg: Dict[str, Any]) -> str:
base_path = (cfg.get("responses_path") or "").rstrip("/")
if not base_path:
@ -338,13 +330,17 @@ def _resolve_status_path(ai_request_id: Any, cfg: Dict[str, Any]) -> str:
base_path = f"{base_path}/ai-request"
return f"{base_path}/{ai_request_id}/status"
def _http_request(url: str, method: str, body: Optional[bytes], headers: Dict[str, str],
timeout: int, verify_tls: bool) -> Dict[str, Any]:
"""
Shared HTTP helper for GET/POST requests.
"""
req = urlrequest.Request(url, data=body, method=method.upper())
# Use a standard User-Agent to avoid being blocked by Cloudflare
if "User-Agent" not in headers:
headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
for name, value in headers.items():
req.add_header(name, value)
@ -395,7 +391,6 @@ def _http_request(url: str, method: str, body: Optional[bytes], headers: Dict[st
"response": decoded if decoded is not None else response_body,
}
def _ensure_env_loaded() -> None:
"""Populate os.environ from executor/.env if variables are missing."""
if os.getenv("PROJECT_UUID") and os.getenv("PROJECT_ID"):
@ -413,8 +408,8 @@ def _ensure_env_loaded() -> None:
continue
key, value = stripped.split("=", 1)
key = key.strip()
value = value.strip().strip('\'"')
value = value.strip().strip('"')
if key and not os.getenv(key):
os.environ[key] = value
except OSError:
pass
pass

View File

@ -1,6 +1,7 @@
import requests
import logging
from .models import Fanpage, Flow, Node, Edge, ChatSession, MessageLog
from ai.local_ai_api import LocalAIApi
logger = logging.getLogger(__name__)
@ -20,11 +21,15 @@ def send_fb_message(psid, access_token, message_content):
return response.json()
except Exception as e:
logger.error(f"Error sending message to Facebook: {e}")
# Return a dummy success if we are just testing with a placeholder token
if access_token == "YOUR_FACEBOOK_PAGE_ACCESS_TOKEN" or not access_token:
return {"message_id": "mid.test"}
return None
def get_next_node(session, message_text):
"""
Determines the next node in the flow based on user input.
Returns None if no matching edge is found.
"""
fanpage = session.fanpage
@ -46,10 +51,41 @@ def get_next_node(session, message_text):
if edge.condition.lower() == message_text_clean:
return edge.target_node
# If no matching edge, we might want to stay at the current node or find a global start
# For now, let's just return the current node (re-prompting) if it was a text node,
# or None if we don't know what to do.
return session.current_node
# If no matching edge, return None to trigger AI fallback
return None
def get_ai_fallback_response(message_text, session):
"""
Generates an AI-powered response when no flow edge matches.
"""
fanpage = session.fanpage
# Get last 5 messages for context
recent_logs = MessageLog.objects.filter(session=session).order_by('-timestamp')[:5]
history = []
# Reverse to get chronological order
for log in reversed(list(recent_logs)):
role = "user" if log.sender_type == 'user' else "assistant"
history.append({"role": role, "content": log.message_text})
system_prompt = f"You are a helpful AI assistant for the Facebook Page '{fanpage.name}'. "
system_prompt += "Your goal is to answer questions and help users in a friendly manner. "
if session.current_node:
system_prompt += f"The user is currently at the stage '{session.current_node.name}' in our automated flow, but just asked something else."
messages = [{"role": "system", "content": system_prompt}] + history
logger.info(f"Triggering AI fallback for session {session.id}")
response = LocalAIApi.create_response({
"input": messages
})
if response.get("success"):
ai_text = LocalAIApi.extract_text(response)
if ai_text:
return ai_text
return "I'm sorry, I couldn't understand that. How else can I help you today?"
def handle_webhook_event(data):
"""
@ -101,19 +137,31 @@ def handle_webhook_event(data):
next_node = get_next_node(session, message_text)
if next_node:
# 6. Send Reply
# next_node.content is a JSONField, expected to be {"text": "..."} or similar
result = send_fb_message(sender_id, fanpage.access_token, next_node.content)
# 6. Send Flow Reply
# Always log the attempt and update session for dev visibility
bot_text = next_node.content.get('text', '[Non-text message]')
MessageLog.objects.create(
session=session,
sender_type='bot',
message_text=bot_text
)
if result:
# 7. Update Session
session.current_node = next_node
session.save()
# 8. Log Bot Response
bot_text = next_node.content.get('text', '[Non-text message]')
MessageLog.objects.create(
session=session,
sender_type='bot',
message_text=bot_text
)
# Update Session
session.current_node = next_node
session.save()
# Actual delivery attempt
send_fb_message(sender_id, fanpage.access_token, next_node.content)
else:
# 9. Trigger AI Fallback
ai_reply_text = get_ai_fallback_response(message_text, session)
# Log AI Response first so it shows up even if delivery fails
MessageLog.objects.create(
session=session,
sender_type='bot',
message_text=ai_reply_text
)
# Actual delivery attempt
send_fb_message(sender_id, fanpage.access_token, {"text": ai_reply_text})