""" Flare – ConfigProvider (TTS/STT support) """ from __future__ import annotations import json, os from pathlib import Path from typing import Any, Dict, List, Optional, Union from datetime import datetime import commentjson from utils import log from pydantic import BaseModel, Field, HttpUrl, ValidationError, field_validator, validator from encryption_utils import decrypt # ---------------- Parameter Collection Config --------- class ParameterCollectionConfig(BaseModel): """Configuration for smart parameter collection""" max_params_per_question: int = Field(default=2, ge=1, le=5) smart_grouping: bool = Field(default=True) retry_unanswered: bool = Field(default=True) collection_prompt: str = Field(default=""" You are a helpful assistant collecting information from the user. Conversation context: {{conversation_history}} Intent: {{intent_name}} - {{intent_caption}} Already collected: {{collected_params}} Still needed: {{missing_params}} Previously asked but not answered: {{unanswered_params}} Rules: 1. Ask for maximum {{max_params}} parameters in one question 2. Group parameters that naturally go together (like from/to cities, dates) 3. If some parameters were asked before but not answered, include them again 4. Be natural and conversational in {{project_language}} 5. Use context from the conversation to make the question flow naturally Generate ONLY the question, nothing else.""") class Config: extra = "allow" 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 configurations tts_engine: str = Field("no_tts", pattern=r"^(no_tts|elevenlabs|blaze)$") tts_engine_api_key: Optional[str] = None tts_settings: Optional[Dict[str, Any]] = Field(default_factory=lambda: { "use_ssml": False }) # STT configurations stt_engine: str = Field("no_stt", pattern=r"^(no_stt|google|azure|amazon|gpt4o_realtime|flicker)$") 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 }) parameter_collection_config: ParameterCollectionConfig = Field(default_factory=ParameterCollectionConfig) 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_tts_settings(self) -> Dict[str, Any]: """Get TTS settings with defaults""" return self.tts_settings or { "use_ssml": False } def get_stt_api_key(self) -> Optional[str]: """Get decrypted STT API key or credentials path""" raw_key = self.stt_engine_api_key if raw_key and raw_key.startswith("enc:"): from encryption_utils import decrypt decrypted = decrypt(raw_key) log(f"🔓 STT key decrypted: {'***' + decrypted[-4:] if decrypted else 'None'}") return decrypted log(f"🔑 STT key/path: {'***' + raw_key[-4:] if raw_key else 'None'}") return raw_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] = {} class Config: extra = "allow" populate_by_name = True 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" 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] = "" enabled: bool = True last_version_number: Optional[int] = None versions: List[VersionConfig] class Config: extra = "allow" # ---------------- Activity Log ----------- class ActivityLogEntry(BaseModel): timestamp: str username: 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 reload(cls) -> ServiceConfig: """Force reload configuration from file""" cls._instance = None return cls.get() @classmethod def update_config(cls, config_dict: dict): """Update the current configuration with new values""" if cls._instance is None: cls.get() # Update global config if 'config' in config_dict: for key, value in config_dict['config'].items(): if hasattr(cls._instance.global_config, key): setattr(cls._instance.global_config, key, value) # Save to file cls._instance.save() @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) # Ensure required fields exist in config data if 'config' not in config_data: config_data['config'] = {} config_section = config_data['config'] # Set defaults for missing fields config_section.setdefault('work_mode', 'hfcloud') config_section.setdefault('spark_endpoint', 'http://localhost:7861') config_section.setdefault('cloud_token', None) config_section.setdefault('internal_prompt', None) config_section.setdefault('tts_engine', 'no_tts') config_section.setdefault('tts_engine_api_key', None) config_section.setdefault('tts_settings', {'use_ssml': False}) config_section.setdefault('stt_engine', 'no_stt') config_section.setdefault('stt_engine_api_key', None) config_section.setdefault('stt_settings', { '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 }) pcc = config_data['config'].get('parameter_collection_config') if pcc is None: # Yoksa default değerlerle ekle config_data['config']['parameter_collection_config'] = ParameterCollectionConfig() config_section.setdefault('users', []) # Convert string body/headers to dict if needed for api in config_data.get('apis', []): if isinstance(api.get('headers'), str): try: api['headers'] = json.loads(api['headers']) except: api['headers'] = {} if isinstance(api.get('body_template'), str): try: api['body_template'] = json.loads(api['body_template']) except: api['body_template'] = {} # Handle auth section if api.get('auth'): auth = api['auth'] if isinstance(auth.get('token_request_body'), str): try: auth['token_request_body'] = json.loads(auth['token_request_body']) except: auth['token_request_body'] = {} if isinstance(auth.get('token_refresh_body'), str): try: auth['token_refresh_body'] = json.loads(auth['token_refresh_body']) except: auth['token_refresh_body'] = {} # Fix activity_log entries if needed if 'activity_log' in config_data: for entry in config_data['activity_log']: # Add missing username field if 'username' not in entry: entry['username'] = entry.get('user', 'system') # Ensure all required fields exist entry.setdefault('action', 'UNKNOWN') entry.setdefault('entity_type', 'unknown') entry.setdefault('timestamp', datetime.now().isoformat()) # 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("Please set these as HuggingFace Space Secrets for cloud deployment.") else: # On-premise mode - check for .env file env_path = Path(__file__).parent / ".env" if not env_path.exists(): log("⚠️ Running in on-premise mode but .env file not found") # Docker ortamında yazma izni olmayabilir, sadece uyarı ver log("⚠️ Cannot create .env file in Docker environment. Using default values.") # Set default environment variables if not already set if not os.getenv("JWT_SECRET"): os.environ["JWT_SECRET"] = "flare-admin-secret-key-change-in-production" if not os.getenv("JWT_ALGORITHM"): os.environ["JWT_ALGORITHM"] = "HS256" if not os.getenv("JWT_EXPIRATION_HOURS"): os.environ["JWT_EXPIRATION_HOURS"] = "24" if not os.getenv("FLARE_TOKEN_KEY"): os.environ["FLARE_TOKEN_KEY"] = "flare-token-encryption-key" if not os.getenv("SPARK_TOKEN"): os.environ["SPARK_TOKEN"] = "your-spark-token-here" log("✅ Default environment variables set.") # Forward references GlobalConfig.model_rebuild() VersionConfig.model_rebuild() ServiceConfig.model_rebuild()