feat: Project Indexer + Context Manager + GeminiCaller 구현 및 테스트 #task-187 #task-188 #task-189
This commit is contained in:
89
core/context_manager.py
Normal file
89
core/context_manager.py
Normal 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)
|
||||
Reference in New Issue
Block a user