isa-model 0.4.0__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.
- isa_model/client.py +466 -43
- isa_model/core/cache/redis_cache.py +12 -3
- isa_model/core/config/config_manager.py +230 -3
- isa_model/core/config.py +90 -0
- isa_model/core/database/direct_db_client.py +114 -0
- isa_model/core/database/migration_manager.py +563 -0
- isa_model/core/database/migrations.py +21 -1
- isa_model/core/database/supabase_client.py +154 -19
- isa_model/core/dependencies.py +316 -0
- isa_model/core/discovery/__init__.py +19 -0
- isa_model/core/discovery/consul_discovery.py +190 -0
- isa_model/core/logging/__init__.py +54 -0
- isa_model/core/logging/influx_logger.py +523 -0
- isa_model/core/logging/loki_logger.py +160 -0
- isa_model/core/models/__init__.py +27 -18
- isa_model/core/models/config_models.py +625 -0
- isa_model/core/models/deployment_billing_tracker.py +430 -0
- isa_model/core/models/model_manager.py +40 -17
- isa_model/core/models/model_metadata.py +690 -0
- isa_model/core/models/model_repo.py +174 -18
- isa_model/core/models/system_models.py +857 -0
- isa_model/core/repositories/__init__.py +9 -0
- isa_model/core/repositories/config_repository.py +912 -0
- isa_model/core/services/intelligent_model_selector.py +399 -21
- isa_model/core/storage/hf_storage.py +1 -1
- isa_model/core/types.py +1 -0
- isa_model/deployment/__init__.py +5 -48
- isa_model/deployment/core/__init__.py +2 -31
- isa_model/deployment/core/deployment_manager.py +1278 -370
- isa_model/deployment/local/__init__.py +31 -0
- isa_model/deployment/local/config.py +248 -0
- isa_model/deployment/local/gpu_gateway.py +607 -0
- isa_model/deployment/local/health_checker.py +428 -0
- isa_model/deployment/local/provider.py +586 -0
- isa_model/deployment/local/tensorrt_service.py +621 -0
- isa_model/deployment/local/transformers_service.py +644 -0
- isa_model/deployment/local/vllm_service.py +527 -0
- isa_model/deployment/modal/__init__.py +8 -0
- isa_model/deployment/modal/config.py +136 -0
- isa_model/deployment/{services/auto_hf_modal_deployer.py → modal/deployer.py} +1 -1
- isa_model/deployment/modal/services/__init__.py +3 -0
- isa_model/deployment/modal/services/audio/__init__.py +1 -0
- isa_model/deployment/modal/services/embedding/__init__.py +1 -0
- isa_model/deployment/modal/services/llm/__init__.py +1 -0
- isa_model/deployment/modal/services/llm/isa_llm_service.py +424 -0
- isa_model/deployment/modal/services/video/__init__.py +1 -0
- isa_model/deployment/modal/services/vision/__init__.py +1 -0
- isa_model/deployment/models/org-org-acme-corp-tenant-a-service-llm-20250825-225822/tenant-a-service_modal_service.py +48 -0
- isa_model/deployment/models/org-test-org-123-prefix-test-service-llm-20250825-225822/prefix-test-service_modal_service.py +48 -0
- isa_model/deployment/models/test-llm-service-llm-20250825-204442/test-llm-service_modal_service.py +48 -0
- isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-212906/test-monitoring-gpt2_modal_service.py +48 -0
- isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-213009/test-monitoring-gpt2_modal_service.py +48 -0
- isa_model/deployment/storage/__init__.py +5 -0
- isa_model/deployment/storage/deployment_repository.py +824 -0
- isa_model/deployment/triton/__init__.py +10 -0
- isa_model/deployment/triton/config.py +196 -0
- isa_model/deployment/triton/configs/__init__.py +1 -0
- isa_model/deployment/triton/provider.py +512 -0
- isa_model/deployment/triton/scripts/__init__.py +1 -0
- isa_model/deployment/triton/templates/__init__.py +1 -0
- isa_model/inference/__init__.py +47 -1
- isa_model/inference/ai_factory.py +137 -10
- isa_model/inference/legacy_services/__init__.py +21 -0
- isa_model/inference/legacy_services/model_evaluation.py +637 -0
- isa_model/inference/legacy_services/model_service.py +573 -0
- isa_model/inference/legacy_services/model_serving.py +717 -0
- isa_model/inference/legacy_services/model_training.py +561 -0
- isa_model/inference/models/__init__.py +21 -0
- isa_model/inference/models/inference_config.py +551 -0
- isa_model/inference/models/inference_record.py +675 -0
- isa_model/inference/models/performance_models.py +714 -0
- isa_model/inference/repositories/__init__.py +9 -0
- isa_model/inference/repositories/inference_repository.py +828 -0
- isa_model/inference/services/audio/base_stt_service.py +184 -11
- isa_model/inference/services/audio/openai_stt_service.py +22 -6
- isa_model/inference/services/custom_model_manager.py +277 -0
- isa_model/inference/services/embedding/ollama_embed_service.py +15 -3
- isa_model/inference/services/embedding/resilient_embed_service.py +285 -0
- isa_model/inference/services/llm/__init__.py +10 -2
- isa_model/inference/services/llm/base_llm_service.py +335 -24
- isa_model/inference/services/llm/cerebras_llm_service.py +628 -0
- isa_model/inference/services/llm/helpers/llm_adapter.py +9 -4
- isa_model/inference/services/llm/helpers/llm_prompts.py +342 -0
- isa_model/inference/services/llm/helpers/llm_utils.py +321 -23
- isa_model/inference/services/llm/huggingface_llm_service.py +581 -0
- isa_model/inference/services/llm/local_llm_service.py +747 -0
- isa_model/inference/services/llm/ollama_llm_service.py +9 -2
- isa_model/inference/services/llm/openai_llm_service.py +33 -16
- isa_model/inference/services/llm/yyds_llm_service.py +8 -2
- isa_model/inference/services/vision/__init__.py +22 -1
- isa_model/inference/services/vision/blip_vision_service.py +359 -0
- isa_model/inference/services/vision/helpers/image_utils.py +8 -5
- isa_model/inference/services/vision/isa_vision_service.py +65 -4
- isa_model/inference/services/vision/openai_vision_service.py +19 -10
- isa_model/inference/services/vision/vgg16_vision_service.py +257 -0
- isa_model/serving/api/cache_manager.py +245 -0
- isa_model/serving/api/dependencies/__init__.py +1 -0
- isa_model/serving/api/dependencies/auth.py +194 -0
- isa_model/serving/api/dependencies/database.py +139 -0
- isa_model/serving/api/error_handlers.py +284 -0
- isa_model/serving/api/fastapi_server.py +172 -22
- isa_model/serving/api/middleware/auth.py +8 -2
- isa_model/serving/api/middleware/security.py +23 -33
- isa_model/serving/api/middleware/tenant_context.py +414 -0
- isa_model/serving/api/routes/analytics.py +4 -1
- isa_model/serving/api/routes/config.py +645 -0
- isa_model/serving/api/routes/deployment_billing.py +315 -0
- isa_model/serving/api/routes/deployments.py +138 -2
- isa_model/serving/api/routes/gpu_gateway.py +440 -0
- isa_model/serving/api/routes/health.py +32 -12
- isa_model/serving/api/routes/inference_monitoring.py +486 -0
- isa_model/serving/api/routes/local_deployments.py +448 -0
- isa_model/serving/api/routes/tenants.py +575 -0
- isa_model/serving/api/routes/unified.py +680 -18
- isa_model/serving/api/routes/webhooks.py +479 -0
- isa_model/serving/api/startup.py +68 -54
- isa_model/utils/gpu_utils.py +311 -0
- {isa_model-0.4.0.dist-info → isa_model-0.4.3.dist-info}/METADATA +66 -24
- isa_model-0.4.3.dist-info/RECORD +193 -0
- isa_model/core/storage/minio_storage.py +0 -0
- isa_model/deployment/cloud/__init__.py +0 -9
- isa_model/deployment/cloud/modal/__init__.py +0 -10
- isa_model/deployment/core/deployment_config.py +0 -356
- isa_model/deployment/core/isa_deployment_service.py +0 -401
- isa_model/deployment/gpu_int8_ds8/app/server.py +0 -66
- isa_model/deployment/gpu_int8_ds8/scripts/test_client.py +0 -43
- isa_model/deployment/gpu_int8_ds8/scripts/test_client_os.py +0 -35
- isa_model/deployment/runtime/deployed_service.py +0 -338
- isa_model/deployment/services/__init__.py +0 -9
- isa_model/deployment/services/auto_deploy_vision_service.py +0 -538
- isa_model/deployment/services/model_service.py +0 -332
- isa_model/deployment/services/service_monitor.py +0 -356
- isa_model/deployment/services/service_registry.py +0 -527
- isa_model/eval/__init__.py +0 -92
- isa_model/eval/benchmarks/__init__.py +0 -27
- isa_model/eval/benchmarks/multimodal_datasets.py +0 -460
- isa_model/eval/benchmarks.py +0 -701
- isa_model/eval/config/__init__.py +0 -10
- isa_model/eval/config/evaluation_config.py +0 -108
- isa_model/eval/evaluators/__init__.py +0 -24
- isa_model/eval/evaluators/audio_evaluator.py +0 -727
- isa_model/eval/evaluators/base_evaluator.py +0 -503
- isa_model/eval/evaluators/embedding_evaluator.py +0 -742
- isa_model/eval/evaluators/llm_evaluator.py +0 -472
- isa_model/eval/evaluators/vision_evaluator.py +0 -564
- isa_model/eval/example_evaluation.py +0 -395
- isa_model/eval/factory.py +0 -798
- isa_model/eval/infrastructure/__init__.py +0 -24
- isa_model/eval/infrastructure/experiment_tracker.py +0 -466
- isa_model/eval/isa_benchmarks.py +0 -700
- isa_model/eval/isa_integration.py +0 -582
- isa_model/eval/metrics.py +0 -951
- isa_model/eval/tests/unit/test_basic.py +0 -396
- isa_model/serving/api/routes/evaluations.py +0 -579
- isa_model/training/__init__.py +0 -168
- isa_model/training/annotation/annotation_schema.py +0 -47
- isa_model/training/annotation/processors/annotation_processor.py +0 -126
- isa_model/training/annotation/storage/dataset_manager.py +0 -131
- isa_model/training/annotation/storage/dataset_schema.py +0 -44
- isa_model/training/annotation/tests/test_annotation_flow.py +0 -109
- isa_model/training/annotation/tests/test_minio copy.py +0 -113
- isa_model/training/annotation/tests/test_minio_upload.py +0 -43
- isa_model/training/annotation/views/annotation_controller.py +0 -158
- isa_model/training/cloud/__init__.py +0 -22
- isa_model/training/cloud/job_orchestrator.py +0 -402
- isa_model/training/cloud/runpod_trainer.py +0 -454
- isa_model/training/cloud/storage_manager.py +0 -482
- isa_model/training/core/__init__.py +0 -26
- isa_model/training/core/config.py +0 -181
- isa_model/training/core/dataset.py +0 -222
- isa_model/training/core/trainer.py +0 -720
- isa_model/training/core/utils.py +0 -213
- isa_model/training/examples/intelligent_training_example.py +0 -281
- isa_model/training/factory.py +0 -424
- isa_model/training/intelligent/__init__.py +0 -25
- isa_model/training/intelligent/decision_engine.py +0 -643
- isa_model/training/intelligent/intelligent_factory.py +0 -888
- isa_model/training/intelligent/knowledge_base.py +0 -751
- isa_model/training/intelligent/resource_optimizer.py +0 -839
- isa_model/training/intelligent/task_classifier.py +0 -576
- isa_model/training/storage/__init__.py +0 -24
- isa_model/training/storage/core_integration.py +0 -439
- isa_model/training/storage/training_repository.py +0 -552
- isa_model/training/storage/training_storage.py +0 -628
- isa_model-0.4.0.dist-info/RECORD +0 -182
- /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_chatTTS_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_fish_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_openvoice_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_service_v2.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/embedding}/isa_embed_rerank_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/video}/isa_video_hunyuan_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ocr_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_qwen25_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_table_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ui_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ui_service_optimized.py +0 -0
- /isa_model/deployment/{services → modal/services/vision}/simple_auto_deploy_vision_service.py +0 -0
- {isa_model-0.4.0.dist-info → isa_model-0.4.3.dist-info}/WHEEL +0 -0
- {isa_model-0.4.0.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
|
+
})
|