Spaces:
Building
Building
""" | |
Flare – ConfigProvider (with Provider Abstraction and Multi-language Support) | |
""" | |
from __future__ import annotations | |
import json, os, sys, re | |
import threading | |
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 | |
# ===================== Models ===================== | |
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 | |
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 | |
description: Optional[str] = None | |
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[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 | |
class LLMConfig(BaseModel): | |
repo_id: str | |
generation_config: Dict[str, Any] = {} | |
use_fine_tune: bool = False | |
fine_tune_zip: str = "" | |
class VersionConfig(BaseModel): | |
no: int # Tek version numarası alanı | |
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 | |
welcome_prompt: Optional[str] = None | |
llm: LLMConfig | |
intents: List[IntentConfig] | |
class Config: | |
extra = "allow" | |
class ProjectConfig(BaseModel): | |
id: Optional[int] = None | |
name: str | |
caption: Optional[str] = "" | |
icon: Optional[str] = "folder" | |
description: Optional[str] = "" | |
enabled: bool = True | |
version_id_counter: int = 1 # last_version_number yerine sadece bu kullanılacak | |
versions: List[VersionConfig] | |
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" | |
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 | |
class ParameterCollectionConfig(BaseModel): | |
"""Configuration for smart parameter collection""" | |
max_params_per_question: int = Field(default=2, ge=1, le=5) | |
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 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: Union[str, List[LocalizedCaption]] | |
dependencies: List[str] = [] | |
requiresApproval: bool = False # YENİ ALAN | |
examples: List[LocalizedExample] = [] | |
detection_prompt: str | |
parameters: List[ParameterConfig] = [] | |
action: str | |
fallback_timeout_prompt: Optional[str] = None | |
fallback_error_prompt: Optional[str] = None | |
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" | |
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] | |
# ===================== Global Configuration ===================== | |
class GlobalConfig(BaseModel): | |
# Provider settings (replaces work_mode, cloud_token, spark_endpoint) | |
llm_provider: ProviderSettings | |
tts_provider: ProviderSettings = Field(default_factory=lambda: ProviderSettings(name="no_tts")) | |
stt_provider: ProviderSettings = Field(default_factory=lambda: 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: | |
if provider.api_key.startswith("enc:"): | |
return decrypt(provider.api_key) | |
return provider.api_key | |
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") if self.llm_provider else False | |
def get_gpt_model(self) -> str: | |
"""Get the GPT model name for OpenAI API""" | |
if self.llm_provider.name == "gpt4o": | |
return "gpt-4o" | |
elif self.llm_provider.name == "gpt4o-mini": | |
return "gpt-4o-mini" | |
return 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 if not a.deleted} | |
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'] = {} | |
# Handle backward compatibility - convert old format to new | |
if 'work_mode' in config_data.get('config', {}): | |
cls._migrate_old_config(config_data) | |
# 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']: | |
# token_request_body string ise dict'e çevir | |
if 'token_request_body' in api['auth'] and isinstance(api['auth']['token_request_body'], str): | |
try: | |
api['auth']['token_request_body'] = json.loads(api['auth']['token_request_body']) | |
except: | |
api['auth']['token_request_body'] = {} | |
# token_refresh_body string ise dict'e çevir | |
if 'token_refresh_body' in api['auth'] and isinstance(api['auth']['token_refresh_body'], str): | |
try: | |
api['auth']['token_refresh_body'] = json.loads(api['auth']['token_refresh_body']) | |
except: | |
api['auth']['token_refresh_body'] = {} | |
# Eski body_template alanı için backward compatibility | |
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 _strip_jsonc(text: str) -> str: | |
"""Remove comments and trailing commas from JSONC""" | |
# Remove single-line comments | |
text = re.sub(r'//.*$', '', text, flags=re.MULTILINE) | |
# Remove multi-line comments | |
text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL) | |
# Remove trailing commas before } or ] | |
# This is the critical fix for line 107 error | |
text = re.sub(r',\s*([}\]])', r'\1', text) | |
return text | |
def _migrate_old_config(cls, config_data: dict): | |
"""Migrate old config format to new provider-based format""" | |
log("🔄 Migrating old config format to new provider format...") | |
old_config = config_data.get('config', {}) | |
# Create default providers if not exists | |
if 'providers' not in old_config: | |
old_config['providers'] = [ | |
{ | |
"type": "llm", | |
"name": "spark", | |
"display_name": "Spark (HuggingFace)", | |
"requires_endpoint": True, | |
"requires_api_key": True, | |
"requires_repo_info": True | |
}, | |
{ | |
"type": "llm", | |
"name": "gpt4o", | |
"display_name": "GPT-4o", | |
"requires_endpoint": False, | |
"requires_api_key": True, | |
"requires_repo_info": False | |
}, | |
{ | |
"type": "llm", | |
"name": "gpt4o-mini", | |
"display_name": "GPT-4o Mini", | |
"requires_endpoint": False, | |
"requires_api_key": True, | |
"requires_repo_info": False | |
}, | |
{ | |
"type": "tts", | |
"name": "elevenlabs", | |
"display_name": "ElevenLabs", | |
"requires_endpoint": False, | |
"requires_api_key": True | |
}, | |
{ | |
"type": "stt", | |
"name": "google", | |
"display_name": "Google Speech-to-Text", | |
"requires_endpoint": False, | |
"requires_api_key": True | |
} | |
] | |
# Migrate LLM provider | |
work_mode = old_config.get('work_mode', 'hfcloud') | |
if work_mode in ['gpt4o', 'gpt4o-mini']: | |
provider_name = work_mode | |
api_key = old_config.get('cloud_token', '') | |
endpoint = None | |
else: | |
provider_name = 'spark' | |
api_key = old_config.get('cloud_token', '') | |
endpoint = old_config.get('spark_endpoint', '') | |
old_config['llm_provider'] = { | |
"name": provider_name, | |
"api_key": api_key, | |
"endpoint": endpoint, | |
"settings": { | |
"internal_prompt": old_config.get('internal_prompt', ''), | |
"parameter_collection_config": old_config.get('parameter_collection_config', { | |
"max_params_per_question": 2, | |
"retry_unanswered": True, | |
"collection_prompt": ParameterCollectionConfig().collection_prompt | |
}) | |
} | |
} | |
# Migrate TTS provider | |
tts_engine = old_config.get('tts_engine', 'no_tts') | |
old_config['tts_provider'] = { | |
"name": tts_engine, | |
"api_key": old_config.get('tts_engine_api_key', ''), | |
"settings": old_config.get('tts_settings', {}) | |
} | |
# Migrate STT provider | |
stt_engine = old_config.get('stt_engine', 'no_stt') | |
old_config['stt_provider'] = { | |
"name": stt_engine, | |
"api_key": old_config.get('stt_engine_api_key', ''), | |
"settings": old_config.get('stt_settings', {}) | |
} | |
# Migrate projects - language settings | |
for project in config_data.get('projects', []): | |
if 'default_language' in project: | |
# Map language names to locale codes | |
lang_to_locale = { | |
'Türkçe': 'tr', | |
'Turkish': 'tr', | |
'English': 'en', | |
'Deutsch': 'de', | |
'German': 'de' | |
} | |
project['default_locale'] = lang_to_locale.get(project['default_language'], 'tr') | |
del project['default_language'] | |
if 'supported_languages' in project: | |
# Convert to locale codes | |
supported_locales = [] | |
for lang in project['supported_languages']: | |
locale = lang_to_locale.get(lang, lang) | |
if locale not in supported_locales: | |
supported_locales.append(locale) | |
project['supported_locales'] = supported_locales | |
del project['supported_languages'] | |
# Migrate intent examples and parameter captions | |
for version in project.get('versions', []): | |
for intent in version.get('intents', []): | |
# Migrate examples | |
if 'examples' in intent and isinstance(intent['examples'], list): | |
new_examples = [] | |
for example in intent['examples']: | |
if isinstance(example, str): | |
# Old format - use project default locale | |
new_examples.append({ | |
"locale_code": project.get('default_locale', 'tr'), | |
"example": example | |
}) | |
elif isinstance(example, dict) and 'locale_code' in example: | |
# Already new format | |
new_examples.append(example) | |
intent['examples'] = new_examples | |
# Migrate parameter captions | |
for param in intent.get('parameters', []): | |
if 'caption' in param: | |
if isinstance(param['caption'], str): | |
# Old format - convert to multi-language | |
param['caption'] = [{ | |
"locale_code": project.get('default_locale', 'tr'), | |
"caption": param['caption'] | |
}] | |
elif isinstance(param['caption'], list) and param['caption'] and isinstance(param['caption'][0], dict): | |
# Already new format | |
pass | |
# Remove old fields | |
fields_to_remove = ['work_mode', 'cloud_token', 'spark_endpoint', 'internal_prompt', | |
'tts_engine', 'tts_engine_api_key', 'tts_settings', | |
'stt_engine', 'stt_engine_api_key', 'stt_settings', | |
'parameter_collection_config'] | |
for field in fields_to_remove: | |
old_config.pop(field, None) | |
log("✅ Config migration completed") | |
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[Dict[str, Any]] = None) -> None: | |
"""Add activity log entry with detailed context""" | |
if cls._instance is None: | |
cls.get() | |
# Build detailed context | |
detail_str = None | |
if details: | |
detail_str = json.dumps(details, ensure_ascii=False) | |
else: | |
# Auto-generate details based on action | |
if action == "CREATE_PROJECT": | |
detail_str = f"Created new project '{entity_name}'" | |
elif action == "UPDATE_PROJECT": | |
detail_str = f"Updated project configuration" | |
elif action == "DELETE_PROJECT": | |
detail_str = f"Soft deleted project '{entity_name}'" | |
elif action == "CREATE_VERSION": | |
detail_str = f"Created new version for {entity_name}" | |
elif action == "PUBLISH_VERSION": | |
detail_str = f"Published version {entity_id}" | |
elif action == "CREATE_INTENT": | |
detail_str = f"Added intent '{entity_name}'" | |
elif action == "UPDATE_INTENT": | |
detail_str = f"Modified intent configuration" | |
elif action == "DELETE_INTENT": | |
detail_str = f"Removed intent '{entity_name}'" | |
elif action == "CREATE_API": | |
detail_str = f"Created API endpoint '{entity_name}'" | |
elif action == "UPDATE_API": | |
detail_str = f"Modified API configuration" | |
elif action == "DELETE_API": | |
detail_str = f"Soft deleted API '{entity_name}'" | |
elif action == "LOGIN": | |
detail_str = f"User logged in" | |
elif action == "LOGOUT": | |
detail_str = f"User logged out" | |
elif action == "IMPORT_PROJECT": | |
detail_str = f"Imported project from file" | |
elif action == "EXPORT_PROJECT": | |
detail_str = f"Exported project '{entity_name}'" | |
entry = ActivityLogEntry( | |
timestamp=datetime.now().isoformat() + "Z", | |
username=username, | |
action=action, | |
entity_type=entity_type, | |
entity_id=entity_id, | |
entity_name=entity_name, | |
details=detail_str | |
) | |
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:] | |
# Save immediately for audit trail | |
cls._instance.save() | |
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") | |
# Increment global project counter | |
cls._instance.project_id_counter += 1 | |
# 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( | |
no=1, # Sadece no kullan | |
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.", | |
welcome_prompt=None, | |
llm=LLMConfig( | |
repo_id="ytu-ce-cosmos/Turkish-Llama-8b-Instruct-v0.1", | |
generation_config={ | |
"max_new_tokens": 256, | |
"temperature": 0.7 | |
} | |
), | |
intents=[] | |
) | |
new_project.versions.append(initial_version) | |
# Add to config | |
cls._instance.projects.append(new_project) | |
# 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( | |
no=idx + 1, | |
caption=version_data.get("caption", f"v{idx + 1}"), | |
description=version_data.get("description", ""), | |
published=version_data.get("published", False), | |
general_prompt=version_data.get("general_prompt", ""), | |
welcome_prompt=version_data.get("welcome_prompt"), | |
llm=LLMConfig(**version_data.get("llm", {})), | |
intents=[IntentConfig(**i) for i in version_data.get("intents", [])], | |
created_date=datetime.now().isoformat() + "Z", | |
created_by=username | |
) | |
new_project.versions.append(new_version) | |
# Update version counter | |
new_project.version_id_counter = max(new_project.version_id_counter, new_version.no) | |
# 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 |