""" Flare – ConfigProvider (date type support) """ from __future__ import annotations import json, os from pathlib import Path from typing import Any, Dict, List, Optional import commentjson from utils import log from pydantic import BaseModel, Field, HttpUrl, ValidationError from encryption_utils import decrypt class GlobalConfig(BaseModel): work_mode: str = Field("hfcloud", pattern=r"^(hfcloud|cloud|on-premise|gpt4o|gpt4o-mini)$") cloud_token: Optional[str] = None spark_endpoint: HttpUrl internal_prompt: Optional[str] = None # Yeni alan users: List["UserConfig"] = [] def is_gpt_mode(self) -> bool: """Check if running in GPT mode (any variant)""" return self.work_mode in ("gpt4o", "gpt4o-mini") def get_gpt_model(self) -> str: """Get the GPT model name for OpenAI API""" if self.work_mode == "gpt4o": return "gpt-4o" elif self.work_mode == "gpt4o-mini": return "gpt-4o-mini" return None def get_plain_token(self) -> Optional[str]: if self.cloud_token: # Lazy import to avoid circular dependency from encryption_utils import decrypt return decrypt(self.cloud_token) return None def is_cloud_mode(self) -> bool: """Check if running in cloud mode (hfcloud or cloud)""" return self.work_mode in ("hfcloud", "cloud") def is_on_premise(self) -> bool: """Check if running in on-premise mode""" return self.work_mode == "on-premise" # ---------------- Global ----------------- class UserConfig(BaseModel): username: str password_hash: str salt: str # ---------------- 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" populate_by_name = True class ResponseMappingConfig(BaseModel): """Response mapping configuration""" variable_name: str = Field(..., pattern=r"^[a-z_][a-z0-9_]*$") type: str = Field(..., pattern=r"^(int|float|bool|str|date)$") json_path: str 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 response_mappings: List[ResponseMappingConfig] = [] # Yeni alan class Config: extra = "allow" populate_by_name = True # ---------------- Intent / Param --------- class ParameterConfig(BaseModel): name: str caption: Optional[str] = "" type: str = Field(..., pattern=r"^(int|float|bool|str|string|date)$") # Added 'date' required: bool = True variable_name: str 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: if self.type == "string": return "str" elif self.type == "date": return "str" # Store dates as strings in ISO format return 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" populate_by_name = True class ProjectConfig(BaseModel): id: Optional[int] = None name: str caption: Optional[str] = "" icon: Optional[str] = "folder" description: Optional[str] = "" enabled: bool = True default_language: str = "Turkish" supported_languages: List[str] = Field(default_factory=lambda: ["tr"]) timezone: str = "Europe/Istanbul" region: str = "tr-TR" last_version_number: Optional[int] = None versions: List[VersionConfig] # Audit fields last_update_date: Optional[str] = None last_update_user: Optional[str] = None created_date: Optional[str] = None created_by: Optional[str] = None deleted: bool = False 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() cls._check_environment_setup() return cls._instance @classmethod def _load(cls) -> ServiceConfig: """Load configuration from service_config.jsonc""" try: log(f"📂 Loading config from: {cls._CONFIG_PATH}") if not cls._CONFIG_PATH.exists(): raise FileNotFoundError(f"Config file not found: {cls._CONFIG_PATH}") with open(cls._CONFIG_PATH, 'r', encoding='utf-8') as f: config_data = commentjson.load(f) # Create ServiceConfig instance service_config = ServiceConfig(**config_data) log("✅ Configuration loaded successfully") return service_config except FileNotFoundError as e: log(f"❌ Config file not found: {e}") raise except json.JSONDecodeError as e: log(f"❌ Invalid JSON in config file: {e}") raise except ValidationError as e: log(f"❌ Config validation error: {e}") raise except Exception as e: log(f"❌ Unexpected error loading config: {e}") raise @classmethod def _check_environment_setup(cls): """Check if environment is properly configured based on work_mode""" config = cls._instance.global_config if config.is_cloud_mode(): # Cloud mode - check for HuggingFace Secrets missing_secrets = [] if not os.getenv("JWT_SECRET"): missing_secrets.append("JWT_SECRET") if not os.getenv("FLARE_TOKEN_KEY"): missing_secrets.append("FLARE_TOKEN_KEY") if not os.getenv("SPARK_TOKEN"): missing_secrets.append("SPARK_TOKEN") if missing_secrets: log(f"⚠️ Running in {config.work_mode} mode. Missing secrets: {', '.join(missing_secrets)}") log("📌 Add these in HuggingFace Space Settings → Repository secrets") else: log(f"✅ Running in {config.work_mode} mode with all required secrets") elif config.is_on_premise(): # On-premise mode - check for .env file env_path = Path(".env") if env_path.exists(): from dotenv import load_dotenv load_dotenv() log("✅ Running in on-premise mode with .env file") else: log("⚠️ Running in on-premise mode but .env file not found") log("📌 Copy .env.example to .env and configure it")