""" Flare – ConfigProvider (date type support) """ from __future__ import annotations import json, os from pathlib import Path from typing import Any, Dict, List, Optional, Union import commentjson from utils import log from pydantic import BaseModel, Field, HttpUrl, ValidationError, field_validator 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 # TTS/STT configurations tts_engine: str = Field("no_tts", pattern=r"^(no_tts|elevenlabs|blaze)$") tts_engine_api_key: Optional[str] = None stt_engine: str = Field("no_stt", pattern=r"^(no_stt|google|azure|amazon|gpt4o_realtime)$") stt_engine_api_key: Optional[str] = None stt_settings: Optional[Dict[str, Any]] = Field(default_factory=lambda: { "speech_timeout_ms": 2000, "noise_reduction_level": 2, "vad_sensitivity": 0.5, "language": "tr-TR", "model": "latest_long", "use_enhanced": True, "enable_punctuation": True, "interim_results": True }) 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 get_tts_api_key(self) -> Optional[str]: """Get decrypted TTS API key""" raw_key = self.tts_engine_api_key if raw_key and raw_key.startswith("enc:"): from encryption_utils import decrypt decrypted = decrypt(raw_key) log(f"🔓 TTS key decrypted: {'***' + decrypted[-4:] if decrypted else 'None'}") return decrypted log(f"🔑 TTS key not encrypted: {'***' + raw_key[-4:] if raw_key else 'None'}") return raw_key def get_stt_api_key(self) -> Optional[str]: """Get decrypted STT API key""" if self.stt_engine_api_key and self.stt_engine_api_key.startswith("enc:"): from encryption_utils import decrypt return decrypt(self.stt_engine_api_key) return self.stt_engine_api_key def get_stt_settings(self) -> Dict[str, Any]: """Get STT settings with defaults""" return self.stt_settings or { "speech_timeout_ms": 2000, "noise_reduction_level": 2, "vad_sensitivity": 0.5, "language": "tr-TR", "model": "latest_long", "use_enhanced": True, "enable_punctuation": True, "interim_results": True } 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] = {} @field_validator('token_request_body', 'token_refresh_body', mode='before') def parse_json_strings(cls, v): """Parse JSON strings to dict""" if isinstance(v, str): try: return json.loads(v) except json.JSONDecodeError: log(f"⚠️ Invalid JSON string in auth config: {v}") return {} return v 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[Union[str, ProxyConfig]] = None auth: Optional[APIAuthConfig] = None response_prompt: Optional[str] = None response_mappings: List[ResponseMappingConfig] = [] # Yeni alan # 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 @field_validator('headers', 'body_template', mode='before') def parse_json_strings(cls, v): """Parse JSON strings to dict""" if isinstance(v, str): try: return json.loads(v) except json.JSONDecodeError: log(f"⚠️ Invalid JSON string in API config: {v}") return {} return v 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 version_number: int caption: Optional[str] = "" published: bool = False general_prompt: str llm: LLMConfig intents: List[IntentConfig] # 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 publish_date: Optional[str] = None published_by: Optional[str] = None 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" version_id_counter: int = 1 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" # ---------------- Activity Log ----------- class ActivityLogEntry(BaseModel): id: int timestamp: str user: str action: str entity_type: str entity_id: Optional[int] = None entity_name: Optional[str] = None details: Optional[str] = None # ---------------- Service Config --------- class ServiceConfig(BaseModel): global_config: GlobalConfig = Field(..., alias="config") projects: List[ProjectConfig] apis: List[APIConfig] activity_log: List[ActivityLogEntry] = [] # Config level fields project_id_counter: int = 1 last_update_date: Optional[str] = None last_update_user: Optional[str] = None # 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) -> Optional[APIConfig]: return self._api_by_name.get(name) def to_jsonc_dict(self) -> dict: """Convert to dict for saving to JSONC file""" data = self.model_dump(by_alias=True, exclude={'_api_by_name'}) # Convert API configs for api in data.get('apis', []): # Convert headers and body_template to JSON strings if 'headers' in api and isinstance(api['headers'], dict): api['headers'] = json.dumps(api['headers'], ensure_ascii=False) if 'body_template' in api and isinstance(api['body_template'], dict): api['body_template'] = json.dumps(api['body_template'], ensure_ascii=False) # Convert auth configs if 'auth' in api and api['auth']: if 'token_request_body' in api['auth'] and isinstance(api['auth']['token_request_body'], dict): api['auth']['token_request_body'] = json.dumps(api['auth']['token_request_body'], ensure_ascii=False) if 'token_refresh_body' in api['auth'] and isinstance(api['auth']['token_refresh_body'], dict): api['auth']['token_refresh_body'] = json.dumps(api['auth']['token_refresh_body'], ensure_ascii=False) return data def save(self): """Save configuration to file""" config_path = Path(__file__).parent / "service_config.jsonc" data = self.to_jsonc_dict() with open(config_path, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) log("✅ Configuration saved to service_config.jsonc") # ---------------- 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") @classmethod def save(cls): """Save current configuration to file""" if cls._instance: cls._instance.save()