izuemon commited on
Commit
1394d41
·
verified ·
1 Parent(s): 6c3fd2e

Update v2_gpt.py

Browse files
Files changed (1) hide show
  1. v2_gpt.py +68 -215
v2_gpt.py CHANGED
@@ -1,4 +1,4 @@
1
- # main_debug.py
2
  import json
3
  import os
4
  import random
@@ -6,15 +6,11 @@ import string
6
  import threading
7
  import time
8
  import traceback
9
- from collections import deque
10
  from datetime import datetime
11
 
12
  import requests
13
- import torch
14
- from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline, set_seed
15
-
16
  from channel_auth import ChannelWorksClient
17
- from channel_msg import ChannelMessenger # ★ ライブラリ名修正
18
 
19
  # ---------------------------------------------------------------------------
20
  # 設定
@@ -22,29 +18,24 @@ from channel_msg import ChannelMessenger # ★ ライブラリ名修正
22
  CW_EMAIL = os.environ["CW_EMAIL"]
23
  CW_PASSWORD = os.environ["CW_PASSWORD"]
24
 
25
- ROOM_ID = "240996"
26
- TARGET_CHAT_ID = "583391"
27
  GROUP_PATH = f"/groups/{TARGET_CHAT_ID}"
28
 
29
- # ★ ローカル LLM 設定
30
- MODEL_NAME = "sbintuitions/sarashina2.2-3b-instruct-v0.1"
31
- SYSTEM_PROMPT = "あなたは親切な日本語アシスタントです。簡潔に回答してください。"
32
- MAX_NEW_TOKENS = 256
33
- TEMPERATURE = 0.7
34
- TOP_P = 0.9
35
- SEED = 123
36
- HISTORY_MAX_TURNS = 10 # 保持する user+assistant ペア数
37
 
38
- # 生成中に届いたメッセージをまとめるまでの待ち時間(秒)
39
- BATCH_DEBOUNCE_SEC = 0.3
40
 
41
  CHANNEL_SEND_URL = (
42
  f"https://api.channel.works/desk/channels/{ROOM_ID}"
43
  f"/groups/{TARGET_CHAT_ID}/messages"
44
  )
45
 
46
- # ★ デバッグログを全開にする
47
- LOG_LEVEL = ChannelWorksClient.LOG_DEBUG # 2
48
 
49
 
50
  # ---------------------------------------------------------------------------
@@ -55,100 +46,48 @@ def log(tag: str, msg: str) -> None:
55
  print(f"[{ts}] [{tag}] {msg}", flush=True)
56
 
57
 
58
- def log_block(tag: str, title: str, body: str) -> None:
59
- ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
60
- print(f"\n[{ts}] [{tag}] ===== {title} =====", flush=True)
61
- print(body, flush=True)
62
- print(f"[{ts}] [{tag}] ===== /{title} =====\n", flush=True)
63
-
64
-
65
  def _make_request_id() -> str:
66
  suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=4))
67
  return f"desk-web-{int(time.time() * 1000)}{suffix}"
68
 
69
 
70
  # ---------------------------------------------------------------------------
71
- # ローカル LLM (Sarashina2.2-3B-instruct)
72
  # ---------------------------------------------------------------------------
73
- _chat_pipeline = None
74
- _model_lock = threading.Lock()
75
-
76
-
77
- def load_model() -> None:
78
- """起動時に一度だけモデルをロードする。"""
79
- global _chat_pipeline
80
- log("MODEL", f"loading {MODEL_NAME} ...")
81
- t0 = time.monotonic()
82
-
83
- set_seed(SEED)
84
 
85
- tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
86
- # pad_token 未設定モデルへの保険
87
- if tokenizer.pad_token is None:
88
- tokenizer.pad_token = tokenizer.eos_token
89
 
90
- model = AutoModelForCausalLM.from_pretrained(
91
- MODEL_NAME,
92
- torch_dtype=torch.bfloat16,
93
- device_map="auto",
94
- )
95
-
96
- _chat_pipeline = pipeline(
97
- "text-generation",
98
- model=model,
99
- tokenizer=tokenizer,
100
- )
101
-
102
- log("MODEL", f"loaded in {time.monotonic() - t0:.1f}s")
103
 
104
 
105
- def _extract_answer(output: dict) -> str:
106
  """
107
- pipeline の出力から assistant の本文だけを取り出す。
108
- - return_full_text=False かつ messages 入力の場合は文字列で返ることが多い
109
- - 環境によっては messages のリストで返ることもあるため両対応
 
 
110
  """
111
- gen = output.get("generated_text")
112
- if isinstance(gen, list):
113
- for m in reversed(gen):
114
- if isinstance(m, dict) and m.get("role") == "assistant":
115
- return (m.get("content") or "").strip()
116
- return ""
117
- if isinstance(gen, str):
118
- return gen.strip()
119
- return str(gen).strip()
120
-
121
-
122
- def ask_model(history: list[dict], user_text: str) -> str:
123
- """
124
- history: [{"role": "user"/"assistant", "content": "..."}] のリスト
125
- user_text: 今回のユーザー入力 (生成中に溜まったメッセージを連結したもの)
126
- """
127
- messages = [{"role": "system", "content": SYSTEM_PROMPT}]
128
- messages.extend(history)
129
- messages.append({"role": "user", "content": user_text})
130
-
131
- log_block("LLM-REQ", "messages",
132
- json.dumps(messages, ensure_ascii=False, indent=2)[:4000])
133
-
134
- t0 = time.monotonic()
135
- # 推論は直列化する (同時呼び出しによる VRAM 圧迫を避ける)
136
- with _model_lock:
137
- outputs = _chat_pipeline(
138
- messages,
139
- max_new_tokens=MAX_NEW_TOKENS,
140
- do_sample=True,
141
- temperature=TEMPERATURE,
142
- top_p=TOP_P,
143
- return_full_text=False,
144
- num_return_sequences=1,
145
- )
146
- dt = time.monotonic() - t0
147
- log("LLM-RES", f"elapsed={dt:.2f}s")
148
-
149
- answer = _extract_answer(outputs[0])
150
- log_block("LLM-RES-BODY", "answer", answer[:2000])
151
- return answer
152
 
153
 
154
  # ---------------------------------------------------------------------------
@@ -159,8 +98,6 @@ def send_channel_message(client: ChannelWorksClient, text: str) -> requests.Resp
159
  x_account = tokens.get("x-account") or ""
160
  x_account_refresh = tokens.get("x-account-refresh") or ""
161
 
162
- log("SEND-TOK", f"x-account={x_account[:24]}... refresh={x_account_refresh[:24]}...")
163
-
164
  cookie_parts = []
165
  if x_account:
166
  cookie_parts.append(f"x-account={x_account}")
@@ -194,19 +131,14 @@ def send_channel_message(client: ChannelWorksClient, text: str) -> requests.Resp
194
  "customPayload": None,
195
  }
196
 
197
- log("SEND-REQ", f"POST {CHANNEL_SEND_URL}")
198
- log_block("SEND-REQ-BODY", "request", json.dumps(body, ensure_ascii=False, indent=2))
199
-
200
- t0 = time.monotonic()
201
  resp = requests.post(
202
  CHANNEL_SEND_URL,
203
  headers=headers,
204
  data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
205
  timeout=30,
206
  )
207
- dt = time.monotonic() - t0
208
- log("SEND-RES", f"status={resp.status_code} elapsed={dt:.2f}s")
209
- log_block("SEND-RES-BODY", "response", resp.text[:2000])
210
  return resp
211
 
212
 
@@ -214,12 +146,8 @@ def send_channel_message(client: ChannelWorksClient, text: str) -> requests.Resp
214
  # メイン
215
  # ---------------------------------------------------------------------------
216
  def main() -> None:
217
- log("BOOT", f"CW_EMAIL={CW_EMAIL[:3]}*** / room_id={ROOM_ID} / target_chat_id={TARGET_CHAT_ID}")
218
-
219
- # --- 0) モデルロード (最初にやる。ログインより先でも可) ---
220
- load_model()
221
 
222
- # --- 1) ログイン ---
223
  log("AUTH", "ChannelWorksClient 初期化")
224
  client = ChannelWorksClient(
225
  room_id=ROOM_ID,
@@ -227,14 +155,8 @@ def main() -> None:
227
  password=CW_PASSWORD,
228
  log_level=LOG_LEVEL,
229
  )
230
- log("AUTH", "sign_in + boot 実行")
231
  client.login()
232
  log("AUTH", "login() 完了")
233
- tokens = client.get_tokens() or {}
234
- log("AUTH", f"tokens: {[k for k, v in tokens.items() if v]}")
235
-
236
- log("AUTH", f"room_id(client)={getattr(client, 'room_id', None)}")
237
- log("AUTH", f"_account_id={getattr(client, '_account_id', None)}")
238
 
239
  client.start_auto_touch()
240
  log("AUTH", "auto_touch 開始")
@@ -242,86 +164,12 @@ def main() -> None:
242
  my_account_id = str(getattr(client, "_account_id", "") or "")
243
  log("AUTH", f"my_account_id={my_account_id}")
244
 
245
- # --- 2) 会話履歴 & 生成ジョブ用の状態 ---
246
- history: deque[dict] = deque(maxlen=HISTORY_MAX_TURNS * 2)
247
- history_lock = threading.Lock()
248
-
249
- # 生成中に届いたメッセージをためるバッファ
250
- pending_texts: list[str] = []
251
- pending_lock = threading.Lock()
252
- new_message_event = threading.Event()
253
-
254
- # 重複受信防止
255
  seen_ids: set[str] = set()
256
  seen_lock = threading.Lock()
257
 
258
- # --- 3) 1 回分の処理 ---
259
- def handle_message(user_text: str) -> None:
260
- log("HANDLE", f"start text={user_text!r}")
261
- with history_lock:
262
- hist_snapshot = list(history)
263
-
264
- try:
265
- answer = ask_model(hist_snapshot, user_text)
266
- except Exception as exc:
267
- log("HANDLE-ERR", f"ask_model 失敗: {exc!r}")
268
- traceback.print_exc()
269
- return
270
- log("HANDLE", f"answer={answer[:100]!r}...")
271
-
272
- try:
273
- resp = send_channel_message(client, answer)
274
- if 200 <= resp.status_code < 300:
275
- log("SEND-OK", f"status={resp.status_code}")
276
- else:
277
- log("SEND-NG", f"status={resp.status_code} body={resp.text[:300]}")
278
- except Exception as exc:
279
- log("SEND-ERR", f"{exc!r}")
280
- traceback.print_exc()
281
- return
282
-
283
- # 送信に成功した分だけ履歴に積む
284
- with history_lock:
285
- history.append({"role": "user", "content": user_text})
286
- history.append({"role": "assistant", "content": answer})
287
-
288
- # --- 4) ワーカースレッド ---
289
- # on_push は「ためて起こす」だけ。実処理はこのワーカーが直列で行う。
290
- # → 生成中に来たメッセージは pending_texts に残り、次サイクルで
291
- # 連結されて 1 つの入力として扱われる。
292
- def worker() -> None:
293
- while True:
294
- new_message_event.wait()
295
- # 生成直前にまとめて取り込むための小休止
296
- # (この間に来たメッセージもバッチに含める)
297
- time.sleep(BATCH_DEBOUNCE_SEC)
298
- new_message_event.clear()
299
-
300
- with pending_lock:
301
- if not pending_texts:
302
- continue
303
- # 生成中に届いたメッセージをまとめて 1 つの質問にする
304
- combined = "\n".join(pending_texts)
305
- pending_texts.clear()
306
-
307
- try:
308
- handle_message(combined)
309
- except Exception:
310
- log("WORKER-ERR", "handle_message で例外")
311
- traceback.print_exc()
312
-
313
- threading.Thread(target=worker, daemon=True, name="llm-worker").start()
314
-
315
- # --- 5) 受信ハンドラ ---
316
  def on_push(wrapper: dict) -> None:
317
- log_block(
318
- "PUSH-RAW",
319
- f"event={wrapper.get('event')} type={wrapper.get('type')}",
320
- json.dumps(wrapper, ensure_ascii=False, indent=2)[:4000],
321
- )
322
-
323
  if wrapper.get("event") != "create":
324
- log("PUSH-SKIP", f"event != create ({wrapper.get('event')})")
325
  return
326
 
327
  entity = wrapper.get("entity") or {}
@@ -330,17 +178,8 @@ def main() -> None:
330
  msg_id = entity.get("id") or ""
331
  plain = entity.get("plainText") or ""
332
 
333
- log(
334
- "PUSH-CHK",
335
- f"chatId={chat_id!r} (want {TARGET_CHAT_ID!r}) "
336
- f"personId={person_id!r} (self {my_account_id!r}) "
337
- f"msgId={msg_id!r} plainText={plain!r}",
338
- )
339
-
340
  if chat_id != TARGET_CHAT_ID:
341
- log("PUSH-SKIP", f"chatId mismatch: got={chat_id!r} want={TARGET_CHAT_ID!r}")
342
  return
343
-
344
  if person_id and my_account_id and person_id == my_account_id:
345
  log("PUSH-SKIP", "自分のメッセージのため無視")
346
  return
@@ -348,22 +187,38 @@ def main() -> None:
348
  if msg_id:
349
  with seen_lock:
350
  if msg_id in seen_ids:
351
- log("PUSH-SKIP", f"既出 msgId={msg_id!r}")
352
  return
353
  seen_ids.add(msg_id)
354
  if len(seen_ids) > 2000:
355
  seen_ids.clear()
356
 
357
  if not plain.strip():
358
- log("PUSH-SKIP", "plainText が空")
359
  return
360
 
361
- log("PUSH-HIT", f"キューへ追加: {plain!r}")
362
- with pending_lock:
363
- pending_texts.append(plain)
364
- new_message_event.set()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
 
366
- # --- 6) Messenger 起動 ---
367
  log("WS", "ChannelMessenger 初期化")
368
  messenger = ChannelMessenger(
369
  client=client,
@@ -373,7 +228,7 @@ def main() -> None:
373
  on_push=on_push,
374
  )
375
 
376
- log("WS", "start() 呼び出し(WS 接続開始)")
377
  try:
378
  messenger.start()
379
  except Exception as exc:
@@ -394,12 +249,10 @@ def main() -> None:
394
  threading.Thread(target=watchdog, daemon=True).start()
395
 
396
  try:
397
- log("WS", "wait() で受信待機")
398
  messenger.wait()
399
  except KeyboardInterrupt:
400
  log("WS", "KeyboardInterrupt")
401
  finally:
402
- log("WS", "停止処理")
403
  messenger.stop()
404
  client.stop_auto_touch()
405
  log("WS", "終了")
 
1
+ # main_janken.py
2
  import json
3
  import os
4
  import random
 
6
  import threading
7
  import time
8
  import traceback
 
9
  from datetime import datetime
10
 
11
  import requests
 
 
 
12
  from channel_auth import ChannelWorksClient
13
+ from channel_msg import ChannelMessenger
14
 
15
  # ---------------------------------------------------------------------------
16
  # 設定
 
18
  CW_EMAIL = os.environ["CW_EMAIL"]
19
  CW_PASSWORD = os.environ["CW_PASSWORD"]
20
 
21
+ ROOM_ID = "241135"
22
+ TARGET_CHAT_ID = "574798"
23
  GROUP_PATH = f"/groups/{TARGET_CHAT_ID}"
24
 
25
+ # じゃんけんの手
26
+ HANDS = ["✊", "✌", "✋"]
27
+ # 手の強さ: ✊=0, ✌=1, ✋=2 (グー<チョキ<パー<グー の三すくみ)
28
+ HAND_INDEX = {h: i for i, h in enumerate(HANDS)}
 
 
 
 
29
 
30
+ # じゃんけんを促すキーワード
31
+ TRIGGER_WORDS = ["じゃんけん", "じゃんけんぽん", "じゃんけんぽい", "じゃんけんほい"]
32
 
33
  CHANNEL_SEND_URL = (
34
  f"https://api.channel.works/desk/channels/{ROOM_ID}"
35
  f"/groups/{TARGET_CHAT_ID}/messages"
36
  )
37
 
38
+ LOG_LEVEL = ChannelWorksClient.LOG_DEBUG
 
39
 
40
 
41
  # ---------------------------------------------------------------------------
 
46
  print(f"[{ts}] [{tag}] {msg}", flush=True)
47
 
48
 
 
 
 
 
 
 
 
49
  def _make_request_id() -> str:
50
  suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=4))
51
  return f"desk-web-{int(time.time() * 1000)}{suffix}"
52
 
53
 
54
  # ---------------------------------------------------------------------------
55
+ # じゃんけん判定
56
  # ---------------------------------------------------------------------------
57
+ def detect_user_hand(text: str) -> str | None:
58
+ """メッセージ本文からユーザーが出した手(絵文字)を探す。"""
59
+ for h in HANDS:
60
+ if h in text:
61
+ return h
62
+ return None
 
 
 
 
 
63
 
 
 
 
 
64
 
65
+ def has_trigger_word(text: str) -> bool:
66
+ """じゃんけんを促すキーワードが含まれているか。"""
67
+ return any(w in text for w in TRIGGER_WORDS)
 
 
 
 
 
 
 
 
 
 
68
 
69
 
70
+ def judge(my_hand: str, user_hand: str) -> str:
71
  """
72
+ 自分の手と相手の手から勝敗メッセージを返す。
73
+ ✊(0) < ✌(1) < ✋(2) < ✊(0) の三すくみ。
74
+ (my - user) % 3 == 1 なら自分(ボット)の勝ち
75
+ (my - user) % 3 == 2 なら相手の勝ち
76
+ 0 ならあいこ
77
  """
78
+ diff = (HAND_INDEX[my_hand] - HAND_INDEX[user_hand]) % 3
79
+ if diff == 0:
80
+ return "あいこ"
81
+ if diff == 1:
82
+ return "私の勝ち!"
83
+ return "あなたの勝ち!"
84
+
85
+
86
+ def make_reply(user_hand: str) -> str:
87
+ """ランダムな手を出して勝敗メッセージを生成する。"""
88
+ my_hand = random.choice(HANDS)
89
+ result = judge(my_hand, user_hand)
90
+ return f"{my_hand} {result}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
 
93
  # ---------------------------------------------------------------------------
 
98
  x_account = tokens.get("x-account") or ""
99
  x_account_refresh = tokens.get("x-account-refresh") or ""
100
 
 
 
101
  cookie_parts = []
102
  if x_account:
103
  cookie_parts.append(f"x-account={x_account}")
 
131
  "customPayload": None,
132
  }
133
 
134
+ log("SEND-REQ", f"POST {CHANNEL_SEND_URL} text={text!r}")
 
 
 
135
  resp = requests.post(
136
  CHANNEL_SEND_URL,
137
  headers=headers,
138
  data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
139
  timeout=30,
140
  )
141
+ log("SEND-RES", f"status={resp.status_code}")
 
 
142
  return resp
143
 
144
 
 
146
  # メイン
147
  # ---------------------------------------------------------------------------
148
  def main() -> None:
149
+ log("BOOT", f"room_id={ROOM_ID} / target_chat_id={TARGET_CHAT_ID}")
 
 
 
150
 
 
151
  log("AUTH", "ChannelWorksClient 初期化")
152
  client = ChannelWorksClient(
153
  room_id=ROOM_ID,
 
155
  password=CW_PASSWORD,
156
  log_level=LOG_LEVEL,
157
  )
 
158
  client.login()
159
  log("AUTH", "login() 完了")
 
 
 
 
 
160
 
161
  client.start_auto_touch()
162
  log("AUTH", "auto_touch 開始")
 
164
  my_account_id = str(getattr(client, "_account_id", "") or "")
165
  log("AUTH", f"my_account_id={my_account_id}")
166
 
 
 
 
 
 
 
 
 
 
 
167
  seen_ids: set[str] = set()
168
  seen_lock = threading.Lock()
169
 
170
+ # 重複・連投防止用
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  def on_push(wrapper: dict) -> None:
 
 
 
 
 
 
172
  if wrapper.get("event") != "create":
 
173
  return
174
 
175
  entity = wrapper.get("entity") or {}
 
178
  msg_id = entity.get("id") or ""
179
  plain = entity.get("plainText") or ""
180
 
 
 
 
 
 
 
 
181
  if chat_id != TARGET_CHAT_ID:
 
182
  return
 
183
  if person_id and my_account_id and person_id == my_account_id:
184
  log("PUSH-SKIP", "自分のメッセージのため無視")
185
  return
 
187
  if msg_id:
188
  with seen_lock:
189
  if msg_id in seen_ids:
 
190
  return
191
  seen_ids.add(msg_id)
192
  if len(seen_ids) > 2000:
193
  seen_ids.clear()
194
 
195
  if not plain.strip():
 
196
  return
197
 
198
+ log("PUSH", f"受信: {plain!r}")
199
+
200
+ # 「じゃんけん」系ワード + 手の絵文字が含まれているか判定
201
+ if not has_trigger_word(plain):
202
+ log("PUSH-SKIP", "じゃんけんワードなし")
203
+ return
204
+ user_hand = detect_user_hand(plain)
205
+ if user_hand is None:
206
+ log("PUSH-SKIP", "手の絵文字なし")
207
+ return
208
+
209
+ reply = make_reply(user_hand)
210
+ log("REPLY", f"user={user_hand} -> {reply!r}")
211
+
212
+ try:
213
+ resp = send_channel_message(client, reply)
214
+ if 200 <= resp.status_code < 300:
215
+ log("SEND-OK", f"status={resp.status_code}")
216
+ else:
217
+ log("SEND-NG", f"status={resp.status_code} body={resp.text[:300]}")
218
+ except Exception as exc:
219
+ log("SEND-ERR", f"{exc!r}")
220
+ traceback.print_exc()
221
 
 
222
  log("WS", "ChannelMessenger 初期化")
223
  messenger = ChannelMessenger(
224
  client=client,
 
228
  on_push=on_push,
229
  )
230
 
231
+ log("WS", "start() 呼び出し")
232
  try:
233
  messenger.start()
234
  except Exception as exc:
 
249
  threading.Thread(target=watchdog, daemon=True).start()
250
 
251
  try:
 
252
  messenger.wait()
253
  except KeyboardInterrupt:
254
  log("WS", "KeyboardInterrupt")
255
  finally:
 
256
  messenger.stop()
257
  client.stop_auto_touch()
258
  log("WS", "終了")