debug(bot): classify_and_route 상세 로그 추가 + 파이프라인 검증 스크립트
This commit is contained in:
148
_test_pipeline.py
Normal file
148
_test_pipeline.py
Normal 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())
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user