flare / config_provider.py
ciyidogan's picture
Update config_provider.py
3a3a88f verified
raw
history blame
6.4 kB
"""
Flare – Configuration Loader (JSONC + Pydantic v2)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
• Global, Project, Version, Intent, API tanımları
• JSONC yorum temizleme (string literal’lere dokunmaz)
• Alias’lar (version_number, max_attempts vb.)
• apis[] artık liste → runtime’da dict’e map’lenir
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field, HttpUrl, ValidationError
from utils import log
# ---------------- Global -----------------
class UserConfig(BaseModel):
username: str
password_hash: str
salt: str
class GlobalConfig(BaseModel):
work_mode: str = Field("hfcloud", pattern=r"^(hfcloud|cloud|on-premise)$")
cloud_token: Optional[str] = None
spark_endpoint: HttpUrl
users: List[UserConfig] = []
# ---------------- Retry / Proxy ----------
class RetryConfig(BaseModel):
retry_count: int = Field(3, alias="max_attempts")
backoff_seconds: int = 2
strategy: str = Field("static", pattern=r"^(static|exponential)$")
class ProxyConfig(BaseModel):
enabled: bool = True
url: HttpUrl
# ---------------- API & Auth -------------
class APIAuthConfig(BaseModel):
enabled: bool = False
token_endpoint: Optional[HttpUrl] = None
response_token_path: str = "access_token"
token_request_body: Dict[str, Any] = Field({}, alias="body_template")
token_refresh_endpoint: Optional[HttpUrl] = None
token_refresh_body: Dict[str, Any] = {}
class Config:
extra = "allow"
class APIConfig(BaseModel):
name: str
url: HttpUrl
method: str = Field("GET", pattern=r"^(GET|POST|PUT|PATCH|DELETE)$")
headers: Dict[str, Any] = {}
body_template: Dict[str, Any] = {}
timeout_seconds: int = 10
retry: RetryConfig = RetryConfig()
proxy: Optional[str | ProxyConfig] = None
auth: Optional[APIAuthConfig] = None
response_prompt: Optional[str] = None
class Config:
extra = "allow"
# ---------------- Intent / Param ---------
class ParameterConfig(BaseModel):
name: str
caption: Optional[str] = ""
type: str = Field(..., pattern=r"^(int|float|bool|str|string)$")
required: bool = True
extraction_prompt: Optional[str] = None
validation_regex: Optional[str] = None
invalid_prompt: Optional[str] = None
type_error_prompt: Optional[str] = None
def canonical_type(self) -> str:
return "str" if self.type == "string" else self.type
class IntentConfig(BaseModel):
name: str
caption: Optional[str] = ""
locale: str = "tr-TR"
dependencies: List[str] = []
examples: List[str] = []
detection_prompt: Optional[str] = None
parameters: List[ParameterConfig] = []
action: str
fallback_timeout_prompt: Optional[str] = None
fallback_error_prompt: Optional[str] = None
class Config:
extra = "allow"
# ---------------- Version / Project ------
class LLMConfig(BaseModel):
repo_id: str
generation_config: Dict[str, Any] = {}
use_fine_tune: bool = False
fine_tune_zip: str = ""
class VersionConfig(BaseModel):
id: int = Field(..., alias="version_number")
caption: Optional[str] = ""
published: bool = False
general_prompt: str
llm: LLMConfig
intents: List[IntentConfig]
class Config:
extra = "allow"
class ProjectConfig(BaseModel):
id: Optional[int] = None
name: str
caption: Optional[str] = ""
enabled: bool = True
last_version_number: Optional[int] = None
versions: List[VersionConfig]
class Config:
extra = "allow"
# ---------------- Service Config ---------
class ServiceConfig(BaseModel):
global_config: GlobalConfig = Field(..., alias="config")
projects: List[ProjectConfig]
apis: List[APIConfig]
# runtime helpers (skip validation)
_api_by_name: Dict[str, APIConfig] = {}
def build_index(self):
self._api_by_name = {a.name: a for a in self.apis}
def get_api(self, name: str) -> APIConfig | None:
return self._api_by_name.get(name)
# ---------------- Provider Singleton -----
class ConfigProvider:
_instance: Optional[ServiceConfig] = None
_CONFIG_PATH = Path(__file__).parent / "service_config.jsonc"
@classmethod
def get(cls) -> ServiceConfig:
if cls._instance is None:
cls._instance = cls._load()
cls._instance.build_index()
return cls._instance
# -------- Internal helpers ------------
@classmethod
def _load(cls) -> ServiceConfig:
log(f"📥 Loading service config from {cls._CONFIG_PATH.name} …")
raw = cls._CONFIG_PATH.read_text(encoding="utf-8")
json_str = cls._strip_jsonc(raw)
try:
data = json.loads(json_str)
cfg = ServiceConfig.model_validate(data)
log("✅ Service config loaded.")
return cfg
except (json.JSONDecodeError, ValidationError) as exc:
log(f"❌ Config validation error: {exc}")
raise
@staticmethod
def _strip_jsonc(text: str) -> str:
"""Remove // and /* */ comments (string-aware)."""
OUT, STR, ESC, SLASH, BLOCK = 0, 1, 2, 3, 4
state, res, i = OUT, [], 0
while i < len(text):
ch = text[i]
if state == OUT:
if ch == '"':
state, res = STR, res + [ch]
elif ch == '/':
nxt = text[i + 1] if i + 1 < len(text) else ""
if nxt == '/':
state, i = SLASH, i + 1
elif nxt == '*':
state, i = BLOCK, i + 1
else:
res.append(ch)
else:
res.append(ch)
elif state == STR:
res.append(ch)
state = ESC if ch == '\\' else (OUT if ch == '"' else STR)
elif state == ESC:
res.append(ch)
state = STR
elif state == SLASH:
if ch == '\n':
res.append(ch)
state = OUT
elif state == BLOCK:
if ch == '*' and i + 1 < len(text) and text[i + 1] == '/':
i, state = i + 1, OUT
i += 1
return ''.join(res)