refactor: 전체 구조 점검 - 데드코드 제거, 에이전트 모드 통일, 취소 기능
1. 데드 코드 제거: execute() 146줄, _read_project_files() 31줄 2. 전 역할 agent 모드: summarize도 call_agent로 변경 3. 작업 취소: 취소/stop/cancel 입력으로 실행 중 작업 중단 4. 중복 방지: 채널당 1작업만 허용
This commit is contained in:
@@ -55,6 +55,9 @@ bot = commands.Bot(
|
||||
# 워크스페이스 매니저 (전역)
|
||||
ws_manager = WorkspaceManager()
|
||||
|
||||
# 실행 중인 작업 추적 (채널ID → asyncio.Task)
|
||||
_running_tasks: dict[int, asyncio.Task] = {}
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# 대화 기억
|
||||
@@ -222,6 +225,24 @@ async def on_message(message: discord.Message):
|
||||
if not user_text:
|
||||
return
|
||||
|
||||
# 취소 명령어 확인
|
||||
cancel_keywords = {"취소", "stop", "cancel", "중지", "멈춰"}
|
||||
if user_text.lower() in cancel_keywords:
|
||||
channel_id = message.channel.id
|
||||
if channel_id in _running_tasks and not _running_tasks[channel_id].done():
|
||||
_running_tasks[channel_id].cancel()
|
||||
del _running_tasks[channel_id]
|
||||
await message.reply(
|
||||
embed=discord.Embed(
|
||||
title="🛑 작업 취소됨",
|
||||
description="실행 중인 작업을 취소했습니다.",
|
||||
color=0xE74C3C,
|
||||
)
|
||||
)
|
||||
else:
|
||||
await message.reply("실행 중인 작업이 없습니다.")
|
||||
return
|
||||
|
||||
# 통합 프롬프트 호출
|
||||
async with message.channel.typing():
|
||||
try:
|
||||
@@ -246,7 +267,29 @@ async def on_message(message: discord.Message):
|
||||
await message.channel.send(
|
||||
f"ℹ️ {note} 미설정 상태입니다. 로컬 작업만 진행됩니다."
|
||||
)
|
||||
await _handle_task(message, user_text, ws)
|
||||
|
||||
# 작업을 추적 가능한 Task로 실행
|
||||
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_task():
|
||||
try:
|
||||
await _handle_task(message, user_text, ws)
|
||||
except asyncio.CancelledError:
|
||||
await message.channel.send(
|
||||
embed=discord.Embed(
|
||||
title="🛑 작업 취소됨",
|
||||
description="작업이 사용자에 의해 취소되었습니다.",
|
||||
color=0xE74C3C,
|
||||
)
|
||||
)
|
||||
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(
|
||||
|
||||
@@ -113,37 +113,6 @@ class TaskPipeline:
|
||||
|
||||
return processed
|
||||
|
||||
# ──────────────────────────────────────────
|
||||
# 프로젝트 파일 읽기 (공용)
|
||||
# ──────────────────────────────────────────
|
||||
|
||||
def _read_project_files(self) -> list[str]:
|
||||
"""프로젝트의 모든 텍스트 파일 읽기."""
|
||||
import os
|
||||
|
||||
project_files = []
|
||||
project_root = Path(self.project_path)
|
||||
skip_dirs = {".git", "__pycache__", "node_modules", ".venv", "venv", "dist", "build"}
|
||||
binary_exts = {".png", ".jpg", ".jpeg", ".gif", ".ico", ".woff", ".woff2", ".ttf",
|
||||
".eot", ".mp3", ".mp4", ".zip", ".tar", ".gz", ".exe", ".dll",
|
||||
".so", ".pyc", ".pyo", ".db", ".sqlite"}
|
||||
|
||||
for root, dirs, files in os.walk(self.project_path):
|
||||
dirs[:] = [d for d in dirs if d not in skip_dirs]
|
||||
for fname in files:
|
||||
fpath = Path(root) / fname
|
||||
if fpath.suffix.lower() in binary_exts:
|
||||
continue
|
||||
try:
|
||||
rel = fpath.relative_to(project_root)
|
||||
content = fpath.read_text(encoding="utf-8", errors="replace")
|
||||
project_files.append(
|
||||
f"### {rel}\n```\n{content}\n```"
|
||||
)
|
||||
except (OSError, UnicodeDecodeError):
|
||||
continue
|
||||
|
||||
return project_files
|
||||
|
||||
# ──────────────────────────────────────────
|
||||
# Planner 자가 검증 (오케스트레이션)
|
||||
@@ -237,7 +206,9 @@ class TaskPipeline:
|
||||
f"위 정보를 바탕으로 총평을 작성하세요."
|
||||
)
|
||||
|
||||
response = await self.gemini.call("summarizer", prompt, timeout=60)
|
||||
response = await self.gemini.call_agent(
|
||||
"summarizer", prompt, cwd=self.project_path, timeout=120,
|
||||
)
|
||||
self._log("summarize", user_request, response)
|
||||
|
||||
summary = self._extract_json(response)
|
||||
@@ -249,121 +220,6 @@ class TaskPipeline:
|
||||
"next_steps": [],
|
||||
}
|
||||
|
||||
# ──────────────────────────────────────────
|
||||
# 전체 파이프라인 (재시도 루프 포함)
|
||||
# ──────────────────────────────────────────
|
||||
|
||||
async def execute(self, user_request: str) -> dict:
|
||||
"""Plan -> Code(에이전트, 병렬) -> Review -> 재시도 -> 총평 -> 기록.
|
||||
|
||||
Coder가 에이전트 모드로 직접 파일을 생성/수정합니다.
|
||||
리뷰 실패 시 최대 MAX_REVIEW_RETRIES회 재시도합니다.
|
||||
성공/실패 모두 docs에 기록됩니다.
|
||||
"""
|
||||
result = {
|
||||
"request": user_request,
|
||||
"plan": None,
|
||||
"code_outputs": [],
|
||||
"review": None,
|
||||
"summary": None,
|
||||
"errors": [],
|
||||
"retry_count": 0,
|
||||
}
|
||||
|
||||
try:
|
||||
# 1. Plan
|
||||
plan = await self.plan(user_request)
|
||||
result["plan"] = plan
|
||||
|
||||
tasks = plan.get("tasks", [])
|
||||
if not tasks:
|
||||
result["summary"] = {
|
||||
"title": "태스크 없음",
|
||||
"summary": "Planner가 태스크를 생성하지 못했습니다.",
|
||||
"changes": [],
|
||||
"warnings": ["요청을 더 구체적으로 해주세요."],
|
||||
"next_steps": [],
|
||||
}
|
||||
self.docs.record_session(user_request, result["summary"], plan)
|
||||
return result
|
||||
|
||||
# 2. Code + Review (재시도 루프)
|
||||
review = None
|
||||
code_outputs = []
|
||||
for attempt in range(1 + MAX_REVIEW_RETRIES):
|
||||
# Code 병렬 실행 (에이전트 모드 — 파일 직접 쓰기)
|
||||
code_outputs = await self.code_parallel(tasks)
|
||||
result["code_outputs"] = [o[:500] for o in code_outputs]
|
||||
|
||||
error_count = sum(1 for o in code_outputs if o.startswith("[ERROR]"))
|
||||
if error_count > 0:
|
||||
result["errors"].append(
|
||||
f"코딩 실패: {error_count}/{len(tasks)}개 (시도 {attempt+1})"
|
||||
)
|
||||
|
||||
# Review
|
||||
review = await self.batch_review(tasks, code_outputs)
|
||||
result["review"] = review
|
||||
|
||||
# 리뷰 통과 여부 확인
|
||||
passed = review.get("passed", True)
|
||||
if isinstance(passed, str):
|
||||
passed = passed.lower() in ("true", "yes", "pass")
|
||||
|
||||
if passed:
|
||||
logger.info(f"리뷰 통과 (시도 {attempt+1})")
|
||||
break
|
||||
else:
|
||||
result["retry_count"] = attempt + 1
|
||||
if attempt < MAX_REVIEW_RETRIES:
|
||||
logger.warning(
|
||||
f"리뷰 실패 -- 재시도 {attempt+2}/{1+MAX_REVIEW_RETRIES}"
|
||||
)
|
||||
# 리뷰 피드백을 태스크에 추가
|
||||
feedback = review.get("summary", str(review))[:500]
|
||||
for task in tasks:
|
||||
task["review_feedback"] = (
|
||||
f"이전 시도에서 다음 리뷰 피드백을 받았습니다. "
|
||||
f"반드시 수정하세요:\n{feedback}"
|
||||
)
|
||||
else:
|
||||
result["errors"].append(
|
||||
f"리뷰 {1+MAX_REVIEW_RETRIES}회 시도 모두 실패"
|
||||
)
|
||||
|
||||
# 3. 총평
|
||||
summary = await self.summarize(
|
||||
user_request, plan, code_outputs, review
|
||||
)
|
||||
if result["errors"]:
|
||||
existing_warnings = summary.get("warnings", [])
|
||||
summary["warnings"] = existing_warnings + result["errors"]
|
||||
if result["retry_count"] > 0:
|
||||
summary["retries"] = result["retry_count"]
|
||||
result["summary"] = summary
|
||||
|
||||
except Exception as e:
|
||||
result["errors"].append(f"파이프라인 오류: {str(e)}")
|
||||
result["summary"] = {
|
||||
"title": "작업 실패",
|
||||
"summary": f"파이프라인 실행 중 오류 발생: {str(e)}",
|
||||
"changes": [],
|
||||
"warnings": result["errors"],
|
||||
"next_steps": ["오류 내용 확인 후 다시 시도"],
|
||||
}
|
||||
self._log("pipeline_error", user_request, str(e))
|
||||
|
||||
finally:
|
||||
self.docs.record_session(
|
||||
user_request,
|
||||
result.get("summary", {"summary": "기록 없음"}),
|
||||
result.get("plan"),
|
||||
)
|
||||
self.docs.append_changelog(
|
||||
result.get("summary", {}).get("title", user_request[:50])
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
# ──────────────────────────────────────────
|
||||
# 유틸리티
|
||||
|
||||
Reference in New Issue
Block a user