ciyidogan commited on
Commit
437a989
·
verified ·
1 Parent(s): 08c7d42

Update admin_routes.py

Browse files
Files changed (1) hide show
  1. admin_routes.py +257 -303
admin_routes.py CHANGED
@@ -28,6 +28,22 @@ from config_models import VersionConfig, IntentConfig, LLMConfiguration
28
  security = HTTPBearer()
29
  router = APIRouter(tags=["admin"])
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  # ===================== Models =====================
32
  class LoginRequest(BaseModel):
33
  username: str
@@ -125,6 +141,7 @@ class TestRequest(BaseModel):
125
 
126
  # ===================== Auth Endpoints =====================
127
  @router.post("/login", response_model=LoginResponse)
 
128
  async def login(request: LoginRequest):
129
  """User login endpoint"""
130
  cfg = ConfigProvider.get()
@@ -159,6 +176,7 @@ async def login(request: LoginRequest):
159
  return LoginResponse(token=token, username=request.username)
160
 
161
  @router.post("/change-password")
 
162
  async def change_password(
163
  request: ChangePasswordRequest,
164
  username: str = Depends(verify_token)
@@ -205,6 +223,7 @@ async def change_password(
205
 
206
  # ===================== Locales Endpoints =====================
207
  @router.get("/locales")
 
208
  async def get_available_locales(username: str = Depends(verify_token)):
209
  """Get all system-supported locales"""
210
  from locale_manager import LocaleManager
@@ -217,6 +236,7 @@ async def get_available_locales(username: str = Depends(verify_token)):
217
  }
218
 
219
  @router.get("/locales/{locale_code}")
 
220
  async def get_locale_details(
221
  locale_code: str,
222
  username: str = Depends(verify_token)
@@ -233,6 +253,7 @@ async def get_locale_details(
233
 
234
  # ===================== Environment Endpoints =====================
235
  @router.get("/environment")
 
236
  async def get_environment(username: str = Depends(verify_token)):
237
  """Get environment configuration with provider info"""
238
  cfg = ConfigProvider.get()
@@ -359,6 +380,7 @@ async def get_environment(username: str = Depends(verify_token)):
359
  return response
360
 
361
  @router.put("/environment")
 
362
  async def update_environment(
363
  update: EnvironmentUpdate,
364
  username: str = Depends(verify_token)
@@ -403,12 +425,14 @@ async def update_environment(
403
 
404
  # ===================== Project Endpoints =====================
405
  @router.get("/projects/names")
 
406
  def list_enabled_projects():
407
  """Get list of enabled project names for chat"""
408
  cfg = ConfigProvider.get()
409
  return [p.name for p in cfg.projects if p.enabled and not getattr(p, 'deleted', False)]
410
 
411
  @router.get("/projects")
 
412
  async def list_projects(
413
  include_deleted: bool = False,
414
  username: str = Depends(verify_token)
@@ -424,6 +448,7 @@ async def list_projects(
424
  return [p.model_dump() for p in projects]
425
 
426
  @router.get("/projects/{project_id}")
 
427
  async def get_project(
428
  project_id: int,
429
  username: str = Depends(verify_token)
@@ -436,6 +461,7 @@ async def get_project(
436
  return project.model_dump()
437
 
438
  @router.post("/projects")
 
439
  async def create_project(
440
  project: ProjectCreate,
441
  username: str = Depends(verify_token)
@@ -459,67 +485,50 @@ async def create_project(
459
  status_code=400,
460
  detail="Default locale must be one of the supported locales"
461
  )
462
-
463
- try:
464
- # Debug log for project creation
465
- log_debug(f"🔍 Creating project '{project.name}' with default_locale: {project.default_locale}")
466
-
467
- new_project = ConfigProvider.create_project(project.model_dump(), username)
468
-
469
- # Debug log for initial version
470
- if new_project.versions:
471
- initial_version = new_project.versions[0]
472
- log_debug(f"🔍 Initial version created - no: {initial_version.no}, published: {initial_version.published}, type: {type(initial_version.published)}")
473
-
474
- log_info(f"✅ Project '{project.name}' created by {username}")
475
- return new_project.model_dump()
476
-
477
- except DuplicateResourceError:
478
- raise HTTPException(status_code=409, detail=f"Project with name '{project.name}' already exists")
479
- except Exception as e:
480
- log_error(f"❌ Error creating project", e)
481
- raise HTTPException(status_code=500, detail=str(e))
482
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  @router.put("/projects/{project_id}")
 
484
  async def update_project(
485
  project_id: int,
486
  update: ProjectUpdate,
487
  username: str = Depends(verify_token)
488
  ):
489
  """Update existing project with race condition handling"""
490
- try:
491
- log_info(f"🔍 Update request for project {project_id} by {username}")
492
- log_info(f"🔍 Received last_update_date: {update.last_update_date}")
493
-
494
- # Mevcut project'i al ve durumunu logla
495
- current_project = ConfigProvider.get_project(project_id)
496
- if current_project:
497
- log_info(f"🔍 Current project last_update_date: {current_project.last_update_date}")
498
- log_info(f"🔍 Current project last_update_user: {current_project.last_update_user}")
499
-
500
- # Optimistic locking kontrolü
501
- result = ConfigProvider.update_project(
502
- project_id,
503
- update.model_dump(),
504
- username,
505
- expected_last_update=update.last_update_date
506
- )
507
-
508
- log_info(f"✅ Project {project_id} updated by {username}")
509
- return result
510
- except RaceConditionError as e:
511
- log_warning(f"⚠️ Race condition detected for project {project_id}")
512
- raise HTTPException(
513
- status_code=409,
514
- detail=e.to_http_detail()
515
- )
516
- except ResourceNotFoundError:
517
- raise HTTPException(status_code=404, detail="Project not found")
518
- except Exception as e:
519
- log_error(f"❌ Error updating project {project_id}", e)
520
- raise HTTPException(status_code=500, detail=str(e))
521
 
522
  @router.delete("/projects/{project_id}")
 
523
  async def delete_project(project_id: int, username: str = Depends(verify_token)):
524
  """Delete project (soft delete)"""
525
  ConfigProvider.delete_project(project_id, username)
@@ -537,134 +546,125 @@ async def toggle_project(project_id: int, username: str = Depends(verify_token))
537
 
538
  # ===================== Import/Export Endpoints =====================
539
  @router.get("/projects/{project_id}/export")
 
540
  async def export_project(
541
  project_id: int,
542
  username: str = Depends(verify_token)
543
  ):
544
  """Export project as JSON"""
545
- try:
546
- project = ConfigProvider.get_project(project_id)
547
- if not project:
548
- raise HTTPException(status_code=404, detail="Project not found")
549
-
550
- # Prepare export data
551
- export_data = {
552
- "name": project.name,
553
- "caption": project.caption,
554
- "icon": project.icon,
555
- "description": project.description,
556
- "default_locale": project.default_locale,
557
- "supported_locales": project.supported_locales,
558
- "timezone": project.timezone,
559
- "region": project.region,
560
- "versions": []
561
- }
562
-
563
- # Add versions (only non-deleted)
564
- for version in project.versions:
565
- if not getattr(version, 'deleted', False):
566
- version_data = {
567
- "caption": version.caption,
568
- "description": getattr(version, 'description', ''),
569
- "general_prompt": version.general_prompt,
570
- "welcome_prompt": getattr(version, 'welcome_prompt', None),
571
- "llm": version.llm.model_dump() if version.llm else {},
572
- "intents": [intent.model_dump() for intent in version.intents]
573
- }
574
- export_data["versions"].append(version_data)
575
-
576
- log_info(f"✅ Project '{project.name}' exported by {username}")
577
-
578
- return export_data
579
-
580
- except Exception as e:
581
- log_error(f"❌ Error exporting project {project_id}", e)
582
- raise HTTPException(status_code=500, detail=str(e))
583
-
584
  @router.post("/projects/import")
 
585
  async def import_project(
586
  project_data: dict = Body(...),
587
  username: str = Depends(verify_token)
588
  ):
589
  """Import project from JSON"""
590
- try:
591
- # Validate required fields
592
- if not project_data.get('name'):
593
- raise HTTPException(status_code=400, detail="Project name is required")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594
 
595
- # Check for duplicate name
596
- cfg = ConfigProvider.get()
597
- if any(p.name == project_data['name'] for p in cfg.projects if not p.deleted):
598
- raise HTTPException(
599
- status_code=409,
600
- detail=f"Project with name '{project_data['name']}' already exists"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
  )
 
602
 
603
- # Create project
604
- new_project_data = {
605
- "name": project_data['name'],
606
- "caption": project_data.get('caption', project_data['name']),
607
- "icon": project_data.get('icon', 'folder'),
608
- "description": project_data.get('description', ''),
609
- "default_locale": project_data.get('default_locale', 'tr'),
610
- "supported_locales": project_data.get('supported_locales', ['tr']),
611
- "timezone": project_data.get('timezone', 'Europe/Istanbul'),
612
- "region": project_data.get('region', 'tr-TR')
613
- }
614
-
615
- # Create project
616
- new_project = ConfigProvider.create_project(new_project_data, username)
617
-
618
- # Import versions
619
- if 'versions' in project_data and project_data['versions']:
620
- # Remove the initial version that was auto-created
621
- if new_project.versions:
622
- new_project.versions.clear()
623
-
624
- # Add imported versions
625
- for idx, version_data in enumerate(project_data['versions']):
626
- version = VersionConfig(
627
- no=idx + 1,
628
- caption=version_data.get('caption', f'Version {idx + 1}'),
629
- description=version_data.get('description', ''),
630
- published=False, # Imported versions are unpublished
631
- deleted=False,
632
- general_prompt=version_data.get('general_prompt', ''),
633
- welcome_prompt=version_data.get('welcome_prompt'),
634
- llm=LLMConfiguration(**version_data.get('llm', {
635
- 'repo_id': '',
636
- 'generation_config': {
637
- 'max_new_tokens': 512,
638
- 'temperature': 0.7,
639
- 'top_p': 0.9
640
- },
641
- 'use_fine_tune': False,
642
- 'fine_tune_zip': ''
643
- })),
644
- intents=[IntentConfig(**intent) for intent in version_data.get('intents', [])],
645
- created_date=get_current_timestamp(),
646
- created_by=username
647
- )
648
- new_project.versions.append(version)
649
-
650
- # Update version counter
651
- new_project.version_id_counter = len(new_project.versions) + 1
652
-
653
- # Save updated project
654
- ConfigProvider.save(cfg, username)
655
-
656
- log_info(f"✅ Project '{new_project.name}' imported by {username}")
657
 
658
- return {"success": True, "project_id": new_project.id, "project_name": new_project.name}
659
-
660
- except DuplicateResourceError as e:
661
- raise HTTPException(status_code=409, detail=str(e))
662
- except Exception as e:
663
- log_error(f" Error importing project", e)
664
- raise HTTPException(status_code=500, detail=str(e))
665
 
666
  # ===================== Version Endpoints =====================
667
  @router.get("/projects/{project_id}/versions")
 
668
  async def list_versions(
669
  project_id: int,
670
  include_deleted: bool = False,
@@ -684,6 +684,7 @@ async def list_versions(
684
  return [v.model_dump() for v in versions]
685
 
686
  @router.post("/projects/{project_id}/versions")
 
687
  async def create_version(
688
  project_id: int,
689
  version_data: VersionCreate,
@@ -696,6 +697,7 @@ async def create_version(
696
  return new_version.model_dump()
697
 
698
  @router.put("/projects/{project_id}/versions/{version_no}")
 
699
  async def update_version(
700
  project_id: int,
701
  version_no: int,
@@ -704,51 +706,25 @@ async def update_version(
704
  username: str = Depends(verify_token)
705
  ):
706
  """Update version with race condition handling"""
707
- try:
708
- log_debug(f"🔍 Version update request - project: {project_id}, version: {version_no}, user: {username}")
709
-
710
- # Force parametresi kontrolü
711
- if force:
712
- log_warning(f"⚠️ Force update requested for version {version_no} by {username}")
713
-
714
- result = ConfigProvider.update_version(
715
- project_id,
716
- version_no,
717
- update.model_dump(),
718
- username,
719
- expected_last_update=update.last_update_date if not force else None
720
- )
721
-
722
- log_info(f"✅ Version {version_no} updated by {username}")
723
- return result
724
-
725
- except ValidationError as e:
726
- log_error(f"❌ Validation error for version {version_no}: {str(e)}")
727
- raise HTTPException(status_code=400, detail=str(e))
728
- except RaceConditionError as e:
729
- if force:
730
- # Force modunda race condition'ı yoksay
731
- result = ConfigProvider.update_version(
732
- project_id,
733
- version_no,
734
- update.model_dump(),
735
- username,
736
- expected_last_update=None
737
- )
738
- return result
739
- else:
740
- log_warning(f"⚠️ Race condition detected for version {version_no}")
741
- raise HTTPException(
742
- status_code=409,
743
- detail=e.to_http_detail()
744
- )
745
- except ResourceNotFoundError:
746
- raise HTTPException(status_code=404, detail="Version not found")
747
- except Exception as e:
748
- log_error(f"❌ Error updating version {version_no}", e)
749
- raise HTTPException(status_code=500, detail=str(e))
750
 
751
  @router.post("/projects/{project_id}/versions/{version_no}/publish")
 
752
  async def publish_version(
753
  project_id: int,
754
  version_no: int,
@@ -773,6 +749,7 @@ async def publish_version(
773
  return {"success": True}
774
 
775
  @router.delete("/projects/{project_id}/versions/{version_no}")
 
776
  async def delete_version(
777
  project_id: int,
778
  version_no: int,
@@ -785,6 +762,7 @@ async def delete_version(
785
  return {"success": True}
786
 
787
  @router.get("/projects/{project_name}/versions")
 
788
  async def get_project_versions(
789
  project_name: str,
790
  username: str = Depends(verify_token)
@@ -818,6 +796,7 @@ async def get_project_versions(
818
  }
819
 
820
  @router.get("/projects/{project_id}/versions/{version1_id}/compare/{version2_id}")
 
821
  async def compare_versions(
822
  project_id: int,
823
  version1_no: int,
@@ -878,6 +857,7 @@ async def compare_versions(
878
 
879
  # ===================== API Endpoints =====================
880
  @router.get("/apis")
 
881
  async def list_apis(
882
  include_deleted: bool = False,
883
  username: str = Depends(verify_token)
@@ -893,6 +873,7 @@ async def list_apis(
893
  return [a.model_dump() for a in apis]
894
 
895
  @router.post("/apis")
 
896
  async def create_api(api: APICreate, username: str = Depends(verify_token)):
897
  """Create new API"""
898
  new_api = ConfigProvider.create_api(api.model_dump(), username)
@@ -901,36 +882,25 @@ async def create_api(api: APICreate, username: str = Depends(verify_token)):
901
  return new_api.model_dump()
902
 
903
  @router.put("/apis/{api_name}")
 
904
  async def update_api(
905
  api_name: str,
906
  update: APIUpdate,
907
  username: str = Depends(verify_token)
908
  ):
909
  """Update API configuration with race condition handling"""
910
- try:
911
- result = ConfigProvider.update_api(
912
- api_name,
913
- update.model_dump(),
914
- username,
915
- expected_last_update=update.last_update_date
916
- )
917
-
918
- log_info(f"✅ API '{api_name}' updated by {username}")
919
- return result
920
-
921
- except RaceConditionError as e:
922
- log_warning(f"⚠️ Race condition detected for API '{api_name}'")
923
- raise HTTPException(
924
- status_code=409,
925
- detail=e.to_http_detail()
926
- )
927
- except ResourceNotFoundError:
928
- raise HTTPException(status_code=404, detail="API not found")
929
- except Exception as e:
930
- log_error(f"❌ Error updating API '{api_name}'", e)
931
- raise HTTPException(status_code=500, detail=str(e))
932
 
933
  @router.delete("/apis/{api_name}")
 
934
  async def delete_api(api_name: str, username: str = Depends(verify_token)):
935
  """Delete API (soft delete)"""
936
  ConfigProvider.delete_api(api_name, username)
@@ -939,6 +909,7 @@ async def delete_api(api_name: str, username: str = Depends(verify_token)):
939
  return {"success": True}
940
 
941
  @router.post("/validate/regex")
 
942
  async def validate_regex(
943
  request: dict = Body(...),
944
  username: str = Depends(verify_token)
@@ -947,31 +918,20 @@ async def validate_regex(
947
  pattern = request.get("pattern", "")
948
  test_value = request.get("test_value", "")
949
 
950
- try:
951
- import re
952
- compiled_regex = re.compile(pattern)
953
- matches = bool(compiled_regex.match(test_value))
954
-
955
- return {
956
- "valid": True,
957
- "matches": matches,
958
- "pattern": pattern,
959
- "test_value": test_value
960
- }
961
- except DuplicateResourceError as e:
962
- # Let the exception handler deal with it
963
- raise
964
- except Exception as e:
965
- return {
966
- "valid": False,
967
- "matches": False,
968
- "error": str(e),
969
- "pattern": pattern,
970
- "test_value": test_value
971
- }
972
 
973
  # ===================== Test Endpoints =====================
974
  @router.post("/test/run-all")
 
975
  async def run_all_tests(
976
  request: TestRequest,
977
  username: str = Depends(verify_token)
@@ -992,6 +952,7 @@ async def run_all_tests(
992
  }
993
 
994
  @router.get("/test/status/{test_run_id}")
 
995
  async def get_test_status(
996
  test_run_id: str,
997
  username: str = Depends(verify_token)
@@ -1011,6 +972,7 @@ async def get_test_status(
1011
 
1012
  # ===================== Activity Log =====================
1013
  @router.get("/activity-log")
 
1014
  async def get_activity_log(
1015
  limit: int = Query(100, ge=1, le=1000),
1016
  entity_type: Optional[str] = None,
@@ -1034,6 +996,7 @@ class TTSRequest(BaseModel):
1034
  language: Optional[str] = "tr-TR"
1035
 
1036
  @router.post("/tts/generate")
 
1037
  async def generate_tts(
1038
  request: TTSRequest,
1039
  username: str = Depends(verify_token)
@@ -1043,71 +1006,62 @@ async def generate_tts(
1043
  from tts_preprocessor import TTSPreprocessor
1044
  import base64
1045
 
1046
- try:
1047
- # Create TTS provider
1048
- tts_provider = TTSFactory.create_provider()
1049
-
1050
- if not tts_provider:
1051
- # Return empty response for no TTS
1052
- return Response(
1053
- content=b"",
1054
- media_type="audio/mpeg",
1055
- headers={"X-TTS-Status": "disabled"}
1056
- )
1057
-
1058
- log_info(f"🎤 TTS request: '{request.text[:50]}...' with provider: {tts_provider.get_provider_name()}")
1059
-
1060
- # Preprocess text if needed
1061
- preprocessor = TTSPreprocessor(language=request.language)
1062
- processed_text = preprocessor.preprocess(
1063
- request.text,
1064
- tts_provider.get_preprocessing_flags()
1065
- )
1066
-
1067
- # Generate audio
1068
- audio_data = await tts_provider.synthesize(
1069
- text=processed_text,
1070
- voice_id=request.voice_id
1071
- )
1072
-
1073
- # Return audio as binary response
1074
  return Response(
1075
- content=audio_data,
1076
  media_type="audio/mpeg",
1077
- headers={
1078
- "Content-Disposition": 'inline; filename="tts_output.mp3"',
1079
- "X-TTS-Provider": tts_provider.get_provider_name()
1080
- }
1081
  )
1082
-
1083
- except Exception as e:
1084
- log_error("❌ TTS generation error", e)
1085
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1086
 
1087
  @router.get("/tts/voices")
 
1088
  async def get_tts_voices(username: str = Depends(verify_token)):
1089
  """Get available TTS voices"""
1090
  from tts_factory import TTSFactory
1091
 
1092
- try:
1093
- tts_provider = TTSFactory.create_provider()
1094
-
1095
- if not tts_provider:
1096
- return {"voices": []}
1097
-
1098
- voices = tts_provider.get_supported_voices()
1099
-
1100
- # Convert dict to list format
1101
- voice_list = [
1102
- {"id": voice_id, "name": voice_name}
1103
- for voice_id, voice_name in voices.items()
1104
- ]
1105
-
1106
- return {"voices": voice_list}
1107
-
1108
- except Exception as e:
1109
- log_error("❌ Error getting TTS voices", e)
1110
  return {"voices": []}
 
 
 
 
 
 
 
 
 
 
1111
 
1112
  # ===================== Helper Functions =====================
1113
  async def notify_llm_startup(project, version):
 
28
  security = HTTPBearer()
29
  router = APIRouter(tags=["admin"])
30
 
31
+ # ===================== Decarators =====================
32
+ def handle_exceptions(func):
33
+ """Decorator to handle exceptions consistently"""
34
+ @wraps(func)
35
+ async def wrapper(*args, **kwargs):
36
+ try:
37
+ return await func(*args, **kwargs)
38
+ except FlareException:
39
+ # Let global handlers deal with our custom exceptions
40
+ raise
41
+ except Exception as e:
42
+ # Log and convert unexpected exceptions to HTTP 500
43
+ log_error(f"❌ Unexpected error in {func.__name__}", e)
44
+ raise HTTPException(status_code=500, detail=str(e))
45
+ return wrapper
46
+
47
  # ===================== Models =====================
48
  class LoginRequest(BaseModel):
49
  username: str
 
141
 
142
  # ===================== Auth Endpoints =====================
143
  @router.post("/login", response_model=LoginResponse)
144
+ @handle_exceptions
145
  async def login(request: LoginRequest):
146
  """User login endpoint"""
147
  cfg = ConfigProvider.get()
 
176
  return LoginResponse(token=token, username=request.username)
177
 
178
  @router.post("/change-password")
179
+ @handle_exceptions
180
  async def change_password(
181
  request: ChangePasswordRequest,
182
  username: str = Depends(verify_token)
 
223
 
224
  # ===================== Locales Endpoints =====================
225
  @router.get("/locales")
226
+ @handle_exceptions
227
  async def get_available_locales(username: str = Depends(verify_token)):
228
  """Get all system-supported locales"""
229
  from locale_manager import LocaleManager
 
236
  }
237
 
238
  @router.get("/locales/{locale_code}")
239
+ @handle_exceptions
240
  async def get_locale_details(
241
  locale_code: str,
242
  username: str = Depends(verify_token)
 
253
 
254
  # ===================== Environment Endpoints =====================
255
  @router.get("/environment")
256
+ @handle_exceptions
257
  async def get_environment(username: str = Depends(verify_token)):
258
  """Get environment configuration with provider info"""
259
  cfg = ConfigProvider.get()
 
380
  return response
381
 
382
  @router.put("/environment")
383
+ @handle_exceptions
384
  async def update_environment(
385
  update: EnvironmentUpdate,
386
  username: str = Depends(verify_token)
 
425
 
426
  # ===================== Project Endpoints =====================
427
  @router.get("/projects/names")
428
+ @handle_exceptions
429
  def list_enabled_projects():
430
  """Get list of enabled project names for chat"""
431
  cfg = ConfigProvider.get()
432
  return [p.name for p in cfg.projects if p.enabled and not getattr(p, 'deleted', False)]
433
 
434
  @router.get("/projects")
435
+ @handle_exceptions
436
  async def list_projects(
437
  include_deleted: bool = False,
438
  username: str = Depends(verify_token)
 
448
  return [p.model_dump() for p in projects]
449
 
450
  @router.get("/projects/{project_id}")
451
+ @handle_exceptions
452
  async def get_project(
453
  project_id: int,
454
  username: str = Depends(verify_token)
 
461
  return project.model_dump()
462
 
463
  @router.post("/projects")
464
+ @handle_exceptions
465
  async def create_project(
466
  project: ProjectCreate,
467
  username: str = Depends(verify_token)
 
485
  status_code=400,
486
  detail="Default locale must be one of the supported locales"
487
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
 
489
+ # Debug log for project creation
490
+ log_debug(f"🔍 Creating project '{project.name}' with default_locale: {project.default_locale}")
491
+
492
+ new_project = ConfigProvider.create_project(project.model_dump(), username)
493
+
494
+ # Debug log for initial version
495
+ if new_project.versions:
496
+ initial_version = new_project.versions[0]
497
+ log_debug(f"🔍 Initial version created - no: {initial_version.no}, published: {initial_version.published}, type: {type(initial_version.published)}")
498
+
499
+ log_info(f"✅ Project '{project.name}' created by {username}")
500
+ return new_project.model_dump()
501
+
502
  @router.put("/projects/{project_id}")
503
+ @handle_exceptions
504
  async def update_project(
505
  project_id: int,
506
  update: ProjectUpdate,
507
  username: str = Depends(verify_token)
508
  ):
509
  """Update existing project with race condition handling"""
510
+ log_info(f"🔍 Update request for project {project_id} by {username}")
511
+ log_info(f"🔍 Received last_update_date: {update.last_update_date}")
512
+
513
+ # Mevcut project'i al ve durumunu logla
514
+ current_project = ConfigProvider.get_project(project_id)
515
+ if current_project:
516
+ log_info(f"🔍 Current project last_update_date: {current_project.last_update_date}")
517
+ log_info(f"🔍 Current project last_update_user: {current_project.last_update_user}")
518
+
519
+ # Optimistic locking kontrolü
520
+ result = ConfigProvider.update_project(
521
+ project_id,
522
+ update.model_dump(),
523
+ username,
524
+ expected_last_update=update.last_update_date
525
+ )
526
+
527
+ log_info(f"✅ Project {project_id} updated by {username}")
528
+ return result
 
 
 
 
 
 
 
 
 
 
 
 
529
 
530
  @router.delete("/projects/{project_id}")
531
+ @handle_exceptions
532
  async def delete_project(project_id: int, username: str = Depends(verify_token)):
533
  """Delete project (soft delete)"""
534
  ConfigProvider.delete_project(project_id, username)
 
546
 
547
  # ===================== Import/Export Endpoints =====================
548
  @router.get("/projects/{project_id}/export")
549
+ @handle_exceptions
550
  async def export_project(
551
  project_id: int,
552
  username: str = Depends(verify_token)
553
  ):
554
  """Export project as JSON"""
555
+ project = ConfigProvider.get_project(project_id)
556
+ if not project:
557
+ raise HTTPException(status_code=404, detail="Project not found")
558
+
559
+ # Prepare export data
560
+ export_data = {
561
+ "name": project.name,
562
+ "caption": project.caption,
563
+ "icon": project.icon,
564
+ "description": project.description,
565
+ "default_locale": project.default_locale,
566
+ "supported_locales": project.supported_locales,
567
+ "timezone": project.timezone,
568
+ "region": project.region,
569
+ "versions": []
570
+ }
571
+
572
+ # Add versions (only non-deleted)
573
+ for version in project.versions:
574
+ if not getattr(version, 'deleted', False):
575
+ version_data = {
576
+ "caption": version.caption,
577
+ "description": getattr(version, 'description', ''),
578
+ "general_prompt": version.general_prompt,
579
+ "welcome_prompt": getattr(version, 'welcome_prompt', None),
580
+ "llm": version.llm.model_dump() if version.llm else {},
581
+ "intents": [intent.model_dump() for intent in version.intents]
582
+ }
583
+ export_data["versions"].append(version_data)
584
+
585
+ log_info(f"✅ Project '{project.name}' exported by {username}")
586
+
587
+ return export_data
588
+
 
 
 
 
 
589
  @router.post("/projects/import")
590
+ @handle_exceptions
591
  async def import_project(
592
  project_data: dict = Body(...),
593
  username: str = Depends(verify_token)
594
  ):
595
  """Import project from JSON"""
596
+ # Validate required fields
597
+ if not project_data.get('name'):
598
+ raise HTTPException(status_code=400, detail="Project name is required")
599
+
600
+ # Check for duplicate name
601
+ cfg = ConfigProvider.get()
602
+ if any(p.name == project_data['name'] for p in cfg.projects if not p.deleted):
603
+ raise HTTPException(
604
+ status_code=409,
605
+ detail=f"Project with name '{project_data['name']}' already exists"
606
+ )
607
+
608
+ # Create project
609
+ new_project_data = {
610
+ "name": project_data['name'],
611
+ "caption": project_data.get('caption', project_data['name']),
612
+ "icon": project_data.get('icon', 'folder'),
613
+ "description": project_data.get('description', ''),
614
+ "default_locale": project_data.get('default_locale', 'tr'),
615
+ "supported_locales": project_data.get('supported_locales', ['tr']),
616
+ "timezone": project_data.get('timezone', 'Europe/Istanbul'),
617
+ "region": project_data.get('region', 'tr-TR')
618
+ }
619
+
620
+ # Create project
621
+ new_project = ConfigProvider.create_project(new_project_data, username)
622
+
623
+ # Import versions
624
+ if 'versions' in project_data and project_data['versions']:
625
+ # Remove the initial version that was auto-created
626
+ if new_project.versions:
627
+ new_project.versions.clear()
628
 
629
+ # Add imported versions
630
+ for idx, version_data in enumerate(project_data['versions']):
631
+ version = VersionConfig(
632
+ no=idx + 1,
633
+ caption=version_data.get('caption', f'Version {idx + 1}'),
634
+ description=version_data.get('description', ''),
635
+ published=False, # Imported versions are unpublished
636
+ deleted=False,
637
+ general_prompt=version_data.get('general_prompt', ''),
638
+ welcome_prompt=version_data.get('welcome_prompt'),
639
+ llm=LLMConfiguration(**version_data.get('llm', {
640
+ 'repo_id': '',
641
+ 'generation_config': {
642
+ 'max_new_tokens': 512,
643
+ 'temperature': 0.7,
644
+ 'top_p': 0.9
645
+ },
646
+ 'use_fine_tune': False,
647
+ 'fine_tune_zip': ''
648
+ })),
649
+ intents=[IntentConfig(**intent) for intent in version_data.get('intents', [])],
650
+ created_date=get_current_timestamp(),
651
+ created_by=username
652
  )
653
+ new_project.versions.append(version)
654
 
655
+ # Update version counter
656
+ new_project.version_id_counter = len(new_project.versions) + 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
657
 
658
+ # Save updated project
659
+ ConfigProvider.save(cfg, username)
660
+
661
+ log_info(f"✅ Project '{new_project.name}' imported by {username}")
662
+
663
+ return {"success": True, "project_id": new_project.id, "project_name": new_project.name}
 
664
 
665
  # ===================== Version Endpoints =====================
666
  @router.get("/projects/{project_id}/versions")
667
+ @handle_exceptions
668
  async def list_versions(
669
  project_id: int,
670
  include_deleted: bool = False,
 
684
  return [v.model_dump() for v in versions]
685
 
686
  @router.post("/projects/{project_id}/versions")
687
+ @handle_exceptions
688
  async def create_version(
689
  project_id: int,
690
  version_data: VersionCreate,
 
697
  return new_version.model_dump()
698
 
699
  @router.put("/projects/{project_id}/versions/{version_no}")
700
+ @handle_exceptions
701
  async def update_version(
702
  project_id: int,
703
  version_no: int,
 
706
  username: str = Depends(verify_token)
707
  ):
708
  """Update version with race condition handling"""
709
+ log_debug(f"🔍 Version update request - project: {project_id}, version: {version_no}, user: {username}")
710
+
711
+ # Force parametresi kontrolü
712
+ if force:
713
+ log_warning(f"⚠️ Force update requested for version {version_no} by {username}")
714
+
715
+ result = ConfigProvider.update_version(
716
+ project_id,
717
+ version_no,
718
+ update.model_dump(),
719
+ username,
720
+ expected_last_update=update.last_update_date if not force else None
721
+ )
722
+
723
+ log_info(f"✅ Version {version_no} updated by {username}")
724
+ return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
725
 
726
  @router.post("/projects/{project_id}/versions/{version_no}/publish")
727
+ @handle_exceptions
728
  async def publish_version(
729
  project_id: int,
730
  version_no: int,
 
749
  return {"success": True}
750
 
751
  @router.delete("/projects/{project_id}/versions/{version_no}")
752
+ @handle_exceptions
753
  async def delete_version(
754
  project_id: int,
755
  version_no: int,
 
762
  return {"success": True}
763
 
764
  @router.get("/projects/{project_name}/versions")
765
+ @handle_exceptions
766
  async def get_project_versions(
767
  project_name: str,
768
  username: str = Depends(verify_token)
 
796
  }
797
 
798
  @router.get("/projects/{project_id}/versions/{version1_id}/compare/{version2_id}")
799
+ @handle_exceptions
800
  async def compare_versions(
801
  project_id: int,
802
  version1_no: int,
 
857
 
858
  # ===================== API Endpoints =====================
859
  @router.get("/apis")
860
+ @handle_exceptions
861
  async def list_apis(
862
  include_deleted: bool = False,
863
  username: str = Depends(verify_token)
 
873
  return [a.model_dump() for a in apis]
874
 
875
  @router.post("/apis")
876
+ @handle_exceptions
877
  async def create_api(api: APICreate, username: str = Depends(verify_token)):
878
  """Create new API"""
879
  new_api = ConfigProvider.create_api(api.model_dump(), username)
 
882
  return new_api.model_dump()
883
 
884
  @router.put("/apis/{api_name}")
885
+ @handle_exceptions
886
  async def update_api(
887
  api_name: str,
888
  update: APIUpdate,
889
  username: str = Depends(verify_token)
890
  ):
891
  """Update API configuration with race condition handling"""
892
+ result = ConfigProvider.update_api(
893
+ api_name,
894
+ update.model_dump(),
895
+ username,
896
+ expected_last_update=update.last_update_date
897
+ )
898
+
899
+ log_info(f"✅ API '{api_name}' updated by {username}")
900
+ return result
 
 
 
 
 
 
 
 
 
 
 
 
 
901
 
902
  @router.delete("/apis/{api_name}")
903
+ @handle_exceptions
904
  async def delete_api(api_name: str, username: str = Depends(verify_token)):
905
  """Delete API (soft delete)"""
906
  ConfigProvider.delete_api(api_name, username)
 
909
  return {"success": True}
910
 
911
  @router.post("/validate/regex")
912
+ @handle_exceptions
913
  async def validate_regex(
914
  request: dict = Body(...),
915
  username: str = Depends(verify_token)
 
918
  pattern = request.get("pattern", "")
919
  test_value = request.get("test_value", "")
920
 
921
+ import re
922
+ compiled_regex = re.compile(pattern)
923
+ matches = bool(compiled_regex.match(test_value))
924
+
925
+ return {
926
+ "valid": True,
927
+ "matches": matches,
928
+ "pattern": pattern,
929
+ "test_value": test_value
930
+ }
 
 
 
 
 
 
 
 
 
 
 
 
931
 
932
  # ===================== Test Endpoints =====================
933
  @router.post("/test/run-all")
934
+ @handle_exceptions
935
  async def run_all_tests(
936
  request: TestRequest,
937
  username: str = Depends(verify_token)
 
952
  }
953
 
954
  @router.get("/test/status/{test_run_id}")
955
+ @handle_exceptions
956
  async def get_test_status(
957
  test_run_id: str,
958
  username: str = Depends(verify_token)
 
972
 
973
  # ===================== Activity Log =====================
974
  @router.get("/activity-log")
975
+ @handle_exceptions
976
  async def get_activity_log(
977
  limit: int = Query(100, ge=1, le=1000),
978
  entity_type: Optional[str] = None,
 
996
  language: Optional[str] = "tr-TR"
997
 
998
  @router.post("/tts/generate")
999
+ @handle_exceptions
1000
  async def generate_tts(
1001
  request: TTSRequest,
1002
  username: str = Depends(verify_token)
 
1006
  from tts_preprocessor import TTSPreprocessor
1007
  import base64
1008
 
1009
+ # Create TTS provider
1010
+ tts_provider = TTSFactory.create_provider()
1011
+
1012
+ if not tts_provider:
1013
+ # Return empty response for no TTS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1014
  return Response(
1015
+ content=b"",
1016
  media_type="audio/mpeg",
1017
+ headers={"X-TTS-Status": "disabled"}
 
 
 
1018
  )
1019
+
1020
+ log_info(f"🎤 TTS request: '{request.text[:50]}...' with provider: {tts_provider.get_provider_name()}")
1021
+
1022
+ # Preprocess text if needed
1023
+ preprocessor = TTSPreprocessor(language=request.language)
1024
+ processed_text = preprocessor.preprocess(
1025
+ request.text,
1026
+ tts_provider.get_preprocessing_flags()
1027
+ )
1028
+
1029
+ # Generate audio
1030
+ audio_data = await tts_provider.synthesize(
1031
+ text=processed_text,
1032
+ voice_id=request.voice_id
1033
+ )
1034
+
1035
+ # Return audio as binary response
1036
+ return Response(
1037
+ content=audio_data,
1038
+ media_type="audio/mpeg",
1039
+ headers={
1040
+ "Content-Disposition": 'inline; filename="tts_output.mp3"',
1041
+ "X-TTS-Provider": tts_provider.get_provider_name()
1042
+ }
1043
+ )
1044
 
1045
  @router.get("/tts/voices")
1046
+ @handle_exceptions
1047
  async def get_tts_voices(username: str = Depends(verify_token)):
1048
  """Get available TTS voices"""
1049
  from tts_factory import TTSFactory
1050
 
1051
+ tts_provider = TTSFactory.create_provider()
1052
+
1053
+ if not tts_provider:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1054
  return {"voices": []}
1055
+
1056
+ voices = tts_provider.get_supported_voices()
1057
+
1058
+ # Convert dict to list format
1059
+ voice_list = [
1060
+ {"id": voice_id, "name": voice_name}
1061
+ for voice_id, voice_name in voices.items()
1062
+ ]
1063
+
1064
+ return {"voices": voice_list}
1065
 
1066
  # ===================== Helper Functions =====================
1067
  async def notify_llm_startup(project, version):