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.
- cortexdb_connectors/__init__.py +40 -0
- cortexdb_connectors/base.py +364 -0
- cortexdb_connectors/cli.py +476 -0
- cortexdb_connectors/confluence/__init__.py +359 -0
- cortexdb_connectors/discord/__init__.py +646 -0
- cortexdb_connectors/github/__init__.py +309 -0
- cortexdb_connectors/gitlab/__init__.py +749 -0
- cortexdb_connectors/google_workspace/__init__.py +1033 -0
- cortexdb_connectors/hubspot/__init__.py +489 -0
- cortexdb_connectors/insights/__init__.py +40 -0
- cortexdb_connectors/insights/api.py +252 -0
- cortexdb_connectors/insights/detectors.py +771 -0
- cortexdb_connectors/insights/engine.py +308 -0
- cortexdb_connectors/intercom/__init__.py +631 -0
- cortexdb_connectors/jira/__init__.py +298 -0
- cortexdb_connectors/linear/__init__.py +705 -0
- cortexdb_connectors/notion/__init__.py +702 -0
- cortexdb_connectors/pagerduty/__init__.py +298 -0
- cortexdb_connectors/salesforce/__init__.py +399 -0
- cortexdb_connectors/servicenow/__init__.py +460 -0
- cortexdb_connectors/slack/__init__.py +331 -0
- cortexdb_connectors/state.py +145 -0
- cortexdb_connectors/teams/__init__.py +656 -0
- cortexdb_connectors/webhooks.py +672 -0
- cortexdb_connectors/worker.py +192 -0
- cortexdb_connectors/zendesk/__init__.py +496 -0
- cortexdb_connectors-0.2.0.dist-info/METADATA +207 -0
- cortexdb_connectors-0.2.0.dist-info/RECORD +31 -0
- cortexdb_connectors-0.2.0.dist-info/WHEEL +5 -0
- cortexdb_connectors-0.2.0.dist-info/entry_points.txt +2 -0
- cortexdb_connectors-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""InsightsEngine -- periodic analysis orchestrator for CortexDB.
|
|
2
|
+
|
|
3
|
+
The engine manages a collection of insight detectors, runs them on a
|
|
4
|
+
configurable schedule, and maintains an in-memory store of generated
|
|
5
|
+
insights that can be queried via the companion FastAPI router.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
engine = InsightsEngine(
|
|
10
|
+
cortex_url="http://localhost:8080",
|
|
11
|
+
cortex_api_key="secret",
|
|
12
|
+
interval_seconds=300,
|
|
13
|
+
)
|
|
14
|
+
# One-shot analysis:
|
|
15
|
+
insights = await engine.run_once()
|
|
16
|
+
|
|
17
|
+
# Periodic background loop (runs until cancelled):
|
|
18
|
+
await engine.start()
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import logging
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from cortexdb_connectors.insights.detectors import (
|
|
29
|
+
BaseDetector,
|
|
30
|
+
CortexAPIClient,
|
|
31
|
+
IncidentSpikeDetector,
|
|
32
|
+
Insight,
|
|
33
|
+
InsightSeverity,
|
|
34
|
+
InsightType,
|
|
35
|
+
KnowledgeGapDetector,
|
|
36
|
+
NewDependencyDetector,
|
|
37
|
+
OwnershipGapDetector,
|
|
38
|
+
StaleRunbookDetector,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class InsightsEngine:
|
|
45
|
+
"""Orchestrates periodic insight detection against CortexDB data.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
cortex_url:
|
|
50
|
+
Base URL of the CortexDB HTTP API (e.g. ``http://localhost:8080``).
|
|
51
|
+
cortex_api_key:
|
|
52
|
+
Bearer token for CortexDB authentication.
|
|
53
|
+
tenant_id:
|
|
54
|
+
CortexDB tenant identifier (default ``"default"``).
|
|
55
|
+
interval_seconds:
|
|
56
|
+
Seconds between analysis passes when running in periodic mode
|
|
57
|
+
(default 300 = 5 minutes).
|
|
58
|
+
detectors:
|
|
59
|
+
Optional list of detector instances. If not provided, all
|
|
60
|
+
built-in detectors are registered with default settings.
|
|
61
|
+
max_insights:
|
|
62
|
+
Maximum number of insights to retain in memory (default 1000).
|
|
63
|
+
Oldest insights beyond this limit are evicted.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
cortex_url: str,
|
|
69
|
+
cortex_api_key: str,
|
|
70
|
+
tenant_id: str = "default",
|
|
71
|
+
interval_seconds: int = 300,
|
|
72
|
+
detectors: list[BaseDetector] | None = None,
|
|
73
|
+
max_insights: int = 1000,
|
|
74
|
+
) -> None:
|
|
75
|
+
self.client = CortexAPIClient(
|
|
76
|
+
base_url=cortex_url,
|
|
77
|
+
api_key=cortex_api_key,
|
|
78
|
+
tenant_id=tenant_id,
|
|
79
|
+
)
|
|
80
|
+
self.interval_seconds = interval_seconds
|
|
81
|
+
self.max_insights = max_insights
|
|
82
|
+
|
|
83
|
+
self._detectors: list[BaseDetector] = detectors or self._default_detectors()
|
|
84
|
+
self._insights: dict[str, Insight] = {}
|
|
85
|
+
self._running = False
|
|
86
|
+
self._task: asyncio.Task[None] | None = None
|
|
87
|
+
self._last_run_at: datetime | None = None
|
|
88
|
+
self._run_count: int = 0
|
|
89
|
+
|
|
90
|
+
# -- Detector management -------------------------------------------------
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _default_detectors() -> list[BaseDetector]:
|
|
94
|
+
"""Return the full set of built-in detectors with default config."""
|
|
95
|
+
return [
|
|
96
|
+
IncidentSpikeDetector(),
|
|
97
|
+
NewDependencyDetector(),
|
|
98
|
+
KnowledgeGapDetector(),
|
|
99
|
+
OwnershipGapDetector(),
|
|
100
|
+
StaleRunbookDetector(),
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
def register_detector(self, detector: BaseDetector) -> None:
|
|
104
|
+
"""Add a custom detector to the engine."""
|
|
105
|
+
self._detectors.append(detector)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def detectors(self) -> list[BaseDetector]:
|
|
109
|
+
"""Return the list of registered detectors."""
|
|
110
|
+
return list(self._detectors)
|
|
111
|
+
|
|
112
|
+
# -- Insight store -------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def insights(self) -> list[Insight]:
|
|
116
|
+
"""Return all stored insights, newest first."""
|
|
117
|
+
return sorted(
|
|
118
|
+
self._insights.values(),
|
|
119
|
+
key=lambda i: i.detected_at,
|
|
120
|
+
reverse=True,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def get_insight(self, insight_id: str) -> Insight | None:
|
|
124
|
+
"""Look up a single insight by id."""
|
|
125
|
+
return self._insights.get(insight_id)
|
|
126
|
+
|
|
127
|
+
def dismiss_insight(self, insight_id: str) -> Insight | None:
|
|
128
|
+
"""Mark an insight as dismissed.
|
|
129
|
+
|
|
130
|
+
Returns the updated insight, or ``None`` if not found.
|
|
131
|
+
"""
|
|
132
|
+
insight = self._insights.get(insight_id)
|
|
133
|
+
if insight is None:
|
|
134
|
+
return None
|
|
135
|
+
insight.dismissed = True
|
|
136
|
+
insight.dismissed_at = datetime.now(timezone.utc)
|
|
137
|
+
return insight
|
|
138
|
+
|
|
139
|
+
def query_insights(
|
|
140
|
+
self,
|
|
141
|
+
*,
|
|
142
|
+
insight_type: InsightType | None = None,
|
|
143
|
+
severity: InsightSeverity | None = None,
|
|
144
|
+
entity: str | None = None,
|
|
145
|
+
include_dismissed: bool = False,
|
|
146
|
+
limit: int = 50,
|
|
147
|
+
offset: int = 0,
|
|
148
|
+
) -> list[Insight]:
|
|
149
|
+
"""Query stored insights with optional filters.
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
insight_type:
|
|
154
|
+
Filter by insight type.
|
|
155
|
+
severity:
|
|
156
|
+
Filter by severity level.
|
|
157
|
+
entity:
|
|
158
|
+
Filter to insights mentioning this entity name (case-insensitive
|
|
159
|
+
substring match).
|
|
160
|
+
include_dismissed:
|
|
161
|
+
Whether to include dismissed insights (default ``False``).
|
|
162
|
+
limit:
|
|
163
|
+
Maximum number of results to return.
|
|
164
|
+
offset:
|
|
165
|
+
Number of results to skip (for pagination).
|
|
166
|
+
"""
|
|
167
|
+
results = self.insights # already sorted newest-first
|
|
168
|
+
|
|
169
|
+
if not include_dismissed:
|
|
170
|
+
results = [i for i in results if not i.dismissed]
|
|
171
|
+
if insight_type is not None:
|
|
172
|
+
results = [i for i in results if i.insight_type == insight_type]
|
|
173
|
+
if severity is not None:
|
|
174
|
+
results = [i for i in results if i.severity == severity]
|
|
175
|
+
if entity is not None:
|
|
176
|
+
entity_lower = entity.lower()
|
|
177
|
+
results = [
|
|
178
|
+
i for i in results
|
|
179
|
+
if any(entity_lower in e.lower() for e in i.entities)
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
return results[offset : offset + limit]
|
|
183
|
+
|
|
184
|
+
def _store_insight(self, insight: Insight) -> None:
|
|
185
|
+
"""Store an insight, merging by id and enforcing the size limit."""
|
|
186
|
+
existing = self._insights.get(insight.id)
|
|
187
|
+
if existing is not None and existing.dismissed:
|
|
188
|
+
# Do not overwrite a dismissed insight with a re-detection.
|
|
189
|
+
return
|
|
190
|
+
self._insights[insight.id] = insight
|
|
191
|
+
|
|
192
|
+
# Evict oldest non-dismissed insights beyond the cap.
|
|
193
|
+
if len(self._insights) > self.max_insights:
|
|
194
|
+
sorted_ids = sorted(
|
|
195
|
+
self._insights,
|
|
196
|
+
key=lambda k: self._insights[k].detected_at,
|
|
197
|
+
)
|
|
198
|
+
excess = len(self._insights) - self.max_insights
|
|
199
|
+
for iid in sorted_ids[:excess]:
|
|
200
|
+
if not self._insights[iid].dismissed:
|
|
201
|
+
del self._insights[iid]
|
|
202
|
+
|
|
203
|
+
# -- Execution -----------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
async def run_once(self) -> list[Insight]:
|
|
206
|
+
"""Execute all detectors once and return newly generated insights.
|
|
207
|
+
|
|
208
|
+
This is safe to call from application code for on-demand analysis.
|
|
209
|
+
"""
|
|
210
|
+
new_insights: list[Insight] = []
|
|
211
|
+
logger.info(
|
|
212
|
+
"InsightsEngine: starting analysis pass (%d detectors)",
|
|
213
|
+
len(self._detectors),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
for detector in self._detectors:
|
|
217
|
+
detector_name = type(detector).__name__
|
|
218
|
+
try:
|
|
219
|
+
detected = await detector.detect(self.client)
|
|
220
|
+
for insight in detected:
|
|
221
|
+
self._store_insight(insight)
|
|
222
|
+
new_insights.append(insight)
|
|
223
|
+
logger.info(
|
|
224
|
+
"InsightsEngine: %s produced %d insight(s)",
|
|
225
|
+
detector_name, len(detected),
|
|
226
|
+
)
|
|
227
|
+
except Exception:
|
|
228
|
+
logger.exception(
|
|
229
|
+
"InsightsEngine: detector %s raised an unexpected error",
|
|
230
|
+
detector_name,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
self._last_run_at = datetime.now(timezone.utc)
|
|
234
|
+
self._run_count += 1
|
|
235
|
+
|
|
236
|
+
logger.info(
|
|
237
|
+
"InsightsEngine: analysis pass complete, %d new insight(s), "
|
|
238
|
+
"%d total stored",
|
|
239
|
+
len(new_insights), len(self._insights),
|
|
240
|
+
)
|
|
241
|
+
return new_insights
|
|
242
|
+
|
|
243
|
+
async def start(self) -> None:
|
|
244
|
+
"""Start the periodic analysis loop.
|
|
245
|
+
|
|
246
|
+
This method blocks until ``stop()`` is called or the task is
|
|
247
|
+
cancelled. It is designed to be run as an ``asyncio.Task``.
|
|
248
|
+
"""
|
|
249
|
+
if self._running:
|
|
250
|
+
logger.warning("InsightsEngine: already running")
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
self._running = True
|
|
254
|
+
logger.info(
|
|
255
|
+
"InsightsEngine: starting periodic loop (interval=%ds)",
|
|
256
|
+
self.interval_seconds,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
while self._running:
|
|
261
|
+
try:
|
|
262
|
+
await self.run_once()
|
|
263
|
+
except Exception:
|
|
264
|
+
logger.exception(
|
|
265
|
+
"InsightsEngine: unhandled error in analysis loop"
|
|
266
|
+
)
|
|
267
|
+
await asyncio.sleep(self.interval_seconds)
|
|
268
|
+
finally:
|
|
269
|
+
self._running = False
|
|
270
|
+
logger.info("InsightsEngine: periodic loop stopped")
|
|
271
|
+
|
|
272
|
+
def start_background(self) -> asyncio.Task[None]:
|
|
273
|
+
"""Launch the periodic loop as a background asyncio task.
|
|
274
|
+
|
|
275
|
+
Returns the created task so the caller can cancel or await it.
|
|
276
|
+
"""
|
|
277
|
+
self._task = asyncio.create_task(self.start(), name="insights-engine")
|
|
278
|
+
return self._task
|
|
279
|
+
|
|
280
|
+
async def stop(self) -> None:
|
|
281
|
+
"""Stop the periodic analysis loop gracefully."""
|
|
282
|
+
self._running = False
|
|
283
|
+
if self._task is not None and not self._task.done():
|
|
284
|
+
self._task.cancel()
|
|
285
|
+
try:
|
|
286
|
+
await self._task
|
|
287
|
+
except asyncio.CancelledError:
|
|
288
|
+
pass
|
|
289
|
+
self._task = None
|
|
290
|
+
logger.info("InsightsEngine: stopped")
|
|
291
|
+
|
|
292
|
+
# -- Status / health -----------------------------------------------------
|
|
293
|
+
|
|
294
|
+
def status(self) -> dict[str, Any]:
|
|
295
|
+
"""Return engine status suitable for health check endpoints."""
|
|
296
|
+
return {
|
|
297
|
+
"running": self._running,
|
|
298
|
+
"interval_seconds": self.interval_seconds,
|
|
299
|
+
"detector_count": len(self._detectors),
|
|
300
|
+
"insight_count": len(self._insights),
|
|
301
|
+
"active_insight_count": sum(
|
|
302
|
+
1 for i in self._insights.values() if not i.dismissed
|
|
303
|
+
),
|
|
304
|
+
"last_run_at": (
|
|
305
|
+
self._last_run_at.isoformat() if self._last_run_at else None
|
|
306
|
+
),
|
|
307
|
+
"run_count": self._run_count,
|
|
308
|
+
}
|