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,489 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HubSpot connector for CortexDB.
|
|
3
|
+
|
|
4
|
+
Polls HubSpot CRM for Contacts, Companies, Deals, Tickets, Notes, Emails,
|
|
5
|
+
Calls, and Tasks, converting them into CortexDB Episodes. Uses the
|
|
6
|
+
``hubspot-api-client`` library for API v3 access.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime, timedelta, timezone
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from cortexdb_connectors.base import (
|
|
16
|
+
Actor,
|
|
17
|
+
CortexConnector,
|
|
18
|
+
EntityRef,
|
|
19
|
+
Episode,
|
|
20
|
+
EpisodeType,
|
|
21
|
+
RawEvent,
|
|
22
|
+
Source,
|
|
23
|
+
Visibility,
|
|
24
|
+
VisibilityLevel,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
_SOURCE = Source(system="hubspot")
|
|
30
|
+
|
|
31
|
+
_DEFAULT_OBJECTS = frozenset(
|
|
32
|
+
["contacts", "companies", "deals", "tickets", "notes", "emails", "calls", "tasks"]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
_OBJECT_TYPE_MAP: dict[str, EpisodeType] = {
|
|
36
|
+
"contacts": EpisodeType.DOCUMENT,
|
|
37
|
+
"companies": EpisodeType.DOCUMENT,
|
|
38
|
+
"deals": EpisodeType.DOCUMENT,
|
|
39
|
+
"tickets": EpisodeType.ISSUE,
|
|
40
|
+
"notes": EpisodeType.MESSAGE,
|
|
41
|
+
"emails": EpisodeType.MESSAGE,
|
|
42
|
+
"calls": EpisodeType.MESSAGE,
|
|
43
|
+
"tasks": EpisodeType.ISSUE,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class HubSpotConnector(CortexConnector):
|
|
48
|
+
"""Connector that syncs HubSpot CRM objects into CortexDB.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
cortex_url:
|
|
53
|
+
Base URL of the CortexDB API.
|
|
54
|
+
cortex_api_key:
|
|
55
|
+
Bearer token for CortexDB authentication.
|
|
56
|
+
access_token:
|
|
57
|
+
HubSpot private app access token.
|
|
58
|
+
objects:
|
|
59
|
+
HubSpot object types to ingest. Defaults to contacts, companies,
|
|
60
|
+
deals, tickets, notes, emails, calls, tasks.
|
|
61
|
+
namespace:
|
|
62
|
+
CortexDB namespace for ingested episodes.
|
|
63
|
+
backfill_days:
|
|
64
|
+
Number of days of historical data to backfill on first sync.
|
|
65
|
+
tenant_id:
|
|
66
|
+
CortexDB tenant identifier.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
connector_name: str = "hubspot"
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
cortex_url: str,
|
|
74
|
+
cortex_api_key: str,
|
|
75
|
+
access_token: str,
|
|
76
|
+
objects: list[str] | None = None,
|
|
77
|
+
namespace: str = "hubspot",
|
|
78
|
+
backfill_days: int = 90,
|
|
79
|
+
tenant_id: str = "default",
|
|
80
|
+
) -> None:
|
|
81
|
+
super().__init__(cortex_url, cortex_api_key, tenant_id)
|
|
82
|
+
self.access_token = access_token
|
|
83
|
+
self.object_filter = set(objects) if objects else set(_DEFAULT_OBJECTS)
|
|
84
|
+
self.namespace = namespace
|
|
85
|
+
self.backfill_days = backfill_days
|
|
86
|
+
self._client: Any = None
|
|
87
|
+
|
|
88
|
+
# -- HubSpot client management -------------------------------------------
|
|
89
|
+
|
|
90
|
+
def _get_client(self) -> Any:
|
|
91
|
+
"""Lazy-initialise and return the HubSpot API client."""
|
|
92
|
+
if self._client is not None:
|
|
93
|
+
return self._client
|
|
94
|
+
|
|
95
|
+
from hubspot import HubSpot
|
|
96
|
+
|
|
97
|
+
self._client = HubSpot(access_token=self.access_token)
|
|
98
|
+
return self._client
|
|
99
|
+
|
|
100
|
+
# -- CortexConnector interface -------------------------------------------
|
|
101
|
+
|
|
102
|
+
async def fetch_events(
|
|
103
|
+
self, since: datetime | None = None
|
|
104
|
+
) -> list[RawEvent]:
|
|
105
|
+
"""Fetch records from configured HubSpot objects.
|
|
106
|
+
|
|
107
|
+
Uses the CRM search API with ``lastmodifieddate`` filter for
|
|
108
|
+
incremental sync and handles cursor-based pagination.
|
|
109
|
+
"""
|
|
110
|
+
cutoff = since or datetime.now(timezone.utc) - timedelta(
|
|
111
|
+
days=self.backfill_days
|
|
112
|
+
)
|
|
113
|
+
cutoff_ms = int(cutoff.timestamp() * 1000)
|
|
114
|
+
raw_events: list[RawEvent] = []
|
|
115
|
+
|
|
116
|
+
crm_objects = {"contacts", "companies", "deals", "tickets"}
|
|
117
|
+
engagement_objects = {"notes", "emails", "calls", "tasks"}
|
|
118
|
+
|
|
119
|
+
for obj_type in self.object_filter:
|
|
120
|
+
try:
|
|
121
|
+
if obj_type in crm_objects:
|
|
122
|
+
raw_events.extend(
|
|
123
|
+
self._fetch_crm_object(obj_type, cutoff_ms)
|
|
124
|
+
)
|
|
125
|
+
elif obj_type in engagement_objects:
|
|
126
|
+
raw_events.extend(
|
|
127
|
+
self._fetch_engagements(obj_type, cutoff_ms)
|
|
128
|
+
)
|
|
129
|
+
except Exception:
|
|
130
|
+
logger.exception("Failed to fetch HubSpot %s", obj_type)
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
return raw_events
|
|
134
|
+
|
|
135
|
+
def normalize(self, raw: RawEvent) -> Episode:
|
|
136
|
+
"""Convert a HubSpot record ``RawEvent`` into a CortexDB ``Episode``."""
|
|
137
|
+
data = raw.data
|
|
138
|
+
obj_type = data.get("object_type", "unknown")
|
|
139
|
+
properties = data.get("properties", {})
|
|
140
|
+
episode_type = _OBJECT_TYPE_MAP.get(obj_type, EpisodeType.CUSTOM)
|
|
141
|
+
|
|
142
|
+
actor = self._extract_actor(properties)
|
|
143
|
+
content = self._build_content(obj_type, properties)
|
|
144
|
+
entities = self._extract_entities(obj_type, data)
|
|
145
|
+
tags = ["hubspot", obj_type]
|
|
146
|
+
|
|
147
|
+
metadata: dict[str, Any] = {
|
|
148
|
+
"hubspot_object": obj_type,
|
|
149
|
+
"hubspot_id": data.get("record_id", ""),
|
|
150
|
+
}
|
|
151
|
+
if properties.get("dealstage"):
|
|
152
|
+
metadata["deal_stage"] = properties["dealstage"]
|
|
153
|
+
if properties.get("pipeline"):
|
|
154
|
+
metadata["pipeline"] = properties["pipeline"]
|
|
155
|
+
if properties.get("hs_ticket_priority"):
|
|
156
|
+
metadata["priority"] = properties["hs_ticket_priority"]
|
|
157
|
+
|
|
158
|
+
thread_id = self._derive_thread_id(obj_type, data)
|
|
159
|
+
|
|
160
|
+
return Episode(
|
|
161
|
+
actor=actor,
|
|
162
|
+
source=_SOURCE,
|
|
163
|
+
episode_type=episode_type,
|
|
164
|
+
occurred_at=raw.timestamp,
|
|
165
|
+
content=content,
|
|
166
|
+
raw_payload=data,
|
|
167
|
+
tenant_id=self.tenant_id,
|
|
168
|
+
namespace=self.namespace,
|
|
169
|
+
entities=entities,
|
|
170
|
+
tags=tags,
|
|
171
|
+
metadata=metadata,
|
|
172
|
+
thread_id=thread_id,
|
|
173
|
+
idempotency_key=f"hs:{obj_type}:{data.get('record_id', raw.external_id)}",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def map_permissions(self, raw: RawEvent) -> Visibility:
|
|
177
|
+
"""HubSpot CRM data is internal; always ``Organization`` visibility."""
|
|
178
|
+
return Visibility(level=VisibilityLevel.ORGANIZATION)
|
|
179
|
+
|
|
180
|
+
# -- fetchers ------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
def _fetch_crm_object(
|
|
183
|
+
self, obj_type: str, cutoff_ms: int
|
|
184
|
+
) -> list[RawEvent]:
|
|
185
|
+
"""Fetch a CRM object type via the search API with pagination."""
|
|
186
|
+
client = self._get_client()
|
|
187
|
+
api_map = {
|
|
188
|
+
"contacts": client.crm.contacts,
|
|
189
|
+
"companies": client.crm.companies,
|
|
190
|
+
"deals": client.crm.deals,
|
|
191
|
+
"tickets": client.crm.tickets,
|
|
192
|
+
}
|
|
193
|
+
api = api_map.get(obj_type)
|
|
194
|
+
if api is None:
|
|
195
|
+
return []
|
|
196
|
+
|
|
197
|
+
properties = self._properties_for(obj_type)
|
|
198
|
+
events: list[RawEvent] = []
|
|
199
|
+
after: str | None = None
|
|
200
|
+
|
|
201
|
+
while True:
|
|
202
|
+
filter_groups = [
|
|
203
|
+
{
|
|
204
|
+
"filters": [
|
|
205
|
+
{
|
|
206
|
+
"propertyName": "lastmodifieddate",
|
|
207
|
+
"operator": "GTE",
|
|
208
|
+
"value": str(cutoff_ms),
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
search_request = {
|
|
214
|
+
"filter_groups": filter_groups,
|
|
215
|
+
"properties": properties,
|
|
216
|
+
"limit": 100,
|
|
217
|
+
"sorts": [
|
|
218
|
+
{"propertyName": "lastmodifieddate", "direction": "DESCENDING"}
|
|
219
|
+
],
|
|
220
|
+
}
|
|
221
|
+
if after:
|
|
222
|
+
search_request["after"] = after
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
page = api.search_api.do_search(search_request=search_request)
|
|
226
|
+
except Exception:
|
|
227
|
+
logger.exception("HubSpot search failed for %s", obj_type)
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
for result in page.results:
|
|
231
|
+
props = result.properties or {}
|
|
232
|
+
ts = self._parse_hubspot_ts(
|
|
233
|
+
props.get("lastmodifieddate")
|
|
234
|
+
or props.get("hs_lastmodifieddate")
|
|
235
|
+
)
|
|
236
|
+
events.append(
|
|
237
|
+
RawEvent(
|
|
238
|
+
source="hubspot",
|
|
239
|
+
external_id=str(result.id),
|
|
240
|
+
timestamp=ts,
|
|
241
|
+
data={
|
|
242
|
+
"object_type": obj_type,
|
|
243
|
+
"record_id": str(result.id),
|
|
244
|
+
"properties": props,
|
|
245
|
+
},
|
|
246
|
+
permissions={},
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if page.paging and page.paging.next:
|
|
251
|
+
after = page.paging.next.after
|
|
252
|
+
else:
|
|
253
|
+
break
|
|
254
|
+
|
|
255
|
+
return events
|
|
256
|
+
|
|
257
|
+
def _fetch_engagements(
|
|
258
|
+
self, obj_type: str, cutoff_ms: int
|
|
259
|
+
) -> list[RawEvent]:
|
|
260
|
+
"""Fetch engagement objects (notes, emails, calls, tasks)."""
|
|
261
|
+
client = self._get_client()
|
|
262
|
+
api_map = {
|
|
263
|
+
"notes": client.crm.objects.notes,
|
|
264
|
+
"emails": client.crm.objects.emails,
|
|
265
|
+
"calls": client.crm.objects.calls,
|
|
266
|
+
"tasks": client.crm.objects.tasks,
|
|
267
|
+
}
|
|
268
|
+
api = api_map.get(obj_type)
|
|
269
|
+
if api is None:
|
|
270
|
+
return []
|
|
271
|
+
|
|
272
|
+
events: list[RawEvent] = []
|
|
273
|
+
after: str | None = None
|
|
274
|
+
|
|
275
|
+
while True:
|
|
276
|
+
try:
|
|
277
|
+
kwargs: dict[str, Any] = {"limit": 100}
|
|
278
|
+
if after:
|
|
279
|
+
kwargs["after"] = after
|
|
280
|
+
page = api.basic_api.get_page(**kwargs)
|
|
281
|
+
except Exception:
|
|
282
|
+
logger.exception("HubSpot list failed for %s", obj_type)
|
|
283
|
+
break
|
|
284
|
+
|
|
285
|
+
for result in page.results:
|
|
286
|
+
props = result.properties or {}
|
|
287
|
+
ts = self._parse_hubspot_ts(
|
|
288
|
+
props.get("hs_lastmodifieddate")
|
|
289
|
+
or props.get("hs_timestamp")
|
|
290
|
+
)
|
|
291
|
+
if ts.timestamp() * 1000 < cutoff_ms:
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
events.append(
|
|
295
|
+
RawEvent(
|
|
296
|
+
source="hubspot",
|
|
297
|
+
external_id=str(result.id),
|
|
298
|
+
timestamp=ts,
|
|
299
|
+
data={
|
|
300
|
+
"object_type": obj_type,
|
|
301
|
+
"record_id": str(result.id),
|
|
302
|
+
"properties": props,
|
|
303
|
+
},
|
|
304
|
+
permissions={},
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if page.paging and page.paging.next:
|
|
309
|
+
after = page.paging.next.after
|
|
310
|
+
else:
|
|
311
|
+
break
|
|
312
|
+
|
|
313
|
+
return events
|
|
314
|
+
|
|
315
|
+
# -- helpers -------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
@staticmethod
|
|
318
|
+
def _properties_for(obj_type: str) -> list[str]:
|
|
319
|
+
"""Return properties to request for a CRM object type."""
|
|
320
|
+
props_map: dict[str, list[str]] = {
|
|
321
|
+
"contacts": [
|
|
322
|
+
"firstname", "lastname", "email", "company",
|
|
323
|
+
"phone", "lifecyclestage", "lastmodifieddate",
|
|
324
|
+
"hubspot_owner_id",
|
|
325
|
+
],
|
|
326
|
+
"companies": [
|
|
327
|
+
"name", "domain", "industry", "numberofemployees",
|
|
328
|
+
"annualrevenue", "lastmodifieddate", "hubspot_owner_id",
|
|
329
|
+
],
|
|
330
|
+
"deals": [
|
|
331
|
+
"dealname", "dealstage", "amount", "pipeline",
|
|
332
|
+
"closedate", "hubspot_owner_id", "lastmodifieddate",
|
|
333
|
+
],
|
|
334
|
+
"tickets": [
|
|
335
|
+
"subject", "content", "hs_pipeline", "hs_pipeline_stage",
|
|
336
|
+
"hs_ticket_priority", "hubspot_owner_id",
|
|
337
|
+
"lastmodifieddate",
|
|
338
|
+
],
|
|
339
|
+
}
|
|
340
|
+
return props_map.get(obj_type, ["lastmodifieddate"])
|
|
341
|
+
|
|
342
|
+
@staticmethod
|
|
343
|
+
def _parse_hubspot_ts(value: str | None) -> datetime:
|
|
344
|
+
"""Parse a HubSpot timestamp (epoch-ms string or ISO)."""
|
|
345
|
+
if not value:
|
|
346
|
+
return datetime.now(timezone.utc)
|
|
347
|
+
try:
|
|
348
|
+
return datetime.fromtimestamp(int(value) / 1000, tz=timezone.utc)
|
|
349
|
+
except (ValueError, TypeError, OSError):
|
|
350
|
+
pass
|
|
351
|
+
try:
|
|
352
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
353
|
+
except (ValueError, TypeError):
|
|
354
|
+
return datetime.now(timezone.utc)
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def _extract_actor(properties: dict[str, Any]) -> Actor:
|
|
358
|
+
"""Extract actor from HubSpot record properties."""
|
|
359
|
+
owner_id = properties.get("hubspot_owner_id") or ""
|
|
360
|
+
email = properties.get("email") or ""
|
|
361
|
+
name = " ".join(
|
|
362
|
+
filter(
|
|
363
|
+
None,
|
|
364
|
+
[
|
|
365
|
+
properties.get("firstname", ""),
|
|
366
|
+
properties.get("lastname", ""),
|
|
367
|
+
],
|
|
368
|
+
)
|
|
369
|
+
) or owner_id
|
|
370
|
+
return Actor(id=owner_id or "unknown", name=name, email=email or None)
|
|
371
|
+
|
|
372
|
+
@staticmethod
|
|
373
|
+
def _build_content(obj_type: str, properties: dict[str, Any]) -> str:
|
|
374
|
+
"""Build human-readable content from HubSpot record properties."""
|
|
375
|
+
if obj_type == "contacts":
|
|
376
|
+
name = " ".join(
|
|
377
|
+
filter(
|
|
378
|
+
None,
|
|
379
|
+
[properties.get("firstname"), properties.get("lastname")],
|
|
380
|
+
)
|
|
381
|
+
) or "Unknown contact"
|
|
382
|
+
company = properties.get("company", "")
|
|
383
|
+
email = properties.get("email", "")
|
|
384
|
+
parts = [f"Contact: {name}"]
|
|
385
|
+
if company:
|
|
386
|
+
parts.append(f"Company: {company}")
|
|
387
|
+
if email:
|
|
388
|
+
parts.append(f"Email: {email}")
|
|
389
|
+
return " | ".join(parts)
|
|
390
|
+
|
|
391
|
+
if obj_type == "companies":
|
|
392
|
+
name = properties.get("name", "Unnamed company")
|
|
393
|
+
industry = properties.get("industry", "")
|
|
394
|
+
domain = properties.get("domain", "")
|
|
395
|
+
return f"Company: {name} | {industry} | {domain}".rstrip(" | ")
|
|
396
|
+
|
|
397
|
+
if obj_type == "deals":
|
|
398
|
+
name = properties.get("dealname", "Untitled deal")
|
|
399
|
+
stage = properties.get("dealstage", "")
|
|
400
|
+
amount = properties.get("amount")
|
|
401
|
+
amount_str = f" (${float(amount):,.2f})" if amount else ""
|
|
402
|
+
return f"Deal [{stage}]: {name}{amount_str}"
|
|
403
|
+
|
|
404
|
+
if obj_type == "tickets":
|
|
405
|
+
subject = properties.get("subject", "Untitled ticket")
|
|
406
|
+
content = (properties.get("content") or "")[:500]
|
|
407
|
+
priority = properties.get("hs_ticket_priority", "")
|
|
408
|
+
return f"Ticket [{priority}]: {subject}\n{content}".strip()
|
|
409
|
+
|
|
410
|
+
if obj_type == "notes":
|
|
411
|
+
body = (properties.get("hs_note_body") or "")[:500]
|
|
412
|
+
return f"Note: {body}" if body else "Note (empty)"
|
|
413
|
+
|
|
414
|
+
if obj_type == "emails":
|
|
415
|
+
subject = properties.get("hs_email_subject", "No subject")
|
|
416
|
+
text = (properties.get("hs_email_text") or "")[:500]
|
|
417
|
+
return f"Email: {subject}\n{text}".strip()
|
|
418
|
+
|
|
419
|
+
if obj_type == "calls":
|
|
420
|
+
title = properties.get("hs_call_title", "Call")
|
|
421
|
+
body = (properties.get("hs_call_body") or "")[:500]
|
|
422
|
+
duration = properties.get("hs_call_duration")
|
|
423
|
+
dur_str = f" ({duration}ms)" if duration else ""
|
|
424
|
+
return f"Call: {title}{dur_str}\n{body}".strip()
|
|
425
|
+
|
|
426
|
+
if obj_type == "tasks":
|
|
427
|
+
subject = properties.get("hs_task_subject", "Untitled task")
|
|
428
|
+
body = (properties.get("hs_task_body") or "")[:300]
|
|
429
|
+
status = properties.get("hs_task_status", "")
|
|
430
|
+
return f"Task [{status}]: {subject}\n{body}".strip()
|
|
431
|
+
|
|
432
|
+
return f"HubSpot {obj_type}"
|
|
433
|
+
|
|
434
|
+
@staticmethod
|
|
435
|
+
def _extract_entities(
|
|
436
|
+
obj_type: str, data: dict[str, Any]
|
|
437
|
+
) -> list[EntityRef]:
|
|
438
|
+
"""Extract entity references from a HubSpot record."""
|
|
439
|
+
entities: list[EntityRef] = []
|
|
440
|
+
record_id = data.get("record_id", "")
|
|
441
|
+
properties = data.get("properties", {})
|
|
442
|
+
|
|
443
|
+
if record_id:
|
|
444
|
+
display = (
|
|
445
|
+
properties.get("dealname")
|
|
446
|
+
or properties.get("name")
|
|
447
|
+
or properties.get("subject")
|
|
448
|
+
or properties.get("email")
|
|
449
|
+
or record_id
|
|
450
|
+
)
|
|
451
|
+
entities.append(
|
|
452
|
+
EntityRef(
|
|
453
|
+
entity_type=obj_type.rstrip("s"),
|
|
454
|
+
entity_id=record_id,
|
|
455
|
+
display_name=str(display),
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
owner_id = properties.get("hubspot_owner_id")
|
|
460
|
+
if owner_id:
|
|
461
|
+
entities.append(
|
|
462
|
+
EntityRef(
|
|
463
|
+
entity_type="user",
|
|
464
|
+
entity_id=str(owner_id),
|
|
465
|
+
display_name=str(owner_id),
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
pipeline = properties.get("pipeline") or properties.get("hs_pipeline")
|
|
470
|
+
if pipeline:
|
|
471
|
+
entities.append(
|
|
472
|
+
EntityRef(
|
|
473
|
+
entity_type="pipeline",
|
|
474
|
+
entity_id=str(pipeline),
|
|
475
|
+
display_name=str(pipeline),
|
|
476
|
+
)
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
return entities
|
|
480
|
+
|
|
481
|
+
@staticmethod
|
|
482
|
+
def _derive_thread_id(
|
|
483
|
+
obj_type: str, data: dict[str, Any]
|
|
484
|
+
) -> str | None:
|
|
485
|
+
"""Derive a thread_id for grouping related records."""
|
|
486
|
+
record_id = data.get("record_id", "")
|
|
487
|
+
if not record_id:
|
|
488
|
+
return None
|
|
489
|
+
return f"hs:{obj_type}:{record_id}"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Proactive Insights Engine for CortexDB.
|
|
2
|
+
|
|
3
|
+
Auto-generates intelligence from stored episodes and entities by running
|
|
4
|
+
periodic analysis detectors. Each detector scans the CortexDB data plane
|
|
5
|
+
for a specific pattern (incident spikes, knowledge gaps, stale runbooks,
|
|
6
|
+
etc.) and produces ``Insight`` objects that surface actionable information
|
|
7
|
+
to operators and AI agents.
|
|
8
|
+
|
|
9
|
+
Typical usage::
|
|
10
|
+
|
|
11
|
+
from cortexdb_connectors.insights import InsightsEngine
|
|
12
|
+
|
|
13
|
+
engine = InsightsEngine(cortex_url="http://localhost:8080", cortex_api_key="...")
|
|
14
|
+
await engine.run_once() # single analysis pass
|
|
15
|
+
await engine.start() # periodic background loop
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from cortexdb_connectors.insights.detectors import (
|
|
19
|
+
Insight,
|
|
20
|
+
InsightSeverity,
|
|
21
|
+
InsightType,
|
|
22
|
+
IncidentSpikeDetector,
|
|
23
|
+
KnowledgeGapDetector,
|
|
24
|
+
NewDependencyDetector,
|
|
25
|
+
OwnershipGapDetector,
|
|
26
|
+
StaleRunbookDetector,
|
|
27
|
+
)
|
|
28
|
+
from cortexdb_connectors.insights.engine import InsightsEngine
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"Insight",
|
|
32
|
+
"InsightSeverity",
|
|
33
|
+
"InsightType",
|
|
34
|
+
"InsightsEngine",
|
|
35
|
+
"IncidentSpikeDetector",
|
|
36
|
+
"KnowledgeGapDetector",
|
|
37
|
+
"NewDependencyDetector",
|
|
38
|
+
"OwnershipGapDetector",
|
|
39
|
+
"StaleRunbookDetector",
|
|
40
|
+
]
|