ciyidogan commited on
Commit
864cecb
·
verified ·
1 Parent(s): 5713a15

Update config_provider.py

Browse files
Files changed (1) hide show
  1. config_provider.py +827 -833
config_provider.py CHANGED
@@ -1,854 +1,848 @@
1
- """
2
- Thread-Safe Configuration Provider for Flare Platform
3
- """
4
- import threading
5
- import os
6
- import json
7
- import commentjson
8
- from typing import Optional, Dict, List, Any
9
- from datetime import datetime
10
- from pathlib import Path
11
- import tempfile
12
- import shutil
13
- from utils import get_current_timestamp, normalize_timestamp, timestamps_equal
14
-
15
- from config_models import (
16
- ServiceConfig, GlobalConfig, ProjectConfig, VersionConfig,
17
- IntentConfig, APIConfig, ActivityLogEntry, ParameterConfig,
18
- LLMConfiguration, GenerationConfig
19
- )
20
- from logger import log_info, log_error, log_warning, log_debug, LogTimer
21
- from exceptions import (
22
- RaceConditionError, ConfigurationError, ResourceNotFoundError,
23
- DuplicateResourceError, ValidationError
24
- )
25
- from encryption_utils import encrypt, decrypt
26
-
27
- class ConfigProvider:
28
- """Thread-safe singleton configuration provider"""
29
-
30
- _instance: Optional[ServiceConfig] = None
31
- _lock = threading.RLock() # Reentrant lock for nested calls
32
- _file_lock = threading.Lock() # Separate lock for file operations
33
- _CONFIG_PATH = Path(__file__).parent / "service_config.jsonc"
34
-
35
- @staticmethod
36
- def _normalize_date(date_str: Optional[str]) -> str:
37
- """Normalize date string for comparison"""
38
- if not date_str:
39
- return ""
40
- return date_str.replace(' ', 'T').replace('+00:00', 'Z').replace('.000Z', 'Z')
41
-
42
- @classmethod
43
- def get(cls) -> ServiceConfig:
44
- """Get cached configuration - thread-safe"""
45
- if cls._instance is None:
46
- with cls._lock:
47
- # Double-checked locking pattern
48
- if cls._instance is None:
49
- with LogTimer("config_load"):
50
- cls._instance = cls._load()
51
- cls._instance.build_index()
52
- log_info("Configuration loaded successfully")
53
- return cls._instance
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
- @classmethod
56
- def reload(cls) -> ServiceConfig:
57
- """Force reload configuration from file"""
58
- with cls._lock:
59
- log_info("Reloading configuration...")
60
- cls._instance = None
61
- return cls.get()
62
 
63
- @classmethod
64
- def _load(cls) -> ServiceConfig:
65
- """Load configuration from file"""
66
- try:
67
- if not cls._CONFIG_PATH.exists():
68
- raise ConfigurationError(
69
- f"Config file not found: {cls._CONFIG_PATH}",
70
- config_key="service_config.jsonc"
71
- )
72
-
73
- with open(cls._CONFIG_PATH, 'r', encoding='utf-8') as f:
74
- config_data = commentjson.load(f)
75
-
76
- # Debug: İlk project'in tarihini kontrol et
77
- if 'projects' in config_data and len(config_data['projects']) > 0:
78
- first_project = config_data['projects'][0]
79
- log_debug(f"🔍 Raw project data - last_update_date: {first_project.get('last_update_date')}")
80
-
81
- # Ensure required fields
82
- if 'config' not in config_data:
83
- config_data['config'] = {}
84
-
85
- # Parse API configs (handle JSON strings)
86
- if 'apis' in config_data:
87
- cls._parse_api_configs(config_data['apis'])
88
-
89
- # Validate and create model
90
- cfg = ServiceConfig.model_validate(config_data)
91
-
92
- # Debug: Model'e dönüştükten sonra kontrol et
93
- if cfg.projects and len(cfg.projects) > 0:
94
- log_debug(f"🔍 Parsed project - last_update_date: {cfg.projects[0].last_update_date}")
95
- log_debug(f"🔍 Type: {type(cfg.projects[0].last_update_date)}")
96
-
97
- log_debug(
98
- "Configuration loaded",
99
- projects=len(cfg.projects),
100
- apis=len(cfg.apis),
101
- users=len(cfg.global_config.users)
102
- )
103
-
104
- return cfg
105
-
106
- except Exception as e:
107
- log_error(f"Error loading config", error=str(e), path=str(cls._CONFIG_PATH))
108
- raise ConfigurationError(f"Failed to load configuration: {e}")
109
-
110
- @classmethod
111
- def _parse_api_configs(cls, apis: List[Dict[str, Any]]) -> None:
112
- """Parse JSON string fields in API configs"""
113
- for api in apis:
114
- # Parse headers
115
- if 'headers' in api and isinstance(api['headers'], str):
116
- try:
117
- api['headers'] = json.loads(api['headers'])
118
- except json.JSONDecodeError:
119
- api['headers'] = {}
120
-
121
- # Parse body_template
122
- if 'body_template' in api and isinstance(api['body_template'], str):
123
- try:
124
- api['body_template'] = json.loads(api['body_template'])
125
- except json.JSONDecodeError:
126
- api['body_template'] = {}
127
-
128
- # Parse auth configs
129
- if 'auth' in api and api['auth']:
130
- cls._parse_auth_config(api['auth'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
- @classmethod
133
- def _parse_auth_config(cls, auth: Dict[str, Any]) -> None:
134
- """Parse auth configuration"""
135
- # Parse token_request_body
136
- if 'token_request_body' in auth and isinstance(auth['token_request_body'], str):
137
- try:
138
- auth['token_request_body'] = json.loads(auth['token_request_body'])
139
- except json.JSONDecodeError:
140
- auth['token_request_body'] = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
- # Parse token_refresh_body
143
- if 'token_refresh_body' in auth and isinstance(auth['token_refresh_body'], str):
144
- try:
145
- auth['token_refresh_body'] = json.loads(auth['token_refresh_body'])
146
- except json.JSONDecodeError:
147
- auth['token_refresh_body'] = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
- @classmethod
150
- def save(cls, config: ServiceConfig, username: str) -> None:
151
- """Thread-safe configuration save with optimistic locking"""
152
- with cls._file_lock:
153
- try:
154
- # Load current config for race condition check
155
- try:
156
- current_config = cls._load()
157
-
158
- # Check for race condition
159
- if config.last_update_date and current_config.last_update_date:
160
- if not timestamps_equal(config.last_update_date, current_config.last_update_date):
161
- raise RaceConditionError(
162
- "Configuration was modified by another user",
163
- current_user=username,
164
- last_update_user=current_config.last_update_user,
165
- last_update_date=current_config.last_update_date,
166
- entity_type="configuration"
167
- )
168
- except ConfigurationError as e:
169
- # Eğer mevcut config yüklenemiyorsa, race condition kontrolünü atla
170
- log_warning(f"Could not load current config for race condition check: {e}")
171
- current_config = None
172
-
173
- # Update metadata
174
- config.last_update_date = get_current_timestamp()
175
- config.last_update_user = username
176
-
177
- # Convert to JSON - Pydantic v2 kullanımı
178
- data = config.model_dump(mode='json')
179
- json_str = json.dumps(data, ensure_ascii=False, indent=2)
180
-
181
- # Backup current file if exists
182
- backup_path = None
183
- if cls._CONFIG_PATH.exists():
184
- backup_path = cls._CONFIG_PATH.with_suffix('.backup')
185
- shutil.copy2(str(cls._CONFIG_PATH), str(backup_path))
186
- log_debug(f"Created backup at {backup_path}")
187
-
188
- try:
189
- # Write to temporary file first
190
- temp_path = cls._CONFIG_PATH.with_suffix('.tmp')
191
- with open(temp_path, 'w', encoding='utf-8') as f:
192
- f.write(json_str)
193
-
194
- # Validate the temp file by trying to load it
195
- with open(temp_path, 'r', encoding='utf-8') as f:
196
- test_data = commentjson.load(f)
197
- ServiceConfig.model_validate(test_data)
198
-
199
- # If validation passes, replace the original
200
- shutil.move(str(temp_path), str(cls._CONFIG_PATH))
201
-
202
- # Delete backup if save successful
203
- if backup_path and backup_path.exists():
204
- backup_path.unlink()
205
-
206
- except Exception as e:
207
- # Restore from backup if something went wrong
208
- if backup_path and backup_path.exists():
209
- shutil.move(str(backup_path), str(cls._CONFIG_PATH))
210
- log_error(f"Restored configuration from backup due to error: {e}")
211
- raise
212
-
213
- # Update cached instance
214
- with cls._lock:
215
- cls._instance = config
216
-
217
- log_info(
218
- "Configuration saved successfully",
219
- user=username,
220
- last_update=config.last_update_date
221
- )
222
-
223
- except Exception as e:
224
- log_error("Failed to save configuration", error=str(e), user=username)
225
- raise
226
 
227
- # ===================== Environment Methods =====================
 
 
 
228
 
229
- @classmethod
230
- def update_environment(cls, update_data: dict, username: str) -> None:
231
- """Update environment configuration"""
232
- with cls._lock:
233
- config = cls.get()
234
-
235
- # Update providers
236
- if 'llm_provider' in update_data:
237
- config.global_config.llm_provider = update_data['llm_provider']
238
-
239
- if 'tts_provider' in update_data:
240
- config.global_config.tts_provider = update_data['tts_provider']
241
-
242
- if 'stt_provider' in update_data:
243
- config.global_config.stt_provider = update_data['stt_provider']
244
-
245
- # Log activity
246
- cls._add_activity(
247
- config, username, "UPDATE_ENVIRONMENT",
248
- "environment", None, None,
249
- f"Updated providers"
250
- )
251
-
252
- # Save
253
- cls.save(config, username)
254
 
255
- # ===================== Project Methods =====================
 
 
256
 
257
- @classmethod
258
- def get_project(cls, project_id: int) -> Optional[ProjectConfig]:
259
- """Get project by ID"""
260
- config = cls.get()
261
- return next((p for p in config.projects if p.id == project_id), None)
262
 
263
- @classmethod
264
- def create_project(cls, project_data: dict, username: str) -> ProjectConfig:
265
- """Create new project with initial version"""
266
- with cls._lock:
267
- config = cls.get()
268
-
269
- # Check for duplicate name
270
- if any(p.name == project_data['name'] for p in config.projects):
271
- raise DuplicateResourceError("project", project_data['name'])
272
-
273
- # Create project
274
- project = ProjectConfig(
275
- id=config.project_id_counter,
276
- created_date=get_current_timestamp(),
277
- created_by=username,
278
- version_id_counter=1, # Başlangıç değeri
279
- versions=[], # Boş başla
280
- **project_data
281
- )
282
-
283
- # Create initial version with proper models
284
- initial_version = VersionConfig(
285
- no=1,
286
- caption="Initial version",
287
- description="Auto-generated initial version",
288
- published=False, # Explicitly set to False
289
- deleted=False,
290
- general_prompt="You are a helpful assistant.",
291
- welcome_prompt=None,
292
- llm=LLMConfiguration(
293
- repo_id="ytu-ce-cosmos/Turkish-Llama-8b-Instruct-v0.1",
294
- generation_config=GenerationConfig(
295
- max_new_tokens=512,
296
- temperature=0.7,
297
- top_p=0.9,
298
- repetition_penalty=1.1,
299
- do_sample=True
300
- ),
301
- use_fine_tune=False,
302
- fine_tune_zip=""
303
- ),
304
- intents=[],
305
- created_date=get_current_timestamp(),
306
- created_by=username,
307
- last_update_date=None,
308
- last_update_user=None,
309
- publish_date=None,
310
- published_by=None
311
- )
312
-
313
- # Add initial version to project
314
- project.versions.append(initial_version)
315
- project.version_id_counter = 2 # Next version will be 2
316
-
317
- # Update config
318
- config.projects.append(project)
319
- config.project_id_counter += 1
320
-
321
- # Log activity
322
- cls._add_activity(
323
- config, username, "CREATE_PROJECT",
324
- "project", project.id, project.name,
325
- f"Created with initial version"
326
- )
327
-
328
- # Save
329
- cls.save(config, username)
330
-
331
- log_info(
332
- "Project created with initial version",
333
- project_id=project.id,
334
- name=project.name,
335
- user=username
336
- )
337
-
338
- return project
339
-
340
- @classmethod
341
- def update_project(cls, project_id: int, update_data: dict, username: str, expected_last_update: Optional[str] = None) -> ProjectConfig:
342
- """Update project with optimistic locking"""
343
- with cls._lock:
344
- config = cls.get()
345
- project = cls.get_project(project_id)
346
-
347
- if not project:
348
- raise ResourceNotFoundError("project", project_id)
349
-
350
- # Check race condition
351
- if expected_last_update is not None and expected_last_update != '':
352
- if project.last_update_date and not timestamps_equal(expected_last_update, project.last_update_date):
353
- raise RaceConditionError(
354
- f"Project '{project.name}' was modified by another user",
355
- current_user=username,
356
- last_update_user=project.last_update_user,
357
- last_update_date=project.last_update_date,
358
- entity_type="project",
359
- entity_id=project_id
360
- )
361
-
362
- # Update fields
363
- for key, value in update_data.items():
364
- if hasattr(project, key) and key not in ['id', 'created_date', 'created_by', 'last_update_date', 'last_update_user']:
365
- setattr(project, key, value)
366
-
367
- project.last_update_date = get_current_timestamp()
368
- project.last_update_user = username
369
-
370
- # Log activity
371
- cls._add_activity(
372
- config, username, "UPDATE_PROJECT",
373
- "project", project.id, project.name
374
- )
375
-
376
- # Save
377
- cls.save(config, username)
378
-
379
- log_info(
380
- "Project updated",
381
- project_id=project.id,
382
- user=username
383
- )
384
-
385
- return project
386
 
387
- @classmethod
388
- def delete_project(cls, project_id: int, username: str) -> None:
389
- """Soft delete project"""
390
- with cls._lock:
391
- config = cls.get()
392
- project = cls.get_project(project_id)
393
-
394
- if not project:
395
- raise ResourceNotFoundError("project", project_id)
396
-
397
- project.deleted = True
398
- project.last_update_date = get_current_timestamp()
399
- project.last_update_user = username
400
-
401
- # Log activity
402
- cls._add_activity(
403
- config, username, "DELETE_PROJECT",
404
- "project", project.id, project.name
405
- )
406
-
407
- # Save
408
- cls.save(config, username)
409
-
410
- log_info(
411
- "Project deleted",
412
- project_id=project.id,
413
- user=username
414
- )
415
 
416
- @classmethod
417
- def toggle_project(cls, project_id: int, username: str) -> bool:
418
- """Toggle project enabled status"""
419
- with cls._lock:
420
- config = cls.get()
421
- project = cls.get_project(project_id)
422
-
423
- if not project:
424
- raise ResourceNotFoundError("project", project_id)
425
-
426
- project.enabled = not project.enabled
427
- project.last_update_date = get_current_timestamp()
428
- project.last_update_user = username
429
-
430
- # Log activity
431
- cls._add_activity(
432
- config, username, "TOGGLE_PROJECT",
433
- "project", project.id, project.name,
434
- f"{'Enabled' if project.enabled else 'Disabled'}"
435
- )
436
-
437
- # Save
438
- cls.save(config, username)
439
-
440
- log_info(
441
- "Project toggled",
442
- project_id=project.id,
443
- enabled=project.enabled,
444
- user=username
445
- )
446
-
447
- return project.enabled
448
 
449
- # ===================== Version Methods =====================
 
 
 
 
 
 
 
 
 
 
 
 
450
 
451
- @classmethod
452
- def create_version(cls, project_id: int, version_data: dict, username: str) -> VersionConfig:
453
- """Create new version"""
454
- with cls._lock:
455
- config = cls.get()
456
- project = cls.get_project(project_id)
457
-
458
- if not project:
459
- raise ResourceNotFoundError("project", project_id)
460
-
461
- # Handle source version copy
462
- if 'source_version_no' in version_data and version_data['source_version_no']:
463
- source_version = next((v for v in project.versions if v.no == version_data['source_version_no']), None)
464
- if source_version:
465
- # Copy from source version
466
- version_dict = source_version.model_dump()
467
- # Remove fields that shouldn't be copied
468
- for field in ['no', 'created_date', 'created_by', 'published', 'publish_date',
469
- 'published_by', 'last_update_date', 'last_update_user']:
470
- version_dict.pop(field, None)
471
- # Override with provided data
472
- version_dict['caption'] = version_data.get('caption', f"Copy of {source_version.caption}")
473
- else:
474
- # Source not found, create blank
475
- version_dict = {
476
- 'caption': version_data.get('caption', 'New Version'),
477
- 'general_prompt': '',
478
- 'welcome_prompt': None,
479
- 'llm': {
480
- 'repo_id': '',
481
- 'generation_config': {
482
- 'max_new_tokens': 512,
483
- 'temperature': 0.7,
484
- 'top_p': 0.95,
485
- 'repetition_penalty': 1.1
486
- },
487
- 'use_fine_tune': False,
488
- 'fine_tune_zip': ''
489
- },
490
- 'intents': []
491
- }
492
- else:
493
- # Create blank version
494
- version_dict = {
495
- 'caption': version_data.get('caption', 'New Version'),
496
- 'general_prompt': '',
497
- 'welcome_prompt': None,
498
- 'llm': {
499
- 'repo_id': '',
500
- 'generation_config': {
501
- 'max_new_tokens': 512,
502
- 'temperature': 0.7,
503
- 'top_p': 0.95,
504
- 'repetition_penalty': 1.1
505
- },
506
- 'use_fine_tune': False,
507
- 'fine_tune_zip': ''
508
- },
509
- 'intents': []
510
- }
511
-
512
- # Create version
513
- version = VersionConfig(
514
- no=project.version_id_counter,
515
- published=False, # New versions are always unpublished
516
- deleted=False,
517
- created_date=get_current_timestamp(),
518
- created_by=username,
519
- **version_dict
520
- )
521
-
522
- # Update project
523
- project.versions.append(version)
524
- project.version_id_counter += 1
525
- project.last_update_date = get_current_timestamp()
526
- project.last_update_user = username
527
-
528
- # Log activity
529
- cls._add_activity(
530
- config, username, "CREATE_VERSION",
531
- "version", version.no, f"{project.name} v{version.no}",
532
- f"Project: {project.name}"
533
- )
534
-
535
- # Save
536
- cls.save(config, username)
537
-
538
- log_info(
539
- "Version created",
540
- project_id=project.id,
541
- version_no=version.no,
542
- user=username
543
- )
544
-
545
- return version
546
 
547
- @classmethod
548
- def publish_version(cls, project_id: int, version_no: int, username: str) -> tuple[ProjectConfig, VersionConfig]:
549
- """Publish a version"""
550
- with cls._lock:
551
- config = cls.get()
552
- project = cls.get_project(project_id)
553
-
554
- if not project:
555
- raise ResourceNotFoundError("project", project_id)
556
-
557
- version = next((v for v in project.versions if v.no == version_no), None)
558
- if not version:
559
- raise ResourceNotFoundError("version", version_no)
560
-
561
- # Unpublish other versions
562
- for v in project.versions:
563
- if v.published and v.no != version_no:
564
- v.published = False
565
-
566
- # Publish this version
567
- version.published = True
568
- version.publish_date = get_current_timestamp()
569
- version.published_by = username
570
-
571
- # Update project
572
- project.last_update_date = get_current_timestamp()
573
- project.last_update_user = username
574
-
575
- # Log activity
576
- cls._add_activity(
577
- config, username, "PUBLISH_VERSION",
578
- "version", version.no, f"{project.name} v{version.no}",
579
- f"Published version {version.no}"
580
- )
581
-
582
- # Save
583
- cls.save(config, username)
584
-
585
- log_info(
586
- "Version published",
587
- project_id=project.id,
588
- version_no=version.no,
589
- user=username
590
- )
591
-
592
- return project, version
593
-
594
- @classmethod
595
- def update_version(cls, project_id: int, version_no: int, update_data: dict, username: str, expected_last_update: Optional[str] = None) -> VersionConfig:
596
- """Update version with optimistic locking"""
597
- with cls._lock:
598
- config = cls.get()
599
- project = cls.get_project(project_id)
600
-
601
- if not project:
602
- raise ResourceNotFoundError("project", project_id)
603
-
604
- version = next((v for v in project.versions if v.no == version_no), None)
605
- if not version:
606
- raise ResourceNotFoundError("version", version_no)
607
-
608
- # Published versions cannot be edited
609
- if version.published:
610
- raise ValidationError("Published versions cannot be modified")
611
-
612
- # Check race condition
613
- if expected_last_update is not None and expected_last_update != '':
614
- if version.last_update_date and not timestamps_equal(expected_last_update, version.last_update_date):
615
- raise RaceConditionError(
616
- f"Version '{version.no}' was modified by another user",
617
- current_user=username,
618
- last_update_user=version.last_update_user,
619
- last_update_date=version.last_update_date,
620
- entity_type="version",
621
- entity_id=f"{project_id}:{version_no}"
622
- )
623
-
624
- # Update fields
625
- for key, value in update_data.items():
626
- if hasattr(version, key) and key not in ['no', 'created_date', 'created_by', 'published', 'last_update_date']:
627
- setattr(version, key, value)
628
-
629
- version.last_update_date = get_current_timestamp()
630
- version.last_update_user = username
631
-
632
- # Update project last update
633
- project.last_update_date = get_current_timestamp()
634
- project.last_update_user = username
635
-
636
- # Log activity
637
- cls._add_activity(
638
- config, username, "UPDATE_VERSION",
639
- "version", f"{project.id}:{version.no}", f"{project.name} v{version.no}"
640
- )
641
-
642
- # Save
643
- cls.save(config, username)
644
-
645
- log_info(
646
- "Version updated",
647
- project_id=project.id,
648
- version_no=version.no,
649
- user=username
650
- )
651
-
652
- return version
653
 
654
- @classmethod
655
- def delete_version(cls, project_id: int, version_no: int, username: str) -> None:
656
- """Soft delete version"""
657
- with cls._lock:
658
- config = cls.get()
659
- project = cls.get_project(project_id)
660
-
661
- if not project:
662
- raise ResourceNotFoundError("project", project_id)
663
-
664
- version = next((v for v in project.versions if v.no == version_no), None)
665
- if not version:
666
- raise ResourceNotFoundError("version", version_no)
667
-
668
- if version.published:
669
- raise ValidationError("Cannot delete published version")
670
-
671
- version.deleted = True
672
- version.last_update_date = get_current_timestamp()
673
- version.last_update_user = username
674
-
675
- # Update project
676
- project.last_update_date = get_current_timestamp()
677
- project.last_update_user = username
678
-
679
- # Log activity
680
- cls._add_activity(
681
- config, username, "DELETE_VERSION",
682
- "version", f"{project.id}:{version.no}", f"{project.name} v{version.no}"
683
- )
684
-
685
- # Save
686
- cls.save(config, username)
687
-
688
- log_info(
689
- "Version deleted",
690
- project_id=project.id,
691
- version_no=version.no,
692
- user=username
693
- )
694
 
695
- # ===================== API Methods =====================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
 
697
- @classmethod
698
- def create_api(cls, api_data: dict, username: str) -> APIConfig:
699
- """Create new API"""
700
- with cls._lock:
701
- config = cls.get()
702
-
703
- # Check for duplicate name
704
- if any(a.name == api_data['name'] for a in config.apis):
705
- raise DuplicateResourceError("api", api_data['name'])
706
-
707
- # Create API
708
- api = APIConfig(
709
- created_date=get_current_timestamp(),
710
- created_by=username,
711
- **api_data
712
- )
713
-
714
- # Add to config
715
- config.apis.append(api)
716
-
717
- # Rebuild index
718
- config.build_index()
719
-
720
- # Log activity
721
- cls._add_activity(
722
- config, username, "CREATE_API",
723
- "api", api.name, api.name
724
- )
725
-
726
- # Save
727
- cls.save(config, username)
728
-
729
- log_info(
730
- "API created",
731
- api_name=api.name,
732
- user=username
733
- )
734
-
735
- return api
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
736
 
737
- @classmethod
738
- def update_api(cls, api_name: str, update_data: dict, username: str, expected_last_update: Optional[str] = None) -> APIConfig:
739
- """Update API with optimistic locking"""
740
- with cls._lock:
741
- config = cls.get()
742
- api = config.get_api(api_name)
743
-
744
- if not api:
745
- raise ResourceNotFoundError("api", api_name)
746
-
747
- # Check race condition
748
- if expected_last_update is not None and expected_last_update != '':
749
- if api.last_update_date and not timestamps_equal(expected_last_update, api.last_update_date):
750
- raise RaceConditionError(
751
- f"API '{api.name}' was modified by another user",
752
- current_user=username,
753
- last_update_user=api.last_update_user,
754
- last_update_date=api.last_update_date,
755
- entity_type="api",
756
- entity_id=api.name
757
- )
758
-
759
- # Update fields
760
- for key, value in update_data.items():
761
- if hasattr(api, key) and key not in ['name', 'created_date', 'created_by', 'last_update_date']:
762
- setattr(api, key, value)
763
-
764
- api.last_update_date = get_current_timestamp()
765
- api.last_update_user = username
766
-
767
- # Rebuild index
768
- config.build_index()
769
-
770
- # Log activity
771
- cls._add_activity(
772
- config, username, "UPDATE_API",
773
- "api", api.name, api.name
774
- )
775
-
776
- # Save
777
- cls.save(config, username)
778
-
779
- log_info(
780
- "API updated",
781
- api_name=api.name,
782
- user=username
783
- )
784
-
785
- return api
786
 
787
- @classmethod
788
- def delete_api(cls, api_name: str, username: str) -> None:
789
- """Soft delete API"""
790
- with cls._lock:
791
- config = cls.get()
792
- api = config.get_api(api_name)
793
-
794
- if not api:
795
- raise ResourceNotFoundError("api", api_name)
796
-
797
- api.deleted = True
798
- api.last_update_date = get_current_timestamp()
799
- api.last_update_user = username
800
-
801
- # Rebuild index
802
- config.build_index()
803
-
804
- # Log activity
805
- cls._add_activity(
806
- config, username, "DELETE_API",
807
- "api", api.name, api.name
808
- )
809
-
810
- # Save
811
- cls.save(config, username)
812
-
813
- log_info(
814
- "API deleted",
815
- api_name=api.name,
816
- user=username
817
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
818
 
819
- # ===================== Activity Methods =====================
820
- @classmethod
821
- def _add_activity(
822
- cls,
823
- config: ServiceConfig,
824
- username: str,
825
- action: str,
826
- entity_type: str,
827
- entity_id: Any,
828
- entity_name: Optional[str] = None,
829
- details: Optional[str] = None
830
- ) -> None:
831
- """Add activity log entry"""
832
- # Activity ID'sini oluştur - mevcut en yüksek ID'yi bul
833
- max_id = 0
834
- if config.activity_log:
835
- max_id = max((entry.id for entry in config.activity_log if entry.id), default=0)
836
-
837
- activity_id = max_id + 1
838
-
839
- activity = ActivityLogEntry(
840
- id=activity_id,
841
- timestamp=get_current_timestamp(), # utils'ten import etmeyi unutma
842
- username=username,
843
- action=action,
844
- entity_type=entity_type,
845
- entity_id=str(entity_id) if entity_id else None,
846
- entity_name=entity_name,
847
- details=details
848
- )
849
-
850
- config.activity_log.append(activity)
851
-
852
- # Keep only last 1000 entries
853
- if len(config.activity_log) > 1000:
854
- config.activity_log = config.activity_log[-1000:]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, Inject, OnInit } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormBuilder, FormGroup, Validators, ReactiveFormsModule, FormArray, FormsModule } from '@angular/forms';
4
+ import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule, MatDialog } from '@angular/material/dialog';
5
+ import { MatTabsModule } from '@angular/material/tabs';
6
+ import { MatFormFieldModule } from '@angular/material/form-field';
7
+ import { MatInputModule } from '@angular/material/input';
8
+ import { MatSelectModule } from '@angular/material/select';
9
+ import { MatCheckboxModule } from '@angular/material/checkbox';
10
+ import { MatButtonModule } from '@angular/material/button';
11
+ import { MatIconModule } from '@angular/material/icon';
12
+ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
13
+ import { MatTableModule } from '@angular/material/table';
14
+ import { MatChipsModule } from '@angular/material/chips';
15
+ import { MatExpansionModule } from '@angular/material/expansion';
16
+ import { MatDividerModule } from '@angular/material/divider';
17
+ import { MatProgressBarModule } from '@angular/material/progress-bar';
18
+ import { MatListModule } from '@angular/material/list';
19
+ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
20
+ import { MatBadgeModule } from '@angular/material/badge';
21
+ import { ApiService, Project, Version } from '../../services/api.service';
22
+ import { LocaleManagerService } from '../../services/locale-manager.service';
23
+ import ConfirmDialogComponent from '../confirm-dialog/confirm-dialog.component';
24
+
25
+ // Interfaces for multi-language support
26
+ interface LocalizedExample {
27
+ locale_code: string;
28
+ example: string;
29
+ }
30
+
31
+ interface LocalizedCaption {
32
+ locale_code: string;
33
+ caption: string;
34
+ }
35
+
36
+ interface Locale {
37
+ code: string;
38
+ name: string;
39
+ }
40
+
41
+ @Component({
42
+ selector: 'app-version-edit-dialog',
43
+ standalone: true,
44
+ imports: [
45
+ CommonModule,
46
+ ReactiveFormsModule,
47
+ FormsModule,
48
+ MatDialogModule,
49
+ MatTabsModule,
50
+ MatFormFieldModule,
51
+ MatInputModule,
52
+ MatSelectModule,
53
+ MatCheckboxModule,
54
+ MatButtonModule,
55
+ MatIconModule,
56
+ MatSnackBarModule,
57
+ MatTableModule,
58
+ MatChipsModule,
59
+ MatExpansionModule,
60
+ MatDividerModule,
61
+ MatProgressBarModule,
62
+ MatListModule,
63
+ MatProgressSpinnerModule,
64
+ MatBadgeModule
65
+ ],
66
+ templateUrl: './version-edit-dialog.component.html',
67
+ styleUrls: ['./version-edit-dialog.component.scss']
68
+ })
69
+ export default class VersionEditDialogComponent implements OnInit {
70
+ project: Project;
71
+ versions: Version[] = [];
72
+ selectedVersion: Version | null = null;
73
+ versionForm!: FormGroup;
74
+
75
+ loading = false;
76
+ saving = false;
77
+ publishing = false;
78
+ creating = false;
79
+ isDirty = false;
80
+ testing = false;
81
+
82
+ selectedTabIndex = 0;
83
+ testUserMessage = '';
84
+ testResult: any = null;
85
+
86
+ // Multi-language support
87
+ selectedExampleLocale: string = 'tr';
88
+ availableLocales: Locale[] = [];
89
+
90
+ constructor(
91
+ private fb: FormBuilder,
92
+ private apiService: ApiService,
93
+ private localeService: LocaleManagerService,
94
+ private snackBar: MatSnackBar,
95
+ private dialog: MatDialog,
96
+ public dialogRef: MatDialogRef<VersionEditDialogComponent>,
97
+ @Inject(MAT_DIALOG_DATA) public data: any
98
+ ) {
99
+ this.project = data.project;
100
+ this.versions = [...this.project.versions].sort((a, b) => b.no - a.no);
101
+ this.selectedExampleLocale = this.project.default_locale || 'tr';
102
+ }
103
+
104
+ ngOnInit() {
105
+ this.initializeForm();
106
+ this.loadAvailableLocales();
107
 
108
+ // Select the latest unpublished version or the latest version
109
+ const unpublished = this.versions.find(v => !v.published);
110
+ this.selectedVersion = unpublished || this.versions[0] || null;
 
 
 
 
111
 
112
+ if (this.selectedVersion) {
113
+ this.loadVersion(this.selectedVersion);
114
+ }
115
+
116
+ this.versionForm.valueChanges.subscribe(() => {
117
+ this.isDirty = true;
118
+ });
119
+ }
120
+
121
+ initializeForm() {
122
+ this.versionForm = this.fb.group({
123
+ no: [{value: '', disabled: true}],
124
+ caption: ['', Validators.required],
125
+ published: [{value: false, disabled: true}],
126
+ general_prompt: ['', Validators.required],
127
+ welcome_prompt: [''], // Added welcome_prompt field
128
+ llm: this.fb.group({
129
+ repo_id: ['', Validators.required],
130
+ generation_config: this.fb.group({
131
+ max_new_tokens: [256, [Validators.required, Validators.min(1), Validators.max(2048)]],
132
+ temperature: [0.2, [Validators.required, Validators.min(0), Validators.max(2)]],
133
+ top_p: [0.8, [Validators.required, Validators.min(0), Validators.max(1)]],
134
+ repetition_penalty: [1.1, [Validators.required, Validators.min(1), Validators.max(2)]]
135
+ }),
136
+ use_fine_tune: [false],
137
+ fine_tune_zip: ['']
138
+ }),
139
+ intents: this.fb.array([]),
140
+ last_update_date: ['']
141
+ });
142
+
143
+ // Watch for fine-tune toggle
144
+ this.versionForm.get('llm.use_fine_tune')?.valueChanges.subscribe(useFineTune => {
145
+ const fineTuneControl = this.versionForm.get('llm.fine_tune_zip');
146
+ if (useFineTune) {
147
+ fineTuneControl?.setValidators([Validators.required]);
148
+ } else {
149
+ fineTuneControl?.clearValidators();
150
+ fineTuneControl?.setValue('');
151
+ }
152
+ fineTuneControl?.updateValueAndValidity();
153
+ });
154
+ }
155
+
156
+ async loadAvailableLocales() {
157
+ // Get supported locales from project
158
+ const supportedCodes = [
159
+ this.project.default_locale,
160
+ ...(this.project.supported_locales || [])
161
+ ].filter(Boolean);
162
+
163
+ // Get locale details
164
+ for (const code of supportedCodes) {
165
+ if (!code) continue; // Skip undefined/null values
166
+
167
+ try {
168
+ const localeInfo = await this.localeService.getLocaleDetails(code).toPromise();
169
+ if (localeInfo) {
170
+ this.availableLocales.push({
171
+ code: localeInfo.code,
172
+ name: localeInfo.name
173
+ });
174
+ }
175
+ } catch (error) {
176
+ // Use fallback for known locales
177
+ const fallbackNames: { [key: string]: string } = {
178
+ 'tr': 'Türkçe',
179
+ 'en': 'English',
180
+ 'de': 'Deutsch',
181
+ 'fr': 'Français',
182
+ 'es': 'Español'
183
+ };
184
+ if (code && fallbackNames[code]) {
185
+ this.availableLocales.push({
186
+ code: code,
187
+ name: fallbackNames[code]
188
+ });
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ getAvailableLocales(): Locale[] {
195
+ return this.availableLocales;
196
+ }
197
+
198
+ getLocaleName(localeCode: string): string {
199
+ const locale = this.availableLocales.find(l => l.code === localeCode);
200
+ return locale?.name || localeCode;
201
+ }
202
+
203
+ loadVersion(version: Version) {
204
+ this.selectedVersion = version;
205
 
206
+ // Form değerlerini set et
207
+ this.versionForm.patchValue({
208
+ no: version.no,
209
+ caption: version.caption || '',
210
+ published: version.published || false,
211
+ general_prompt: (version as any).general_prompt || '',
212
+ welcome_prompt: (version as any).welcome_prompt || '', // Added welcome_prompt
213
+ last_update_date: version.last_update_date || ''
214
+ });
215
+
216
+ // LLM config'i ayrı set et
217
+ if ((version as any).llm) {
218
+ this.versionForm.patchValue({
219
+ llm: {
220
+ repo_id: (version as any).llm.repo_id || '',
221
+ generation_config: (version as any).llm.generation_config || {
222
+ max_new_tokens: 512,
223
+ temperature: 0.7,
224
+ top_p: 0.95,
225
+ repetition_penalty: 1.1
226
+ },
227
+ use_fine_tune: (version as any).llm.use_fine_tune || false,
228
+ fine_tune_zip: (version as any).llm.fine_tune_zip || ''
229
+ }
230
+ });
231
+ }
232
+
233
+ // Clear and rebuild intents
234
+ this.intents.clear();
235
+ ((version as any).intents || []).forEach((intent: any) => {
236
+ this.intents.push(this.createIntentFormGroup(intent));
237
+ });
238
+
239
+ this.isDirty = false;
240
+ }
241
+
242
+ async loadVersions() {
243
+ this.loading = true;
244
+ try {
245
+ const project = await this.apiService.getProject(this.project.id).toPromise();
246
+ if (project) {
247
+ this.project = project;
248
+ this.versions = [...project.versions].sort((a, b) => b.no - a.no);
249
 
250
+ // Re-select current version if it still exists
251
+ if (this.selectedVersion) {
252
+ const currentVersion = this.versions.find(v => v.no === this.selectedVersion!.no);
253
+ if (currentVersion) {
254
+ this.loadVersion(currentVersion);
255
+ } else if (this.versions.length > 0) {
256
+ this.loadVersion(this.versions[0]);
257
+ }
258
+ } else if (this.versions.length > 0) {
259
+ this.loadVersion(this.versions[0]);
260
+ }
261
+ }
262
+ } catch (error) {
263
+ this.snackBar.open('Failed to reload versions', 'Close', { duration: 3000 });
264
+ } finally {
265
+ this.loading = false;
266
+ }
267
+ }
268
+
269
+ createIntentFormGroup(intent: any = {}): FormGroup {
270
+ const group = this.fb.group({
271
+ name: [intent.name || '', [Validators.required, Validators.pattern(/^[a-zA-Z0-9-]+$/)]],
272
+ caption: [intent.caption || ''],
273
+ detection_prompt: [intent.detection_prompt || '', Validators.required],
274
+ examples: [intent.examples || []], // Store as array, not FormArray
275
+ parameters: this.fb.array([]),
276
+ action: [intent.action || '', Validators.required],
277
+ fallback_timeout_prompt: [intent.fallback_timeout_prompt || ''],
278
+ fallback_error_prompt: [intent.fallback_error_prompt || '']
279
+ });
280
+
281
+ // Parameters'ı ayrı olarak ekle
282
+ if (intent.parameters && Array.isArray(intent.parameters)) {
283
+ const parametersArray = group.get('parameters') as FormArray;
284
+ intent.parameters.forEach((param: any) => {
285
+ parametersArray.push(this.createParameterFormGroup(param));
286
+ });
287
+ }
288
+
289
+ return group;
290
+ }
291
+
292
+ createParameterFormGroup(param: any = {}): FormGroup {
293
+ return this.fb.group({
294
+ name: [param.name || '', [Validators.required, Validators.pattern(/^[a-zA-Z0-9_]+$/)]],
295
+ caption: [param.caption || []],
296
+ type: [param.type || 'str', Validators.required],
297
+ required: [param.required !== false],
298
+ variable_name: [param.variable_name || '', Validators.required],
299
+ extraction_prompt: [param.extraction_prompt || ''],
300
+ validation_regex: [param.validation_regex || ''],
301
+ invalid_prompt: [param.invalid_prompt || ''],
302
+ type_error_prompt: [param.type_error_prompt || '']
303
+ });
304
+ }
305
+
306
+ get intents() {
307
+ return this.versionForm.get('intents') as FormArray;
308
+ }
309
+
310
+ getIntentParameters(intentIndex: number): FormArray {
311
+ return this.intents.at(intentIndex).get('parameters') as FormArray;
312
+ }
313
+
314
+ // LocalizedExample support methods
315
+ getLocalizedExamples(examples: any[], locale: string): LocalizedExample[] {
316
+ if (!examples || !Array.isArray(examples)) return [];
317
 
318
+ // Check if examples are in new format
319
+ if (examples.length > 0 && typeof examples[0] === 'object' && 'locale_code' in examples[0]) {
320
+ return examples.filter(ex => ex.locale_code === locale);
321
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
 
323
+ // Old format - convert to new
324
+ if (typeof examples[0] === 'string') {
325
+ return examples.map(ex => ({ locale_code: locale, example: ex }));
326
+ }
327
 
328
+ return [];
329
+ }
330
+
331
+ getParameterCaptionDisplay(captions: LocalizedCaption[]): string {
332
+ if (!captions || !Array.isArray(captions) || captions.length === 0) {
333
+ return '(No caption)';
334
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
 
336
+ // Try to find caption for selected locale
337
+ const selectedCaption = captions.find(c => c.locale_code === this.selectedExampleLocale);
338
+ if (selectedCaption) return selectedCaption.caption;
339
 
340
+ // Try default locale
341
+ const defaultCaption = captions.find(c => c.locale_code === this.project.default_locale);
342
+ if (defaultCaption) return defaultCaption.caption;
 
 
343
 
344
+ // Return first available caption
345
+ return captions[0].caption;
346
+ }
347
+
348
+ addLocalizedExample(intentIndex: number, example: string) {
349
+ if (!example.trim()) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
 
351
+ const intent = this.intents.at(intentIndex);
352
+ const currentExamples = intent.get('examples')?.value || [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
+ // Check if already exists
355
+ const exists = currentExamples.some((ex: any) =>
356
+ ex.locale_code === this.selectedExampleLocale && ex.example === example.trim()
357
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
 
359
+ if (!exists) {
360
+ const newExamples = [...currentExamples, {
361
+ locale_code: this.selectedExampleLocale,
362
+ example: example.trim()
363
+ }];
364
+ intent.patchValue({ examples: newExamples });
365
+ this.isDirty = true;
366
+ }
367
+ }
368
+
369
+ removeLocalizedExample(intentIndex: number, exampleToRemove: LocalizedExample) {
370
+ const intent = this.intents.at(intentIndex);
371
+ const currentExamples = intent.get('examples')?.value || [];
372
 
373
+ const newExamples = currentExamples.filter((ex: any) =>
374
+ !(ex.locale_code === exampleToRemove.locale_code && ex.example === exampleToRemove.example)
375
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
 
377
+ intent.patchValue({ examples: newExamples });
378
+ this.isDirty = true;
379
+ }
380
+
381
+ addParameter(intentIndex: number) {
382
+ const parameters = this.getIntentParameters(intentIndex);
383
+ parameters.push(this.createParameterFormGroup());
384
+ this.isDirty = true;
385
+ }
386
+
387
+ removeParameter(intentIndex: number, paramIndex: number) {
388
+ const parameters = this.getIntentParameters(intentIndex);
389
+ parameters.removeAt(paramIndex);
390
+ this.isDirty = true;
391
+ }
392
+
393
+ // Check if version can be edited
394
+ get canEdit(): boolean {
395
+ return !this.selectedVersion?.published;
396
+ }
397
+
398
+ addIntent() {
399
+ this.intents.push(this.createIntentFormGroup());
400
+ this.isDirty = true;
401
+ }
402
+
403
+ removeIntent(index: number) {
404
+ const intent = this.intents.at(index).value;
405
+ const dialogRef = this.dialog.open(ConfirmDialogComponent, {
406
+ width: '400px',
407
+ data: {
408
+ title: 'Delete Intent',
409
+ message: `Are you sure you want to delete intent "${intent.name}"?`,
410
+ confirmText: 'Delete',
411
+ confirmColor: 'warn'
412
+ }
413
+ });
414
+
415
+ dialogRef.afterClosed().subscribe(confirmed => {
416
+ if (confirmed) {
417
+ this.intents.removeAt(index);
418
+ this.isDirty = true;
419
+ }
420
+ });
421
+ }
422
+
423
+ async editIntent(intentIndex: number) {
424
+ const { default: IntentEditDialogComponent } = await import('../intent-edit-dialog/intent-edit-dialog.component');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
 
426
+ const intent = this.intents.at(intentIndex);
427
+ const currentValue = intent.value;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
 
429
+ // Intent verilerini dialog'a gönder
430
+ const dialogRef = this.dialog.open(IntentEditDialogComponent, {
431
+ width: '90vw',
432
+ maxWidth: '1000px',
433
+ data: {
434
+ intent: {
435
+ ...currentValue,
436
+ examples: currentValue.examples || [],
437
+ parameters: currentValue.parameters || []
438
+ },
439
+ project: this.project,
440
+ apis: await this.getAvailableAPIs()
441
+ }
442
+ });
443
+
444
+ dialogRef.afterClosed().subscribe(result => {
445
+ if (result) {
446
+ // Update intent with result
447
+ intent.patchValue({
448
+ name: result.name,
449
+ caption: result.caption,
450
+ detection_prompt: result.detection_prompt,
451
+ examples: result.examples || [],
452
+ action: result.action,
453
+ fallback_timeout_prompt: result.fallback_timeout_prompt,
454
+ fallback_error_prompt: result.fallback_error_prompt
455
+ });
456
+
457
+ // Update parameters
458
+ const parametersArray = intent.get('parameters') as FormArray;
459
+ parametersArray.clear();
460
+ (result.parameters || []).forEach((param: any) => {
461
+ parametersArray.push(this.createParameterFormGroup(param));
462
+ });
463
+
464
+ this.isDirty = true;
465
+ }
466
+ });
467
+ }
468
+
469
+ async getAvailableAPIs(): Promise<any[]> {
470
+ try {
471
+ return await this.apiService.getAPIs().toPromise() || [];
472
+ } catch {
473
+ return [];
474
+ }
475
+ }
476
+
477
+ async createVersion() {
478
+ const publishedVersions = this.versions.filter(v => v.published);
479
 
480
+ const dialogRef = this.dialog.open(ConfirmDialogComponent, {
481
+ width: '500px',
482
+ data: {
483
+ title: 'Create New Version',
484
+ message: 'Which published version would you like to use as a base for the new version?',
485
+ showDropdown: true,
486
+ dropdownOptions: publishedVersions.map(v => ({
487
+ value: v.no,
488
+ label: `Version ${v.no} - ${v.caption || 'No description'}`
489
+ })),
490
+ dropdownPlaceholder: 'Select published version (or leave empty for blank)',
491
+ confirmText: 'Create',
492
+ cancelText: 'Cancel'
493
+ }
494
+ });
495
+
496
+ dialogRef.afterClosed().subscribe(async (result) => {
497
+ if (result?.confirmed) {
498
+ this.creating = true;
499
+ try {
500
+ let newVersionData;
501
+
502
+ if (result.selectedValue) {
503
+ // Copy from selected version - we need to get the full version data
504
+ const sourceVersion = this.versions.find(v => v.no === result.selectedValue);
505
+ if (sourceVersion) {
506
+ // Load the full version data from the current form if it's the selected version
507
+ if (sourceVersion.no === this.selectedVersion?.no) {
508
+ const formValue = this.versionForm.getRawValue();
509
+ newVersionData = {
510
+ ...formValue,
511
+ no: undefined,
512
+ published: false,
513
+ last_update_date: undefined,
514
+ caption: `Copy of ${sourceVersion.caption || `version ${sourceVersion.no}`}`
515
+ };
516
+ } else {
517
+ // For other versions, we only have basic info, so create minimal copy
518
+ newVersionData = {
519
+ caption: `Copy of ${sourceVersion.caption || `version ${sourceVersion.no}`}`,
520
+ general_prompt: '',
521
+ llm: {
522
+ repo_id: '',
523
+ generation_config: {
524
+ max_new_tokens: 512,
525
+ temperature: 0.7,
526
+ top_p: 0.95,
527
+ repetition_penalty: 1.1
528
+ },
529
+ use_fine_tune: false,
530
+ fine_tune_zip: ''
531
+ },
532
+ intents: []
533
+ };
534
+ }
535
+ }
536
+ } else {
537
+ // Create blank version
538
+ newVersionData = {
539
+ caption: `Version ${this.versions.length + 1}`,
540
+ general_prompt: '',
541
+ llm: {
542
+ repo_id: '',
543
+ generation_config: {
544
+ max_new_tokens: 512,
545
+ temperature: 0.7,
546
+ top_p: 0.95,
547
+ repetition_penalty: 1.1
548
+ },
549
+ use_fine_tune: false,
550
+ fine_tune_zip: ''
551
+ },
552
+ intents: []
553
+ };
554
+ }
555
+
556
+ if (newVersionData) {
557
+ await this.apiService.createVersion(this.project.id, newVersionData).toPromise();
558
+ await this.loadVersions();
559
+ this.snackBar.open('Version created successfully!', 'Close', { duration: 3000 });
560
+ }
561
+ } catch (error) {
562
+ this.snackBar.open('Failed to create version', 'Close', { duration: 3000 });
563
+ } finally {
564
+ this.creating = false;
565
+ }
566
+ }
567
+ });
568
+ }
569
+
570
+ async saveVersion() {
571
+ if (!this.selectedVersion || !this.canEdit) {
572
+ this.snackBar.open('Cannot save published version', 'Close', { duration: 3000 });
573
+ return;
574
+ }
575
 
576
+ if (this.versionForm.invalid) {
577
+ const invalidFields: string[] = [];
578
+ Object.keys(this.versionForm.controls).forEach(key => {
579
+ const control = this.versionForm.get(key);
580
+ if (control && control.invalid) {
581
+ invalidFields.push(key);
582
+ }
583
+ });
584
+
585
+ this.intents.controls.forEach((intent, index) => {
586
+ if (intent.invalid) {
587
+ invalidFields.push(`Intent ${index + 1}`);
588
+ }
589
+ });
590
+
591
+ this.snackBar.open(`Please fix validation errors in: ${invalidFields.join(', ')}`, 'Close', {
592
+ duration: 5000
593
+ });
594
+ return;
595
+ }
596
+
597
+ const currentVersion = this.selectedVersion!;
598
+
599
+ this.saving = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
600
 
601
+ try {
602
+ const formValue = this.versionForm.getRawValue();
603
+
604
+ // updateData'yı backend'in beklediği formatta hazırla
605
+ const updateData = {
606
+ caption: formValue.caption,
607
+ general_prompt: formValue.general_prompt || '',
608
+ welcome_prompt: formValue.welcome_prompt || '', // Added welcome_prompt
609
+ llm: formValue.llm,
610
+ intents: formValue.intents.map((intent: any) => ({
611
+ name: intent.name,
612
+ caption: intent.caption,
613
+ detection_prompt: intent.detection_prompt,
614
+ examples: Array.isArray(intent.examples) ? intent.examples : [],
615
+ parameters: Array.isArray(intent.parameters) ? intent.parameters.map((param: any) => ({
616
+ name: param.name,
617
+ caption: param.caption,
618
+ type: param.type,
619
+ required: param.required,
620
+ variable_name: param.variable_name,
621
+ extraction_prompt: param.extraction_prompt,
622
+ validation_regex: param.validation_regex,
623
+ invalid_prompt: param.invalid_prompt,
624
+ type_error_prompt: param.type_error_prompt
625
+ })) : [],
626
+ action: intent.action,
627
+ fallback_timeout_prompt: intent.fallback_timeout_prompt,
628
+ fallback_error_prompt: intent.fallback_error_prompt
629
+ })),
630
+ last_update_date: currentVersion.last_update_date || ''
631
+ };
632
+
633
+ console.log('Saving version data:', JSON.stringify(updateData, null, 2));
634
+
635
+ const result = await this.apiService.updateVersion(
636
+ this.project.id,
637
+ currentVersion.no,
638
+ updateData
639
+ ).toPromise();
640
+
641
+ this.snackBar.open('Version saved successfully', 'Close', { duration: 3000 });
642
+
643
+ this.isDirty = false;
644
+
645
+ if (result) {
646
+ this.selectedVersion = result;
647
+ this.versionForm.patchValue({
648
+ last_update_date: result.last_update_date
649
+ });
650
+ }
651
+
652
+ await this.loadVersions();
653
+
654
+ } catch (error: any) {
655
+ console.error('Save error:', error);
656
+
657
+ if (error.status === 409) {
658
+ // Race condition handling
659
+ await this.handleRaceCondition(currentVersion);
660
+ } else if (error.status === 400 && error.error?.detail?.includes('Published versions')) {
661
+ this.snackBar.open('Published versions cannot be modified. Create a new version instead.', 'Close', {
662
+ duration: 5000,
663
+ panelClass: 'error-snackbar'
664
+ });
665
+ } else {
666
+ const errorMessage = error.error?.detail || error.message || 'Failed to save version';
667
+ this.snackBar.open(errorMessage, 'Close', {
668
+ duration: 5000,
669
+ panelClass: 'error-snackbar'
670
+ });
671
+ }
672
+ } finally {
673
+ this.saving = false;
674
+ }
675
+ }
676
+
677
+ // Race condition handling
678
+ private async handleRaceCondition(currentVersion: Version) {
679
+ const formValue = this.versionForm.getRawValue();
680
 
681
+ const retryUpdateData = {
682
+ caption: formValue.caption,
683
+ general_prompt: formValue.general_prompt || '',
684
+ llm: formValue.llm,
685
+ intents: formValue.intents.map((intent: any) => ({
686
+ name: intent.name,
687
+ caption: intent.caption,
688
+ detection_prompt: intent.detection_prompt,
689
+ examples: Array.isArray(intent.examples) ? intent.examples : [],
690
+ parameters: Array.isArray(intent.parameters) ? intent.parameters : [],
691
+ action: intent.action,
692
+ fallback_timeout_prompt: intent.fallback_timeout_prompt,
693
+ fallback_error_prompt: intent.fallback_error_prompt
694
+ })),
695
+ last_update_date: currentVersion.last_update_date || ''
696
+ };
697
+
698
+ const dialogRef = this.dialog.open(ConfirmDialogComponent, {
699
+ width: '500px',
700
+ data: {
701
+ title: 'Version Modified',
702
+ message: 'This version was modified by another user. Do you want to reload and lose your changes, or force save?',
703
+ confirmText: 'Force Save',
704
+ cancelText: 'Reload',
705
+ confirmColor: 'warn'
706
+ }
707
+ });
708
+
709
+ dialogRef.afterClosed().subscribe(async (forceSave) => {
710
+ if (forceSave) {
711
+ try {
712
+ await this.apiService.updateVersion(
713
+ this.project.id,
714
+ currentVersion.no,
715
+ retryUpdateData,
716
+ true
717
+ ).toPromise();
718
+ this.snackBar.open('Version force saved', 'Close', { duration: 3000 });
719
+ await this.loadVersions();
720
+ } catch (err: any) {
721
+ this.snackBar.open(err.error?.detail || 'Force save failed', 'Close', {
722
+ duration: 5000,
723
+ panelClass: 'error-snackbar'
724
+ });
725
+ }
726
+ } else {
727
+ await this.loadVersions();
728
+ }
729
+ });
730
+ }
731
+
732
+ async publishVersion() {
733
+ if (!this.selectedVersion) return;
734
+
735
+ const dialogRef = this.dialog.open(ConfirmDialogComponent, {
736
+ width: '500px',
737
+ data: {
738
+ title: 'Publish Version',
739
+ message: `Are you sure you want to publish version "${this.selectedVersion.caption}"? This will unpublish all other versions.`,
740
+ confirmText: 'Publish',
741
+ confirmColor: 'primary'
742
+ }
743
+ });
744
+
745
+ dialogRef.afterClosed().subscribe(async (confirmed) => {
746
+ if (confirmed && this.selectedVersion) {
747
+ this.publishing = true;
748
+ try {
749
+ await this.apiService.publishVersion(
750
+ this.project.id,
751
+ this.selectedVersion.no
752
+ ).toPromise();
753
+
754
+ this.snackBar.open('Version published successfully', 'Close', { duration: 3000 });
755
+
756
+ // Reload to get updated data
757
+ await this.reloadProject();
758
+
759
+ } catch (error: any) {
760
+ this.snackBar.open(error.error?.detail || 'Failed to publish version', 'Close', {
761
+ duration: 5000,
762
+ panelClass: 'error-snackbar'
763
+ });
764
+ } finally {
765
+ this.publishing = false;
766
+ }
767
+ }
768
+ });
769
+ }
770
+
771
+ async deleteVersion() {
772
+ if (!this.selectedVersion || this.selectedVersion.published) return;
773
+
774
+ const dialogRef = this.dialog.open(ConfirmDialogComponent, {
775
+ width: '400px',
776
+ data: {
777
+ title: 'Delete Version',
778
+ message: `Are you sure you want to delete version "${this.selectedVersion.caption}"?`,
779
+ confirmText: 'Delete',
780
+ confirmColor: 'warn'
781
+ }
782
+ });
783
+
784
+ dialogRef.afterClosed().subscribe(async (confirmed) => {
785
+ if (confirmed && this.selectedVersion) {
786
+ try {
787
+ await this.apiService.deleteVersion(
788
+ this.project.id,
789
+ this.selectedVersion.no
790
+ ).toPromise();
791
+
792
+ this.snackBar.open('Version deleted successfully', 'Close', { duration: 3000 });
793
+
794
+ // Reload and select another version
795
+ await this.reloadProject();
796
+
797
+ if (this.versions.length > 0) {
798
+ this.loadVersion(this.versions[0]);
799
+ } else {
800
+ this.selectedVersion = null;
801
+ }
802
+
803
+ } catch (error: any) {
804
+ this.snackBar.open(error.error?.detail || 'Failed to delete version', 'Close', {
805
+ duration: 5000,
806
+ panelClass: 'error-snackbar'
807
+ });
808
+ }
809
+ }
810
+ });
811
+ }
812
+
813
+ async testIntentDetection() {
814
+ if (!this.testUserMessage.trim()) {
815
+ this.snackBar.open('Please enter a test message', 'Close', { duration: 3000 });
816
+ return;
817
+ }
818
+
819
+ this.testing = true;
820
+ this.testResult = null;
821
+
822
+ // Simulate intent detection test
823
+ setTimeout(() => {
824
+ // This is a mock - in real implementation, this would call the Spark service
825
+ const intents = this.versionForm.get('intents')?.value || [];
826
+
827
+ // Simple matching for demo
828
+ let detectedIntent = null;
829
+ let confidence = 0;
830
+
831
+ for (const intent of intents) {
832
+ // Check examples in all locales
833
+ const allExamples = intent.examples || [];
834
+ for (const example of allExamples) {
835
+ const exampleText = typeof example === 'string' ? example : example.example;
836
+ if (this.testUserMessage.toLowerCase().includes(exampleText.toLowerCase())) {
837
+ detectedIntent = intent.name;
838
+ confidence = 0.95;
839
+ break;
840
+ }
841
+ }
842
+ if (detectedIntent) break;
843
+ }
844
+
845
+ // Random detection for demo
846
+ if (!detectedIntent && intents.length > 0) {
847
+ const randomIntent = intents[Math.floor(Math.random() * intents.length)];
848
+ detectedIntent