isa-model 0.3.6__py3-none-any.whl → 0.3.8__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 CHANGED
@@ -89,6 +89,47 @@ class ISAModelClient:
89
89
 
90
90
  logger.info("ISA Model Client initialized")
91
91
 
92
+ async def stream(
93
+ self,
94
+ input_data: Union[str, bytes, Path, Dict[str, Any]],
95
+ task: str,
96
+ service_type: str,
97
+ model_hint: Optional[str] = None,
98
+ provider_hint: Optional[str] = None,
99
+ **kwargs
100
+ ):
101
+ """
102
+ Streaming invoke method that yields tokens in real-time
103
+
104
+ Args:
105
+ input_data: Input data (text for LLM streaming)
106
+ task: Task to perform
107
+ service_type: Type of service (only "text" supports streaming)
108
+ model_hint: Optional model preference
109
+ provider_hint: Optional provider preference
110
+ **kwargs: Additional parameters
111
+
112
+ Yields:
113
+ Individual tokens as they arrive from the model
114
+
115
+ Example:
116
+ async for token in client.stream("Hello world", "chat", "text"):
117
+ print(token, end="", flush=True)
118
+ """
119
+ if service_type != "text":
120
+ raise ValueError("Streaming is only supported for text/LLM services")
121
+
122
+ try:
123
+ if self.mode == "api":
124
+ async for token in self._stream_api(input_data, task, service_type, model_hint, provider_hint, **kwargs):
125
+ yield token
126
+ else:
127
+ async for token in self._stream_local(input_data, task, service_type, model_hint, provider_hint, **kwargs):
128
+ yield token
129
+ except Exception as e:
130
+ logger.error(f"Failed to stream {task} on {service_type}: {e}")
131
+ raise
132
+
92
133
  async def invoke(
93
134
  self,
94
135
  input_data: Union[str, bytes, Path, Dict[str, Any]],
@@ -96,8 +137,10 @@ class ISAModelClient:
96
137
  service_type: str,
97
138
  model_hint: Optional[str] = None,
98
139
  provider_hint: Optional[str] = None,
140
+ stream: bool = False,
141
+ tools: Optional[List[Any]] = None,
99
142
  **kwargs
100
- ) -> Dict[str, Any]:
143
+ ) -> Union[Dict[str, Any], object]:
101
144
  """
102
145
  Unified invoke method with intelligent model selection
103
146
 
@@ -107,10 +150,13 @@ class ISAModelClient:
107
150
  service_type: Type of service (vision, audio, text, image, embedding)
108
151
  model_hint: Optional model preference
109
152
  provider_hint: Optional provider preference
153
+ stream: Enable streaming for text services (returns AsyncGenerator)
154
+ tools: Optional list of tools for function calling (only for text services)
110
155
  **kwargs: Additional task-specific parameters
111
156
 
112
157
  Returns:
113
- Unified response dictionary with result and metadata
158
+ If stream=False: Unified response dictionary with result and metadata
159
+ If stream=True: AsyncGenerator yielding tokens (only for text services)
114
160
 
115
161
  Examples:
116
162
  # Vision tasks
@@ -126,6 +172,17 @@ class ISAModelClient:
126
172
  await client.invoke("Translate this text", "translate", "text")
127
173
  await client.invoke("What is AI?", "chat", "text")
128
174
 
175
+ # Streaming text
176
+ async for token in await client.invoke("Hello", "chat", "text", stream=True):
177
+ print(token, end="", flush=True)
178
+
179
+ # Text with tools
180
+ await client.invoke("What's 5+3?", "chat", "text", tools=[calculator_function])
181
+
182
+ # Streaming with tools
183
+ async for token in await client.invoke("What's 5+3?", "chat", "text", stream=True, tools=[calculator_function]):
184
+ print(token, end="")
185
+
129
186
  # Image generation
130
187
  await client.invoke("A beautiful sunset", "generate_image", "image")
131
188
 
@@ -133,7 +190,33 @@ class ISAModelClient:
133
190
  await client.invoke("Text to embed", "create_embedding", "embedding")
134
191
  """
135
192
  try:
136
- # Route to appropriate mode
193
+ # Handle streaming case
194
+ if stream:
195
+ if service_type != "text":
196
+ raise ValueError("Streaming is only supported for text services")
197
+
198
+ if self.mode == "api":
199
+ return self._stream_api(
200
+ input_data=input_data,
201
+ task=task,
202
+ service_type=service_type,
203
+ model_hint=model_hint,
204
+ provider_hint=provider_hint,
205
+ tools=tools,
206
+ **kwargs
207
+ )
208
+ else:
209
+ return self._stream_local(
210
+ input_data=input_data,
211
+ task=task,
212
+ service_type=service_type,
213
+ model_hint=model_hint,
214
+ provider_hint=provider_hint,
215
+ tools=tools,
216
+ **kwargs
217
+ )
218
+
219
+ # Route to appropriate mode for non-streaming
137
220
  if self.mode == "api":
138
221
  return await self._invoke_api(
139
222
  input_data=input_data,
@@ -141,6 +224,7 @@ class ISAModelClient:
141
224
  service_type=service_type,
142
225
  model_hint=model_hint,
143
226
  provider_hint=provider_hint,
227
+ tools=tools,
144
228
  **kwargs
145
229
  )
146
230
  else:
@@ -150,6 +234,7 @@ class ISAModelClient:
150
234
  service_type=service_type,
151
235
  model_hint=model_hint,
152
236
  provider_hint=provider_hint,
237
+ tools=tools,
153
238
  **kwargs
154
239
  )
155
240
 
@@ -277,7 +362,8 @@ class ISAModelClient:
277
362
  service_type: str,
278
363
  model_name: str,
279
364
  provider: str,
280
- task: str
365
+ task: str,
366
+ tools: Optional[List[Any]] = None
281
367
  ) -> Any:
282
368
  """Get appropriate service instance"""
283
369
 
@@ -285,7 +371,11 @@ class ISAModelClient:
285
371
 
286
372
  # Check cache first
287
373
  if cache_key in self._service_cache:
288
- return self._service_cache[cache_key]
374
+ service = self._service_cache[cache_key]
375
+ # If tools are needed, bind them to the service
376
+ if tools and service_type == "text":
377
+ return service.bind_tools(tools)
378
+ return service
289
379
 
290
380
  try:
291
381
  # Route to appropriate AIFactory method
@@ -315,6 +405,11 @@ class ISAModelClient:
315
405
 
316
406
  # Cache the service
317
407
  self._service_cache[cache_key] = service
408
+
409
+ # If tools are needed, bind them to the service
410
+ if tools and service_type == "text":
411
+ return service.bind_tools(tools)
412
+
318
413
  return service
319
414
 
320
415
  except Exception as e:
@@ -544,6 +639,7 @@ class ISAModelClient:
544
639
  service_type: str,
545
640
  model_hint: Optional[str] = None,
546
641
  provider_hint: Optional[str] = None,
642
+ tools: Optional[List[Any]] = None,
547
643
  **kwargs
548
644
  ) -> Dict[str, Any]:
549
645
  """Local invoke using AI Factory (original logic)"""
@@ -562,7 +658,8 @@ class ISAModelClient:
562
658
  service_type=service_type,
563
659
  model_name=selected_model["model_id"],
564
660
  provider=selected_model["provider"],
565
- task=task
661
+ task=task,
662
+ tools=tools
566
663
  )
567
664
 
568
665
  # Step 3: Execute task with unified interface
@@ -744,6 +841,103 @@ class ISAModelClient:
744
841
  logger.error(f"API binary upload failed: {e}")
745
842
  raise
746
843
 
844
+ async def _stream_local(
845
+ self,
846
+ input_data: Union[str, bytes, Path, Dict[str, Any]],
847
+ task: str,
848
+ service_type: str,
849
+ model_hint: Optional[str] = None,
850
+ provider_hint: Optional[str] = None,
851
+ tools: Optional[List[Any]] = None,
852
+ **kwargs
853
+ ):
854
+ """Local streaming using AI Factory"""
855
+ # Step 1: Select best model for this task
856
+ selected_model = await self._select_model(
857
+ input_data=input_data,
858
+ task=task,
859
+ service_type=service_type,
860
+ model_hint=model_hint,
861
+ provider_hint=provider_hint
862
+ )
863
+
864
+ # Step 2: Get appropriate service
865
+ service = await self._get_service(
866
+ service_type=service_type,
867
+ model_name=selected_model["model_id"],
868
+ provider=selected_model["provider"],
869
+ task=task,
870
+ tools=tools
871
+ )
872
+
873
+ # Step 3: Yield tokens from the stream
874
+ async for token in service.astream(input_data):
875
+ yield token
876
+
877
+ async def _stream_api(
878
+ self,
879
+ input_data: Union[str, bytes, Path, Dict[str, Any]],
880
+ task: str,
881
+ service_type: str,
882
+ model_hint: Optional[str] = None,
883
+ provider_hint: Optional[str] = None,
884
+ **kwargs
885
+ ):
886
+ """API streaming using Server-Sent Events (SSE)"""
887
+
888
+ # Only support text streaming for now
889
+ if not isinstance(input_data, (str, dict)):
890
+ raise ValueError("API streaming only supports text input")
891
+
892
+ payload = {
893
+ "input_data": input_data,
894
+ "task": task,
895
+ "service_type": service_type,
896
+ "model_hint": model_hint,
897
+ "provider_hint": provider_hint,
898
+ "stream": True,
899
+ "parameters": kwargs
900
+ }
901
+
902
+ async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=300)) as session:
903
+ try:
904
+ async with session.post(
905
+ f"{self.api_url}/api/v1/stream",
906
+ json=payload,
907
+ headers=self.headers
908
+ ) as response:
909
+
910
+ if response.status == 200:
911
+ # Parse SSE stream
912
+ async for line in response.content:
913
+ if line:
914
+ line_str = line.decode().strip()
915
+ if line_str.startswith("data: "):
916
+ try:
917
+ # Parse SSE data
918
+ import json
919
+ json_str = line_str[6:] # Remove "data: " prefix
920
+ data = json.loads(json_str)
921
+
922
+ if data.get("type") == "token" and "token" in data:
923
+ yield data["token"]
924
+ elif data.get("type") == "completion":
925
+ # End of stream
926
+ break
927
+ elif data.get("type") == "error":
928
+ raise Exception(f"Server error: {data.get('error')}")
929
+
930
+ except json.JSONDecodeError:
931
+ # Skip malformed lines
932
+ continue
933
+ else:
934
+ error_data = await response.text()
935
+ raise Exception(f"API streaming error {response.status}: {error_data}")
936
+
937
+ except Exception as e:
938
+ logger.error(f"API streaming failed: {e}")
939
+ raise
940
+
747
941
 
748
942
  # Convenience function for quick access
749
943
  def create_client(
@@ -19,10 +19,11 @@ class AutoDeployVisionService(BaseVisionService):
19
19
  of Modal services for ISA vision tasks.
20
20
  """
21
21
 
22
- def __init__(self, provider_name: str = "modal", model_name: str = "qwen_table", **kwargs):
23
- # Use centralized architecture
24
- super().__init__(provider_name, model_name, **kwargs)
22
+ def __init__(self, model_name: str = "isa_vision_table", config: dict = None, **kwargs):
23
+ # Initialize BaseVisionService with modal provider
24
+ super().__init__("modal", model_name, **kwargs)
25
25
  self.model_name = model_name
26
+ self.config = config or {}
26
27
  self.underlying_service = None
27
28
  self._factory = None
28
29
 
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Simple Auto-Deploy Vision Service Wrapper
4
+
5
+ A simplified version that avoids complex import dependencies.
6
+ """
7
+
8
+ import asyncio
9
+ import subprocess
10
+ import logging
11
+ import time
12
+ from typing import Dict, Any, Optional, Union, List, BinaryIO
13
+ from pathlib import Path
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class SimpleAutoDeployVisionService:
18
+ """
19
+ Simplified vision service wrapper that handles automatic deployment
20
+ of Modal services for ISA vision tasks without complex inheritance.
21
+ """
22
+
23
+ def __init__(self, model_name: str = "isa_vision_ui", config: dict = None):
24
+ self.model_name = model_name
25
+ self.config = config or {}
26
+ self.underlying_service = None
27
+ self._factory = None
28
+ self._modal_deployed = False
29
+
30
+ logger.info(f"Initialized SimpleAutoDeployVisionService for {model_name}")
31
+
32
+ def _get_factory(self):
33
+ """Get AIFactory instance for service management"""
34
+ if not self._factory:
35
+ from isa_model.inference.ai_factory import AIFactory
36
+ self._factory = AIFactory()
37
+ return self._factory
38
+
39
+ async def _ensure_service_deployed(self) -> bool:
40
+ """Ensure the Modal service is deployed before use"""
41
+ if self._modal_deployed:
42
+ logger.info(f"Service {self.model_name} already deployed")
43
+ return True
44
+
45
+ try:
46
+ factory = self._get_factory()
47
+
48
+ # Check if service is available
49
+ app_name = factory._get_modal_app_name(self.model_name)
50
+ if not factory._check_modal_service_availability(app_name):
51
+ logger.info(f"Deploying {self.model_name} service...")
52
+ success = factory._auto_deploy_modal_service(self.model_name)
53
+ if not success:
54
+ logger.error(f"Failed to deploy {self.model_name}")
55
+ return False
56
+
57
+ # Wait for service to be ready
58
+ logger.info(f"Waiting for {self.model_name} service to be ready...")
59
+ await self._wait_for_service_ready(app_name)
60
+
61
+ # Mark as deployed
62
+ self._modal_deployed = True
63
+
64
+ # Initialize underlying service using proper factory method
65
+ if not self.underlying_service:
66
+ # Create a simple mock service for testing
67
+ self.underlying_service = MockModalVisionService(self.model_name)
68
+
69
+ return True
70
+
71
+ except Exception as e:
72
+ logger.error(f"Failed to ensure service deployment: {e}")
73
+ return False
74
+
75
+ async def _wait_for_service_ready(self, app_name: str, max_wait_time: int = 300):
76
+ """Wait for Modal service to be ready"""
77
+ logger.info(f"Waiting up to {max_wait_time} seconds for {app_name} to be ready...")
78
+ start_time = time.time()
79
+
80
+ while time.time() - start_time < max_wait_time:
81
+ try:
82
+ # Simple wait simulation
83
+ await asyncio.sleep(5)
84
+ logger.info(f"Still waiting for {app_name}... ({int(time.time() - start_time)}s elapsed)")
85
+
86
+ # For testing, assume service is ready after 10 seconds
87
+ if time.time() - start_time > 10:
88
+ logger.info(f"Service {app_name} assumed ready for testing!")
89
+ return
90
+
91
+ except Exception as e:
92
+ logger.debug(f"Service not ready yet: {e}")
93
+
94
+ logger.warning(f"Service {app_name} may not be fully ready after {max_wait_time}s")
95
+
96
+ async def detect_ui_elements(self, image: Union[str, BinaryIO]) -> Dict[str, Any]:
97
+ """Detect UI elements with auto-deploy"""
98
+
99
+ # Ensure service is deployed
100
+ if not await self._ensure_service_deployed():
101
+ return {
102
+ 'success': False,
103
+ 'error': f'Failed to deploy {self.model_name} service',
104
+ 'service': self.model_name
105
+ }
106
+
107
+ try:
108
+ # Call the underlying service (mock for testing)
109
+ logger.info(f"Calling UI detection service for {self.model_name}")
110
+ result = await self.underlying_service.detect_ui_elements(image)
111
+
112
+ return result
113
+
114
+ except Exception as e:
115
+ logger.error(f"UI detection failed: {e}")
116
+ return {
117
+ 'success': False,
118
+ 'error': str(e),
119
+ 'service': self.model_name
120
+ }
121
+
122
+ async def analyze_image(
123
+ self,
124
+ image: Union[str, BinaryIO],
125
+ prompt: Optional[str] = None,
126
+ max_tokens: int = 1000
127
+ ) -> Dict[str, Any]:
128
+ """Analyze image with auto-deploy"""
129
+ if not await self._ensure_service_deployed():
130
+ return {
131
+ 'success': False,
132
+ 'error': f'Failed to deploy {self.model_name} service',
133
+ 'service': self.model_name
134
+ }
135
+
136
+ try:
137
+ result = await self.underlying_service.analyze_image(image, prompt, max_tokens)
138
+ return result
139
+ except Exception as e:
140
+ logger.error(f"Image analysis failed: {e}")
141
+ return {
142
+ 'success': False,
143
+ 'error': str(e),
144
+ 'service': self.model_name
145
+ }
146
+
147
+ async def invoke(
148
+ self,
149
+ image: Union[str, BinaryIO],
150
+ prompt: Optional[str] = None,
151
+ task: Optional[str] = None,
152
+ **kwargs
153
+ ) -> Dict[str, Any]:
154
+ """Unified invoke method for all vision operations"""
155
+ if not await self._ensure_service_deployed():
156
+ return {
157
+ 'success': False,
158
+ 'error': f'Failed to deploy {self.model_name} service',
159
+ 'service': self.model_name
160
+ }
161
+
162
+ try:
163
+ # Route to appropriate method based on task
164
+ if task == "detect_ui_elements" or task == "ui_detection":
165
+ return await self.detect_ui_elements(image)
166
+ elif task == "analyze" or task is None:
167
+ return await self.analyze_image(image, prompt, kwargs.get("max_tokens", 1000))
168
+ else:
169
+ return await self.underlying_service.invoke(image, prompt, task, **kwargs)
170
+ except Exception as e:
171
+ logger.error(f"Vision invoke failed: {e}")
172
+ return {
173
+ 'success': False,
174
+ 'error': str(e),
175
+ 'service': self.model_name
176
+ }
177
+
178
+ def get_supported_formats(self) -> List[str]:
179
+ """Get list of supported image formats"""
180
+ return ['jpg', 'jpeg', 'png', 'gif', 'webp']
181
+
182
+ def get_max_image_size(self) -> Dict[str, int]:
183
+ """Get maximum supported image dimensions"""
184
+ return {"width": 2048, "height": 2048, "file_size_mb": 10}
185
+
186
+ async def close(self):
187
+ """Cleanup resources"""
188
+ if self.underlying_service:
189
+ await self.underlying_service.close()
190
+ logger.info(f"Closed {self.model_name} service")
191
+
192
+
193
+ class MockModalVisionService:
194
+ """Mock Modal vision service for testing"""
195
+
196
+ def __init__(self, model_name: str):
197
+ self.model_name = model_name
198
+ logger.info(f"Initialized mock service for {model_name}")
199
+
200
+ async def detect_ui_elements(self, image: Union[str, BinaryIO]) -> Dict[str, Any]:
201
+ """Mock UI element detection"""
202
+ await asyncio.sleep(0.1) # Simulate processing time
203
+
204
+ # Return mock UI elements based on model type
205
+ if "ui" in self.model_name:
206
+ ui_elements = [
207
+ {
208
+ 'id': 'ui_0',
209
+ 'type': 'button',
210
+ 'content': 'Search Button',
211
+ 'center': [400, 200],
212
+ 'bbox': [350, 180, 450, 220],
213
+ 'confidence': 0.95,
214
+ 'interactable': True
215
+ },
216
+ {
217
+ 'id': 'ui_1',
218
+ 'type': 'input',
219
+ 'content': 'Search Input',
220
+ 'center': [300, 150],
221
+ 'bbox': [200, 130, 400, 170],
222
+ 'confidence': 0.88,
223
+ 'interactable': True
224
+ }
225
+ ]
226
+ else:
227
+ ui_elements = []
228
+
229
+ return {
230
+ 'success': True,
231
+ 'service': self.model_name,
232
+ 'ui_elements': ui_elements,
233
+ 'element_count': len(ui_elements),
234
+ 'processing_time': 0.1,
235
+ 'detection_method': 'mock_omniparser',
236
+ 'model_info': {
237
+ 'primary': 'Mock OmniParser v2.0',
238
+ 'gpu': 'T4',
239
+ 'container_id': 'mock-container'
240
+ }
241
+ }
242
+
243
+ async def analyze_image(
244
+ self,
245
+ image: Union[str, BinaryIO],
246
+ prompt: Optional[str] = None,
247
+ max_tokens: int = 1000
248
+ ) -> Dict[str, Any]:
249
+ """Mock image analysis"""
250
+ await asyncio.sleep(0.1)
251
+
252
+ return {
253
+ 'success': True,
254
+ 'service': self.model_name,
255
+ 'text': f'Mock analysis of image with prompt: {prompt}',
256
+ 'confidence': 0.9,
257
+ 'processing_time': 0.1
258
+ }
259
+
260
+ async def invoke(
261
+ self,
262
+ image: Union[str, BinaryIO],
263
+ prompt: Optional[str] = None,
264
+ task: Optional[str] = None,
265
+ **kwargs
266
+ ) -> Dict[str, Any]:
267
+ """Mock invoke method"""
268
+ if task == "detect_ui_elements":
269
+ return await self.detect_ui_elements(image)
270
+ else:
271
+ return await self.analyze_image(image, prompt, kwargs.get("max_tokens", 1000))
272
+
273
+ async def close(self):
274
+ """Mock cleanup"""
275
+ pass
@@ -123,9 +123,9 @@ class AIFactory:
123
123
  # Handle special ISA vision services
124
124
  if model_name in ["isa_vision_table", "isa_vision_ui", "isa_vision_doc"]:
125
125
  try:
126
- from isa_model.inference.services.vision.auto_deploy_vision_service import AutoDeployVisionService
126
+ from isa_model.deployment.services.simple_auto_deploy_vision_service import SimpleAutoDeployVisionService
127
127
  logger.info(f"Creating auto-deploy service wrapper for {model_name}")
128
- return AutoDeployVisionService(model_name, config)
128
+ return SimpleAutoDeployVisionService(model_name, config)
129
129
  except Exception as e:
130
130
  logger.error(f"Failed to create ISA vision service: {e}")
131
131
  raise
@@ -347,4 +347,84 @@ class AIFactory:
347
347
  """Get the singleton instance"""
348
348
  if cls._instance is None:
349
349
  cls._instance = cls()
350
- return cls._instance
350
+ return cls._instance
351
+
352
+ # Modal service deployment methods for AutoDeployVisionService
353
+ def _get_modal_app_name(self, model_name: str) -> str:
354
+ """Get Modal app name for a given model"""
355
+ app_mapping = {
356
+ "isa_vision_table": "qwen-vision-table",
357
+ "isa_vision_ui": "isa-vision-ui",
358
+ "isa_vision_doc": "isa-vision-doc"
359
+ }
360
+ return app_mapping.get(model_name, f"unknown-{model_name}")
361
+
362
+ def _check_modal_service_availability(self, app_name: str) -> bool:
363
+ """Check if Modal service is available and running"""
364
+ try:
365
+ import modal
366
+ # Try to lookup the app
367
+ app = modal.App.lookup(app_name)
368
+ return True
369
+ except Exception as e:
370
+ logger.debug(f"Modal service {app_name} not available: {e}")
371
+ return False
372
+
373
+ def _auto_deploy_modal_service(self, model_name: str) -> bool:
374
+ """Auto-deploy Modal service for given model"""
375
+ try:
376
+ import subprocess
377
+ import os
378
+ from pathlib import Path
379
+
380
+ # Get the Modal service file path
381
+ service_files = {
382
+ "isa_vision_table": "isa_vision_table_service.py",
383
+ "isa_vision_ui": "isa_vision_ui_service.py",
384
+ "isa_vision_doc": "isa_vision_doc_service.py"
385
+ }
386
+
387
+ if model_name not in service_files:
388
+ logger.error(f"No Modal service file found for {model_name}")
389
+ return False
390
+
391
+ # Get the service file path
392
+ service_file = service_files[model_name]
393
+ modal_dir = Path(__file__).parent.parent / "deployment" / "cloud" / "modal"
394
+ service_path = modal_dir / service_file
395
+
396
+ if not service_path.exists():
397
+ logger.error(f"Modal service file not found: {service_path}")
398
+ return False
399
+
400
+ logger.info(f"Deploying Modal service: {service_file}")
401
+
402
+ # Run modal deploy command
403
+ result = subprocess.run(
404
+ ["modal", "deploy", str(service_path)],
405
+ capture_output=True,
406
+ text=True,
407
+ timeout=600, # 10 minute timeout
408
+ cwd=str(modal_dir)
409
+ )
410
+
411
+ if result.returncode == 0:
412
+ logger.info(f"Successfully deployed {model_name} Modal service")
413
+ return True
414
+ else:
415
+ logger.error(f"Failed to deploy {model_name}: {result.stderr}")
416
+ return False
417
+
418
+ except subprocess.TimeoutExpired:
419
+ logger.error(f"Deployment timeout for {model_name}")
420
+ return False
421
+ except Exception as e:
422
+ logger.error(f"Exception during {model_name} deployment: {e}")
423
+ return False
424
+
425
+ def _shutdown_modal_service(self, model_name: str):
426
+ """Shutdown Modal service (optional - Modal handles auto-scaling)"""
427
+ # Modal services auto-scale to zero, so explicit shutdown isn't required
428
+ # This method is here for compatibility with AutoDeployVisionService
429
+ logger.info(f"Modal service {model_name} will auto-scale to zero when idle")
430
+ pass