isa-model 0.4.0__py3-none-any.whl → 0.4.4__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/client.py +466 -43
- isa_model/core/cache/redis_cache.py +12 -3
- isa_model/core/config/config_manager.py +230 -3
- isa_model/core/config.py +90 -0
- isa_model/core/database/direct_db_client.py +114 -0
- isa_model/core/database/migration_manager.py +563 -0
- isa_model/core/database/migrations.py +21 -1
- isa_model/core/database/supabase_client.py +154 -19
- isa_model/core/dependencies.py +316 -0
- isa_model/core/discovery/__init__.py +19 -0
- isa_model/core/discovery/consul_discovery.py +190 -0
- isa_model/core/logging/__init__.py +54 -0
- isa_model/core/logging/influx_logger.py +523 -0
- isa_model/core/logging/loki_logger.py +160 -0
- isa_model/core/models/__init__.py +27 -18
- isa_model/core/models/config_models.py +625 -0
- isa_model/core/models/deployment_billing_tracker.py +430 -0
- isa_model/core/models/model_manager.py +35 -80
- isa_model/core/models/model_metadata.py +690 -0
- isa_model/core/models/model_repo.py +174 -18
- isa_model/core/models/system_models.py +857 -0
- isa_model/core/repositories/__init__.py +9 -0
- isa_model/core/repositories/config_repository.py +912 -0
- isa_model/core/services/intelligent_model_selector.py +399 -21
- isa_model/core/types.py +1 -0
- isa_model/deployment/__init__.py +5 -48
- isa_model/deployment/core/__init__.py +2 -31
- isa_model/deployment/core/deployment_manager.py +1278 -370
- isa_model/deployment/modal/__init__.py +8 -0
- isa_model/deployment/modal/config.py +136 -0
- isa_model/deployment/{services/auto_hf_modal_deployer.py → modal/deployer.py} +1 -1
- isa_model/deployment/modal/services/__init__.py +3 -0
- isa_model/deployment/modal/services/audio/__init__.py +1 -0
- isa_model/deployment/modal/services/embedding/__init__.py +1 -0
- isa_model/deployment/modal/services/llm/__init__.py +1 -0
- isa_model/deployment/modal/services/llm/isa_llm_service.py +424 -0
- isa_model/deployment/modal/services/video/__init__.py +1 -0
- isa_model/deployment/modal/services/vision/__init__.py +1 -0
- isa_model/deployment/models/org-org-acme-corp-tenant-a-service-llm-20250825-225822/tenant-a-service_modal_service.py +48 -0
- isa_model/deployment/models/org-test-org-123-prefix-test-service-llm-20250825-225822/prefix-test-service_modal_service.py +48 -0
- isa_model/deployment/models/test-llm-service-llm-20250825-204442/test-llm-service_modal_service.py +48 -0
- isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-212906/test-monitoring-gpt2_modal_service.py +48 -0
- isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-213009/test-monitoring-gpt2_modal_service.py +48 -0
- isa_model/deployment/storage/__init__.py +5 -0
- isa_model/deployment/storage/deployment_repository.py +824 -0
- isa_model/deployment/triton/__init__.py +10 -0
- isa_model/deployment/triton/config.py +196 -0
- isa_model/deployment/triton/configs/__init__.py +1 -0
- isa_model/deployment/triton/provider.py +512 -0
- isa_model/deployment/triton/scripts/__init__.py +1 -0
- isa_model/deployment/triton/templates/__init__.py +1 -0
- isa_model/inference/__init__.py +47 -1
- isa_model/inference/ai_factory.py +137 -10
- isa_model/inference/legacy_services/__init__.py +21 -0
- isa_model/inference/legacy_services/model_evaluation.py +637 -0
- isa_model/inference/legacy_services/model_service.py +573 -0
- isa_model/inference/legacy_services/model_serving.py +717 -0
- isa_model/inference/legacy_services/model_training.py +561 -0
- isa_model/inference/models/__init__.py +21 -0
- isa_model/inference/models/inference_config.py +551 -0
- isa_model/inference/models/inference_record.py +675 -0
- isa_model/inference/models/performance_models.py +714 -0
- isa_model/inference/repositories/__init__.py +9 -0
- isa_model/inference/repositories/inference_repository.py +828 -0
- isa_model/inference/services/audio/base_stt_service.py +184 -11
- isa_model/inference/services/audio/openai_stt_service.py +22 -6
- isa_model/inference/services/embedding/ollama_embed_service.py +15 -3
- isa_model/inference/services/embedding/resilient_embed_service.py +285 -0
- isa_model/inference/services/llm/__init__.py +10 -2
- isa_model/inference/services/llm/base_llm_service.py +335 -24
- isa_model/inference/services/llm/cerebras_llm_service.py +628 -0
- isa_model/inference/services/llm/helpers/llm_adapter.py +9 -4
- isa_model/inference/services/llm/helpers/llm_prompts.py +342 -0
- isa_model/inference/services/llm/helpers/llm_utils.py +321 -23
- isa_model/inference/services/llm/huggingface_llm_service.py +581 -0
- isa_model/inference/services/llm/ollama_llm_service.py +9 -2
- isa_model/inference/services/llm/openai_llm_service.py +33 -16
- isa_model/inference/services/llm/yyds_llm_service.py +8 -2
- isa_model/inference/services/vision/__init__.py +22 -1
- isa_model/inference/services/vision/helpers/image_utils.py +8 -5
- isa_model/inference/services/vision/isa_vision_service.py +65 -4
- isa_model/inference/services/vision/openai_vision_service.py +19 -10
- isa_model/inference/services/vision/vgg16_vision_service.py +257 -0
- isa_model/serving/api/cache_manager.py +245 -0
- isa_model/serving/api/dependencies/__init__.py +1 -0
- isa_model/serving/api/dependencies/auth.py +194 -0
- isa_model/serving/api/dependencies/database.py +139 -0
- isa_model/serving/api/error_handlers.py +284 -0
- isa_model/serving/api/fastapi_server.py +172 -22
- isa_model/serving/api/middleware/auth.py +8 -2
- isa_model/serving/api/middleware/security.py +23 -33
- isa_model/serving/api/middleware/tenant_context.py +414 -0
- isa_model/serving/api/routes/analytics.py +4 -1
- isa_model/serving/api/routes/config.py +645 -0
- isa_model/serving/api/routes/deployment_billing.py +315 -0
- isa_model/serving/api/routes/deployments.py +138 -2
- isa_model/serving/api/routes/gpu_gateway.py +440 -0
- isa_model/serving/api/routes/health.py +32 -12
- isa_model/serving/api/routes/inference_monitoring.py +486 -0
- isa_model/serving/api/routes/local_deployments.py +448 -0
- isa_model/serving/api/routes/tenants.py +575 -0
- isa_model/serving/api/routes/unified.py +680 -18
- isa_model/serving/api/routes/webhooks.py +479 -0
- isa_model/serving/api/startup.py +68 -54
- isa_model/utils/gpu_utils.py +311 -0
- {isa_model-0.4.0.dist-info → isa_model-0.4.4.dist-info}/METADATA +71 -24
- isa_model-0.4.4.dist-info/RECORD +180 -0
- isa_model/core/security/secrets.py +0 -358
- isa_model/core/storage/hf_storage.py +0 -419
- isa_model/core/storage/minio_storage.py +0 -0
- isa_model/deployment/cloud/__init__.py +0 -9
- isa_model/deployment/cloud/modal/__init__.py +0 -10
- isa_model/deployment/core/deployment_config.py +0 -356
- isa_model/deployment/core/isa_deployment_service.py +0 -401
- isa_model/deployment/gpu_int8_ds8/app/server.py +0 -66
- isa_model/deployment/gpu_int8_ds8/scripts/test_client.py +0 -43
- isa_model/deployment/gpu_int8_ds8/scripts/test_client_os.py +0 -35
- isa_model/deployment/runtime/deployed_service.py +0 -338
- isa_model/deployment/services/__init__.py +0 -9
- isa_model/deployment/services/auto_deploy_vision_service.py +0 -538
- isa_model/deployment/services/model_service.py +0 -332
- isa_model/deployment/services/service_monitor.py +0 -356
- isa_model/deployment/services/service_registry.py +0 -527
- isa_model/eval/__init__.py +0 -92
- isa_model/eval/benchmarks/__init__.py +0 -27
- isa_model/eval/benchmarks/multimodal_datasets.py +0 -460
- isa_model/eval/benchmarks.py +0 -701
- isa_model/eval/config/__init__.py +0 -10
- isa_model/eval/config/evaluation_config.py +0 -108
- isa_model/eval/evaluators/__init__.py +0 -24
- isa_model/eval/evaluators/audio_evaluator.py +0 -727
- isa_model/eval/evaluators/base_evaluator.py +0 -503
- isa_model/eval/evaluators/embedding_evaluator.py +0 -742
- isa_model/eval/evaluators/llm_evaluator.py +0 -472
- isa_model/eval/evaluators/vision_evaluator.py +0 -564
- isa_model/eval/example_evaluation.py +0 -395
- isa_model/eval/factory.py +0 -798
- isa_model/eval/infrastructure/__init__.py +0 -24
- isa_model/eval/infrastructure/experiment_tracker.py +0 -466
- isa_model/eval/isa_benchmarks.py +0 -700
- isa_model/eval/isa_integration.py +0 -582
- isa_model/eval/metrics.py +0 -951
- isa_model/eval/tests/unit/test_basic.py +0 -396
- isa_model/serving/api/routes/evaluations.py +0 -579
- isa_model/training/__init__.py +0 -168
- isa_model/training/annotation/annotation_schema.py +0 -47
- isa_model/training/annotation/processors/annotation_processor.py +0 -126
- isa_model/training/annotation/storage/dataset_manager.py +0 -131
- isa_model/training/annotation/storage/dataset_schema.py +0 -44
- isa_model/training/annotation/tests/test_annotation_flow.py +0 -109
- isa_model/training/annotation/tests/test_minio copy.py +0 -113
- isa_model/training/annotation/tests/test_minio_upload.py +0 -43
- isa_model/training/annotation/views/annotation_controller.py +0 -158
- isa_model/training/cloud/__init__.py +0 -22
- isa_model/training/cloud/job_orchestrator.py +0 -402
- isa_model/training/cloud/runpod_trainer.py +0 -454
- isa_model/training/cloud/storage_manager.py +0 -482
- isa_model/training/core/__init__.py +0 -26
- isa_model/training/core/config.py +0 -181
- isa_model/training/core/dataset.py +0 -222
- isa_model/training/core/trainer.py +0 -720
- isa_model/training/core/utils.py +0 -213
- isa_model/training/examples/intelligent_training_example.py +0 -281
- isa_model/training/factory.py +0 -424
- isa_model/training/intelligent/__init__.py +0 -25
- isa_model/training/intelligent/decision_engine.py +0 -643
- isa_model/training/intelligent/intelligent_factory.py +0 -888
- isa_model/training/intelligent/knowledge_base.py +0 -751
- isa_model/training/intelligent/resource_optimizer.py +0 -839
- isa_model/training/intelligent/task_classifier.py +0 -576
- isa_model/training/storage/__init__.py +0 -24
- isa_model/training/storage/core_integration.py +0 -439
- isa_model/training/storage/training_repository.py +0 -552
- isa_model/training/storage/training_storage.py +0 -628
- isa_model-0.4.0.dist-info/RECORD +0 -182
- /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_chatTTS_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_fish_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_openvoice_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_service_v2.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/embedding}/isa_embed_rerank_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/video}/isa_video_hunyuan_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ocr_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_qwen25_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_table_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ui_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ui_service_optimized.py +0 -0
- /isa_model/deployment/{services → modal/services/vision}/simple_auto_deploy_vision_service.py +0 -0
- {isa_model-0.4.0.dist-info → isa_model-0.4.4.dist-info}/WHEEL +0 -0
- {isa_model-0.4.0.dist-info → isa_model-0.4.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,257 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
VGG16 Vision Service
|
4
|
+
Computer vision service using VGG16 for image classification
|
5
|
+
Based on the aircraft damage detection notebook implementation
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
import numpy as np
|
10
|
+
from typing import Dict, List, Any, Optional, Union, BinaryIO
|
11
|
+
import logging
|
12
|
+
from PIL import Image
|
13
|
+
import io
|
14
|
+
|
15
|
+
from .base_vision_service import BaseVisionService
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
def _lazy_import_vgg16_deps():
|
20
|
+
"""Lazy import VGG16 dependencies"""
|
21
|
+
try:
|
22
|
+
import tensorflow as tf
|
23
|
+
from tensorflow.keras.applications import VGG16
|
24
|
+
from tensorflow.keras.layers import Dense, Dropout, Flatten
|
25
|
+
from tensorflow.keras.models import Sequential, Model
|
26
|
+
from tensorflow.keras.optimizers import Adam
|
27
|
+
from tensorflow.keras.preprocessing.image import ImageDataGenerator
|
28
|
+
|
29
|
+
return {
|
30
|
+
'tf': tf,
|
31
|
+
'VGG16': VGG16,
|
32
|
+
'Dense': Dense,
|
33
|
+
'Dropout': Dropout,
|
34
|
+
'Flatten': Flatten,
|
35
|
+
'Sequential': Sequential,
|
36
|
+
'Model': Model,
|
37
|
+
'Adam': Adam,
|
38
|
+
'ImageDataGenerator': ImageDataGenerator,
|
39
|
+
'available': True
|
40
|
+
}
|
41
|
+
except ImportError as e:
|
42
|
+
logger.warning(f"VGG16 dependencies not available: {e}")
|
43
|
+
return {'available': False}
|
44
|
+
|
45
|
+
class VGG16VisionService(BaseVisionService):
|
46
|
+
"""
|
47
|
+
VGG16-based vision service for image classification
|
48
|
+
Provides an alternative implementation to VLM-based classification
|
49
|
+
"""
|
50
|
+
|
51
|
+
def __init__(self, model_path: Optional[str] = None, class_names: Optional[List[str]] = None):
|
52
|
+
"""
|
53
|
+
Initialize VGG16 vision service
|
54
|
+
|
55
|
+
Args:
|
56
|
+
model_path: Path to trained VGG16 model
|
57
|
+
class_names: List of class names for classification
|
58
|
+
"""
|
59
|
+
super().__init__()
|
60
|
+
|
61
|
+
self.model_path = model_path
|
62
|
+
self.class_names = class_names or ["class_0", "class_1"]
|
63
|
+
self.model = None
|
64
|
+
self.input_shape = (224, 224, 3)
|
65
|
+
|
66
|
+
# Lazy load dependencies
|
67
|
+
self.vgg16_components = _lazy_import_vgg16_deps()
|
68
|
+
|
69
|
+
if not self.vgg16_components['available']:
|
70
|
+
raise ImportError("TensorFlow and VGG16 dependencies are required")
|
71
|
+
|
72
|
+
# Load model if path provided
|
73
|
+
if model_path and os.path.exists(model_path):
|
74
|
+
self._load_model(model_path)
|
75
|
+
|
76
|
+
def _load_model(self, model_path: str):
|
77
|
+
"""Load trained VGG16 model"""
|
78
|
+
try:
|
79
|
+
tf = self.vgg16_components['tf']
|
80
|
+
self.model = tf.keras.models.load_model(model_path)
|
81
|
+
logger.info(f"VGG16 model loaded from {model_path}")
|
82
|
+
except Exception as e:
|
83
|
+
logger.error(f"Error loading VGG16 model: {e}")
|
84
|
+
raise
|
85
|
+
|
86
|
+
def _preprocess_image(self, image: Union[str, BinaryIO]) -> np.ndarray:
|
87
|
+
"""
|
88
|
+
Preprocess image for VGG16 input
|
89
|
+
|
90
|
+
Args:
|
91
|
+
image: Image path or binary data
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
Preprocessed image array
|
95
|
+
"""
|
96
|
+
try:
|
97
|
+
# Handle different image input types
|
98
|
+
if isinstance(image, str):
|
99
|
+
# File path
|
100
|
+
pil_image = Image.open(image).convert('RGB')
|
101
|
+
elif hasattr(image, 'read'):
|
102
|
+
# Binary IO
|
103
|
+
image_data = image.read()
|
104
|
+
pil_image = Image.open(io.BytesIO(image_data)).convert('RGB')
|
105
|
+
else:
|
106
|
+
raise ValueError("Unsupported image format")
|
107
|
+
|
108
|
+
# Resize to VGG16 input size
|
109
|
+
pil_image = pil_image.resize((self.input_shape[0], self.input_shape[1]))
|
110
|
+
|
111
|
+
# Convert to array and normalize
|
112
|
+
image_array = np.array(pil_image) / 255.0
|
113
|
+
|
114
|
+
# Add batch dimension
|
115
|
+
image_batch = np.expand_dims(image_array, axis=0)
|
116
|
+
|
117
|
+
return image_batch, image_array
|
118
|
+
|
119
|
+
except Exception as e:
|
120
|
+
logger.error(f"Error preprocessing image: {e}")
|
121
|
+
raise
|
122
|
+
|
123
|
+
async def classify_image(self,
|
124
|
+
image: Union[str, BinaryIO],
|
125
|
+
categories: Optional[List[str]] = None) -> Dict[str, Any]:
|
126
|
+
"""
|
127
|
+
Classify image using trained VGG16 model
|
128
|
+
|
129
|
+
Args:
|
130
|
+
image: Image path or binary data
|
131
|
+
categories: Optional list of categories (uses model's classes if None)
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
Classification results
|
135
|
+
"""
|
136
|
+
try:
|
137
|
+
if self.model is None:
|
138
|
+
return {
|
139
|
+
"error": "No trained model available. Please load a model first.",
|
140
|
+
"service": "VGG16VisionService"
|
141
|
+
}
|
142
|
+
|
143
|
+
# Preprocess image
|
144
|
+
image_batch, image_array = self._preprocess_image(image)
|
145
|
+
|
146
|
+
# Make prediction
|
147
|
+
predictions = self.model.predict(image_batch, verbose=0)
|
148
|
+
|
149
|
+
# Use provided categories or default class names
|
150
|
+
class_names = categories or self.class_names
|
151
|
+
|
152
|
+
# Process predictions based on model output
|
153
|
+
if len(predictions[0]) == 1: # Binary classification
|
154
|
+
predicted_class_idx = int(predictions[0] > 0.5)
|
155
|
+
confidence = float(predictions[0][0]) if predicted_class_idx == 1 else float(1 - predictions[0][0])
|
156
|
+
|
157
|
+
# Create probability distribution
|
158
|
+
probabilities = {
|
159
|
+
class_names[0]: float(1 - predictions[0][0]),
|
160
|
+
class_names[1]: float(predictions[0][0])
|
161
|
+
}
|
162
|
+
else: # Multiclass classification
|
163
|
+
predicted_class_idx = np.argmax(predictions[0])
|
164
|
+
confidence = float(predictions[0][predicted_class_idx])
|
165
|
+
|
166
|
+
# Create probability distribution
|
167
|
+
probabilities = {
|
168
|
+
class_names[i]: float(predictions[0][i])
|
169
|
+
for i in range(min(len(class_names), len(predictions[0])))
|
170
|
+
}
|
171
|
+
|
172
|
+
predicted_class = class_names[predicted_class_idx] if predicted_class_idx < len(class_names) else f"class_{predicted_class_idx}"
|
173
|
+
|
174
|
+
return {
|
175
|
+
"task": "classify",
|
176
|
+
"service": "VGG16VisionService",
|
177
|
+
"predicted_class": predicted_class,
|
178
|
+
"confidence": confidence,
|
179
|
+
"probabilities": probabilities,
|
180
|
+
"model_type": "VGG16",
|
181
|
+
"success": True
|
182
|
+
}
|
183
|
+
|
184
|
+
except Exception as e:
|
185
|
+
logger.error(f"Error classifying image: {e}")
|
186
|
+
return {
|
187
|
+
"error": str(e),
|
188
|
+
"service": "VGG16VisionService",
|
189
|
+
"success": False
|
190
|
+
}
|
191
|
+
|
192
|
+
async def analyze_image(self,
|
193
|
+
image: Union[str, BinaryIO],
|
194
|
+
prompt: Optional[str] = None,
|
195
|
+
max_tokens: int = 1000) -> Dict[str, Any]:
|
196
|
+
"""
|
197
|
+
Analyze image using VGG16 classification
|
198
|
+
|
199
|
+
Args:
|
200
|
+
image: Image path or binary data
|
201
|
+
prompt: Optional prompt (used to guide interpretation)
|
202
|
+
max_tokens: Not used for classification
|
203
|
+
|
204
|
+
Returns:
|
205
|
+
Analysis results
|
206
|
+
"""
|
207
|
+
# For VGG16, analysis is essentially classification
|
208
|
+
classification_result = await self.classify_image(image)
|
209
|
+
|
210
|
+
if classification_result.get("success"):
|
211
|
+
# Create analysis text based on classification
|
212
|
+
predicted_class = classification_result["predicted_class"]
|
213
|
+
confidence = classification_result["confidence"]
|
214
|
+
|
215
|
+
analysis_text = f"The image has been classified as '{predicted_class}' with {confidence:.2%} confidence."
|
216
|
+
|
217
|
+
if prompt:
|
218
|
+
analysis_text += f" Analysis context: {prompt}"
|
219
|
+
|
220
|
+
return {
|
221
|
+
"task": "analyze",
|
222
|
+
"service": "VGG16VisionService",
|
223
|
+
"text": analysis_text,
|
224
|
+
"confidence": confidence,
|
225
|
+
"classification": classification_result,
|
226
|
+
"success": True
|
227
|
+
}
|
228
|
+
else:
|
229
|
+
return classification_result
|
230
|
+
|
231
|
+
def set_class_names(self, class_names: List[str]):
|
232
|
+
"""Set class names for classification"""
|
233
|
+
self.class_names = class_names
|
234
|
+
|
235
|
+
def load_trained_model(self, model_path: str, class_names: Optional[List[str]] = None):
|
236
|
+
"""
|
237
|
+
Load a trained VGG16 model
|
238
|
+
|
239
|
+
Args:
|
240
|
+
model_path: Path to the trained model
|
241
|
+
class_names: Optional class names
|
242
|
+
"""
|
243
|
+
self._load_model(model_path)
|
244
|
+
if class_names:
|
245
|
+
self.set_class_names(class_names)
|
246
|
+
|
247
|
+
def get_service_info(self) -> Dict[str, Any]:
|
248
|
+
"""Get service information"""
|
249
|
+
return {
|
250
|
+
"service_name": "VGG16VisionService",
|
251
|
+
"model_type": "VGG16",
|
252
|
+
"capabilities": ["classify", "analyze"],
|
253
|
+
"model_loaded": self.model is not None,
|
254
|
+
"input_shape": self.input_shape,
|
255
|
+
"class_names": self.class_names,
|
256
|
+
"dependencies_available": self.vgg16_components['available']
|
257
|
+
}
|
@@ -0,0 +1,245 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
"""
|
5
|
+
Simple Cache Manager for ISA Model API
|
6
|
+
Provides in-memory caching to improve API performance
|
7
|
+
"""
|
8
|
+
|
9
|
+
import time
|
10
|
+
import logging
|
11
|
+
from typing import Dict, Any, Optional, Callable
|
12
|
+
from dataclasses import dataclass
|
13
|
+
from threading import RLock
|
14
|
+
import asyncio
|
15
|
+
import hashlib
|
16
|
+
import json
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class CacheEntry:
|
22
|
+
"""Cache entry with data and metadata"""
|
23
|
+
data: Any
|
24
|
+
created_at: float
|
25
|
+
ttl: float
|
26
|
+
access_count: int = 0
|
27
|
+
last_accessed: float = None
|
28
|
+
|
29
|
+
def is_expired(self) -> bool:
|
30
|
+
"""Check if cache entry is expired"""
|
31
|
+
return time.time() - self.created_at > self.ttl
|
32
|
+
|
33
|
+
def access(self) -> Any:
|
34
|
+
"""Mark as accessed and return data"""
|
35
|
+
self.access_count += 1
|
36
|
+
self.last_accessed = time.time()
|
37
|
+
return self.data
|
38
|
+
|
39
|
+
class APICache:
|
40
|
+
"""
|
41
|
+
Simple in-memory cache for API responses
|
42
|
+
Thread-safe with automatic expiration
|
43
|
+
"""
|
44
|
+
|
45
|
+
def __init__(self, default_ttl: float = 300.0, max_size: int = 1000):
|
46
|
+
self.default_ttl = default_ttl # 5 minutes default
|
47
|
+
self.max_size = max_size
|
48
|
+
self._cache: Dict[str, CacheEntry] = {}
|
49
|
+
self._lock = RLock()
|
50
|
+
self._stats = {
|
51
|
+
"hits": 0,
|
52
|
+
"misses": 0,
|
53
|
+
"evictions": 0,
|
54
|
+
"total_requests": 0
|
55
|
+
}
|
56
|
+
|
57
|
+
def _generate_key(self, *args, **kwargs) -> str:
|
58
|
+
"""Generate cache key from arguments"""
|
59
|
+
# Create a stable key from arguments
|
60
|
+
key_data = {
|
61
|
+
"args": args,
|
62
|
+
"kwargs": sorted(kwargs.items()) if kwargs else {}
|
63
|
+
}
|
64
|
+
key_string = json.dumps(key_data, sort_keys=True, default=str)
|
65
|
+
return hashlib.md5(key_string.encode()).hexdigest()
|
66
|
+
|
67
|
+
def _cleanup_expired(self):
|
68
|
+
"""Remove expired entries"""
|
69
|
+
with self._lock:
|
70
|
+
current_time = time.time()
|
71
|
+
expired_keys = [
|
72
|
+
key for key, entry in self._cache.items()
|
73
|
+
if entry.is_expired()
|
74
|
+
]
|
75
|
+
|
76
|
+
for key in expired_keys:
|
77
|
+
del self._cache[key]
|
78
|
+
|
79
|
+
if expired_keys:
|
80
|
+
logger.debug(f"Cleaned up {len(expired_keys)} expired cache entries")
|
81
|
+
|
82
|
+
def _evict_lru(self):
|
83
|
+
"""Evict least recently used entries when cache is full"""
|
84
|
+
with self._lock:
|
85
|
+
if len(self._cache) >= self.max_size:
|
86
|
+
# Sort by last_accessed time (LRU)
|
87
|
+
sorted_entries = sorted(
|
88
|
+
self._cache.items(),
|
89
|
+
key=lambda x: x[1].last_accessed or x[1].created_at
|
90
|
+
)
|
91
|
+
|
92
|
+
# Remove oldest 20% of entries
|
93
|
+
num_to_remove = max(1, len(sorted_entries) // 5)
|
94
|
+
for key, _ in sorted_entries[:num_to_remove]:
|
95
|
+
del self._cache[key]
|
96
|
+
self._stats["evictions"] += 1
|
97
|
+
|
98
|
+
logger.debug(f"Evicted {num_to_remove} LRU cache entries")
|
99
|
+
|
100
|
+
def get(self, key: str) -> Optional[Any]:
|
101
|
+
"""Get cached value by key"""
|
102
|
+
with self._lock:
|
103
|
+
self._stats["total_requests"] += 1
|
104
|
+
|
105
|
+
if key in self._cache:
|
106
|
+
entry = self._cache[key]
|
107
|
+
if not entry.is_expired():
|
108
|
+
self._stats["hits"] += 1
|
109
|
+
return entry.access()
|
110
|
+
else:
|
111
|
+
# Remove expired entry
|
112
|
+
del self._cache[key]
|
113
|
+
|
114
|
+
self._stats["misses"] += 1
|
115
|
+
return None
|
116
|
+
|
117
|
+
def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
|
118
|
+
"""Set cached value with optional TTL"""
|
119
|
+
with self._lock:
|
120
|
+
# Cleanup and eviction
|
121
|
+
self._cleanup_expired()
|
122
|
+
self._evict_lru()
|
123
|
+
|
124
|
+
entry = CacheEntry(
|
125
|
+
data=value,
|
126
|
+
created_at=time.time(),
|
127
|
+
ttl=ttl or self.default_ttl,
|
128
|
+
last_accessed=time.time()
|
129
|
+
)
|
130
|
+
|
131
|
+
self._cache[key] = entry
|
132
|
+
|
133
|
+
def delete(self, key: str) -> bool:
|
134
|
+
"""Delete cached value"""
|
135
|
+
with self._lock:
|
136
|
+
if key in self._cache:
|
137
|
+
del self._cache[key]
|
138
|
+
return True
|
139
|
+
return False
|
140
|
+
|
141
|
+
def clear(self) -> None:
|
142
|
+
"""Clear all cached values"""
|
143
|
+
with self._lock:
|
144
|
+
self._cache.clear()
|
145
|
+
logger.info("Cache cleared")
|
146
|
+
|
147
|
+
def get_stats(self) -> Dict[str, Any]:
|
148
|
+
"""Get cache statistics"""
|
149
|
+
with self._lock:
|
150
|
+
hit_rate = (
|
151
|
+
self._stats["hits"] / self._stats["total_requests"]
|
152
|
+
if self._stats["total_requests"] > 0 else 0
|
153
|
+
)
|
154
|
+
|
155
|
+
return {
|
156
|
+
"cache_size": len(self._cache),
|
157
|
+
"max_size": self.max_size,
|
158
|
+
"default_ttl": self.default_ttl,
|
159
|
+
"hit_rate": round(hit_rate * 100, 2),
|
160
|
+
**self._stats
|
161
|
+
}
|
162
|
+
|
163
|
+
# Decorator for caching function results
|
164
|
+
def cached(ttl: float = 300.0, cache_key_func: Optional[Callable] = None):
|
165
|
+
"""
|
166
|
+
Decorator to cache function results
|
167
|
+
|
168
|
+
Args:
|
169
|
+
ttl: Time to live in seconds
|
170
|
+
cache_key_func: Custom function to generate cache key
|
171
|
+
"""
|
172
|
+
def decorator(func):
|
173
|
+
async def async_wrapper(*args, **kwargs):
|
174
|
+
# Generate cache key
|
175
|
+
if cache_key_func:
|
176
|
+
cache_key = cache_key_func(*args, **kwargs)
|
177
|
+
else:
|
178
|
+
cache_key = api_cache._generate_key(func.__name__, *args, **kwargs)
|
179
|
+
|
180
|
+
# Try to get from cache
|
181
|
+
cached_result = api_cache.get(cache_key)
|
182
|
+
if cached_result is not None:
|
183
|
+
logger.debug(f"Cache hit for {func.__name__}")
|
184
|
+
return cached_result
|
185
|
+
|
186
|
+
# Execute function and cache result
|
187
|
+
try:
|
188
|
+
result = await func(*args, **kwargs)
|
189
|
+
api_cache.set(cache_key, result, ttl)
|
190
|
+
logger.debug(f"Cached result for {func.__name__}")
|
191
|
+
return result
|
192
|
+
except Exception as e:
|
193
|
+
logger.error(f"Function {func.__name__} failed: {e}")
|
194
|
+
raise
|
195
|
+
|
196
|
+
def sync_wrapper(*args, **kwargs):
|
197
|
+
# Generate cache key
|
198
|
+
if cache_key_func:
|
199
|
+
cache_key = cache_key_func(*args, **kwargs)
|
200
|
+
else:
|
201
|
+
cache_key = api_cache._generate_key(func.__name__, *args, **kwargs)
|
202
|
+
|
203
|
+
# Try to get from cache
|
204
|
+
cached_result = api_cache.get(cache_key)
|
205
|
+
if cached_result is not None:
|
206
|
+
logger.debug(f"Cache hit for {func.__name__}")
|
207
|
+
return cached_result
|
208
|
+
|
209
|
+
# Execute function and cache result
|
210
|
+
try:
|
211
|
+
result = func(*args, **kwargs)
|
212
|
+
api_cache.set(cache_key, result, ttl)
|
213
|
+
logger.debug(f"Cached result for {func.__name__}")
|
214
|
+
return result
|
215
|
+
except Exception as e:
|
216
|
+
logger.error(f"Function {func.__name__} failed: {e}")
|
217
|
+
raise
|
218
|
+
|
219
|
+
# Return appropriate wrapper based on function type
|
220
|
+
if asyncio.iscoroutinefunction(func):
|
221
|
+
return async_wrapper
|
222
|
+
else:
|
223
|
+
return sync_wrapper
|
224
|
+
|
225
|
+
return decorator
|
226
|
+
|
227
|
+
# Global cache instance
|
228
|
+
api_cache = APICache(default_ttl=300.0, max_size=1000)
|
229
|
+
|
230
|
+
def get_api_cache() -> APICache:
|
231
|
+
"""Get the global API cache instance"""
|
232
|
+
return api_cache
|
233
|
+
|
234
|
+
# Cache key generators for common patterns
|
235
|
+
def model_list_cache_key(service_type=None):
|
236
|
+
"""Generate cache key for model list API"""
|
237
|
+
return f"models_list_{service_type or 'all'}"
|
238
|
+
|
239
|
+
def provider_list_cache_key():
|
240
|
+
"""Generate cache key for provider list API"""
|
241
|
+
return "providers_list"
|
242
|
+
|
243
|
+
def custom_models_cache_key(model_type=None, provider=None):
|
244
|
+
"""Generate cache key for custom models API"""
|
245
|
+
return f"custom_models_{model_type or 'all'}_{provider or 'all'}"
|
@@ -0,0 +1 @@
|
|
1
|
+
# Dependencies module
|
@@ -0,0 +1,194 @@
|
|
1
|
+
"""
|
2
|
+
Authentication and Authorization Dependencies
|
3
|
+
|
4
|
+
FastAPI dependencies for handling authentication, authorization,
|
5
|
+
and tenant access control. Integrates with existing auth middleware.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from fastapi import HTTPException, Depends, Request
|
9
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
10
|
+
from typing import Optional, Dict, Any
|
11
|
+
import logging
|
12
|
+
|
13
|
+
from ..middleware.tenant_context import get_tenant_context, require_tenant_context, TenantContext
|
14
|
+
from ..middleware.auth import authenticate_api_key, require_admin_access as require_api_admin
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
class User:
|
19
|
+
"""User model for authentication"""
|
20
|
+
def __init__(self, user_id: str, email: str, role: str = "member", organization_id: str = None):
|
21
|
+
self.user_id = user_id
|
22
|
+
self.email = email
|
23
|
+
self.role = role
|
24
|
+
self.organization_id = organization_id
|
25
|
+
|
26
|
+
def is_admin(self) -> bool:
|
27
|
+
return self.role in ["admin", "owner"]
|
28
|
+
|
29
|
+
def is_system_admin(self) -> bool:
|
30
|
+
return self.role == "system_admin"
|
31
|
+
|
32
|
+
class Organization:
|
33
|
+
"""Organization model"""
|
34
|
+
def __init__(self, organization_id: str, name: str, plan: str = "starter", status: str = "active"):
|
35
|
+
self.organization_id = organization_id
|
36
|
+
self.name = name
|
37
|
+
self.plan = plan
|
38
|
+
self.status = status
|
39
|
+
|
40
|
+
async def get_current_user(
|
41
|
+
auth_data: Dict = Depends(authenticate_api_key)
|
42
|
+
) -> Optional[User]:
|
43
|
+
"""
|
44
|
+
Get current authenticated user from existing auth system.
|
45
|
+
Integrates with the existing API key authentication.
|
46
|
+
"""
|
47
|
+
try:
|
48
|
+
if not auth_data.get("authenticated", True): # Anonymous when auth disabled
|
49
|
+
return None
|
50
|
+
|
51
|
+
# Get tenant context which should contain user info
|
52
|
+
tenant_context = get_tenant_context()
|
53
|
+
|
54
|
+
# Create user from auth data and tenant context
|
55
|
+
user_id = tenant_context.user_id if tenant_context else auth_data.get("name", "anonymous")
|
56
|
+
organization_id = tenant_context.organization_id if tenant_context else None
|
57
|
+
|
58
|
+
# Map API key scopes to user roles
|
59
|
+
scopes = auth_data.get("scopes", [])
|
60
|
+
if "admin" in scopes:
|
61
|
+
role = "admin"
|
62
|
+
elif "write" in scopes:
|
63
|
+
role = "member"
|
64
|
+
else:
|
65
|
+
role = "viewer"
|
66
|
+
|
67
|
+
return User(
|
68
|
+
user_id=user_id,
|
69
|
+
email=f"{user_id}@example.com", # TODO: Get from database
|
70
|
+
role=role,
|
71
|
+
organization_id=organization_id
|
72
|
+
)
|
73
|
+
|
74
|
+
except Exception as e:
|
75
|
+
logger.error(f"Error getting current user: {e}")
|
76
|
+
return None
|
77
|
+
|
78
|
+
async def require_authenticated_user(
|
79
|
+
current_user: Optional[User] = Depends(get_current_user)
|
80
|
+
) -> User:
|
81
|
+
"""
|
82
|
+
Require authenticated user or raise 401 error.
|
83
|
+
"""
|
84
|
+
if not current_user:
|
85
|
+
raise HTTPException(
|
86
|
+
status_code=401,
|
87
|
+
detail="Authentication required"
|
88
|
+
)
|
89
|
+
return current_user
|
90
|
+
|
91
|
+
async def get_current_organization() -> Optional[Organization]:
|
92
|
+
"""
|
93
|
+
Get current organization from tenant context.
|
94
|
+
"""
|
95
|
+
try:
|
96
|
+
tenant_context = get_tenant_context()
|
97
|
+
if tenant_context:
|
98
|
+
return Organization(
|
99
|
+
organization_id=tenant_context.organization_id,
|
100
|
+
name="Organization", # TODO: Get from database
|
101
|
+
plan=tenant_context.plan,
|
102
|
+
status="active"
|
103
|
+
)
|
104
|
+
return None
|
105
|
+
|
106
|
+
except Exception as e:
|
107
|
+
logger.error(f"Error getting current organization: {e}")
|
108
|
+
return None
|
109
|
+
|
110
|
+
async def require_organization_access() -> Organization:
|
111
|
+
"""
|
112
|
+
Require organization context or raise 401 error.
|
113
|
+
"""
|
114
|
+
org = await get_current_organization()
|
115
|
+
if not org:
|
116
|
+
raise HTTPException(
|
117
|
+
status_code=401,
|
118
|
+
detail="Organization access required"
|
119
|
+
)
|
120
|
+
return org
|
121
|
+
|
122
|
+
async def require_admin(
|
123
|
+
auth_data: Dict = Depends(require_api_admin),
|
124
|
+
current_user: User = Depends(get_current_user)
|
125
|
+
) -> User:
|
126
|
+
"""
|
127
|
+
Require admin role within organization.
|
128
|
+
Uses existing API key admin check plus tenant context.
|
129
|
+
"""
|
130
|
+
if not current_user or not current_user.is_admin():
|
131
|
+
raise HTTPException(
|
132
|
+
status_code=403,
|
133
|
+
detail="Admin privileges required"
|
134
|
+
)
|
135
|
+
return current_user
|
136
|
+
|
137
|
+
async def require_system_admin(
|
138
|
+
auth_data: Dict = Depends(require_api_admin)
|
139
|
+
) -> Dict:
|
140
|
+
"""
|
141
|
+
Require system admin role (for tenant management).
|
142
|
+
For now, maps to API key admin access.
|
143
|
+
"""
|
144
|
+
# For system admin, require API key admin access
|
145
|
+
return auth_data
|
146
|
+
|
147
|
+
def require_plan(min_plan: str):
|
148
|
+
"""
|
149
|
+
Factory function to create dependency that requires specific plan level.
|
150
|
+
Usage: Depends(require_plan("pro"))
|
151
|
+
"""
|
152
|
+
async def _check_plan():
|
153
|
+
tenant_context = get_tenant_context()
|
154
|
+
if not tenant_context:
|
155
|
+
raise HTTPException(
|
156
|
+
status_code=401,
|
157
|
+
detail="Authentication required"
|
158
|
+
)
|
159
|
+
|
160
|
+
plan_hierarchy = {
|
161
|
+
"starter": 1,
|
162
|
+
"pro": 2,
|
163
|
+
"enterprise": 3
|
164
|
+
}
|
165
|
+
|
166
|
+
current_plan_level = plan_hierarchy.get(tenant_context.plan, 0)
|
167
|
+
required_plan_level = plan_hierarchy.get(min_plan, 99)
|
168
|
+
|
169
|
+
if current_plan_level < required_plan_level:
|
170
|
+
raise HTTPException(
|
171
|
+
status_code=403,
|
172
|
+
detail=f"Plan upgrade required. Current: {tenant_context.plan}, Required: {min_plan}"
|
173
|
+
)
|
174
|
+
|
175
|
+
return tenant_context
|
176
|
+
|
177
|
+
return _check_plan
|
178
|
+
|
179
|
+
def check_resource_access(resource_type: str, action: str = "read"):
|
180
|
+
"""
|
181
|
+
Factory function to create dependency that checks resource access permissions.
|
182
|
+
"""
|
183
|
+
async def _check_access():
|
184
|
+
tenant_context = require_tenant_context()
|
185
|
+
|
186
|
+
if not tenant_context.can_access_resource(resource_type, action):
|
187
|
+
raise HTTPException(
|
188
|
+
status_code=403,
|
189
|
+
detail=f"Access denied to {resource_type}"
|
190
|
+
)
|
191
|
+
|
192
|
+
return tenant_context
|
193
|
+
|
194
|
+
return _check_access
|