ciyidogan commited on
Commit
1b7f304
·
verified ·
1 Parent(s): eddeaf8

Delete config_provider.py

Browse files
Files changed (1) hide show
  1. config_provider.py +0 -1102
config_provider.py DELETED
@@ -1,1102 +0,0 @@
1
- """
2
- Flare – ConfigProvider (with Provider Abstraction and Multi-language Support)
3
- """
4
-
5
- from __future__ import annotations
6
- import json, os, sys, re
7
- import threading
8
- from pathlib import Path
9
- from typing import Any, Dict, List, Optional, Union
10
- from datetime import datetime
11
- import commentjson
12
- from utils import log
13
- from pydantic import BaseModel, Field, HttpUrl, ValidationError, field_validator, validator
14
- from encryption_utils import decrypt
15
-
16
- # ===================== Models =====================
17
- class ProviderConfig(BaseModel):
18
- """Provider definition with requirements"""
19
- type: str = Field(..., pattern=r"^(llm|tts|stt)$")
20
- name: str
21
- display_name: str
22
- requires_endpoint: bool = False
23
- requires_api_key: bool = True
24
- requires_repo_info: bool = False
25
- description: Optional[str] = None
26
-
27
- class ProviderSettings(BaseModel):
28
- """Runtime provider settings"""
29
- name: str
30
- api_key: Optional[str] = None
31
- endpoint: Optional[str] = None
32
- settings: Dict[str, Any] = Field(default_factory=dict)
33
-
34
- class LocalizedCaption(BaseModel):
35
- """Multi-language caption support"""
36
- locale_code: str
37
- caption: str
38
-
39
- class LocalizedExample(BaseModel):
40
- """Multi-language example support"""
41
- locale_code: str
42
- example: str
43
-
44
- class UserConfig(BaseModel):
45
- username: str
46
- password_hash: str
47
- salt: str
48
-
49
- class RetryConfig(BaseModel):
50
- retry_count: int = Field(3, alias="max_attempts")
51
- backoff_seconds: int = 2
52
- strategy: str = Field("static", pattern=r"^(static|exponential)$")
53
-
54
- class ProxyConfig(BaseModel):
55
- enabled: bool = True
56
- url: HttpUrl
57
-
58
- class APIAuthConfig(BaseModel):
59
- enabled: bool = False
60
- token_endpoint: Optional[HttpUrl] = None
61
- response_token_path: str = "access_token"
62
- token_request_body: Dict[str, Any] = Field({}, alias="body_template")
63
- token_refresh_endpoint: Optional[HttpUrl] = None
64
- token_refresh_body: Dict[str, Any] = {}
65
-
66
- class Config:
67
- extra = "allow"
68
- populate_by_name = True
69
-
70
- class APIConfig(BaseModel):
71
- name: str
72
- description: Optional[str] = None
73
- url: HttpUrl
74
- method: str = Field("GET", pattern=r"^(GET|POST|PUT|PATCH|DELETE)$")
75
- headers: Dict[str, Any] = {}
76
- body_template: Dict[str, Any] = {}
77
- timeout_seconds: int = 10
78
- retry: RetryConfig = RetryConfig()
79
- proxy: Optional[Union[str, ProxyConfig]] = None
80
- auth: Optional[APIAuthConfig] = None
81
- response_prompt: Optional[str] = None
82
- response_mappings: List[Dict[str, Any]] = []
83
- deleted: bool = False
84
- last_update_date: Optional[str] = None
85
- last_update_user: Optional[str] = None
86
- created_date: Optional[str] = None
87
- created_by: Optional[str] = None
88
-
89
- class Config:
90
- extra = "allow"
91
- populate_by_name = True
92
-
93
- class LLMConfig(BaseModel):
94
- repo_id: str
95
- generation_config: Dict[str, Any] = {}
96
- use_fine_tune: bool = False
97
- fine_tune_zip: str = ""
98
-
99
- class VersionConfig(BaseModel):
100
- no: int # Tek version numarası alanı
101
- caption: Optional[str] = ""
102
- description: Optional[str] = ""
103
- published: bool = False
104
- deleted: bool = False
105
- created_date: Optional[str] = None
106
- created_by: Optional[str] = None
107
- last_update_date: Optional[str] = None
108
- last_update_user: Optional[str] = None
109
- publish_date: Optional[str] = None
110
- published_by: Optional[str] = None
111
- general_prompt: str
112
- welcome_prompt: Optional[str] = None
113
- llm: LLMConfig
114
- intents: List[IntentConfig]
115
-
116
- class Config:
117
- extra = "allow"
118
-
119
- class ProjectConfig(BaseModel):
120
- id: Optional[int] = None
121
- name: str
122
- caption: Optional[str] = ""
123
- icon: Optional[str] = "folder"
124
- description: Optional[str] = ""
125
- enabled: bool = True
126
- version_id_counter: int = 1 # last_version_number yerine sadece bu kullanılacak
127
- versions: List[VersionConfig]
128
- default_locale: str = "tr"
129
- supported_locales: List[str] = ["tr"]
130
- timezone: Optional[str] = "Europe/Istanbul"
131
- region: Optional[str] = "tr-TR"
132
- deleted: bool = False
133
- created_date: Optional[str] = None
134
- created_by: Optional[str] = None
135
- last_update_date: Optional[str] = None
136
- last_update_user: Optional[str] = None
137
-
138
- class Config:
139
- extra = "allow"
140
-
141
- class ActivityLogEntry(BaseModel):
142
- timestamp: str
143
- username: str
144
- action: str
145
- entity_type: str
146
- entity_id: Optional[int] = None
147
- entity_name: Optional[str] = None
148
- details: Optional[str] = None
149
-
150
- class ParameterCollectionConfig(BaseModel):
151
- """Configuration for smart parameter collection"""
152
- max_params_per_question: int = Field(default=2, ge=1, le=5)
153
- retry_unanswered: bool = Field(default=True)
154
- collection_prompt: str = Field(default="""
155
- You are a helpful assistant collecting information from the user.
156
-
157
- Conversation context:
158
- {{conversation_history}}
159
-
160
- Intent: {{intent_name}} - {{intent_caption}}
161
-
162
- Already collected:
163
- {{collected_params}}
164
-
165
- Still needed:
166
- {{missing_params}}
167
-
168
- Previously asked but not answered:
169
- {{unanswered_params}}
170
-
171
- Rules:
172
- 1. Ask for maximum {{max_params}} parameters in one question
173
- 2. Group parameters that naturally go together (like from/to cities, dates)
174
- 3. If some parameters were asked before but not answered, include them again
175
- 4. Be natural and conversational in {{project_language}}
176
- 5. Use context from the conversation to make the question flow naturally
177
-
178
- Generate ONLY the question, nothing else.""")
179
-
180
- class Config:
181
- extra = "allow"
182
-
183
- class ParameterConfig(BaseModel):
184
- name: str
185
- caption: List[LocalizedCaption] = [] # Multi-language captions
186
- type: str = Field(..., pattern=r"^(int|float|bool|str|string|date)$")
187
- required: bool = True
188
- variable_name: str
189
- extraction_prompt: Optional[str] = None
190
- validation_regex: Optional[str] = None
191
- invalid_prompt: Optional[str] = None
192
- type_error_prompt: Optional[str] = None
193
-
194
- def canonical_type(self) -> str:
195
- if self.type == "string":
196
- return "str"
197
- elif self.type == "date":
198
- return "str" # Store dates as strings in ISO format
199
- return self.type
200
-
201
- def get_caption_for_locale(self, locale_code: str, default_locale: str = "tr") -> str:
202
- """Get caption for specific locale with fallback"""
203
- # Try exact match
204
- caption = next((c.caption for c in self.caption if c.locale_code == locale_code), None)
205
- if caption:
206
- return caption
207
-
208
- # Try language code only (e.g., "tr" from "tr-TR")
209
- lang_code = locale_code.split("-")[0]
210
- caption = next((c.caption for c in self.caption if c.locale_code.startswith(lang_code)), None)
211
- if caption:
212
- return caption
213
-
214
- # Try default locale
215
- caption = next((c.caption for c in self.caption if c.locale_code.startswith(default_locale)), None)
216
- if caption:
217
- return caption
218
-
219
- # Return first available or name
220
- return self.caption[0].caption if self.caption else self.name
221
-
222
- class IntentConfig(BaseModel):
223
- name: str
224
- caption: Union[str, List[LocalizedCaption]]
225
- dependencies: List[str] = []
226
- requiresApproval: bool = False # YENİ ALAN
227
- examples: List[LocalizedExample] = []
228
- detection_prompt: str
229
- parameters: List[ParameterConfig] = []
230
- action: str
231
- fallback_timeout_prompt: Optional[str] = None
232
- fallback_error_prompt: Optional[str] = None
233
- deleted: bool = False
234
- created_date: Optional[str] = None
235
- created_by: Optional[str] = None
236
- last_update_date: Optional[str] = None
237
- last_update_user: Optional[str] = None
238
-
239
- class Config:
240
- extra = "allow"
241
-
242
- def get_examples_for_locale(self, locale_code: str) -> List[str]:
243
- """Get examples for specific locale"""
244
- # Try exact match
245
- examples = [e.example for e in self.examples if e.locale_code == locale_code]
246
- if examples:
247
- return examples
248
-
249
- # Try language code only
250
- lang_code = locale_code.split("-")[0]
251
- examples = [e.example for e in self.examples if e.locale_code.startswith(lang_code)]
252
- if examples:
253
- return examples
254
-
255
- # Return all examples if no locale match
256
- return [e.example for e in self.examples]
257
-
258
- # ===================== Global Configuration =====================
259
- class GlobalConfig(BaseModel):
260
- # Provider settings (replaces work_mode, cloud_token, spark_endpoint)
261
- llm_provider: ProviderSettings
262
- tts_provider: ProviderSettings = Field(default_factory=lambda: ProviderSettings(name="no_tts"))
263
- stt_provider: ProviderSettings = Field(default_factory=lambda: ProviderSettings(name="no_stt"))
264
-
265
- # Available providers
266
- providers: List[ProviderConfig] = []
267
-
268
- # User management
269
- users: List["UserConfig"] = []
270
-
271
- # Helper methods for providers
272
- def get_provider_config(self, provider_type: str, provider_name: str) -> Optional[ProviderConfig]:
273
- """Get provider configuration by type and name"""
274
- return next(
275
- (p for p in self.providers if p.type == provider_type and p.name == provider_name),
276
- None
277
- )
278
-
279
- def get_providers_by_type(self, provider_type: str) -> List[ProviderConfig]:
280
- """Get all providers of a specific type"""
281
- return [p for p in self.providers if p.type == provider_type]
282
-
283
- def get_plain_api_key(self, provider_type: str) -> Optional[str]:
284
- """Get decrypted API key for a provider type"""
285
- provider_map = {
286
- "llm": self.llm_provider,
287
- "tts": self.tts_provider,
288
- "stt": self.stt_provider
289
- }
290
- provider = provider_map.get(provider_type)
291
- if provider and provider.api_key:
292
- if provider.api_key.startswith("enc:"):
293
- return decrypt(provider.api_key)
294
- return provider.api_key
295
- return None
296
-
297
- # Backward compatibility helpers
298
- def is_cloud_mode(self) -> bool:
299
- """Check if running in cloud mode (HuggingFace)"""
300
- return bool(os.environ.get("SPACE_ID"))
301
-
302
- def is_gpt_mode(self) -> bool:
303
- """Check if using GPT provider"""
304
- return self.llm_provider.name.startswith("gpt4o") if self.llm_provider else False
305
-
306
- def get_gpt_model(self) -> str:
307
- """Get the GPT model name for OpenAI API"""
308
- if self.llm_provider.name == "gpt4o":
309
- return "gpt-4o"
310
- elif self.llm_provider.name == "gpt4o-mini":
311
- return "gpt-4o-mini"
312
- return None
313
-
314
- # ---------------- Service Config ---------
315
- class ServiceConfig(BaseModel):
316
- global_config: GlobalConfig = Field(..., alias="config")
317
- projects: List[ProjectConfig]
318
- apis: List[APIConfig]
319
- activity_log: List[ActivityLogEntry] = []
320
-
321
- # Config level fields
322
- project_id_counter: int = 1
323
- last_update_date: Optional[str] = None
324
- last_update_user: Optional[str] = None
325
-
326
- # runtime helpers (skip validation)
327
- _api_by_name: Dict[str, APIConfig] = {}
328
-
329
- def build_index(self):
330
- self._api_by_name = {a.name: a for a in self.apis if not a.deleted}
331
-
332
- def get_api(self, name: str) -> Optional[APIConfig]:
333
- return self._api_by_name.get(name)
334
-
335
- def to_jsonc_dict(self) -> dict:
336
- """Convert to dict for saving to JSONC file"""
337
- data = self.model_dump(by_alias=True, exclude={'_api_by_name'})
338
-
339
- # Convert API configs
340
- for api in data.get('apis', []):
341
- # Convert headers and body_template to JSON strings if needed
342
- if 'headers' in api and isinstance(api['headers'], dict) and api['headers']:
343
- api['headers'] = json.dumps(api['headers'], ensure_ascii=False)
344
- if 'body_template' in api and isinstance(api['body_template'], dict) and api['body_template']:
345
- api['body_template'] = json.dumps(api['body_template'], ensure_ascii=False)
346
-
347
- # Convert auth configs
348
- if 'auth' in api and api['auth']:
349
- if 'token_request_body' in api['auth'] and isinstance(api['auth']['token_request_body'], dict):
350
- api['auth']['body_template'] = api['auth']['token_request_body']
351
- del api['auth']['token_request_body']
352
- if 'token_refresh_body' in api['auth'] and isinstance(api['auth']['token_refresh_body'], dict):
353
- api['auth']['token_refresh_body'] = json.dumps(api['auth']['token_refresh_body'], ensure_ascii=False)
354
-
355
- return data
356
-
357
- def save(self):
358
- """Save configuration to file"""
359
- config_path = Path(__file__).parent / "service_config.jsonc"
360
- data = self.to_jsonc_dict()
361
-
362
- # Pretty print with indentation
363
- json_str = json.dumps(data, ensure_ascii=False, indent=2)
364
-
365
- with open(config_path, 'w', encoding='utf-8') as f:
366
- f.write(json_str)
367
-
368
- log("✅ Configuration saved to service_config.jsonc")
369
-
370
- # ---------------- Provider Singleton -----
371
- class ConfigProvider:
372
- _instance: Optional[ServiceConfig] = None
373
- _CONFIG_PATH = Path(__file__).parent / "service_config.jsonc"
374
- _lock = threading.Lock()
375
- _environment_checked = False
376
-
377
- @classmethod
378
- def get(cls) -> ServiceConfig:
379
- """Get cached config - thread-safe"""
380
- if cls._instance is None:
381
- with cls._lock:
382
- # Double-checked locking pattern
383
- if cls._instance is None:
384
- cls._instance = cls._load()
385
- cls._instance.build_index()
386
- # Environment kontrolünü sadece ilk yüklemede yap
387
- if not cls._environment_checked:
388
- cls._check_environment_setup()
389
- cls._environment_checked = True
390
- return cls._instance
391
-
392
- @classmethod
393
- def reload(cls) -> ServiceConfig:
394
- """Force reload configuration from file - used after UI saves"""
395
- with cls._lock:
396
- log("🔄 Reloading configuration...")
397
- cls._instance = None
398
- return cls.get()
399
-
400
- @classmethod
401
- def _load(cls) -> ServiceConfig:
402
- """Load configuration from service_config.jsonc"""
403
- try:
404
- log(f"📂 Loading config from: {cls._CONFIG_PATH}")
405
-
406
- if not cls._CONFIG_PATH.exists():
407
- raise FileNotFoundError(f"Config file not found: {cls._CONFIG_PATH}")
408
-
409
- with open(cls._CONFIG_PATH, 'r', encoding='utf-8') as f:
410
- config_data = commentjson.load(f)
411
-
412
- # Ensure required fields exist in config data
413
- if 'config' not in config_data:
414
- config_data['config'] = {}
415
-
416
- # Handle backward compatibility - convert old format to new
417
- if 'work_mode' in config_data.get('config', {}):
418
- cls._migrate_old_config(config_data)
419
-
420
- # Parse API configs specially
421
- if 'apis' in config_data:
422
- for api in config_data['apis']:
423
- # Parse JSON string fields
424
- if 'headers' in api and isinstance(api['headers'], str):
425
- try:
426
- api['headers'] = json.loads(api['headers'])
427
- except:
428
- api['headers'] = {}
429
-
430
- if 'body_template' in api and isinstance(api['body_template'], str):
431
- try:
432
- api['body_template'] = json.loads(api['body_template'])
433
- except:
434
- api['body_template'] = {}
435
-
436
- # Parse auth configs
437
- if 'auth' in api and api['auth']:
438
- # token_request_body string ise dict'e çevir
439
- if 'token_request_body' in api['auth'] and isinstance(api['auth']['token_request_body'], str):
440
- try:
441
- api['auth']['token_request_body'] = json.loads(api['auth']['token_request_body'])
442
- except:
443
- api['auth']['token_request_body'] = {}
444
-
445
- # token_refresh_body string ise dict'e çevir
446
- if 'token_refresh_body' in api['auth'] and isinstance(api['auth']['token_refresh_body'], str):
447
- try:
448
- api['auth']['token_refresh_body'] = json.loads(api['auth']['token_refresh_body'])
449
- except:
450
- api['auth']['token_refresh_body'] = {}
451
-
452
- # Eski body_template alanı için backward compatibility
453
- if 'body_template' in api['auth'] and isinstance(api['auth']['body_template'], str):
454
- try:
455
- api['auth']['token_request_body'] = json.loads(api['auth']['body_template'])
456
- except:
457
- api['auth']['token_request_body'] = {}
458
-
459
- # Load and validate
460
- cfg = ServiceConfig.model_validate(config_data)
461
- log("✅ Configuration loaded successfully")
462
- return cfg
463
-
464
- except Exception as e:
465
- log(f"❌ Error loading config: {e}")
466
- raise
467
-
468
- @staticmethod
469
- def _strip_jsonc(text: str) -> str:
470
- """Remove comments and trailing commas from JSONC"""
471
- # Remove single-line comments
472
- text = re.sub(r'//.*$', '', text, flags=re.MULTILINE)
473
-
474
- # Remove multi-line comments
475
- text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)
476
-
477
- # Remove trailing commas before } or ]
478
- # This is the critical fix for line 107 error
479
- text = re.sub(r',\s*([}\]])', r'\1', text)
480
-
481
- return text
482
-
483
- @classmethod
484
- def _migrate_old_config(cls, config_data: dict):
485
- """Migrate old config format to new provider-based format"""
486
- log("🔄 Migrating old config format to new provider format...")
487
-
488
- old_config = config_data.get('config', {})
489
-
490
- # Create default providers if not exists
491
- if 'providers' not in old_config:
492
- old_config['providers'] = [
493
- {
494
- "type": "llm",
495
- "name": "spark",
496
- "display_name": "Spark (HuggingFace)",
497
- "requires_endpoint": True,
498
- "requires_api_key": True,
499
- "requires_repo_info": True
500
- },
501
- {
502
- "type": "llm",
503
- "name": "gpt4o",
504
- "display_name": "GPT-4o",
505
- "requires_endpoint": False,
506
- "requires_api_key": True,
507
- "requires_repo_info": False
508
- },
509
- {
510
- "type": "llm",
511
- "name": "gpt4o-mini",
512
- "display_name": "GPT-4o Mini",
513
- "requires_endpoint": False,
514
- "requires_api_key": True,
515
- "requires_repo_info": False
516
- },
517
- {
518
- "type": "tts",
519
- "name": "elevenlabs",
520
- "display_name": "ElevenLabs",
521
- "requires_endpoint": False,
522
- "requires_api_key": True
523
- },
524
- {
525
- "type": "stt",
526
- "name": "google",
527
- "display_name": "Google Speech-to-Text",
528
- "requires_endpoint": False,
529
- "requires_api_key": True
530
- }
531
- ]
532
-
533
- # Migrate LLM provider
534
- work_mode = old_config.get('work_mode', 'hfcloud')
535
- if work_mode in ['gpt4o', 'gpt4o-mini']:
536
- provider_name = work_mode
537
- api_key = old_config.get('cloud_token', '')
538
- endpoint = None
539
- else:
540
- provider_name = 'spark'
541
- api_key = old_config.get('cloud_token', '')
542
- endpoint = old_config.get('spark_endpoint', '')
543
-
544
- old_config['llm_provider'] = {
545
- "name": provider_name,
546
- "api_key": api_key,
547
- "endpoint": endpoint,
548
- "settings": {
549
- "internal_prompt": old_config.get('internal_prompt', ''),
550
- "parameter_collection_config": old_config.get('parameter_collection_config', {
551
- "max_params_per_question": 2,
552
- "retry_unanswered": True,
553
- "collection_prompt": ParameterCollectionConfig().collection_prompt
554
- })
555
- }
556
- }
557
-
558
- # Migrate TTS provider
559
- tts_engine = old_config.get('tts_engine', 'no_tts')
560
- old_config['tts_provider'] = {
561
- "name": tts_engine,
562
- "api_key": old_config.get('tts_engine_api_key', ''),
563
- "settings": old_config.get('tts_settings', {})
564
- }
565
-
566
- # Migrate STT provider
567
- stt_engine = old_config.get('stt_engine', 'no_stt')
568
- old_config['stt_provider'] = {
569
- "name": stt_engine,
570
- "api_key": old_config.get('stt_engine_api_key', ''),
571
- "settings": old_config.get('stt_settings', {})
572
- }
573
-
574
- # Migrate projects - language settings
575
- for project in config_data.get('projects', []):
576
- if 'default_language' in project:
577
- # Map language names to locale codes
578
- lang_to_locale = {
579
- 'Türkçe': 'tr',
580
- 'Turkish': 'tr',
581
- 'English': 'en',
582
- 'Deutsch': 'de',
583
- 'German': 'de'
584
- }
585
- project['default_locale'] = lang_to_locale.get(project['default_language'], 'tr')
586
- del project['default_language']
587
-
588
- if 'supported_languages' in project:
589
- # Convert to locale codes
590
- supported_locales = []
591
- for lang in project['supported_languages']:
592
- locale = lang_to_locale.get(lang, lang)
593
- if locale not in supported_locales:
594
- supported_locales.append(locale)
595
- project['supported_locales'] = supported_locales
596
- del project['supported_languages']
597
-
598
- # Migrate intent examples and parameter captions
599
- for version in project.get('versions', []):
600
- for intent in version.get('intents', []):
601
- # Migrate examples
602
- if 'examples' in intent and isinstance(intent['examples'], list):
603
- new_examples = []
604
- for example in intent['examples']:
605
- if isinstance(example, str):
606
- # Old format - use project default locale
607
- new_examples.append({
608
- "locale_code": project.get('default_locale', 'tr'),
609
- "example": example
610
- })
611
- elif isinstance(example, dict) and 'locale_code' in example:
612
- # Already new format
613
- new_examples.append(example)
614
- intent['examples'] = new_examples
615
-
616
- # Migrate parameter captions
617
- for param in intent.get('parameters', []):
618
- if 'caption' in param:
619
- if isinstance(param['caption'], str):
620
- # Old format - convert to multi-language
621
- param['caption'] = [{
622
- "locale_code": project.get('default_locale', 'tr'),
623
- "caption": param['caption']
624
- }]
625
- elif isinstance(param['caption'], list) and param['caption'] and isinstance(param['caption'][0], dict):
626
- # Already new format
627
- pass
628
-
629
- # Remove old fields
630
- fields_to_remove = ['work_mode', 'cloud_token', 'spark_endpoint', 'internal_prompt',
631
- 'tts_engine', 'tts_engine_api_key', 'tts_settings',
632
- 'stt_engine', 'stt_engine_api_key', 'stt_settings',
633
- 'parameter_collection_config']
634
- for field in fields_to_remove:
635
- old_config.pop(field, None)
636
-
637
- log("✅ Config migration completed")
638
-
639
- @classmethod
640
- def _check_environment_setup(cls):
641
- """Check if environment is properly configured"""
642
- if not cls._instance:
643
- return
644
-
645
- cfg = cls._instance.global_config
646
-
647
- # Check LLM provider
648
- if not cfg.llm_provider or not cfg.llm_provider.name:
649
- log("⚠️ WARNING: No LLM provider configured")
650
- return
651
-
652
- provider_config = cfg.get_provider_config("llm", cfg.llm_provider.name)
653
- if not provider_config:
654
- log(f"⚠️ WARNING: Unknown LLM provider: {cfg.llm_provider.name}")
655
- return
656
-
657
- # Check requirements
658
- if provider_config.requires_api_key and not cfg.llm_provider.api_key:
659
- log(f"⚠️ WARNING: {provider_config.display_name} requires API key but none configured")
660
-
661
- if provider_config.requires_endpoint and not cfg.llm_provider.endpoint:
662
- log(f"⚠️ WARNING: {provider_config.display_name} requires endpoint but none configured")
663
-
664
- log(f"✅ LLM Provider: {provider_config.display_name}")
665
-
666
- # ===================== CRUD Operations =====================
667
- @classmethod
668
- def add_activity_log(cls, username: str, action: str, entity_type: str,
669
- entity_id: Optional[int] = None, entity_name: Optional[str] = None,
670
- details: Optional[Dict[str, Any]] = None) -> None:
671
- """Add activity log entry with detailed context"""
672
- if cls._instance is None:
673
- cls.get()
674
-
675
- # Build detailed context
676
- detail_str = None
677
- if details:
678
- detail_str = json.dumps(details, ensure_ascii=False)
679
- else:
680
- # Auto-generate details based on action
681
- if action == "CREATE_PROJECT":
682
- detail_str = f"Created new project '{entity_name}'"
683
- elif action == "UPDATE_PROJECT":
684
- detail_str = f"Updated project configuration"
685
- elif action == "DELETE_PROJECT":
686
- detail_str = f"Soft deleted project '{entity_name}'"
687
- elif action == "CREATE_VERSION":
688
- detail_str = f"Created new version for {entity_name}"
689
- elif action == "PUBLISH_VERSION":
690
- detail_str = f"Published version {entity_id}"
691
- elif action == "CREATE_INTENT":
692
- detail_str = f"Added intent '{entity_name}'"
693
- elif action == "UPDATE_INTENT":
694
- detail_str = f"Modified intent configuration"
695
- elif action == "DELETE_INTENT":
696
- detail_str = f"Removed intent '{entity_name}'"
697
- elif action == "CREATE_API":
698
- detail_str = f"Created API endpoint '{entity_name}'"
699
- elif action == "UPDATE_API":
700
- detail_str = f"Modified API configuration"
701
- elif action == "DELETE_API":
702
- detail_str = f"Soft deleted API '{entity_name}'"
703
- elif action == "LOGIN":
704
- detail_str = f"User logged in"
705
- elif action == "LOGOUT":
706
- detail_str = f"User logged out"
707
- elif action == "IMPORT_PROJECT":
708
- detail_str = f"Imported project from file"
709
- elif action == "EXPORT_PROJECT":
710
- detail_str = f"Exported project '{entity_name}'"
711
-
712
- entry = ActivityLogEntry(
713
- timestamp=datetime.now().isoformat() + "Z",
714
- username=username,
715
- action=action,
716
- entity_type=entity_type,
717
- entity_id=entity_id,
718
- entity_name=entity_name,
719
- details=detail_str
720
- )
721
-
722
- cls._instance.activity_log.append(entry)
723
-
724
- # Keep only last 1000 entries
725
- if len(cls._instance.activity_log) > 1000:
726
- cls._instance.activity_log = cls._instance.activity_log[-1000:]
727
-
728
- # Save immediately for audit trail
729
- cls._instance.save()
730
-
731
- @classmethod
732
- def update_environment(cls, update_data: dict, username: str) -> None:
733
- """Update environment configuration"""
734
- if cls._instance is None:
735
- cls.get()
736
-
737
- config = cls._instance.global_config
738
-
739
- # Update provider settings
740
- if 'llm_provider' in update_data:
741
- llm_data = update_data['llm_provider']
742
- if 'api_key' in llm_data and llm_data['api_key'] and not llm_data['api_key'].startswith('enc:'):
743
- from encryption_utils import encrypt
744
- llm_data['api_key'] = encrypt(llm_data['api_key'])
745
- config.llm_provider = ProviderSettings(**llm_data)
746
-
747
- if 'tts_provider' in update_data:
748
- tts_data = update_data['tts_provider']
749
- if 'api_key' in tts_data and tts_data['api_key'] and not tts_data['api_key'].startswith('enc:'):
750
- from encryption_utils import encrypt
751
- tts_data['api_key'] = encrypt(tts_data['api_key'])
752
- config.tts_provider = ProviderSettings(**tts_data)
753
-
754
- if 'stt_provider' in update_data:
755
- stt_data = update_data['stt_provider']
756
- if 'api_key' in stt_data and stt_data['api_key'] and not stt_data['api_key'].startswith('enc:'):
757
- from encryption_utils import encrypt
758
- stt_data['api_key'] = encrypt(stt_data['api_key'])
759
- config.stt_provider = ProviderSettings(**stt_data)
760
-
761
- # Update metadata
762
- cls._instance.last_update_date = datetime.now().isoformat() + "Z"
763
- cls._instance.last_update_user = username
764
-
765
- # Add activity log
766
- cls.add_activity_log(username, "UPDATE_ENVIRONMENT", "environment", None, None,
767
- f"Updated providers: LLM={config.llm_provider.name}, TTS={config.tts_provider.name}, STT={config.stt_provider.name}")
768
-
769
- # Save
770
- cls._instance.save()
771
-
772
- @classmethod
773
- def get_project(cls, project_id: int) -> Optional[ProjectConfig]:
774
- """Get project by ID"""
775
- if cls._instance is None:
776
- cls.get()
777
-
778
- return next((p for p in cls._instance.projects if p.id == project_id), None)
779
-
780
- @classmethod
781
- def create_project(cls, project_data: dict, username: str) -> ProjectConfig:
782
- """Create new project with initial version"""
783
- if cls._instance is None:
784
- cls.get()
785
-
786
- # Check name uniqueness
787
- if any(p.name == project_data['name'] for p in cls._instance.projects if not p.deleted):
788
- raise ValueError(f"Project name '{project_data['name']}' already exists")
789
-
790
- # Increment global project counter
791
- cls._instance.project_id_counter += 1
792
-
793
- # Create project
794
- new_project = ProjectConfig(
795
- id=cls._instance.project_id_counter,
796
- name=project_data['name'],
797
- caption=project_data.get('caption', ''),
798
- icon=project_data.get('icon', 'folder'),
799
- description=project_data.get('description', ''),
800
- enabled=True,
801
- default_locale=project_data.get('default_locale', 'tr'),
802
- supported_locales=project_data.get('supported_locales', ['tr']),
803
- timezone=project_data.get('timezone', 'Europe/Istanbul'),
804
- region=project_data.get('region', 'tr-TR'),
805
- version_id_counter=1,
806
- versions=[],
807
- deleted=False,
808
- created_date=datetime.now().isoformat() + "Z",
809
- created_by=username,
810
- last_update_date=datetime.now().isoformat() + "Z",
811
- last_update_user=username
812
- )
813
-
814
- # Create initial version
815
- initial_version = VersionConfig(
816
- no=1, # Sadece no kullan
817
- caption="Version 1",
818
- description="Initial version",
819
- published=False,
820
- deleted=False,
821
- created_date=datetime.now().isoformat() + "Z",
822
- created_by=username,
823
- last_update_date=datetime.now().isoformat() + "Z",
824
- last_update_user=username,
825
- general_prompt="You are a helpful assistant.",
826
- welcome_prompt=None,
827
- llm=LLMConfig(
828
- repo_id="ytu-ce-cosmos/Turkish-Llama-8b-Instruct-v0.1",
829
- generation_config={
830
- "max_new_tokens": 256,
831
- "temperature": 0.7
832
- }
833
- ),
834
- intents=[]
835
- )
836
-
837
- new_project.versions.append(initial_version)
838
-
839
- # Add to config
840
- cls._instance.projects.append(new_project)
841
-
842
- # Add activity log
843
- cls.add_activity_log(username, "CREATE_PROJECT", "project", new_project.id, new_project.name)
844
-
845
- # Save
846
- cls._instance.save()
847
-
848
- return new_project
849
-
850
- @classmethod
851
- def update_project(cls, project_id: int, update_data: dict, username: str) -> ProjectConfig:
852
- """Update project"""
853
- if cls._instance is None:
854
- cls.get()
855
-
856
- project = cls.get_project(project_id)
857
- if not project:
858
- raise ValueError(f"Project not found: {project_id}")
859
-
860
- if project.deleted:
861
- raise ValueError("Cannot update deleted project")
862
-
863
- # Update fields
864
- if 'caption' in update_data:
865
- project.caption = update_data['caption']
866
- if 'icon' in update_data:
867
- project.icon = update_data['icon']
868
- if 'description' in update_data:
869
- project.description = update_data['description']
870
- if 'default_locale' in update_data:
871
- project.default_locale = update_data['default_locale']
872
- if 'supported_locales' in update_data:
873
- project.supported_locales = update_data['supported_locales']
874
- if 'timezone' in update_data:
875
- project.timezone = update_data['timezone']
876
- if 'region' in update_data:
877
- project.region = update_data['region']
878
-
879
- # Update metadata
880
- project.last_update_date = datetime.now().isoformat() + "Z"
881
- project.last_update_user = username
882
-
883
- # Add activity log
884
- cls.add_activity_log(username, "UPDATE_PROJECT", "project", project.id, project.name)
885
-
886
- # Save
887
- cls._instance.save()
888
-
889
- return project
890
-
891
- @classmethod
892
- def delete_project(cls, project_id: int, username: str) -> None:
893
- """Soft delete project"""
894
- if cls._instance is None:
895
- cls.get()
896
-
897
- project = cls.get_project(project_id)
898
- if not project:
899
- raise ValueError(f"Project not found: {project_id}")
900
-
901
- project.deleted = True
902
- project.last_update_date = datetime.now().isoformat() + "Z"
903
- project.last_update_user = username
904
-
905
- # Add activity log
906
- cls.add_activity_log(username, "DELETE_PROJECT", "project", project.id, project.name)
907
-
908
- # Save
909
- cls._instance.save()
910
-
911
- @classmethod
912
- def toggle_project(cls, project_id: int, username: str) -> bool:
913
- """Toggle project enabled status"""
914
- if cls._instance is None:
915
- cls.get()
916
-
917
- project = cls.get_project(project_id)
918
- if not project:
919
- raise ValueError(f"Project not found: {project_id}")
920
-
921
- project.enabled = not project.enabled
922
- project.last_update_date = datetime.now().isoformat() + "Z"
923
- project.last_update_user = username
924
-
925
- # Add activity log
926
- action = "ENABLE_PROJECT" if project.enabled else "DISABLE_PROJECT"
927
- cls.add_activity_log(username, action, "project", project.id, project.name)
928
-
929
- # Save
930
- cls._instance.save()
931
-
932
- return project.enabled
933
-
934
- @classmethod
935
- def create_api(cls, api_data: dict, username: str) -> APIConfig:
936
- """Create new API"""
937
- if cls._instance is None:
938
- cls.get()
939
-
940
- # Check name uniqueness
941
- if any(a.name == api_data['name'] for a in cls._instance.apis if not a.deleted):
942
- raise ValueError(f"API name '{api_data['name']}' already exists")
943
-
944
- # Create API
945
- new_api = APIConfig(
946
- name=api_data['name'],
947
- url=api_data['url'],
948
- method=api_data.get('method', 'GET'),
949
- headers=api_data.get('headers', {}),
950
- body_template=api_data.get('body_template', {}),
951
- timeout_seconds=api_data.get('timeout_seconds', 10),
952
- retry=RetryConfig(**api_data.get('retry', {})) if 'retry' in api_data else RetryConfig(),
953
- proxy=api_data.get('proxy'),
954
- auth=APIAuthConfig(**api_data.get('auth', {})) if api_data.get('auth') else None,
955
- response_prompt=api_data.get('response_prompt'),
956
- response_mappings=api_data.get('response_mappings', []),
957
- deleted=False,
958
- created_date=datetime.now().isoformat() + "Z",
959
- created_by=username,
960
- last_update_date=datetime.now().isoformat() + "Z",
961
- last_update_user=username
962
- )
963
-
964
- # Add to config
965
- cls._instance.apis.append(new_api)
966
-
967
- # Rebuild index
968
- cls._instance.build_index()
969
-
970
- # Add activity log
971
- cls.add_activity_log(username, "CREATE_API", "api", None, new_api.name)
972
-
973
- # Save
974
- cls._instance.save()
975
-
976
- return new_api
977
-
978
- @classmethod
979
- def update_api(cls, api_name: str, update_data: dict, username: str) -> APIConfig:
980
- """Update API"""
981
- if cls._instance is None:
982
- cls.get()
983
-
984
- api = next((a for a in cls._instance.apis if a.name == api_name and not a.deleted), None)
985
- if not api:
986
- raise ValueError(f"API not found: {api_name}")
987
-
988
- # Update fields
989
- for field in ['url', 'method', 'headers', 'body_template', 'timeout_seconds',
990
- 'proxy', 'response_prompt', 'response_mappings']:
991
- if field in update_data:
992
- setattr(api, field, update_data[field])
993
-
994
- # Update retry config
995
- if 'retry' in update_data:
996
- api.retry = RetryConfig(**update_data['retry'])
997
-
998
- # Update auth config
999
- if 'auth' in update_data:
1000
- if update_data['auth']:
1001
- api.auth = APIAuthConfig(**update_data['auth'])
1002
- else:
1003
- api.auth = None
1004
-
1005
- # Update metadata
1006
- api.last_update_date = datetime.now().isoformat() + "Z"
1007
- api.last_update_user = username
1008
-
1009
- # Add activity log
1010
- cls.add_activity_log(username, "UPDATE_API", "api", None, api.name)
1011
-
1012
- # Save
1013
- cls._instance.save()
1014
-
1015
- return api
1016
-
1017
- @classmethod
1018
- def delete_api(cls, api_name: str, username: str) -> None:
1019
- """Soft delete API"""
1020
- if cls._instance is None:
1021
- cls.get()
1022
-
1023
- api = next((a for a in cls._instance.apis if a.name == api_name and not a.deleted), None)
1024
- if not api:
1025
- raise ValueError(f"API not found: {api_name}")
1026
-
1027
- # Check if API is used in any intent
1028
- for project in cls._instance.projects:
1029
- if getattr(project, 'deleted', False):
1030
- continue
1031
-
1032
- for version in project.versions:
1033
- if getattr(version, 'deleted', False):
1034
- continue
1035
-
1036
- for intent in version.intents:
1037
- if intent.action == api_name:
1038
- raise ValueError(f"API is used in intent '{intent.name}' in project '{project.name}' version {version.no}")
1039
-
1040
- api.deleted = True
1041
- api.last_update_date = datetime.now().isoformat() + "Z"
1042
- api.last_update_user = username
1043
-
1044
- # Add activity log
1045
- cls.add_activity_log(username, "DELETE_API", "api", None, api_name)
1046
-
1047
- # Save
1048
- cls._instance.save()
1049
-
1050
- @classmethod
1051
- def import_project(cls, project_data: dict, username: str) -> ProjectConfig:
1052
- """Import project from JSON"""
1053
- if cls._instance is None:
1054
- cls.get()
1055
-
1056
- # Validate structure
1057
- if "name" not in project_data:
1058
- raise ValueError("Invalid project data")
1059
-
1060
- # Create new project with imported data
1061
- imported_data = {
1062
- "name": project_data["name"],
1063
- "caption": project_data.get("caption", ""),
1064
- "icon": project_data.get("icon", "folder"),
1065
- "description": project_data.get("description", ""),
1066
- "default_locale": project_data.get("default_locale", "tr"),
1067
- "supported_locales": project_data.get("supported_locales", ["tr"])
1068
- }
1069
-
1070
- # Create project
1071
- new_project = cls.create_project(imported_data, username)
1072
-
1073
- # Clear default version
1074
- new_project.versions = []
1075
-
1076
- # Import versions
1077
- for idx, version_data in enumerate(project_data.get("versions", [])):
1078
- new_version = VersionConfig(
1079
- no=idx + 1,
1080
- caption=version_data.get("caption", f"v{idx + 1}"),
1081
- description=version_data.get("description", ""),
1082
- published=version_data.get("published", False),
1083
- general_prompt=version_data.get("general_prompt", ""),
1084
- welcome_prompt=version_data.get("welcome_prompt"),
1085
- llm=LLMConfig(**version_data.get("llm", {})),
1086
- intents=[IntentConfig(**i) for i in version_data.get("intents", [])],
1087
- created_date=datetime.now().isoformat() + "Z",
1088
- created_by=username
1089
- )
1090
- new_project.versions.append(new_version)
1091
-
1092
- # Update version counter
1093
- new_project.version_id_counter = max(new_project.version_id_counter, new_version.no)
1094
-
1095
- # Save
1096
- cls._instance.save()
1097
-
1098
- # Add activity log
1099
- cls.add_activity_log(username, "IMPORT_PROJECT", "project", new_project.id, new_project.name,
1100
- f"Imported with {len(new_project.versions)} versions")
1101
-
1102
- return new_project