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:
@@ -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")
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user