feat: gemini-3-flash-preview + 역할별 thinkingBudget

- 모델: gemini-3-flash-preview (2.0 Flash → 3 Flash 업그레이드)
- 역할별 thinkingBudget 동적 조절:
  unified/summarizer = 512  (가벼운 분류/요약)
  planner            = 4096 (계획/검수)
  coder/reviewer     = 8192 (구현/비평)
- ~/.gemini/settings.json에 호출 직전 반영
- Docker에서도 settings.json 마운트로 동작
This commit is contained in:
2026-03-07 00:48:30 +09:00
parent fcd3ebfd41
commit 01bf8aa258
2 changed files with 64 additions and 7 deletions

View File

@@ -1,12 +1,11 @@
"""GeminiCaller — gemini CLI 호출. """GeminiCaller — gemini CLI 호출.
두 가지 모드: 모든 역할이 gemini-3-flash-preview 모델을 사용하며,
1. call() — 텍스트 입출력 (분류/리뷰/총평) 역할별 thinkingBudget을 동적으로 조절합니다.
2. call_agent() — 프로젝트 디렉토리에서 에이전트 실행 (코딩)
→ Gemini가 직접 파일 읽기/쓰기/명령 실행
""" """
import asyncio import asyncio
import json
import logging import logging
import time import time
import sys import sys
@@ -15,6 +14,18 @@ from pathlib import Path
logger = logging.getLogger("variet.gemini") logger = logging.getLogger("variet.gemini")
ROLE_PROMPTS_DIR = Path(__file__).parent.parent / "prompts" ROLE_PROMPTS_DIR = Path(__file__).parent.parent / "prompts"
GEMINI_MODEL = "gemini-3-flash-preview"
# 역할별 thinkingBudget (토큰 단위)
# 512=가벼운 분류/요약, 4096=계획/검수, 8192=구현/비평
ROLE_THINKING: dict[str, int] = {
"unified": 512,
"summarizer": 512,
"planner": 4096,
"coder": 8192,
"reviewer": 8192,
}
DEFAULT_THINKING = 4096
# 동시 호출 제한 (Gemini AI Ultra 120RPM 고려) # 동시 호출 제한 (Gemini AI Ultra 120RPM 고려)
_semaphore = asyncio.Semaphore(4) _semaphore = asyncio.Semaphore(4)
@@ -22,6 +33,9 @@ _semaphore = asyncio.Semaphore(4)
# Windows에서 PS ExecutionPolicy 우회 # Windows에서 PS ExecutionPolicy 우회
_IS_WIN = sys.platform == "win32" _IS_WIN = sys.platform == "win32"
# ~/.gemini/settings.json 경로
_SETTINGS_PATH = Path.home() / ".gemini" / "settings.json"
class GeminiCallError(Exception): class GeminiCallError(Exception):
"""Gemini CLI 호출 실패.""" """Gemini CLI 호출 실패."""
@@ -37,10 +51,34 @@ class GeminiCaller:
self.last_call_time = 0.0 self.last_call_time = 0.0
def _build_cmd(self): def _build_cmd(self):
"""OS에 맞는 gemini 커맨드 빌드.""" """에 맞는 gemini 커맨드 빌드."""
if _IS_WIN: if _IS_WIN:
return ["cmd", "/c", "gemini", "--approval-mode", "yolo"] return ["cmd", "/c", "gemini", "--model", GEMINI_MODEL,
return ["gemini", "--approval-mode", "yolo"] "--approval-mode", "yolo"]
return ["gemini", "--model", GEMINI_MODEL, "--approval-mode", "yolo"]
def _set_thinking_budget(self, role: str):
"""역할별 thinkingBudget을 settings.json에 반영."""
budget = ROLE_THINKING.get(role, DEFAULT_THINKING)
try:
if _SETTINGS_PATH.exists():
settings = json.loads(_SETTINGS_PATH.read_text(encoding="utf-8"))
else:
settings = {}
# modelConfigs.default.thinkingConfig.thinkingBudget 설정
configs = settings.setdefault("modelConfigs", {})
default = configs.setdefault("default", {})
thinking = default.setdefault("thinkingConfig", {})
thinking["thinkingBudget"] = budget
_SETTINGS_PATH.write_text(
json.dumps(settings, indent=2, ensure_ascii=False),
encoding="utf-8",
)
logger.debug(f"thinkingBudget={budget} for role={role}")
except Exception as e:
logger.warning(f"thinkingBudget 설정 실패 (role={role}): {e}")
def _clean_output(self, raw: str) -> str: def _clean_output(self, raw: str) -> str:
"""Gemini 출력에서 노이즈 라인 제거.""" """Gemini 출력에서 노이즈 라인 제거."""
@@ -70,6 +108,7 @@ class GeminiCaller:
async def _call_text(self, role: str, context: str, timeout: int) -> str: async def _call_text(self, role: str, context: str, timeout: int) -> str:
"""텍스트 전용 호출.""" """텍스트 전용 호출."""
self._set_thinking_budget(role)
prompt_file = ROLE_PROMPTS_DIR / f"{role}.md" prompt_file = ROLE_PROMPTS_DIR / f"{role}.md"
if prompt_file.exists(): if prompt_file.exists():
system_prompt = prompt_file.read_text(encoding="utf-8") system_prompt = prompt_file.read_text(encoding="utf-8")
@@ -143,6 +182,7 @@ class GeminiCaller:
self, role: str, context: str, cwd: str, timeout: int, self, role: str, context: str, cwd: str, timeout: int,
) -> str: ) -> str:
"""에이전트 모드 구현.""" """에이전트 모드 구현."""
self._set_thinking_budget(role)
prompt_file = ROLE_PROMPTS_DIR / f"{role}.md" prompt_file = ROLE_PROMPTS_DIR / f"{role}.md"
if prompt_file.exists(): if prompt_file.exists():
system_prompt = prompt_file.read_text(encoding="utf-8") system_prompt = prompt_file.read_text(encoding="utf-8")

View File

@@ -15,5 +15,22 @@
"project_id": 0 "project_id": 0
}, },
"docs_path": "docs/wiki" "docs_path": "docs/wiki"
},
"1479489442249969796": {
"name": "test_2",
"path": "c:\\Users\\Certes\\Desktop\\VW_Proj\\test_2",
"channel_id": 1479489442249969796,
"git": {
"url": "",
"token": "",
"repo": "",
"branch": "main"
},
"vikunja": {
"url": "",
"token": "",
"project_id": 0
},
"docs_path": "docs/wiki"
} }
} }