mcpField
H
hebrew-chatbot-builder

hebrew-chatbot-builder

github.com/skills-il开发13145 次安装3 天前更新
代码仓库

关于 hebrew-chatbot-builder

Hebrew Chatbot Builder 是用于构建希伯来语对话机器人的技能,覆盖消息平台和网页聊天组件。它侧重希伯来语语体与性别表达、以色列实体提取,以及从右向左的正确显示。

主要功能

  1. 01WhatsApp 与 Telegram 脚手架
  2. 02希伯来语语体与性别表达指导
  3. 03RTL 网页聊天布局指导
  4. 04以色列实体提取模式
  5. 05希伯来语意图与回复设计

适用场景

  1. 01构建希伯来语 WhatsApp 订单状态机器人
  2. 02为网站添加 RTL 希伯来语聊天组件
  3. 03为正式场景设计希伯来语机器人对话
  4. 04处理以色列日期、电话号码和金额

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/skills-il/developer-tools --skill hebrew-chatbot-builder

安装后在 Claude Code 中输入/hebrew-chatbot-builder确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evidence.json
metadata.json
optimization-log.json
SKILL_HE.md
references/
hebrew-chatbot-phrases.md
whatsapp-business-api-guide.md
scripts/
telegram-bot-scaffold.py
whatsapp-webhook-handler.py

详细介绍

希伯来语聊天机器人构建器

构建可投入生产环境的对话式 AI 聊天机器人,并原生支持希伯来语。本技能涵盖平台集成(WhatsApp、Telegram、网页)、希伯来语语言模式、RTL UI 组件,以及面向希伯来语使用者的对话流程设计。

说明

构建希伯来语聊天机器人时,请遵循以下流程:

  1. 确定平台/渠道。 确认机器人运行在 WhatsApp Business API、Telegram、嵌入式网页小组件,还是多个渠道上。每种渠道都有不同的脚手架和消息模型。
  2. 确认正式或非正式语体。 询问用户,受众期望使用非正式希伯来语(“דוגרי”,推荐用于大多数面向消费者的机器人)还是正式希伯来语(政府、银行、法律)。这一选择会影响每一条回复文案。
  3. 询问性别处理策略。 决定是尽早询问并记住用户的性别、全程使用性别中立的措辞,还是使用斜杠写法(“את/ה”)。请参阅“性别感知回复”部分。
  4. 使用相应的内置脚本搭建脚手架。 WhatsApp 使用 scripts/whatsapp-webhook-handler.py,Telegram 使用 scripts/telegram-bot-scaffold.py 作为起点,然后调整希伯来语回复模板和对话流程。
  5. 接入实体提取。 添加 extract_israeli_entities()(或等效实现),使机器人能够从希伯来语自由文本输入中识别以色列电话号码、谢克尔金额、日期和 Teudat Zehut。
  6. 验证 RTL 渲染。 对于网页小组件,在最外层容器上设置 dir="rtl" 并在真实设备上测试。对于 WhatsApp 和 Telegram,请验证希伯来语文本和交互按钮在 iOS 和 Android 上都能正确渲染。

示例

示例 1:希伯来语 WhatsApp 订单状态机器人

用户说:“构建一个希伯来语 WhatsApp 机器人,让客户可以查询订单状态。”

操作:

  1. 确定渠道:WhatsApp Business Cloud API。
  2. 确认使用非正式语体(面向消费者)和性别中立的措辞,以覆盖更广泛的用户。
  3. 从 scripts/whatsapp-webhook-handler.py 开始,其中已包含 webhook 验证、签名检查和希伯来语回复模板。
  4. 接入交互式按钮菜单(“בדיקת הזמנה”、“שאלות נפוצות”、“דבר/י עם נציג”),并添加 waiting_order_number 状态,用于验证订单号并查询订单。
  5. 通过 Meta Business Suite 提交希伯来语 order_confirmation_he 模板,以便主动发送更新。

结果:一个可运行的 webhook 处理程序,能用希伯来语问候用户、处理按钮回复并回答订单状态查询。

示例 2:为现有网站添加 RTL 希伯来语网页聊天小组件

用户说:“为我的网站添加一个布局正确的 RTL 聊天小组件。”

操作:

  1. 确定渠道:嵌入式网页小组件。
  2. 确认使用非正式语体,并在 CTA 中使用斜杠写法。
  3. 使用“网页聊天小组件”部分中的 RTL 聊天气泡布局 CSS,以及 MessageBubble/detectDirection 组件。
  4. 在最外层聊天容器元素上设置 dir="rtl"(不只是在 CSS 中设置),并为输入框添加 direction: rtl。
  5. 在浏览器 DevTools 中验证对齐方式:用户气泡在右侧,机器人气泡在左侧,时间戳位于末端一侧。

结果:一个能自然呈现希伯来语、按气泡处理希伯来语/英语混合消息,并且不会因父级框架设置 direction: ltr 而错乱的聊天小组件。

希伯来语对话设计

正式与非正式语体

希伯来语有明显的正式和非正式语体。请根据受众选择:

非正式(推荐用于大多数面向消费者的机器人):

  • 使用第二人称单数:את/ה(你)
  • 使用较短的句子
  • 使用口语表达:“מה קורה?”、“אין בעיה”、“סבבה”

正式(推荐用于政府、银行、法律场景):

  • 使用第二人称复数或被动语态
  • 使用语法规范的完整句子
  • 使用正式表达:“כיצד נוכל לסייע?”、“בבקשה המתן/י”
考虑性别的回复

希伯来语动词和形容词会随性别发生词形变化。请妥善处理:

策略 1:尽早询问并记住

Bot: "היי! לפני שנתחיל, איך לפנות אליך?"
Options: [זכר] [נקבה] [לא משנה לי]

策略 2:使用性别中立的表达

-- Instead of: "אתה/את מוזמן/מוזמנת להמשיך"
-- Use: "ניתן להמשיך" or "אפשר להמשיך"

-- Instead of: "רוצה/רוצה לראות?"
-- Use: "לראות עוד אפשרויות?"

策略 3:使用斜杠形式(以色列科技行业常见)

"את/ה מוזמן/ת לבדוק את האפשרויות"
聊天中的希伯来语日期/时间格式
# Date formats for chat messages
# Israeli standard: DD/MM/YYYY or DD.MM.YYYY
# In conversation: "יום שלישי, 14 במרץ" (Tuesday, March 14)

# Time format: 24-hour clock is standard in Israel
# "בשעה 14:30" (at 14:30), not "2:30 PM"

# Relative time in Hebrew
RELATIVE_TIME_HE = {
    "just_now": "עכשיו",
    "minutes_ago": "לפני {n} דקות",
    "hours_ago": "לפני {n} שעות",
    "yesterday": "אתמול",
    "days_ago": "לפני {n} ימים",
    "today": "היום",
    "tomorrow": "מחר",
}

WhatsApp Business API 集成

通过 Cloud API 设置

WhatsApp Cloud API(Meta 的官方 API)是以色列企业的推荐方案:

  1. 创建 Meta Business 账户,网址为 business.facebook.com
  2. 在 Meta Business Suite 中设置 WhatsApp Business 账户
  3. 在 Meta Developers 中创建应用并添加 WhatsApp 产品
  4. 获取电话号码:支持以色列号码(+972)
  5. 生成访问令牌,用于 API 调用
消息模板(希伯来语)

WhatsApp 要求外发消息使用预先批准的模板。通过 Meta Business Suite 提交希伯来语模板:

# Template example: Order confirmation
# Template name: order_confirmation_he
# Language: he
# Body: "שלום {{1}}, ההזמנה שלך מספר {{2}} התקבלה בהצלחה. סכום: ₪{{3}}. צפי למשלוח: {{4}}."

import requests

def send_template_message(phone_number: str, template_data: dict):
    """Send a WhatsApp template message in Hebrew."""
    url = f"https://graph.facebook.com/v25.0/{PHONE_NUMBER_ID}/messages"

    payload = {
        "messaging_product": "whatsapp",
        "to": phone_number,  # E.164 format: 972501234567
        "type": "template",
        "template": {
            "name": "order_confirmation_he",
            "language": {"code": "he"},
            "components": [
                {
                    "type": "body",
                    "parameters": [
                        {"type": "text", "text": template_data["customer_name"]},
                        {"type": "text", "text": template_data["order_id"]},
                        {"type": "text", "text": template_data["amount"]},
                        {"type": "text", "text": template_data["delivery_date"]},
                    ]
                }
            ]
        }
    }

    headers = {
        "Authorization": f"Bearer {ACCESS_TOKEN}",
        "Content-Type": "application/json"
    }

    response = requests.post(url, json=payload, headers=headers)
    return response.json()
交互式消息

WhatsApp 支持交互式按钮和列表,这些功能适用于希伯来语:

def send_interactive_buttons(phone_number: str):
    """Send interactive buttons in Hebrew."""
    payload = {
        "messaging_product": "whatsapp",
        "to": phone_number,
        "type": "interactive",
        "interactive": {
            "type": "button",
            "body": {
                "text": "איך אפשר לעזור לך היום?"
            },
            "action": {
                "buttons": [
                    {
                        "type": "reply",
                        "reply": {"id": "check_order", "title": "בדיקת הזמנה"}
                    },
                    {
                        "type": "reply",
                        "reply": {"id": "support", "title": "תמיכה טכנית"}
                    },
                    {
                        "type": "reply",
                        "reply": {"id": "hours", "title": "שעות פעילות"}
                    }
                ]
            }
        }
    }

    return send_whatsapp_message(payload)


def send_interactive_list(phone_number: str):
    """Send an interactive list in Hebrew."""
    payload = {
        "messaging_product": "whatsapp",
        "to": phone_number,
        "type": "interactive",
        "interactive": {
            "type": "list",
            "body": {
                "text": "בחר/י את הנושא שמעניין אותך:"
            },
            "action": {
                "button": "לרשימת האפשרויות",
                "sections": [
                    {
                        "title": "שירותים",
                        "rows": [
                            {"id": "pricing", "title": "מחירון", "description": "צפייה במחירים עדכניים"},
                            {"id": "catalog", "title": "קטלוג", "description": "עיון במוצרים שלנו"},
                            {"id": "branches", "title": "סניפים", "description": "מציאת הסניף הקרוב"}
                        ]
                    },
                    {
                        "title": "תמיכה",
                        "rows": [
                            {"id": "faq", "title": "שאלות נפוצות", "description": "תשובות לשאלות שכיחות"},
                            {"id": "human", "title": "נציג אנושי", "description": "שיחה עם נציג"}
                        ]
                    }
                ]
            }
        }
    }

    return send_whatsapp_message(payload)
Webhook 处理
from flask import Flask, request, jsonify
import hmac
import hashlib

app = Flask(__name__)

VERIFY_TOKEN = "your_verify_token"
APP_SECRET = "your_app_secret"

@app.route("/webhook", methods=["GET"])
def verify_webhook():
    """Handle WhatsApp webhook verification."""
    mode = request.args.get("hub.mode")
    token = request.args.get("hub.verify_token")
    challenge = request.args.get("hub.challenge")

    if mode == "subscribe" and token == VERIFY_TOKEN:
        return challenge, 200
    return "Forbidden", 403

@app.route("/webhook", methods=["POST"])
def handle_webhook():
    """Process incoming WhatsApp messages."""
    # Verify signature
    signature = request.headers.get("X-Hub-Signature-256", "")
    body = request.get_data()
    expected = "sha256=" + hmac.new(
        APP_SECRET.encode(), body, hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        return "Invalid signature", 403

    data = request.get_json()

    for entry in data.get("entry", []):
        for change in entry.get("changes", []):
            if change["field"] == "messages":
                for message in change["value"].get("messages", []):
                    handle_incoming_message(message)

    return jsonify({"status": "ok"}), 200

def handle_incoming_message(message: dict):
    """Process a single incoming message."""
    sender = message["from"]  # Phone number
    msg_type = message["type"]

    if msg_type == "text":
        text = message["text"]["body"]
        # Process Hebrew text
        process_hebrew_input(sender, text)

    elif msg_type == "interactive":
        # Handle button/list replies
        if "button_reply" in message["interactive"]:
            button_id = message["interactive"]["button_reply"]["id"]
            handle_button_click(sender, button_id)
        elif "list_reply" in message["interactive"]:
            list_id = message["interactive"]["list_reply"]["id"]
            handle_list_selection(sender, list_id)

Telegram 机器人集成

BotFather 设置
  1. 在 Telegram 上打开 @BotFather
  2. 发送 /newbot 并按照提示操作
  3. 设置希伯来语说明:发送 /setdescription,然后发送希伯来语文本
  4. 设置希伯来语命令菜单:
/setcommands

start - התחל שיחה
help - עזרה
menu - תפריט ראשי
order - הזמנה חדשה
status - סטטוס הזמנה
faq - שאלות נפוצות
language - שפה / Language
希伯来语内联键盘
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import Application, CommandHandler, CallbackQueryHandler

async def start(update: Update, context):
    """Send a Hebrew welcome message with inline keyboard."""
    keyboard = [
        [
            InlineKeyboardButton("הזמנה חדשה", callback_data="new_order"),
            InlineKeyboardButton("בדיקת סטטוס", callback_data="check_status"),
        ],
        [
            InlineKeyboardButton("שאלות נפוצות", callback_data="faq"),
            InlineKeyboardButton("דבר/י עם נציג", callback_data="human_agent"),
        ],
        [
            InlineKeyboardButton("English", callback_data="lang_en"),
        ],
    ]
    reply_markup = InlineKeyboardMarkup(keyboard)

    await update.message.reply_text(
        "שלום! 👋\n\n"
        "אני הבוט של [שם העסק]. איך אפשר לעזור?",
        reply_markup=reply_markup,
    )

async def button_handler(update: Update, context):
    """Handle inline keyboard button presses."""
    query = update.callback_query
    await query.answer()  # Acknowledge the button press

    if query.data == "new_order":
        await query.edit_message_text("מעולה! בוא/י נתחיל הזמנה.\n\nמה תרצה/י להזמין?")
    elif query.data == "check_status":
        await query.edit_message_text("שלח/י לי את מספר ההזמנה ואבדוק עבורך.")
    elif query.data == "faq":
        await show_faq(query)
    elif query.data == "human_agent":
        await query.edit_message_text(
            "מעביר אותך לנציג אנושי.\n"
            "שעות הפעילות שלנו: א'-ה' 9:00-17:00\n"
            "נציג יחזור אליך בהקדם."
        )
    elif query.data == "lang_en":
        await query.edit_message_text("Switching to English. How can I help you?")

# Build the application
app = Application.builder().token("YOUR_BOT_TOKEN").build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CallbackQueryHandler(button_handler))
群组机器人权限

对于希伯来语群组机器人,请设置适当的权限:

# In BotFather:
# /setjoingroups - Enable/disable joining groups
# /setprivacy - Set privacy mode (recommended: enabled, bot only sees commands)

# Handle group messages differently
async def handle_message(update: Update, context):
    chat_type = update.effective_chat.type

    if chat_type in ("group", "supergroup"):
        # In groups, only respond to commands or mentions
        if update.message.text and (
            update.message.text.startswith("/") or
            f"@{context.bot.username}" in update.message.text
        ):
            await process_group_command(update, context)
    else:
        # In private chat, respond to all messages
        await process_private_message(update, context)

Web 聊天组件

RTL 聊天气泡布局
/* RTL Chat Container */
.chat-container {
  direction: rtl;
  display: flex;
  flex-direction: column;
  height: 100vh;
  font-family: 'Heebo', 'Assistant', sans-serif;
}

/* Message area */
.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Message bubble base */
.message-bubble {
  max-width: 75%;
  padding: 10px 14px;
  border-radius: 16px;
  font-size: 14px;
  line-height: 1.6;
  word-wrap: break-word;
}

/* User message (right side in RTL) */
.message-user {
  align-self: flex-start; /* In RTL, flex-start is right */
  background-color: #dcf8c6;
  border-bottom-right-radius: 4px; /* Tail on right for RTL */
  color: #111;
}

/* Bot message (left side in RTL) */
.message-bot {
  align-self: flex-end; /* In RTL, flex-end is left */
  background-color: #fff;
  border-bottom-left-radius: 4px; /* Tail on left for RTL */
  color: #111;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}

/* Timestamp */
.message-time {
  font-size: 11px;
  color: #999;
  margin-top: 4px;
  text-align: left; /* Time on the trailing side */
}

/* Input area */
.chat-input-container {
  display: flex;
  gap: 8px;
  padding: 12px 16px;
  border-top: 1px solid #e0e0e0;
  background: #fff;
}

.chat-input {
  flex: 1;
  padding: 10px 14px;
  border: 1px solid #ddd;
  border-radius: 24px;
  font-size: 14px;
  direction: rtl;
  text-align: right;
  font-family: inherit;
}

.chat-input::placeholder {
  color: #999;
  text-align: right;
}

.chat-send-btn {
  background: #25d366;
  color: #fff;
  border: none;
  border-radius: 50%;
  width: 40px;
  height: 40px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Flip send icon for RTL */
  transform: scaleX(-1);
}
希伯来语输入指示器
// React component for Hebrew typing indicator
interface TypingIndicatorProps {
  isTyping: boolean;
}

function TypingIndicator({ isTyping }: TypingIndicatorProps) {
  if (!isTyping) return null;

  return (
    <div className="message-bubble message-bot typing-indicator">
      <span className="typing-text">מקליד/ה...</span>
      <span className="typing-dots">
        <span className="dot" />
        <span className="dot" />
        <span className="dot" />
      </span>
    </div>
  );
}

// CSS for typing animation
/*
.typing-dots {
  display: inline-flex;
  gap: 4px;
  margin-right: 8px;
}

.typing-dots .dot {
  width: 6px;
  height: 6px;
  background: #999;
  border-radius: 50%;
  animation: typing-bounce 1.4s infinite ease-in-out;
}

.typing-dots .dot:nth-child(1) { animation-delay: 0s; }
.typing-dots .dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dots .dot:nth-child(3) { animation-delay: 0.4s; }

@keyframes typing-bounce {
  0%, 60%, 100% { transform: translateY(0); }
  30% { transform: translateY(-6px); }
}
*/
消息对齐逻辑
// Determine text direction for mixed Hebrew/English messages
function detectDirection(text: string): 'rtl' | 'ltr' {
  // Check first strong directional character
  const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0700-\u074F]/;
  const ltrRegex = /[a-zA-Z]/;

  for (const char of text) {
    if (rtlRegex.test(char)) return 'rtl';
    if (ltrRegex.test(char)) return 'ltr';
  }

  return 'rtl'; // Default to RTL for Hebrew-first apps
}

// Apply to message bubble
function MessageBubble({ text, sender }: { text: string; sender: 'user' | 'bot' }) {
  const direction = detectDirection(text);

  return (
    <div
      className={`message-bubble message-${sender}`}
      style={{ direction, textAlign: direction === 'rtl' ? 'right' : 'left' }}
    >
      {text}
    </div>
  );
}

希伯来语 NLP 模式

意图检测

希伯来语复杂的形态变化使意图检测颇具挑战。主要策略:

# Common Hebrew intents with example phrases
HEBREW_INTENTS = {
    "greeting": [
        "שלום", "היי", "הי", "בוקר טוב", "ערב טוב",
        "מה קורה", "מה נשמע", "אהלן",
    ],
    "farewell": [
        "ביי", "להתראות", "שלום", "יום טוב", "לילה טוב",
        "תודה וביי", "נתראה",
    ],
    "help": [
        "עזרה", "אני צריך עזרה", "אני צריכה עזרה",
        "תעזור לי", "תעזרי לי", "איך עושים", "מה עושים",
    ],
    "order_status": [
        "איפה ההזמנה", "סטטוס הזמנה", "מתי מגיע",
        "עדכון משלוח", "בדיקת הזמנה", "מספר מעקב",
    ],
    "complaint": [
        "לא מרוצה", "בעיה", "תלונה", "לא עובד",
        "שירות גרוע", "רוצה להתלונן", "קיבלתי מוצר פגום",
    ],
    "pricing": [
        "כמה עולה", "מה המחיר", "מחירון", "הנחה",
        "מבצע", "זול יותר", "יקר מדי",
    ],
    "human_agent": [
        "נציג", "אדם אמיתי", "תעביר לנציג",
        "לדבר עם מישהו", "מנהל", "אני רוצה לדבר עם בנאדם",
    ],
}

def detect_intent(text: str) -> tuple[str, float]:
    """Detect intent from Hebrew text using keyword matching.
    For production, use an LLM or fine-tuned model instead."""
    text_lower = text.strip()
    best_intent = "unknown"
    best_score = 0.0

    for intent, phrases in HEBREW_INTENTS.items():
        for phrase in phrases:
            if phrase in text_lower:
                # Longer phrase match = higher confidence
                score = len(phrase) / max(len(text_lower), 1)
                if score > best_score:
                    best_score = score
                    best_intent = intent

    return best_intent, min(best_score * 2, 1.0)  # Normalize score

简单的子串匹配存在的注意事项(上线前请阅读):

  • 尼库德符号 / 元音标记:上述关键词列表假定文本没有元音标记。如果用户输入(或你的列表)包含尼库德符号,phrase in text 就会失效。请先从两侧移除尼库德符号,例如 re.sub(r'[֑-ׇ]', '', text)。
  • 前缀介词:希伯来语会将 ב/ל/מ/ה/ו/כ/ש 附加到后续词语,因此“בהזמנה”不会匹配关键词“הזמנה”。简单的 phrase in text 会悄无声息地漏掉这些情况。对于演示之外的用途,请使用能够处理形态变化的希伯来语 NLP 工具,例如 YAP 形态分析器(https://github.com/OnlpLab/yap)或 Stanza 的希伯来语管线,或者改用大模型(见下文)。

基于大模型的意图分类和回复生成(推荐用于生产环境):

子串匹配无法应对改述、拼写错误和形态变化。生产环境中,请提示大模型对意图进行分类并起草回复。一个精简模式:

# Pseudocode - call your provider's SDK (Anthropic, OpenAI, or Google).
INTENT_SYSTEM_PROMPT = """אתה מסווג כוונות לצ'אטבוט שירות בעברית.
החזר JSON בלבד: {"intent": "<one of: greeting|farewell|help|order_status|complaint|pricing|human_agent|unknown>", "confidence": 0.0-1.0}.
התחשב במורפולוגיה עברית (אותיות שימוש ב/ל/מ), שגיאות כתיב וסלנג."""

# 1. Classification turn: send INTENT_SYSTEM_PROMPT + the user message, parse the JSON.
# 2. Response turn: send a separate system prompt with the detected intent,
#    the conversation history, and the register/gender decisions, ask for the Hebrew reply.

指导建议:

  • 将分类和回复生成拆分为两次调用,以便分别记录意图准确率。
  • 强制使用 JSON 输出并进行验证;如果解析失败,则回退到关键词匹配器。
  • 在希伯来语处理方面表现良好的模型(截至 2026 年年中):Anthropic Claude(Sonnet/Opus 等级)、OpenAI GPT-5.x 系列,以及 Google Gemini 3.x。模型名称和等级经常变化(GPT-4o/4.1 和 Gemini 2.x 已于 2026 年初退役),因此在确定使用某个模型前,请查看各提供商当前的模型列表。
  • 对于希伯来语原生方案,可评估 Dicta-LM 3.0(Dicta、巴伊兰大学),用于本地部署分类。
  • 防范提示注入(OWASP LLM01)。 如果自由文本用户输入会传递给能够触发操作(查询订单、转接人工客服、创建工单)的大模型,请将其视为不可信内容:用明确的分隔符包裹用户文本,指示模型忽略其中的任何指令,绝不回显系统提示词,并在执行前根据允许列表验证模型选择的操作。
  • Web 聊天组件采用流式传输,WhatsApp/Telegram 则先缓冲。 Web 聊天应流式传输令牌,以降低用户感知到的延迟;WhatsApp 和 Telegram 无法流式传输部分消息,因此应显示输入指示器,并在回复准备就绪后一次性发送完整内容。
实体提取

从希伯来语文本中提取以色列特有的实体:

import re

def extract_israeli_entities(text: str) -> dict:
    """Extract Israeli-specific entities from Hebrew text."""
    entities = {}

    # Israeli phone numbers.
    # The digit-boundary lookarounds are load-bearing. Without them the landline pattern
    # also matches the FIRST NINE characters of an unseparated mobile number, so
    # "0521234567" yields both the real number and the bogus "052123456". Anything that
    # later dials or messages an extracted number would then have a 50% chance of using a
    # truncated one. Matches are also de-duplicated while preserving order.
    phone_patterns = [
        r'(?<!\d)05\d[\s-]?\d{3}[\s-]?\d{4}(?!\d)',      # Mobile: 050-1234567
        r'(?<!\d)0[2-9][\s-]?\d{3}[\s-]?\d{4}(?!\d)',     # Landline: 02-1234567
        r'(?<!\d)\+?972[\s-]?\d{1,2}[\s-]?\d{3}[\s-]?\d{4}(?!\d)',  # International
    ]
    phones: list[str] = []
    for pattern in phone_patterns:
        for match in re.findall(pattern, text):
            # An Israeli landline written without separators is nine digits, and so is a
            # Teudat Zehut. Anything that passes the ID check digit is far more likely to
            # be an ID than a phone number, and classifying it as a phone means some
            # downstream job dials a national ID and files it in a non-sensitive field.
            if match.isdigit() and len(match) == 9 and is_valid_teudat_zehut(match):
                continue
            if match not in phones:
                phones.append(match)
    if phones:
        entities["phone_numbers"] = phones

    # NIS amounts (shekel)
    nis_patterns = [
        r'₪\s?[\d,]+(?:\.\d{2})?',           # ₪100 or ₪1,000.00
        r'[\d,]+(?:\.\d{2})?\s?(?:₪|שקל|שקלים|ש"ח|שח)',  # 100 שקל
    ]
    for pattern in nis_patterns:
        matches = re.findall(pattern, text)
        if matches:
            entities.setdefault("amounts_nis", []).extend(matches)

    # Israeli dates (DD/MM/YYYY or DD.MM.YYYY)
    date_patterns = [
        r'\d{1,2}[/.]\d{1,2}[/.]\d{2,4}',    # 14/03/2025
        r'\d{1,2}\s+ב?(?:ינואר|פברואר|מרץ|אפריל|מאי|יוני|יולי|אוגוסט|ספטמבר|אוקטובר|נובמבר|דצמבר)',
    ]
    for pattern in date_patterns:
        matches = re.findall(pattern, text)
        if matches:
            entities.setdefault("dates", []).extend(matches)

    # Teudat Zehut (9 digits, standalone)
    # NOTE: this regex matches ANY 9-digit run with no check-digit validation.
    # It will accept phone numbers, order IDs, and typos. Validate matches
    # with is_valid_teudat_zehut() below before treating them as a real ID.
    tz_pattern = r'(?<!\d)\d{9}(?!\d)'
    tz_matches = [m for m in re.findall(tz_pattern, text) if is_valid_teudat_zehut(m)]
    if tz_matches:
        entities["teudat_zehut"] = tz_matches

    # Email addresses
    email_pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
    email_matches = re.findall(email_pattern, text)
    if email_matches:
        entities["emails"] = email_matches

    return entities


def is_valid_teudat_zehut(value: str) -> bool:
    """Validate an Israeli Teudat Zehut using the official check-digit algorithm.

    A 9-digit run is NOT a valid ID just because it has 9 digits. This applies
    the Luhn-style weighting the Interior Ministry uses, so phone numbers and
    random digit runs are rejected.
    """
    digits = value.strip()
    if len(digits) != 9 or not digits.isdigit():
        return False
    # All-zeros passes the mod-10 check but is a placeholder, not an ID. It is the most
    # common value in test fixtures and seed data, so reject it explicitly.
    if set(digits) == {"0"}:
        return False
    total = 0
    for i, ch in enumerate(digits):
        n = int(ch) * (1 if i % 2 == 0 else 2)
        total += n if n < 10 else n - 9
    return total % 10 == 0
希伯来语情感分析
# Simple Hebrew sentiment lexicon (for basic analysis)
# For production, use an LLM or Hebrew-trained model
HEBREW_SENTIMENT = {
    # Positive
    "מעולה": 1.0, "מצוין": 1.0, "נהדר": 0.9, "אהבתי": 0.9,
    "טוב": 0.6, "יפה": 0.6, "נחמד": 0.5, "בסדר": 0.3,
    "תודה": 0.4, "תודה רבה": 0.7, "ממליץ": 0.8, "ממליצה": 0.8,
    "מרוצה": 0.8, "שמח": 0.7, "שמחה": 0.7, "אחלה": 0.8,
    "סבבה": 0.5, "קול": 0.5, "בומבה": 0.9,

    # Negative
    "גרוע": -0.9, "נורא": -0.9, "איום": -1.0, "מאכזב": -0.8,
    "רע": -0.7, "לא טוב": -0.6, "בעיה": -0.5, "תקלה": -0.5,
    "לא עובד": -0.7, "לא מרוצה": -0.8, "מתסכל": -0.7, "עצבני": -0.8,
    "חרא": -1.0, "זבל": -0.9, "בושה": -0.8, "אכזבה": -0.8,
}

def analyze_hebrew_sentiment(text: str) -> dict:
    """Basic Hebrew sentiment analysis using lexicon matching."""
    scores = []
    words_found = []

    # Check multi-word expressions first (sorted by length, longest first)
    sorted_expressions = sorted(HEBREW_SENTIMENT.keys(), key=len, reverse=True)

    remaining_text = text
    for expression in sorted_expressions:
        if expression in remaining_text:
            scores.append(HEBREW_SENTIMENT[expression])
            words_found.append(expression)
            remaining_text = remaining_text.replace(expression, "", 1)

    if not scores:
        return {"sentiment": "neutral", "score": 0.0, "words": []}

    avg_score = sum(scores) / len(scores)

    if avg_score > 0.2:
        sentiment = "positive"
    elif avg_score < -0.2:
        sentiment = "negative"
    else:
        sentiment = "neutral"

    return {
        "sentiment": sentiment,
        "score": round(avg_score, 2),
        "words": words_found,
    }

对话流程

希伯来语菜单树
# Conversation state machine for Hebrew chatbot
CONVERSATION_FLOWS = {
    "main_menu": {
        "message": "שלום! איך אפשר לעזור?\nבחר/י אחת מהאפשרויות:",
        "options": {
            "1": {"label": "הזמנה חדשה", "next": "new_order"},
            "2": {"label": "בדיקת סטטוס", "next": "check_status"},
            "3": {"label": "שאלות נפוצות", "next": "faq"},
            "4": {"label": "דבר/י עם נציג", "next": "human_handoff"},
        },
    },
    "new_order": {
        "message": "מעולה! בוא/י נתחיל הזמנה.\nמה תרצה/י להזמין?",
        "type": "free_text",
        "next": "order_quantity",
        "back": "main_menu",
    },
    "order_quantity": {
        "message": "כמה יחידות?",
        "type": "number",
        "validation": {"min": 1, "max": 100},
        "error": "נא להזין מספר בין 1 ל-100",
        "next": "order_confirm",
        "back": "new_order",
    },
    "order_confirm": {
        "message": "לסיכום:\n{order_summary}\n\nלאשר הזמנה?",
        "options": {
            "1": {"label": "אישור", "next": "order_complete"},
            "2": {"label": "ביטול", "next": "main_menu"},
        },
    },
    "order_complete": {
        "message": "ההזמנה בוצעה בהצלחה!\nמספר הזמנה: {order_id}\nתודה ויום טוב!",
        "next": "main_menu",
    },
    "check_status": {
        "message": "שלח/י לי את מספר ההזמנה (6 ספרות):",
        "type": "pattern",
        "pattern": r"^\d{6}$",
        "error": "מספר הזמנה צריך להכיל 6 ספרות. נסה/י שוב:",
        "next": "show_status",
        "back": "main_menu",
    },
    "faq": {
        "message": "שאלות נפוצות:\n\n1. שעות פעילות\n2. מדיניות החזרות\n3. אזורי משלוח\n4. אמצעי תשלום\n\nבחר/י נושא:",
        "options": {
            "1": {"label": "שעות פעילות", "response": "שעות פעילות:\nא'-ה': 9:00-17:00\nו': 9:00-13:00\nשבת: סגור"},
            "2": {"label": "החזרות", "response": "ניתן להחזיר מוצרים עד 14 יום מתאריך הרכישה.\nיש להציג חשבונית."},
            "3": {"label": "משלוחים", "response": "אנחנו שולחים לכל הארץ.\nמשלוח רגיל: 5-7 ימי עסקים.\nמשלוח מהיר: 1-2 ימי עסקים."},
            "4": {"label": "תשלום", "response": "אמצעי תשלום:\n- כרטיס אשראי (ויזה, מאסטרקארד, אמקס)\n- ביט / פייבוקס\n- העברה בנקאית\n- תשלומים (עד 12 תשלומים ללא ריבית)"},
        },
        "back": "main_menu",
    },
    "human_handoff": {
        "message": "מעביר אותך לנציג אנושי.\nשעות פעילות: א'-ה' 9:00-17:00.\n\nבינתיים, תאר/י בקצרה את הנושא שלך כדי שנוכל לעזור מהר יותר:",
        "type": "free_text",
        "action": "create_support_ticket",
    },
}
希伯来语回退处理
FALLBACK_RESPONSES = [
    "לא הצלחתי להבין. אפשר לנסח אחרת?",
    "סליחה, לא הבנתי את הבקשה. נסה/י שוב או הקלד/י 'תפריט' לאפשרויות.",
    "לא בטוח/ה שהבנתי. אפשר לבחור מהאפשרויות או לכתוב 'עזרה'.",
]

CONFUSED_AFTER_ATTEMPTS = (
    "נראה שאני מתקשה להבין. בוא/י ננסה אחרת.\n"
    "הקלד/י מספר מהתפריט, או 'נציג' לשיחה עם אדם."
)

def get_fallback_response(attempt_count: int) -> str:
    """Get an appropriate fallback response based on attempt count."""
    if attempt_count >= 3:
        return CONFUSED_AFTER_ATTEMPTS
    return FALLBACK_RESPONSES[attempt_count % len(FALLBACK_RESPONSES)]
转接至人工 Agent
async def handoff_to_human(user_id: str, context: dict):
    """Transfer conversation to human agent."""
    handoff_message = (
        "תודה על הסבלנות. מעביר/ה אותך לנציג אנושי.\n"
        "זמן המתנה משוער: {wait_time} דקות.\n\n"
        "כל מה שכתבת עד עכשיו יועבר לנציג."
    )

    # Create support ticket with conversation history
    ticket = {
        "user_id": user_id,
        "channel": context.get("channel", "web"),  # whatsapp/telegram/web
        "language": "he",
        "conversation_history": context.get("history", []),
        "detected_intent": context.get("last_intent", "unknown"),
        "sentiment": context.get("sentiment", "neutral"),
        "created_at": datetime.now().isoformat(),
    }

    # Queue for human agent
    await support_queue.add(ticket)

    return handoff_message.format(
        wait_time=await support_queue.estimated_wait()
    )

常见希伯来语聊天机器人短语

基本短语

如需一组可直接复制粘贴的希伯来语对话短语(问候、确认、处理中、成功、错误、回退、告别、等待、道歉),包括音译和每个短语的使用场景,请参阅 references/hebrew-chatbot-phrases.md。

捆绑资源

此技能在 scripts/ 目录中包含辅助脚本:

  • whatsapp-webhook-handler.py:完整的 WhatsApp Cloud API webhook 处理程序,支持签名验证、消息路由和希伯来语响应模板
  • telegram-bot-scaffold.py:支持希伯来语的 Telegram 机器人入门模板,包含内联键盘、对话状态管理和命令处理程序

references/ 目录中还包含参考文档:

  • hebrew-chatbot-phrases.md:全面的希伯来语机器人对话短语,按类别整理并附有音译
  • whatsapp-business-api-guide.md:面向以色列企业的 WhatsApp Business API 分步设置指南

注意事项

  • 希伯来语聊天机器人响应必须采用从右向左(RTL)对齐。Agent 可能会生成缺少 dir="rtl" 属性的 HTML/CSS,导致希伯来语文本左对齐,显得不自然。
  • 希伯来语动词有性别变化。聊天机器人在称呼用户时,应使用不区分性别的形式,或询问用户偏好的性别。Agent 可能会默认使用希伯来语阳性形式。
  • 常见的希伯来语问候语会随一天中的时段变化:"בוקר טוב"(早上)、"צהריים טובים"(中午)、"ערב טוב"(晚上)。Agent 可能会不分时段地使用同一种问候语。
  • 希伯来语的词语切分方式不同于英语。前置介词(ב-、ל-、מ-)会与后面的词连写。Agent 可能会错误地拆分词元,导致意图检测失效。
  • 以色列用户期望聊天机器人采用非正式的交流方式("דוגרי")。过于正式的希伯来语听起来机械、不自然。Agent 可能会生成正式的希伯来语,让以色列用户感到疏远。
  • Webhook 签名验证必须采用默认拒绝策略,且不能以密钥是否存在为条件。 看似自然的 if APP_SECRET: 包装语句正是问题所在:用于检测密钥未设置的环境变量校验通常位于 if __name__ == "__main__": 下,而 WSGI 服务(gunicorn handler:app)不会执行这段代码。Webhook 端点实际部署时正是采用这种方式,因为 Meta 要求使用 HTTPS。结果就是,生产环境中的端点会接受所有伪造的负载,而所有本地测试却都能通过。密钥缺失时,应拒绝提供服务(HTTP 500)。
  • 切勿在 webhook 主机上部署 app.run(debug=True)。 Werkzeug 调试器本身就支持远程代码执行,而 webhook 主机必然可从公网访问。将开发服务器绑定到 127.0.0.1,并在生产环境中在其前方部署真正的 WSGI 服务和 TLS。
  • 九位数的一串数字不是 Teudat Zehut,未锚定的电话号码正则表达式则会凭空识别出号码。 使用校验位算法验证 ID 匹配结果,并拒绝全零占位符;使用数字边界前后查找锚定电话号码模式,否则固定电话模式也会匹配一串未分隔手机号码的前九个字符,除了真实号码,还会多给你一个截断号码。
  • 在一个命名常量中固定 Graph API 版本。 Meta 会在某个版本发布约两年后将其弃用(v25.0 于 2026 年 2 月发布,支持至 2028 年 7 月 29 日)。将版本散布在四个文件的 URL 中,会让原本只需改一行的升级变成考古工作;若有一处遗漏,则只会导致某些消息类型出错。

参考链接

来源URL检查内容
WhatsApp Cloud API 文档https://developers.facebook.com/documentation/business-messaging/whatsapp/about-the-platformAPI 版本、消息类型、webhook 格式
Meta Graph API 更新日志https://developers.facebook.com/docs/graph-api/changelog/版本支持期限。每个版本大约在发布两年后停止支持;v25.0(2026 年 2 月)支持至 2028 年 7 月 29 日
Telegram Bot API 文档https://core.telegram.org/bots/apiBot API 方法、内联键盘、webhook 设置。页面底部的更新日志会标明当前版本(Bot API 10.3,2026 年 8 月 24 日);本技能刻意不固定版本,因此请查看更新日志,了解之后新增的基础功能
python-telegram-bot 文档https://docs.python-telegram-bot.org/en/stable/库版本、异步 API 变更
Meta Business Suitehttps://business.facebook.com/模板创建、电话号码设置

故障排除

部署后 WhatsApp webhook 返回 403

  • 原因:X-Hub-Signature-256 验证失败,因为未设置 APP_SECRET 或设置了错误的值。Meta 使用你的 App Secret 为 webhook 负载签名,而不是使用验证令牌。
  • 解决方案:将 WHATSAPP_APP_SECRET 设置为 Meta Developers > App Settings > Basic > App Secret 中的值。重启服务,并确认值完全一致(末尾没有空格)。

Meta 拒绝了 WhatsApp 模板中的希伯来语文本

  • 原因:模板正文中的变量({{1}})构成了整条消息,或者在 Utility 类别的模板中使用了促销用语。Meta 的审核流程会标记这些模式。
  • 解决方案:确保模板文本中有足够的固定内容来搭配变量。如果被拒绝,请在 Meta Business Suite > WhatsApp > Message Templates 中查看拒绝原因,调整措辞后重新提交。

Telegram 机器人在群组中忽略消息

  • 原因:BotFather 默认启用了隐私模式。在隐私模式下,机器人只会接收以 / 开头或通过 @username 提及机器人的消息。
  • 解决方案:可以将机器人设计为在群组中仅使用命令(推荐),也可以通过 BotFather 关闭隐私模式:/setprivacy > Disable。请注意,关闭隐私模式意味着机器人会接收所有群组消息。

RTL 聊天组件中的文本尽管设置了 direction: rtl,仍然左对齐

  • 原因:子元素覆盖了文本方向,或者 CSS 应用到了错误的容器。使用在 body 层级设置了 direction: ltr 的 UI 框架时常会发生这种情况。
  • 解决方案:在最外层的聊天容器 HTML 元素上设置 dir="rtl"(而不仅仅是设置 CSS)。还要分别为输入框和所有消息气泡容器添加 direction: rtl。使用浏览器 DevTools 检查覆盖发生的位置。

webhook 处理程序日志显示“WHATSAPP_ACCESS_TOKEN was renamed to FACEBOOK_ACCESS_TOKEN”

  • 原因:环境变量已重命名。随附的 scripts/whatsapp-webhook-handler.py 过去会读取 WHATSAPP_ACCESS_TOKEN;现在只读取 FACEBOOK_ACCESS_TOKEN,因此使用旧名称配置的服务没有访问令牌。错误会在启动时记录;本地开发服务器随后会退出,而在 gunicorn 或其他 WSGI 服务下,应用会继续运行,但每条发出的消息都会因身份验证错误而失败。
  • 解决方案:在设置该变量的所有位置(shell 配置文件、.env、systemd 单元、容器或托管平台密钥)将其重命名为 FACEBOOK_ACCESS_TOKEN,然后重启。令牌值本身不变。

评论

支持 Markdown