isa-model 0.3.4__py3-none-any.whl → 0.3.5__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 (49) hide show
  1. isa_model/config/__init__.py +9 -0
  2. isa_model/config/config_manager.py +213 -0
  3. isa_model/core/model_manager.py +5 -0
  4. isa_model/core/model_registry.py +39 -6
  5. isa_model/core/storage/supabase_storage.py +344 -0
  6. isa_model/core/vision_models_init.py +116 -0
  7. isa_model/deployment/cloud/__init__.py +9 -0
  8. isa_model/deployment/cloud/modal/__init__.py +10 -0
  9. isa_model/deployment/cloud/modal/isa_vision_doc_service.py +612 -0
  10. isa_model/deployment/cloud/modal/isa_vision_ui_service.py +305 -0
  11. isa_model/inference/ai_factory.py +238 -14
  12. isa_model/inference/providers/modal_provider.py +109 -0
  13. isa_model/inference/providers/yyds_provider.py +108 -0
  14. isa_model/inference/services/__init__.py +2 -1
  15. isa_model/inference/services/base_service.py +0 -38
  16. isa_model/inference/services/llm/base_llm_service.py +32 -0
  17. isa_model/inference/services/llm/llm_adapter.py +40 -0
  18. isa_model/inference/services/llm/ollama_llm_service.py +104 -3
  19. isa_model/inference/services/llm/openai_llm_service.py +67 -15
  20. isa_model/inference/services/llm/yyds_llm_service.py +254 -0
  21. isa_model/inference/services/stacked/__init__.py +26 -0
  22. isa_model/inference/services/stacked/base_stacked_service.py +269 -0
  23. isa_model/inference/services/stacked/config.py +426 -0
  24. isa_model/inference/services/stacked/doc_analysis_service.py +640 -0
  25. isa_model/inference/services/stacked/flux_professional_service.py +579 -0
  26. isa_model/inference/services/stacked/ui_analysis_service.py +1319 -0
  27. isa_model/inference/services/vision/base_image_gen_service.py +0 -34
  28. isa_model/inference/services/vision/base_vision_service.py +46 -2
  29. isa_model/inference/services/vision/isA_vision_service.py +402 -0
  30. isa_model/inference/services/vision/openai_vision_service.py +151 -9
  31. isa_model/inference/services/vision/replicate_image_gen_service.py +166 -38
  32. isa_model/inference/services/vision/replicate_vision_service.py +693 -0
  33. isa_model/serving/__init__.py +19 -0
  34. isa_model/serving/api/__init__.py +10 -0
  35. isa_model/serving/api/fastapi_server.py +84 -0
  36. isa_model/serving/api/middleware/__init__.py +9 -0
  37. isa_model/serving/api/middleware/request_logger.py +88 -0
  38. isa_model/serving/api/routes/__init__.py +5 -0
  39. isa_model/serving/api/routes/health.py +82 -0
  40. isa_model/serving/api/routes/llm.py +19 -0
  41. isa_model/serving/api/routes/ui_analysis.py +223 -0
  42. isa_model/serving/api/routes/vision.py +19 -0
  43. isa_model/serving/api/schemas/__init__.py +17 -0
  44. isa_model/serving/api/schemas/common.py +33 -0
  45. isa_model/serving/api/schemas/ui_analysis.py +78 -0
  46. {isa_model-0.3.4.dist-info → isa_model-0.3.5.dist-info}/METADATA +1 -1
  47. {isa_model-0.3.4.dist-info → isa_model-0.3.5.dist-info}/RECORD +49 -17
  48. {isa_model-0.3.4.dist-info → isa_model-0.3.5.dist-info}/WHEEL +0 -0
  49. {isa_model-0.3.4.dist-info → isa_model-0.3.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,9 @@
1
+ """
2
+ Configuration Management Module
3
+
4
+ Unified configuration system for all ISA Model components
5
+ """
6
+
7
+ from .config_manager import ConfigManager, get_config, DeploymentConfig, ModelConfig
8
+
9
+ __all__ = ["ConfigManager", "get_config", "DeploymentConfig", "ModelConfig"]
@@ -0,0 +1,213 @@
1
+ """
2
+ Configuration Manager
3
+
4
+ Central configuration management with environment support
5
+ """
6
+
7
+ import os
8
+ import yaml
9
+ from typing import Dict, Any, Optional
10
+ from pathlib import Path
11
+ from dataclasses import dataclass
12
+ import logging
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ @dataclass
17
+ class ConfigSection:
18
+ """Base configuration section"""
19
+ def to_dict(self) -> Dict[str, Any]:
20
+ return self.__dict__
21
+
22
+ @dataclass
23
+ class DeploymentConfig(ConfigSection):
24
+ """Deployment configuration"""
25
+ platform: str = "replicate" # replicate, modal, aws, local
26
+ modal_app_name: str = "isa-ui-analysis"
27
+ modal_gpu_type: str = "A100-40GB"
28
+ modal_memory: int = 32768
29
+ modal_timeout: int = 1800
30
+ modal_keep_warm: int = 1
31
+
32
+ @dataclass
33
+ class ModelConfig(ConfigSection):
34
+ """Model configuration"""
35
+ ui_detection_model: str = "microsoft/omniparser-v2"
36
+ ui_planning_model: str = "gpt-4o-mini"
37
+ fallback_detection: str = "yolov8n"
38
+ quantization: bool = False
39
+ batch_size: int = 1
40
+
41
+ @dataclass
42
+ class ServingConfig(ConfigSection):
43
+ """Serving configuration"""
44
+ host: str = "0.0.0.0"
45
+ port: int = 8000
46
+ workers: int = 1
47
+ reload: bool = False
48
+ log_level: str = "info"
49
+ cors_origins: list = None
50
+
51
+ def __post_init__(self):
52
+ if self.cors_origins is None:
53
+ self.cors_origins = ["*"]
54
+
55
+ @dataclass
56
+ class APIConfig(ConfigSection):
57
+ """API configuration"""
58
+ rate_limit: int = 100 # requests per minute
59
+ max_file_size: int = 10 * 1024 * 1024 # 10MB
60
+ cache_ttl: int = 3600 # 1 hour
61
+ enable_auth: bool = False
62
+
63
+ @dataclass
64
+ class ISAConfig:
65
+ """Complete ISA configuration"""
66
+ environment: str
67
+ deployment: DeploymentConfig
68
+ models: ModelConfig
69
+ serving: ServingConfig
70
+ api: APIConfig
71
+
72
+ def to_dict(self) -> Dict[str, Any]:
73
+ return {
74
+ "environment": self.environment,
75
+ "deployment": self.deployment.to_dict(),
76
+ "models": self.models.to_dict(),
77
+ "serving": self.serving.to_dict(),
78
+ "api": self.api.to_dict()
79
+ }
80
+
81
+ class ConfigManager:
82
+ """Configuration manager with environment support"""
83
+
84
+ _instance = None
85
+ _config = None
86
+
87
+ def __new__(cls):
88
+ if cls._instance is None:
89
+ cls._instance = super(ConfigManager, cls).__new__(cls)
90
+ return cls._instance
91
+
92
+ def __init__(self):
93
+ if self._config is None:
94
+ self._load_config()
95
+
96
+ def _load_config(self):
97
+ """Load configuration from environment and files"""
98
+ env = os.getenv("ISA_ENV", "development")
99
+
100
+ # Default configurations
101
+ default_config = {
102
+ "deployment": DeploymentConfig(),
103
+ "models": ModelConfig(),
104
+ "serving": ServingConfig(),
105
+ "api": APIConfig()
106
+ }
107
+
108
+ # Load environment-specific configuration
109
+ config_file = self._get_config_file(env)
110
+ if config_file and config_file.exists():
111
+ try:
112
+ with open(config_file, 'r') as f:
113
+ file_config = yaml.safe_load(f)
114
+
115
+ # Merge configurations
116
+ self._config = self._merge_configs(default_config, file_config, env)
117
+ logger.info(f"Loaded configuration for environment: {env}")
118
+
119
+ except Exception as e:
120
+ logger.warning(f"Failed to load config file {config_file}: {e}")
121
+ self._config = ISAConfig(environment=env, **default_config)
122
+ else:
123
+ logger.info(f"No config file found for {env}, using defaults")
124
+ self._config = ISAConfig(environment=env, **default_config)
125
+
126
+ # Override with environment variables
127
+ self._apply_env_overrides()
128
+
129
+ def _get_config_file(self, env: str) -> Optional[Path]:
130
+ """Get configuration file path for environment"""
131
+ # Try to find config file in multiple locations
132
+ possible_paths = [
133
+ Path(__file__).parent / "environments" / f"{env}.yaml",
134
+ Path.cwd() / "config" / f"{env}.yaml",
135
+ Path.cwd() / f"config_{env}.yaml"
136
+ ]
137
+
138
+ for path in possible_paths:
139
+ if path.exists():
140
+ return path
141
+ return None
142
+
143
+ def _merge_configs(self, default: Dict, file_config: Dict, env: str) -> ISAConfig:
144
+ """Merge default and file configurations"""
145
+
146
+ # Update deployment config
147
+ deployment_data = {**default["deployment"].__dict__}
148
+ if "deployment" in file_config:
149
+ deployment_data.update(file_config["deployment"])
150
+ deployment = DeploymentConfig(**deployment_data)
151
+
152
+ # Update model config
153
+ models_data = {**default["models"].__dict__}
154
+ if "models" in file_config:
155
+ models_data.update(file_config["models"])
156
+ models = ModelConfig(**models_data)
157
+
158
+ # Update serving config
159
+ serving_data = {**default["serving"].__dict__}
160
+ if "serving" in file_config:
161
+ serving_data.update(file_config["serving"])
162
+ serving = ServingConfig(**serving_data)
163
+
164
+ # Update API config
165
+ api_data = {**default["api"].__dict__}
166
+ if "api" in file_config:
167
+ api_data.update(file_config["api"])
168
+ api = APIConfig(**api_data)
169
+
170
+ return ISAConfig(
171
+ environment=env,
172
+ deployment=deployment,
173
+ models=models,
174
+ serving=serving,
175
+ api=api
176
+ )
177
+
178
+ def _apply_env_overrides(self):
179
+ """Apply environment variable overrides"""
180
+ # Deployment overrides
181
+ if os.getenv("ISA_DEPLOYMENT_PLATFORM"):
182
+ self._config.deployment.platform = os.getenv("ISA_DEPLOYMENT_PLATFORM")
183
+
184
+ # Model overrides
185
+ if os.getenv("ISA_UI_DETECTION_MODEL"):
186
+ self._config.models.ui_detection_model = os.getenv("ISA_UI_DETECTION_MODEL")
187
+
188
+ # Serving overrides
189
+ if os.getenv("ISA_SERVING_PORT"):
190
+ self._config.serving.port = int(os.getenv("ISA_SERVING_PORT"))
191
+
192
+ if os.getenv("ISA_SERVING_HOST"):
193
+ self._config.serving.host = os.getenv("ISA_SERVING_HOST")
194
+
195
+ def get_config(self) -> ISAConfig:
196
+ """Get current configuration"""
197
+ return self._config
198
+
199
+ def reload(self):
200
+ """Reload configuration"""
201
+ self._config = None
202
+ self._load_config()
203
+
204
+ # Singleton instance
205
+ _config_manager = ConfigManager()
206
+
207
+ def get_config() -> ISAConfig:
208
+ """Get configuration instance"""
209
+ return _config_manager.get_config()
210
+
211
+ def reload_config():
212
+ """Reload configuration"""
213
+ _config_manager.reload()
@@ -41,6 +41,11 @@ class ModelManager:
41
41
  "meta/meta-llama-3-8b-instruct": {"input": 0.05, "output": 0.25},
42
42
  "kokoro-82m": {"input": 0.0, "output": 0.4}, # ~$0.0004 per second
43
43
  "jaaari/kokoro-82m:f559560eb822dc509045f3921a1921234918b91739db4bf3daab2169b71c7a13": {"input": 0.0, "output": 0.4},
44
+ },
45
+ # YYDS Models
46
+ "yyds": {
47
+ "claude-sonnet-4-20250514": {"input": 4.5, "output": 22.5}, # $0.0045/1K = $4.5/1M, $0.0225/1K = $22.5/1M
48
+ "claude-3-5-sonnet-20241022": {"input": 3.0, "output": 15.0}, # $0.003/1K = $3.0/1M, $0.015/1K = $15.0/1M
44
49
  }
45
50
  }
46
51
 
@@ -20,6 +20,10 @@ class ModelCapability(str, Enum):
20
20
  IMAGE_ANALYSIS = "image_analysis"
21
21
  AUDIO_TRANSCRIPTION = "audio_transcription"
22
22
  IMAGE_UNDERSTANDING = "image_understanding"
23
+ UI_DETECTION = "ui_detection"
24
+ OCR = "ocr"
25
+ TABLE_DETECTION = "table_detection"
26
+ TABLE_STRUCTURE_RECOGNITION = "table_structure_recognition"
23
27
 
24
28
  class ModelType(str, Enum):
25
29
  """Model types"""
@@ -32,13 +36,33 @@ class ModelType(str, Enum):
32
36
  VISION = "vision"
33
37
 
34
38
  class ModelRegistry:
35
- """SQLite-based registry for model metadata and capabilities"""
39
+ """Model registry with SQLite or Supabase backend"""
36
40
 
37
- def __init__(self, db_path: str = "./models/model_registry.db"):
38
- self.db_path = Path(db_path)
39
- self.db_path.parent.mkdir(parents=True, exist_ok=True)
40
- self._lock = threading.Lock()
41
- self._initialize_database()
41
+ def __init__(self, db_path: str = "./models/model_registry.db", use_supabase: bool = None):
42
+ # Auto-detect Supabase if environment variables are set
43
+ if use_supabase is None:
44
+ import os
45
+ use_supabase = bool(os.getenv("SUPABASE_URL") and os.getenv("SUPABASE_ANON_KEY"))
46
+
47
+ self.use_supabase = use_supabase
48
+
49
+ if self.use_supabase:
50
+ try:
51
+ from .storage.supabase_storage import SupabaseModelRegistry
52
+ self.backend = SupabaseModelRegistry()
53
+ logger.info("Using Supabase backend for model registry")
54
+ except ImportError as e:
55
+ logger.warning(f"Supabase not available, falling back to SQLite: {e}")
56
+ self.use_supabase = False
57
+
58
+ if not self.use_supabase:
59
+ # Use SQLite backend
60
+ self.db_path = Path(db_path)
61
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
62
+ self._lock = threading.Lock()
63
+ self._initialize_database()
64
+ self.backend = None
65
+ logger.info("Using SQLite backend for model registry")
42
66
 
43
67
  def _initialize_database(self):
44
68
  """Initialize SQLite database with required tables"""
@@ -78,6 +102,15 @@ class ModelRegistry:
78
102
  capabilities: List[ModelCapability],
79
103
  metadata: Dict[str, Any]) -> bool:
80
104
  """Register a model with its capabilities and metadata"""
105
+ if self.use_supabase:
106
+ return self.backend.register_model(
107
+ model_id=model_id,
108
+ model_type=model_type.value,
109
+ capabilities=[cap.value for cap in capabilities],
110
+ metadata=metadata
111
+ )
112
+
113
+ # SQLite implementation
81
114
  try:
82
115
  with self._lock:
83
116
  with sqlite3.connect(self.db_path) as conn:
@@ -0,0 +1,344 @@
1
+ """
2
+ Supabase Storage Implementation for Model Registry
3
+
4
+ Uses Supabase as the backend database for model metadata and capabilities
5
+ Supports the full model lifecycle with cloud-based storage
6
+ """
7
+
8
+ import os
9
+ import json
10
+ import logging
11
+ from typing import Optional, Dict, Any, List
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+
15
+ try:
16
+ from supabase import create_client, Client
17
+ from dotenv import load_dotenv
18
+ SUPABASE_AVAILABLE = True
19
+ except ImportError:
20
+ SUPABASE_AVAILABLE = False
21
+
22
+ from ..model_storage import ModelStorage
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ class SupabaseModelRegistry:
27
+ """
28
+ Supabase-based model registry for metadata and capabilities
29
+
30
+ Replaces SQLite with cloud-based Supabase database
31
+ """
32
+
33
+ def __init__(self):
34
+ if not SUPABASE_AVAILABLE:
35
+ raise ImportError("supabase-py is required. Install with: pip install supabase")
36
+
37
+ # Load environment variables
38
+ load_dotenv()
39
+
40
+ self.supabase_url = os.getenv("SUPABASE_URL")
41
+ self.supabase_key = os.getenv("SUPABASE_ANON_KEY")
42
+
43
+ if not self.supabase_url or not self.supabase_key:
44
+ raise ValueError("SUPABASE_URL and SUPABASE_ANON_KEY must be set in environment")
45
+
46
+ # Initialize Supabase client
47
+ self.supabase: Client = create_client(self.supabase_url, self.supabase_key)
48
+
49
+ # Initialize tables if needed
50
+ self._ensure_tables()
51
+
52
+ logger.info("Supabase model registry initialized")
53
+
54
+ def _ensure_tables(self):
55
+ """Ensure required tables exist in Supabase"""
56
+ # Note: In production, these tables should be created via Supabase migrations
57
+ # This is just for development/initialization
58
+ try:
59
+ # Check if models table exists by trying to query it
60
+ result = self.supabase.table('models').select('model_id').limit(1).execute()
61
+ except Exception as e:
62
+ logger.warning(f"Models table might not exist: {e}")
63
+ # In production, you would run proper migrations here
64
+
65
+ def register_model(self,
66
+ model_id: str,
67
+ model_type: str,
68
+ capabilities: List[str],
69
+ metadata: Dict[str, Any]) -> bool:
70
+ """Register a model with its capabilities and metadata"""
71
+ try:
72
+ current_time = datetime.now().isoformat()
73
+
74
+ # Prepare model data
75
+ model_data = {
76
+ 'model_id': model_id,
77
+ 'model_type': model_type,
78
+ 'metadata': json.dumps(metadata),
79
+ 'created_at': current_time,
80
+ 'updated_at': current_time
81
+ }
82
+
83
+ # Insert or update model
84
+ result = self.supabase.table('models').upsert(model_data).execute()
85
+
86
+ if not result.data:
87
+ logger.error(f"Failed to insert model {model_id}")
88
+ return False
89
+
90
+ # Delete existing capabilities
91
+ self.supabase.table('model_capabilities').delete().eq('model_id', model_id).execute()
92
+
93
+ # Insert new capabilities
94
+ if capabilities:
95
+ capability_data = [
96
+ {
97
+ 'model_id': model_id,
98
+ 'capability': capability,
99
+ 'created_at': current_time
100
+ }
101
+ for capability in capabilities
102
+ ]
103
+
104
+ cap_result = self.supabase.table('model_capabilities').insert(capability_data).execute()
105
+
106
+ if not cap_result.data:
107
+ logger.error(f"Failed to insert capabilities for {model_id}")
108
+ return False
109
+
110
+ logger.info(f"Successfully registered model {model_id}")
111
+ return True
112
+
113
+ except Exception as e:
114
+ logger.error(f"Failed to register model {model_id}: {e}")
115
+ return False
116
+
117
+ def unregister_model(self, model_id: str) -> bool:
118
+ """Unregister a model"""
119
+ try:
120
+ # Delete model (capabilities will be cascade deleted)
121
+ result = self.supabase.table('models').delete().eq('model_id', model_id).execute()
122
+
123
+ if result.data:
124
+ logger.info(f"Unregistered model {model_id}")
125
+ return True
126
+ return False
127
+
128
+ except Exception as e:
129
+ logger.error(f"Failed to unregister model {model_id}: {e}")
130
+ return False
131
+
132
+ def get_model_info(self, model_id: str) -> Optional[Dict[str, Any]]:
133
+ """Get model information"""
134
+ try:
135
+ # Get model info
136
+ model_result = self.supabase.table('models').select('*').eq('model_id', model_id).execute()
137
+
138
+ if not model_result.data:
139
+ return None
140
+
141
+ model_row = model_result.data[0]
142
+
143
+ # Get capabilities
144
+ cap_result = self.supabase.table('model_capabilities').select('capability').eq('model_id', model_id).execute()
145
+ capabilities = [cap['capability'] for cap in cap_result.data]
146
+
147
+ model_info = {
148
+ "model_id": model_row["model_id"],
149
+ "type": model_row["model_type"],
150
+ "capabilities": capabilities,
151
+ "metadata": json.loads(model_row["metadata"]) if model_row["metadata"] else {},
152
+ "created_at": model_row["created_at"],
153
+ "updated_at": model_row["updated_at"]
154
+ }
155
+
156
+ return model_info
157
+
158
+ except Exception as e:
159
+ logger.error(f"Failed to get model info for {model_id}: {e}")
160
+ return None
161
+
162
+ def get_models_by_type(self, model_type: str) -> Dict[str, Dict[str, Any]]:
163
+ """Get all models of a specific type"""
164
+ try:
165
+ models_result = self.supabase.table('models').select('*').eq('model_type', model_type).execute()
166
+
167
+ result = {}
168
+ for model in models_result.data:
169
+ model_id = model["model_id"]
170
+
171
+ # Get capabilities for this model
172
+ cap_result = self.supabase.table('model_capabilities').select('capability').eq('model_id', model_id).execute()
173
+ capabilities = [cap['capability'] for cap in cap_result.data]
174
+
175
+ result[model_id] = {
176
+ "type": model["model_type"],
177
+ "capabilities": capabilities,
178
+ "metadata": json.loads(model["metadata"]) if model["metadata"] else {},
179
+ "created_at": model["created_at"],
180
+ "updated_at": model["updated_at"]
181
+ }
182
+
183
+ return result
184
+
185
+ except Exception as e:
186
+ logger.error(f"Failed to get models by type {model_type}: {e}")
187
+ return {}
188
+
189
+ def get_models_by_capability(self, capability: str) -> Dict[str, Dict[str, Any]]:
190
+ """Get all models with a specific capability"""
191
+ try:
192
+ # Join query to get models with specific capability
193
+ query = """
194
+ SELECT DISTINCT m.*, mc.capability
195
+ FROM models m
196
+ INNER JOIN model_capabilities mc ON m.model_id = mc.model_id
197
+ WHERE mc.capability = %s
198
+ """
199
+
200
+ # Use RPC for complex queries
201
+ result = self.supabase.rpc('get_models_by_capability', {'capability_name': capability}).execute()
202
+
203
+ if result.data:
204
+ models_dict = {}
205
+ for row in result.data:
206
+ model_id = row['model_id']
207
+ if model_id not in models_dict:
208
+ # Get all capabilities for this model
209
+ cap_result = self.supabase.table('model_capabilities').select('capability').eq('model_id', model_id).execute()
210
+ capabilities = [cap['capability'] for cap in cap_result.data]
211
+
212
+ models_dict[model_id] = {
213
+ "type": row["model_type"],
214
+ "capabilities": capabilities,
215
+ "metadata": json.loads(row["metadata"]) if row["metadata"] else {},
216
+ "created_at": row["created_at"],
217
+ "updated_at": row["updated_at"]
218
+ }
219
+
220
+ return models_dict
221
+
222
+ # Fallback: manual join if RPC not available
223
+ cap_result = self.supabase.table('model_capabilities').select('model_id').eq('capability', capability).execute()
224
+ model_ids = [row['model_id'] for row in cap_result.data]
225
+
226
+ if not model_ids:
227
+ return {}
228
+
229
+ models_result = self.supabase.table('models').select('*').in_('model_id', model_ids).execute()
230
+
231
+ result_dict = {}
232
+ for model in models_result.data:
233
+ model_id = model["model_id"]
234
+
235
+ # Get all capabilities for this model
236
+ all_caps_result = self.supabase.table('model_capabilities').select('capability').eq('model_id', model_id).execute()
237
+ capabilities = [cap['capability'] for cap in all_caps_result.data]
238
+
239
+ result_dict[model_id] = {
240
+ "type": model["model_type"],
241
+ "capabilities": capabilities,
242
+ "metadata": json.loads(model["metadata"]) if model["metadata"] else {},
243
+ "created_at": model["created_at"],
244
+ "updated_at": model["updated_at"]
245
+ }
246
+
247
+ return result_dict
248
+
249
+ except Exception as e:
250
+ logger.error(f"Failed to get models by capability {capability}: {e}")
251
+ return {}
252
+
253
+ def has_capability(self, model_id: str, capability: str) -> bool:
254
+ """Check if a model has a specific capability"""
255
+ try:
256
+ result = self.supabase.table('model_capabilities').select('model_id').eq('model_id', model_id).eq('capability', capability).execute()
257
+
258
+ return len(result.data) > 0
259
+
260
+ except Exception as e:
261
+ logger.error(f"Failed to check capability for {model_id}: {e}")
262
+ return False
263
+
264
+ def list_models(self) -> Dict[str, Dict[str, Any]]:
265
+ """List all registered models"""
266
+ try:
267
+ models_result = self.supabase.table('models').select('*').order('created_at', desc=True).execute()
268
+
269
+ result = {}
270
+ for model in models_result.data:
271
+ model_id = model["model_id"]
272
+
273
+ # Get capabilities for this model
274
+ cap_result = self.supabase.table('model_capabilities').select('capability').eq('model_id', model_id).execute()
275
+ capabilities = [cap['capability'] for cap in cap_result.data]
276
+
277
+ result[model_id] = {
278
+ "type": model["model_type"],
279
+ "capabilities": capabilities,
280
+ "metadata": json.loads(model["metadata"]) if model["metadata"] else {},
281
+ "created_at": model["created_at"],
282
+ "updated_at": model["updated_at"]
283
+ }
284
+
285
+ return result
286
+
287
+ except Exception as e:
288
+ logger.error(f"Failed to list models: {e}")
289
+ return {}
290
+
291
+ def get_stats(self) -> Dict[str, Any]:
292
+ """Get registry statistics"""
293
+ try:
294
+ # Count total models
295
+ total_result = self.supabase.table('models').select('model_id', count='exact').execute()
296
+ total_models = total_result.count if total_result.count is not None else 0
297
+
298
+ # Count by type
299
+ type_result = self.supabase.rpc('get_model_type_counts').execute()
300
+ type_counts = {row['model_type']: row['count'] for row in type_result.data} if type_result.data else {}
301
+
302
+ # Count by capability
303
+ cap_result = self.supabase.rpc('get_capability_counts').execute()
304
+ capability_counts = {row['capability']: row['count'] for row in cap_result.data} if cap_result.data else {}
305
+
306
+ return {
307
+ "total_models": total_models,
308
+ "models_by_type": type_counts,
309
+ "models_by_capability": capability_counts
310
+ }
311
+
312
+ except Exception as e:
313
+ logger.error(f"Failed to get stats: {e}")
314
+ return {"total_models": 0, "models_by_type": {}, "models_by_capability": {}}
315
+
316
+ def search_models(self, query: str) -> Dict[str, Dict[str, Any]]:
317
+ """Search models by name or metadata"""
318
+ try:
319
+ # Search in model_id and metadata
320
+ models_result = self.supabase.table('models').select('*').or_(
321
+ f'model_id.ilike.%{query}%,metadata.ilike.%{query}%'
322
+ ).order('created_at', desc=True).execute()
323
+
324
+ result = {}
325
+ for model in models_result.data:
326
+ model_id = model["model_id"]
327
+
328
+ # Get capabilities for this model
329
+ cap_result = self.supabase.table('model_capabilities').select('capability').eq('model_id', model_id).execute()
330
+ capabilities = [cap['capability'] for cap in cap_result.data]
331
+
332
+ result[model_id] = {
333
+ "type": model["model_type"],
334
+ "capabilities": capabilities,
335
+ "metadata": json.loads(model["metadata"]) if model["metadata"] else {},
336
+ "created_at": model["created_at"],
337
+ "updated_at": model["updated_at"]
338
+ }
339
+
340
+ return result
341
+
342
+ except Exception as e:
343
+ logger.error(f"Failed to search models with query '{query}': {e}")
344
+ return {}