-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathai_handler.py
More file actions
executable file
·197 lines (162 loc) · 7.82 KB
/
Copy pathai_handler.py
File metadata and controls
executable file
·197 lines (162 loc) · 7.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# ai_handler.py
import re
import time
from datetime import datetime
from config import config
from member_loader import get_family_call_map, get_primary_child, get_family, get_primary_call, mask_names, unmask_names
import llm_client
def needs_search(text):
"""検索キーワードを含む発話はWeb検索を使う"""
return any(kw in text for kw in config.SEARCH_KEYWORDS)
# アンディ待機モード状態
_andy_wait = {
"active": False,
"expires_at": 0.0,
}
def _is_english(text):
"""テキストが主に英語かどうか判定(英字比率50%以上)"""
letters = [c for c in text if c.isalpha()]
if not letters:
return False
en_count = sum(1 for c in letters if ord(c) < 128)
return (en_count / len(letters)) >= 0.5
def detect_intent(user_text):
"""発話の意図を判定して返す
Returns:
'andy_direct' : 「アンディ」+英語 or 待機モード中+英語 → アンディに転送
'andy_english' : 「アンディ」+「英語で/英語に」→ フレーズ教えて待機モードON
'andy_minecraft' : 「アンディ」のみ → Minecraft文脈でassistantが応答
'english_yuno' : 英語のみ → assistantが英語で応答
'normal' : それ以外 → 通常応答
"""
has_andy = any(kw in user_text for kw in config.ANDY_KEYWORDS)
has_en_kw = any(kw in user_text for kw in config.ENGLISH_TRANSLATE_KEYWORDS)
is_english = _is_english(user_text)
# 待機モードの期限チェック
if _andy_wait["active"] and time.time() > _andy_wait["expires_at"]:
_andy_wait["active"] = False
print("[INTENT] アンディ待機モード タイムアウト")
# 待機モード中 or 「アンディ」+英語 → アンディに転送
if is_english and (_andy_wait["active"] or has_andy):
return "andy_direct"
# 「アンディ」+「英語で/英語に」→ フレーズ教えて待機モードON
if has_andy and has_en_kw:
return "andy_english"
# 「アンディ」のみ → マイクラ文脈
if has_andy:
return "andy_minecraft"
# 英語のみ → ユノが英語で応答
if is_english:
return "english_yuno"
return "normal"
def set_andy_wait_mode(active):
"""アンディ待機モードのON/OFF"""
_andy_wait["active"] = active
_andy_wait["expires_at"] = time.time() + config.ANDY_WAIT_TIMEOUT if active else 0.0
print(f"[INTENT] アンディ待機モード {'ON' if active else 'OFF'}")
# 英語モード
_english_mode = {
"active": False,
}
def set_english_mode(active: bool):
_english_mode["active"] = active
print(f"[INTENT] 英語モード {'ON' if active else 'OFF'}")
def is_english_mode() -> bool:
return _english_mode["active"]
def _remove_katakana_reading(text: str) -> str:
"""「英単語」の後に続くカタカナ読みを除去する"""
# 「発音は〜って感じ」パターンを除去
text = re.sub(r'[。、]?発音は[「」ァ-ヶー・\s]+(?:って感じ)?(?:かな)?', '', text)
# 英単語直後のカタカナ括弧を除去(例:school(スクール))
text = re.sub(r'([A-Za-z\"\'])\s*[((][ァ-ヶー・]+[))]', r'\1', text)
return text.strip()
def get_ai_response(user_text, context, speaker, mode="normal"):
"""AIで応答生成(llm_client経由・プロバイダー切り替え対応)"""
# ★ AIに渡す前に名前をマスク
masked_text = mask_names(user_text)
recent_text = ""
for conv in context.get("recent_conversations", []):
recent_text += f" {mask_names(conv['user'])} → {mask_names(conv['assistant'])}\n"
# ★ 家族構成もマスク
family_text = chr(10).join(
f"- {mask_names(name)}: {role}"
for name, role in context.get('family', {}).items()
)
notes_text = mask_names(context.get('notes', '注釈なし'))
game_text = ""
for game in context.get("game_progress", []):
game_text += f" {game['game_name']}: {game['progress']}\n"
if needs_search(user_text):
sentence_rule = "- 回答は検索結果の内容を優先してね!3〜4文、150文字程度でまとめてね!"
else:
sentence_rule = "- 2文程度、50文字以内でまとめてね!"
_call_map = get_family_call_map() # {"family1": "お母さん", ...}
_child_name = get_primary_child()
family_call = _call_map.get(speaker)
if family_call:
speaker_rule = f"- 今話しかけているのは{family_call}です。{family_call}に向けて返答してください。{_child_name}には話しかけないでください。"
elif speaker == "other":
speaker_rule = f"- 話者は{_child_name}ではありません。返答に「{_child_name}」という名前を含めないでください。"
else:
speaker_rule = f"- 話者は{_child_name}です。"
mode_rule = ""
if mode == "andy_english":
mode_rule = f"""
- userがMinecraftのAI assistantに英語で話しかけたいと思っています。
- userの日本語を英語フレーズに変換して1つ教えてあげてください。
- 例:「"Andy, where are the diamonds?" って言ってみよう!」
- フレーズは短く、言いやすいものにしてください。
"""
elif mode == "andy_minecraft":
mode_rule = """
- MinecraftのAI assistantに関する話題です。
- Minecraftの文脈で自然に会話してください。
"""
elif mode == "english_yuno":
mode_rule = "- 英語で返答してください。"
# マスクラベルの呼び名ルールを動的生成
family_call_rules = "\n".join(
f"- [家族{i}]のことは「{get_primary_call(member)}」と呼んでください。"
for i, member in enumerate(get_family(), 1)
)
prompt = f"""
あなたの名前は「{config.ASSISTANT_NAME}」。{context.get('persona', '')} {config.ASSISTANT_PERSONA}
# 家族構成
{family_text}
# 最近の会話
{recent_text if recent_text else '今日はまだ会話していない'}
# ゲーム進行状況
{game_text if game_text else 'ゲーム情報なし'}
# 子供についての注釈
{notes_text}
現在時刻: {datetime.now().strftime('%Y年%m月%d日 %H:%M')}
話者: {speaker}
会話のルール:
{mode_rule}
- {sentence_rule}
- {speaker_rule}
- {family_call_rules}
- userの発言を繰り返したり、言い換えたりしないでね!
- 直近、5件程度の出力にuserの名前がある場合は、userの名前を入れないでね!
- 直近、5件程度の出力に「お」とか「あ」のような感嘆がある場合は、感嘆を入れないようにしてね。
- 「最近の会話」を参照し、話題が続いている場合はそれまでの文脈を踏まえて返答する
- 以下の「好きなもの」に関するキーワードが出たら、前の文脈と自然に繋げる
{context.get('interests', '')}
- わからないことは必要以上に想像で補わず、「わからないなー」と正直に回答してください。
- 「おはよう」等の挨拶は、された時だけ返してね!
- 「〜だよ」「〜だな」「〜じゃない?」など親しみやすい口調
- 電話で喋っているようなつもりで話してください。
- できないお願いは「ごめんね、それ僕にはできないんだ〜」って優しく断ってね。
ユーザー: {masked_text}
アシスタント: """
try:
if needs_search(user_text):
raw = llm_client.call_search(prompt)
else:
raw = llm_client.call(prompt)
ai_text = unmask_names(raw)
ai_text = _remove_katakana_reading(ai_text)
return ai_text
except Exception as e:
print(f"[ERROR] LLM エラー: {e}")
return "ごめん、今ちょっと調子悪いや...後でまた話そうね!"