isa-model 0.3.5__py3-none-any.whl → 0.3.7__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 (88) hide show
  1. isa_model/__init__.py +30 -1
  2. isa_model/client.py +937 -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 +538 -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/deployment/services/simple_auto_deploy_vision_service.py +275 -0
  25. isa_model/eval/__init__.py +80 -44
  26. isa_model/eval/config/__init__.py +10 -0
  27. isa_model/eval/config/evaluation_config.py +108 -0
  28. isa_model/eval/evaluators/__init__.py +18 -0
  29. isa_model/eval/evaluators/base_evaluator.py +503 -0
  30. isa_model/eval/evaluators/llm_evaluator.py +472 -0
  31. isa_model/eval/factory.py +417 -709
  32. isa_model/eval/infrastructure/__init__.py +24 -0
  33. isa_model/eval/infrastructure/experiment_tracker.py +466 -0
  34. isa_model/eval/metrics.py +191 -21
  35. isa_model/inference/ai_factory.py +257 -601
  36. isa_model/inference/services/audio/base_stt_service.py +65 -1
  37. isa_model/inference/services/audio/base_tts_service.py +75 -1
  38. isa_model/inference/services/audio/openai_stt_service.py +189 -151
  39. isa_model/inference/services/audio/openai_tts_service.py +12 -10
  40. isa_model/inference/services/audio/replicate_tts_service.py +61 -56
  41. isa_model/inference/services/base_service.py +55 -17
  42. isa_model/inference/services/embedding/base_embed_service.py +65 -1
  43. isa_model/inference/services/embedding/ollama_embed_service.py +103 -43
  44. isa_model/inference/services/embedding/openai_embed_service.py +8 -10
  45. isa_model/inference/services/helpers/stacked_config.py +148 -0
  46. isa_model/inference/services/img/__init__.py +18 -0
  47. isa_model/inference/services/{vision → img}/base_image_gen_service.py +80 -1
  48. isa_model/inference/services/{stacked → img}/flux_professional_service.py +25 -1
  49. isa_model/inference/services/{stacked → img/helpers}/base_stacked_service.py +40 -35
  50. isa_model/inference/services/{vision → img}/replicate_image_gen_service.py +44 -31
  51. isa_model/inference/services/llm/__init__.py +3 -3
  52. isa_model/inference/services/llm/base_llm_service.py +492 -40
  53. isa_model/inference/services/llm/helpers/llm_prompts.py +258 -0
  54. isa_model/inference/services/llm/helpers/llm_utils.py +280 -0
  55. isa_model/inference/services/llm/ollama_llm_service.py +51 -17
  56. isa_model/inference/services/llm/openai_llm_service.py +70 -19
  57. isa_model/inference/services/llm/yyds_llm_service.py +24 -23
  58. isa_model/inference/services/vision/__init__.py +38 -4
  59. isa_model/inference/services/vision/base_vision_service.py +218 -117
  60. isa_model/inference/services/vision/{isA_vision_service.py → disabled/isA_vision_service.py} +98 -0
  61. isa_model/inference/services/{stacked → vision}/doc_analysis_service.py +1 -1
  62. isa_model/inference/services/vision/helpers/base_stacked_service.py +274 -0
  63. isa_model/inference/services/vision/helpers/image_utils.py +272 -3
  64. isa_model/inference/services/vision/helpers/vision_prompts.py +297 -0
  65. isa_model/inference/services/vision/openai_vision_service.py +104 -307
  66. isa_model/inference/services/vision/replicate_vision_service.py +140 -325
  67. isa_model/inference/services/{stacked → vision}/ui_analysis_service.py +2 -498
  68. isa_model/scripts/register_models.py +370 -0
  69. isa_model/scripts/register_models_with_embeddings.py +510 -0
  70. isa_model/serving/api/fastapi_server.py +6 -1
  71. isa_model/serving/api/routes/unified.py +274 -0
  72. {isa_model-0.3.5.dist-info → isa_model-0.3.7.dist-info}/METADATA +4 -1
  73. {isa_model-0.3.5.dist-info → isa_model-0.3.7.dist-info}/RECORD +78 -53
  74. isa_model/config/__init__.py +0 -9
  75. isa_model/config/config_manager.py +0 -213
  76. isa_model/core/model_manager.py +0 -213
  77. isa_model/core/model_registry.py +0 -375
  78. isa_model/core/vision_models_init.py +0 -116
  79. isa_model/inference/billing_tracker.py +0 -406
  80. isa_model/inference/services/llm/triton_llm_service.py +0 -481
  81. isa_model/inference/services/stacked/__init__.py +0 -26
  82. isa_model/inference/services/stacked/config.py +0 -426
  83. isa_model/inference/services/vision/ollama_vision_service.py +0 -194
  84. /isa_model/core/{model_storage.py → models/model_storage.py} +0 -0
  85. /isa_model/inference/services/{vision → embedding}/helpers/text_splitter.py +0 -0
  86. /isa_model/inference/services/llm/{llm_adapter.py → helpers/llm_adapter.py} +0 -0
  87. {isa_model-0.3.5.dist-info → isa_model-0.3.7.dist-info}/WHEEL +0 -0
  88. {isa_model-0.3.5.dist-info → isa_model-0.3.7.dist-info}/top_level.txt +0 -0
isa_model/client.py ADDED
@@ -0,0 +1,937 @@
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 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
+
133
+ async def invoke(
134
+ self,
135
+ input_data: Union[str, bytes, Path, Dict[str, Any]],
136
+ task: str,
137
+ service_type: str,
138
+ model_hint: Optional[str] = None,
139
+ provider_hint: Optional[str] = None,
140
+ stream: bool = False,
141
+ **kwargs
142
+ ) -> Union[Dict[str, Any], object]:
143
+ """
144
+ Unified invoke method with intelligent model selection
145
+
146
+ Args:
147
+ input_data: Input data (image path, text, audio, etc.)
148
+ task: Task to perform (analyze_image, generate_speech, transcribe, etc.)
149
+ service_type: Type of service (vision, audio, text, image, embedding)
150
+ model_hint: Optional model preference
151
+ provider_hint: Optional provider preference
152
+ stream: Enable streaming for text services (returns AsyncGenerator)
153
+ **kwargs: Additional task-specific parameters
154
+
155
+ Returns:
156
+ If stream=False: Unified response dictionary with result and metadata
157
+ If stream=True: AsyncGenerator yielding tokens (only for text services)
158
+
159
+ Examples:
160
+ # Vision tasks
161
+ await client.invoke("image.jpg", "analyze_image", "vision")
162
+ await client.invoke("screenshot.png", "detect_ui_elements", "vision")
163
+ await client.invoke("document.pdf", "extract_table", "vision")
164
+
165
+ # Audio tasks
166
+ await client.invoke("Hello world", "generate_speech", "audio")
167
+ await client.invoke("audio.mp3", "transcribe", "audio")
168
+
169
+ # Text tasks
170
+ await client.invoke("Translate this text", "translate", "text")
171
+ await client.invoke("What is AI?", "chat", "text")
172
+
173
+ # Streaming text
174
+ async for token in await client.invoke("Hello", "chat", "text", stream=True):
175
+ print(token, end="", flush=True)
176
+
177
+ # Image generation
178
+ await client.invoke("A beautiful sunset", "generate_image", "image")
179
+
180
+ # Embedding
181
+ await client.invoke("Text to embed", "create_embedding", "embedding")
182
+ """
183
+ try:
184
+ # Handle streaming case
185
+ if stream:
186
+ if service_type != "text":
187
+ raise ValueError("Streaming is only supported for text services")
188
+
189
+ if self.mode == "api":
190
+ return self._stream_api(
191
+ input_data=input_data,
192
+ task=task,
193
+ service_type=service_type,
194
+ model_hint=model_hint,
195
+ provider_hint=provider_hint,
196
+ **kwargs
197
+ )
198
+ else:
199
+ return self._stream_local(
200
+ input_data=input_data,
201
+ task=task,
202
+ service_type=service_type,
203
+ model_hint=model_hint,
204
+ provider_hint=provider_hint,
205
+ **kwargs
206
+ )
207
+
208
+ # Route to appropriate mode for non-streaming
209
+ if self.mode == "api":
210
+ return await self._invoke_api(
211
+ input_data=input_data,
212
+ task=task,
213
+ service_type=service_type,
214
+ model_hint=model_hint,
215
+ provider_hint=provider_hint,
216
+ **kwargs
217
+ )
218
+ else:
219
+ return await self._invoke_local(
220
+ input_data=input_data,
221
+ task=task,
222
+ service_type=service_type,
223
+ model_hint=model_hint,
224
+ provider_hint=provider_hint,
225
+ **kwargs
226
+ )
227
+
228
+ except Exception as e:
229
+ logger.error(f"Failed to invoke {task} on {service_type}: {e}")
230
+ return {
231
+ "success": False,
232
+ "error": str(e),
233
+ "metadata": {
234
+ "task": task,
235
+ "service_type": service_type,
236
+ "input_type": type(input_data).__name__
237
+ }
238
+ }
239
+
240
+ async def _select_model(
241
+ self,
242
+ input_data: Any,
243
+ task: str,
244
+ service_type: str,
245
+ model_hint: Optional[str] = None,
246
+ provider_hint: Optional[str] = None
247
+ ) -> Dict[str, Any]:
248
+ """Select the best model for the given task"""
249
+
250
+ # If explicit hints provided, use them
251
+ if model_hint and provider_hint:
252
+ return {
253
+ "model_id": model_hint,
254
+ "provider": provider_hint,
255
+ "reason": "User specified"
256
+ }
257
+
258
+ # Use intelligent model selector if available
259
+ if INTELLIGENT_SELECTOR_AVAILABLE:
260
+ try:
261
+ # Initialize model selector if not already done
262
+ if self.model_selector is None:
263
+ self.model_selector = await get_model_selector(self.config)
264
+
265
+ # Create selection request
266
+ request = f"{task} for {service_type}"
267
+ if isinstance(input_data, (str, Path)):
268
+ request += f" with input: {str(input_data)[:100]}"
269
+
270
+ selection = await self.model_selector.select_model(
271
+ request=request,
272
+ service_type=service_type,
273
+ context={
274
+ "task": task,
275
+ "input_type": type(input_data).__name__,
276
+ "provider_hint": provider_hint,
277
+ "model_hint": model_hint
278
+ }
279
+ )
280
+
281
+ if selection["success"]:
282
+ return {
283
+ "model_id": selection["selected_model"]["model_id"],
284
+ "provider": selection["selected_model"]["provider"],
285
+ "reason": selection["selection_reason"]
286
+ }
287
+
288
+ except Exception as e:
289
+ logger.warning(f"Intelligent selection failed: {e}, using defaults")
290
+
291
+ # Fallback to default model selection
292
+ return self._get_default_model(service_type, task, provider_hint)
293
+
294
+ def _get_default_model(
295
+ self,
296
+ service_type: str,
297
+ task: str,
298
+ provider_hint: Optional[str] = None
299
+ ) -> Dict[str, Any]:
300
+ """Get default model for service type and task"""
301
+
302
+ defaults = {
303
+ "vision": {
304
+ "model_id": "gpt-4o-mini",
305
+ "provider": "openai"
306
+ },
307
+ "audio": {
308
+ "tts": {"model_id": "tts-1", "provider": "openai"},
309
+ "stt": {"model_id": "whisper-1", "provider": "openai"},
310
+ "default": {"model_id": "whisper-1", "provider": "openai"}
311
+ },
312
+ "text": {
313
+ "model_id": "gpt-4.1-mini",
314
+ "provider": "openai"
315
+ },
316
+ "image": {
317
+ "model_id": "black-forest-labs/flux-schnell",
318
+ "provider": "replicate"
319
+ },
320
+ "embedding": {
321
+ "model_id": "text-embedding-3-small",
322
+ "provider": "openai"
323
+ }
324
+ }
325
+
326
+ # Handle audio service type with task-specific models
327
+ if service_type == "audio":
328
+ if "speech" in task or "tts" in task:
329
+ default = defaults["audio"]["tts"]
330
+ elif "transcribe" in task or "stt" in task:
331
+ default = defaults["audio"]["stt"]
332
+ else:
333
+ default = defaults["audio"]["default"]
334
+ else:
335
+ default = defaults.get(service_type, defaults["vision"])
336
+
337
+ # Apply provider hint if provided
338
+ if provider_hint:
339
+ default = dict(default)
340
+ default["provider"] = provider_hint
341
+
342
+ return {
343
+ **default,
344
+ "reason": "Default selection"
345
+ }
346
+
347
+ async def _get_service(
348
+ self,
349
+ service_type: str,
350
+ model_name: str,
351
+ provider: str,
352
+ task: str
353
+ ) -> Any:
354
+ """Get appropriate service instance"""
355
+
356
+ cache_key = f"{service_type}_{provider}_{model_name}"
357
+
358
+ # Check cache first
359
+ if cache_key in self._service_cache:
360
+ return self._service_cache[cache_key]
361
+
362
+ try:
363
+ # Route to appropriate AIFactory method
364
+ if service_type == "vision":
365
+ service = self.ai_factory.get_vision(model_name, provider)
366
+
367
+ elif service_type == "audio":
368
+ if "speech" in task or "tts" in task:
369
+ service = self.ai_factory.get_tts(model_name, provider)
370
+ elif "transcribe" in task or "stt" in task:
371
+ service = self.ai_factory.get_stt(model_name, provider)
372
+ else:
373
+ # Default to STT for unknown audio tasks
374
+ service = self.ai_factory.get_stt(model_name, provider)
375
+
376
+ elif service_type == "text":
377
+ service = self.ai_factory.get_llm(model_name, provider)
378
+
379
+ elif service_type == "image":
380
+ service = self.ai_factory.get_img("t2i", model_name, provider)
381
+
382
+ elif service_type == "embedding":
383
+ service = self.ai_factory.get_embed(model_name, provider)
384
+
385
+ else:
386
+ raise ValueError(f"Unsupported service type: {service_type}")
387
+
388
+ # Cache the service
389
+ self._service_cache[cache_key] = service
390
+ return service
391
+
392
+ except Exception as e:
393
+ logger.error(f"Failed to get service {service_type}/{provider}/{model_name}: {e}")
394
+ raise
395
+
396
+ async def _execute_task(
397
+ self,
398
+ service: Any,
399
+ input_data: Any,
400
+ task: str,
401
+ service_type: str,
402
+ **kwargs
403
+ ) -> Any:
404
+ """Execute the task using the appropriate service"""
405
+
406
+ try:
407
+ if service_type == "vision":
408
+ return await self._execute_vision_task(service, input_data, task, **kwargs)
409
+
410
+ elif service_type == "audio":
411
+ return await self._execute_audio_task(service, input_data, task, **kwargs)
412
+
413
+ elif service_type == "text":
414
+ return await self._execute_text_task(service, input_data, task, **kwargs)
415
+
416
+ elif service_type == "image":
417
+ return await self._execute_image_task(service, input_data, task, **kwargs)
418
+
419
+ elif service_type == "embedding":
420
+ return await self._execute_embedding_task(service, input_data, task, **kwargs)
421
+
422
+ else:
423
+ raise ValueError(f"Unsupported service type: {service_type}")
424
+
425
+ except Exception as e:
426
+ logger.error(f"Task execution failed: {e}")
427
+ raise
428
+
429
+ async def _execute_vision_task(self, service, input_data, task, **kwargs):
430
+ """Execute vision-related tasks using unified invoke method"""
431
+
432
+ # Map common task names to unified task names
433
+ task_mapping = {
434
+ "analyze_image": "analyze_image",
435
+ "detect_ui_elements": "detect_ui",
436
+ "extract_table": "extract_table",
437
+ "extract_text": "extract_text",
438
+ "ocr": "extract_text",
439
+ "describe": "analyze_image"
440
+ }
441
+
442
+ unified_task = task_mapping.get(task, task)
443
+
444
+ # Use unified invoke method with proper parameters
445
+ return await service.invoke(
446
+ image=input_data,
447
+ task=unified_task,
448
+ **kwargs
449
+ )
450
+
451
+ async def _execute_audio_task(self, service, input_data, task, **kwargs):
452
+ """Execute audio-related tasks using unified invoke method"""
453
+
454
+ # Map common task names to unified task names
455
+ task_mapping = {
456
+ "generate_speech": "synthesize",
457
+ "text_to_speech": "synthesize",
458
+ "tts": "synthesize",
459
+ "transcribe": "transcribe",
460
+ "speech_to_text": "transcribe",
461
+ "stt": "transcribe",
462
+ "translate": "translate",
463
+ "detect_language": "detect_language"
464
+ }
465
+
466
+ unified_task = task_mapping.get(task, task)
467
+
468
+ # Use unified invoke method with correct parameter name based on task type
469
+ if unified_task in ["synthesize", "text_to_speech", "tts"]:
470
+ # TTS services expect 'text' parameter
471
+ return await service.invoke(
472
+ text=input_data,
473
+ task=unified_task,
474
+ **kwargs
475
+ )
476
+ else:
477
+ # STT services expect 'audio_input' parameter
478
+ return await service.invoke(
479
+ audio_input=input_data,
480
+ task=unified_task,
481
+ **kwargs
482
+ )
483
+
484
+ async def _execute_text_task(self, service, input_data, task, **kwargs):
485
+ """Execute text-related tasks using unified invoke method"""
486
+
487
+ # Map common task names to unified task names
488
+ task_mapping = {
489
+ "chat": "chat",
490
+ "generate": "generate",
491
+ "complete": "complete",
492
+ "translate": "translate",
493
+ "summarize": "summarize",
494
+ "analyze": "analyze",
495
+ "extract": "extract",
496
+ "classify": "classify"
497
+ }
498
+
499
+ unified_task = task_mapping.get(task, task)
500
+
501
+ # Use unified invoke method
502
+ return await service.invoke(
503
+ input_data=input_data,
504
+ task=unified_task,
505
+ **kwargs
506
+ )
507
+
508
+ async def _execute_image_task(self, service, input_data, task, **kwargs):
509
+ """Execute image generation tasks using unified invoke method"""
510
+
511
+ # Map common task names to unified task names
512
+ task_mapping = {
513
+ "generate_image": "generate",
514
+ "generate": "generate",
515
+ "img2img": "img2img",
516
+ "image_to_image": "img2img",
517
+ "generate_batch": "generate_batch"
518
+ }
519
+
520
+ unified_task = task_mapping.get(task, task)
521
+
522
+ # Use unified invoke method
523
+ return await service.invoke(
524
+ prompt=input_data,
525
+ task=unified_task,
526
+ **kwargs
527
+ )
528
+
529
+ async def _execute_embedding_task(self, service, input_data, task, **kwargs):
530
+ """Execute embedding tasks using unified invoke method"""
531
+
532
+ # Map common task names to unified task names
533
+ task_mapping = {
534
+ "create_embedding": "embed",
535
+ "embed": "embed",
536
+ "embed_batch": "embed_batch",
537
+ "chunk_and_embed": "chunk_and_embed",
538
+ "similarity": "similarity",
539
+ "find_similar": "find_similar"
540
+ }
541
+
542
+ unified_task = task_mapping.get(task, task)
543
+
544
+ # Use unified invoke method
545
+ return await service.invoke(
546
+ input_data=input_data,
547
+ task=unified_task,
548
+ **kwargs
549
+ )
550
+
551
+ def clear_cache(self):
552
+ """Clear service cache"""
553
+ self._service_cache.clear()
554
+ logger.info("Service cache cleared")
555
+
556
+ async def get_available_models(self, service_type: Optional[str] = None) -> List[Dict[str, Any]]:
557
+ """Get list of available models
558
+
559
+ Args:
560
+ service_type: Optional filter by service type
561
+
562
+ Returns:
563
+ List of available models with metadata
564
+ """
565
+ if INTELLIGENT_SELECTOR_AVAILABLE:
566
+ try:
567
+ if self.model_selector is None:
568
+ self.model_selector = await get_model_selector(self.config)
569
+ return await self.model_selector.get_available_models(service_type)
570
+ except Exception as e:
571
+ logger.error(f"Failed to get available models: {e}")
572
+ return []
573
+ else:
574
+ return []
575
+
576
+ async def health_check(self) -> Dict[str, Any]:
577
+ """Check health of client and underlying services
578
+
579
+ Returns:
580
+ Health status dictionary
581
+ """
582
+ try:
583
+ health_status = {
584
+ "client": "healthy",
585
+ "ai_factory": "healthy" if self.ai_factory else "unavailable",
586
+ "model_selector": "healthy" if self.model_selector else "unavailable",
587
+ "services": {}
588
+ }
589
+
590
+ # Check a few key services
591
+ test_services = [
592
+ ("vision", "openai", "gpt-4.1-mini"),
593
+ ("audio", "openai", "whisper-1"),
594
+ ("text", "openai", "gpt-4.1-mini")
595
+ ]
596
+
597
+ for service_type, provider, model in test_services:
598
+ try:
599
+ await self._get_service(service_type, model, provider, "test")
600
+ health_status["services"][f"{service_type}_{provider}"] = "healthy"
601
+ except Exception as e:
602
+ health_status["services"][f"{service_type}_{provider}"] = f"error: {str(e)}"
603
+
604
+ return health_status
605
+
606
+ except Exception as e:
607
+ return {
608
+ "client": "error",
609
+ "error": str(e)
610
+ }
611
+
612
+ async def _invoke_local(
613
+ self,
614
+ input_data: Union[str, bytes, Path, Dict[str, Any]],
615
+ task: str,
616
+ service_type: str,
617
+ model_hint: Optional[str] = None,
618
+ provider_hint: Optional[str] = None,
619
+ **kwargs
620
+ ) -> Dict[str, Any]:
621
+ """Local invoke using AI Factory (original logic)"""
622
+ try:
623
+ # Step 1: Select best model for this task
624
+ selected_model = await self._select_model(
625
+ input_data=input_data,
626
+ task=task,
627
+ service_type=service_type,
628
+ model_hint=model_hint,
629
+ provider_hint=provider_hint
630
+ )
631
+
632
+ # Step 2: Get appropriate service
633
+ service = await self._get_service(
634
+ service_type=service_type,
635
+ model_name=selected_model["model_id"],
636
+ provider=selected_model["provider"],
637
+ task=task
638
+ )
639
+
640
+ # Step 3: Execute task with unified interface
641
+ result = await self._execute_task(
642
+ service=service,
643
+ input_data=input_data,
644
+ task=task,
645
+ service_type=service_type,
646
+ **kwargs
647
+ )
648
+
649
+ # Step 4: Return unified response
650
+ return {
651
+ "success": True,
652
+ "result": result,
653
+ "metadata": {
654
+ "model_used": selected_model["model_id"],
655
+ "provider": selected_model["provider"],
656
+ "task": task,
657
+ "service_type": service_type,
658
+ "selection_reason": selected_model.get("reason", "Default selection")
659
+ }
660
+ }
661
+ except Exception as e:
662
+ logger.error(f"Local invoke failed: {e}")
663
+ raise
664
+
665
+ async def _invoke_api(
666
+ self,
667
+ input_data: Union[str, bytes, Path, Dict[str, Any]],
668
+ task: str,
669
+ service_type: str,
670
+ model_hint: Optional[str] = None,
671
+ provider_hint: Optional[str] = None,
672
+ **kwargs
673
+ ) -> Dict[str, Any]:
674
+ """API invoke using HTTP requests"""
675
+
676
+ # Handle file inputs
677
+ if isinstance(input_data, Path):
678
+ return await self._invoke_api_file(
679
+ file_path=input_data,
680
+ task=task,
681
+ service_type=service_type,
682
+ model_hint=model_hint,
683
+ provider_hint=provider_hint,
684
+ **kwargs
685
+ )
686
+
687
+ # Handle binary data
688
+ if isinstance(input_data, bytes):
689
+ return await self._invoke_api_binary(
690
+ data=input_data,
691
+ task=task,
692
+ service_type=service_type,
693
+ model_hint=model_hint,
694
+ provider_hint=provider_hint,
695
+ **kwargs
696
+ )
697
+
698
+ # Handle text/JSON data
699
+ payload = {
700
+ "input_data": input_data,
701
+ "task": task,
702
+ "service_type": service_type,
703
+ "model_hint": model_hint,
704
+ "provider_hint": provider_hint,
705
+ "parameters": kwargs
706
+ }
707
+
708
+ async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=300)) as session:
709
+ try:
710
+ async with session.post(
711
+ f"{self.api_url}/api/v1/invoke",
712
+ json=payload,
713
+ headers=self.headers
714
+ ) as response:
715
+
716
+ if response.status == 200:
717
+ return await response.json()
718
+ else:
719
+ error_data = await response.text()
720
+ raise Exception(f"API error {response.status}: {error_data}")
721
+
722
+ except Exception as e:
723
+ logger.error(f"API invoke failed: {e}")
724
+ raise
725
+
726
+ async def _invoke_api_file(
727
+ self,
728
+ file_path: Path,
729
+ task: str,
730
+ service_type: str,
731
+ model_hint: Optional[str] = None,
732
+ provider_hint: Optional[str] = None,
733
+ **kwargs
734
+ ) -> Dict[str, Any]:
735
+ """API file upload"""
736
+
737
+ if not file_path.exists():
738
+ raise FileNotFoundError(f"File not found: {file_path}")
739
+
740
+ data = aiohttp.FormData()
741
+ data.add_field('task', task)
742
+ data.add_field('service_type', service_type)
743
+
744
+ if model_hint:
745
+ data.add_field('model_hint', model_hint)
746
+ if provider_hint:
747
+ data.add_field('provider_hint', provider_hint)
748
+
749
+ data.add_field('file',
750
+ open(file_path, 'rb'),
751
+ filename=file_path.name,
752
+ content_type='application/octet-stream')
753
+
754
+ headers = {k: v for k, v in self.headers.items() if k != "Content-Type"}
755
+
756
+ async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=300)) as session:
757
+ try:
758
+ async with session.post(
759
+ f"{self.api_url}/api/v1/invoke-file",
760
+ data=data,
761
+ headers=headers
762
+ ) as response:
763
+
764
+ if response.status == 200:
765
+ return await response.json()
766
+ else:
767
+ error_data = await response.text()
768
+ raise Exception(f"API error {response.status}: {error_data}")
769
+
770
+ except Exception as e:
771
+ logger.error(f"API file upload failed: {e}")
772
+ raise
773
+
774
+ async def _invoke_api_binary(
775
+ self,
776
+ data: bytes,
777
+ task: str,
778
+ service_type: str,
779
+ model_hint: Optional[str] = None,
780
+ provider_hint: Optional[str] = None,
781
+ **kwargs
782
+ ) -> Dict[str, Any]:
783
+ """API binary upload"""
784
+
785
+ form_data = aiohttp.FormData()
786
+ form_data.add_field('task', task)
787
+ form_data.add_field('service_type', service_type)
788
+
789
+ if model_hint:
790
+ form_data.add_field('model_hint', model_hint)
791
+ if provider_hint:
792
+ form_data.add_field('provider_hint', provider_hint)
793
+
794
+ form_data.add_field('file',
795
+ data,
796
+ filename='data.bin',
797
+ content_type='application/octet-stream')
798
+
799
+ headers = {k: v for k, v in self.headers.items() if k != "Content-Type"}
800
+
801
+ async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=300)) as session:
802
+ try:
803
+ async with session.post(
804
+ f"{self.api_url}/api/v1/invoke-file",
805
+ data=form_data,
806
+ headers=headers
807
+ ) as response:
808
+
809
+ if response.status == 200:
810
+ return await response.json()
811
+ else:
812
+ error_data = await response.text()
813
+ raise Exception(f"API error {response.status}: {error_data}")
814
+
815
+ except Exception as e:
816
+ logger.error(f"API binary upload failed: {e}")
817
+ raise
818
+
819
+ async def _stream_local(
820
+ self,
821
+ input_data: Union[str, bytes, Path, Dict[str, Any]],
822
+ task: str,
823
+ service_type: str,
824
+ model_hint: Optional[str] = None,
825
+ provider_hint: Optional[str] = None,
826
+ **kwargs
827
+ ):
828
+ """Local streaming using AI Factory"""
829
+ # Step 1: Select best model for this task
830
+ selected_model = await self._select_model(
831
+ input_data=input_data,
832
+ task=task,
833
+ service_type=service_type,
834
+ model_hint=model_hint,
835
+ provider_hint=provider_hint
836
+ )
837
+
838
+ # Step 2: Get appropriate service
839
+ service = await self._get_service(
840
+ service_type=service_type,
841
+ model_name=selected_model["model_id"],
842
+ provider=selected_model["provider"],
843
+ task=task
844
+ )
845
+
846
+ # Step 3: Yield tokens from the stream
847
+ async for token in service.astream(input_data):
848
+ yield token
849
+
850
+ async def _stream_api(
851
+ self,
852
+ input_data: Union[str, bytes, Path, Dict[str, Any]],
853
+ task: str,
854
+ service_type: str,
855
+ model_hint: Optional[str] = None,
856
+ provider_hint: Optional[str] = None,
857
+ **kwargs
858
+ ):
859
+ """API streaming using Server-Sent Events (SSE)"""
860
+
861
+ # Only support text streaming for now
862
+ if not isinstance(input_data, (str, dict)):
863
+ raise ValueError("API streaming only supports text input")
864
+
865
+ payload = {
866
+ "input_data": input_data,
867
+ "task": task,
868
+ "service_type": service_type,
869
+ "model_hint": model_hint,
870
+ "provider_hint": provider_hint,
871
+ "stream": True,
872
+ "parameters": kwargs
873
+ }
874
+
875
+ async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=300)) as session:
876
+ try:
877
+ async with session.post(
878
+ f"{self.api_url}/api/v1/stream",
879
+ json=payload,
880
+ headers=self.headers
881
+ ) as response:
882
+
883
+ if response.status == 200:
884
+ # Parse SSE stream
885
+ async for line in response.content:
886
+ if line:
887
+ line_str = line.decode().strip()
888
+ if line_str.startswith("data: "):
889
+ try:
890
+ # Parse SSE data
891
+ import json
892
+ json_str = line_str[6:] # Remove "data: " prefix
893
+ data = json.loads(json_str)
894
+
895
+ if data.get("type") == "token" and "token" in data:
896
+ yield data["token"]
897
+ elif data.get("type") == "completion":
898
+ # End of stream
899
+ break
900
+ elif data.get("type") == "error":
901
+ raise Exception(f"Server error: {data.get('error')}")
902
+
903
+ except json.JSONDecodeError:
904
+ # Skip malformed lines
905
+ continue
906
+ else:
907
+ error_data = await response.text()
908
+ raise Exception(f"API streaming error {response.status}: {error_data}")
909
+
910
+ except Exception as e:
911
+ logger.error(f"API streaming failed: {e}")
912
+ raise
913
+
914
+
915
+ # Convenience function for quick access
916
+ def create_client(
917
+ config: Optional[Dict[str, Any]] = None,
918
+ mode: str = "local",
919
+ api_url: Optional[str] = None,
920
+ api_key: Optional[str] = None
921
+ ) -> ISAModelClient:
922
+ """Create ISA Model Client instance
923
+
924
+ Args:
925
+ config: Optional configuration
926
+ mode: "local" for direct AI Factory, "api" for HTTP API calls
927
+ api_url: API base URL (required if mode="api")
928
+ api_key: API key for authentication (optional)
929
+
930
+ Returns:
931
+ ISAModelClient instance
932
+ """
933
+ return ISAModelClient(config=config, mode=mode, api_url=api_url, api_key=api_key)
934
+
935
+
936
+ # Export for easy import
937
+ __all__ = ["ISAModelClient", "create_client"]