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、Web)、ヘブライ語の言語パターン、RTL UIコンポーネント、ヘブライ語話者向けの会話フロー設計を扱います。
手順
ヘブライ語チャットボットを構築する際は、次の手順に従ってください。
- プラットフォーム/チャネルを特定します。 ボットがWhatsApp Business API、Telegram、埋め込み型Webウィジェットのどれで動作するのか、あるいは複数を同時に使用するのかを確認します。それぞれでスキャフォールドとメッセージモデルが異なります。
- フォーマルな文体か、カジュアルな文体かを確認します。 ユーザーに、対象ユーザーがカジュアルなヘブライ語(「דוגרי」。ほとんどの消費者向けボットに推奨)とフォーマルなヘブライ語(政府、銀行、法律関連)のどちらを期待しているか尋ねます。この選択は、すべての応答文に影響します。
- 性別への対応方針を確認します。 ユーザーの性別を早い段階で尋ねて記憶する、全体を通して性別に依存しない表現を使う、スラッシュ表記(「את/ה」)を使う、のいずれかを決めます。「性別に配慮した応答」セクションを参照してください。
- 該当する同梱スクリプトを使ってスキャフォールドを作成します。 WhatsAppの場合は
scripts/whatsapp-webhook-handler.py、Telegramの場合はscripts/telegram-bot-scaffold.pyを出発点として使用し、ヘブライ語の応答テンプレートと会話フローを適用します。 - エンティティ抽出を組み込みます。 ボットが自由形式のヘブライ語入力から、イスラエルの電話番号、シェケルの金額、日付、Teudat Zehutを認識できるように、
extract_israeli_entities()(または同等の関数)を追加します。 - RTL表示を確認します。 Webウィジェットでは、最も外側のコンテナに
dir="rtl"を設定し、実機でテストします。WhatsAppとTelegramでは、iOSとAndroidの両方でヘブライ語テキストとインタラクティブボタンが正しく表示されることを確認します。
例
例1:ヘブライ語のWhatsApp注文状況ボット
ユーザーの依頼:「注文状況を確認できる、ヘブライ語のWhatsAppボットを作ってください。」
対応:
- チャネルを特定します:WhatsApp Business Cloud API。
- カジュアルな文体(消費者向け)と、幅広いユーザーに対応できる性別に依存しない表現を確認します。
- Webhookの検証、署名チェック、ヘブライ語の応答テンプレートがすでに含まれている
scripts/whatsapp-webhook-handler.pyを出発点として使用します。 - インタラクティブなボタンメニュー(「בדיקת הזמנה」、「שאלות נפוצות」、「דבר/י עם נציג」)と、注文番号を検証して検索する
waiting_order_number状態を組み込みます。 - 事前通知の送信用に、ヘブライ語の
order_confirmation_heテンプレートをMeta Business Suiteから申請します。
結果:ヘブライ語でユーザーに挨拶し、ボタンへの返信を振り分け、注文状況に関する問い合わせに答える、動作可能なWebhookハンドラーが完成します。
例2:既存サイトにRTL対応のヘブライ語Webチャットウィジェットを追加する
ユーザーの依頼:「適切なRTLレイアウトのヘブライ語チャットウィジェットをWebサイトに追加してください。」
対応:
- チャネルを特定します:埋め込み型Webウィジェット。
- カジュアルな文体と、CTAでのスラッシュ表記を確認します。
- 「Webチャットウィジェット」セクションにある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)は、イスラエルの企業に推奨される方法です。
- business.facebook.com で Meta Business アカウントを作成します
- 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では、これらを見逃してしまいます。デモの範囲を超える用途では、たとえば YAP 形態素解析ツール(https://github.com/OnlpLab/yap)や Stanza のヘブライ語パイプラインなど、形態論に対応したヘブライ語 NLP ツールを使うか、後述の LLM に切り替えてください。
LLM による意図分類と応答生成(本番環境に推奨):
部分文字列マッチングでは、言い換え、誤字、形態変化に対応できません。本番環境では、LLM に意図を分類させ、応答の下書きを生成させます。簡潔なパターンは次のとおりです。
# 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.
ガイダンス:
- 分類と応答生成は別々の 2 回の呼び出しにして、意図の精度を個別に記録できるようにします。
- 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、Bar-Ilan University)を評価してください。
- プロンプトインジェクション(OWASP LLM01)への対策を講じてください。 注文照会、担当者への引き継ぎ、チケット作成などのアクションを実行できる LLM にフリーテキストのユーザー入力を渡す場合、その入力は信頼できないものとして扱います。ユーザーテキストを明確な区切り文字で囲み、その中の指示を無視するようモデルに指示してください。また、システムプロンプトを決してそのまま出力させず、選択されたアクションを許可リストに照らして検証してから実行します。
- 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)]
人間のエージェントへの引き継ぎ
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)に配置する必要があります。エージェントが
dir="rtl"属性のないHTML/CSSを生成する場合があり、その結果、ヘブライ語のテキストが左揃えになって不自然に見えることがあります。 - ヘブライ語では、動詞の活用が性別によって変わります。ユーザーに話しかけるチャットボットでは、性別に依存しない形を使うか、ユーザーが希望する性別を尋ねる必要があります。エージェントは、男性形のヘブライ語をデフォルトで使うことがあります。
- ヘブライ語の一般的な挨拶は、時間帯によって変わります。「בוקר טוב」(朝)、「צהריים טובים」(昼)、「ערב טוב」(夕方)です。エージェントは、時間帯に関係なく同じ挨拶を使うことがあります。
- ヘブライ語の単語のトークン化は英語とは異なります。接頭辞の前置詞(ב-、ל-、מ-)は後続の単語に付けて表記します。エージェントがトークンを誤って分割し、インテント検出に支障をきたすことがあります。
- イスラエルのユーザーは、くだけたチャットボットとのやりとり(「דוגרי」)を好みます。かしこまりすぎたヘブライ語は機械的で不自然に聞こえます。エージェントがフォーマルなヘブライ語を生成すると、イスラエルのユーザーに敬遠されることがあります。
- Webhookの署名検証は必ずフェイルクローズにし、シークレットが設定されているかどうかを条件にして検証を行わないでください。 一見自然な
if APP_SECRET:のラッパーが不具合の原因です。未設定のシークレットを検出する環境変数の検証は通常if __name__ == "__main__":の中に置かれますが、これはWSGIサーバー(gunicorn handler:app)では実行されません。MetaがHTTPSを要求するため、Webhookエンドポイントは実際にはこの方法でデプロイされます。その結果、ローカルのテストにはすべて合格する一方、本番環境のエンドポイントが偽造されたペイロードをすべて受け入れてしまいます。シークレットが未設定の場合は、サービスの提供を拒否してください(HTTP 500)。 - Webhookホストで
app.run(debug=True)を絶対に使わないでください。 Werkzeugデバッガーは設計上、リモートコード実行を可能にします。また、Webhookホストは定義上、パブリックにアクセス可能です。開発サーバーは127.0.0.1にバインドし、本番環境ではその前段に実際のWSGIサーバーとTLSを配置してください。 - 9桁の数字列はTeudat Zehutではなく、開始位置や終了位置が固定されていない電話番号の正規表現は、存在しない番号を作り出します。 IDの一致はチェックディジットアルゴリズムで検証し、すべてゼロのプレースホルダーは拒否してください。電話番号のパターンには、数字との境界を示す先読み・後読みを指定してください。そうしないと、固定電話のパターンも区切りのない携帯電話番号の先頭9桁に一致し、実際の番号に加えて切り詰められた番号まで取得してしまいます。
- Graph APIのバージョンは、名前付き定数ひとつに固定してください。 Metaは、バージョンをリリースからおよそ2年後に廃止します(v25.0は2026年2月のリリースで、2028年7月29日までサポートされます)。4つのファイルにわたるURLにバージョンを散在させると、1行の更新が考古学のような作業になり、更新漏れがあると一部のメッセージタイプだけで問題が発生します。
参考リンク
| ソース | 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/ | バージョンのサポート期間。各バージョンはリリースから約2年後にサポートが終了します。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 を返す
- 原因:
APP_SECRETが設定されていないか、値が正しくないため、X-Hub-Signature-256の検証に失敗しています。Meta は検証トークンではなく、App Secret を使って Webhook ペイロードに署名します。 - 解決方法:Meta Developers > App Settings > Basic > App Secret にある値を
WHATSAPP_APP_SECRETに設定します。サーバーを再起動し、値が完全に一致していること(末尾に空白がないこと)を確認してください。
WhatsApp テンプレートのヘブライ語テキストが Meta に拒否される
- 原因:テンプレート本文にメッセージ全体を占める変数({{1}})が含まれているか、Utility カテゴリーのテンプレートで宣伝文句を使用しています。Meta の審査プロセスでは、こうしたパターンが検出されます。
- 解決方法:変数の前後に、固定テキストを十分に含めてください。拒否された場合は、Meta Business Suite > WhatsApp > Message Templates で拒否理由を確認し、文面を修正して再送信してください。
Telegram ボットがグループ内のメッセージを無視する
- 原因:BotFather では、プライバシーモードがデフォルトで有効になっています。プライバシーモードでは、ボットは
/で始まるメッセージ、または @username でボットに言及したメッセージだけを受信します。 - 解決方法:グループ内ではコマンドのみで動作するようにボットを設計する(推奨)か、BotFather でプライバシーモードを無効にします:
/setprivacy> Disable。プライバシーモードを無効にすると、ボットはグループ内のすべてのメッセージを受信することに注意してください。
direction: rtl にもかかわらず、RTL チャットウィジェットのテキストが左揃えになる
- 原因:子要素が方向を上書きしているか、CSS が誤ったコンテナーに適用されています。
direction: ltrを body レベルで設定する UI フレームワークを使用している場合によく発生します。 - 解決方法:CSS だけでなく、最も外側のチャットコンテナーの HTML 要素にも
dir="rtl"を設定します。また、入力フィールドと各メッセージバブルコンテナーにも個別に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 サーバーではアプリが動作し続け、送信するすべてのメッセージが認証エラーになります。 - 解決方法:シェルプロファイル、
.env、systemd ユニット、コンテナー、ホスティングのシークレットなど、変数を設定しているすべての箇所で名前をFACEBOOK_ACCESS_TOKENに変更し、再起動します。トークンの値自体は変わりません。