fix(anime): 파이프라인 5건 수정 — 에피소드 정규식(v2/S01E), 릴리스 그룹 필터, 자막 보호, 배치 다운로드, 타임아웃
This commit is contained in:
180
handlers/task_handler.py
Normal file
180
handlers/task_handler.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""태스크 핸들러 — 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,
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user