Files
variet-agent/handlers/task_handler.py

181 lines
5.8 KiB
Python

"""태스크 핸들러 — Agent 1회 실행 (discord_bot.py에서 분리).
NLU에서 mode="task"로 분류된 요청을 처리합니다.
Agent가 plan+code+verify를 한 세션에서 수행합니다.
"""
import uuid
import logging
import asyncio
import discord
from core.gemini_caller import GeminiCallError
from handlers.renderer import safe_send_embed
logger = logging.getLogger("variet.handlers.task")
async def handle_task(
message: discord.Message,
text: str,
ws,
history: str = "",
mode: str = "task",
):
"""작업 요청 — Agent 1회 통합 실행 + 결과 표시.
Args:
message: Discord 메시지
text: 사용자 입력 텍스트
ws: Workspace 객체
history: 대화 히스토리 문자열
mode: 'task'(코딩) 또는 'anime'(도구 실행)
"""
from core.task_pipeline import TaskPipeline
task_id = uuid.uuid4().hex[:8]
# ── 1. 접수 ──
embed = discord.Embed(
title="⚙️ 작업 중...",
description=f"```{text[:200]}```",
color=0xF39C12,
)
embed.set_footer(text=f"ID: {task_id} | {ws.name}")
status_msg = await message.channel.send(embed=embed)
try:
pipeline = TaskPipeline(
project_path=ws.path,
docs_subpath=ws.docs_path,
)
pipeline.setup()
# ── 2. Agent 실행 (진행 상태 실시간 업데이트) ──
import time as _time
_start_time = _time.time()
_last_update = [0.0]
_current_status = ["작업 중..."]
async def _progress(status_text: str):
now = _time.time()
if now - _last_update[0] < 2.0:
return
_last_update[0] = now
_current_status[0] = status_text
elapsed = int(now - _start_time)
try:
embed.title = f"⚙️ {status_text}"
embed.set_footer(text=f"ID: {task_id} | {ws.name} | {elapsed}초 경과")
await status_msg.edit(embed=embed)
except Exception:
pass
# heartbeat: 출력이 없어도 10초마다 경과시간 갱신
_heartbeat_running = [True]
async def _heartbeat():
while _heartbeat_running[0]:
await asyncio.sleep(10)
if not _heartbeat_running[0]:
break
elapsed = int(_time.time() - _start_time)
try:
embed.set_footer(text=f"ID: {task_id} | {ws.name} | {elapsed}초 경과")
await status_msg.edit(embed=embed)
except Exception:
pass
heartbeat_task = asyncio.create_task(_heartbeat())
# mode→role 매핑: anime='operator'(도구 실행), task='agent'(코딩)
role = "operator" if mode == "anime" else "agent"
timeout = 600 # anime 배치 작업(20+건 순차)도 충분한 시간 확보
try:
result = await pipeline.execute(
text, history=history, progress_callback=_progress,
role=role,
)
finally:
_heartbeat_running[0] = False
try:
heartbeat_task.cancel()
except Exception:
pass
# ── 3. 결과 표시 ──
title = result.get("title", "작업 완료")
summary = result.get("summary", "완료")
verified = result.get("verified", False)
color = 0x2ECC71 if verified else 0xF39C12
result_embed = discord.Embed(
title=f"{'' if verified else '📋'} {title}",
description=summary[:2000],
color=color,
)
# 변경 사항
changes = result.get("changes", [])
if changes:
if isinstance(changes[0], dict):
val = "\n".join(
f"• **{c.get('title', c.get('file', c.get('name', '?')))}** — "
f"{c.get('action', c.get('description', c.get('summary', '')))}"
for c in changes[:10]
)
else:
val = "\n".join(f"{s}" for s in changes[:10])
result_embed.add_field(name="변경 사항", value=val[:1000], inline=False)
# 주의사항
warnings = result.get("warnings", [])
if warnings:
result_embed.add_field(
name="⚠️ 주의",
value="\n".join(f"{w}" for w in warnings[:5])[:1000],
inline=False,
)
# 다음 단계
next_steps = result.get("next_steps", [])
if next_steps:
result_embed.add_field(
name="🔜 다음 단계",
value="\n".join(f"{s}" for s in next_steps[:5])[:1000],
inline=False,
)
result_embed.set_footer(text=f"ID: {task_id} | {ws.name}")
await status_msg.edit(embed=result_embed)
# 기록 (실패해도 봇 종료 방지)
try:
pipeline.docs.record_session(text, result, {})
pipeline.docs.append_changelog(title)
except Exception as doc_err:
logger.warning(f"세션 기록 실패: {doc_err}")
except GeminiCallError as e:
await status_msg.edit(
embed=discord.Embed(
title="❌ AI 호출 오류",
description=(
f"```{str(e)[:300]}```\n\n"
f"💡 요청을 더 짧게/구체적으로 다시 시도해보세요."
),
color=0xE74C3C,
)
)
except Exception as e:
logger.error(f"작업 오류: {e}", exc_info=True)
await status_msg.edit(
embed=discord.Embed(
title="❌ 예기치 않은 오류",
description=f"```{str(e)[:300]}```",
color=0xE74C3C,
)
)