feat: Coder를 에이전트 모드로 전환 + 리뷰 재시도 루프

핵심 변경:
- gemini_caller.py: call_agent() 추가 (cwd 지원, 5분 타임아웃)
  Gemini가 프로젝트 디렉토리에서 직접 파일 읽기/쓰기/실행
- task_pipeline.py: Coder가 call_agent() 사용, file_applier 의존 제거
  리뷰 실패 시 최대 2회 재시도 (피드백 포함)
- discord_bot.py: pipeline.execute() 호출로 단순화
- coder.md: 파일 직접 쓰기 지시 (코드블록 출력 금지)
- 검증: echo prompt | gemini --cwd=VW_Proj → test_agent.txt 생성 확인
This commit is contained in:
2026-03-06 22:13:06 +09:00
parent 83c043863c
commit bccc673713
6 changed files with 483 additions and 187 deletions

View File

@@ -285,7 +285,7 @@ async def _handle_task(message: discord.Message, text: str, ws):
description=f"```{text[:200]}```",
color=0x3498DB,
)
embed.set_footer(text=f"ID: {task_id} — 워크스페이스: {ws.name}")
embed.set_footer(text=f"ID: {task_id} | {ws.name}")
status_msg = await message.channel.send(embed=embed)
try:
@@ -297,102 +297,73 @@ async def _handle_task(message: discord.Message, text: str, ws):
)
pipeline.setup()
# Plan
# 진행 상태 표시
embed.color = 0xF39C12
embed.set_footer(text=f"🔍 작업 분해 중... (ID: {task_id})")
await status_msg.edit(embed=embed)
plan = await pipeline.plan(text)
tasks = plan.get("tasks", [])
# 전체 파이프라인 실행 (Plan -> Code(에이전트) -> Review(재시도) -> 총평)
result = await pipeline.execute(text)
plan_embed = discord.Embed(
title="📝 작업 계획",
description=f"```{plan.get('summary', str(plan))[:500]}```",
color=0x2ECC71,
)
plan = result.get("plan", {})
tasks = plan.get("tasks", [])
review = result.get("review", {})
summary = result.get("summary", {})
# 계획 표시
if tasks:
task_list = "\n".join(
f"{t.get('title', t.get('description', '?'))}"
for t in tasks[:10]
)
plan_embed = discord.Embed(
title="📝 작업 계획",
description=plan.get("summary", "")[:500],
color=0x2ECC71,
)
plan_embed.add_field(
name=f"태스크 ({len(tasks)}개)", value=task_list[:1000], inline=False,
)
await message.channel.send(embed=plan_embed)
await message.channel.send(embed=plan_embed)
if not tasks:
# 리뷰 결과
if review:
passed = review.get("passed", True)
if isinstance(passed, str):
passed = passed.lower() in ("true", "yes", "pass")
retry_count = result.get("retry_count", 0)
retry_info = f" (재시도 {retry_count}회)" if retry_count > 0 else ""
await message.channel.send(
embed=discord.Embed(
title="⚠️ 실행할 태스크 없음",
description="요청을 더 구체적으로 해주세요.",
color=0xF39C12,
title=f"{'' if passed else '⚠️'} 리뷰 결과{retry_info}",
description=review.get("summary", str(review))[:500],
color=0x2ECC71 if passed else 0xE74C3C,
)
)
return
# Code 병렬
code_embed = discord.Embed(
title=f"⚙️ 코딩 중... ({len(tasks)}개 병렬)",
description="\n".join(
f"{t.get('title', '?')[:60]}" for t in tasks
),
color=0xE67E22,
)
code_msg = await message.channel.send(embed=code_embed)
code_outputs = await pipeline.code_parallel(tasks)
code_embed.title = f"✅ 코딩 완료 ({len(tasks)}개)"
code_embed.color = 0x2ECC71
await code_msg.edit(embed=code_embed)
# 파일 적용
from core.file_applier import parse_code_output, apply_changes
all_applied = []
for output in code_outputs:
if not output.startswith("[ERROR]"):
changes = parse_code_output(output)
if changes:
applied = apply_changes(changes, ws.path)
all_applied.extend(applied)
if all_applied:
files_text = "\n".join(f"• `{f['path']}` ({f['action']})" for f in all_applied[:15])
await message.channel.send(
embed=discord.Embed(title=f"📁 파일 적용 ({len(all_applied)}개)",
description=files_text, color=0x3498DB)
)
# Batch Review
review = await pipeline.batch_review(tasks, code_outputs)
passed = review.get("passed", True)
await message.channel.send(
embed=discord.Embed(
title=f"{'' if passed else '⚠️'} 리뷰 결과",
description=review.get("summary", str(review))[:500],
color=0x2ECC71 if passed else 0xE74C3C,
)
)
# 총평
summary = await pipeline.summarize(text, plan, code_outputs, review, all_applied)
summary_embed = discord.Embed(
title=f"📊 {summary.get('title', '작업 완료')}",
description=summary.get("summary", "완료"),
color=0x9B59B6,
)
for field_name, key, emoji in [
("변경 사항", "changes", ""),
("⚠️ 주의", "warnings", ""),
("🔜 다음 단계", "next_steps", ""),
for field_name, key in [
("변경 사항", "changes"),
("⚠️ 주의", "warnings"),
("🔜 다음 단계", "next_steps"),
]:
items = summary.get(key, [])
if items:
if key == "changes":
val = "\n".join(f"• `{c.get('file','?')}` — {c.get('description','')}" for c in items[:10])
if key == "changes" and isinstance(items[0], dict):
val = "\n".join(
f"• `{c.get('file','?')}` - {c.get('description','')}"
for c in items[:10]
)
else:
val = "\n".join(f"{s}" for s in items)
summary_embed.add_field(name=field_name, value=val[:1000], inline=False)
summary_embed.set_footer(text=f"ID: {task_id} | {ws.name}")
await message.channel.send(embed=summary_embed)