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()
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user