truthound-dashboard 1.4.3__py3-none-any.whl → 1.5.0__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 (205) hide show
  1. truthound_dashboard/api/alerts.py +75 -86
  2. truthound_dashboard/api/anomaly.py +7 -13
  3. truthound_dashboard/api/cross_alerts.py +38 -52
  4. truthound_dashboard/api/drift.py +49 -59
  5. truthound_dashboard/api/drift_monitor.py +234 -79
  6. truthound_dashboard/api/enterprise_sampling.py +498 -0
  7. truthound_dashboard/api/history.py +57 -5
  8. truthound_dashboard/api/lineage.py +3 -48
  9. truthound_dashboard/api/maintenance.py +104 -49
  10. truthound_dashboard/api/mask.py +1 -2
  11. truthound_dashboard/api/middleware.py +2 -1
  12. truthound_dashboard/api/model_monitoring.py +435 -311
  13. truthound_dashboard/api/notifications.py +227 -191
  14. truthound_dashboard/api/notifications_advanced.py +21 -20
  15. truthound_dashboard/api/observability.py +586 -0
  16. truthound_dashboard/api/plugins.py +2 -433
  17. truthound_dashboard/api/profile.py +199 -37
  18. truthound_dashboard/api/quality_reporter.py +701 -0
  19. truthound_dashboard/api/reports.py +7 -16
  20. truthound_dashboard/api/router.py +66 -0
  21. truthound_dashboard/api/rule_suggestions.py +5 -5
  22. truthound_dashboard/api/scan.py +17 -19
  23. truthound_dashboard/api/schedules.py +85 -50
  24. truthound_dashboard/api/schema_evolution.py +6 -6
  25. truthound_dashboard/api/schema_watcher.py +667 -0
  26. truthound_dashboard/api/sources.py +98 -27
  27. truthound_dashboard/api/tiering.py +1323 -0
  28. truthound_dashboard/api/triggers.py +14 -11
  29. truthound_dashboard/api/validations.py +12 -11
  30. truthound_dashboard/api/versioning.py +1 -6
  31. truthound_dashboard/core/__init__.py +129 -3
  32. truthound_dashboard/core/actions/__init__.py +62 -0
  33. truthound_dashboard/core/actions/custom.py +426 -0
  34. truthound_dashboard/core/actions/notifications.py +910 -0
  35. truthound_dashboard/core/actions/storage.py +472 -0
  36. truthound_dashboard/core/actions/webhook.py +281 -0
  37. truthound_dashboard/core/anomaly.py +262 -67
  38. truthound_dashboard/core/anomaly_explainer.py +4 -3
  39. truthound_dashboard/core/backends/__init__.py +67 -0
  40. truthound_dashboard/core/backends/base.py +299 -0
  41. truthound_dashboard/core/backends/errors.py +191 -0
  42. truthound_dashboard/core/backends/factory.py +423 -0
  43. truthound_dashboard/core/backends/mock_backend.py +451 -0
  44. truthound_dashboard/core/backends/truthound_backend.py +718 -0
  45. truthound_dashboard/core/checkpoint/__init__.py +87 -0
  46. truthound_dashboard/core/checkpoint/adapters.py +814 -0
  47. truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
  48. truthound_dashboard/core/checkpoint/runner.py +270 -0
  49. truthound_dashboard/core/connections.py +437 -10
  50. truthound_dashboard/core/converters/__init__.py +14 -0
  51. truthound_dashboard/core/converters/truthound.py +620 -0
  52. truthound_dashboard/core/cross_alerts.py +540 -320
  53. truthound_dashboard/core/datasource_factory.py +1672 -0
  54. truthound_dashboard/core/drift_monitor.py +216 -20
  55. truthound_dashboard/core/enterprise_sampling.py +1291 -0
  56. truthound_dashboard/core/interfaces/__init__.py +225 -0
  57. truthound_dashboard/core/interfaces/actions.py +652 -0
  58. truthound_dashboard/core/interfaces/base.py +247 -0
  59. truthound_dashboard/core/interfaces/checkpoint.py +676 -0
  60. truthound_dashboard/core/interfaces/protocols.py +664 -0
  61. truthound_dashboard/core/interfaces/reporters.py +650 -0
  62. truthound_dashboard/core/interfaces/routing.py +646 -0
  63. truthound_dashboard/core/interfaces/triggers.py +619 -0
  64. truthound_dashboard/core/lineage.py +407 -71
  65. truthound_dashboard/core/model_monitoring.py +431 -3
  66. truthound_dashboard/core/notifications/base.py +4 -0
  67. truthound_dashboard/core/notifications/channels.py +501 -1203
  68. truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
  69. truthound_dashboard/core/notifications/deduplication/service.py +131 -348
  70. truthound_dashboard/core/notifications/dispatcher.py +202 -11
  71. truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
  72. truthound_dashboard/core/notifications/escalation/engine.py +168 -358
  73. truthound_dashboard/core/notifications/routing/__init__.py +88 -128
  74. truthound_dashboard/core/notifications/routing/engine.py +90 -317
  75. truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
  76. truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
  77. truthound_dashboard/core/notifications/throttling/builder.py +117 -255
  78. truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
  79. truthound_dashboard/core/phase5/collaboration.py +1 -1
  80. truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
  81. truthound_dashboard/core/quality_reporter.py +1359 -0
  82. truthound_dashboard/core/report_history.py +0 -6
  83. truthound_dashboard/core/reporters/__init__.py +175 -14
  84. truthound_dashboard/core/reporters/adapters.py +943 -0
  85. truthound_dashboard/core/reporters/base.py +0 -3
  86. truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
  87. truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
  88. truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
  89. truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
  90. truthound_dashboard/core/reporters/compat.py +266 -0
  91. truthound_dashboard/core/reporters/csv_reporter.py +2 -35
  92. truthound_dashboard/core/reporters/factory.py +526 -0
  93. truthound_dashboard/core/reporters/interfaces.py +745 -0
  94. truthound_dashboard/core/reporters/registry.py +1 -10
  95. truthound_dashboard/core/scheduler.py +165 -0
  96. truthound_dashboard/core/schema_evolution.py +3 -3
  97. truthound_dashboard/core/schema_watcher.py +1528 -0
  98. truthound_dashboard/core/services.py +595 -76
  99. truthound_dashboard/core/store_manager.py +810 -0
  100. truthound_dashboard/core/streaming_anomaly.py +169 -4
  101. truthound_dashboard/core/tiering.py +1309 -0
  102. truthound_dashboard/core/triggers/evaluators.py +178 -8
  103. truthound_dashboard/core/truthound_adapter.py +2620 -197
  104. truthound_dashboard/core/unified_alerts.py +23 -20
  105. truthound_dashboard/db/__init__.py +8 -0
  106. truthound_dashboard/db/database.py +8 -2
  107. truthound_dashboard/db/models.py +944 -25
  108. truthound_dashboard/db/repository.py +2 -0
  109. truthound_dashboard/main.py +11 -0
  110. truthound_dashboard/schemas/__init__.py +177 -16
  111. truthound_dashboard/schemas/base.py +44 -23
  112. truthound_dashboard/schemas/collaboration.py +19 -6
  113. truthound_dashboard/schemas/cross_alerts.py +19 -3
  114. truthound_dashboard/schemas/drift.py +61 -55
  115. truthound_dashboard/schemas/drift_monitor.py +67 -23
  116. truthound_dashboard/schemas/enterprise_sampling.py +653 -0
  117. truthound_dashboard/schemas/lineage.py +0 -33
  118. truthound_dashboard/schemas/mask.py +10 -8
  119. truthound_dashboard/schemas/model_monitoring.py +89 -10
  120. truthound_dashboard/schemas/notifications_advanced.py +13 -0
  121. truthound_dashboard/schemas/observability.py +453 -0
  122. truthound_dashboard/schemas/plugins.py +0 -280
  123. truthound_dashboard/schemas/profile.py +154 -247
  124. truthound_dashboard/schemas/quality_reporter.py +403 -0
  125. truthound_dashboard/schemas/reports.py +2 -2
  126. truthound_dashboard/schemas/rule_suggestion.py +8 -1
  127. truthound_dashboard/schemas/scan.py +4 -24
  128. truthound_dashboard/schemas/schedule.py +11 -3
  129. truthound_dashboard/schemas/schema_watcher.py +727 -0
  130. truthound_dashboard/schemas/source.py +17 -2
  131. truthound_dashboard/schemas/tiering.py +822 -0
  132. truthound_dashboard/schemas/triggers.py +16 -0
  133. truthound_dashboard/schemas/unified_alerts.py +7 -0
  134. truthound_dashboard/schemas/validation.py +0 -13
  135. truthound_dashboard/schemas/validators/base.py +41 -21
  136. truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
  137. truthound_dashboard/schemas/validators/localization_validators.py +273 -0
  138. truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
  139. truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
  140. truthound_dashboard/schemas/validators/referential_validators.py +312 -0
  141. truthound_dashboard/schemas/validators/registry.py +93 -8
  142. truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
  143. truthound_dashboard/schemas/versioning.py +1 -6
  144. truthound_dashboard/static/index.html +2 -2
  145. truthound_dashboard-1.5.0.dist-info/METADATA +309 -0
  146. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/RECORD +149 -148
  147. truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
  148. truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
  149. truthound_dashboard/core/plugins/hooks/manager.py +0 -403
  150. truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
  151. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
  152. truthound_dashboard/core/reporters/junit_reporter.py +0 -233
  153. truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
  154. truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
  155. truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
  156. truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
  157. truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
  158. truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
  159. truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
  160. truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
  161. truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
  162. truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
  163. truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
  164. truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
  165. truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
  166. truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
  167. truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
  168. truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
  169. truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
  170. truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
  171. truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
  172. truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
  173. truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
  174. truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
  175. truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
  176. truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
  177. truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
  178. truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
  179. truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
  180. truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
  181. truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
  182. truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
  183. truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
  184. truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
  185. truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
  186. truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
  187. truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
  188. truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
  189. truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
  190. truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
  191. truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
  192. truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
  193. truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
  194. truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
  195. truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
  196. truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
  197. truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
  198. truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
  199. truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
  200. truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
  201. truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
  202. truthound_dashboard-1.4.3.dist-info/METADATA +0 -505
  203. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
  205. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -13,12 +13,12 @@ from fastapi import APIRouter, Depends, HTTPException, Query
13
13
  from sqlalchemy.ext.asyncio import AsyncSession
14
14
 
15
15
  from ..core.unified_alerts import UnifiedAlertsService
16
- from ..db import get_session
17
- from ..schemas.base import DataResponse
16
+ from ..db import get_db_session
18
17
  from ..schemas.unified_alerts import (
19
18
  AcknowledgeAlertRequest,
20
19
  AlertCorrelation,
21
20
  AlertCorrelationResponse,
21
+ AlertCountResponse,
22
22
  AlertSeverity,
23
23
  AlertSource,
24
24
  AlertStatus,
@@ -33,7 +33,7 @@ from ..schemas.unified_alerts import (
33
33
  router = APIRouter(prefix="/alerts", tags=["alerts"])
34
34
 
35
35
 
36
- def get_service(session: AsyncSession = Depends(get_session)) -> UnifiedAlertsService:
36
+ def get_service(session: AsyncSession = Depends(get_db_session)) -> UnifiedAlertsService:
37
37
  """Get unified alerts service instance."""
38
38
  return UnifiedAlertsService(session)
39
39
 
@@ -43,7 +43,7 @@ def get_service(session: AsyncSession = Depends(get_session)) -> UnifiedAlertsSe
43
43
  # =============================================================================
44
44
 
45
45
 
46
- @router.get("", response_model=DataResponse[UnifiedAlertListResponse])
46
+ @router.get("", response_model=UnifiedAlertListResponse)
47
47
  async def list_alerts(
48
48
  source: AlertSource | None = None,
49
49
  severity: AlertSeverity | None = None,
@@ -53,7 +53,7 @@ async def list_alerts(
53
53
  offset: int = Query(0, ge=0),
54
54
  limit: int = Query(50, ge=1, le=200),
55
55
  service: UnifiedAlertsService = Depends(get_service),
56
- ):
56
+ ) -> UnifiedAlertListResponse:
57
57
  """List all unified alerts from all sources.
58
58
 
59
59
  Aggregates alerts from:
@@ -72,34 +72,31 @@ async def list_alerts(
72
72
  limit=limit,
73
73
  )
74
74
 
75
- return DataResponse(
76
- data=UnifiedAlertListResponse(
77
- items=alerts,
78
- total=total,
79
- offset=offset,
80
- limit=limit,
81
- )
75
+ return UnifiedAlertListResponse(
76
+ items=alerts,
77
+ total=total,
78
+ offset=offset,
79
+ limit=limit,
82
80
  )
83
81
 
84
82
 
85
- @router.get("/summary", response_model=DataResponse[AlertSummary])
83
+ @router.get("/summary", response_model=AlertSummary)
86
84
  async def get_alert_summary(
87
85
  time_range_hours: int = Query(24, ge=1, le=720, description="Time range for summary"),
88
86
  service: UnifiedAlertsService = Depends(get_service),
89
- ):
87
+ ) -> AlertSummary:
90
88
  """Get alert summary statistics.
91
89
 
92
90
  Returns counts by severity, source, status, and trend data.
93
91
  """
94
- summary = await service.get_alert_summary(time_range_hours=time_range_hours)
95
- return DataResponse(data=summary)
92
+ return await service.get_alert_summary(time_range_hours=time_range_hours)
96
93
 
97
94
 
98
- @router.get("/count")
95
+ @router.get("/count", response_model=AlertCountResponse)
99
96
  async def get_alert_count(
100
97
  status: AlertStatus | None = Query(None, description="Filter by status"),
101
98
  service: UnifiedAlertsService = Depends(get_service),
102
- ):
99
+ ) -> AlertCountResponse:
103
100
  """Get quick alert count (for badges).
104
101
 
105
102
  Returns just the count of alerts matching the criteria.
@@ -110,25 +107,66 @@ async def get_alert_count(
110
107
  limit=1, # We only need the count
111
108
  )
112
109
 
113
- return DataResponse(
114
- data={
115
- "count": total,
116
- "status_filter": status.value if status else "all",
117
- }
110
+ return AlertCountResponse(
111
+ count=total,
112
+ status_filter=status.value if status else "all",
118
113
  )
119
114
 
120
115
 
121
- @router.get("/{alert_id}", response_model=DataResponse[UnifiedAlertResponse])
116
+ @router.get("/{alert_id}", response_model=UnifiedAlertResponse)
122
117
  async def get_alert(
123
118
  alert_id: str,
124
119
  service: UnifiedAlertsService = Depends(get_service),
125
- ):
120
+ ) -> UnifiedAlertResponse:
126
121
  """Get a specific alert by unified ID."""
127
122
  alert = await service.get_alert_by_id(alert_id)
128
123
  if not alert:
129
124
  raise HTTPException(status_code=404, detail="Alert not found")
130
125
 
131
- return DataResponse(data=alert)
126
+ return alert
127
+
128
+
129
+ # =============================================================================
130
+ # Bulk Action Endpoints (must be before /{alert_id} routes to avoid path conflict)
131
+ # =============================================================================
132
+
133
+
134
+ @router.post("/bulk/acknowledge", response_model=BulkAlertActionResponse)
135
+ async def bulk_acknowledge_alerts(
136
+ request: BulkAlertActionRequest,
137
+ service: UnifiedAlertsService = Depends(get_service),
138
+ ) -> BulkAlertActionResponse:
139
+ """Bulk acknowledge multiple alerts."""
140
+ success, failed, failed_ids = await service.bulk_acknowledge(
141
+ request.alert_ids,
142
+ request.actor,
143
+ request.message,
144
+ )
145
+
146
+ return BulkAlertActionResponse(
147
+ success_count=success,
148
+ failed_count=failed,
149
+ failed_ids=failed_ids,
150
+ )
151
+
152
+
153
+ @router.post("/bulk/resolve", response_model=BulkAlertActionResponse)
154
+ async def bulk_resolve_alerts(
155
+ request: BulkAlertActionRequest,
156
+ service: UnifiedAlertsService = Depends(get_service),
157
+ ) -> BulkAlertActionResponse:
158
+ """Bulk resolve multiple alerts."""
159
+ success, failed, failed_ids = await service.bulk_resolve(
160
+ request.alert_ids,
161
+ request.actor,
162
+ request.message,
163
+ )
164
+
165
+ return BulkAlertActionResponse(
166
+ success_count=success,
167
+ failed_count=failed,
168
+ failed_ids=failed_ids,
169
+ )
132
170
 
133
171
 
134
172
  # =============================================================================
@@ -136,12 +174,12 @@ async def get_alert(
136
174
  # =============================================================================
137
175
 
138
176
 
139
- @router.post("/{alert_id}/acknowledge", response_model=DataResponse[UnifiedAlertResponse])
177
+ @router.post("/{alert_id}/acknowledge", response_model=UnifiedAlertResponse)
140
178
  async def acknowledge_alert(
141
179
  alert_id: str,
142
180
  request: AcknowledgeAlertRequest,
143
181
  service: UnifiedAlertsService = Depends(get_service),
144
- ):
182
+ ) -> UnifiedAlertResponse:
145
183
  """Acknowledge an alert.
146
184
 
147
185
  Note: Not all alert types support acknowledgement.
@@ -154,15 +192,15 @@ async def acknowledge_alert(
154
192
  detail="Alert not found or not eligible for acknowledgement",
155
193
  )
156
194
 
157
- return DataResponse(data=alert)
195
+ return alert
158
196
 
159
197
 
160
- @router.post("/{alert_id}/resolve", response_model=DataResponse[UnifiedAlertResponse])
198
+ @router.post("/{alert_id}/resolve", response_model=UnifiedAlertResponse)
161
199
  async def resolve_alert(
162
200
  alert_id: str,
163
201
  request: ResolveAlertRequest,
164
202
  service: UnifiedAlertsService = Depends(get_service),
165
- ):
203
+ ) -> UnifiedAlertResponse:
166
204
  """Resolve an alert.
167
205
 
168
206
  Note: Not all alert types support resolution.
@@ -175,54 +213,7 @@ async def resolve_alert(
175
213
  detail="Alert not found or not eligible for resolution",
176
214
  )
177
215
 
178
- return DataResponse(data=alert)
179
-
180
-
181
- # =============================================================================
182
- # Bulk Action Endpoints
183
- # =============================================================================
184
-
185
-
186
- @router.post("/bulk/acknowledge", response_model=DataResponse[BulkAlertActionResponse])
187
- async def bulk_acknowledge_alerts(
188
- request: BulkAlertActionRequest,
189
- service: UnifiedAlertsService = Depends(get_service),
190
- ):
191
- """Bulk acknowledge multiple alerts."""
192
- success, failed, failed_ids = await service.bulk_acknowledge(
193
- request.alert_ids,
194
- request.actor,
195
- request.message,
196
- )
197
-
198
- return DataResponse(
199
- data=BulkAlertActionResponse(
200
- success_count=success,
201
- failed_count=failed,
202
- failed_ids=failed_ids,
203
- )
204
- )
205
-
206
-
207
- @router.post("/bulk/resolve", response_model=DataResponse[BulkAlertActionResponse])
208
- async def bulk_resolve_alerts(
209
- request: BulkAlertActionRequest,
210
- service: UnifiedAlertsService = Depends(get_service),
211
- ):
212
- """Bulk resolve multiple alerts."""
213
- success, failed, failed_ids = await service.bulk_resolve(
214
- request.alert_ids,
215
- request.actor,
216
- request.message,
217
- )
218
-
219
- return DataResponse(
220
- data=BulkAlertActionResponse(
221
- success_count=success,
222
- failed_count=failed,
223
- failed_ids=failed_ids,
224
- )
225
- )
216
+ return alert
226
217
 
227
218
 
228
219
  # =============================================================================
@@ -230,12 +221,12 @@ async def bulk_resolve_alerts(
230
221
  # =============================================================================
231
222
 
232
223
 
233
- @router.get("/{alert_id}/correlations", response_model=DataResponse[AlertCorrelationResponse])
224
+ @router.get("/{alert_id}/correlations", response_model=AlertCorrelationResponse)
234
225
  async def get_alert_correlations(
235
226
  alert_id: str,
236
227
  time_window_hours: int = Query(1, ge=1, le=24, description="Correlation time window"),
237
228
  service: UnifiedAlertsService = Depends(get_service),
238
- ):
229
+ ) -> AlertCorrelationResponse:
239
230
  """Get correlated alerts for a given alert.
240
231
 
241
232
  Finds alerts that are related by:
@@ -250,9 +241,7 @@ async def get_alert_correlations(
250
241
 
251
242
  total_correlated = sum(len(c.related_alerts) for c in correlations)
252
243
 
253
- return DataResponse(
254
- data=AlertCorrelationResponse(
255
- correlations=correlations,
256
- total_correlated=total_correlated,
257
- )
244
+ return AlertCorrelationResponse(
245
+ correlations=correlations,
246
+ total_correlated=total_correlated,
258
247
  )
@@ -115,7 +115,7 @@ async def run_anomaly_detection(
115
115
 
116
116
 
117
117
  @router.get(
118
- "/anomaly/{detection_id}",
118
+ "/anomaly/detection/{detection_id}",
119
119
  response_model=AnomalyDetectionResponse,
120
120
  summary="Get detection result",
121
121
  description="Get a specific anomaly detection result by ID",
@@ -182,14 +182,14 @@ async def list_detections(
182
182
 
183
183
  @router.get(
184
184
  "/sources/{source_id}/anomaly/latest",
185
- response_model=AnomalyDetectionResponse,
185
+ response_model=AnomalyDetectionResponse | None,
186
186
  summary="Get latest detection",
187
187
  description="Get the latest anomaly detection result for a source",
188
188
  )
189
189
  async def get_latest_detection(
190
190
  service: AnomalyDetectionServiceDep,
191
191
  source_id: Annotated[str, Path(description="Source ID")],
192
- ) -> AnomalyDetectionResponse:
192
+ ) -> AnomalyDetectionResponse | None:
193
193
  """Get the latest detection for a source.
194
194
 
195
195
  Args:
@@ -197,17 +197,11 @@ async def get_latest_detection(
197
197
  source_id: Source ID.
198
198
 
199
199
  Returns:
200
- Latest detection result.
201
-
202
- Raises:
203
- HTTPException: 404 if no detections found.
200
+ Latest detection result, or None if no detections found.
204
201
  """
205
202
  detection = await service.get_latest_detection(source_id)
206
203
  if detection is None:
207
- raise HTTPException(
208
- status_code=404,
209
- detail="No detections found for this source",
210
- )
204
+ return None
211
205
  return _detection_to_response(detection)
212
206
 
213
207
 
@@ -246,7 +240,7 @@ async def list_algorithms(
246
240
 
247
241
 
248
242
  @router.post(
249
- "/anomaly/{detection_id}/explain",
243
+ "/anomaly/detection/{detection_id}/explain",
250
244
  response_model=ExplainabilityResponse,
251
245
  summary="Generate anomaly explanations",
252
246
  description="Generate SHAP/LIME explanations for specific anomaly rows",
@@ -314,7 +308,7 @@ async def explain_anomaly(
314
308
 
315
309
 
316
310
  @router.get(
317
- "/anomaly/{detection_id}/explanations",
311
+ "/anomaly/detection/{detection_id}/explanations",
318
312
  response_model=CachedExplanationsListResponse,
319
313
  summary="Get cached explanations",
320
314
  description="Get cached SHAP/LIME explanations for a detection",
@@ -2,11 +2,17 @@
2
2
 
3
3
  This module provides REST API endpoints for cross-feature integration
4
4
  between Anomaly Detection and Drift Monitoring alerts.
5
+
6
+ API Design: Direct Response Style
7
+ - Single resources return the resource directly
8
+ - List endpoints return PaginatedResponse with data, total, offset, limit
9
+ - Errors are handled via HTTPException
10
+ - Success is indicated by HTTP status codes (200, 201, 204)
5
11
  """
6
12
 
7
13
  from __future__ import annotations
8
14
 
9
- from typing import Annotated
15
+ from typing import Annotated, Any
10
16
 
11
17
  from fastapi import APIRouter, Depends, HTTPException, Query
12
18
 
@@ -14,6 +20,7 @@ from truthound_dashboard.core.cross_alerts import CrossAlertService
14
20
  from truthound_dashboard.schemas.cross_alerts import (
15
21
  CrossAlertCorrelation,
16
22
  CrossAlertCorrelationListResponse,
23
+ CorrelationSearchResult,
17
24
  AutoTriggerConfig,
18
25
  AutoTriggerConfigCreate,
19
26
  AutoTriggerConfigUpdate,
@@ -70,7 +77,6 @@ async def get_correlations(
70
77
  )
71
78
 
72
79
  return CrossAlertCorrelationListResponse(
73
- success=True,
74
80
  data=[CrossAlertCorrelation(**c) for c in correlations],
75
81
  total=total,
76
82
  offset=offset,
@@ -80,7 +86,7 @@ async def get_correlations(
80
86
 
81
87
  @router.get(
82
88
  "/cross-alerts/correlations/{source_id}",
83
- response_model=dict,
89
+ response_model=CorrelationSearchResult,
84
90
  summary="Find correlations for source",
85
91
  description="Find correlated anomaly and drift alerts for a specific source.",
86
92
  )
@@ -89,7 +95,7 @@ async def find_correlations_for_source(
89
95
  service: CrossAlertServiceDep,
90
96
  time_window_hours: int = Query(24, ge=1, le=168, description="Time window in hours"),
91
97
  limit: int = Query(50, ge=1, le=100, description="Maximum items to return"),
92
- ) -> dict:
98
+ ) -> CorrelationSearchResult:
93
99
  """Find correlations for a specific source.
94
100
 
95
101
  This actively searches for correlations between recent anomaly
@@ -102,7 +108,7 @@ async def find_correlations_for_source(
102
108
  limit: Maximum correlations to return.
103
109
 
104
110
  Returns:
105
- Dictionary with correlations found.
111
+ Search result with found correlations.
106
112
  """
107
113
  correlations = await service.correlate_anomaly_drift(
108
114
  source_id=source_id,
@@ -110,11 +116,10 @@ async def find_correlations_for_source(
110
116
  limit=limit,
111
117
  )
112
118
 
113
- return {
114
- "success": True,
115
- "data": correlations,
116
- "total": len(correlations),
117
- }
119
+ return CorrelationSearchResult(
120
+ correlations=[CrossAlertCorrelation(**c) for c in correlations],
121
+ total=len(correlations),
122
+ )
118
123
 
119
124
 
120
125
  # =============================================================================
@@ -124,14 +129,14 @@ async def find_correlations_for_source(
124
129
 
125
130
  @router.get(
126
131
  "/cross-alerts/config",
127
- response_model=dict,
132
+ response_model=AutoTriggerConfig,
128
133
  summary="Get auto-trigger config",
129
134
  description="Get auto-trigger configuration (global or source-specific).",
130
135
  )
131
136
  async def get_config(
132
137
  service: CrossAlertServiceDep,
133
138
  source_id: str | None = Query(None, description="Source ID for source-specific config"),
134
- ) -> dict:
139
+ ) -> AutoTriggerConfig:
135
140
  """Get auto-trigger configuration.
136
141
 
137
142
  Args:
@@ -139,18 +144,15 @@ async def get_config(
139
144
  source_id: Optional source ID for source-specific config.
140
145
 
141
146
  Returns:
142
- Configuration dictionary.
147
+ Configuration object.
143
148
  """
144
- config = service.get_config(source_id)
145
- return {
146
- "success": True,
147
- "data": config,
148
- }
149
+ config = await service.get_config(source_id)
150
+ return AutoTriggerConfig(**config)
149
151
 
150
152
 
151
153
  @router.post(
152
154
  "/cross-alerts/config",
153
- response_model=dict,
155
+ response_model=AutoTriggerConfig,
154
156
  status_code=201,
155
157
  summary="Create/update auto-trigger config",
156
158
  description="Create or update auto-trigger configuration.",
@@ -158,7 +160,7 @@ async def get_config(
158
160
  async def update_config(
159
161
  request: AutoTriggerConfigCreate,
160
162
  service: CrossAlertServiceDep,
161
- ) -> dict:
163
+ ) -> AutoTriggerConfig:
162
164
  """Create or update auto-trigger configuration.
163
165
 
164
166
  Args:
@@ -171,17 +173,14 @@ async def update_config(
171
173
  update_data = request.model_dump(exclude_unset=True)
172
174
  source_id = update_data.pop("source_id", None)
173
175
 
174
- config = service.update_config(source_id, **update_data)
176
+ config = await service.update_config(source_id, **update_data)
175
177
 
176
- return {
177
- "success": True,
178
- "data": config,
179
- }
178
+ return AutoTriggerConfig(**config)
180
179
 
181
180
 
182
181
  @router.put(
183
182
  "/cross-alerts/config",
184
- response_model=dict,
183
+ response_model=AutoTriggerConfig,
185
184
  summary="Update auto-trigger config",
186
185
  description="Update existing auto-trigger configuration.",
187
186
  )
@@ -189,7 +188,7 @@ async def patch_config(
189
188
  request: AutoTriggerConfigUpdate,
190
189
  service: CrossAlertServiceDep,
191
190
  source_id: str | None = Query(None, description="Source ID for source-specific config"),
192
- ) -> dict:
191
+ ) -> AutoTriggerConfig:
193
192
  """Update auto-trigger configuration.
194
193
 
195
194
  Args:
@@ -201,12 +200,9 @@ async def patch_config(
201
200
  Updated configuration.
202
201
  """
203
202
  update_data = request.model_dump(exclude_unset=True)
204
- config = service.update_config(source_id, **update_data)
203
+ config = await service.update_config(source_id, **update_data)
205
204
 
206
- return {
207
- "success": True,
208
- "data": config,
209
- }
205
+ return AutoTriggerConfig(**config)
210
206
 
211
207
 
212
208
  # =============================================================================
@@ -244,7 +240,6 @@ async def list_events(
244
240
  )
245
241
 
246
242
  return AutoTriggerEventListResponse(
247
- success=True,
248
243
  data=[AutoTriggerEvent(**e) for e in events],
249
244
  total=total,
250
245
  offset=offset,
@@ -259,14 +254,14 @@ async def list_events(
259
254
 
260
255
  @router.post(
261
256
  "/cross-alerts/trigger/drift-on-anomaly/{detection_id}",
262
- response_model=dict,
257
+ response_model=AutoTriggerEvent,
263
258
  summary="Trigger drift check on anomaly",
264
259
  description="Manually trigger a drift check based on an anomaly detection result.",
265
260
  )
266
261
  async def trigger_drift_on_anomaly(
267
262
  detection_id: str,
268
263
  service: CrossAlertServiceDep,
269
- ) -> dict:
264
+ ) -> AutoTriggerEvent:
270
265
  """Manually trigger drift check after anomaly detection.
271
266
 
272
267
  Args:
@@ -284,22 +279,19 @@ async def trigger_drift_on_anomaly(
284
279
  if not event:
285
280
  raise HTTPException(status_code=404, detail="Detection not found")
286
281
 
287
- return {
288
- "success": True,
289
- "data": event,
290
- }
282
+ return AutoTriggerEvent(**event)
291
283
 
292
284
 
293
285
  @router.post(
294
286
  "/cross-alerts/trigger/anomaly-on-drift/{monitor_id}",
295
- response_model=dict,
287
+ response_model=AutoTriggerEvent,
296
288
  summary="Trigger anomaly check on drift",
297
289
  description="Manually trigger an anomaly check based on drift detection.",
298
290
  )
299
291
  async def trigger_anomaly_on_drift(
300
292
  monitor_id: str,
301
293
  service: CrossAlertServiceDep,
302
- ) -> dict:
294
+ ) -> AutoTriggerEvent:
303
295
  """Manually trigger anomaly check after drift detection.
304
296
 
305
297
  Args:
@@ -317,10 +309,7 @@ async def trigger_anomaly_on_drift(
317
309
  if not event:
318
310
  raise HTTPException(status_code=404, detail="Monitor not found")
319
311
 
320
- return {
321
- "success": True,
322
- "data": event,
323
- }
312
+ return AutoTriggerEvent(**event)
324
313
 
325
314
 
326
315
  # =============================================================================
@@ -330,23 +319,20 @@ async def trigger_anomaly_on_drift(
330
319
 
331
320
  @router.get(
332
321
  "/cross-alerts/summary",
333
- response_model=dict,
322
+ response_model=CrossAlertSummary,
334
323
  summary="Get cross-alert summary",
335
324
  description="Get summary statistics for cross-alert correlations.",
336
325
  )
337
326
  async def get_summary(
338
327
  service: CrossAlertServiceDep,
339
- ) -> dict:
328
+ ) -> CrossAlertSummary:
340
329
  """Get cross-alert summary statistics.
341
330
 
342
331
  Args:
343
332
  service: Injected cross-alert service.
344
333
 
345
334
  Returns:
346
- Summary statistics dictionary.
335
+ Summary statistics.
347
336
  """
348
337
  summary = await service.get_summary()
349
- return {
350
- "success": True,
351
- "data": summary,
352
- }
338
+ return CrossAlertSummary(**summary)