feat: Project Indexer + Context Manager + GeminiCaller 구현 및 테스트 #task-187 #task-188 #task-189

This commit is contained in:
quantlab
2026-03-06 17:15:54 +09:00
parent 0e3d85f9da
commit 9192770300
5 changed files with 497 additions and 0 deletions

89
core/context_manager.py Normal file
View File

@@ -0,0 +1,89 @@
"""Context Manager — 관련 파일 선별 + 토큰 예산 제어.
Gemini CLI Context Rot 해결의 핵심.
태스크에 필요한 파일만 골라 토큰 예산 내에서 컨텍스트를 구성.
"""
from pathlib import Path
from core.project_indexer import ProjectIndex
# 대략적 토큰 추정: 1 토큰 ≈ 4 bytes (영문), 2 bytes (한글)
BYTES_PER_TOKEN = 3
class ContextManager:
"""태스크별 컨텍스트를 생성합니다."""
def __init__(self, index: ProjectIndex, token_budget: int = 50_000):
self.index = index
self.token_budget = token_budget
def gather(self, task: str, max_files: int = 15) -> str:
"""태스크에 필요한 파일만 선별하여 컨텍스트 생성."""
# 1. 태스크에서 관련 파일 찾기
relevant = self.index.find_relevant(task)
# 2. import 관계로 확장
if relevant:
expanded = self.index.expand_dependencies(relevant[:5], depth=1)
else:
expanded = []
# 3. 관련 파일을 우선순위 순으로 정렬 (원래 순서 유지)
ordered = []
seen = set()
for f in relevant + expanded:
if f not in seen:
ordered.append(f)
seen.add(f)
# 4. 토큰 예산 내에서 파일 포함
context_parts = []
total_tokens = 0
files_included = 0
# 프로젝트 구조 요약 항상 포함
structure = self.index.get_structure_summary()
structure_tokens = len(structure.encode("utf-8")) // BYTES_PER_TOKEN
context_parts.append(f"=== PROJECT STRUCTURE ===\n{structure}")
total_tokens += structure_tokens
for fpath in ordered[:max_files]:
info = self.index.files.get(fpath)
if not info:
continue
file_tokens = info.size // BYTES_PER_TOKEN
if total_tokens + file_tokens > self.token_budget:
context_parts.append(
f"\n=== SKIPPED: {fpath} ({info.line_count}L, budget exceeded) ==="
)
continue
try:
abs_path = self.index.project_path / fpath
content = abs_path.read_text(encoding="utf-8", errors="ignore")
except Exception:
continue
context_parts.append(
f"\n=== FILE: {fpath} ({info.line_count}L) ===\n{content}"
)
total_tokens += file_tokens
files_included += 1
context_parts.append(
f"\n=== CONTEXT SUMMARY: {files_included} files, ~{total_tokens} tokens ==="
)
return "\n".join(context_parts)
def gather_for_review(self, original: str, modified: str, task: str) -> str:
"""리뷰용 컨텍스트: 원본 + 수정본 + 관련 타입."""
parts = [
f"=== TASK: {task} ===",
f"\n=== ORIGINAL ===\n{original}",
f"\n=== MODIFIED ===\n{modified}",
]
return "\n".join(parts)