关于 hebrew-chatbot-builder
Hebrew Chatbot Builder 是用于构建希伯来语对话机器人的技能,覆盖消息平台和网页聊天组件。它侧重希伯来语语体与性别表达、以色列实体提取,以及从右向左的正确显示。
主要功能
- 01WhatsApp 与 Telegram 脚手架
- 02希伯来语语体与性别表达指导
- 03RTL 网页聊天布局指导
- 04以色列实体提取模式
- 05希伯来语意图与回复设计
适用场景
- 01构建希伯来语 WhatsApp 订单状态机器人
- 02为网站添加 RTL 希伯来语聊天组件
- 03为正式场景设计希伯来语机器人对话
- 04处理以色列日期、电话号码和金额
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ skills-il/ developer-tools --skill hebrew-chatbot-builder安装后在 Claude Code 中输入/hebrew-chatbot-builder确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evidence.jsonmetadata.jsonoptimization-log.jsonSKILL_HE.mdreferences/hebrew-chatbot-phrases.mdwhatsapp-business-api-guide.mdscripts/telegram-bot-scaffold.pywhatsapp-webhook-handler.py详细介绍
希伯来语聊天机器人构建器
构建可投入生产环境的对话式 AI 聊天机器人,并原生支持希伯来语。本技能涵盖平台集成(WhatsApp、Telegram、网页)、希伯来语语言模式、RTL UI 组件,以及面向希伯来语使用者的对话流程设计。
说明
构建希伯来语聊天机器人时,请遵循以下流程:
- 确定平台/渠道。 确认机器人运行在 WhatsApp Business API、Telegram、嵌入式网页小组件,还是多个渠道上。每种渠道都有不同的脚手架和消息模型。
- 确认正式或非正式语体。 询问用户,受众期望使用非正式希伯来语(“דוגרי”,推荐用于大多数面向消费者的机器人)还是正式希伯来语(政府、银行、法律)。这一选择会影响每一条回复文案。
- 询问性别处理策略。 决定是尽早询问并记住用户的性别、全程使用性别中立的措辞,还是使用斜杠写法(“את/ה”)。请参阅“性别感知回复”部分。
- 使用相应的内置脚本搭建脚手架。 WhatsApp 使用
scripts/whatsapp-webhook-handler.py,Telegram 使用scripts/telegram-bot-scaffold.py作为起点,然后调整希伯来语回复模板和对话流程。 - 接入实体提取。 添加
extract_israeli_entities()(或等效实现),使机器人能够从希伯来语自由文本输入中识别以色列电话号码、谢克尔金额、日期和 Teudat Zehut。 - 验证 RTL 渲染。 对于网页小组件,在最外层容器上设置
dir="rtl"并在真实设备上测试。对于 WhatsApp 和 Telegram,请验证希伯来语文本和交互按钮在 iOS 和 Android 上都能正确渲染。
示例
示例 1:希伯来语 WhatsApp 订单状态机器人
用户说:“构建一个希伯来语 WhatsApp 机器人,让客户可以查询订单状态。”
操作:
- 确定渠道:WhatsApp Business Cloud API。
- 确认使用非正式语体(面向消费者)和性别中立的措辞,以覆盖更广泛的用户。
- 从
scripts/whatsapp-webhook-handler.py开始,其中已包含 webhook 验证、签名检查和希伯来语回复模板。 - 接入交互式按钮菜单(“בדיקת הזמנה”、“שאלות נפוצות”、“דבר/י עם נציג”),并添加
waiting_order_number状态,用于验证订单号并查询订单。 - 通过 Meta Business Suite 提交希伯来语
order_confirmation_he模板,以便主动发送更新。
结果:一个可运行的 webhook 处理程序,能用希伯来语问候用户、处理按钮回复并回答订单状态查询。
示例 2:为现有网站添加 RTL 希伯来语网页聊天小组件
用户说:“为我的网站添加一个布局正确的 RTL 聊天小组件。”
操作:
- 确定渠道:嵌入式网页小组件。
- 确认使用非正式语体,并在 CTA 中使用斜杠写法。
- 使用“网页聊天小组件”部分中的 RTL 聊天气泡布局 CSS,以及
MessageBubble/detectDirection组件。 - 在最外层聊天容器元素上设置
dir="rtl"(不只是在 CSS 中设置),并为输入框添加direction: rtl。 - 在浏览器 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)是以色列企业的推荐方案:
- 创建 Meta Business 账户,网址为 business.facebook.com
- 在 Meta Business Suite 中设置 WhatsApp Business 账户
- 在 Meta Developers 中创建应用并添加 WhatsApp 产品
- 获取电话号码:支持以色列号码(+972)
- 生成访问令牌,用于 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 设置
- 在 Telegram 上打开 @BotFather
- 发送
/newbot并按照提示操作 - 设置希伯来语说明:发送
/setdescription,然后发送希伯来语文本 - 设置希伯来语命令菜单:
/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-platform | API 版本、消息类型、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/api | Bot API 方法、内联键盘、webhook 设置。页面底部的更新日志会标明当前版本(Bot API 10.3,2026 年 8 月 24 日);本技能刻意不固定版本,因此请查看更新日志,了解之后新增的基础功能 |
| python-telegram-bot 文档 | https://docs.python-telegram-bot.org/en/stable/ | 库版本、异步 API 变更 |
| Meta Business Suite | https://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,然后重启。令牌值本身不变。