From 35b9813d44e99351756178df2b45db675e39f9ef Mon Sep 17 00:00:00 2001 From: Variet Agent Date: Wed, 18 Mar 2026 18:26:33 +0900 Subject: [PATCH] =?UTF-8?q?debug(bot):=20classify=5Fand=5Froute=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EB=A1=9C=EA=B7=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?+=20=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _test_pipeline.py | 148 +++++++++++++++++++++++++++++++++++++++++++++ api/discord_bot.py | 12 +++- 2 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 _test_pipeline.py diff --git a/_test_pipeline.py b/_test_pipeline.py new file mode 100644 index 0000000..dc264b3 --- /dev/null +++ b/_test_pipeline.py @@ -0,0 +1,148 @@ +"""전수 검증: unified prompt → JSON parse → NC handler.""" + +import asyncio +import io +import json +import sys +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") +sys.path.insert(0, ".") + + +# ── Step 1: _parse_unified_response 검증 ── + +def _parse_unified_response(raw: str) -> dict: + import re + m = re.search(r"```json\s*\n(.+?)```", raw, re.DOTALL) + if m: + try: + return json.loads(m.group(1)) + except json.JSONDecodeError: + pass + + start = raw.find("{") + if start != -1: + depth = 0 + for i in range(start, len(raw)): + if raw[i] == "{": + depth += 1 + elif raw[i] == "}": + depth -= 1 + if depth == 0: + try: + return json.loads(raw[start:i + 1]) + except json.JSONDecodeError as e: + print(f" JSON decode error: {e}") + print(f" Attempted: {raw[start:i+1][:200]}") + break + + return {"mode": "chat", "response": raw} + + +# 테스트 케이스들 +test_cases = [ + # (1) 일반 JSON + '{\n "mode": "nextcloud",\n "tool": "files",\n "op": "list",\n "params": {\n "path": "/"\n },\n "summary": "test"\n}', + # (2) 끝에 ``` 붙은 경우 + '{\n "mode": "nextcloud",\n "tool": "files",\n "op": "list",\n "params": {\n "path": "/"\n },\n "summary": "test"\n}\n```', + # (3) ```json 블록 + '```json\n{\n "mode": "chat",\n "response": "안녕하세요"\n}\n```', + # (4) 텍스트 + JSON + 'Here is the result:\n{\n "mode": "nextcloud",\n "tool": "mail",\n "op": "unread",\n "params": {"limit": 5},\n "summary": "test"\n}', + # (5) 빈 문자열 + '', + # (6) mode 없는 JSON + '{"error": "something"}', +] + +print("=== Step 1: JSON 파서 테스트 ===\n") +for i, tc in enumerate(test_cases, 1): + result = _parse_unified_response(tc) + mode = result.get("mode", "?") + status = "✅" if mode != "chat" or "response" in result else "❌" + print(f" [{i}] {status} mode={mode} keys={list(result.keys())}") + if mode == "nextcloud": + print(f" tool={result.get('tool')} op={result.get('op')} params={result.get('params')}") + + +# ── Step 2: NC handler 검증 (실제 API 호출) ── + +async def test_nc_handler(): + print("\n=== Step 2: NC handler 메서드 검증 ===\n") + + from handlers.nc_handler import NCHandler + handler = NCHandler() + + # files.list_dir + print(" [1] files.list_dir('')...") + try: + files = await handler.files.list_dir("") + print(f" ✅ {len(files)}건: {', '.join(f.name for f in files[:5])}") + except Exception as e: + print(f" ❌ {e}") + + # files.search + print(" [2] files.search('pdf')...") + try: + files = await handler.files.search("pdf") + print(f" ✅ {len(files)}건: {', '.join(f.name for f in files[:5])}") + except Exception as e: + print(f" ❌ {e}") + + # calendar.get_today (method exists?) + print(" [3] calendar 메서드 확인...") + cal = handler.calendar + has_today = hasattr(cal, "get_today") + has_week = hasattr(cal, "get_week") + has_events = hasattr(cal, "get_events") + print(f" get_today={has_today} get_week={has_week} get_events={has_events}") + + # mail.get_unread + print(" [4] mail.get_unread(3)...") + try: + msgs = await handler.mail.get_unread(3) + print(f" ✅ {len(msgs)}건") + except Exception as e: + print(f" ❌ {e}") + + +# ── Step 3: NC handler.handle() 모의 호출 검증 ── + +async def test_handle_dispatch(): + print("\n=== Step 3: handle() 디스패치 검증 ===\n") + + from handlers.nc_handler import NCHandler + handler = NCHandler() + + # handle() 내부 _handle_files 호출 경로 확인 + action = {"mode": "nextcloud", "tool": "files", "op": "list", "params": {"path": ""}} + + class FakeChannel: + """Discord channel 모의 객체.""" + sent = [] + + async def send(self, content=None, embed=None): + if embed: + self.sent.append(f"[EMBED] title={embed.title}, desc_len={len(embed.description or '')}") + elif content: + self.sent.append(f"[TEXT] {content[:100]}") + print(f" → send: {self.sent[-1]}") + + ch = FakeChannel() + try: + await handler.handle(action, ch) + if ch.sent: + print(f" ✅ {len(ch.sent)}건 전송됨") + else: + print(f" ❌ 전송 없음") + except Exception as e: + print(f" ❌ handle() 오류: {e}") + import traceback + traceback.print_exc() + + +async def main(): + await test_nc_handler() + await test_handle_dispatch() + + +asyncio.run(main()) diff --git a/api/discord_bot.py b/api/discord_bot.py index 670b882..b1e0b72 100644 --- a/api/discord_bot.py +++ b/api/discord_bot.py @@ -384,10 +384,13 @@ async def on_message(message: discord.Message): gemini = GeminiCaller() history = await _get_channel_history(message.channel, limit=10) classify_input = f"{history}## User Message\n{user_text}" + logger.info(f"[분류] 입력: {user_text[:80]}") raw = await gemini.call("unified", classify_input, timeout=60) + logger.info(f"[분류] Gemini 출력 ({len(raw)}자): {raw[:200]}") # JSON 파싱 parsed = _parse_unified_response(raw) + logger.info(f"[분류] 파싱 결과: {parsed}") # 진행 메시지 삭제 if progress_msg: @@ -397,17 +400,20 @@ async def on_message(message: discord.Message): pass mode = parsed.get("mode", "chat") - logger.info(f"분류 결과: mode={mode} — \"{user_text[:50]}\"") + logger.info(f"[라우팅] mode={mode} — \"{user_text[:50]}\"") # ── 라우팅 ── if mode == "nextcloud": # NC 핸들러로 직접 라우팅 + logger.info(f"[NC] tool={parsed.get('tool')} op={parsed.get('op')} params={parsed.get('params')}") await _nc_handler.handle(parsed, message.channel) + logger.info("[NC] handle 완료") elif mode == "chat": # 즉시 응답 response = parsed.get("response", "") + logger.info(f"[chat] 응답 길이: {len(response)}") if response: if len(response) <= 2000: await message.reply(response) @@ -435,6 +441,7 @@ async def on_message(message: discord.Message): elif mode == "task": # 에이전트 모드 (파일 작업 필요) + logger.info("[task] 에이전트 호출 시작") async with message.channel.typing(): response = await _agent_call(user_text, history, ws.path) if response: @@ -459,9 +466,10 @@ async def on_message(message: discord.Message): ) ) except GeminiCallError as e: + logger.error(f"[분류] GeminiCallError: {e}") await message.reply(f"⚠️ AI 호출 오류: {str(e)[:200]}") except Exception as e: - logger.error(f"분류/라우팅 오류: {e}", exc_info=True) + logger.error(f"[분류/라우팅] 예외: {e}", exc_info=True) await message.reply(f"❌ 오류: {str(e)[:200]}") finally: _running_tasks.pop(channel_id, None)