isa-model 0.3.5__py3-none-any.whl → 0.3.6__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 (87) hide show
  1. isa_model/__init__.py +30 -1
  2. isa_model/client.py +770 -0
  3. isa_model/core/config/__init__.py +16 -0
  4. isa_model/core/config/config_manager.py +514 -0
  5. isa_model/core/config.py +426 -0
  6. isa_model/core/models/model_billing_tracker.py +476 -0
  7. isa_model/core/models/model_manager.py +399 -0
  8. isa_model/core/{storage/supabase_storage.py → models/model_repo.py} +72 -73
  9. isa_model/core/pricing_manager.py +426 -0
  10. isa_model/core/services/__init__.py +19 -0
  11. isa_model/core/services/intelligent_model_selector.py +547 -0
  12. isa_model/core/types.py +291 -0
  13. isa_model/deployment/__init__.py +2 -0
  14. isa_model/deployment/cloud/modal/isa_vision_doc_service.py +157 -3
  15. isa_model/deployment/cloud/modal/isa_vision_table_service.py +532 -0
  16. isa_model/deployment/cloud/modal/isa_vision_ui_service.py +104 -3
  17. isa_model/deployment/cloud/modal/register_models.py +321 -0
  18. isa_model/deployment/runtime/deployed_service.py +338 -0
  19. isa_model/deployment/services/__init__.py +9 -0
  20. isa_model/deployment/services/auto_deploy_vision_service.py +537 -0
  21. isa_model/deployment/services/model_service.py +332 -0
  22. isa_model/deployment/services/service_monitor.py +356 -0
  23. isa_model/deployment/services/service_registry.py +527 -0
  24. isa_model/eval/__init__.py +80 -44
  25. isa_model/eval/config/__init__.py +10 -0
  26. isa_model/eval/config/evaluation_config.py +108 -0
  27. isa_model/eval/evaluators/__init__.py +18 -0
  28. isa_model/eval/evaluators/base_evaluator.py +503 -0
  29. isa_model/eval/evaluators/llm_evaluator.py +472 -0
  30. isa_model/eval/factory.py +417 -709
  31. isa_model/eval/infrastructure/__init__.py +24 -0
  32. isa_model/eval/infrastructure/experiment_tracker.py +466 -0
  33. isa_model/eval/metrics.py +191 -21
  34. isa_model/inference/ai_factory.py +181 -605
  35. isa_model/inference/services/audio/base_stt_service.py +65 -1
  36. isa_model/inference/services/audio/base_tts_service.py +75 -1
  37. isa_model/inference/services/audio/openai_stt_service.py +189 -151
  38. isa_model/inference/services/audio/openai_tts_service.py +12 -10
  39. isa_model/inference/services/audio/replicate_tts_service.py +61 -56
  40. isa_model/inference/services/base_service.py +55 -17
  41. isa_model/inference/services/embedding/base_embed_service.py +65 -1
  42. isa_model/inference/services/embedding/ollama_embed_service.py +103 -43
  43. isa_model/inference/services/embedding/openai_embed_service.py +8 -10
  44. isa_model/inference/services/helpers/stacked_config.py +148 -0
  45. isa_model/inference/services/img/__init__.py +18 -0
  46. isa_model/inference/services/{vision → img}/base_image_gen_service.py +80 -1
  47. isa_model/inference/services/{stacked → img}/flux_professional_service.py +25 -1
  48. isa_model/inference/services/{stacked → img/helpers}/base_stacked_service.py +40 -35
  49. isa_model/inference/services/{vision → img}/replicate_image_gen_service.py +44 -31
  50. isa_model/inference/services/llm/__init__.py +3 -3
  51. isa_model/inference/services/llm/base_llm_service.py +492 -40
  52. isa_model/inference/services/llm/helpers/llm_prompts.py +258 -0
  53. isa_model/inference/services/llm/helpers/llm_utils.py +280 -0
  54. isa_model/inference/services/llm/ollama_llm_service.py +51 -17
  55. isa_model/inference/services/llm/openai_llm_service.py +70 -19
  56. isa_model/inference/services/llm/yyds_llm_service.py +24 -23
  57. isa_model/inference/services/vision/__init__.py +38 -4
  58. isa_model/inference/services/vision/base_vision_service.py +218 -117
  59. isa_model/inference/services/vision/{isA_vision_service.py → disabled/isA_vision_service.py} +98 -0
  60. isa_model/inference/services/{stacked → vision}/doc_analysis_service.py +1 -1
  61. isa_model/inference/services/vision/helpers/base_stacked_service.py +274 -0
  62. isa_model/inference/services/vision/helpers/image_utils.py +272 -3
  63. isa_model/inference/services/vision/helpers/vision_prompts.py +297 -0
  64. isa_model/inference/services/vision/openai_vision_service.py +104 -307
  65. isa_model/inference/services/vision/replicate_vision_service.py +140 -325
  66. isa_model/inference/services/{stacked → vision}/ui_analysis_service.py +2 -498
  67. isa_model/scripts/register_models.py +370 -0
  68. isa_model/scripts/register_models_with_embeddings.py +510 -0
  69. isa_model/serving/api/fastapi_server.py +6 -1
  70. isa_model/serving/api/routes/unified.py +202 -0
  71. {isa_model-0.3.5.dist-info → isa_model-0.3.6.dist-info}/METADATA +4 -1
  72. {isa_model-0.3.5.dist-info → isa_model-0.3.6.dist-info}/RECORD +77 -53
  73. isa_model/config/__init__.py +0 -9
  74. isa_model/config/config_manager.py +0 -213
  75. isa_model/core/model_manager.py +0 -213
  76. isa_model/core/model_registry.py +0 -375
  77. isa_model/core/vision_models_init.py +0 -116
  78. isa_model/inference/billing_tracker.py +0 -406
  79. isa_model/inference/services/llm/triton_llm_service.py +0 -481
  80. isa_model/inference/services/stacked/__init__.py +0 -26
  81. isa_model/inference/services/stacked/config.py +0 -426
  82. isa_model/inference/services/vision/ollama_vision_service.py +0 -194
  83. /isa_model/core/{model_storage.py → models/model_storage.py} +0 -0
  84. /isa_model/inference/services/{vision → embedding}/helpers/text_splitter.py +0 -0
  85. /isa_model/inference/services/llm/{llm_adapter.py → helpers/llm_adapter.py} +0 -0
  86. {isa_model-0.3.5.dist-info → isa_model-0.3.6.dist-info}/WHEEL +0 -0
  87. {isa_model-0.3.5.dist-info → isa_model-0.3.6.dist-info}/top_level.txt +0 -0
isa_model/client.py ADDED
@@ -0,0 +1,770 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ ISA Model Client - Unified interface for all AI services
6
+ Provides intelligent model selection and simplified API
7
+ """
8
+
9
+ import logging
10
+ import asyncio
11
+ from typing import Any, Dict, Optional, List, Union
12
+ from pathlib import Path
13
+ import aiohttp
14
+
15
+ from isa_model.inference.ai_factory import AIFactory
16
+
17
+ try:
18
+ from isa_model.core.services.intelligent_model_selector import IntelligentModelSelector, get_model_selector
19
+ INTELLIGENT_SELECTOR_AVAILABLE = True
20
+ except ImportError:
21
+ IntelligentModelSelector = None
22
+ get_model_selector = None
23
+ INTELLIGENT_SELECTOR_AVAILABLE = False
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class ISAModelClient:
29
+ """
30
+ Unified ISA Model Client with intelligent model selection
31
+
32
+ Usage:
33
+ client = ISAModelClient()
34
+ response = await client.invoke("image.jpg", "analyze_image", "vision")
35
+ response = await client.invoke("Hello world", "generate_speech", "audio")
36
+ response = await client.invoke("audio.mp3", "transcribe", "audio")
37
+ """
38
+
39
+ def __init__(self,
40
+ config: Optional[Dict[str, Any]] = None,
41
+ mode: str = "local",
42
+ api_url: Optional[str] = None,
43
+ api_key: Optional[str] = None):
44
+ """Initialize ISA Model Client
45
+
46
+ Args:
47
+ config: Optional configuration override
48
+ mode: "local" for direct AI Factory, "api" for HTTP API calls
49
+ api_url: API base URL (required if mode="api")
50
+ api_key: API key for authentication (optional)
51
+ """
52
+ self.config = config or {}
53
+ self.mode = mode
54
+ self.api_url = api_url.rstrip('/') if api_url else None
55
+ self.api_key = api_key
56
+
57
+ # Setup HTTP headers for API mode
58
+ if self.mode == "api":
59
+ if not self.api_url:
60
+ raise ValueError("api_url is required when mode='api'")
61
+
62
+ self.headers = {
63
+ "Content-Type": "application/json",
64
+ "User-Agent": "ISA-Model-Client/1.0.0"
65
+ }
66
+ if self.api_key:
67
+ self.headers["Authorization"] = f"Bearer {self.api_key}"
68
+
69
+ # Initialize AI Factory for local mode
70
+ if self.mode == "local":
71
+ self.ai_factory = AIFactory.get_instance()
72
+ else:
73
+ self.ai_factory = None
74
+
75
+ # Initialize intelligent model selector
76
+ self.model_selector = None
77
+ if INTELLIGENT_SELECTOR_AVAILABLE:
78
+ try:
79
+ # Initialize asynchronously later
80
+ self._model_selector_task = None
81
+ logger.info("Intelligent model selector will be initialized on first use")
82
+ except Exception as e:
83
+ logger.warning(f"Failed to setup model selector: {e}")
84
+ else:
85
+ logger.info("Intelligent model selector not available, using default selection")
86
+
87
+ # Cache for frequently used services
88
+ self._service_cache: Dict[str, Any] = {}
89
+
90
+ logger.info("ISA Model Client initialized")
91
+
92
+ async def invoke(
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
+ ) -> Dict[str, Any]:
101
+ """
102
+ Unified invoke method with intelligent model selection
103
+
104
+ Args:
105
+ input_data: Input data (image path, text, audio, etc.)
106
+ task: Task to perform (analyze_image, generate_speech, transcribe, etc.)
107
+ service_type: Type of service (vision, audio, text, image, embedding)
108
+ model_hint: Optional model preference
109
+ provider_hint: Optional provider preference
110
+ **kwargs: Additional task-specific parameters
111
+
112
+ Returns:
113
+ Unified response dictionary with result and metadata
114
+
115
+ Examples:
116
+ # Vision tasks
117
+ await client.invoke("image.jpg", "analyze_image", "vision")
118
+ await client.invoke("screenshot.png", "detect_ui_elements", "vision")
119
+ await client.invoke("document.pdf", "extract_table", "vision")
120
+
121
+ # Audio tasks
122
+ await client.invoke("Hello world", "generate_speech", "audio")
123
+ await client.invoke("audio.mp3", "transcribe", "audio")
124
+
125
+ # Text tasks
126
+ await client.invoke("Translate this text", "translate", "text")
127
+ await client.invoke("What is AI?", "chat", "text")
128
+
129
+ # Image generation
130
+ await client.invoke("A beautiful sunset", "generate_image", "image")
131
+
132
+ # Embedding
133
+ await client.invoke("Text to embed", "create_embedding", "embedding")
134
+ """
135
+ try:
136
+ # Route to appropriate mode
137
+ if self.mode == "api":
138
+ return await self._invoke_api(
139
+ input_data=input_data,
140
+ task=task,
141
+ service_type=service_type,
142
+ model_hint=model_hint,
143
+ provider_hint=provider_hint,
144
+ **kwargs
145
+ )
146
+ else:
147
+ return await self._invoke_local(
148
+ input_data=input_data,
149
+ task=task,
150
+ service_type=service_type,
151
+ model_hint=model_hint,
152
+ provider_hint=provider_hint,
153
+ **kwargs
154
+ )
155
+
156
+ except Exception as e:
157
+ logger.error(f"Failed to invoke {task} on {service_type}: {e}")
158
+ return {
159
+ "success": False,
160
+ "error": str(e),
161
+ "metadata": {
162
+ "task": task,
163
+ "service_type": service_type,
164
+ "input_type": type(input_data).__name__
165
+ }
166
+ }
167
+
168
+ async def _select_model(
169
+ self,
170
+ input_data: Any,
171
+ task: str,
172
+ service_type: str,
173
+ model_hint: Optional[str] = None,
174
+ provider_hint: Optional[str] = None
175
+ ) -> Dict[str, Any]:
176
+ """Select the best model for the given task"""
177
+
178
+ # If explicit hints provided, use them
179
+ if model_hint and provider_hint:
180
+ return {
181
+ "model_id": model_hint,
182
+ "provider": provider_hint,
183
+ "reason": "User specified"
184
+ }
185
+
186
+ # Use intelligent model selector if available
187
+ if INTELLIGENT_SELECTOR_AVAILABLE:
188
+ try:
189
+ # Initialize model selector if not already done
190
+ if self.model_selector is None:
191
+ self.model_selector = await get_model_selector(self.config)
192
+
193
+ # Create selection request
194
+ request = f"{task} for {service_type}"
195
+ if isinstance(input_data, (str, Path)):
196
+ request += f" with input: {str(input_data)[:100]}"
197
+
198
+ selection = await self.model_selector.select_model(
199
+ request=request,
200
+ service_type=service_type,
201
+ context={
202
+ "task": task,
203
+ "input_type": type(input_data).__name__,
204
+ "provider_hint": provider_hint,
205
+ "model_hint": model_hint
206
+ }
207
+ )
208
+
209
+ if selection["success"]:
210
+ return {
211
+ "model_id": selection["selected_model"]["model_id"],
212
+ "provider": selection["selected_model"]["provider"],
213
+ "reason": selection["selection_reason"]
214
+ }
215
+
216
+ except Exception as e:
217
+ logger.warning(f"Intelligent selection failed: {e}, using defaults")
218
+
219
+ # Fallback to default model selection
220
+ return self._get_default_model(service_type, task, provider_hint)
221
+
222
+ def _get_default_model(
223
+ self,
224
+ service_type: str,
225
+ task: str,
226
+ provider_hint: Optional[str] = None
227
+ ) -> Dict[str, Any]:
228
+ """Get default model for service type and task"""
229
+
230
+ defaults = {
231
+ "vision": {
232
+ "model_id": "gpt-4o-mini",
233
+ "provider": "openai"
234
+ },
235
+ "audio": {
236
+ "tts": {"model_id": "tts-1", "provider": "openai"},
237
+ "stt": {"model_id": "whisper-1", "provider": "openai"},
238
+ "default": {"model_id": "whisper-1", "provider": "openai"}
239
+ },
240
+ "text": {
241
+ "model_id": "gpt-4.1-mini",
242
+ "provider": "openai"
243
+ },
244
+ "image": {
245
+ "model_id": "black-forest-labs/flux-schnell",
246
+ "provider": "replicate"
247
+ },
248
+ "embedding": {
249
+ "model_id": "text-embedding-3-small",
250
+ "provider": "openai"
251
+ }
252
+ }
253
+
254
+ # Handle audio service type with task-specific models
255
+ if service_type == "audio":
256
+ if "speech" in task or "tts" in task:
257
+ default = defaults["audio"]["tts"]
258
+ elif "transcribe" in task or "stt" in task:
259
+ default = defaults["audio"]["stt"]
260
+ else:
261
+ default = defaults["audio"]["default"]
262
+ else:
263
+ default = defaults.get(service_type, defaults["vision"])
264
+
265
+ # Apply provider hint if provided
266
+ if provider_hint:
267
+ default = dict(default)
268
+ default["provider"] = provider_hint
269
+
270
+ return {
271
+ **default,
272
+ "reason": "Default selection"
273
+ }
274
+
275
+ async def _get_service(
276
+ self,
277
+ service_type: str,
278
+ model_name: str,
279
+ provider: str,
280
+ task: str
281
+ ) -> Any:
282
+ """Get appropriate service instance"""
283
+
284
+ cache_key = f"{service_type}_{provider}_{model_name}"
285
+
286
+ # Check cache first
287
+ if cache_key in self._service_cache:
288
+ return self._service_cache[cache_key]
289
+
290
+ try:
291
+ # Route to appropriate AIFactory method
292
+ if service_type == "vision":
293
+ service = self.ai_factory.get_vision(model_name, provider)
294
+
295
+ elif service_type == "audio":
296
+ if "speech" in task or "tts" in task:
297
+ service = self.ai_factory.get_tts(model_name, provider)
298
+ elif "transcribe" in task or "stt" in task:
299
+ service = self.ai_factory.get_stt(model_name, provider)
300
+ else:
301
+ # Default to STT for unknown audio tasks
302
+ service = self.ai_factory.get_stt(model_name, provider)
303
+
304
+ elif service_type == "text":
305
+ service = self.ai_factory.get_llm(model_name, provider)
306
+
307
+ elif service_type == "image":
308
+ service = self.ai_factory.get_img("t2i", model_name, provider)
309
+
310
+ elif service_type == "embedding":
311
+ service = self.ai_factory.get_embed(model_name, provider)
312
+
313
+ else:
314
+ raise ValueError(f"Unsupported service type: {service_type}")
315
+
316
+ # Cache the service
317
+ self._service_cache[cache_key] = service
318
+ return service
319
+
320
+ except Exception as e:
321
+ logger.error(f"Failed to get service {service_type}/{provider}/{model_name}: {e}")
322
+ raise
323
+
324
+ async def _execute_task(
325
+ self,
326
+ service: Any,
327
+ input_data: Any,
328
+ task: str,
329
+ service_type: str,
330
+ **kwargs
331
+ ) -> Any:
332
+ """Execute the task using the appropriate service"""
333
+
334
+ try:
335
+ if service_type == "vision":
336
+ return await self._execute_vision_task(service, input_data, task, **kwargs)
337
+
338
+ elif service_type == "audio":
339
+ return await self._execute_audio_task(service, input_data, task, **kwargs)
340
+
341
+ elif service_type == "text":
342
+ return await self._execute_text_task(service, input_data, task, **kwargs)
343
+
344
+ elif service_type == "image":
345
+ return await self._execute_image_task(service, input_data, task, **kwargs)
346
+
347
+ elif service_type == "embedding":
348
+ return await self._execute_embedding_task(service, input_data, task, **kwargs)
349
+
350
+ else:
351
+ raise ValueError(f"Unsupported service type: {service_type}")
352
+
353
+ except Exception as e:
354
+ logger.error(f"Task execution failed: {e}")
355
+ raise
356
+
357
+ async def _execute_vision_task(self, service, input_data, task, **kwargs):
358
+ """Execute vision-related tasks using unified invoke method"""
359
+
360
+ # Map common task names to unified task names
361
+ task_mapping = {
362
+ "analyze_image": "analyze_image",
363
+ "detect_ui_elements": "detect_ui",
364
+ "extract_table": "extract_table",
365
+ "extract_text": "extract_text",
366
+ "ocr": "extract_text",
367
+ "describe": "analyze_image"
368
+ }
369
+
370
+ unified_task = task_mapping.get(task, task)
371
+
372
+ # Use unified invoke method with proper parameters
373
+ return await service.invoke(
374
+ image=input_data,
375
+ task=unified_task,
376
+ **kwargs
377
+ )
378
+
379
+ async def _execute_audio_task(self, service, input_data, task, **kwargs):
380
+ """Execute audio-related tasks using unified invoke method"""
381
+
382
+ # Map common task names to unified task names
383
+ task_mapping = {
384
+ "generate_speech": "synthesize",
385
+ "text_to_speech": "synthesize",
386
+ "tts": "synthesize",
387
+ "transcribe": "transcribe",
388
+ "speech_to_text": "transcribe",
389
+ "stt": "transcribe",
390
+ "translate": "translate",
391
+ "detect_language": "detect_language"
392
+ }
393
+
394
+ unified_task = task_mapping.get(task, task)
395
+
396
+ # Use unified invoke method with correct parameter name based on task type
397
+ if unified_task in ["synthesize", "text_to_speech", "tts"]:
398
+ # TTS services expect 'text' parameter
399
+ return await service.invoke(
400
+ text=input_data,
401
+ task=unified_task,
402
+ **kwargs
403
+ )
404
+ else:
405
+ # STT services expect 'audio_input' parameter
406
+ return await service.invoke(
407
+ audio_input=input_data,
408
+ task=unified_task,
409
+ **kwargs
410
+ )
411
+
412
+ async def _execute_text_task(self, service, input_data, task, **kwargs):
413
+ """Execute text-related tasks using unified invoke method"""
414
+
415
+ # Map common task names to unified task names
416
+ task_mapping = {
417
+ "chat": "chat",
418
+ "generate": "generate",
419
+ "complete": "complete",
420
+ "translate": "translate",
421
+ "summarize": "summarize",
422
+ "analyze": "analyze",
423
+ "extract": "extract",
424
+ "classify": "classify"
425
+ }
426
+
427
+ unified_task = task_mapping.get(task, task)
428
+
429
+ # Use unified invoke method
430
+ return await service.invoke(
431
+ input_data=input_data,
432
+ task=unified_task,
433
+ **kwargs
434
+ )
435
+
436
+ async def _execute_image_task(self, service, input_data, task, **kwargs):
437
+ """Execute image generation tasks using unified invoke method"""
438
+
439
+ # Map common task names to unified task names
440
+ task_mapping = {
441
+ "generate_image": "generate",
442
+ "generate": "generate",
443
+ "img2img": "img2img",
444
+ "image_to_image": "img2img",
445
+ "generate_batch": "generate_batch"
446
+ }
447
+
448
+ unified_task = task_mapping.get(task, task)
449
+
450
+ # Use unified invoke method
451
+ return await service.invoke(
452
+ prompt=input_data,
453
+ task=unified_task,
454
+ **kwargs
455
+ )
456
+
457
+ async def _execute_embedding_task(self, service, input_data, task, **kwargs):
458
+ """Execute embedding tasks using unified invoke method"""
459
+
460
+ # Map common task names to unified task names
461
+ task_mapping = {
462
+ "create_embedding": "embed",
463
+ "embed": "embed",
464
+ "embed_batch": "embed_batch",
465
+ "chunk_and_embed": "chunk_and_embed",
466
+ "similarity": "similarity",
467
+ "find_similar": "find_similar"
468
+ }
469
+
470
+ unified_task = task_mapping.get(task, task)
471
+
472
+ # Use unified invoke method
473
+ return await service.invoke(
474
+ input_data=input_data,
475
+ task=unified_task,
476
+ **kwargs
477
+ )
478
+
479
+ def clear_cache(self):
480
+ """Clear service cache"""
481
+ self._service_cache.clear()
482
+ logger.info("Service cache cleared")
483
+
484
+ async def get_available_models(self, service_type: Optional[str] = None) -> List[Dict[str, Any]]:
485
+ """Get list of available models
486
+
487
+ Args:
488
+ service_type: Optional filter by service type
489
+
490
+ Returns:
491
+ List of available models with metadata
492
+ """
493
+ if INTELLIGENT_SELECTOR_AVAILABLE:
494
+ try:
495
+ if self.model_selector is None:
496
+ self.model_selector = await get_model_selector(self.config)
497
+ return await self.model_selector.get_available_models(service_type)
498
+ except Exception as e:
499
+ logger.error(f"Failed to get available models: {e}")
500
+ return []
501
+ else:
502
+ return []
503
+
504
+ async def health_check(self) -> Dict[str, Any]:
505
+ """Check health of client and underlying services
506
+
507
+ Returns:
508
+ Health status dictionary
509
+ """
510
+ try:
511
+ health_status = {
512
+ "client": "healthy",
513
+ "ai_factory": "healthy" if self.ai_factory else "unavailable",
514
+ "model_selector": "healthy" if self.model_selector else "unavailable",
515
+ "services": {}
516
+ }
517
+
518
+ # Check a few key services
519
+ test_services = [
520
+ ("vision", "openai", "gpt-4.1-mini"),
521
+ ("audio", "openai", "whisper-1"),
522
+ ("text", "openai", "gpt-4.1-mini")
523
+ ]
524
+
525
+ for service_type, provider, model in test_services:
526
+ try:
527
+ await self._get_service(service_type, model, provider, "test")
528
+ health_status["services"][f"{service_type}_{provider}"] = "healthy"
529
+ except Exception as e:
530
+ health_status["services"][f"{service_type}_{provider}"] = f"error: {str(e)}"
531
+
532
+ return health_status
533
+
534
+ except Exception as e:
535
+ return {
536
+ "client": "error",
537
+ "error": str(e)
538
+ }
539
+
540
+ async def _invoke_local(
541
+ self,
542
+ input_data: Union[str, bytes, Path, Dict[str, Any]],
543
+ task: str,
544
+ service_type: str,
545
+ model_hint: Optional[str] = None,
546
+ provider_hint: Optional[str] = None,
547
+ **kwargs
548
+ ) -> Dict[str, Any]:
549
+ """Local invoke using AI Factory (original logic)"""
550
+ try:
551
+ # Step 1: Select best model for this task
552
+ selected_model = await self._select_model(
553
+ input_data=input_data,
554
+ task=task,
555
+ service_type=service_type,
556
+ model_hint=model_hint,
557
+ provider_hint=provider_hint
558
+ )
559
+
560
+ # Step 2: Get appropriate service
561
+ service = await self._get_service(
562
+ service_type=service_type,
563
+ model_name=selected_model["model_id"],
564
+ provider=selected_model["provider"],
565
+ task=task
566
+ )
567
+
568
+ # Step 3: Execute task with unified interface
569
+ result = await self._execute_task(
570
+ service=service,
571
+ input_data=input_data,
572
+ task=task,
573
+ service_type=service_type,
574
+ **kwargs
575
+ )
576
+
577
+ # Step 4: Return unified response
578
+ return {
579
+ "success": True,
580
+ "result": result,
581
+ "metadata": {
582
+ "model_used": selected_model["model_id"],
583
+ "provider": selected_model["provider"],
584
+ "task": task,
585
+ "service_type": service_type,
586
+ "selection_reason": selected_model.get("reason", "Default selection")
587
+ }
588
+ }
589
+ except Exception as e:
590
+ logger.error(f"Local invoke failed: {e}")
591
+ raise
592
+
593
+ async def _invoke_api(
594
+ self,
595
+ input_data: Union[str, bytes, Path, Dict[str, Any]],
596
+ task: str,
597
+ service_type: str,
598
+ model_hint: Optional[str] = None,
599
+ provider_hint: Optional[str] = None,
600
+ **kwargs
601
+ ) -> Dict[str, Any]:
602
+ """API invoke using HTTP requests"""
603
+
604
+ # Handle file inputs
605
+ if isinstance(input_data, Path):
606
+ return await self._invoke_api_file(
607
+ file_path=input_data,
608
+ task=task,
609
+ service_type=service_type,
610
+ model_hint=model_hint,
611
+ provider_hint=provider_hint,
612
+ **kwargs
613
+ )
614
+
615
+ # Handle binary data
616
+ if isinstance(input_data, bytes):
617
+ return await self._invoke_api_binary(
618
+ data=input_data,
619
+ task=task,
620
+ service_type=service_type,
621
+ model_hint=model_hint,
622
+ provider_hint=provider_hint,
623
+ **kwargs
624
+ )
625
+
626
+ # Handle text/JSON data
627
+ payload = {
628
+ "input_data": input_data,
629
+ "task": task,
630
+ "service_type": service_type,
631
+ "model_hint": model_hint,
632
+ "provider_hint": provider_hint,
633
+ "parameters": kwargs
634
+ }
635
+
636
+ async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=300)) as session:
637
+ try:
638
+ async with session.post(
639
+ f"{self.api_url}/api/v1/invoke",
640
+ json=payload,
641
+ headers=self.headers
642
+ ) as response:
643
+
644
+ if response.status == 200:
645
+ return await response.json()
646
+ else:
647
+ error_data = await response.text()
648
+ raise Exception(f"API error {response.status}: {error_data}")
649
+
650
+ except Exception as e:
651
+ logger.error(f"API invoke failed: {e}")
652
+ raise
653
+
654
+ async def _invoke_api_file(
655
+ self,
656
+ file_path: Path,
657
+ task: str,
658
+ service_type: str,
659
+ model_hint: Optional[str] = None,
660
+ provider_hint: Optional[str] = None,
661
+ **kwargs
662
+ ) -> Dict[str, Any]:
663
+ """API file upload"""
664
+
665
+ if not file_path.exists():
666
+ raise FileNotFoundError(f"File not found: {file_path}")
667
+
668
+ data = aiohttp.FormData()
669
+ data.add_field('task', task)
670
+ data.add_field('service_type', service_type)
671
+
672
+ if model_hint:
673
+ data.add_field('model_hint', model_hint)
674
+ if provider_hint:
675
+ data.add_field('provider_hint', provider_hint)
676
+
677
+ data.add_field('file',
678
+ open(file_path, 'rb'),
679
+ filename=file_path.name,
680
+ content_type='application/octet-stream')
681
+
682
+ headers = {k: v for k, v in self.headers.items() if k != "Content-Type"}
683
+
684
+ async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=300)) as session:
685
+ try:
686
+ async with session.post(
687
+ f"{self.api_url}/api/v1/invoke-file",
688
+ data=data,
689
+ headers=headers
690
+ ) as response:
691
+
692
+ if response.status == 200:
693
+ return await response.json()
694
+ else:
695
+ error_data = await response.text()
696
+ raise Exception(f"API error {response.status}: {error_data}")
697
+
698
+ except Exception as e:
699
+ logger.error(f"API file upload failed: {e}")
700
+ raise
701
+
702
+ async def _invoke_api_binary(
703
+ self,
704
+ data: bytes,
705
+ task: str,
706
+ service_type: str,
707
+ model_hint: Optional[str] = None,
708
+ provider_hint: Optional[str] = None,
709
+ **kwargs
710
+ ) -> Dict[str, Any]:
711
+ """API binary upload"""
712
+
713
+ form_data = aiohttp.FormData()
714
+ form_data.add_field('task', task)
715
+ form_data.add_field('service_type', service_type)
716
+
717
+ if model_hint:
718
+ form_data.add_field('model_hint', model_hint)
719
+ if provider_hint:
720
+ form_data.add_field('provider_hint', provider_hint)
721
+
722
+ form_data.add_field('file',
723
+ data,
724
+ filename='data.bin',
725
+ content_type='application/octet-stream')
726
+
727
+ headers = {k: v for k, v in self.headers.items() if k != "Content-Type"}
728
+
729
+ async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=300)) as session:
730
+ try:
731
+ async with session.post(
732
+ f"{self.api_url}/api/v1/invoke-file",
733
+ data=form_data,
734
+ headers=headers
735
+ ) as response:
736
+
737
+ if response.status == 200:
738
+ return await response.json()
739
+ else:
740
+ error_data = await response.text()
741
+ raise Exception(f"API error {response.status}: {error_data}")
742
+
743
+ except Exception as e:
744
+ logger.error(f"API binary upload failed: {e}")
745
+ raise
746
+
747
+
748
+ # Convenience function for quick access
749
+ def create_client(
750
+ config: Optional[Dict[str, Any]] = None,
751
+ mode: str = "local",
752
+ api_url: Optional[str] = None,
753
+ api_key: Optional[str] = None
754
+ ) -> ISAModelClient:
755
+ """Create ISA Model Client instance
756
+
757
+ Args:
758
+ config: Optional configuration
759
+ mode: "local" for direct AI Factory, "api" for HTTP API calls
760
+ api_url: API base URL (required if mode="api")
761
+ api_key: API key for authentication (optional)
762
+
763
+ Returns:
764
+ ISAModelClient instance
765
+ """
766
+ return ISAModelClient(config=config, mode=mode, api_url=api_url, api_key=api_key)
767
+
768
+
769
+ # Export for easy import
770
+ __all__ = ["ISAModelClient", "create_client"]