"""전역 설정. 모든 외부 접점(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 (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()