refactor(agent): MCP 기반 에이전트 아키텍처 재설계 — unified.md 분류기 제거, Gemini CLI + MCP 자율 도구 호출로 전환

This commit is contained in:
2026-03-12 16:52:20 +09:00
parent acc8533ef2
commit 246d2a26c4
10 changed files with 592 additions and 128 deletions

View File

@@ -124,52 +124,27 @@ async def safe_send_embed(channel, embed: discord.Embed):
# ──────────────────────────────────────────────
# 통합 프롬프트 (1회 호출: 분류 + 응답/계획)
# 에이전트 호출 (MCP 도구 자동 사용)
# ──────────────────────────────────────────────
async def _unified_call(text: str, history: str, project_path: str) -> dict:
"""통합 프롬프트로 1회 호출 — chat/task/clarify 자동 분기."""
gemini = GeminiCaller(project_path)
async def _agent_call(text: str, history: str, project_path: str) -> str:
"""Gemini CLI 에이전트 모드로 호출 — MCP 도구를 자율적으로 사용.
# docs 인덱스 주입
from core.docs_manager import DocsManager
docs = DocsManager(project_path)
docs_index = docs.get_docs_index()
분류(chat/task/anime) 없이 에이전트가 직접 판단하여
도구를 사용하거나 바로 답변합니다.
"""
gemini = GeminiCaller(project_path)
context = (
f"{history}"
f"## Workspace\nPath: {project_path}\n\n"
f"## Project Docs\n{docs_index}\n\n"
f"## User Message\n{text}"
)
raw = await gemini.call("unified", context, timeout=120)
# JSON 추출
try:
# ```json ... ``` 패턴
match = re.search(r'```json\s*\n(.*?)\n\s*```', raw, re.DOTALL)
if match:
return json.loads(match.group(1))
# 중첩 { } 찾기
brace_depth = 0
start = -1
for i, ch in enumerate(raw):
if ch == '{':
if brace_depth == 0:
start = i
brace_depth += 1
elif ch == '}':
brace_depth -= 1
if brace_depth == 0 and start >= 0:
return json.loads(raw[start:i + 1])
except (json.JSONDecodeError, AttributeError):
pass
# 파싱 실패 → chat으로 처리
logger.warning(f"통합 프롬프트 JSON 파싱 실패: {raw[:100]}")
return {"mode": "chat", "response": raw}
response = await gemini.call_agent(
"agent", context, cwd=project_path, timeout=300,
)
return response
# ──────────────────────────────────────────────
@@ -297,75 +272,68 @@ async def on_message(message: discord.Message):
await message.reply("실행 중인 작업이 없습니다.")
return
# 통합 프롬프트 호출
async with message.channel.typing():
# 에이전트 호출 (MCP 도구 자동 사용)
channel_id = message.channel.id
if channel_id in _running_tasks and not _running_tasks[channel_id].done():
await message.reply("⚠️ 이미 작업이 실행 중입니다. `취소` 후 다시 요청하세요.")
return
async def _tracked_agent():
progress_msg = None
try:
history = await _get_channel_history(message.channel, limit=10)
result = await _unified_call(user_text, history, ws.path)
except GeminiCallError as e:
await message.reply(f"⚠️ AI 호출 오류: {str(e)[:200]}")
return
except Exception as e:
logger.error(f"통합 호출 오류: {e}", exc_info=True)
await message.reply(f"❌ 오류: {str(e)[:200]}")
return
mode = result.get("mode", "chat")
logger.info(f"통합 분류: {mode} - \"{user_text[:50]}\"")
if mode == "anime":
# 애니메이션 도구 자동 호출
async with message.channel.typing():
await _handle_anime(message, result)
elif mode == "task":
# Git/Vikunja 미설정 안내 (차단하지 않음)
if not ws.is_ready:
missing = ws.missing_configs
note = " / ".join(missing)
await message.channel.send(
f" {note} 미설정 상태입니다. 로컬 작업만 진행됩니다."
# 진행 표시
progress_msg = await message.channel.send(
embed=discord.Embed(
title="🤖 에이전트 처리 중...",
description=f"```{user_text[:200]}```",
color=0xF39C12,
)
)
# 작업을 추적 가능한 Task로 실행
channel_id = message.channel.id
if channel_id in _running_tasks and not _running_tasks[channel_id].done():
await message.reply("⚠️ 이미 작업이 실행 중입니다. `취소` 후 다시 요청하세요.")
return
async with message.channel.typing():
history = await _get_channel_history(message.channel, limit=10)
response = await _agent_call(user_text, history, ws.path)
async def _tracked_task():
try:
await _handle_task(message, user_text, ws)
except asyncio.CancelledError:
await message.channel.send(
embed=discord.Embed(
title="🛑 작업 취소됨",
description="작업이 사용자에 의해 취소되었습니다.",
color=0xE74C3C,
)
logger.info(f"에이전트 응답: \"{user_text[:50]}\" -> {len(response)}")
# 진행 메시지 삭제
if progress_msg:
try:
await progress_msg.delete()
except Exception:
pass
if not response:
await message.reply("응답을 생성하지 못했습니다.")
return
# 응답 전송
if len(response) <= 2000:
await message.reply(response)
else:
for i in range(0, len(response), 4000):
chunk = response[i:i + 4000]
embed = discord.Embed(description=chunk, color=0x3498DB)
await message.channel.send(embed=embed)
except asyncio.CancelledError:
await message.channel.send(
embed=discord.Embed(
title="🛑 작업 취소됨",
description="작업이 사용자에 의해 취소되었습니다.",
color=0xE74C3C,
)
finally:
_running_tasks.pop(channel_id, None)
)
except GeminiCallError as e:
await message.reply(f"⚠️ AI 호출 오류: {str(e)[:200]}")
except Exception as e:
logger.error(f"에이전트 호출 오류: {e}", exc_info=True)
await message.reply(f"❌ 오류: {str(e)[:200]}")
finally:
_running_tasks.pop(channel_id, None)
task = asyncio.create_task(_tracked_task())
_running_tasks[channel_id] = task
elif mode == "clarify":
question = result.get("question", "더 구체적으로 말씀해 주시겠어요?")
embed = discord.Embed(
title="🤔 확인이 필요해요",
description=question,
color=0xF39C12,
)
await message.reply(embed=embed)
else:
# chat — 즉답
response = result.get("response", "응답을 생성하지 못했습니다.")
if len(response) <= 2000:
await message.reply(response)
else:
for i in range(0, len(response), 4000):
chunk = response[i:i + 4000]
embed = discord.Embed(description=chunk, color=0x3498DB)
await message.channel.send(embed=embed)
task = asyncio.create_task(_tracked_agent())
_running_tasks[channel_id] = task
# ──────────────────────────────────────────────
@@ -1325,13 +1293,11 @@ async def _create_task_thread(
try:
async with thread.typing():
history = ""
result = await _unified_call(request_text, history, ws.path)
response = await _agent_call(request_text, history, ws.path)
mode = result.get("mode", "chat")
logger.info(f"[스레드] 통합 분류: {mode} - \"{request_text[:50]}\"")
logger.info(f"[스레드] 에이전트 응답: \"{request_text[:50]}\" -> {len(response)}")
if mode == "chat":
response = result.get("response", "응답을 생성하지 못했습니다.")
if response:
if len(response) <= 2000:
await thread.send(response)
else:
@@ -1339,23 +1305,8 @@ async def _create_task_thread(
chunk = response[i:i + 4000]
embed = discord.Embed(description=chunk, color=0x3498DB)
await thread.send(embed=embed)
elif mode == "clarify":
question = result.get("question", "더 구체적으로 말씀해 주시겠어요?")
embed = discord.Embed(
title="🤔 확인이 필요해요",
description=question,
color=0xF39C12,
)
await thread.send(embed=embed)
else: # task
# task 모드 — 스레드에서 파이프라인 안내
await thread.send(
embed=discord.Embed(
title="⚙️ 작업 모드 감지",
description="이 스레드에서 작업 요청을 다시 입력해주세요.\n스레드 내 메시지는 자동으로 처리됩니다.",
color=0xF39C12,
)
)
else:
await thread.send("응답을 생성하지 못했습니다.")
except Exception as e:
logger.error(f"스레드 초기 호출 오류: {e}", exc_info=True)
await thread.send(f"⚠️ 초기 호출 오류: {str(e)[:200]}")