isa-model 0.3.91__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. isa_model/client.py +732 -573
  2. isa_model/core/cache/redis_cache.py +401 -0
  3. isa_model/core/config/config_manager.py +53 -10
  4. isa_model/core/config.py +1 -1
  5. isa_model/core/database/__init__.py +1 -0
  6. isa_model/core/database/migrations.py +277 -0
  7. isa_model/core/database/supabase_client.py +123 -0
  8. isa_model/core/models/__init__.py +37 -0
  9. isa_model/core/models/model_billing_tracker.py +60 -88
  10. isa_model/core/models/model_manager.py +36 -18
  11. isa_model/core/models/model_repo.py +44 -38
  12. isa_model/core/models/model_statistics_tracker.py +234 -0
  13. isa_model/core/models/model_storage.py +0 -1
  14. isa_model/core/models/model_version_manager.py +959 -0
  15. isa_model/core/pricing_manager.py +2 -249
  16. isa_model/core/resilience/circuit_breaker.py +366 -0
  17. isa_model/core/security/secrets.py +358 -0
  18. isa_model/core/services/__init__.py +2 -4
  19. isa_model/core/services/intelligent_model_selector.py +101 -370
  20. isa_model/core/storage/hf_storage.py +1 -1
  21. isa_model/core/types.py +7 -0
  22. isa_model/deployment/cloud/modal/isa_audio_chatTTS_service.py +520 -0
  23. isa_model/deployment/cloud/modal/isa_audio_fish_service.py +0 -0
  24. isa_model/deployment/cloud/modal/isa_audio_openvoice_service.py +758 -0
  25. isa_model/deployment/cloud/modal/isa_audio_service_v2.py +1044 -0
  26. isa_model/deployment/cloud/modal/isa_embed_rerank_service.py +296 -0
  27. isa_model/deployment/cloud/modal/isa_video_hunyuan_service.py +423 -0
  28. isa_model/deployment/cloud/modal/isa_vision_ocr_service.py +519 -0
  29. isa_model/deployment/cloud/modal/isa_vision_qwen25_service.py +709 -0
  30. isa_model/deployment/cloud/modal/isa_vision_table_service.py +467 -323
  31. isa_model/deployment/cloud/modal/isa_vision_ui_service.py +607 -180
  32. isa_model/deployment/cloud/modal/isa_vision_ui_service_optimized.py +660 -0
  33. isa_model/deployment/core/deployment_manager.py +6 -4
  34. isa_model/deployment/services/auto_hf_modal_deployer.py +894 -0
  35. isa_model/eval/benchmarks/__init__.py +27 -0
  36. isa_model/eval/benchmarks/multimodal_datasets.py +460 -0
  37. isa_model/eval/benchmarks.py +244 -12
  38. isa_model/eval/evaluators/__init__.py +8 -2
  39. isa_model/eval/evaluators/audio_evaluator.py +727 -0
  40. isa_model/eval/evaluators/embedding_evaluator.py +742 -0
  41. isa_model/eval/evaluators/vision_evaluator.py +564 -0
  42. isa_model/eval/example_evaluation.py +395 -0
  43. isa_model/eval/factory.py +272 -5
  44. isa_model/eval/isa_benchmarks.py +700 -0
  45. isa_model/eval/isa_integration.py +582 -0
  46. isa_model/eval/metrics.py +159 -6
  47. isa_model/eval/tests/unit/test_basic.py +396 -0
  48. isa_model/inference/ai_factory.py +44 -8
  49. isa_model/inference/services/audio/__init__.py +21 -0
  50. isa_model/inference/services/audio/base_realtime_service.py +225 -0
  51. isa_model/inference/services/audio/isa_tts_service.py +0 -0
  52. isa_model/inference/services/audio/openai_realtime_service.py +320 -124
  53. isa_model/inference/services/audio/openai_stt_service.py +32 -6
  54. isa_model/inference/services/base_service.py +17 -1
  55. isa_model/inference/services/embedding/__init__.py +13 -0
  56. isa_model/inference/services/embedding/base_embed_service.py +111 -8
  57. isa_model/inference/services/embedding/isa_embed_service.py +305 -0
  58. isa_model/inference/services/embedding/openai_embed_service.py +2 -4
  59. isa_model/inference/services/embedding/tests/test_embedding.py +222 -0
  60. isa_model/inference/services/img/__init__.py +2 -2
  61. isa_model/inference/services/img/base_image_gen_service.py +24 -7
  62. isa_model/inference/services/img/replicate_image_gen_service.py +84 -422
  63. isa_model/inference/services/img/services/replicate_face_swap.py +193 -0
  64. isa_model/inference/services/img/services/replicate_flux.py +226 -0
  65. isa_model/inference/services/img/services/replicate_flux_kontext.py +219 -0
  66. isa_model/inference/services/img/services/replicate_sticker_maker.py +249 -0
  67. isa_model/inference/services/img/tests/test_img_client.py +297 -0
  68. isa_model/inference/services/llm/base_llm_service.py +30 -6
  69. isa_model/inference/services/llm/helpers/llm_adapter.py +63 -9
  70. isa_model/inference/services/llm/ollama_llm_service.py +2 -1
  71. isa_model/inference/services/llm/openai_llm_service.py +652 -55
  72. isa_model/inference/services/llm/yyds_llm_service.py +2 -1
  73. isa_model/inference/services/vision/__init__.py +5 -5
  74. isa_model/inference/services/vision/base_vision_service.py +118 -185
  75. isa_model/inference/services/vision/helpers/image_utils.py +11 -5
  76. isa_model/inference/services/vision/isa_vision_service.py +573 -0
  77. isa_model/inference/services/vision/tests/test_ocr_client.py +284 -0
  78. isa_model/serving/api/fastapi_server.py +88 -16
  79. isa_model/serving/api/middleware/auth.py +311 -0
  80. isa_model/serving/api/middleware/security.py +278 -0
  81. isa_model/serving/api/routes/analytics.py +486 -0
  82. isa_model/serving/api/routes/deployments.py +339 -0
  83. isa_model/serving/api/routes/evaluations.py +579 -0
  84. isa_model/serving/api/routes/logs.py +430 -0
  85. isa_model/serving/api/routes/settings.py +582 -0
  86. isa_model/serving/api/routes/unified.py +324 -165
  87. isa_model/serving/api/startup.py +304 -0
  88. isa_model/serving/modal_proxy_server.py +249 -0
  89. isa_model/training/__init__.py +100 -6
  90. isa_model/training/core/__init__.py +4 -1
  91. isa_model/training/examples/intelligent_training_example.py +281 -0
  92. isa_model/training/intelligent/__init__.py +25 -0
  93. isa_model/training/intelligent/decision_engine.py +643 -0
  94. isa_model/training/intelligent/intelligent_factory.py +888 -0
  95. isa_model/training/intelligent/knowledge_base.py +751 -0
  96. isa_model/training/intelligent/resource_optimizer.py +839 -0
  97. isa_model/training/intelligent/task_classifier.py +576 -0
  98. isa_model/training/storage/__init__.py +24 -0
  99. isa_model/training/storage/core_integration.py +439 -0
  100. isa_model/training/storage/training_repository.py +552 -0
  101. isa_model/training/storage/training_storage.py +628 -0
  102. {isa_model-0.3.91.dist-info → isa_model-0.4.0.dist-info}/METADATA +13 -1
  103. isa_model-0.4.0.dist-info/RECORD +182 -0
  104. isa_model/deployment/cloud/modal/isa_vision_doc_service.py +0 -766
  105. isa_model/deployment/cloud/modal/register_models.py +0 -321
  106. isa_model/inference/adapter/unified_api.py +0 -248
  107. isa_model/inference/services/helpers/stacked_config.py +0 -148
  108. isa_model/inference/services/img/flux_professional_service.py +0 -603
  109. isa_model/inference/services/img/helpers/base_stacked_service.py +0 -274
  110. isa_model/inference/services/others/table_transformer_service.py +0 -61
  111. isa_model/inference/services/vision/doc_analysis_service.py +0 -640
  112. isa_model/inference/services/vision/helpers/base_stacked_service.py +0 -274
  113. isa_model/inference/services/vision/ui_analysis_service.py +0 -823
  114. isa_model/scripts/inference_tracker.py +0 -283
  115. isa_model/scripts/mlflow_manager.py +0 -379
  116. isa_model/scripts/model_registry.py +0 -465
  117. isa_model/scripts/register_models.py +0 -370
  118. isa_model/scripts/register_models_with_embeddings.py +0 -510
  119. isa_model/scripts/start_mlflow.py +0 -95
  120. isa_model/scripts/training_tracker.py +0 -257
  121. isa_model-0.3.91.dist-info/RECORD +0 -138
  122. {isa_model-0.3.91.dist-info → isa_model-0.4.0.dist-info}/WHEEL +0 -0
  123. {isa_model-0.3.91.dist-info → isa_model-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,959 @@
1
+ """
2
+ Model Version Manager for Core Module
3
+
4
+ Manages model versions, lineage tracking, and lifecycle management.
5
+ Integrates with the existing ModelRegistry and provides version control
6
+ for all models in the system.
7
+
8
+ This is the central version management system that works with:
9
+ - Training module (for newly trained models)
10
+ - External model imports
11
+ - Model updates and fine-tuning
12
+ - Deployment and serving
13
+ """
14
+
15
+ import logging
16
+ import json
17
+ from typing import Dict, List, Optional, Any, Tuple
18
+ from datetime import datetime
19
+ from dataclasses import dataclass, asdict
20
+ from enum import Enum
21
+
22
+ try:
23
+ from ..database.supabase_client import get_supabase_client
24
+ SUPABASE_AVAILABLE = True
25
+ except ImportError:
26
+ SUPABASE_AVAILABLE = False
27
+
28
+ from .model_repo import ModelRegistry, ModelType, ModelCapability
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class VersionType(str, Enum):
34
+ """Version increment types"""
35
+ MAJOR = "major" # Breaking changes, new architecture
36
+ MINOR = "minor" # New features, significant improvements
37
+ PATCH = "patch" # Bug fixes, small improvements
38
+
39
+
40
+ @dataclass
41
+ class ModelVersion:
42
+ """Model version information"""
43
+ version_id: str # Unique version identifier
44
+ model_id: str # Base model identifier
45
+ version_number: str # Semantic version (e.g., "1.2.3")
46
+ version_type: VersionType # Type of version increment
47
+
48
+ # Model information
49
+ model_path: Optional[str] = None
50
+ model_size_mb: Optional[float] = None
51
+ model_format: Optional[str] = None
52
+
53
+ # Training/source information
54
+ source_type: Optional[str] = None # "training", "import", "fine_tune"
55
+ source_id: Optional[str] = None # Training job ID, import ID, etc.
56
+ base_model: Optional[str] = None
57
+ dataset_source: Optional[str] = None
58
+
59
+ # Performance metrics
60
+ performance_metrics: Optional[Dict[str, float]] = None
61
+ quality_score: Optional[float] = None
62
+ benchmark_scores: Optional[Dict[str, float]] = None
63
+
64
+ # Metadata
65
+ description: Optional[str] = None
66
+ tags: Optional[Dict[str, str]] = None
67
+ created_at: Optional[datetime] = None
68
+ created_by: Optional[str] = None
69
+
70
+ # Lineage and relationships
71
+ parent_version: Optional[str] = None
72
+ derived_from: Optional[List[str]] = None
73
+ children_versions: Optional[List[str]] = None
74
+
75
+ # Status and flags
76
+ status: str = "active" # "active", "deprecated", "archived"
77
+ is_production: bool = False
78
+ is_default: bool = False
79
+
80
+ # Core integration
81
+ core_model_id: Optional[str] = None
82
+ model_type: Optional[ModelType] = None
83
+ capabilities: Optional[List[ModelCapability]] = None
84
+
85
+
86
+ class ModelVersionManager:
87
+ """
88
+ Central model version management system.
89
+
90
+ Provides comprehensive version control for all models in the system,
91
+ including trained models, imported models, and fine-tuned variants.
92
+
93
+ Example:
94
+ ```python
95
+ from isa_model.core.models import ModelVersionManager
96
+
97
+ version_manager = ModelVersionManager()
98
+
99
+ # Create version from training
100
+ version = version_manager.create_version_from_training(
101
+ training_job_id="training_abc123",
102
+ model_path="/path/to/model",
103
+ base_model="google/gemma-2-4b-it",
104
+ performance_metrics={"accuracy": 0.95}
105
+ )
106
+
107
+ # List all versions of a model
108
+ versions = version_manager.list_versions("gemma_2_4b_it_chat")
109
+
110
+ # Compare versions
111
+ comparison = version_manager.compare_versions("v1.0.0", "v1.1.0")
112
+ ```
113
+ """
114
+
115
+ def __init__(self, model_registry: Optional[ModelRegistry] = None):
116
+ """
117
+ Initialize version manager.
118
+
119
+ Args:
120
+ model_registry: Core model registry instance
121
+ """
122
+ self.model_registry = model_registry or ModelRegistry()
123
+
124
+ # Initialize Supabase connection
125
+ if SUPABASE_AVAILABLE:
126
+ try:
127
+ self.supabase_client = get_supabase_client()
128
+ self.supabase_available = True
129
+ self._ensure_version_tables()
130
+ except Exception as e:
131
+ logger.warning(f"Supabase not available for version management: {e}")
132
+ self.supabase_client = None
133
+ self.supabase_available = False
134
+ else:
135
+ self.supabase_client = None
136
+ self.supabase_available = False
137
+
138
+ logger.info(f"ModelVersionManager initialized (supabase: {self.supabase_available})")
139
+
140
+ def create_version_from_training(self,
141
+ training_job_id: str,
142
+ model_path: str,
143
+ base_model: str,
144
+ task_type: str = "text_generation",
145
+ performance_metrics: Optional[Dict[str, float]] = None,
146
+ version_type: VersionType = VersionType.MINOR,
147
+ description: Optional[str] = None,
148
+ created_by: Optional[str] = None,
149
+ tags: Optional[Dict[str, str]] = None) -> ModelVersion:
150
+ """
151
+ Create a new model version from a training job.
152
+
153
+ Args:
154
+ training_job_id: ID of the training job
155
+ model_path: Path to the trained model
156
+ base_model: Base model that was fine-tuned
157
+ task_type: Type of task the model was trained for
158
+ performance_metrics: Model performance metrics
159
+ version_type: Type of version increment
160
+ description: Version description
161
+ created_by: User who created this version
162
+ tags: Additional tags
163
+
164
+ Returns:
165
+ Created model version
166
+ """
167
+ try:
168
+ # Generate model ID from base model and task
169
+ model_id = self._generate_model_id(base_model, task_type)
170
+
171
+ # Get next version number
172
+ version_number = self._get_next_version(model_id, version_type)
173
+
174
+ # Determine model type and capabilities
175
+ model_type = self._infer_model_type(task_type)
176
+ capabilities = self._infer_capabilities(task_type)
177
+
178
+ # Create version
179
+ version = ModelVersion(
180
+ version_id=f"{model_id}:v{version_number}",
181
+ model_id=model_id,
182
+ version_number=version_number,
183
+ version_type=version_type,
184
+ model_path=model_path,
185
+ source_type="training",
186
+ source_id=training_job_id,
187
+ base_model=base_model,
188
+ performance_metrics=performance_metrics,
189
+ description=description or f"Trained {task_type} model v{version_number}",
190
+ tags=tags or {},
191
+ created_at=datetime.now(),
192
+ created_by=created_by,
193
+ model_type=model_type,
194
+ capabilities=capabilities
195
+ )
196
+
197
+ # Calculate quality score
198
+ if performance_metrics:
199
+ version.quality_score = self._calculate_quality_score(performance_metrics)
200
+
201
+ # Get model size
202
+ version.model_size_mb = self._calculate_model_size(model_path)
203
+
204
+ # Save version
205
+ success = self._save_version(version)
206
+ if not success:
207
+ raise Exception("Failed to save model version")
208
+
209
+ # Register in core model registry
210
+ core_model_id = self._register_in_core_registry(version)
211
+ if core_model_id:
212
+ version.core_model_id = core_model_id
213
+ self._update_version(version)
214
+
215
+ logger.info(f"Created model version from training: {version.version_id}")
216
+ return version
217
+
218
+ except Exception as e:
219
+ logger.error(f"Failed to create version from training: {e}")
220
+ raise
221
+
222
+ def create_version_from_import(self,
223
+ model_path: str,
224
+ model_name: str,
225
+ model_type: ModelType,
226
+ capabilities: List[ModelCapability],
227
+ version_type: VersionType = VersionType.MAJOR,
228
+ description: Optional[str] = None,
229
+ created_by: Optional[str] = None,
230
+ tags: Optional[Dict[str, str]] = None,
231
+ metadata: Optional[Dict[str, Any]] = None) -> ModelVersion:
232
+ """
233
+ Create a new model version from an imported model.
234
+
235
+ Args:
236
+ model_path: Path to the imported model
237
+ model_name: Name of the model
238
+ model_type: Type of the model
239
+ capabilities: Model capabilities
240
+ version_type: Type of version increment
241
+ description: Version description
242
+ created_by: User who imported this model
243
+ tags: Additional tags
244
+ metadata: Additional metadata
245
+
246
+ Returns:
247
+ Created model version
248
+ """
249
+ try:
250
+ # Use model name as model ID
251
+ model_id = self._clean_model_name(model_name)
252
+
253
+ # Get next version number
254
+ version_number = self._get_next_version(model_id, version_type)
255
+
256
+ # Create version
257
+ version = ModelVersion(
258
+ version_id=f"{model_id}:v{version_number}",
259
+ model_id=model_id,
260
+ version_number=version_number,
261
+ version_type=version_type,
262
+ model_path=model_path,
263
+ source_type="import",
264
+ description=description or f"Imported {model_name} v{version_number}",
265
+ tags=tags or {},
266
+ created_at=datetime.now(),
267
+ created_by=created_by,
268
+ model_type=model_type,
269
+ capabilities=capabilities
270
+ )
271
+
272
+ # Get model size
273
+ version.model_size_mb = self._calculate_model_size(model_path)
274
+
275
+ # Save version
276
+ success = self._save_version(version)
277
+ if not success:
278
+ raise Exception("Failed to save model version")
279
+
280
+ # Register in core model registry
281
+ core_model_id = self._register_in_core_registry(version, metadata)
282
+ if core_model_id:
283
+ version.core_model_id = core_model_id
284
+ self._update_version(version)
285
+
286
+ logger.info(f"Created model version from import: {version.version_id}")
287
+ return version
288
+
289
+ except Exception as e:
290
+ logger.error(f"Failed to create version from import: {e}")
291
+ raise
292
+
293
+ def get_version(self, version_id: str) -> Optional[ModelVersion]:
294
+ """Get model version by ID."""
295
+ try:
296
+ return self._load_version(version_id)
297
+ except Exception as e:
298
+ logger.error(f"Failed to get version {version_id}: {e}")
299
+ return None
300
+
301
+ def list_versions(self,
302
+ model_id: Optional[str] = None,
303
+ status: Optional[str] = None,
304
+ created_by: Optional[str] = None,
305
+ limit: int = 50) -> List[ModelVersion]:
306
+ """
307
+ List model versions with filtering.
308
+
309
+ Args:
310
+ model_id: Filter by model ID
311
+ status: Filter by status ("active", "deprecated", "archived")
312
+ created_by: Filter by creator
313
+ limit: Maximum number of versions
314
+
315
+ Returns:
316
+ List of model versions
317
+ """
318
+ try:
319
+ return self._list_versions(model_id, status, created_by, limit)
320
+ except Exception as e:
321
+ logger.error(f"Failed to list versions: {e}")
322
+ return []
323
+
324
+ def get_latest_version(self, model_id: str, status: str = "active") -> Optional[ModelVersion]:
325
+ """Get latest version of a model."""
326
+ try:
327
+ versions = self.list_versions(model_id=model_id, status=status, limit=1)
328
+ return versions[0] if versions else None
329
+ except Exception as e:
330
+ logger.error(f"Failed to get latest version for {model_id}: {e}")
331
+ return None
332
+
333
+ def get_default_version(self, model_id: str) -> Optional[ModelVersion]:
334
+ """Get default version of a model."""
335
+ try:
336
+ versions = self.list_versions(model_id=model_id, limit=100)
337
+ default_versions = [v for v in versions if v.is_default]
338
+ return default_versions[0] if default_versions else None
339
+ except Exception as e:
340
+ logger.error(f"Failed to get default version for {model_id}: {e}")
341
+ return None
342
+
343
+ def set_default_version(self, version_id: str) -> bool:
344
+ """Set a version as the default for its model."""
345
+ try:
346
+ version = self.get_version(version_id)
347
+ if not version:
348
+ return False
349
+
350
+ # Remove default flag from all versions of this model
351
+ versions = self.list_versions(model_id=version.model_id, limit=1000)
352
+ for v in versions:
353
+ if v.is_default:
354
+ v.is_default = False
355
+ self._update_version(v)
356
+
357
+ # Set this version as default
358
+ version.is_default = True
359
+ success = self._update_version(version)
360
+
361
+ if success:
362
+ logger.info(f"Set default version: {version_id}")
363
+
364
+ return success
365
+
366
+ except Exception as e:
367
+ logger.error(f"Failed to set default version {version_id}: {e}")
368
+ return False
369
+
370
+ def deprecate_version(self, version_id: str, reason: Optional[str] = None) -> bool:
371
+ """Deprecate a model version."""
372
+ try:
373
+ version = self.get_version(version_id)
374
+ if not version:
375
+ return False
376
+
377
+ version.status = "deprecated"
378
+ if reason:
379
+ if not version.tags:
380
+ version.tags = {}
381
+ version.tags["deprecation_reason"] = reason
382
+
383
+ success = self._update_version(version)
384
+
385
+ if success:
386
+ logger.info(f"Deprecated version: {version_id}")
387
+
388
+ return success
389
+
390
+ except Exception as e:
391
+ logger.error(f"Failed to deprecate version {version_id}: {e}")
392
+ return False
393
+
394
+ def compare_versions(self, version_id_1: str, version_id_2: str) -> Dict[str, Any]:
395
+ """
396
+ Compare two model versions.
397
+
398
+ Args:
399
+ version_id_1: First version ID
400
+ version_id_2: Second version ID
401
+
402
+ Returns:
403
+ Comparison results
404
+ """
405
+ try:
406
+ v1 = self.get_version(version_id_1)
407
+ v2 = self.get_version(version_id_2)
408
+
409
+ if not v1 or not v2:
410
+ return {"error": "One or both versions not found"}
411
+
412
+ comparison = {
413
+ "version_1": {
414
+ "id": v1.version_id,
415
+ "number": v1.version_number,
416
+ "created_at": v1.created_at.isoformat() if v1.created_at else None,
417
+ "quality_score": v1.quality_score,
418
+ "model_size_mb": v1.model_size_mb,
419
+ "status": v1.status
420
+ },
421
+ "version_2": {
422
+ "id": v2.version_id,
423
+ "number": v2.version_number,
424
+ "created_at": v2.created_at.isoformat() if v2.created_at else None,
425
+ "quality_score": v2.quality_score,
426
+ "model_size_mb": v2.model_size_mb,
427
+ "status": v2.status
428
+ },
429
+ "differences": {}
430
+ }
431
+
432
+ # Compare quality scores
433
+ if v1.quality_score is not None and v2.quality_score is not None:
434
+ comparison["differences"]["quality_improvement"] = v2.quality_score - v1.quality_score
435
+
436
+ # Compare model sizes
437
+ if v1.model_size_mb is not None and v2.model_size_mb is not None:
438
+ comparison["differences"]["size_change_mb"] = v2.model_size_mb - v1.model_size_mb
439
+
440
+ # Compare performance metrics
441
+ if v1.performance_metrics and v2.performance_metrics:
442
+ metrics_diff = {}
443
+ all_metrics = set(v1.performance_metrics.keys()) | set(v2.performance_metrics.keys())
444
+
445
+ for metric in all_metrics:
446
+ val1 = v1.performance_metrics.get(metric)
447
+ val2 = v2.performance_metrics.get(metric)
448
+
449
+ if val1 is not None and val2 is not None:
450
+ metrics_diff[metric] = {
451
+ "v1": val1,
452
+ "v2": val2,
453
+ "difference": val2 - val1,
454
+ "improvement": val2 > val1
455
+ }
456
+
457
+ comparison["differences"]["metrics"] = metrics_diff
458
+
459
+ return comparison
460
+
461
+ except Exception as e:
462
+ logger.error(f"Failed to compare versions: {e}")
463
+ return {"error": str(e)}
464
+
465
+ def get_version_lineage(self, version_id: str, depth: int = 5) -> Dict[str, Any]:
466
+ """
467
+ Get version lineage tree.
468
+
469
+ Args:
470
+ version_id: Version ID to trace
471
+ depth: Maximum depth to traverse
472
+
473
+ Returns:
474
+ Lineage information
475
+ """
476
+ try:
477
+ version = self.get_version(version_id)
478
+ if not version:
479
+ return {"error": "Version not found"}
480
+
481
+ lineage = {
482
+ "version": version.version_id,
483
+ "model_id": version.model_id,
484
+ "ancestors": self._get_ancestors(version_id, depth),
485
+ "descendants": self._get_descendants(version_id, depth)
486
+ }
487
+
488
+ return lineage
489
+
490
+ except Exception as e:
491
+ logger.error(f"Failed to get version lineage: {e}")
492
+ return {"error": str(e)}
493
+
494
+ def delete_version(self, version_id: str, force: bool = False) -> bool:
495
+ """
496
+ Delete a model version.
497
+
498
+ Args:
499
+ version_id: Version ID to delete
500
+ force: Force deletion even if version is default or production
501
+
502
+ Returns:
503
+ True if successful
504
+ """
505
+ try:
506
+ version = self.get_version(version_id)
507
+ if not version:
508
+ return False
509
+
510
+ # Check if version can be deleted
511
+ if not force:
512
+ if version.is_default:
513
+ raise ValueError("Cannot delete default version. Set another version as default first.")
514
+ if version.is_production:
515
+ raise ValueError("Cannot delete production version. Mark as non-production first.")
516
+
517
+ # Unregister from core model registry
518
+ if version.core_model_id:
519
+ try:
520
+ self.model_registry.unregister_model(version.core_model_id)
521
+ except Exception as e:
522
+ logger.warning(f"Failed to unregister from core registry: {e}")
523
+
524
+ # Delete version data
525
+ success = self._delete_version(version_id)
526
+
527
+ if success:
528
+ logger.info(f"Deleted model version: {version_id}")
529
+
530
+ return success
531
+
532
+ except Exception as e:
533
+ logger.error(f"Failed to delete version {version_id}: {e}")
534
+ return False
535
+
536
+ def get_statistics(self) -> Dict[str, Any]:
537
+ """Get version management statistics."""
538
+ try:
539
+ all_versions = self.list_versions(limit=10000)
540
+
541
+ stats = {
542
+ "total_versions": len(all_versions),
543
+ "unique_models": len(set(v.model_id for v in all_versions)),
544
+ "status_breakdown": {},
545
+ "source_breakdown": {},
546
+ "version_type_breakdown": {},
547
+ "average_quality_score": 0.0,
548
+ "total_model_size_gb": 0.0
549
+ }
550
+
551
+ quality_scores = []
552
+ total_size = 0.0
553
+
554
+ for version in all_versions:
555
+ # Count by status
556
+ status = version.status
557
+ stats["status_breakdown"][status] = stats["status_breakdown"].get(status, 0) + 1
558
+
559
+ # Count by source type
560
+ source = version.source_type or "unknown"
561
+ stats["source_breakdown"][source] = stats["source_breakdown"].get(source, 0) + 1
562
+
563
+ # Count by version type
564
+ vtype = version.version_type.value
565
+ stats["version_type_breakdown"][vtype] = stats["version_type_breakdown"].get(vtype, 0) + 1
566
+
567
+ # Collect quality scores
568
+ if version.quality_score is not None:
569
+ quality_scores.append(version.quality_score)
570
+
571
+ # Sum model sizes
572
+ if version.model_size_mb is not None:
573
+ total_size += version.model_size_mb
574
+
575
+ # Calculate averages
576
+ if quality_scores:
577
+ stats["average_quality_score"] = sum(quality_scores) / len(quality_scores)
578
+
579
+ stats["total_model_size_gb"] = total_size / 1024.0
580
+
581
+ return stats
582
+
583
+ except Exception as e:
584
+ logger.error(f"Failed to get statistics: {e}")
585
+ return {"error": str(e)}
586
+
587
+ # Private methods
588
+
589
+ def _generate_model_id(self, base_model: str, task_type: str) -> str:
590
+ """Generate model ID from base model and task type."""
591
+ clean_model = self._clean_model_name(base_model)
592
+ clean_task = task_type.replace("-", "_").replace(" ", "_")
593
+ return f"{clean_model}_{clean_task}"
594
+
595
+ def _clean_model_name(self, model_name: str) -> str:
596
+ """Clean model name for use as ID."""
597
+ return model_name.replace("/", "_").replace("-", "_").replace(" ", "_").lower()
598
+
599
+ def _get_next_version(self, model_id: str, version_type: VersionType) -> str:
600
+ """Get next semantic version number."""
601
+ try:
602
+ versions = self.list_versions(model_id=model_id, limit=1000)
603
+
604
+ if not versions:
605
+ return "1.0.0"
606
+
607
+ # Parse latest version number
608
+ latest = versions[0] # Assuming sorted by version desc
609
+ version_parts = latest.version_number.split(".")
610
+
611
+ if len(version_parts) != 3:
612
+ return "1.0.0"
613
+
614
+ major, minor, patch = map(int, version_parts)
615
+
616
+ # Increment based on type
617
+ if version_type == VersionType.MAJOR:
618
+ major += 1
619
+ minor = 0
620
+ patch = 0
621
+ elif version_type == VersionType.MINOR:
622
+ minor += 1
623
+ patch = 0
624
+ else: # PATCH
625
+ patch += 1
626
+
627
+ return f"{major}.{minor}.{patch}"
628
+
629
+ except Exception:
630
+ return "1.0.0"
631
+
632
+ def _infer_model_type(self, task_type: str) -> ModelType:
633
+ """Infer model type from task type."""
634
+ task_lower = task_type.lower()
635
+
636
+ if "embedding" in task_lower:
637
+ return ModelType.EMBEDDING
638
+ elif "image" in task_lower:
639
+ return ModelType.IMAGE
640
+ elif "audio" in task_lower:
641
+ return ModelType.AUDIO
642
+ elif "vision" in task_lower:
643
+ return ModelType.VISION
644
+ else:
645
+ return ModelType.LLM
646
+
647
+ def _infer_capabilities(self, task_type: str) -> List[ModelCapability]:
648
+ """Infer model capabilities from task type."""
649
+ task_lower = task_type.lower()
650
+ capabilities = []
651
+
652
+ if "chat" in task_lower or "conversation" in task_lower:
653
+ capabilities.extend([ModelCapability.CHAT, ModelCapability.TEXT_GENERATION])
654
+ elif "classification" in task_lower:
655
+ capabilities.append(ModelCapability.TEXT_GENERATION)
656
+ elif "embedding" in task_lower:
657
+ capabilities.append(ModelCapability.EMBEDDING)
658
+ elif "reasoning" in task_lower:
659
+ capabilities.append(ModelCapability.REASONING)
660
+ elif "image" in task_lower:
661
+ if "generation" in task_lower:
662
+ capabilities.append(ModelCapability.IMAGE_GENERATION)
663
+ else:
664
+ capabilities.append(ModelCapability.IMAGE_ANALYSIS)
665
+ else:
666
+ capabilities.append(ModelCapability.TEXT_GENERATION)
667
+
668
+ return capabilities
669
+
670
+ def _calculate_quality_score(self, metrics: Dict[str, float]) -> float:
671
+ """Calculate overall quality score from metrics."""
672
+ try:
673
+ score = 0.0
674
+ count = 0
675
+
676
+ # Common metrics (higher is better)
677
+ for metric in ["accuracy", "f1_score", "bleu_score"]:
678
+ if metric in metrics:
679
+ score += metrics[metric]
680
+ count += 1
681
+
682
+ # Loss metrics (lower is better, so invert)
683
+ for loss_metric in ["validation_loss", "loss"]:
684
+ if loss_metric in metrics and metrics[loss_metric] > 0:
685
+ score += max(0, 1.0 - metrics[loss_metric])
686
+ count += 1
687
+
688
+ return score / count if count > 0 else 0.5
689
+
690
+ except Exception:
691
+ return 0.5
692
+
693
+ def _calculate_model_size(self, model_path: str) -> Optional[float]:
694
+ """Calculate model size in MB."""
695
+ try:
696
+ import os
697
+
698
+ if not os.path.exists(model_path):
699
+ return None
700
+
701
+ total_size = 0
702
+
703
+ if os.path.isfile(model_path):
704
+ total_size = os.path.getsize(model_path)
705
+ else:
706
+ for dirpath, dirnames, filenames in os.walk(model_path):
707
+ for filename in filenames:
708
+ filepath = os.path.join(dirpath, filename)
709
+ if os.path.exists(filepath):
710
+ total_size += os.path.getsize(filepath)
711
+
712
+ return total_size / (1024 * 1024) # Convert to MB
713
+
714
+ except Exception:
715
+ return None
716
+
717
+ def _register_in_core_registry(self,
718
+ version: ModelVersion,
719
+ additional_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]:
720
+ """Register model version in core registry."""
721
+ try:
722
+ metadata = {
723
+ "version_id": version.version_id,
724
+ "version_number": version.version_number,
725
+ "source_type": version.source_type,
726
+ "source_id": version.source_id,
727
+ "base_model": version.base_model,
728
+ "quality_score": version.quality_score,
729
+ "model_size_mb": version.model_size_mb,
730
+ "created_at": version.created_at.isoformat() if version.created_at else None,
731
+ "provider": "isa_model_core"
732
+ }
733
+
734
+ if additional_metadata:
735
+ metadata.update(additional_metadata)
736
+
737
+ success = self.model_registry.register_model(
738
+ model_id=version.version_id,
739
+ model_type=version.model_type,
740
+ capabilities=version.capabilities or [],
741
+ metadata=metadata
742
+ )
743
+
744
+ return version.version_id if success else None
745
+
746
+ except Exception as e:
747
+ logger.warning(f"Failed to register in core registry: {e}")
748
+ return None
749
+
750
+ def _ensure_version_tables(self):
751
+ """Ensure version management tables exist."""
752
+ if not self.supabase_available:
753
+ return
754
+
755
+ try:
756
+ # Test if model_versions table exists
757
+ self.supabase_client.table('model_versions').select('version_id').limit(1).execute()
758
+ except Exception:
759
+ logger.warning("model_versions table might not exist - would need database migration")
760
+
761
+ def _save_version(self, version: ModelVersion) -> bool:
762
+ """Save model version to storage."""
763
+ if not self.supabase_available:
764
+ logger.warning("No storage backend available for version management")
765
+ return False
766
+
767
+ try:
768
+ version_data = {
769
+ "version_id": version.version_id,
770
+ "model_id": version.model_id,
771
+ "version_number": version.version_number,
772
+ "version_type": version.version_type.value,
773
+ "model_path": version.model_path,
774
+ "model_size_mb": version.model_size_mb,
775
+ "model_format": version.model_format,
776
+ "source_type": version.source_type,
777
+ "source_id": version.source_id,
778
+ "base_model": version.base_model,
779
+ "dataset_source": version.dataset_source,
780
+ "performance_metrics": json.dumps(version.performance_metrics) if version.performance_metrics else None,
781
+ "quality_score": version.quality_score,
782
+ "benchmark_scores": json.dumps(version.benchmark_scores) if version.benchmark_scores else None,
783
+ "description": version.description,
784
+ "tags": json.dumps(version.tags) if version.tags else None,
785
+ "created_at": version.created_at.isoformat() if version.created_at else None,
786
+ "created_by": version.created_by,
787
+ "parent_version": version.parent_version,
788
+ "derived_from": json.dumps(version.derived_from) if version.derived_from else None,
789
+ "children_versions": json.dumps(version.children_versions) if version.children_versions else None,
790
+ "status": version.status,
791
+ "is_production": version.is_production,
792
+ "is_default": version.is_default,
793
+ "core_model_id": version.core_model_id,
794
+ "model_type": version.model_type.value if version.model_type else None,
795
+ "capabilities": json.dumps([cap.value for cap in version.capabilities]) if version.capabilities else None
796
+ }
797
+
798
+ result = self.supabase_client.table('model_versions').upsert(version_data).execute()
799
+ return bool(result.data)
800
+
801
+ except Exception as e:
802
+ logger.error(f"Failed to save version: {e}")
803
+ return False
804
+
805
+ def _load_version(self, version_id: str) -> Optional[ModelVersion]:
806
+ """Load model version from storage."""
807
+ if not self.supabase_available:
808
+ return None
809
+
810
+ try:
811
+ result = self.supabase_client.table('model_versions').select('*').eq('version_id', version_id).execute()
812
+
813
+ if not result.data:
814
+ return None
815
+
816
+ return self._dict_to_version(result.data[0])
817
+
818
+ except Exception as e:
819
+ logger.error(f"Failed to load version {version_id}: {e}")
820
+ return None
821
+
822
+ def _list_versions(self,
823
+ model_id: Optional[str] = None,
824
+ status: Optional[str] = None,
825
+ created_by: Optional[str] = None,
826
+ limit: int = 50) -> List[ModelVersion]:
827
+ """List model versions from storage."""
828
+ if not self.supabase_available:
829
+ return []
830
+
831
+ try:
832
+ query = self.supabase_client.table('model_versions').select('*')
833
+
834
+ if model_id:
835
+ query = query.eq('model_id', model_id)
836
+ if status:
837
+ query = query.eq('status', status)
838
+ if created_by:
839
+ query = query.eq('created_by', created_by)
840
+
841
+ result = query.order('created_at', desc=True).limit(limit).execute()
842
+
843
+ versions = []
844
+ for data in result.data:
845
+ version = self._dict_to_version(data)
846
+ if version:
847
+ versions.append(version)
848
+
849
+ return versions
850
+
851
+ except Exception as e:
852
+ logger.error(f"Failed to list versions: {e}")
853
+ return []
854
+
855
+ def _dict_to_version(self, data: Dict[str, Any]) -> ModelVersion:
856
+ """Convert dictionary to ModelVersion object."""
857
+ # Parse JSON fields
858
+ for field in ['performance_metrics', 'benchmark_scores', 'tags', 'derived_from', 'children_versions']:
859
+ if data.get(field) and isinstance(data[field], str):
860
+ try:
861
+ data[field] = json.loads(data[field])
862
+ except json.JSONDecodeError:
863
+ data[field] = None
864
+
865
+ # Parse capabilities
866
+ if data.get('capabilities') and isinstance(data['capabilities'], str):
867
+ try:
868
+ cap_list = json.loads(data['capabilities'])
869
+ data['capabilities'] = [ModelCapability(cap) for cap in cap_list]
870
+ except (json.JSONDecodeError, ValueError):
871
+ data['capabilities'] = None
872
+
873
+ # Parse datetime
874
+ if data.get('created_at') and isinstance(data['created_at'], str):
875
+ try:
876
+ data['created_at'] = datetime.fromisoformat(data['created_at'].replace('Z', '+00:00'))
877
+ except ValueError:
878
+ data['created_at'] = None
879
+
880
+ # Parse enums
881
+ if data.get('version_type') and isinstance(data['version_type'], str):
882
+ try:
883
+ data['version_type'] = VersionType(data['version_type'])
884
+ except ValueError:
885
+ data['version_type'] = VersionType.MINOR
886
+
887
+ if data.get('model_type') and isinstance(data['model_type'], str):
888
+ try:
889
+ data['model_type'] = ModelType(data['model_type'])
890
+ except ValueError:
891
+ data['model_type'] = None
892
+
893
+ return ModelVersion(**data)
894
+
895
+ def _update_version(self, version: ModelVersion) -> bool:
896
+ """Update existing version in storage."""
897
+ return self._save_version(version)
898
+
899
+ def _delete_version(self, version_id: str) -> bool:
900
+ """Delete version from storage."""
901
+ if not self.supabase_available:
902
+ return False
903
+
904
+ try:
905
+ result = self.supabase_client.table('model_versions').delete().eq('version_id', version_id).execute()
906
+ return bool(result.data)
907
+
908
+ except Exception as e:
909
+ logger.error(f"Failed to delete version: {e}")
910
+ return False
911
+
912
+ def _get_ancestors(self, version_id: str, depth: int) -> List[Dict[str, Any]]:
913
+ """Get version ancestors."""
914
+ ancestors = []
915
+ current_version_id = version_id
916
+
917
+ for _ in range(depth):
918
+ version = self.get_version(current_version_id)
919
+ if not version or not version.parent_version:
920
+ break
921
+
922
+ parent = self.get_version(version.parent_version)
923
+ if not parent:
924
+ break
925
+
926
+ ancestors.append({
927
+ "version_id": parent.version_id,
928
+ "version_number": parent.version_number,
929
+ "created_at": parent.created_at.isoformat() if parent.created_at else None
930
+ })
931
+
932
+ current_version_id = version.parent_version
933
+
934
+ return ancestors
935
+
936
+ def _get_descendants(self, version_id: str, depth: int) -> List[Dict[str, Any]]:
937
+ """Get version descendants."""
938
+ descendants = []
939
+
940
+ def find_children(parent_id: str, current_depth: int):
941
+ if current_depth >= depth:
942
+ return
943
+
944
+ # Find all versions where parent_version equals parent_id
945
+ all_versions = self.list_versions(limit=1000)
946
+ children = [v for v in all_versions if v.parent_version == parent_id]
947
+
948
+ for child in children:
949
+ descendants.append({
950
+ "version_id": child.version_id,
951
+ "version_number": child.version_number,
952
+ "created_at": child.created_at.isoformat() if child.created_at else None
953
+ })
954
+
955
+ # Recursively find children
956
+ find_children(child.version_id, current_depth + 1)
957
+
958
+ find_children(version_id, 0)
959
+ return descendants