ciyidogan commited on
Commit
02bd90d
·
verified ·
1 Parent(s): 864cecb

Update config_provider.py

Browse files
Files changed (1) hide show
  1. config_provider.py +844 -827
config_provider.py CHANGED
@@ -1,848 +1,865 @@
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
 
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
+ last_update_date=None,
520
+ last_update_user=None,
521
+ publish_date=None,
522
+ published_by=None,
523
+ **version_dict
524
+ )
525
+
526
+ # Update project
527
+ project.versions.append(version)
528
+ project.version_id_counter += 1
529
+ project.last_update_date = get_current_timestamp()
530
+ project.last_update_user = username
531
+
532
+ # Log activity
533
+ cls._add_activity(
534
+ config, username, "CREATE_VERSION",
535
+ "version", version.no, f"{project.name} v{version.no}",
536
+ f"Project: {project.name}"
537
+ )
538
+
539
+ # Save
540
+ cls.save(config, username)
541
+
542
+ log_info(
543
+ "Version created",
544
+ project_id=project.id,
545
+ version_no=version.no,
546
+ user=username
547
+ )
548
+
549
+ return version
550
 
551
+ @classmethod
552
+ def publish_version(cls, project_id: int, version_no: int, username: str) -> tuple[ProjectConfig, VersionConfig]:
553
+ """Publish a version"""
554
+ with cls._lock:
555
+ config = cls.get()
556
+ project = cls.get_project(project_id)
557
+
558
+ if not project:
559
+ raise ResourceNotFoundError("project", project_id)
560
+
561
+ version = next((v for v in project.versions if v.no == version_no), None)
562
+ if not version:
563
+ raise ResourceNotFoundError("version", version_no)
564
+
565
+ # Unpublish other versions
566
+ for v in project.versions:
567
+ if v.published and v.no != version_no:
568
+ v.published = False
569
+
570
+ # Publish this version
571
+ version.published = True
572
+ version.publish_date = get_current_timestamp()
573
+ version.published_by = username
574
+
575
+ # Update project
576
+ project.last_update_date = get_current_timestamp()
577
+ project.last_update_user = username
578
+
579
+ # Log activity
580
+ cls._add_activity(
581
+ config, username, "PUBLISH_VERSION",
582
+ "version", version.no, f"{project.name} v{version.no}",
583
+ f"Published version {version.no}"
584
+ )
585
+
586
+ # Save
587
+ cls.save(config, username)
588
+
589
+ log_info(
590
+ "Version published",
591
+ project_id=project.id,
592
+ version_no=version.no,
593
+ user=username
594
+ )
595
+
596
+ return project, version
597
+
598
+ @classmethod
599
+ def update_version(cls, project_id: int, version_no: int, update_data: dict, username: str, expected_last_update: Optional[str] = None) -> VersionConfig:
600
+ """Update version with optimistic locking"""
601
+ with cls._lock:
602
+ config = cls.get()
603
+ project = cls.get_project(project_id)
604
+
605
+ if not project:
606
+ raise ResourceNotFoundError("project", project_id)
607
+
608
+ version = next((v for v in project.versions if v.no == version_no), None)
609
+ if not version:
610
+ raise ResourceNotFoundError("version", version_no)
611
+
612
+ # Debug: Log version published status
613
+ log_debug(f"🔍 Updating version {version_no} - published: {version.published}, type: {type(version.published)}")
614
+
615
+ # Ensure published is a boolean (safety check)
616
+ if version.published is None:
617
+ version.published = False
618
+
619
+ # Published versions cannot be edited
620
+ if version.published:
621
+ raise ValidationError("Published versions cannot be modified")
622
+
623
+ # Check race condition
624
+ if expected_last_update is not None and expected_last_update != '':
625
+ if version.last_update_date and not timestamps_equal(expected_last_update, version.last_update_date):
626
+ raise RaceConditionError(
627
+ f"Version '{version.no}' was modified by another user",
628
+ current_user=username,
629
+ last_update_user=version.last_update_user,
630
+ last_update_date=version.last_update_date,
631
+ entity_type="version",
632
+ entity_id=f"{project_id}:{version_no}"
633
+ )
634
+
635
+ # Update fields
636
+ for key, value in update_data.items():
637
+ if hasattr(version, key) and key not in ['no', 'created_date', 'created_by', 'published', 'last_update_date']:
638
+ setattr(version, key, value)
639
+
640
+ version.last_update_date = get_current_timestamp()
641
+ version.last_update_user = username
642
+
643
+ # Update project last update
644
+ project.last_update_date = get_current_timestamp()
645
+ project.last_update_user = username
646
+
647
+ # Log activity
648
+ cls._add_activity(
649
+ config, username, "UPDATE_VERSION",
650
+ "version", f"{project.id}:{version.no}", f"{project.name} v{version.no}"
651
+ )
652
+
653
+ # Save
654
+ cls.save(config, username)
655
+
656
+ log_info(
657
+ "Version updated",
658
+ project_id=project.id,
659
+ version_no=version.no,
660
+ user=username
661
+ )
662
+
663
+ return version
664
 
665
+ @classmethod
666
+ def delete_version(cls, project_id: int, version_no: int, username: str) -> None:
667
+ """Soft delete version"""
668
+ with cls._lock:
669
+ config = cls.get()
670
+ project = cls.get_project(project_id)
671
+
672
+ if not project:
673
+ raise ResourceNotFoundError("project", project_id)
674
+
675
+ version = next((v for v in project.versions if v.no == version_no), None)
676
+ if not version:
677
+ raise ResourceNotFoundError("version", version_no)
678
+
679
+ if version.published:
680
+ raise ValidationError("Cannot delete published version")
681
+
682
+ version.deleted = True
683
+ version.last_update_date = get_current_timestamp()
684
+ version.last_update_user = username
685
+
686
+ # Update project
687
+ project.last_update_date = get_current_timestamp()
688
+ project.last_update_user = username
689
+
690
+ # Log activity
691
+ cls._add_activity(
692
+ config, username, "DELETE_VERSION",
693
+ "version", f"{project.id}:{version.no}", f"{project.name} v{version.no}"
694
+ )
695
+
696
+ # Save
697
+ cls.save(config, username)
698
+
699
+ log_info(
700
+ "Version deleted",
701
+ project_id=project.id,
702
+ version_no=version.no,
703
+ user=username
704
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
705
 
706
+ # ===================== API Methods =====================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
707
 
708
+ @classmethod
709
+ def create_api(cls, api_data: dict, username: str) -> APIConfig:
710
+ """Create new API"""
711
+ with cls._lock:
712
+ config = cls.get()
713
+
714
+ # Check for duplicate name
715
+ if any(a.name == api_data['name'] for a in config.apis):
716
+ raise DuplicateResourceError("api", api_data['name'])
717
+
718
+ # Create API
719
+ api = APIConfig(
720
+ created_date=get_current_timestamp(),
721
+ created_by=username,
722
+ **api_data
723
+ )
724
+
725
+ # Add to config
726
+ config.apis.append(api)
727
+
728
+ # Rebuild index
729
+ config.build_index()
730
+
731
+ # Log activity
732
+ cls._add_activity(
733
+ config, username, "CREATE_API",
734
+ "api", api.name, api.name
735
+ )
736
+
737
+ # Save
738
+ cls.save(config, username)
739
+
740
+ log_info(
741
+ "API created",
742
+ api_name=api.name,
743
+ user=username
744
+ )
745
+
746
+ return api
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
747
 
748
+ @classmethod
749
+ def update_api(cls, api_name: str, update_data: dict, username: str, expected_last_update: Optional[str] = None) -> APIConfig:
750
+ """Update API with optimistic locking"""
751
+ with cls._lock:
752
+ config = cls.get()
753
+ api = config.get_api(api_name)
754
+
755
+ if not api:
756
+ raise ResourceNotFoundError("api", api_name)
757
+
758
+ # Check race condition
759
+ if expected_last_update is not None and expected_last_update != '':
760
+ if api.last_update_date and not timestamps_equal(expected_last_update, api.last_update_date):
761
+ raise RaceConditionError(
762
+ f"API '{api.name}' was modified by another user",
763
+ current_user=username,
764
+ last_update_user=api.last_update_user,
765
+ last_update_date=api.last_update_date,
766
+ entity_type="api",
767
+ entity_id=api.name
768
+ )
769
+
770
+ # Update fields
771
+ for key, value in update_data.items():
772
+ if hasattr(api, key) and key not in ['name', 'created_date', 'created_by', 'last_update_date']:
773
+ setattr(api, key, value)
774
+
775
+ api.last_update_date = get_current_timestamp()
776
+ api.last_update_user = username
777
+
778
+ # Rebuild index
779
+ config.build_index()
780
+
781
+ # Log activity
782
+ cls._add_activity(
783
+ config, username, "UPDATE_API",
784
+ "api", api.name, api.name
785
+ )
786
+
787
+ # Save
788
+ cls.save(config, username)
789
+
790
+ log_info(
791
+ "API updated",
792
+ api_name=api.name,
793
+ user=username
794
+ )
795
+
796
+ return api
797
+
798
+ @classmethod
799
+ def delete_api(cls, api_name: str, username: str) -> None:
800
+ """Soft delete API"""
801
+ with cls._lock:
802
+ config = cls.get()
803
+ api = config.get_api(api_name)
804
+
805
+ if not api:
806
+ raise ResourceNotFoundError("api", api_name)
807
+
808
+ api.deleted = True
809
+ api.last_update_date = get_current_timestamp()
810
+ api.last_update_user = username
811
+
812
+ # Rebuild index
813
+ config.build_index()
814
+
815
+ # Log activity
816
+ cls._add_activity(
817
+ config, username, "DELETE_API",
818
+ "api", api.name, api.name
819
+ )
820
+
821
+ # Save
822
+ cls.save(config, username)
823
+
824
+ log_info(
825
+ "API deleted",
826
+ api_name=api.name,
827
+ user=username
828
+ )
829
+
830
+ # ===================== Activity Methods =====================
831
+ @classmethod
832
+ def _add_activity(
833
+ cls,
834
+ config: ServiceConfig,
835
+ username: str,
836
+ action: str,
837
+ entity_type: str,
838
+ entity_id: Any,
839
+ entity_name: Optional[str] = None,
840
+ details: Optional[str] = None
841
+ ) -> None:
842
+ """Add activity log entry"""
843
+ # Activity ID'sini oluştur - mevcut en yüksek ID'yi bul
844
+ max_id = 0
845
+ if config.activity_log:
846
+ max_id = max((entry.id for entry in config.activity_log if entry.id), default=0)
847
+
848
+ activity_id = max_id + 1
849
+
850
+ activity = ActivityLogEntry(
851
+ id=activity_id,
852
+ timestamp=get_current_timestamp(), # utils'ten import etmeyi unutma
853
+ username=username,
854
+ action=action,
855
+ entity_type=entity_type,
856
+ entity_id=str(entity_id) if entity_id else None,
857
+ entity_name=entity_name,
858
+ details=details
859
+ )
860
+
861
+ config.activity_log.append(activity)
862
+
863
+ # Keep only last 1000 entries
864
+ if len(config.activity_log) > 1000:
865
+ config.activity_log = config.activity_log[-1000:]