Spaces:
Paused
Paused
| """ | |
| Flare – ConfigProvider (with Provider Abstraction and Multi-language Support) | |
| """ | |
| from __future__ import annotations | |
| import json, os, threading | |
| from pathlib import Path | |
| from typing import Any, Dict, List, Optional | |
| from datetime import datetime | |
| import commentjson | |
| from pydantic import BaseModel, Field, HttpUrl, ValidationError | |
| from utils import log | |
| from encryption_utils import decrypt | |
| # ===================== New Provider Classes ===================== | |
| class ProviderConfig(BaseModel): | |
| """Provider definition with requirements""" | |
| type: str = Field(..., pattern=r"^(llm|tts|stt)$") | |
| name: str | |
| display_name: str | |
| requires_endpoint: bool = False | |
| requires_api_key: bool = True | |
| requires_repo_info: bool = False | |
| description: Optional[str] = None | |
| class ProviderSettings(BaseModel): | |
| """Runtime provider settings""" | |
| name: str | |
| api_key: Optional[str] = None | |
| endpoint: Optional[str] = None | |
| settings: Dict[str, Any] = Field(default_factory=dict) | |
| class LocalizedCaption(BaseModel): | |
| """Multi-language caption support""" | |
| locale_code: str | |
| caption: str | |
| class LocalizedExample(BaseModel): | |
| """Multi-language example support""" | |
| locale_code: str | |
| example: str | |
| # ===================== Global Configuration ===================== | |
| class GlobalConfig(BaseModel): | |
| # Provider settings (replaces work_mode, cloud_token, spark_endpoint) | |
| llm_provider: ProviderSettings | |
| tts_provider: ProviderSettings = ProviderSettings(name="no_tts") | |
| stt_provider: ProviderSettings = ProviderSettings(name="no_stt") | |
| # Available providers | |
| providers: List[ProviderConfig] = [] | |
| # User management | |
| users: List["UserConfig"] = [] | |
| # Helper methods for providers | |
| def get_provider_config(self, provider_type: str, provider_name: str) -> Optional[ProviderConfig]: | |
| """Get provider configuration by type and name""" | |
| return next( | |
| (p for p in self.providers if p.type == provider_type and p.name == provider_name), | |
| None | |
| ) | |
| def get_providers_by_type(self, provider_type: str) -> List[ProviderConfig]: | |
| """Get all providers of a specific type""" | |
| return [p for p in self.providers if p.type == provider_type] | |
| def get_plain_api_key(self, provider_type: str) -> Optional[str]: | |
| """Get decrypted API key for a provider type""" | |
| provider_map = { | |
| "llm": self.llm_provider, | |
| "tts": self.tts_provider, | |
| "stt": self.stt_provider | |
| } | |
| provider = provider_map.get(provider_type) | |
| if provider and provider.api_key: | |
| return decrypt(provider.api_key) if provider.api_key else None | |
| return None | |
| # Backward compatibility helpers | |
| def is_cloud_mode(self) -> bool: | |
| """Check if running in cloud mode (HuggingFace)""" | |
| return bool(os.environ.get("SPACE_ID")) | |
| def is_gpt_mode(self) -> bool: | |
| """Check if using GPT provider""" | |
| return self.llm_provider.name.startswith("gpt4o") | |
| # ===================== Other Config Classes ===================== | |
| class UserConfig(BaseModel): | |
| username: str | |
| password_hash: str | |
| salt: str | |
| 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 | |
| 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 | |
| response_mappings: List[Dict[str, Any]] = [] | |
| deleted: bool = False | |
| last_update_date: Optional[str] = None | |
| last_update_user: Optional[str] = None | |
| created_date: Optional[str] = None | |
| created_by: Optional[str] = None | |
| class Config: | |
| extra = "allow" | |
| populate_by_name = True | |
| # ===================== Intent / Parameter ===================== | |
| class ParameterConfig(BaseModel): | |
| name: str | |
| caption: List[LocalizedCaption] = [] # Multi-language captions | |
| type: str = Field(..., pattern=r"^(int|float|bool|str|string|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 | |
| def get_caption_for_locale(self, locale_code: str, default_locale: str = "tr") -> str: | |
| """Get caption for specific locale with fallback""" | |
| # Try exact match | |
| caption = next((c.caption for c in self.caption if c.locale_code == locale_code), None) | |
| if caption: | |
| return caption | |
| # Try language code only (e.g., "tr" from "tr-TR") | |
| lang_code = locale_code.split("-")[0] | |
| caption = next((c.caption for c in self.caption if c.locale_code.startswith(lang_code)), None) | |
| if caption: | |
| return caption | |
| # Try default locale | |
| caption = next((c.caption for c in self.caption if c.locale_code.startswith(default_locale)), None) | |
| if caption: | |
| return caption | |
| # Return first available or name | |
| return self.caption[0].caption if self.caption else self.name | |
| class IntentConfig(BaseModel): | |
| name: str | |
| caption: Optional[str] = "" | |
| # Removed locale field - will use project's locale settings | |
| dependencies: List[str] = [] | |
| examples: List[LocalizedExample] = [] # Multi-language examples | |
| 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" | |
| def get_examples_for_locale(self, locale_code: str) -> List[str]: | |
| """Get examples for specific locale""" | |
| # Try exact match | |
| examples = [e.example for e in self.examples if e.locale_code == locale_code] | |
| if examples: | |
| return examples | |
| # Try language code only | |
| lang_code = locale_code.split("-")[0] | |
| examples = [e.example for e in self.examples if e.locale_code.startswith(lang_code)] | |
| if examples: | |
| return examples | |
| # Return all examples if no locale match | |
| return [e.example for e in self.examples] | |
| # ===================== 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") | |
| no: Optional[int] = None | |
| caption: Optional[str] = "" | |
| description: Optional[str] = "" | |
| published: bool = False | |
| deleted: bool = False | |
| created_date: Optional[str] = None | |
| created_by: Optional[str] = None | |
| last_update_date: Optional[str] = None | |
| last_update_user: Optional[str] = None | |
| publish_date: Optional[str] = None | |
| published_by: Optional[str] = None | |
| 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 | |
| last_version_number: Optional[int] = None | |
| version_id_counter: int = 1 | |
| versions: List[VersionConfig] | |
| # Language settings - changed from default_language/supported_languages | |
| default_locale: str = "tr" | |
| supported_locales: List[str] = ["tr"] | |
| timezone: Optional[str] = "Europe/Istanbul" | |
| region: Optional[str] = "tr-TR" | |
| deleted: bool = False | |
| created_date: Optional[str] = None | |
| created_by: Optional[str] = None | |
| last_update_date: Optional[str] = None | |
| last_update_user: Optional[str] = None | |
| 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 needed | |
| if 'headers' in api and isinstance(api['headers'], dict) and api['headers']: | |
| api['headers'] = json.dumps(api['headers'], ensure_ascii=False) | |
| if 'body_template' in api and isinstance(api['body_template'], dict) and api['body_template']: | |
| 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']['body_template'] = api['auth']['token_request_body'] | |
| del api['auth']['token_request_body'] | |
| 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() | |
| # Pretty print with indentation | |
| json_str = json.dumps(data, ensure_ascii=False, indent=2) | |
| with open(config_path, 'w', encoding='utf-8') as f: | |
| f.write(json_str) | |
| log("✅ Configuration saved to service_config.jsonc") | |
| # ===================== Provider Singleton ===================== | |
| class ConfigProvider: | |
| _instance: Optional[ServiceConfig] = None | |
| _CONFIG_PATH = Path(__file__).parent / "service_config.jsonc" | |
| _lock = threading.Lock() | |
| _environment_checked = False | |
| def get(cls) -> ServiceConfig: | |
| """Get cached config - thread-safe""" | |
| if cls._instance is None: | |
| with cls._lock: | |
| # Double-checked locking pattern | |
| if cls._instance is None: | |
| cls._instance = cls._load() | |
| cls._instance.build_index() | |
| # Environment kontrolünü sadece ilk yüklemede yap | |
| if not cls._environment_checked: | |
| cls._check_environment_setup() | |
| cls._environment_checked = True | |
| return cls._instance | |
| def reload(cls) -> ServiceConfig: | |
| """Force reload configuration from file - used after UI saves""" | |
| with cls._lock: | |
| log("🔄 Reloading configuration...") | |
| cls._instance = None | |
| return cls.get() | |
| 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'] = {} | |
| # Parse API configs specially | |
| if 'apis' in config_data: | |
| for api in config_data['apis']: | |
| # Parse JSON string fields | |
| if 'headers' in api and isinstance(api['headers'], str): | |
| try: | |
| api['headers'] = json.loads(api['headers']) | |
| except: | |
| api['headers'] = {} | |
| if 'body_template' in api and isinstance(api['body_template'], str): | |
| try: | |
| api['body_template'] = json.loads(api['body_template']) | |
| except: | |
| api['body_template'] = {} | |
| # Parse auth configs | |
| if 'auth' in api and api['auth']: | |
| if 'body_template' in api['auth'] and isinstance(api['auth']['body_template'], str): | |
| try: | |
| api['auth']['token_request_body'] = json.loads(api['auth']['body_template']) | |
| except: | |
| api['auth']['token_request_body'] = {} | |
| # Load and validate | |
| cfg = ServiceConfig.model_validate(config_data) | |
| log("✅ Configuration loaded successfully") | |
| return cfg | |
| except Exception as e: | |
| log(f"❌ Error loading config: {e}") | |
| raise | |
| def _check_environment_setup(cls): | |
| """Check if environment is properly configured""" | |
| if not cls._instance: | |
| return | |
| cfg = cls._instance.global_config | |
| # Check LLM provider | |
| if not cfg.llm_provider or not cfg.llm_provider.name: | |
| log("⚠️ WARNING: No LLM provider configured") | |
| return | |
| provider_config = cfg.get_provider_config("llm", cfg.llm_provider.name) | |
| if not provider_config: | |
| log(f"⚠️ WARNING: Unknown LLM provider: {cfg.llm_provider.name}") | |
| return | |
| # Check requirements | |
| if provider_config.requires_api_key and not cfg.llm_provider.api_key: | |
| log(f"⚠️ WARNING: {provider_config.display_name} requires API key but none configured") | |
| if provider_config.requires_endpoint and not cfg.llm_provider.endpoint: | |
| log(f"⚠️ WARNING: {provider_config.display_name} requires endpoint but none configured") | |
| log(f"✅ LLM Provider: {provider_config.display_name}") | |
| # ===================== CRUD Operations ===================== | |
| def add_activity_log(cls, username: str, action: str, entity_type: str, | |
| entity_id: Optional[int] = None, entity_name: Optional[str] = None, | |
| details: Optional[str] = None): | |
| """Add activity log entry""" | |
| if cls._instance is None: | |
| cls.get() | |
| entry = ActivityLogEntry( | |
| timestamp=datetime.now().isoformat() + "Z", | |
| username=username, | |
| action=action, | |
| entity_type=entity_type, | |
| entity_id=entity_id, | |
| entity_name=entity_name, | |
| details=details | |
| ) | |
| cls._instance.activity_log.append(entry) | |
| # Keep only last 1000 entries | |
| if len(cls._instance.activity_log) > 1000: | |
| cls._instance.activity_log = cls._instance.activity_log[-1000:] | |
| def update_environment(cls, update_data: dict, username: str) -> None: | |
| """Update environment configuration""" | |
| if cls._instance is None: | |
| cls.get() | |
| config = cls._instance.global_config | |
| # Update provider settings | |
| if 'llm_provider' in update_data: | |
| llm_data = update_data['llm_provider'] | |
| if 'api_key' in llm_data and llm_data['api_key'] and not llm_data['api_key'].startswith('enc:'): | |
| from encryption_utils import encrypt | |
| llm_data['api_key'] = encrypt(llm_data['api_key']) | |
| config.llm_provider = ProviderSettings(**llm_data) | |
| if 'tts_provider' in update_data: | |
| tts_data = update_data['tts_provider'] | |
| if 'api_key' in tts_data and tts_data['api_key'] and not tts_data['api_key'].startswith('enc:'): | |
| from encryption_utils import encrypt | |
| tts_data['api_key'] = encrypt(tts_data['api_key']) | |
| config.tts_provider = ProviderSettings(**tts_data) | |
| if 'stt_provider' in update_data: | |
| stt_data = update_data['stt_provider'] | |
| if 'api_key' in stt_data and stt_data['api_key'] and not stt_data['api_key'].startswith('enc:'): | |
| from encryption_utils import encrypt | |
| stt_data['api_key'] = encrypt(stt_data['api_key']) | |
| config.stt_provider = ProviderSettings(**stt_data) | |
| # Update metadata | |
| cls._instance.last_update_date = datetime.now().isoformat() + "Z" | |
| cls._instance.last_update_user = username | |
| # Add activity log | |
| cls.add_activity_log(username, "UPDATE_ENVIRONMENT", "environment", None, None, | |
| f"Updated providers: LLM={config.llm_provider.name}, TTS={config.tts_provider.name}, STT={config.stt_provider.name}") | |
| # Save | |
| cls._instance.save() | |
| def get_project(cls, project_id: int) -> Optional[ProjectConfig]: | |
| """Get project by ID""" | |
| if cls._instance is None: | |
| cls.get() | |
| return next((p for p in cls._instance.projects if p.id == project_id), None) | |
| def create_project(cls, project_data: dict, username: str) -> ProjectConfig: | |
| """Create new project with initial version""" | |
| if cls._instance is None: | |
| cls.get() | |
| # Check name uniqueness | |
| if any(p.name == project_data['name'] for p in cls._instance.projects if not p.deleted): | |
| raise ValueError(f"Project name '{project_data['name']}' already exists") | |
| # Create project | |
| new_project = ProjectConfig( | |
| id=cls._instance.project_id_counter, | |
| name=project_data['name'], | |
| caption=project_data.get('caption', ''), | |
| icon=project_data.get('icon', 'folder'), | |
| description=project_data.get('description', ''), | |
| enabled=True, | |
| default_locale=project_data.get('default_locale', 'tr'), | |
| supported_locales=project_data.get('supported_locales', ['tr']), | |
| timezone=project_data.get('timezone', 'Europe/Istanbul'), | |
| region=project_data.get('region', 'tr-TR'), | |
| version_id_counter=1, | |
| versions=[], | |
| deleted=False, | |
| created_date=datetime.now().isoformat() + "Z", | |
| created_by=username, | |
| last_update_date=datetime.now().isoformat() + "Z", | |
| last_update_user=username | |
| ) | |
| # Create initial version | |
| initial_version = VersionConfig( | |
| id=1, | |
| version_number=1, | |
| no=1, | |
| caption="Version 1", | |
| description="Initial version", | |
| published=False, | |
| deleted=False, | |
| created_date=datetime.now().isoformat() + "Z", | |
| created_by=username, | |
| last_update_date=datetime.now().isoformat() + "Z", | |
| last_update_user=username, | |
| general_prompt="You are a helpful assistant.", | |
| llm=LLMConfig( | |
| repo_id="", | |
| generation_config={ | |
| "max_new_tokens": 512, | |
| "temperature": 0.7, | |
| "top_p": 0.95 | |
| } | |
| ), | |
| intents=[] | |
| ) | |
| new_project.versions.append(initial_version) | |
| new_project.last_version_number = 1 | |
| # Add to config | |
| cls._instance.projects.append(new_project) | |
| cls._instance.project_id_counter += 1 | |
| # Add activity log | |
| cls.add_activity_log(username, "CREATE_PROJECT", "project", new_project.id, new_project.name) | |
| # Save | |
| cls._instance.save() | |
| return new_project | |
| def update_project(cls, project_id: int, update_data: dict, username: str) -> ProjectConfig: | |
| """Update project""" | |
| if cls._instance is None: | |
| cls.get() | |
| project = cls.get_project(project_id) | |
| if not project: | |
| raise ValueError(f"Project not found: {project_id}") | |
| if project.deleted: | |
| raise ValueError("Cannot update deleted project") | |
| # Update fields | |
| if 'caption' in update_data: | |
| project.caption = update_data['caption'] | |
| if 'icon' in update_data: | |
| project.icon = update_data['icon'] | |
| if 'description' in update_data: | |
| project.description = update_data['description'] | |
| if 'default_locale' in update_data: | |
| project.default_locale = update_data['default_locale'] | |
| if 'supported_locales' in update_data: | |
| project.supported_locales = update_data['supported_locales'] | |
| if 'timezone' in update_data: | |
| project.timezone = update_data['timezone'] | |
| if 'region' in update_data: | |
| project.region = update_data['region'] | |
| # Update metadata | |
| project.last_update_date = datetime.now().isoformat() + "Z" | |
| project.last_update_user = username | |
| # Add activity log | |
| cls.add_activity_log(username, "UPDATE_PROJECT", "project", project.id, project.name) | |
| # Save | |
| cls._instance.save() | |
| return project | |
| def delete_project(cls, project_id: int, username: str) -> None: | |
| """Soft delete project""" | |
| if cls._instance is None: | |
| cls.get() | |
| project = cls.get_project(project_id) | |
| if not project: | |
| raise ValueError(f"Project not found: {project_id}") | |
| project.deleted = True | |
| project.last_update_date = datetime.now().isoformat() + "Z" | |
| project.last_update_user = username | |
| # Add activity log | |
| cls.add_activity_log(username, "DELETE_PROJECT", "project", project.id, project.name) | |
| # Save | |
| cls._instance.save() | |
| def toggle_project(cls, project_id: int, username: str) -> bool: | |
| """Toggle project enabled status""" | |
| if cls._instance is None: | |
| cls.get() | |
| project = cls.get_project(project_id) | |
| if not project: | |
| raise ValueError(f"Project not found: {project_id}") | |
| project.enabled = not project.enabled | |
| project.last_update_date = datetime.now().isoformat() + "Z" | |
| project.last_update_user = username | |
| # Add activity log | |
| action = "ENABLE_PROJECT" if project.enabled else "DISABLE_PROJECT" | |
| cls.add_activity_log(username, action, "project", project.id, project.name) | |
| # Save | |
| cls._instance.save() | |
| return project.enabled | |
| def create_api(cls, api_data: dict, username: str) -> APIConfig: | |
| """Create new API""" | |
| if cls._instance is None: | |
| cls.get() | |
| # Check name uniqueness | |
| if any(a.name == api_data['name'] for a in cls._instance.apis if not a.deleted): | |
| raise ValueError(f"API name '{api_data['name']}' already exists") | |
| # Create API | |
| new_api = APIConfig( | |
| name=api_data['name'], | |
| url=api_data['url'], | |
| method=api_data.get('method', 'GET'), | |
| headers=api_data.get('headers', {}), | |
| body_template=api_data.get('body_template', {}), | |
| timeout_seconds=api_data.get('timeout_seconds', 10), | |
| retry=RetryConfig(**api_data.get('retry', {})) if 'retry' in api_data else RetryConfig(), | |
| proxy=api_data.get('proxy'), | |
| auth=APIAuthConfig(**api_data.get('auth', {})) if api_data.get('auth') else None, | |
| response_prompt=api_data.get('response_prompt'), | |
| response_mappings=api_data.get('response_mappings', []), | |
| deleted=False, | |
| created_date=datetime.now().isoformat() + "Z", | |
| created_by=username, | |
| last_update_date=datetime.now().isoformat() + "Z", | |
| last_update_user=username | |
| ) | |
| # Add to config | |
| cls._instance.apis.append(new_api) | |
| # Rebuild index | |
| cls._instance.build_index() | |
| # Add activity log | |
| cls.add_activity_log(username, "CREATE_API", "api", None, new_api.name) | |
| # Save | |
| cls._instance.save() | |
| return new_api | |
| def update_api(cls, api_name: str, update_data: dict, username: str) -> APIConfig: | |
| """Update API""" | |
| if cls._instance is None: | |
| cls.get() | |
| api = next((a for a in cls._instance.apis if a.name == api_name and not a.deleted), None) | |
| if not api: | |
| raise ValueError(f"API not found: {api_name}") | |
| # Update fields | |
| for field in ['url', 'method', 'headers', 'body_template', 'timeout_seconds', | |
| 'proxy', 'response_prompt', 'response_mappings']: | |
| if field in update_data: | |
| setattr(api, field, update_data[field]) | |
| # Update retry config | |
| if 'retry' in update_data: | |
| api.retry = RetryConfig(**update_data['retry']) | |
| # Update auth config | |
| if 'auth' in update_data: | |
| if update_data['auth']: | |
| api.auth = APIAuthConfig(**update_data['auth']) | |
| else: | |
| api.auth = None | |
| # Update metadata | |
| api.last_update_date = datetime.now().isoformat() + "Z" | |
| api.last_update_user = username | |
| # Add activity log | |
| cls.add_activity_log(username, "UPDATE_API", "api", None, api.name) | |
| # Save | |
| cls._instance.save() | |
| return api | |
| def delete_api(cls, api_name: str, username: str) -> None: | |
| """Soft delete API""" | |
| if cls._instance is None: | |
| cls.get() | |
| api = next((a for a in cls._instance.apis if a.name == api_name and not a.deleted), None) | |
| if not api: | |
| raise ValueError(f"API not found: {api_name}") | |
| # Check if API is used in any intent | |
| for project in cls._instance.projects: | |
| if getattr(project, 'deleted', False): | |
| continue | |
| for version in project.versions: | |
| if getattr(version, 'deleted', False): | |
| continue | |
| for intent in version.intents: | |
| if intent.action == api_name: | |
| raise ValueError(f"API is used in intent '{intent.name}' in project '{project.name}' version {version.no}") | |
| api.deleted = True | |
| api.last_update_date = datetime.now().isoformat() + "Z" | |
| api.last_update_user = username | |
| # Add activity log | |
| cls.add_activity_log(username, "DELETE_API", "api", None, api_name) | |
| # Save | |
| cls._instance.save() | |
| def import_project(cls, project_data: dict, username: str) -> ProjectConfig: | |
| """Import project from JSON""" | |
| if cls._instance is None: | |
| cls.get() | |
| # Validate structure | |
| if "name" not in project_data: | |
| raise ValueError("Invalid project data") | |
| # Create new project with imported data | |
| imported_data = { | |
| "name": project_data["name"], | |
| "caption": project_data.get("caption", ""), | |
| "icon": project_data.get("icon", "folder"), | |
| "description": project_data.get("description", ""), | |
| "default_locale": project_data.get("default_locale", "tr"), | |
| "supported_locales": project_data.get("supported_locales", ["tr"]) | |
| } | |
| # Create project | |
| new_project = cls.create_project(imported_data, username) | |
| # Clear default version | |
| new_project.versions = [] | |
| # Import versions | |
| for idx, version_data in enumerate(project_data.get("versions", [])): | |
| new_version = VersionConfig( | |
| id=idx + 1, | |
| version_number=idx + 1, | |
| no=idx + 1, | |
| caption=version_data.get("caption", f"Version {idx + 1}"), | |
| description=version_data.get("description", ""), | |
| published=False, | |
| deleted=False, | |
| created_date=datetime.now().isoformat() + "Z", | |
| created_by=username, | |
| last_update_date=datetime.now().isoformat() + "Z", | |
| last_update_user=username, | |
| general_prompt=version_data.get("general_prompt", ""), | |
| llm=LLMConfig(**version_data.get("llm", {})) if version_data.get("llm") else LLMConfig( | |
| repo_id="", | |
| generation_config={ | |
| "max_new_tokens": 512, | |
| "temperature": 0.7, | |
| "top_p": 0.95 | |
| } | |
| ), | |
| intents=[] | |
| ) | |
| # Import intents | |
| for intent_data in version_data.get("intents", []): | |
| intent = IntentConfig( | |
| name=intent_data.get("name", ""), | |
| caption=intent_data.get("caption", ""), | |
| detection_prompt=intent_data.get("detection_prompt", ""), | |
| action=intent_data.get("action", ""), | |
| fallback_timeout_prompt=intent_data.get("fallback_timeout_prompt"), | |
| fallback_error_prompt=intent_data.get("fallback_error_prompt"), | |
| examples=[], | |
| parameters=[] | |
| ) | |
| # Convert examples | |
| if "examples" in intent_data: | |
| if isinstance(intent_data["examples"], list): | |
| for example in intent_data["examples"]: | |
| if isinstance(example, str): | |
| # Old format - use project default locale | |
| intent.examples.append(LocalizedExample( | |
| locale_code=new_project.default_locale, | |
| example=example | |
| )) | |
| elif isinstance(example, dict): | |
| # New format | |
| intent.examples.append(LocalizedExample(**example)) | |
| # Convert parameters | |
| for param_data in intent_data.get("parameters", []): | |
| param = ParameterConfig( | |
| name=param_data.get("name", ""), | |
| type=param_data.get("type", "str"), | |
| required=param_data.get("required", True), | |
| variable_name=param_data.get("variable_name", param_data.get("name", "")), | |
| extraction_prompt=param_data.get("extraction_prompt"), | |
| validation_regex=param_data.get("validation_regex"), | |
| invalid_prompt=param_data.get("invalid_prompt"), | |
| type_error_prompt=param_data.get("type_error_prompt"), | |
| caption=[] | |
| ) | |
| # Convert caption | |
| if "caption" in param_data: | |
| if isinstance(param_data["caption"], str): | |
| # Old format | |
| param.caption.append(LocalizedCaption( | |
| locale_code=new_project.default_locale, | |
| caption=param_data["caption"] | |
| )) | |
| elif isinstance(param_data["caption"], list): | |
| # New format | |
| for cap in param_data["caption"]: | |
| param.caption.append(LocalizedCaption(**cap)) | |
| intent.parameters.append(param) | |
| new_version.intents.append(intent) | |
| new_project.versions.append(new_version) | |
| new_project.last_version_number = new_version.id | |
| # Update project counter if needed | |
| new_project.version_id_counter = len(new_project.versions) + 1 | |
| # Save | |
| cls._instance.save() | |
| # Add activity log | |
| cls.add_activity_log(username, "IMPORT_PROJECT", "project", new_project.id, new_project.name, | |
| f"Imported with {len(new_project.versions)} versions") | |
| return new_project |