isa-model 0.4.0__py3-none-any.whl → 0.4.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. isa_model/client.py +466 -43
  2. isa_model/core/cache/redis_cache.py +12 -3
  3. isa_model/core/config/config_manager.py +230 -3
  4. isa_model/core/config.py +90 -0
  5. isa_model/core/database/direct_db_client.py +114 -0
  6. isa_model/core/database/migration_manager.py +563 -0
  7. isa_model/core/database/migrations.py +21 -1
  8. isa_model/core/database/supabase_client.py +154 -19
  9. isa_model/core/dependencies.py +316 -0
  10. isa_model/core/discovery/__init__.py +19 -0
  11. isa_model/core/discovery/consul_discovery.py +190 -0
  12. isa_model/core/logging/__init__.py +54 -0
  13. isa_model/core/logging/influx_logger.py +523 -0
  14. isa_model/core/logging/loki_logger.py +160 -0
  15. isa_model/core/models/__init__.py +27 -18
  16. isa_model/core/models/config_models.py +625 -0
  17. isa_model/core/models/deployment_billing_tracker.py +430 -0
  18. isa_model/core/models/model_manager.py +35 -80
  19. isa_model/core/models/model_metadata.py +690 -0
  20. isa_model/core/models/model_repo.py +174 -18
  21. isa_model/core/models/system_models.py +857 -0
  22. isa_model/core/repositories/__init__.py +9 -0
  23. isa_model/core/repositories/config_repository.py +912 -0
  24. isa_model/core/services/intelligent_model_selector.py +399 -21
  25. isa_model/core/types.py +1 -0
  26. isa_model/deployment/__init__.py +5 -48
  27. isa_model/deployment/core/__init__.py +2 -31
  28. isa_model/deployment/core/deployment_manager.py +1278 -370
  29. isa_model/deployment/modal/__init__.py +8 -0
  30. isa_model/deployment/modal/config.py +136 -0
  31. isa_model/deployment/{services/auto_hf_modal_deployer.py → modal/deployer.py} +1 -1
  32. isa_model/deployment/modal/services/__init__.py +3 -0
  33. isa_model/deployment/modal/services/audio/__init__.py +1 -0
  34. isa_model/deployment/modal/services/embedding/__init__.py +1 -0
  35. isa_model/deployment/modal/services/llm/__init__.py +1 -0
  36. isa_model/deployment/modal/services/llm/isa_llm_service.py +424 -0
  37. isa_model/deployment/modal/services/video/__init__.py +1 -0
  38. isa_model/deployment/modal/services/vision/__init__.py +1 -0
  39. isa_model/deployment/models/org-org-acme-corp-tenant-a-service-llm-20250825-225822/tenant-a-service_modal_service.py +48 -0
  40. isa_model/deployment/models/org-test-org-123-prefix-test-service-llm-20250825-225822/prefix-test-service_modal_service.py +48 -0
  41. isa_model/deployment/models/test-llm-service-llm-20250825-204442/test-llm-service_modal_service.py +48 -0
  42. isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-212906/test-monitoring-gpt2_modal_service.py +48 -0
  43. isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-213009/test-monitoring-gpt2_modal_service.py +48 -0
  44. isa_model/deployment/storage/__init__.py +5 -0
  45. isa_model/deployment/storage/deployment_repository.py +824 -0
  46. isa_model/deployment/triton/__init__.py +10 -0
  47. isa_model/deployment/triton/config.py +196 -0
  48. isa_model/deployment/triton/configs/__init__.py +1 -0
  49. isa_model/deployment/triton/provider.py +512 -0
  50. isa_model/deployment/triton/scripts/__init__.py +1 -0
  51. isa_model/deployment/triton/templates/__init__.py +1 -0
  52. isa_model/inference/__init__.py +47 -1
  53. isa_model/inference/ai_factory.py +137 -10
  54. isa_model/inference/legacy_services/__init__.py +21 -0
  55. isa_model/inference/legacy_services/model_evaluation.py +637 -0
  56. isa_model/inference/legacy_services/model_service.py +573 -0
  57. isa_model/inference/legacy_services/model_serving.py +717 -0
  58. isa_model/inference/legacy_services/model_training.py +561 -0
  59. isa_model/inference/models/__init__.py +21 -0
  60. isa_model/inference/models/inference_config.py +551 -0
  61. isa_model/inference/models/inference_record.py +675 -0
  62. isa_model/inference/models/performance_models.py +714 -0
  63. isa_model/inference/repositories/__init__.py +9 -0
  64. isa_model/inference/repositories/inference_repository.py +828 -0
  65. isa_model/inference/services/audio/base_stt_service.py +184 -11
  66. isa_model/inference/services/audio/openai_stt_service.py +22 -6
  67. isa_model/inference/services/embedding/ollama_embed_service.py +15 -3
  68. isa_model/inference/services/embedding/resilient_embed_service.py +285 -0
  69. isa_model/inference/services/llm/__init__.py +10 -2
  70. isa_model/inference/services/llm/base_llm_service.py +335 -24
  71. isa_model/inference/services/llm/cerebras_llm_service.py +628 -0
  72. isa_model/inference/services/llm/helpers/llm_adapter.py +9 -4
  73. isa_model/inference/services/llm/helpers/llm_prompts.py +342 -0
  74. isa_model/inference/services/llm/helpers/llm_utils.py +321 -23
  75. isa_model/inference/services/llm/huggingface_llm_service.py +581 -0
  76. isa_model/inference/services/llm/ollama_llm_service.py +9 -2
  77. isa_model/inference/services/llm/openai_llm_service.py +33 -16
  78. isa_model/inference/services/llm/yyds_llm_service.py +8 -2
  79. isa_model/inference/services/vision/__init__.py +22 -1
  80. isa_model/inference/services/vision/helpers/image_utils.py +8 -5
  81. isa_model/inference/services/vision/isa_vision_service.py +65 -4
  82. isa_model/inference/services/vision/openai_vision_service.py +19 -10
  83. isa_model/inference/services/vision/vgg16_vision_service.py +257 -0
  84. isa_model/serving/api/cache_manager.py +245 -0
  85. isa_model/serving/api/dependencies/__init__.py +1 -0
  86. isa_model/serving/api/dependencies/auth.py +194 -0
  87. isa_model/serving/api/dependencies/database.py +139 -0
  88. isa_model/serving/api/error_handlers.py +284 -0
  89. isa_model/serving/api/fastapi_server.py +172 -22
  90. isa_model/serving/api/middleware/auth.py +8 -2
  91. isa_model/serving/api/middleware/security.py +23 -33
  92. isa_model/serving/api/middleware/tenant_context.py +414 -0
  93. isa_model/serving/api/routes/analytics.py +4 -1
  94. isa_model/serving/api/routes/config.py +645 -0
  95. isa_model/serving/api/routes/deployment_billing.py +315 -0
  96. isa_model/serving/api/routes/deployments.py +138 -2
  97. isa_model/serving/api/routes/gpu_gateway.py +440 -0
  98. isa_model/serving/api/routes/health.py +32 -12
  99. isa_model/serving/api/routes/inference_monitoring.py +486 -0
  100. isa_model/serving/api/routes/local_deployments.py +448 -0
  101. isa_model/serving/api/routes/tenants.py +575 -0
  102. isa_model/serving/api/routes/unified.py +680 -18
  103. isa_model/serving/api/routes/webhooks.py +479 -0
  104. isa_model/serving/api/startup.py +68 -54
  105. isa_model/utils/gpu_utils.py +311 -0
  106. {isa_model-0.4.0.dist-info → isa_model-0.4.4.dist-info}/METADATA +71 -24
  107. isa_model-0.4.4.dist-info/RECORD +180 -0
  108. isa_model/core/security/secrets.py +0 -358
  109. isa_model/core/storage/hf_storage.py +0 -419
  110. isa_model/core/storage/minio_storage.py +0 -0
  111. isa_model/deployment/cloud/__init__.py +0 -9
  112. isa_model/deployment/cloud/modal/__init__.py +0 -10
  113. isa_model/deployment/core/deployment_config.py +0 -356
  114. isa_model/deployment/core/isa_deployment_service.py +0 -401
  115. isa_model/deployment/gpu_int8_ds8/app/server.py +0 -66
  116. isa_model/deployment/gpu_int8_ds8/scripts/test_client.py +0 -43
  117. isa_model/deployment/gpu_int8_ds8/scripts/test_client_os.py +0 -35
  118. isa_model/deployment/runtime/deployed_service.py +0 -338
  119. isa_model/deployment/services/__init__.py +0 -9
  120. isa_model/deployment/services/auto_deploy_vision_service.py +0 -538
  121. isa_model/deployment/services/model_service.py +0 -332
  122. isa_model/deployment/services/service_monitor.py +0 -356
  123. isa_model/deployment/services/service_registry.py +0 -527
  124. isa_model/eval/__init__.py +0 -92
  125. isa_model/eval/benchmarks/__init__.py +0 -27
  126. isa_model/eval/benchmarks/multimodal_datasets.py +0 -460
  127. isa_model/eval/benchmarks.py +0 -701
  128. isa_model/eval/config/__init__.py +0 -10
  129. isa_model/eval/config/evaluation_config.py +0 -108
  130. isa_model/eval/evaluators/__init__.py +0 -24
  131. isa_model/eval/evaluators/audio_evaluator.py +0 -727
  132. isa_model/eval/evaluators/base_evaluator.py +0 -503
  133. isa_model/eval/evaluators/embedding_evaluator.py +0 -742
  134. isa_model/eval/evaluators/llm_evaluator.py +0 -472
  135. isa_model/eval/evaluators/vision_evaluator.py +0 -564
  136. isa_model/eval/example_evaluation.py +0 -395
  137. isa_model/eval/factory.py +0 -798
  138. isa_model/eval/infrastructure/__init__.py +0 -24
  139. isa_model/eval/infrastructure/experiment_tracker.py +0 -466
  140. isa_model/eval/isa_benchmarks.py +0 -700
  141. isa_model/eval/isa_integration.py +0 -582
  142. isa_model/eval/metrics.py +0 -951
  143. isa_model/eval/tests/unit/test_basic.py +0 -396
  144. isa_model/serving/api/routes/evaluations.py +0 -579
  145. isa_model/training/__init__.py +0 -168
  146. isa_model/training/annotation/annotation_schema.py +0 -47
  147. isa_model/training/annotation/processors/annotation_processor.py +0 -126
  148. isa_model/training/annotation/storage/dataset_manager.py +0 -131
  149. isa_model/training/annotation/storage/dataset_schema.py +0 -44
  150. isa_model/training/annotation/tests/test_annotation_flow.py +0 -109
  151. isa_model/training/annotation/tests/test_minio copy.py +0 -113
  152. isa_model/training/annotation/tests/test_minio_upload.py +0 -43
  153. isa_model/training/annotation/views/annotation_controller.py +0 -158
  154. isa_model/training/cloud/__init__.py +0 -22
  155. isa_model/training/cloud/job_orchestrator.py +0 -402
  156. isa_model/training/cloud/runpod_trainer.py +0 -454
  157. isa_model/training/cloud/storage_manager.py +0 -482
  158. isa_model/training/core/__init__.py +0 -26
  159. isa_model/training/core/config.py +0 -181
  160. isa_model/training/core/dataset.py +0 -222
  161. isa_model/training/core/trainer.py +0 -720
  162. isa_model/training/core/utils.py +0 -213
  163. isa_model/training/examples/intelligent_training_example.py +0 -281
  164. isa_model/training/factory.py +0 -424
  165. isa_model/training/intelligent/__init__.py +0 -25
  166. isa_model/training/intelligent/decision_engine.py +0 -643
  167. isa_model/training/intelligent/intelligent_factory.py +0 -888
  168. isa_model/training/intelligent/knowledge_base.py +0 -751
  169. isa_model/training/intelligent/resource_optimizer.py +0 -839
  170. isa_model/training/intelligent/task_classifier.py +0 -576
  171. isa_model/training/storage/__init__.py +0 -24
  172. isa_model/training/storage/core_integration.py +0 -439
  173. isa_model/training/storage/training_repository.py +0 -552
  174. isa_model/training/storage/training_storage.py +0 -628
  175. isa_model-0.4.0.dist-info/RECORD +0 -182
  176. /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_chatTTS_service.py +0 -0
  177. /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_fish_service.py +0 -0
  178. /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_openvoice_service.py +0 -0
  179. /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_service_v2.py +0 -0
  180. /isa_model/deployment/{cloud/modal → modal/services/embedding}/isa_embed_rerank_service.py +0 -0
  181. /isa_model/deployment/{cloud/modal → modal/services/video}/isa_video_hunyuan_service.py +0 -0
  182. /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ocr_service.py +0 -0
  183. /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_qwen25_service.py +0 -0
  184. /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_table_service.py +0 -0
  185. /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ui_service.py +0 -0
  186. /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ui_service_optimized.py +0 -0
  187. /isa_model/deployment/{services → modal/services/vision}/simple_auto_deploy_vision_service.py +0 -0
  188. {isa_model-0.4.0.dist-info → isa_model-0.4.4.dist-info}/WHEEL +0 -0
  189. {isa_model-0.4.0.dist-info → isa_model-0.4.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,479 @@
1
+ """
2
+ Webhooks API Routes
3
+
4
+ Provides webhook management and notification system for:
5
+ - Training job status changes
6
+ - Deployment status updates
7
+ - Model evaluation completion
8
+ - System alerts and notifications
9
+ """
10
+
11
+ from fastapi import APIRouter, HTTPException, BackgroundTasks
12
+ from pydantic import BaseModel, HttpUrl
13
+ from typing import Optional, List, Dict, Any
14
+ import logging
15
+ import asyncio
16
+ import json
17
+ import uuid
18
+ from datetime import datetime
19
+ import aiohttp
20
+
21
+ logger = logging.getLogger(__name__)
22
+ router = APIRouter()
23
+
24
+ # Request/Response Models
25
+ class WebhookConfig(BaseModel):
26
+ """Configuration for a webhook endpoint"""
27
+ url: HttpUrl
28
+ events: List[str] = ["*"] # Which events to listen for
29
+ active: bool = True
30
+ secret: Optional[str] = None # For webhook signature verification
31
+ headers: Dict[str, str] = {} # Custom headers to send
32
+
33
+ class WebhookRequest(BaseModel):
34
+ """Request to create or update a webhook"""
35
+ name: str
36
+ url: HttpUrl
37
+ events: List[str] = ["training.completed", "deployment.completed", "evaluation.completed"]
38
+ active: bool = True
39
+ secret: Optional[str] = None
40
+ headers: Dict[str, str] = {}
41
+
42
+ class WebhookPayload(BaseModel):
43
+ """Standard webhook payload format"""
44
+ event: str
45
+ timestamp: str
46
+ data: Dict[str, Any]
47
+ webhook_id: str
48
+
49
+ class WebhookDelivery(BaseModel):
50
+ """Webhook delivery status"""
51
+ webhook_id: str
52
+ event: str
53
+ status: str # pending, delivered, failed
54
+ attempts: int = 0
55
+ max_attempts: int = 3
56
+ last_attempt: Optional[str] = None
57
+ response_status: Optional[int] = None
58
+ error_message: Optional[str] = None
59
+
60
+ # In-memory webhook storage (in production, use database)
61
+ webhooks = {}
62
+ deliveries = {}
63
+
64
+ @router.get("/health")
65
+ async def webhooks_health():
66
+ """Health check for webhooks service"""
67
+ return {
68
+ "status": "healthy",
69
+ "service": "webhooks",
70
+ "active_webhooks": len([w for w in webhooks.values() if w["active"]]),
71
+ "total_webhooks": len(webhooks)
72
+ }
73
+
74
+ @router.post("/")
75
+ async def create_webhook(request: WebhookRequest):
76
+ """
77
+ Create a new webhook endpoint
78
+ """
79
+ try:
80
+ webhook_id = str(uuid.uuid4())
81
+
82
+ webhook_config = {
83
+ "id": webhook_id,
84
+ "name": request.name,
85
+ "url": str(request.url),
86
+ "events": request.events,
87
+ "active": request.active,
88
+ "secret": request.secret,
89
+ "headers": request.headers,
90
+ "created_at": datetime.utcnow().isoformat(),
91
+ "last_delivery": None,
92
+ "total_deliveries": 0,
93
+ "failed_deliveries": 0
94
+ }
95
+
96
+ webhooks[webhook_id] = webhook_config
97
+
98
+ logger.info(f"Created webhook {webhook_id} for {request.name}")
99
+
100
+ return {
101
+ "success": True,
102
+ "webhook_id": webhook_id,
103
+ "message": f"Webhook '{request.name}' created successfully",
104
+ "config": webhook_config
105
+ }
106
+
107
+ except Exception as e:
108
+ logger.error(f"Failed to create webhook: {e}")
109
+ raise HTTPException(status_code=500, detail=f"Failed to create webhook: {str(e)}")
110
+
111
+ @router.get("/")
112
+ async def list_webhooks():
113
+ """
114
+ List all configured webhooks
115
+ """
116
+ try:
117
+ webhook_list = []
118
+
119
+ for webhook_id, config in webhooks.items():
120
+ webhook_summary = {
121
+ "id": webhook_id,
122
+ "name": config["name"],
123
+ "url": config["url"],
124
+ "events": config["events"],
125
+ "active": config["active"],
126
+ "created_at": config["created_at"],
127
+ "total_deliveries": config["total_deliveries"],
128
+ "failed_deliveries": config["failed_deliveries"],
129
+ "last_delivery": config["last_delivery"]
130
+ }
131
+ webhook_list.append(webhook_summary)
132
+
133
+ return {
134
+ "success": True,
135
+ "webhooks": webhook_list,
136
+ "total_count": len(webhook_list)
137
+ }
138
+
139
+ except Exception as e:
140
+ logger.error(f"Failed to list webhooks: {e}")
141
+ raise HTTPException(status_code=500, detail=f"Failed to list webhooks: {str(e)}")
142
+
143
+ @router.get("/{webhook_id}")
144
+ async def get_webhook(webhook_id: str):
145
+ """
146
+ Get detailed information about a specific webhook
147
+ """
148
+ try:
149
+ if webhook_id not in webhooks:
150
+ raise HTTPException(status_code=404, detail=f"Webhook not found: {webhook_id}")
151
+
152
+ webhook_config = webhooks[webhook_id]
153
+
154
+ # Get recent deliveries for this webhook
155
+ recent_deliveries = [
156
+ delivery for delivery in deliveries.values()
157
+ if delivery.get("webhook_id") == webhook_id
158
+ ]
159
+
160
+ # Sort by timestamp, most recent first
161
+ recent_deliveries.sort(key=lambda x: x.get("last_attempt", ""), reverse=True)
162
+
163
+ return {
164
+ "success": True,
165
+ "webhook": webhook_config,
166
+ "recent_deliveries": recent_deliveries[:10] # Last 10 deliveries
167
+ }
168
+
169
+ except HTTPException:
170
+ raise
171
+ except Exception as e:
172
+ logger.error(f"Failed to get webhook {webhook_id}: {e}")
173
+ raise HTTPException(status_code=500, detail=f"Failed to get webhook: {str(e)}")
174
+
175
+ @router.put("/{webhook_id}")
176
+ async def update_webhook(webhook_id: str, request: WebhookRequest):
177
+ """
178
+ Update an existing webhook configuration
179
+ """
180
+ try:
181
+ if webhook_id not in webhooks:
182
+ raise HTTPException(status_code=404, detail=f"Webhook not found: {webhook_id}")
183
+
184
+ webhook_config = webhooks[webhook_id]
185
+
186
+ # Update configuration
187
+ webhook_config.update({
188
+ "name": request.name,
189
+ "url": str(request.url),
190
+ "events": request.events,
191
+ "active": request.active,
192
+ "secret": request.secret,
193
+ "headers": request.headers,
194
+ "updated_at": datetime.utcnow().isoformat()
195
+ })
196
+
197
+ logger.info(f"Updated webhook {webhook_id}")
198
+
199
+ return {
200
+ "success": True,
201
+ "message": f"Webhook '{request.name}' updated successfully",
202
+ "config": webhook_config
203
+ }
204
+
205
+ except HTTPException:
206
+ raise
207
+ except Exception as e:
208
+ logger.error(f"Failed to update webhook {webhook_id}: {e}")
209
+ raise HTTPException(status_code=500, detail=f"Failed to update webhook: {str(e)}")
210
+
211
+ @router.delete("/{webhook_id}")
212
+ async def delete_webhook(webhook_id: str):
213
+ """
214
+ Delete a webhook endpoint
215
+ """
216
+ try:
217
+ if webhook_id not in webhooks:
218
+ raise HTTPException(status_code=404, detail=f"Webhook not found: {webhook_id}")
219
+
220
+ webhook_name = webhooks[webhook_id]["name"]
221
+ del webhooks[webhook_id]
222
+
223
+ # Clean up associated deliveries
224
+ deliveries_to_remove = [
225
+ delivery_id for delivery_id, delivery in deliveries.items()
226
+ if delivery.get("webhook_id") == webhook_id
227
+ ]
228
+
229
+ for delivery_id in deliveries_to_remove:
230
+ del deliveries[delivery_id]
231
+
232
+ logger.info(f"Deleted webhook {webhook_id} and {len(deliveries_to_remove)} deliveries")
233
+
234
+ return {
235
+ "success": True,
236
+ "message": f"Webhook '{webhook_name}' deleted successfully"
237
+ }
238
+
239
+ except HTTPException:
240
+ raise
241
+ except Exception as e:
242
+ logger.error(f"Failed to delete webhook {webhook_id}: {e}")
243
+ raise HTTPException(status_code=500, detail=f"Failed to delete webhook: {str(e)}")
244
+
245
+ @router.post("/{webhook_id}/test")
246
+ async def test_webhook(webhook_id: str, background_tasks: BackgroundTasks):
247
+ """
248
+ Send a test event to a webhook endpoint
249
+ """
250
+ try:
251
+ if webhook_id not in webhooks:
252
+ raise HTTPException(status_code=404, detail=f"Webhook not found: {webhook_id}")
253
+
254
+ test_payload = {
255
+ "event": "webhook.test",
256
+ "timestamp": datetime.utcnow().isoformat(),
257
+ "data": {
258
+ "message": "This is a test webhook delivery",
259
+ "webhook_id": webhook_id,
260
+ "test": True
261
+ },
262
+ "webhook_id": webhook_id
263
+ }
264
+
265
+ # Send webhook in background
266
+ background_tasks.add_task(deliver_webhook, webhook_id, test_payload)
267
+
268
+ return {
269
+ "success": True,
270
+ "message": f"Test webhook sent to {webhooks[webhook_id]['name']}"
271
+ }
272
+
273
+ except HTTPException:
274
+ raise
275
+ except Exception as e:
276
+ logger.error(f"Failed to test webhook {webhook_id}: {e}")
277
+ raise HTTPException(status_code=500, detail=f"Failed to test webhook: {str(e)}")
278
+
279
+ @router.get("/deliveries/")
280
+ async def list_deliveries(limit: int = 50, webhook_id: Optional[str] = None):
281
+ """
282
+ List recent webhook deliveries
283
+ """
284
+ try:
285
+ delivery_list = []
286
+
287
+ for delivery_id, delivery in deliveries.items():
288
+ if webhook_id and delivery.get("webhook_id") != webhook_id:
289
+ continue
290
+
291
+ delivery_info = {
292
+ "delivery_id": delivery_id,
293
+ "webhook_id": delivery.get("webhook_id"),
294
+ "webhook_name": webhooks.get(delivery.get("webhook_id"), {}).get("name", "Unknown"),
295
+ "event": delivery.get("event"),
296
+ "status": delivery.get("status"),
297
+ "attempts": delivery.get("attempts"),
298
+ "last_attempt": delivery.get("last_attempt"),
299
+ "response_status": delivery.get("response_status"),
300
+ "error_message": delivery.get("error_message")
301
+ }
302
+ delivery_list.append(delivery_info)
303
+
304
+ # Sort by last attempt, most recent first
305
+ delivery_list.sort(key=lambda x: x.get("last_attempt", ""), reverse=True)
306
+
307
+ return {
308
+ "success": True,
309
+ "deliveries": delivery_list[:limit],
310
+ "total_count": len(delivery_list)
311
+ }
312
+
313
+ except Exception as e:
314
+ logger.error(f"Failed to list deliveries: {e}")
315
+ raise HTTPException(status_code=500, detail=f"Failed to list deliveries: {str(e)}")
316
+
317
+ # Webhook delivery functions
318
+
319
+ async def deliver_webhook(webhook_id: str, payload: Dict[str, Any]):
320
+ """
321
+ Deliver a webhook payload to the configured endpoint
322
+ """
323
+ try:
324
+ if webhook_id not in webhooks:
325
+ logger.warning(f"Webhook {webhook_id} not found for delivery")
326
+ return
327
+
328
+ webhook_config = webhooks[webhook_id]
329
+
330
+ if not webhook_config["active"]:
331
+ logger.debug(f"Webhook {webhook_id} is inactive, skipping delivery")
332
+ return
333
+
334
+ # Check if webhook should receive this event
335
+ events = webhook_config.get("events", ["*"])
336
+ event_type = payload.get("event", "")
337
+
338
+ if "*" not in events and event_type not in events:
339
+ logger.debug(f"Webhook {webhook_id} not configured for event {event_type}")
340
+ return
341
+
342
+ delivery_id = str(uuid.uuid4())
343
+ delivery_record = {
344
+ "delivery_id": delivery_id,
345
+ "webhook_id": webhook_id,
346
+ "event": event_type,
347
+ "status": "pending",
348
+ "attempts": 0,
349
+ "max_attempts": 3,
350
+ "created_at": datetime.utcnow().isoformat()
351
+ }
352
+
353
+ deliveries[delivery_id] = delivery_record
354
+
355
+ # Attempt delivery with retries
356
+ success = await attempt_delivery(webhook_config, payload, delivery_record)
357
+
358
+ # Update webhook stats
359
+ webhook_config["total_deliveries"] += 1
360
+ webhook_config["last_delivery"] = datetime.utcnow().isoformat()
361
+
362
+ if not success:
363
+ webhook_config["failed_deliveries"] += 1
364
+
365
+ except Exception as e:
366
+ logger.error(f"Failed to deliver webhook {webhook_id}: {e}")
367
+
368
+ async def attempt_delivery(webhook_config: Dict, payload: Dict, delivery_record: Dict):
369
+ """
370
+ Attempt to deliver webhook with retries
371
+ """
372
+ url = webhook_config["url"]
373
+ headers = {
374
+ "Content-Type": "application/json",
375
+ "User-Agent": "ISA-Model-Webhooks/1.0",
376
+ **webhook_config.get("headers", {})
377
+ }
378
+
379
+ # Add signature if secret is provided
380
+ if webhook_config.get("secret"):
381
+ import hmac
382
+ import hashlib
383
+
384
+ payload_bytes = json.dumps(payload, sort_keys=True).encode()
385
+ signature = hmac.new(
386
+ webhook_config["secret"].encode(),
387
+ payload_bytes,
388
+ hashlib.sha256
389
+ ).hexdigest()
390
+ headers["X-ISA-Signature-256"] = f"sha256={signature}"
391
+
392
+ max_attempts = delivery_record["max_attempts"]
393
+
394
+ for attempt in range(max_attempts):
395
+ try:
396
+ delivery_record["attempts"] = attempt + 1
397
+ delivery_record["last_attempt"] = datetime.utcnow().isoformat()
398
+
399
+ async with aiohttp.ClientSession() as session:
400
+ async with session.post(
401
+ url,
402
+ json=payload,
403
+ headers=headers,
404
+ timeout=aiohttp.ClientTimeout(total=30)
405
+ ) as response:
406
+ delivery_record["response_status"] = response.status
407
+
408
+ if response.status < 300: # 2xx success
409
+ delivery_record["status"] = "delivered"
410
+ logger.info(f"Webhook delivered successfully to {url} (attempt {attempt + 1})")
411
+ return True
412
+ else:
413
+ error_text = await response.text()
414
+ delivery_record["error_message"] = f"HTTP {response.status}: {error_text[:200]}"
415
+ logger.warning(f"Webhook delivery failed with status {response.status}: {error_text[:100]}")
416
+
417
+ except Exception as e:
418
+ delivery_record["error_message"] = f"Connection error: {str(e)[:200]}"
419
+ logger.warning(f"Webhook delivery attempt {attempt + 1} failed: {e}")
420
+
421
+ # Wait before retry (exponential backoff)
422
+ if attempt < max_attempts - 1:
423
+ await asyncio.sleep(2 ** attempt)
424
+
425
+ # All attempts failed
426
+ delivery_record["status"] = "failed"
427
+ logger.error(f"Webhook delivery failed after {max_attempts} attempts to {url}")
428
+ return False
429
+
430
+ # Event publishing functions
431
+
432
+ async def publish_event(event_type: str, data: Dict[str, Any]):
433
+ """
434
+ Publish an event to all matching webhooks
435
+ """
436
+ try:
437
+ payload = {
438
+ "event": event_type,
439
+ "timestamp": datetime.utcnow().isoformat(),
440
+ "data": data
441
+ }
442
+
443
+ # Send to all active webhooks that match the event
444
+ for webhook_id in webhooks:
445
+ asyncio.create_task(deliver_webhook(webhook_id, {**payload, "webhook_id": webhook_id}))
446
+
447
+ logger.info(f"Published event {event_type} to {len(webhooks)} webhooks")
448
+
449
+ except Exception as e:
450
+ logger.error(f"Failed to publish event {event_type}: {e}")
451
+
452
+ # Convenience functions for common events
453
+
454
+ async def notify_training_completed(job_id: str, job_name: str, status: str, **kwargs):
455
+ """Notify about training job completion"""
456
+ await publish_event("training.completed", {
457
+ "job_id": job_id,
458
+ "job_name": job_name,
459
+ "status": status,
460
+ **kwargs
461
+ })
462
+
463
+ async def notify_deployment_completed(deployment_id: str, model_id: str, status: str, **kwargs):
464
+ """Notify about deployment completion"""
465
+ await publish_event("deployment.completed", {
466
+ "deployment_id": deployment_id,
467
+ "model_id": model_id,
468
+ "status": status,
469
+ **kwargs
470
+ })
471
+
472
+ async def notify_evaluation_completed(evaluation_id: str, model_id: str, results: Dict, **kwargs):
473
+ """Notify about evaluation completion"""
474
+ await publish_event("evaluation.completed", {
475
+ "evaluation_id": evaluation_id,
476
+ "model_id": model_id,
477
+ "results": results,
478
+ **kwargs
479
+ })