"""태스크 핸들러 — 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, ) )