"""Entry point for ORTOS AI Consultant. Webhook handler for Bitrix24 Open Lines (outgoing webhooks). """ import os, json, logging, html, urllib.parse, re from http.server import HTTPServer, BaseHTTPRequestHandler from dotenv import load_dotenv logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") for _lib in ("httpx", "httpcore"): logging.getLogger(_lib).setLevel(logging.WARNING) logger = logging.getLogger(__name__) load_dotenv() PORT = int(os.getenv('PORT', '7860')) BITRIX_WEBHOOK_URL = os.getenv('BITRIX_WEBHOOK_URL') BOT_ID = int(os.getenv('BOT_ID', '0')) from bot import process_message _auto_bot_id = 0 _webhook_auth = {} def _parse_form(body: bytes) -> dict: parsed = urllib.parse.parse_qs(body.decode("utf-8", errors="replace")) out = {} for key, vals in parsed.items(): val = vals[0] if len(vals) == 1 else vals brackets = re.findall(r"\[([^\]]+)\]", key) if brackets: section = key.split("[")[0] d = out.setdefault(section, {}) for b in brackets[:-1]: d = d.setdefault(b, {}) d[brackets[-1]] = val else: out[key] = val return out def _ensure_bot_id(data: dict): global _auto_bot_id if _auto_bot_id: return bots = data.get("data", {}).get("BOT", {}) for bid in bots: try: _auto_bot_id = int(bid) logger.info(f"Auto-detected BOT_ID={_auto_bot_id}") return except (ValueError, TypeError): pass def _send_reply(dialog_id: str, text: str): bid = _auto_bot_id or BOT_ID if not bid: logger.error(f"Cannot send: bot_id={bid}") return import httpx client_id = _webhook_auth.get("application_token", "") if _webhook_auth else "" # Try 1: webhook URL + CLIENT_ID (the error said "Client ID not specified") if BITRIX_WEBHOOK_URL and client_id: url = f"{BITRIX_WEBHOOK_URL}imbot.message.add.json" try: resp = httpx.post(url, params={"CLIENT_ID": client_id}, json={ "BOT_ID": bid, "DIALOG_ID": dialog_id, "MESSAGE": text, }, timeout=30) logger.info(f"Reply (webhook+CLIENT_ID) {dialog_id}: {resp.status_code} {resp.text[:300]}") if resp.status_code == 200: return except Exception as e: logger.warning(f"Webhook+CLIENT_ID failed: {e}") # Try 2: webhook URL without CLIENT_ID (might work for im methods) if BITRIX_WEBHOOK_URL: url = f"{BITRIX_WEBHOOK_URL}imbot.message.add.json" try: resp = httpx.post(url, json={ "BOT_ID": bid, "DIALOG_ID": dialog_id, "MESSAGE": text, }, timeout=30) logger.info(f"Reply (webhook no CLIENT_ID) {dialog_id}: {resp.status_code} {resp.text[:300]}") if resp.status_code == 200: return except Exception as e: logger.warning(f"Webhook URL attempt failed: {e}") # Try 3: im.message.add as user 1 if BITRIX_WEBHOOK_URL: url = f"{BITRIX_WEBHOOK_URL}im.message.add.json" try: resp = httpx.post(url, json={ "DIALOG_ID": dialog_id, "MESSAGE": text, }, timeout=30) logger.info(f"Reply (im.message) {dialog_id}: {resp.status_code} {resp.text[:300]}") except Exception as e: logger.error(f"All send methods failed: {e}") def transfer_to_operator(dialog_id: str): chat_id = None if dialog_id.startswith("chat"): try: chat_id = int(dialog_id.replace("chat", "")) except ValueError: pass if not chat_id: logger.error(f"Cannot transfer: no chat_id from {dialog_id}") return import httpx client_id = _webhook_auth.get("application_token", "") if _webhook_auth else "" # Send notification to user _send_reply(dialog_id, "Оператор сейчас подключится. Пожалуйста, ожидайте.") # Transfer chat to contact center if BITRIX_WEBHOOK_URL: url = f"{BITRIX_WEBHOOK_URL}imopenlines.bot.session.operator" try: resp = httpx.post(url, json={"CHAT_ID": chat_id}, timeout=30) logger.info(f"Transfer to operator: {resp.status_code} {resp.text[:300]}") except Exception as e: logger.error(f"Transfer error: {e}") class WebhookHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path in ("/", "/health"): self.send_response(200) self.send_header("Content-Type", "text/plain") self.end_headers() self.wfile.write(b"OK") return if self.path == "/logs": from log_store import get_log entries = get_log() page = """
| Time | Mode | Q | Response | RAG |
|---|---|---|---|---|
| {e['time']} | {e['mode']} | " page += f"{html.escape(e['question'])} | " page += f"{html.escape(e['response'])} | " page += f"{rag_html} |