isa-model 0.3.91__py3-none-any.whl → 0.4.3__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 (228) hide show
  1. isa_model/client.py +1166 -584
  2. isa_model/core/cache/redis_cache.py +410 -0
  3. isa_model/core/config/config_manager.py +282 -12
  4. isa_model/core/config.py +91 -1
  5. isa_model/core/database/__init__.py +1 -0
  6. isa_model/core/database/direct_db_client.py +114 -0
  7. isa_model/core/database/migration_manager.py +563 -0
  8. isa_model/core/database/migrations.py +297 -0
  9. isa_model/core/database/supabase_client.py +258 -0
  10. isa_model/core/dependencies.py +316 -0
  11. isa_model/core/discovery/__init__.py +19 -0
  12. isa_model/core/discovery/consul_discovery.py +190 -0
  13. isa_model/core/logging/__init__.py +54 -0
  14. isa_model/core/logging/influx_logger.py +523 -0
  15. isa_model/core/logging/loki_logger.py +160 -0
  16. isa_model/core/models/__init__.py +46 -0
  17. isa_model/core/models/config_models.py +625 -0
  18. isa_model/core/models/deployment_billing_tracker.py +430 -0
  19. isa_model/core/models/model_billing_tracker.py +60 -88
  20. isa_model/core/models/model_manager.py +66 -25
  21. isa_model/core/models/model_metadata.py +690 -0
  22. isa_model/core/models/model_repo.py +217 -55
  23. isa_model/core/models/model_statistics_tracker.py +234 -0
  24. isa_model/core/models/model_storage.py +0 -1
  25. isa_model/core/models/model_version_manager.py +959 -0
  26. isa_model/core/models/system_models.py +857 -0
  27. isa_model/core/pricing_manager.py +2 -249
  28. isa_model/core/repositories/__init__.py +9 -0
  29. isa_model/core/repositories/config_repository.py +912 -0
  30. isa_model/core/resilience/circuit_breaker.py +366 -0
  31. isa_model/core/security/secrets.py +358 -0
  32. isa_model/core/services/__init__.py +2 -4
  33. isa_model/core/services/intelligent_model_selector.py +479 -370
  34. isa_model/core/storage/hf_storage.py +2 -2
  35. isa_model/core/types.py +8 -0
  36. isa_model/deployment/__init__.py +5 -48
  37. isa_model/deployment/core/__init__.py +2 -31
  38. isa_model/deployment/core/deployment_manager.py +1278 -368
  39. isa_model/deployment/local/__init__.py +31 -0
  40. isa_model/deployment/local/config.py +248 -0
  41. isa_model/deployment/local/gpu_gateway.py +607 -0
  42. isa_model/deployment/local/health_checker.py +428 -0
  43. isa_model/deployment/local/provider.py +586 -0
  44. isa_model/deployment/local/tensorrt_service.py +621 -0
  45. isa_model/deployment/local/transformers_service.py +644 -0
  46. isa_model/deployment/local/vllm_service.py +527 -0
  47. isa_model/deployment/modal/__init__.py +8 -0
  48. isa_model/deployment/modal/config.py +136 -0
  49. isa_model/deployment/modal/deployer.py +894 -0
  50. isa_model/deployment/modal/services/__init__.py +3 -0
  51. isa_model/deployment/modal/services/audio/__init__.py +1 -0
  52. isa_model/deployment/modal/services/audio/isa_audio_chatTTS_service.py +520 -0
  53. isa_model/deployment/modal/services/audio/isa_audio_openvoice_service.py +758 -0
  54. isa_model/deployment/modal/services/audio/isa_audio_service_v2.py +1044 -0
  55. isa_model/deployment/modal/services/embedding/__init__.py +1 -0
  56. isa_model/deployment/modal/services/embedding/isa_embed_rerank_service.py +296 -0
  57. isa_model/deployment/modal/services/llm/__init__.py +1 -0
  58. isa_model/deployment/modal/services/llm/isa_llm_service.py +424 -0
  59. isa_model/deployment/modal/services/video/__init__.py +1 -0
  60. isa_model/deployment/modal/services/video/isa_video_hunyuan_service.py +423 -0
  61. isa_model/deployment/modal/services/vision/__init__.py +1 -0
  62. isa_model/deployment/modal/services/vision/isa_vision_ocr_service.py +519 -0
  63. isa_model/deployment/modal/services/vision/isa_vision_qwen25_service.py +709 -0
  64. isa_model/deployment/modal/services/vision/isa_vision_table_service.py +676 -0
  65. isa_model/deployment/modal/services/vision/isa_vision_ui_service.py +833 -0
  66. isa_model/deployment/modal/services/vision/isa_vision_ui_service_optimized.py +660 -0
  67. isa_model/deployment/models/org-org-acme-corp-tenant-a-service-llm-20250825-225822/tenant-a-service_modal_service.py +48 -0
  68. isa_model/deployment/models/org-test-org-123-prefix-test-service-llm-20250825-225822/prefix-test-service_modal_service.py +48 -0
  69. isa_model/deployment/models/test-llm-service-llm-20250825-204442/test-llm-service_modal_service.py +48 -0
  70. isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-212906/test-monitoring-gpt2_modal_service.py +48 -0
  71. isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-213009/test-monitoring-gpt2_modal_service.py +48 -0
  72. isa_model/deployment/storage/__init__.py +5 -0
  73. isa_model/deployment/storage/deployment_repository.py +824 -0
  74. isa_model/deployment/triton/__init__.py +10 -0
  75. isa_model/deployment/triton/config.py +196 -0
  76. isa_model/deployment/triton/configs/__init__.py +1 -0
  77. isa_model/deployment/triton/provider.py +512 -0
  78. isa_model/deployment/triton/scripts/__init__.py +1 -0
  79. isa_model/deployment/triton/templates/__init__.py +1 -0
  80. isa_model/inference/__init__.py +47 -1
  81. isa_model/inference/ai_factory.py +179 -16
  82. isa_model/inference/legacy_services/__init__.py +21 -0
  83. isa_model/inference/legacy_services/model_evaluation.py +637 -0
  84. isa_model/inference/legacy_services/model_service.py +573 -0
  85. isa_model/inference/legacy_services/model_serving.py +717 -0
  86. isa_model/inference/legacy_services/model_training.py +561 -0
  87. isa_model/inference/models/__init__.py +21 -0
  88. isa_model/inference/models/inference_config.py +551 -0
  89. isa_model/inference/models/inference_record.py +675 -0
  90. isa_model/inference/models/performance_models.py +714 -0
  91. isa_model/inference/repositories/__init__.py +9 -0
  92. isa_model/inference/repositories/inference_repository.py +828 -0
  93. isa_model/inference/services/audio/__init__.py +21 -0
  94. isa_model/inference/services/audio/base_realtime_service.py +225 -0
  95. isa_model/inference/services/audio/base_stt_service.py +184 -11
  96. isa_model/inference/services/audio/isa_tts_service.py +0 -0
  97. isa_model/inference/services/audio/openai_realtime_service.py +320 -124
  98. isa_model/inference/services/audio/openai_stt_service.py +53 -11
  99. isa_model/inference/services/base_service.py +17 -1
  100. isa_model/inference/services/custom_model_manager.py +277 -0
  101. isa_model/inference/services/embedding/__init__.py +13 -0
  102. isa_model/inference/services/embedding/base_embed_service.py +111 -8
  103. isa_model/inference/services/embedding/isa_embed_service.py +305 -0
  104. isa_model/inference/services/embedding/ollama_embed_service.py +15 -3
  105. isa_model/inference/services/embedding/openai_embed_service.py +2 -4
  106. isa_model/inference/services/embedding/resilient_embed_service.py +285 -0
  107. isa_model/inference/services/embedding/tests/test_embedding.py +222 -0
  108. isa_model/inference/services/img/__init__.py +2 -2
  109. isa_model/inference/services/img/base_image_gen_service.py +24 -7
  110. isa_model/inference/services/img/replicate_image_gen_service.py +84 -422
  111. isa_model/inference/services/img/services/replicate_face_swap.py +193 -0
  112. isa_model/inference/services/img/services/replicate_flux.py +226 -0
  113. isa_model/inference/services/img/services/replicate_flux_kontext.py +219 -0
  114. isa_model/inference/services/img/services/replicate_sticker_maker.py +249 -0
  115. isa_model/inference/services/img/tests/test_img_client.py +297 -0
  116. isa_model/inference/services/llm/__init__.py +10 -2
  117. isa_model/inference/services/llm/base_llm_service.py +361 -26
  118. isa_model/inference/services/llm/cerebras_llm_service.py +628 -0
  119. isa_model/inference/services/llm/helpers/llm_adapter.py +71 -12
  120. isa_model/inference/services/llm/helpers/llm_prompts.py +342 -0
  121. isa_model/inference/services/llm/helpers/llm_utils.py +321 -23
  122. isa_model/inference/services/llm/huggingface_llm_service.py +581 -0
  123. isa_model/inference/services/llm/local_llm_service.py +747 -0
  124. isa_model/inference/services/llm/ollama_llm_service.py +11 -3
  125. isa_model/inference/services/llm/openai_llm_service.py +670 -56
  126. isa_model/inference/services/llm/yyds_llm_service.py +10 -3
  127. isa_model/inference/services/vision/__init__.py +27 -6
  128. isa_model/inference/services/vision/base_vision_service.py +118 -185
  129. isa_model/inference/services/vision/blip_vision_service.py +359 -0
  130. isa_model/inference/services/vision/helpers/image_utils.py +19 -10
  131. isa_model/inference/services/vision/isa_vision_service.py +634 -0
  132. isa_model/inference/services/vision/openai_vision_service.py +19 -10
  133. isa_model/inference/services/vision/tests/test_ocr_client.py +284 -0
  134. isa_model/inference/services/vision/vgg16_vision_service.py +257 -0
  135. isa_model/serving/api/cache_manager.py +245 -0
  136. isa_model/serving/api/dependencies/__init__.py +1 -0
  137. isa_model/serving/api/dependencies/auth.py +194 -0
  138. isa_model/serving/api/dependencies/database.py +139 -0
  139. isa_model/serving/api/error_handlers.py +284 -0
  140. isa_model/serving/api/fastapi_server.py +240 -18
  141. isa_model/serving/api/middleware/auth.py +317 -0
  142. isa_model/serving/api/middleware/security.py +268 -0
  143. isa_model/serving/api/middleware/tenant_context.py +414 -0
  144. isa_model/serving/api/routes/analytics.py +489 -0
  145. isa_model/serving/api/routes/config.py +645 -0
  146. isa_model/serving/api/routes/deployment_billing.py +315 -0
  147. isa_model/serving/api/routes/deployments.py +475 -0
  148. isa_model/serving/api/routes/gpu_gateway.py +440 -0
  149. isa_model/serving/api/routes/health.py +32 -12
  150. isa_model/serving/api/routes/inference_monitoring.py +486 -0
  151. isa_model/serving/api/routes/local_deployments.py +448 -0
  152. isa_model/serving/api/routes/logs.py +430 -0
  153. isa_model/serving/api/routes/settings.py +582 -0
  154. isa_model/serving/api/routes/tenants.py +575 -0
  155. isa_model/serving/api/routes/unified.py +992 -171
  156. isa_model/serving/api/routes/webhooks.py +479 -0
  157. isa_model/serving/api/startup.py +318 -0
  158. isa_model/serving/modal_proxy_server.py +249 -0
  159. isa_model/utils/gpu_utils.py +311 -0
  160. {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/METADATA +76 -22
  161. isa_model-0.4.3.dist-info/RECORD +193 -0
  162. isa_model/deployment/cloud/__init__.py +0 -9
  163. isa_model/deployment/cloud/modal/__init__.py +0 -10
  164. isa_model/deployment/cloud/modal/isa_vision_doc_service.py +0 -766
  165. isa_model/deployment/cloud/modal/isa_vision_table_service.py +0 -532
  166. isa_model/deployment/cloud/modal/isa_vision_ui_service.py +0 -406
  167. isa_model/deployment/cloud/modal/register_models.py +0 -321
  168. isa_model/deployment/core/deployment_config.py +0 -356
  169. isa_model/deployment/core/isa_deployment_service.py +0 -401
  170. isa_model/deployment/gpu_int8_ds8/app/server.py +0 -66
  171. isa_model/deployment/gpu_int8_ds8/scripts/test_client.py +0 -43
  172. isa_model/deployment/gpu_int8_ds8/scripts/test_client_os.py +0 -35
  173. isa_model/deployment/runtime/deployed_service.py +0 -338
  174. isa_model/deployment/services/__init__.py +0 -9
  175. isa_model/deployment/services/auto_deploy_vision_service.py +0 -538
  176. isa_model/deployment/services/model_service.py +0 -332
  177. isa_model/deployment/services/service_monitor.py +0 -356
  178. isa_model/deployment/services/service_registry.py +0 -527
  179. isa_model/eval/__init__.py +0 -92
  180. isa_model/eval/benchmarks.py +0 -469
  181. isa_model/eval/config/__init__.py +0 -10
  182. isa_model/eval/config/evaluation_config.py +0 -108
  183. isa_model/eval/evaluators/__init__.py +0 -18
  184. isa_model/eval/evaluators/base_evaluator.py +0 -503
  185. isa_model/eval/evaluators/llm_evaluator.py +0 -472
  186. isa_model/eval/factory.py +0 -531
  187. isa_model/eval/infrastructure/__init__.py +0 -24
  188. isa_model/eval/infrastructure/experiment_tracker.py +0 -466
  189. isa_model/eval/metrics.py +0 -798
  190. isa_model/inference/adapter/unified_api.py +0 -248
  191. isa_model/inference/services/helpers/stacked_config.py +0 -148
  192. isa_model/inference/services/img/flux_professional_service.py +0 -603
  193. isa_model/inference/services/img/helpers/base_stacked_service.py +0 -274
  194. isa_model/inference/services/others/table_transformer_service.py +0 -61
  195. isa_model/inference/services/vision/doc_analysis_service.py +0 -640
  196. isa_model/inference/services/vision/helpers/base_stacked_service.py +0 -274
  197. isa_model/inference/services/vision/ui_analysis_service.py +0 -823
  198. isa_model/scripts/inference_tracker.py +0 -283
  199. isa_model/scripts/mlflow_manager.py +0 -379
  200. isa_model/scripts/model_registry.py +0 -465
  201. isa_model/scripts/register_models.py +0 -370
  202. isa_model/scripts/register_models_with_embeddings.py +0 -510
  203. isa_model/scripts/start_mlflow.py +0 -95
  204. isa_model/scripts/training_tracker.py +0 -257
  205. isa_model/training/__init__.py +0 -74
  206. isa_model/training/annotation/annotation_schema.py +0 -47
  207. isa_model/training/annotation/processors/annotation_processor.py +0 -126
  208. isa_model/training/annotation/storage/dataset_manager.py +0 -131
  209. isa_model/training/annotation/storage/dataset_schema.py +0 -44
  210. isa_model/training/annotation/tests/test_annotation_flow.py +0 -109
  211. isa_model/training/annotation/tests/test_minio copy.py +0 -113
  212. isa_model/training/annotation/tests/test_minio_upload.py +0 -43
  213. isa_model/training/annotation/views/annotation_controller.py +0 -158
  214. isa_model/training/cloud/__init__.py +0 -22
  215. isa_model/training/cloud/job_orchestrator.py +0 -402
  216. isa_model/training/cloud/runpod_trainer.py +0 -454
  217. isa_model/training/cloud/storage_manager.py +0 -482
  218. isa_model/training/core/__init__.py +0 -23
  219. isa_model/training/core/config.py +0 -181
  220. isa_model/training/core/dataset.py +0 -222
  221. isa_model/training/core/trainer.py +0 -720
  222. isa_model/training/core/utils.py +0 -213
  223. isa_model/training/factory.py +0 -424
  224. isa_model-0.3.91.dist-info/RECORD +0 -138
  225. /isa_model/{core/storage/minio_storage.py → deployment/modal/services/audio/isa_audio_fish_service.py} +0 -0
  226. /isa_model/deployment/{services → modal/services/vision}/simple_auto_deploy_vision_service.py +0 -0
  227. {isa_model-0.3.91.dist-info → isa_model-0.4.3.dist-info}/WHEEL +0 -0
  228. {isa_model-0.3.91.dist-info → isa_model-0.4.3.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
+ })