cortexdb-connectors 0.2.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.
@@ -0,0 +1,252 @@
1
+ """FastAPI router exposing the Insights Engine via HTTP endpoints.
2
+
3
+ Mount this router into an existing FastAPI application::
4
+
5
+ from fastapi import FastAPI
6
+ from cortexdb_connectors.insights.api import create_insights_router
7
+ from cortexdb_connectors.insights.engine import InsightsEngine
8
+
9
+ engine = InsightsEngine(cortex_url="...", cortex_api_key="...")
10
+ app = FastAPI()
11
+ app.include_router(create_insights_router(engine))
12
+
13
+ Endpoints
14
+ ---------
15
+ GET /v1/insights List insights with optional filters.
16
+ GET /v1/insights/{id} Retrieve a single insight by id.
17
+ POST /v1/insights/{id}/dismiss Dismiss an insight.
18
+ GET /v1/insights/status Engine health / status.
19
+ POST /v1/insights/run Trigger an on-demand analysis pass.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from datetime import datetime
25
+ from typing import Any
26
+
27
+ from fastapi import APIRouter, HTTPException, Query
28
+ from pydantic import BaseModel, Field
29
+
30
+ from cortexdb_connectors.insights.detectors import (
31
+ Insight,
32
+ InsightSeverity,
33
+ InsightType,
34
+ )
35
+ from cortexdb_connectors.insights.engine import InsightsEngine
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Response models
40
+ # ---------------------------------------------------------------------------
41
+
42
+ class InsightResponse(BaseModel):
43
+ """Single insight in API responses."""
44
+
45
+ id: str
46
+ insight_type: InsightType
47
+ title: str
48
+ description: str
49
+ severity: InsightSeverity
50
+ entities: list[str]
51
+ detected_at: datetime
52
+ confidence: float
53
+ dismissed: bool
54
+ dismissed_at: datetime | None = None
55
+ metadata: dict[str, Any] = Field(default_factory=dict)
56
+
57
+
58
+ class InsightListResponse(BaseModel):
59
+ """Paginated list of insights."""
60
+
61
+ insights: list[InsightResponse]
62
+ total: int
63
+ limit: int
64
+ offset: int
65
+
66
+
67
+ class DismissResponse(BaseModel):
68
+ """Response after dismissing an insight."""
69
+
70
+ id: str
71
+ dismissed: bool
72
+ dismissed_at: datetime | None
73
+
74
+
75
+ class StatusResponse(BaseModel):
76
+ """Engine status response."""
77
+
78
+ running: bool
79
+ interval_seconds: int
80
+ detector_count: int
81
+ insight_count: int
82
+ active_insight_count: int
83
+ last_run_at: str | None
84
+ run_count: int
85
+
86
+
87
+ class RunResponse(BaseModel):
88
+ """Response from triggering an on-demand analysis pass."""
89
+
90
+ new_insights: int
91
+ total_insights: int
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Helpers
96
+ # ---------------------------------------------------------------------------
97
+
98
+ def _insight_to_response(insight: Insight) -> InsightResponse:
99
+ """Convert an internal Insight model to an API response model."""
100
+ return InsightResponse(
101
+ id=insight.id,
102
+ insight_type=insight.insight_type,
103
+ title=insight.title,
104
+ description=insight.description,
105
+ severity=insight.severity,
106
+ entities=insight.entities,
107
+ detected_at=insight.detected_at,
108
+ confidence=insight.confidence,
109
+ dismissed=insight.dismissed,
110
+ dismissed_at=insight.dismissed_at,
111
+ metadata=insight.metadata,
112
+ )
113
+
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # Router factory
117
+ # ---------------------------------------------------------------------------
118
+
119
+ def create_insights_router(engine: InsightsEngine) -> APIRouter:
120
+ """Create a FastAPI router wired to the given ``InsightsEngine``.
121
+
122
+ Parameters
123
+ ----------
124
+ engine:
125
+ A configured InsightsEngine instance whose in-memory insight
126
+ store will be served through the API.
127
+
128
+ Returns
129
+ -------
130
+ APIRouter
131
+ A router with prefix ``/v1/insights`` ready to be mounted on a
132
+ FastAPI application.
133
+ """
134
+ router = APIRouter(prefix="/v1/insights", tags=["insights"])
135
+
136
+ @router.get(
137
+ "",
138
+ response_model=InsightListResponse,
139
+ summary="List insights",
140
+ description=(
141
+ "Return insights with optional filters for type, severity, "
142
+ "entity, and pagination."
143
+ ),
144
+ )
145
+ async def list_insights(
146
+ type: InsightType | None = Query(
147
+ None,
148
+ description="Filter by insight type",
149
+ alias="type",
150
+ ),
151
+ severity: InsightSeverity | None = Query(
152
+ None, description="Filter by severity level"
153
+ ),
154
+ entity: str | None = Query(
155
+ None,
156
+ description=(
157
+ "Filter to insights mentioning this entity "
158
+ "(case-insensitive substring match)"
159
+ ),
160
+ ),
161
+ include_dismissed: bool = Query(
162
+ False, description="Include dismissed insights"
163
+ ),
164
+ limit: int = Query(50, ge=1, le=500, description="Max results"),
165
+ offset: int = Query(0, ge=0, description="Pagination offset"),
166
+ ) -> InsightListResponse:
167
+ """List insights with optional filters."""
168
+ # Get unfiltered count for the total (with same filters minus pagination).
169
+ all_matching = engine.query_insights(
170
+ insight_type=type,
171
+ severity=severity,
172
+ entity=entity,
173
+ include_dismissed=include_dismissed,
174
+ limit=engine.max_insights,
175
+ offset=0,
176
+ )
177
+ total = len(all_matching)
178
+
179
+ page = engine.query_insights(
180
+ insight_type=type,
181
+ severity=severity,
182
+ entity=entity,
183
+ include_dismissed=include_dismissed,
184
+ limit=limit,
185
+ offset=offset,
186
+ )
187
+
188
+ return InsightListResponse(
189
+ insights=[_insight_to_response(i) for i in page],
190
+ total=total,
191
+ limit=limit,
192
+ offset=offset,
193
+ )
194
+
195
+ @router.get(
196
+ "/status",
197
+ response_model=StatusResponse,
198
+ summary="Engine status",
199
+ description="Return the current status of the insights engine.",
200
+ )
201
+ async def get_status() -> StatusResponse:
202
+ """Return engine health information."""
203
+ return StatusResponse(**engine.status())
204
+
205
+ @router.get(
206
+ "/{insight_id}",
207
+ response_model=InsightResponse,
208
+ summary="Get single insight",
209
+ description="Retrieve a single insight by its id.",
210
+ )
211
+ async def get_insight(insight_id: str) -> InsightResponse:
212
+ """Retrieve a single insight by id."""
213
+ insight = engine.get_insight(insight_id)
214
+ if insight is None:
215
+ raise HTTPException(status_code=404, detail="Insight not found")
216
+ return _insight_to_response(insight)
217
+
218
+ @router.post(
219
+ "/{insight_id}/dismiss",
220
+ response_model=DismissResponse,
221
+ summary="Dismiss an insight",
222
+ description=(
223
+ "Mark an insight as dismissed. Dismissed insights will not "
224
+ "reappear in default list queries."
225
+ ),
226
+ )
227
+ async def dismiss_insight(insight_id: str) -> DismissResponse:
228
+ """Dismiss an insight so it no longer surfaces in default queries."""
229
+ insight = engine.dismiss_insight(insight_id)
230
+ if insight is None:
231
+ raise HTTPException(status_code=404, detail="Insight not found")
232
+ return DismissResponse(
233
+ id=insight.id,
234
+ dismissed=insight.dismissed,
235
+ dismissed_at=insight.dismissed_at,
236
+ )
237
+
238
+ @router.post(
239
+ "/run",
240
+ response_model=RunResponse,
241
+ summary="Trigger analysis",
242
+ description="Run all detectors immediately and return a summary.",
243
+ )
244
+ async def trigger_run() -> RunResponse:
245
+ """Trigger an on-demand analysis pass."""
246
+ new_insights = await engine.run_once()
247
+ return RunResponse(
248
+ new_insights=len(new_insights),
249
+ total_insights=len(engine._insights),
250
+ )
251
+
252
+ return router