refactor(agent): MCP 기반 에이전트 아키텍처 재설계 — unified.md 분류기 제거, Gemini CLI + MCP 자율 도구 호출로 전환
This commit is contained in:
@@ -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]}")
|
||||
|
||||
Reference in New Issue
Block a user