feat: Planner/Reviewer 에이전트 모드 + 직접 파일 접근

This commit is contained in:
2026-03-06 23:25:30 +09:00
parent 52402065f3
commit 5f669117b2

View File

@@ -117,31 +117,33 @@ class TaskPipeline:
# 프로젝트 파일 읽기 (공용)
# ──────────────────────────────────────────
def _read_recent_files(self, cutoff_seconds: int = 600) -> list[str]:
"""최근 변경된 프로젝트 파일 읽기."""
def _read_project_files(self) -> list[str]:
"""프로젝트의 모든 텍스트 파일 읽기."""
import os
import time as _time
recent_files = []
cutoff = _time.time() - cutoff_seconds
project_files = []
project_root = Path(self.project_path)
skip_dirs = {".git", "__pycache__", "node_modules", ".venv", "venv"}
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:
if fpath.stat().st_mtime > cutoff:
rel = fpath.relative_to(project_root)
content = fpath.read_text(encoding="utf-8", errors="replace")
recent_files.append(
project_files.append(
f"### {rel}\n```\n{content}\n```"
)
except (OSError, UnicodeDecodeError):
continue
return recent_files
return project_files
# ──────────────────────────────────────────
# Planner 자가 검증 (오케스트레이션)
@@ -151,21 +153,10 @@ class TaskPipeline:
self, user_request: str, plan: dict,
code_outputs: list[str],
) -> dict:
"""Planner가 자기 계획의 달성 여부를 검증.
"""Planner가 자기 계획의 달성 여부를 에이전트 모드로 검증.
실제 파일을 읽어서 계획 충족됐는지 판단합니다.
미달이면 추가 태스크를 생성합니다.
Returns:
{
"satisfied": bool,
"feedback": "미충족 사유",
"additional_tasks": [...] (satisfied=false일 때)
}
프로젝트 디렉토리에서 직접 파일을 읽어서 계획 충족 여부를 판단합니다.
"""
recent_files = self._read_recent_files()
files_section = "\n\n".join(recent_files) if recent_files else "(파일 없음)"
agent_reports = "\n".join(
f"--- Agent {i+1} ---\n{output}"
for i, output in enumerate(code_outputs)
@@ -175,12 +166,12 @@ class TaskPipeline:
f"## 원래 사용자 요청\n{user_request}\n\n"
f"## 내가 세운 계획\n{json.dumps(plan, ensure_ascii=False, indent=2)}\n\n"
f"## 에이전트 보고\n{agent_reports}\n\n"
f"## 현재 프로젝트 파일\n{files_section}\n\n"
f"## 판단 요청\n"
f" 계획이 충족되었는지 판단하세요.\n"
f"현재 디렉토리의 프로젝트 파일을 직접 읽어서 계획이 충족되었는지 확인하세요.\n"
f"필요한 파일만 선택적으로 읽으세요.\n\n"
f"충족되었으면 satisfied=true.\n"
f"미충족이면 satisfied=false + 부족한 부분을 해결할 추가 태스크를 생성하세요.\n\n"
f"JSON 형식:\n"
f"반드시 아래 JSON만 출력하세요:\n"
f"```json\n"
f'{{\n'
f' "satisfied": true|false,\n'
@@ -192,7 +183,9 @@ class TaskPipeline:
f"```"
)
response = await self.gemini.call("planner", prompt, timeout=180)
response = await self.gemini.call_agent(
"planner", prompt, cwd=self.project_path, timeout=180,
)
self._log("planner_verify", user_request, response)
result = self._extract_json(response)
@@ -203,7 +196,7 @@ class TaskPipeline:
# ──────────────────────────────────────────
async def batch_review(self, tasks: list[dict], code_outputs: list[str]) -> dict:
"""에이전트가 생성/수정한 실제 파일을 리뷰."""
"""에이전트 모드로 프로젝트 파일을 직접 읽어 리뷰."""
task_summaries = []
for i, task in enumerate(tasks):
title = task.get("title", task.get("description", f"Task {i+1}"))
@@ -213,26 +206,17 @@ class TaskPipeline:
for i, output in enumerate(code_outputs):
agent_reports.append(f"--- Agent {i+1} 보고 ---\n{output}")
recent_files = self._read_recent_files()
if not recent_files:
return {
"passed": True,
"summary": "파일 변경 없음 또는 삭제 작업 - 자동 통과",
"issues": [],
}
files_section = "\n\n".join(recent_files)
prompt = (
f"## 요청된 태스크\n{chr(10).join(task_summaries)}\n\n"
f"## 에이전트 보고\n{chr(10).join(agent_reports)}\n\n"
f"## 실제 생성/수정된 파일\n{files_section}\n\n"
f"위 파일들이 태스크 요구사항을 충족하는지 리뷰하세요."
f"현재 디렉토리의 프로젝트 파일을 직접 읽어서 리뷰하세요.\n"
f"필요한 파일만 선택적으로 확인하세요."
)
response = await self.gemini.call("reviewer", prompt, timeout=180)
self._log("batch_review", f"{len(tasks)} tasks, {len(recent_files)} files", response)
response = await self.gemini.call_agent(
"reviewer", prompt, cwd=self.project_path, timeout=180,
)
self._log("batch_review", f"{len(tasks)} tasks", response)
review = self._extract_json(response)
return review or {"passed": True, "summary": response, "raw": response}