debug(bot): classify_and_route 상세 로그 추가 + 파이프라인 검증 스크립트

This commit is contained in:
2026-03-18 18:26:33 +09:00
parent 62bc257be6
commit 35b9813d44
2 changed files with 158 additions and 2 deletions

148
_test_pipeline.py Normal file
View File

@@ -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())

View File

@@ -384,10 +384,13 @@ async def on_message(message: discord.Message):
gemini = GeminiCaller() gemini = GeminiCaller()
history = await _get_channel_history(message.channel, limit=10) history = await _get_channel_history(message.channel, limit=10)
classify_input = f"{history}## User Message\n{user_text}" classify_input = f"{history}## User Message\n{user_text}"
logger.info(f"[분류] 입력: {user_text[:80]}")
raw = await gemini.call("unified", classify_input, timeout=60) raw = await gemini.call("unified", classify_input, timeout=60)
logger.info(f"[분류] Gemini 출력 ({len(raw)}자): {raw[:200]}")
# JSON 파싱 # JSON 파싱
parsed = _parse_unified_response(raw) parsed = _parse_unified_response(raw)
logger.info(f"[분류] 파싱 결과: {parsed}")
# 진행 메시지 삭제 # 진행 메시지 삭제
if progress_msg: if progress_msg:
@@ -397,17 +400,20 @@ async def on_message(message: discord.Message):
pass pass
mode = parsed.get("mode", "chat") mode = parsed.get("mode", "chat")
logger.info(f"분류 결과: mode={mode}\"{user_text[:50]}\"") logger.info(f"[라우팅] mode={mode}\"{user_text[:50]}\"")
# ── 라우팅 ── # ── 라우팅 ──
if mode == "nextcloud": if mode == "nextcloud":
# NC 핸들러로 직접 라우팅 # NC 핸들러로 직접 라우팅
logger.info(f"[NC] tool={parsed.get('tool')} op={parsed.get('op')} params={parsed.get('params')}")
await _nc_handler.handle(parsed, message.channel) await _nc_handler.handle(parsed, message.channel)
logger.info("[NC] handle 완료")
elif mode == "chat": elif mode == "chat":
# 즉시 응답 # 즉시 응답
response = parsed.get("response", "") response = parsed.get("response", "")
logger.info(f"[chat] 응답 길이: {len(response)}")
if response: if response:
if len(response) <= 2000: if len(response) <= 2000:
await message.reply(response) await message.reply(response)
@@ -435,6 +441,7 @@ async def on_message(message: discord.Message):
elif mode == "task": elif mode == "task":
# 에이전트 모드 (파일 작업 필요) # 에이전트 모드 (파일 작업 필요)
logger.info("[task] 에이전트 호출 시작")
async with message.channel.typing(): async with message.channel.typing():
response = await _agent_call(user_text, history, ws.path) response = await _agent_call(user_text, history, ws.path)
if response: if response:
@@ -459,9 +466,10 @@ async def on_message(message: discord.Message):
) )
) )
except GeminiCallError as e: except GeminiCallError as e:
logger.error(f"[분류] GeminiCallError: {e}")
await message.reply(f"⚠️ AI 호출 오류: {str(e)[:200]}") await message.reply(f"⚠️ AI 호출 오류: {str(e)[:200]}")
except Exception as e: 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]}") await message.reply(f"❌ 오류: {str(e)[:200]}")
finally: finally:
_running_tasks.pop(channel_id, None) _running_tasks.pop(channel_id, None)