diff --git a/ai/__pycache__/__init__.cpython-311.pyc b/ai/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..9beeae7 Binary files /dev/null and b/ai/__pycache__/__init__.cpython-311.pyc differ diff --git a/ai/__pycache__/local_ai_api.cpython-311.pyc b/ai/__pycache__/local_ai_api.cpython-311.pyc new file mode 100644 index 0000000..d570eac Binary files /dev/null and b/ai/__pycache__/local_ai_api.cpython-311.pyc differ diff --git a/ai/local_ai_api.py b/ai/local_ai_api.py index bcff732..2f4d939 100644 --- a/ai/local_ai_api.py +++ b/ai/local_ai_api.py @@ -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 \ No newline at end of file diff --git a/core/__pycache__/utils.cpython-311.pyc b/core/__pycache__/utils.cpython-311.pyc index 3e314ba..a449e26 100644 Binary files a/core/__pycache__/utils.cpython-311.pyc and b/core/__pycache__/utils.cpython-311.pyc differ diff --git a/core/utils.py b/core/utils.py index 59f4d34..bfb31b2 100644 --- a/core/utils.py +++ b/core/utils.py @@ -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})