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