ciyidogan commited on
Commit
80b3562
·
verified ·
1 Parent(s): 9326c5f

Delete admin_routes.py

Browse files
Files changed (1) hide show
  1. admin_routes.py +0 -1049
admin_routes.py DELETED
@@ -1,1049 +0,0 @@
1
- """Admin API endpoints for Flare (Refactored)
2
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3
- Provides authentication, project, version, and API management endpoints with provider support.
4
- """
5
-
6
- import os
7
- import time
8
- import threading
9
- import hashlib
10
- import bcrypt
11
- from typing import Optional, Dict, List, Any
12
- from datetime import datetime, timedelta, timezone
13
- from fastapi import APIRouter, HTTPException, Depends, Query, Response, Body
14
- from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
15
- from pydantic import BaseModel, Field
16
- import httpx
17
- from functools import wraps
18
-
19
- from utils import verify_token, create_token, get_current_timestamp
20
- from config_provider import ConfigProvider
21
- from logger import log_info, log_error, log_warning, log_debug
22
- from exceptions import (
23
- FlareException,
24
- RaceConditionError,
25
- ValidationError,
26
- ResourceNotFoundError,
27
- AuthenticationError,
28
- AuthorizationError,
29
- DuplicateResourceError
30
- )
31
- from config_models import VersionConfig, IntentConfig, LLMConfiguration
32
-
33
- # ===================== Constants & Config =====================
34
- security = HTTPBearer()
35
- router = APIRouter(tags=["admin"])
36
-
37
- # ===================== Decorators =====================
38
- def handle_exceptions(func):
39
- """Decorator to handle exceptions consistently"""
40
- @wraps(func)
41
- async def wrapper(*args, **kwargs):
42
- try:
43
- return await func(*args, **kwargs)
44
- except HTTPException:
45
- # HTTPException'ları olduğu gibi geçir
46
- raise
47
- except FlareException:
48
- # Let global handlers deal with our custom exceptions
49
- raise
50
- except Exception as e:
51
- # Log and convert unexpected exceptions to HTTP 500
52
- log_error(f"❌ Unexpected error in {func.__name__}", e)
53
- raise HTTPException(status_code=500, detail=str(e))
54
- return wrapper
55
-
56
- # ===================== Models =====================
57
- class LoginRequest(BaseModel):
58
- username: str
59
- password: str
60
-
61
- class LoginResponse(BaseModel):
62
- token: str
63
- username: str
64
-
65
- class ChangePasswordRequest(BaseModel):
66
- current_password: str
67
- new_password: str
68
-
69
- class ProviderSettingsUpdate(BaseModel):
70
- name: str
71
- api_key: Optional[str] = None
72
- endpoint: Optional[str] = None
73
- settings: Dict[str, Any] = Field(default_factory=dict)
74
-
75
- class EnvironmentUpdate(BaseModel):
76
- llm_provider: ProviderSettingsUpdate
77
- tts_provider: ProviderSettingsUpdate
78
- stt_provider: ProviderSettingsUpdate
79
- parameter_collection_config: Optional[Dict[str, Any]] = None
80
-
81
- class ProjectCreate(BaseModel):
82
- name: str
83
- caption: Optional[str] = ""
84
- icon: Optional[str] = "folder"
85
- description: Optional[str] = ""
86
- default_locale: str = "tr"
87
- supported_locales: List[str] = Field(default_factory=lambda: ["tr"])
88
- timezone: str = "Europe/Istanbul"
89
- region: str = "tr-TR"
90
-
91
- class ProjectUpdate(BaseModel):
92
- caption: str
93
- icon: Optional[str] = "folder"
94
- description: Optional[str] = ""
95
- default_locale: str = "tr"
96
- supported_locales: List[str] = Field(default_factory=lambda: ["tr"])
97
- timezone: str = "Europe/Istanbul"
98
- region: str = "tr-TR"
99
- last_update_date: str
100
-
101
- class VersionCreate(BaseModel):
102
- caption: str
103
- source_version_no: int | None = None
104
-
105
- class IntentModel(BaseModel):
106
- name: str
107
- caption: Optional[str] = ""
108
- detection_prompt: str
109
- examples: List[Dict[str, str]] = [] # LocalizedExample format
110
- parameters: List[Dict[str, Any]] = []
111
- action: str
112
- fallback_timeout_prompt: Optional[str] = None
113
- fallback_error_prompt: Optional[str] = None
114
-
115
- class VersionUpdate(BaseModel):
116
- caption: str
117
- general_prompt: str
118
- llm: Dict[str, Any]
119
- intents: List[IntentModel]
120
- last_update_date: str
121
-
122
- class APICreate(BaseModel):
123
- name: str
124
- url: str
125
- method: str = "POST"
126
- headers: Dict[str, str] = {}
127
- body_template: Dict[str, Any] = {}
128
- timeout_seconds: int = 10
129
- retry: Dict[str, Any] = Field(default_factory=lambda: {"retry_count": 3, "backoff_seconds": 2, "strategy": "static"})
130
- proxy: Optional[str] = None
131
- auth: Optional[Dict[str, Any]] = None
132
- response_prompt: Optional[str] = None
133
- response_mappings: List[Dict[str, Any]] = []
134
-
135
- class APIUpdate(BaseModel):
136
- url: str
137
- method: str
138
- headers: Dict[str, str]
139
- body_template: Dict[str, Any]
140
- timeout_seconds: int
141
- retry: Dict[str, Any]
142
- proxy: Optional[str]
143
- auth: Optional[Dict[str, Any]]
144
- response_prompt: Optional[str]
145
- response_mappings: List[Dict[str, Any]] = []
146
- last_update_date: str
147
-
148
- class TestRequest(BaseModel):
149
- test_type: str # "all", "ui", "backend", "integration", "spark"
150
-
151
- # ===================== Auth Endpoints =====================
152
- @router.post("/login", response_model=LoginResponse)
153
- @handle_exceptions
154
- async def login(request: LoginRequest):
155
- """User login endpoint"""
156
- cfg = ConfigProvider.get()
157
-
158
- # Find user
159
- user = next((u for u in cfg.global_config.users if u.username == request.username), None)
160
- if not user:
161
- raise HTTPException(status_code=401, detail="Invalid credentials")
162
-
163
- # Verify password - Try both bcrypt and SHA256 for backward compatibility
164
- password_valid = False
165
-
166
- # First try bcrypt (new format)
167
- try:
168
- if user.password_hash.startswith("$2b$") or user.password_hash.startswith("$2a$"):
169
- password_valid = bcrypt.checkpw(request.password.encode('utf-8'), user.password_hash.encode('utf-8'))
170
- except:
171
- pass
172
-
173
- # If not valid, try SHA256 (old format)
174
- if not password_valid:
175
- sha256_hash = hashlib.sha256(request.password.encode('utf-8')).hexdigest()
176
- password_valid = (user.password_hash == sha256_hash)
177
-
178
- if not password_valid:
179
- raise HTTPException(status_code=401, detail="Invalid credentials")
180
-
181
- # Create token
182
- token = create_token(request.username)
183
-
184
- log_info(f"✅ User '{request.username}' logged in successfully")
185
- return LoginResponse(token=token, username=request.username)
186
-
187
- @router.post("/change-password")
188
- @handle_exceptions
189
- async def change_password(
190
- request: ChangePasswordRequest,
191
- username: str = Depends(verify_token)
192
- ):
193
- """Change user password"""
194
- cfg = ConfigProvider.get()
195
-
196
- # Find user
197
- user = next((u for u in cfg.global_config.users if u.username == username), None)
198
- if not user:
199
- raise HTTPException(status_code=404, detail="User not found")
200
-
201
- # Verify current password - Try both bcrypt and SHA256 for backward compatibility
202
- password_valid = False
203
-
204
- # First try bcrypt (new format)
205
- try:
206
- if user.password_hash.startswith("$2b$") or user.password_hash.startswith("$2a$"):
207
- password_valid = bcrypt.checkpw(request.current_password.encode('utf-8'), user.password_hash.encode('utf-8'))
208
- except:
209
- pass
210
-
211
- # If not valid, try SHA256 (old format)
212
- if not password_valid:
213
- sha256_hash = hashlib.sha256(request.current_password.encode('utf-8')).hexdigest()
214
- password_valid = (user.password_hash == sha256_hash)
215
-
216
- if not password_valid:
217
- raise HTTPException(status_code=401, detail="Current password is incorrect")
218
-
219
- # Generate new password hash (always use bcrypt for new passwords)
220
- salt = bcrypt.gensalt()
221
- new_hash = bcrypt.hashpw(request.new_password.encode('utf-8'), salt)
222
-
223
- # Update user
224
- user.password_hash = new_hash.decode('utf-8')
225
- user.salt = salt.decode('utf-8')
226
-
227
- # Save configuration via ConfigProvider
228
- ConfigProvider.save(cfg, username)
229
-
230
- log_info(f"✅ Password changed for user '{username}'")
231
- return {"success": True}
232
-
233
- # ===================== Locales Endpoints =====================
234
- @router.get("/locales")
235
- @handle_exceptions
236
- async def get_available_locales(username: str = Depends(verify_token)):
237
- """Get all system-supported locales"""
238
- from locale_manager import LocaleManager
239
-
240
- locales = LocaleManager.get_available_locales_with_names()
241
-
242
- return {
243
- "locales": locales,
244
- "default": LocaleManager.get_default_locale()
245
- }
246
-
247
- @router.get("/locales/{locale_code}")
248
- @handle_exceptions
249
- async def get_locale_details(
250
- locale_code: str,
251
- username: str = Depends(verify_token)
252
- ):
253
- """Get detailed information for a specific locale"""
254
- from locale_manager import LocaleManager
255
-
256
- locale_info = LocaleManager.get_locale_details(locale_code)
257
-
258
- if not locale_info:
259
- raise HTTPException(status_code=404, detail=f"Locale '{locale_code}' not found")
260
-
261
- return locale_info
262
-
263
- # ===================== Environment Endpoints =====================
264
- @router.get("/environment")
265
- @handle_exceptions
266
- async def get_environment(username: str = Depends(verify_token)):
267
- """Get environment configuration with provider info"""
268
- cfg = ConfigProvider.get()
269
- env_config = cfg.global_config
270
-
271
- # Provider tabanlı yeni yapıyı destekle
272
- response = {}
273
-
274
- # LLM Provider
275
- if hasattr(env_config, 'llm_provider'):
276
- response["llm_provider"] = env_config.llm_provider
277
-
278
- # TTS Provider
279
- if hasattr(env_config, 'tts_provider'):
280
- response["tts_provider"] = env_config.tts_provider
281
-
282
- # STT Provider
283
- if hasattr(env_config, 'stt_provider'):
284
- response["stt_provider"] = env_config.stt_provider
285
- else:
286
- response["stt_provider"] = {
287
- "name": getattr(env_config, 'stt_engine', 'no_stt'),
288
- "api_key": getattr(env_config, 'stt_engine_api_key', None) or "",
289
- "endpoint": None,
290
- "settings": getattr(env_config, 'stt_settings', {})
291
- }
292
-
293
- # Provider listesi
294
- if hasattr(env_config, 'providers'):
295
- providers_list = []
296
- for provider in env_config.providers:
297
- providers_list.append(provider)
298
- response["providers"] = providers_list
299
- else:
300
- # Varsayılan provider listesi
301
- response["providers"] = [
302
- {
303
- "type": "llm",
304
- "name": "spark_cloud",
305
- "display_name": "Spark LLM (Cloud)",
306
- "requires_endpoint": True,
307
- "requires_api_key": True,
308
- "requires_repo_info": False
309
- },
310
- {
311
- "type": "llm",
312
- "name": "gpt-4o",
313
- "display_name": "GPT-4o",
314
- "requires_endpoint": True,
315
- "requires_api_key": True,
316
- "requires_repo_info": False
317
- },
318
- {
319
- "type": "llm",
320
- "name": "gpt-4o-mini",
321
- "display_name": "GPT-4o Mini",
322
- "requires_endpoint": True,
323
- "requires_api_key": True,
324
- "requires_repo_info": False
325
- },
326
- {
327
- "type": "tts",
328
- "name": "no_tts",
329
- "display_name": "No TTS",
330
- "requires_endpoint": False,
331
- "requires_api_key": False,
332
- "requires_repo_info": False
333
- },
334
- {
335
- "type": "tts",
336
- "name": "elevenlabs",
337
- "display_name": "ElevenLabs",
338
- "requires_endpoint": False,
339
- "requires_api_key": True,
340
- "requires_repo_info": False
341
- },
342
- {
343
- "type": "stt",
344
- "name": "no_stt",
345
- "display_name": "No STT",
346
- "requires_endpoint": False,
347
- "requires_api_key": False,
348
- "requires_repo_info": False
349
- },
350
- {
351
- "type": "stt",
352
- "name": "google",
353
- "display_name": "Google Cloud STT",
354
- "requires_endpoint": False,
355
- "requires_api_key": True,
356
- "requires_repo_info": False
357
- }
358
- ]
359
-
360
- # Parameter collection config
361
- if hasattr(env_config, 'parameter_collection_config'):
362
- response["parameter_collection_config"] = env_config.parameter_collection_config
363
- else:
364
- # Varsayılan değerler
365
- response["parameter_collection_config"] = {
366
- "max_params_per_question": 2,
367
- "retry_unanswered": True,
368
- "smart_grouping": True,
369
- "collection_prompt": "You are a helpful assistant collecting information from the user..."
370
- }
371
-
372
- return response
373
-
374
- @router.put("/environment")
375
- @handle_exceptions
376
- async def update_environment(
377
- update: EnvironmentUpdate,
378
- username: str = Depends(verify_token)
379
- ):
380
- """Update environment configuration with provider validation"""
381
- log_info(f"📝 Updating environment config by {username}")
382
-
383
- cfg = ConfigProvider.get()
384
-
385
- # Validate LLM provider
386
- llm_provider_def = cfg.global_config.get_provider_config("llm", update.llm_provider.name)
387
- if not llm_provider_def:
388
- raise HTTPException(status_code=400, detail=f"Unknown LLM provider: {update.llm_provider.name}")
389
-
390
- if llm_provider_def.requires_api_key and not update.llm_provider.api_key:
391
- raise HTTPException(status_code=400, detail=f"{llm_provider_def.display_name} requires API key")
392
-
393
- if llm_provider_def.requires_endpoint and not update.llm_provider.endpoint:
394
- raise HTTPException(status_code=400, detail=f"{llm_provider_def.display_name} requires endpoint")
395
-
396
- # Validate TTS provider
397
- tts_provider_def = cfg.global_config.get_provider_config("tts", update.tts_provider.name)
398
- if not tts_provider_def:
399
- raise HTTPException(status_code=400, detail=f"Unknown TTS provider: {update.tts_provider.name}")
400
-
401
- if tts_provider_def.requires_api_key and not update.tts_provider.api_key:
402
- raise HTTPException(status_code=400, detail=f"{tts_provider_def.display_name} requires API key")
403
-
404
- # Validate STT provider
405
- stt_provider_def = cfg.global_config.get_provider_config("stt", update.stt_provider.name)
406
- if not stt_provider_def:
407
- raise HTTPException(status_code=400, detail=f"Unknown STT provider: {update.stt_provider.name}")
408
-
409
- if stt_provider_def.requires_api_key and not update.stt_provider.api_key:
410
- raise HTTPException(status_code=400, detail=f"{stt_provider_def.display_name} requires API key")
411
-
412
- # Update via ConfigProvider
413
- ConfigProvider.update_environment(update.model_dump(), username)
414
-
415
- log_info(f"✅ Environment updated to LLM: {update.llm_provider.name}, TTS: {update.tts_provider.name}, STT: {update.stt_provider.name} by {username}")
416
- return {"success": True}
417
-
418
- # ===================== Project Endpoints =====================
419
- @router.get("/projects/names")
420
- @handle_exceptions
421
- async def list_enabled_projects():
422
- """Get list of enabled project names for chat"""
423
- cfg = ConfigProvider.get()
424
- return [p.name for p in cfg.projects if p.enabled and not getattr(p, 'deleted', False)]
425
-
426
- @router.get("/projects")
427
- @handle_exceptions
428
- async def list_projects(
429
- include_deleted: bool = False,
430
- username: str = Depends(verify_token)
431
- ):
432
- """List all projects"""
433
- cfg = ConfigProvider.get()
434
- projects = cfg.projects
435
-
436
- # Filter deleted if needed
437
- if not include_deleted:
438
- projects = [p for p in projects if not getattr(p, 'deleted', False)]
439
-
440
- return [p.model_dump() for p in projects]
441
-
442
- @router.get("/projects/{project_id}")
443
- @handle_exceptions
444
- async def get_project(
445
- project_id: int,
446
- username: str = Depends(verify_token)
447
- ):
448
- """Get single project by ID"""
449
- project = ConfigProvider.get_project(project_id)
450
- if not project or getattr(project, 'deleted', False):
451
- raise HTTPException(status_code=404, detail="Project not found")
452
-
453
- return project.model_dump()
454
-
455
- @router.post("/projects")
456
- @handle_exceptions
457
- async def create_project(
458
- project: ProjectCreate,
459
- username: str = Depends(verify_token)
460
- ):
461
- """Create new project with initial version"""
462
- # Validate supported locales
463
- from locale_manager import LocaleManager
464
-
465
- invalid_locales = LocaleManager.validate_project_languages(project.supported_locales)
466
- if invalid_locales:
467
- available_locales = LocaleManager.get_available_locales_with_names()
468
- available_codes = [locale['code'] for locale in available_locales]
469
- raise HTTPException(
470
- status_code=400,
471
- detail=f"Unsupported locales: {', '.join(invalid_locales)}. Available locales: {', '.join(available_codes)}"
472
- )
473
-
474
- # Check if default locale is in supported locales
475
- if project.default_locale not in project.supported_locales:
476
- raise HTTPException(
477
- status_code=400,
478
- detail="Default locale must be one of the supported locales"
479
- )
480
-
481
- # Debug log for project creation
482
- log_debug(f"🔍 Creating project '{project.name}' with default_locale: {project.default_locale}")
483
-
484
- new_project = ConfigProvider.create_project(project.model_dump(), username)
485
-
486
- # Debug log for initial version
487
- if new_project.versions:
488
- initial_version = new_project.versions[0]
489
- log_debug(f"🔍 Initial version created - no: {initial_version.no}, published: {initial_version.published}, type: {type(initial_version.published)}")
490
-
491
- log_info(f"✅ Project '{project.name}' created by {username}")
492
- return new_project.model_dump()
493
-
494
- @router.put("/projects/{project_id}")
495
- @handle_exceptions
496
- async def update_project(
497
- project_id: int,
498
- update: ProjectUpdate,
499
- username: str = Depends(verify_token)
500
- ):
501
- """Update existing project with race condition handling"""
502
- log_info(f"🔍 Update request for project {project_id} by {username}")
503
- log_info(f"🔍 Received last_update_date: {update.last_update_date}")
504
-
505
- # Mevcut project'i al ve durumunu logla
506
- current_project = ConfigProvider.get_project(project_id)
507
- if current_project:
508
- log_info(f"🔍 Current project last_update_date: {current_project.last_update_date}")
509
- log_info(f"🔍 Current project last_update_user: {current_project.last_update_user}")
510
-
511
- # Optimistic locking kontrolü
512
- result = ConfigProvider.update_project(
513
- project_id,
514
- update.model_dump(),
515
- username,
516
- expected_last_update=update.last_update_date
517
- )
518
-
519
- log_info(f"✅ Project {project_id} updated by {username}")
520
- return result
521
-
522
- @router.delete("/projects/{project_id}")
523
- @handle_exceptions
524
- async def delete_project(project_id: int, username: str = Depends(verify_token)):
525
- """Delete project (soft delete)"""
526
- ConfigProvider.delete_project(project_id, username)
527
-
528
- log_info(f"✅ Project deleted by {username}")
529
- return {"success": True}
530
-
531
- @router.patch("/projects/{project_id}/toggle")
532
- async def toggle_project(project_id: int, username: str = Depends(verify_token)):
533
- """Toggle project enabled status"""
534
- enabled = ConfigProvider.toggle_project(project_id, username)
535
-
536
- log_info(f"✅ Project {'enabled' if enabled else 'disabled'} by {username}")
537
- return {"enabled": enabled}
538
-
539
- # ===================== Import/Export Endpoints =====================
540
- @router.get("/projects/{project_id}/export")
541
- @handle_exceptions
542
- async def export_project(
543
- project_id: int,
544
- username: str = Depends(verify_token)
545
- ):
546
- """Export project as JSON"""
547
- project = ConfigProvider.get_project(project_id)
548
- if not project:
549
- raise HTTPException(status_code=404, detail="Project not found")
550
-
551
- # Prepare export data
552
- export_data = {
553
- "name": project.name,
554
- "caption": project.caption,
555
- "icon": project.icon,
556
- "description": project.description,
557
- "default_locale": project.default_locale,
558
- "supported_locales": project.supported_locales,
559
- "timezone": project.timezone,
560
- "region": project.region,
561
- "versions": []
562
- }
563
-
564
- # Add versions (only non-deleted)
565
- for version in project.versions:
566
- if not getattr(version, 'deleted', False):
567
- version_data = {
568
- "caption": version.caption,
569
- "description": getattr(version, 'description', ''),
570
- "general_prompt": version.general_prompt,
571
- "welcome_prompt": getattr(version, 'welcome_prompt', None),
572
- "llm": version.llm.model_dump() if version.llm else {},
573
- "intents": [intent.model_dump() for intent in version.intents]
574
- }
575
- export_data["versions"].append(version_data)
576
-
577
- log_info(f"✅ Project '{project.name}' exported by {username}")
578
-
579
- return export_data
580
-
581
- @router.post("/projects/import")
582
- @handle_exceptions
583
- async def import_project(
584
- project_data: dict = Body(...),
585
- username: str = Depends(verify_token)
586
- ):
587
- """Import project from JSON"""
588
- # Validate required fields
589
- if not project_data.get('name'):
590
- raise HTTPException(status_code=400, detail="Project name is required")
591
-
592
- # Check for duplicate name
593
- cfg = ConfigProvider.get()
594
- if any(p.name == project_data['name'] for p in cfg.projects if not p.deleted):
595
- raise HTTPException(
596
- status_code=409,
597
- detail=f"Project with name '{project_data['name']}' already exists"
598
- )
599
-
600
- # Create project
601
- new_project_data = {
602
- "name": project_data['name'],
603
- "caption": project_data.get('caption', project_data['name']),
604
- "icon": project_data.get('icon', 'folder'),
605
- "description": project_data.get('description', ''),
606
- "default_locale": project_data.get('default_locale', 'tr'),
607
- "supported_locales": project_data.get('supported_locales', ['tr']),
608
- "timezone": project_data.get('timezone', 'Europe/Istanbul'),
609
- "region": project_data.get('region', 'tr-TR')
610
- }
611
-
612
- # Create project
613
- new_project = ConfigProvider.create_project(new_project_data, username)
614
-
615
- # Import versions
616
- if 'versions' in project_data and project_data['versions']:
617
- # Remove the initial version that was auto-created
618
- if new_project.versions:
619
- new_project.versions.clear()
620
-
621
- # Add imported versions
622
- for idx, version_data in enumerate(project_data['versions']):
623
- version = VersionConfig(
624
- no=idx + 1,
625
- caption=version_data.get('caption', f'Version {idx + 1}'),
626
- description=version_data.get('description', ''),
627
- published=False, # Imported versions are unpublished
628
- deleted=False,
629
- general_prompt=version_data.get('general_prompt', ''),
630
- welcome_prompt=version_data.get('welcome_prompt'),
631
- llm=LLMConfiguration(**version_data.get('llm', {
632
- 'repo_id': '',
633
- 'generation_config': {
634
- 'max_new_tokens': 512,
635
- 'temperature': 0.7,
636
- 'top_p': 0.9
637
- },
638
- 'use_fine_tune': False,
639
- 'fine_tune_zip': ''
640
- })),
641
- intents=[IntentConfig(**intent) for intent in version_data.get('intents', [])],
642
- created_date=get_current_timestamp(),
643
- created_by=username
644
- )
645
- new_project.versions.append(version)
646
-
647
- # Update version counter
648
- new_project.version_id_counter = len(new_project.versions) + 1
649
-
650
- # Save updated project
651
- ConfigProvider.save(cfg, username)
652
-
653
- log_info(f"✅ Project '{new_project.name}' imported by {username}")
654
-
655
- return {"success": True, "project_id": new_project.id, "project_name": new_project.name}
656
-
657
- # ===================== Version Endpoints =====================
658
- @router.get("/projects/{project_id}/versions")
659
- @handle_exceptions
660
- async def list_versions(
661
- project_id: int,
662
- include_deleted: bool = False,
663
- username: str = Depends(verify_token)
664
- ):
665
- """List project versions"""
666
- project = ConfigProvider.get_project(project_id)
667
- if not project:
668
- raise HTTPException(status_code=404, detail="Project not found")
669
-
670
- versions = project.versions
671
-
672
- # Filter deleted if needed
673
- if not include_deleted:
674
- versions = [v for v in versions if not getattr(v, 'deleted', False)]
675
-
676
- return [v.model_dump() for v in versions]
677
-
678
- @router.post("/projects/{project_id}/versions")
679
- @handle_exceptions
680
- async def create_version(
681
- project_id: int,
682
- version_data: VersionCreate,
683
- username: str = Depends(verify_token)
684
- ):
685
- """Create new version"""
686
- new_version = ConfigProvider.create_version(project_id, version_data.model_dump(), username)
687
-
688
- log_info(f"✅ Version created for project {project_id} by {username}")
689
- return new_version.model_dump()
690
-
691
- @router.put("/projects/{project_id}/versions/{version_no}")
692
- @handle_exceptions
693
- async def update_version(
694
- project_id: int,
695
- version_no: int,
696
- update: VersionUpdate,
697
- force: bool = Query(default=False, description="Force update despite conflicts"),
698
- username: str = Depends(verify_token)
699
- ):
700
- """Update version with race condition handling"""
701
- log_debug(f"🔍 Version update request - project: {project_id}, version: {version_no}, user: {username}")
702
-
703
- # Force parametresi kontrolü
704
- if force:
705
- log_warning(f"⚠️ Force update requested for version {version_no} by {username}")
706
-
707
- result = ConfigProvider.update_version(
708
- project_id,
709
- version_no,
710
- update.model_dump(),
711
- username,
712
- expected_last_update=update.last_update_date if not force else None
713
- )
714
-
715
- log_info(f"✅ Version {version_no} updated by {username}")
716
- return result
717
-
718
- @router.post("/projects/{project_id}/versions/{version_no}/publish")
719
- @handle_exceptions
720
- async def publish_version(
721
- project_id: int,
722
- version_no: int,
723
- username: str = Depends(verify_token)
724
- ):
725
- """Publish version"""
726
- project, version = ConfigProvider.publish_version(project_id, version_no, username)
727
-
728
- log_info(f"✅ Version {version_no} published for project '{project.name}' by {username}")
729
-
730
- # Notify LLM provider if project is enabled and provider requires repo info
731
- cfg = ConfigProvider.get()
732
- llm_provider_def = cfg.global_config.get_provider_config("llm", cfg.global_config.llm_provider.name)
733
-
734
- if project.enabled and llm_provider_def and llm_provider_def.requires_repo_info:
735
- try:
736
- await notify_llm_startup(project, version)
737
- except Exception as e:
738
- log_error(f"⚠️ Failed to notify LLM provider", e)
739
- # Don't fail the publish
740
-
741
- return {"success": True}
742
-
743
- @router.delete("/projects/{project_id}/versions/{version_no}")
744
- @handle_exceptions
745
- async def delete_version(
746
- project_id: int,
747
- version_no: int,
748
- username: str = Depends(verify_token)
749
- ):
750
- """Delete version (soft delete)"""
751
- ConfigProvider.delete_version(project_id, version_no, username)
752
-
753
- log_info(f"✅ Version {version_no} deleted for project {project_id} by {username}")
754
- return {"success": True}
755
-
756
- @router.get("/projects/{project_name}/versions")
757
- @handle_exceptions
758
- async def get_project_versions(
759
- project_name: str,
760
- username: str = Depends(verify_token)
761
- ):
762
- """Get all versions of a project for testing"""
763
- cfg = ConfigProvider.get()
764
-
765
- # Find project
766
- project = next((p for p in cfg.projects if p.name == project_name), None)
767
- if not project:
768
- raise HTTPException(status_code=404, detail=f"Project '{project_name}' not found")
769
-
770
- # Return versions with their status
771
- versions = []
772
- for v in project.versions:
773
- if not getattr(v, 'deleted', False):
774
- versions.append({
775
- "version_number": v.no,
776
- "caption": v.caption,
777
- "published": v.published,
778
- "description": getattr(v, 'description', ''),
779
- "intent_count": len(v.intents),
780
- "created_date": getattr(v, 'created_date', None),
781
- "is_current": v.published # Published version is current
782
- })
783
-
784
- return {
785
- "project_name": project_name,
786
- "project_caption": project.caption,
787
- "versions": versions
788
- }
789
-
790
- @router.get("/projects/{project_id}/versions/{version1_id}/compare/{version2_id}")
791
- @handle_exceptions
792
- async def compare_versions(
793
- project_id: int,
794
- version1_no: int,
795
- version2_no: int,
796
- username: str = Depends(verify_token)
797
- ):
798
- """Compare two versions and return differences"""
799
- project = ConfigProvider.get_project(project_id)
800
- if not project:
801
- raise HTTPException(status_code=404, detail="Project not found")
802
-
803
- v1 = next((v for v in project.versions if v.no == version1_no), None)
804
- v2 = next((v for v in project.versions if v.no == version2_no), None)
805
-
806
- if not v1 or not v2:
807
- raise HTTPException(status_code=404, detail="Version not found")
808
-
809
- # Deep comparison
810
- differences = {
811
- 'general_prompt': {
812
- 'changed': v1.general_prompt != v2.general_prompt,
813
- 'v1': v1.general_prompt,
814
- 'v2': v2.general_prompt
815
- },
816
- 'intents': {
817
- 'added': [],
818
- 'removed': [],
819
- 'modified': []
820
- }
821
- }
822
-
823
- # Compare intents
824
- v1_intents = {i.name: i for i in v1.intents}
825
- v2_intents = {i.name: i for i in v2.intents}
826
-
827
- # Find added/removed
828
- differences['intents']['added'] = list(set(v2_intents.keys()) - set(v1_intents.keys()))
829
- differences['intents']['removed'] = list(set(v1_intents.keys()) - set(v2_intents.keys()))
830
-
831
- # Find modified
832
- for intent_name in set(v1_intents.keys()) & set(v2_intents.keys()):
833
- i1, i2 = v1_intents[intent_name], v2_intents[intent_name]
834
- if i1.model_dump() != i2.model_dump():
835
- differences['intents']['modified'].append({
836
- 'name': intent_name,
837
- 'differences': compare_intent_details(i1, i2)
838
- })
839
-
840
- log_info(
841
- f"Version comparison performed",
842
- user=username,
843
- project_id=project_id,
844
- version1_id=version1_id,
845
- version2_id=version2_id
846
- )
847
-
848
- return differences
849
-
850
- # ===================== API Endpoints =====================
851
- @router.get("/apis")
852
- @handle_exceptions
853
- async def list_apis(
854
- include_deleted: bool = False,
855
- username: str = Depends(verify_token)
856
- ):
857
- """List all APIs"""
858
- cfg = ConfigProvider.get()
859
- apis = cfg.apis
860
-
861
- # Filter deleted if needed
862
- if not include_deleted:
863
- apis = [a for a in apis if not getattr(a, 'deleted', False)]
864
-
865
- return [a.model_dump() for a in apis]
866
-
867
- @router.post("/apis")
868
- @handle_exceptions
869
- async def create_api(api: APICreate, username: str = Depends(verify_token)):
870
- """Create new API"""
871
- try:
872
- new_api = ConfigProvider.create_api(api.model_dump(), username)
873
-
874
- log_info(f"✅ API '{api.name}' created by {username}")
875
- return new_api.model_dump()
876
- except DuplicateResourceError as e:
877
- # DuplicateResourceError'ı handle et
878
- raise HTTPException(status_code=409, detail=str(e))
879
-
880
- @router.put("/apis/{api_name}")
881
- @handle_exceptions
882
- async def update_api(
883
- api_name: str,
884
- update: APIUpdate,
885
- username: str = Depends(verify_token)
886
- ):
887
- """Update API configuration with race condition handling"""
888
- result = ConfigProvider.update_api(
889
- api_name,
890
- update.model_dump(),
891
- username,
892
- expected_last_update=update.last_update_date
893
- )
894
-
895
- log_info(f"✅ API '{api_name}' updated by {username}")
896
- return result
897
-
898
- @router.delete("/apis/{api_name}")
899
- @handle_exceptions
900
- async def delete_api(api_name: str, username: str = Depends(verify_token)):
901
- """Delete API (soft delete)"""
902
- ConfigProvider.delete_api(api_name, username)
903
-
904
- log_info(f"✅ API '{api_name}' deleted by {username}")
905
- return {"success": True}
906
-
907
- @router.post("/validate/regex")
908
- @handle_exceptions
909
- async def validate_regex(
910
- request: dict = Body(...),
911
- username: str = Depends(verify_token)
912
- ):
913
- """Validate regex pattern"""
914
- pattern = request.get("pattern", "")
915
- test_value = request.get("test_value", "")
916
-
917
- import re
918
- compiled_regex = re.compile(pattern)
919
- matches = bool(compiled_regex.match(test_value))
920
-
921
- return {
922
- "valid": True,
923
- "matches": matches,
924
- "pattern": pattern,
925
- "test_value": test_value
926
- }
927
-
928
- # ===================== Test Endpoints =====================
929
- @router.post("/test/run-all")
930
- @handle_exceptions
931
- async def run_all_tests(
932
- request: TestRequest,
933
- username: str = Depends(verify_token)
934
- ):
935
- """Run all tests"""
936
- log_info(f"🧪 Running {request.test_type} tests requested by {username}")
937
-
938
- # TODO: Implement test runner
939
- # For now, return mock results
940
- return {
941
- "test_run_id": "test_" + datetime.now().isoformat(),
942
- "status": "running",
943
- "total_tests": 60,
944
- "completed": 0,
945
- "passed": 0,
946
- "failed": 0,
947
- "message": "Test run started"
948
- }
949
-
950
- @router.get("/test/status/{test_run_id}")
951
- @handle_exceptions
952
- async def get_test_status(
953
- test_run_id: str,
954
- username: str = Depends(verify_token)
955
- ):
956
- """Get test run status"""
957
- # TODO: Implement test status tracking
958
- return {
959
- "test_run_id": test_run_id,
960
- "status": "completed",
961
- "total_tests": 60,
962
- "completed": 60,
963
- "passed": 57,
964
- "failed": 3,
965
- "duration": 340.5,
966
- "details": []
967
- }
968
-
969
- # ===================== Activity Log =====================
970
- @router.get("/activity-log")
971
- @handle_exceptions
972
- async def get_activity_log(
973
- limit: int = Query(100, ge=1, le=1000),
974
- entity_type: Optional[str] = None,
975
- username: str = Depends(verify_token)
976
- ):
977
- """Get activity log"""
978
- cfg = ConfigProvider.get()
979
- logs = cfg.activity_log
980
-
981
- # Filter by entity type if specified
982
- if entity_type:
983
- logs = [l for l in logs if l.entity_type == entity_type]
984
-
985
- # Return most recent entries
986
- return logs[-limit:]
987
-
988
- # ===================== Helper Functions =====================
989
- async def notify_llm_startup(project, version):
990
- """Notify LLM provider about project startup"""
991
- from llm_factory import LLMFactory
992
-
993
- try:
994
- llm_provider = LLMFactory.create_provider()
995
-
996
- # Build project config for startup
997
- project_config = {
998
- "name": project.name,
999
- "version_no": version.no,
1000
- "repo_id": version.llm.repo_id,
1001
- "generation_config": version.llm.generation_config,
1002
- "use_fine_tune": version.llm.use_fine_tune,
1003
- "fine_tune_zip": version.llm.fine_tune_zip
1004
- }
1005
-
1006
- success = await llm_provider.startup(project_config)
1007
- if success:
1008
- log_info(f"✅ LLM provider notified for project '{project.name}'")
1009
- else:
1010
- log_info(f"⚠️ LLM provider notification failed for project '{project.name}'")
1011
-
1012
- except Exception as e:
1013
- log_error("❌ Error notifying LLM provider", e)
1014
- raise
1015
-
1016
- # ===================== Cleanup Task =====================
1017
- def cleanup_activity_log():
1018
- """Cleanup old activity log entries"""
1019
- while True:
1020
- try:
1021
- cfg = ConfigProvider.get()
1022
-
1023
- # Keep only last 30 days
1024
- cutoff = datetime.now() - timedelta(days=30)
1025
- cutoff_str = cutoff.isoformat()
1026
-
1027
- original_count = len(cfg.activity_log)
1028
- cfg.activity_log = [
1029
- log for log in cfg.activity_log
1030
- if hasattr(log, 'timestamp') and str(log.timestamp) >= cutoff_str
1031
- ]
1032
-
1033
- if len(cfg.activity_log) < original_count:
1034
- removed = original_count - len(cfg.activity_log)
1035
- log_info(f"🧹 Cleaned up {removed} old activity log entries")
1036
- # ConfigProvider.save(cfg, "system") kullanmalıyız
1037
- ConfigProvider.save(cfg, "system")
1038
-
1039
- except Exception as e:
1040
- log_error("❌ Activity log cleanup error", e)
1041
-
1042
- # Run every hour
1043
- time.sleep(3600)
1044
-
1045
- def start_cleanup_task():
1046
- """Start the cleanup task in background"""
1047
- thread = threading.Thread(target=cleanup_activity_log, daemon=True)
1048
- thread.start()
1049
- log_info("🧹 Activity log cleanup task started")