flare / config_provider.py
ciyidogan's picture
Update config_provider.py
9abe7b9 verified
raw
history blame
45 kB
"""
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
# ===================== 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
# ---------------- 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"
# ===================== 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
# ---------------- 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[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
# ---------------- Intent / Param ---------
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 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
@classmethod
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
@classmethod
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()
@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'] = {}
# 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
@staticmethod
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
@classmethod
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,
"smart_grouping": True,
"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")
@classmethod
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 =====================
@classmethod
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:]
@classmethod
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()
@classmethod
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)
@classmethod
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
@classmethod
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
@classmethod
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()
@classmethod
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
@classmethod
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
@classmethod
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
@classmethod
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()
@classmethod
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