- 게이트웨이/모델이 system 역할을 무시해 평문으로 답하는 경우(팀원 VM /api/ito + 339 실측) LLM_MERGE_SYSTEM=1 로 system 을 user 앞에 합쳐 보낸다 - summarize.ping 이 (그대로 → merge → merge+json_mode off) 순으로 시도해 되는 .env 값을 알려준다. --real 은 실제 조각 추출 프롬프트로 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
85 lines
4.2 KiB
Python
85 lines
4.2 KiB
Python
"""전역 설정. 모든 외부 접점(LLM/임베딩/DB/서버)은 환경변수로만 결정한다."""
|
|
from pathlib import Path
|
|
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(env_file=ROOT / ".env", env_file_encoding="utf-8", extra="ignore")
|
|
|
|
# DB — 기본은 SQLite(개발/폐쇄망 무의존). PostgreSQL 전환은 ASSUMPTIONS.md 참고.
|
|
database_url: str = f"sqlite:///{ROOT / 'data' / 'index.db'}"
|
|
|
|
# LLM (Stage 3 요약) — OpenAI 호환 엔드포인트 가정
|
|
llm_base_url: str = ""
|
|
llm_api_key: str = ""
|
|
llm_model: str = "glm-5.2"
|
|
# 호출 규격 (docs/llm-provider-plan.md)
|
|
# openai — Authorization: Bearer <LLM_API_KEY> (OpenRouter 등)
|
|
# fabrix — 고객사 사내 LLM(FabriX/Gauss). 키 대신 커스텀 헤더 4종 (아래 fabrix_*)
|
|
llm_provider: str = "openai"
|
|
# base_url 뒤에 붙일 경로. 고객사 주소가 이미 완성형이면 빈 문자열로 둔다
|
|
llm_chat_path: str = "/chat/completions"
|
|
# 요청 body 의 model 값. 비우면 LLM_MODEL. (FabriX 는 body 는 "/mnt/models" 고정, 모델은 헤더가 고름)
|
|
llm_body_model: str = ""
|
|
# response_format={"type":"json_object"} 전송 여부. 서버가 400 을 주면 끈다 (파서가 본문에서 JSON 을 뽑는다)
|
|
llm_json_mode: bool = True
|
|
# 출력 토큰 상한. 0 이면 보내지 않음 (OpenRouter 는 잔액 견적에 쓰고, 추론 폭주 방어도 됨)
|
|
llm_max_tokens: int = 0
|
|
# system 메시지를 user 메시지 앞에 합쳐 하나로 보낸다 — 게이트웨이/모델이 system 역할을 무시할 때
|
|
# (실측 2026-09-21: 팀원 VM 게이트웨이(/api/ito) + 모델 339 가 "JSON 만" system 지시를 무시했다)
|
|
llm_merge_system: bool = False
|
|
# FabriX 헤더 값 — x-generative-ai-client / x-openapi-token(Bearer 접두는 코드가 붙임) / x-generative-ai-user-email
|
|
fabrix_client_key: str = ""
|
|
fabrix_openapi_token: str = ""
|
|
fabrix_user_email: str = ""
|
|
# 요약 배치의 동시 LLM 호출 수. DB 쓰기는 메인 스레드에 남기고 호출만 병렬로 돈다.
|
|
llm_concurrency: int = 4
|
|
llm_timeout_s: int = 180
|
|
# 429/5xx 재시도 — 동시성을 올리면 429 가 늘어나므로 백오프와 같이 써야 한다
|
|
llm_max_retries: int = 5
|
|
llm_backoff_base: float = 2.0
|
|
llm_backoff_max: float = 60.0
|
|
# /ingest 가 뒤이어 돌리는 요약의 백엔드:
|
|
# api — 환경변수 키로 호출 (키가 유효할 때)
|
|
# file — 프롬프트를 data/llm_jobs/ 에 쌓아두고 사람·에이전트가 채운다 (키 불필요)
|
|
# off — 자동 요약을 하지 않는다 (배치로만 돌린다)
|
|
summarize_backend: str = "api"
|
|
|
|
# 임베딩 — 모델 미정(§10). 비워두면 벡터 검색 비활성(FTS만 사용).
|
|
embed_base_url: str = ""
|
|
embed_api_key: str = ""
|
|
embed_model: str = ""
|
|
|
|
# 질의 서비스
|
|
index_host: str = "127.0.0.1"
|
|
index_port: int = 8100
|
|
|
|
# 데이터 디렉토리
|
|
data_raw: Path = ROOT / "data" / "raw"
|
|
data_normalized: Path = ROOT / "data" / "normalized"
|
|
data_parsed: Path = ROOT / "data" / "parsed"
|
|
data_summaries: Path = ROOT / "data" / "summaries"
|
|
# file 백엔드의 프롬프트/응답 큐 — LLM 키 없이 요약을 돌리는 입구 (summarize/jobs.py)
|
|
data_llm_jobs: Path = ROOT / "data" / "llm_jobs"
|
|
|
|
# 위키 (계획서 v4 §5.7 — OKF v0.2 출력, 의미 검색의 1차 대상)
|
|
wiki_dir: Path = ROOT / "wiki"
|
|
# 위키 검색 백엔드: qmd | pg (M3.5 파일럿으로 확정 — 계획서 §6.5)
|
|
# "pg" 는 내장 DB 검색 경로를 뜻함. 현 구현은 SQLite FTS5 (ASSUMPTIONS.md §1).
|
|
wiki_search_backend: str = "pg"
|
|
qmd_url: str = "http://127.0.0.1:8181"
|
|
|
|
def llm_enabled(self) -> bool:
|
|
"""api 백엔드로 호출할 수 있는 상태인가 — 규격별 필수값이 다 있는지."""
|
|
if not self.llm_base_url:
|
|
return False
|
|
if self.llm_provider == "fabrix":
|
|
return bool(self.fabrix_client_key and self.fabrix_openapi_token)
|
|
return bool(self.llm_api_key)
|
|
|
|
|
|
settings = Settings()
|