isa-model 0.3.9__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.
- isa_model/__init__.py +1 -1
- isa_model/client.py +732 -565
- isa_model/core/cache/redis_cache.py +401 -0
- isa_model/core/config/config_manager.py +53 -10
- isa_model/core/config.py +1 -1
- isa_model/core/database/__init__.py +1 -0
- isa_model/core/database/migrations.py +277 -0
- isa_model/core/database/supabase_client.py +123 -0
- isa_model/core/models/__init__.py +37 -0
- isa_model/core/models/model_billing_tracker.py +60 -88
- isa_model/core/models/model_manager.py +36 -18
- isa_model/core/models/model_repo.py +44 -38
- isa_model/core/models/model_statistics_tracker.py +234 -0
- isa_model/core/models/model_storage.py +0 -1
- isa_model/core/models/model_version_manager.py +959 -0
- isa_model/core/pricing_manager.py +2 -249
- isa_model/core/resilience/circuit_breaker.py +366 -0
- isa_model/core/security/secrets.py +358 -0
- isa_model/core/services/__init__.py +2 -4
- isa_model/core/services/intelligent_model_selector.py +101 -370
- isa_model/core/storage/hf_storage.py +1 -1
- isa_model/core/types.py +7 -0
- isa_model/deployment/cloud/modal/isa_audio_chatTTS_service.py +520 -0
- isa_model/deployment/cloud/modal/isa_audio_fish_service.py +0 -0
- isa_model/deployment/cloud/modal/isa_audio_openvoice_service.py +758 -0
- isa_model/deployment/cloud/modal/isa_audio_service_v2.py +1044 -0
- isa_model/deployment/cloud/modal/isa_embed_rerank_service.py +296 -0
- isa_model/deployment/cloud/modal/isa_video_hunyuan_service.py +423 -0
- isa_model/deployment/cloud/modal/isa_vision_ocr_service.py +519 -0
- isa_model/deployment/cloud/modal/isa_vision_qwen25_service.py +709 -0
- isa_model/deployment/cloud/modal/isa_vision_table_service.py +467 -323
- isa_model/deployment/cloud/modal/isa_vision_ui_service.py +607 -180
- isa_model/deployment/cloud/modal/isa_vision_ui_service_optimized.py +660 -0
- isa_model/deployment/core/deployment_manager.py +6 -4
- isa_model/deployment/services/auto_hf_modal_deployer.py +894 -0
- isa_model/eval/benchmarks/__init__.py +27 -0
- isa_model/eval/benchmarks/multimodal_datasets.py +460 -0
- isa_model/eval/benchmarks.py +244 -12
- isa_model/eval/evaluators/__init__.py +8 -2
- isa_model/eval/evaluators/audio_evaluator.py +727 -0
- isa_model/eval/evaluators/embedding_evaluator.py +742 -0
- isa_model/eval/evaluators/vision_evaluator.py +564 -0
- isa_model/eval/example_evaluation.py +395 -0
- isa_model/eval/factory.py +272 -5
- isa_model/eval/isa_benchmarks.py +700 -0
- isa_model/eval/isa_integration.py +582 -0
- isa_model/eval/metrics.py +159 -6
- isa_model/eval/tests/unit/test_basic.py +396 -0
- isa_model/inference/ai_factory.py +44 -8
- isa_model/inference/services/audio/__init__.py +21 -0
- isa_model/inference/services/audio/base_realtime_service.py +225 -0
- isa_model/inference/services/audio/isa_tts_service.py +0 -0
- isa_model/inference/services/audio/openai_realtime_service.py +320 -124
- isa_model/inference/services/audio/openai_stt_service.py +32 -6
- isa_model/inference/services/base_service.py +17 -1
- isa_model/inference/services/embedding/__init__.py +13 -0
- isa_model/inference/services/embedding/base_embed_service.py +111 -8
- isa_model/inference/services/embedding/isa_embed_service.py +305 -0
- isa_model/inference/services/embedding/openai_embed_service.py +2 -4
- isa_model/inference/services/embedding/tests/test_embedding.py +222 -0
- isa_model/inference/services/img/__init__.py +2 -2
- isa_model/inference/services/img/base_image_gen_service.py +24 -7
- isa_model/inference/services/img/replicate_image_gen_service.py +84 -422
- isa_model/inference/services/img/services/replicate_face_swap.py +193 -0
- isa_model/inference/services/img/services/replicate_flux.py +226 -0
- isa_model/inference/services/img/services/replicate_flux_kontext.py +219 -0
- isa_model/inference/services/img/services/replicate_sticker_maker.py +249 -0
- isa_model/inference/services/img/tests/test_img_client.py +297 -0
- isa_model/inference/services/llm/base_llm_service.py +30 -6
- isa_model/inference/services/llm/helpers/llm_adapter.py +63 -9
- isa_model/inference/services/llm/ollama_llm_service.py +2 -1
- isa_model/inference/services/llm/openai_llm_service.py +652 -55
- isa_model/inference/services/llm/yyds_llm_service.py +2 -1
- isa_model/inference/services/vision/__init__.py +5 -5
- isa_model/inference/services/vision/base_vision_service.py +118 -185
- isa_model/inference/services/vision/helpers/image_utils.py +11 -5
- isa_model/inference/services/vision/isa_vision_service.py +573 -0
- isa_model/inference/services/vision/tests/test_ocr_client.py +284 -0
- isa_model/serving/api/fastapi_server.py +88 -16
- isa_model/serving/api/middleware/auth.py +311 -0
- isa_model/serving/api/middleware/security.py +278 -0
- isa_model/serving/api/routes/analytics.py +486 -0
- isa_model/serving/api/routes/deployments.py +339 -0
- isa_model/serving/api/routes/evaluations.py +579 -0
- isa_model/serving/api/routes/logs.py +430 -0
- isa_model/serving/api/routes/settings.py +582 -0
- isa_model/serving/api/routes/unified.py +324 -165
- isa_model/serving/api/startup.py +304 -0
- isa_model/serving/modal_proxy_server.py +249 -0
- isa_model/training/__init__.py +100 -6
- isa_model/training/core/__init__.py +4 -1
- isa_model/training/examples/intelligent_training_example.py +281 -0
- isa_model/training/intelligent/__init__.py +25 -0
- isa_model/training/intelligent/decision_engine.py +643 -0
- isa_model/training/intelligent/intelligent_factory.py +888 -0
- isa_model/training/intelligent/knowledge_base.py +751 -0
- isa_model/training/intelligent/resource_optimizer.py +839 -0
- isa_model/training/intelligent/task_classifier.py +576 -0
- isa_model/training/storage/__init__.py +24 -0
- isa_model/training/storage/core_integration.py +439 -0
- isa_model/training/storage/training_repository.py +552 -0
- isa_model/training/storage/training_storage.py +628 -0
- {isa_model-0.3.9.dist-info → isa_model-0.4.0.dist-info}/METADATA +13 -1
- isa_model-0.4.0.dist-info/RECORD +182 -0
- isa_model/deployment/cloud/modal/isa_vision_doc_service.py +0 -766
- isa_model/deployment/cloud/modal/register_models.py +0 -321
- isa_model/inference/adapter/unified_api.py +0 -248
- isa_model/inference/services/helpers/stacked_config.py +0 -148
- isa_model/inference/services/img/flux_professional_service.py +0 -603
- isa_model/inference/services/img/helpers/base_stacked_service.py +0 -274
- isa_model/inference/services/others/table_transformer_service.py +0 -61
- isa_model/inference/services/vision/doc_analysis_service.py +0 -640
- isa_model/inference/services/vision/helpers/base_stacked_service.py +0 -274
- isa_model/inference/services/vision/ui_analysis_service.py +0 -823
- isa_model/scripts/inference_tracker.py +0 -283
- isa_model/scripts/mlflow_manager.py +0 -379
- isa_model/scripts/model_registry.py +0 -465
- isa_model/scripts/register_models.py +0 -370
- isa_model/scripts/register_models_with_embeddings.py +0 -510
- isa_model/scripts/start_mlflow.py +0 -95
- isa_model/scripts/training_tracker.py +0 -257
- isa_model-0.3.9.dist-info/RECORD +0 -138
- {isa_model-0.3.9.dist-info → isa_model-0.4.0.dist-info}/WHEEL +0 -0
- {isa_model-0.3.9.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
|