cortexdb-connectors 0.2.8__tar.gz → 0.2.10__tar.gz
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-0.2.8 → cortexdb_connectors-0.2.10}/PKG-INFO +2 -2
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/README.md +1 -1
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/base.py +131 -16
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/cli.py +31 -6
- cortexdb_connectors-0.2.10/cortexdb_connectors/jira/__init__.py +556 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/webhooks.py +54 -3
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors.egg-info/PKG-INFO +2 -2
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors.egg-info/SOURCES.txt +2 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/pyproject.toml +1 -1
- cortexdb_connectors-0.2.10/tests/test_jira_attachments.py +262 -0
- cortexdb_connectors-0.2.10/tests/test_jira_findings_0_2_9.py +341 -0
- cortexdb_connectors-0.2.8/cortexdb_connectors/jira/__init__.py +0 -301
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/confluence/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/discord/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/freshdesk/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/github/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/gitlab/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/google_workspace/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/hubspot/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/insights/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/insights/api.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/insights/detectors.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/insights/engine.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/intercom/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/linear/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/notion/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/pagerduty/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/salesforce/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/servicenow/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/slack/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/state.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/teams/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/tldv/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/worker.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors/zendesk/__init__.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors.egg-info/dependency_links.txt +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors.egg-info/entry_points.txt +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors.egg-info/requires.txt +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/cortexdb_connectors.egg-info/top_level.txt +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/setup.cfg +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/tests/test_base.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/tests/test_connector_coverage.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/tests/test_github.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/tests/test_ingest_e2e.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/tests/test_slack.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/tests/test_streaming_cursor.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/tests/test_webhooks.py +0 -0
- {cortexdb_connectors-0.2.8 → cortexdb_connectors-0.2.10}/tests/test_webhooks_replay.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cortexdb-connectors
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.10
|
|
4
4
|
Summary: Data connectors for CortexDB — ingest from Slack, GitHub, GitLab, Jira, Linear, Confluence, Notion, PagerDuty, Discord, Teams, Google Workspace, Salesforce, HubSpot, Zendesk, Intercom, and ServiceNow into the v1 memory API.
|
|
5
5
|
Author-email: CortexDB Team <team@cortexdb.ai>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -156,7 +156,7 @@ All 16 connectors are **🟢 Free** to self-host and **🔒 Starter+** ($29/mo)
|
|
|
156
156
|
slack SLACK_BOT_TOKEN
|
|
157
157
|
github GITHUB_TOKEN
|
|
158
158
|
gitlab GITLAB_TOKEN
|
|
159
|
-
jira JIRA_URL, JIRA_EMAIL, JIRA_API_TOKEN
|
|
159
|
+
jira JIRA_URL, JIRA_EMAIL, JIRA_API_TOKEN (incl. attachments → /v1/blobs multimodal extraction, 0.2.10+)
|
|
160
160
|
linear LINEAR_API_KEY
|
|
161
161
|
confluence CONFLUENCE_URL, CONFLUENCE_EMAIL, CONFLUENCE_API_TOKEN
|
|
162
162
|
notion NOTION_TOKEN
|
|
@@ -78,7 +78,7 @@ All 16 connectors are **🟢 Free** to self-host and **🔒 Starter+** ($29/mo)
|
|
|
78
78
|
slack SLACK_BOT_TOKEN
|
|
79
79
|
github GITHUB_TOKEN
|
|
80
80
|
gitlab GITLAB_TOKEN
|
|
81
|
-
jira JIRA_URL, JIRA_EMAIL, JIRA_API_TOKEN
|
|
81
|
+
jira JIRA_URL, JIRA_EMAIL, JIRA_API_TOKEN (incl. attachments → /v1/blobs multimodal extraction, 0.2.10+)
|
|
82
82
|
linear LINEAR_API_KEY
|
|
83
83
|
confluence CONFLUENCE_URL, CONFLUENCE_EMAIL, CONFLUENCE_API_TOKEN
|
|
84
84
|
notion NOTION_TOKEN
|
|
@@ -102,7 +102,7 @@ class VisibilityLevel(str, Enum):
|
|
|
102
102
|
@dataclass(frozen=True)
|
|
103
103
|
class Source:
|
|
104
104
|
system: str
|
|
105
|
-
connector_version: str = "0.2.
|
|
105
|
+
connector_version: str = "0.2.10"
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
@dataclass(frozen=True)
|
|
@@ -126,6 +126,25 @@ class Visibility:
|
|
|
126
126
|
allowed_principals: tuple[str, ...] = ()
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
@dataclass
|
|
130
|
+
class Attachment:
|
|
131
|
+
"""A binary attachment destined for `POST /v1/blobs` + a `blob_ref`
|
|
132
|
+
experience, so CortexDB's multimodal pipeline (Tika/vision/ASR content
|
|
133
|
+
processors) extracts and indexes it.
|
|
134
|
+
|
|
135
|
+
`data` carries the raw bytes (downloaded by the connector's
|
|
136
|
+
`fetch_events`); when `data` is None the attachment is metadata-only
|
|
137
|
+
(e.g. oversized skip) and only its filename line survives in text.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
filename: str
|
|
141
|
+
mime_type: str
|
|
142
|
+
size_bytes: int = 0
|
|
143
|
+
data: bytes | None = None
|
|
144
|
+
source_url: str | None = None
|
|
145
|
+
attachment_id: str = ""
|
|
146
|
+
|
|
147
|
+
|
|
129
148
|
@dataclass
|
|
130
149
|
class RawEvent:
|
|
131
150
|
source: str
|
|
@@ -158,6 +177,11 @@ class Episode:
|
|
|
158
177
|
parent_id: str | None = None
|
|
159
178
|
idempotency_key: str | None = None
|
|
160
179
|
ttl_seconds: int | None = None
|
|
180
|
+
# When set, this episode is a BINARY attachment: `ingest_episode` uploads
|
|
181
|
+
# `blob.data` to `POST /v1/blobs` and sends the experience with
|
|
182
|
+
# `content.kind="blob_ref"` (the multimodal path) instead of text. The
|
|
183
|
+
# `content` field then serves only as the no-bytes fallback text.
|
|
184
|
+
blob: Attachment | None = None
|
|
161
185
|
|
|
162
186
|
|
|
163
187
|
@dataclass
|
|
@@ -325,10 +349,26 @@ class CortexConnector(ABC):
|
|
|
325
349
|
|
|
326
350
|
for key, value in replacements.items():
|
|
327
351
|
tpl = tpl.replace("{" + key + "}", value or "")
|
|
352
|
+
# C6d: an unresolved placeholder used to pass through literally,
|
|
353
|
+
# producing a silently malformed scope (`.../issue:{entity.issue}`)
|
|
354
|
+
# that the server happily stores as a real path. Fail loudly instead —
|
|
355
|
+
# the sync loop records the error per event and holds the cursor.
|
|
356
|
+
leftover = re.findall(r"\{[^{}]+\}", tpl)
|
|
357
|
+
if leftover:
|
|
358
|
+
raise ValueError(
|
|
359
|
+
f"scope template resolved to {tpl!r} with unresolved "
|
|
360
|
+
f"placeholder(s) {', '.join(leftover)} — this episode does "
|
|
361
|
+
"not carry the referenced entity/field."
|
|
362
|
+
)
|
|
328
363
|
return tpl
|
|
329
364
|
|
|
330
|
-
def _envelope(self, episode: Episode) -> dict[str, Any]:
|
|
331
|
-
"""Build the v1 ExperienceItem JSON for this episode.
|
|
365
|
+
def _envelope(self, episode: Episode, blob_id: str | None = None) -> dict[str, Any]:
|
|
366
|
+
"""Build the v1 ExperienceItem JSON for this episode.
|
|
367
|
+
|
|
368
|
+
With `blob_id` (an id returned by `upload_blob`) the content becomes
|
|
369
|
+
`kind="blob_ref"` — the server fetches the stored bytes and runs the
|
|
370
|
+
per-modality content processor on them. Otherwise plain text.
|
|
371
|
+
"""
|
|
332
372
|
scope = self._resolve_scope(episode)
|
|
333
373
|
modality = (
|
|
334
374
|
episode.metadata.get("modality")
|
|
@@ -343,11 +383,19 @@ class CortexConnector(ABC):
|
|
|
343
383
|
for ent in episode.entities:
|
|
344
384
|
labels.append(f"{ent.entity_type}:{ent.entity_id}")
|
|
345
385
|
|
|
346
|
-
|
|
347
|
-
"kind": "
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
386
|
+
if blob_id is not None:
|
|
387
|
+
content: dict[str, Any] = {"kind": "blob_ref", "blob_id": blob_id}
|
|
388
|
+
# Filename/MIME ride labels — blob_ref content carries no text,
|
|
389
|
+
# and labels are the queryable metadata surface.
|
|
390
|
+
if episode.blob is not None:
|
|
391
|
+
labels.append(f"filename={episode.blob.filename}")
|
|
392
|
+
labels.append(f"mime={episode.blob.mime_type}")
|
|
393
|
+
else:
|
|
394
|
+
content = {
|
|
395
|
+
"kind": "message",
|
|
396
|
+
"role": "user" if modality == "conversation" else "system",
|
|
397
|
+
"text": self._guard_content(episode.content, episode),
|
|
398
|
+
}
|
|
351
399
|
# The v1 write envelope is STRICT (deny_unknown_fields) as of v0.8.0:
|
|
352
400
|
# thread_id / meta / preceded_by are NOT envelope context fields, and
|
|
353
401
|
# sending them returns 422 INVALID_BODY (which stalls the sync cursor
|
|
@@ -422,10 +470,67 @@ class CortexConnector(ABC):
|
|
|
422
470
|
)
|
|
423
471
|
return capped
|
|
424
472
|
|
|
473
|
+
def _headers(self, content_type: str = "application/json") -> dict[str, str]:
|
|
474
|
+
return {
|
|
475
|
+
"Authorization": f"Bearer {self.cortex_api_key}",
|
|
476
|
+
"X-Cortex-Actor": self.actor,
|
|
477
|
+
"Content-Type": content_type,
|
|
478
|
+
"User-Agent": f"cortexdb-connectors/{Source(system='base').connector_version}",
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async def upload_blob(self, data: bytes, mime_type: str) -> str:
|
|
482
|
+
"""POST raw bytes to `/v1/blobs`; returns the server `blob_id`."""
|
|
483
|
+
import httpx
|
|
484
|
+
|
|
485
|
+
async with httpx.AsyncClient(timeout=120.0) as client:
|
|
486
|
+
resp = await client.post(
|
|
487
|
+
f"{self.cortex_url}/v1/blobs",
|
|
488
|
+
content=data,
|
|
489
|
+
headers=self._headers(mime_type or "application/octet-stream"),
|
|
490
|
+
)
|
|
491
|
+
if resp.status_code >= 400:
|
|
492
|
+
logger.error(
|
|
493
|
+
"blob upload REJECTED: status=%s bytes=%d mime=%s body=%s",
|
|
494
|
+
resp.status_code, len(data), mime_type, resp.text[:300],
|
|
495
|
+
)
|
|
496
|
+
resp.raise_for_status()
|
|
497
|
+
return resp.json()["blob_id"]
|
|
498
|
+
|
|
499
|
+
async def experience_exists(self, idempotency_key: str, scope: str) -> bool:
|
|
500
|
+
"""Probe `GET /v1/experience/status` for a prior write with this key.
|
|
501
|
+
|
|
502
|
+
Used before attachment blob uploads so a re-sync doesn't store a
|
|
503
|
+
fresh (orphaned) blob copy for an experience that will just replay.
|
|
504
|
+
Best-effort: any probe failure returns False (upload proceeds — the
|
|
505
|
+
experience write itself still dedups via its idempotency key).
|
|
506
|
+
"""
|
|
507
|
+
import httpx
|
|
508
|
+
|
|
509
|
+
try:
|
|
510
|
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
511
|
+
resp = await client.get(
|
|
512
|
+
f"{self.cortex_url}/v1/experience/status",
|
|
513
|
+
params={"idempotency_key": idempotency_key, "scope": scope},
|
|
514
|
+
headers=self._headers(),
|
|
515
|
+
)
|
|
516
|
+
if resp.status_code != 200:
|
|
517
|
+
return False
|
|
518
|
+
return bool(resp.json().get("found"))
|
|
519
|
+
except Exception: # noqa: BLE001
|
|
520
|
+
return False
|
|
521
|
+
|
|
425
522
|
async def ingest_episode(self, episode: Episode) -> str:
|
|
426
523
|
"""POST `/v1/experience` with the v1 envelope.
|
|
427
524
|
|
|
428
|
-
|
|
525
|
+
Blob-carrying episodes (`episode.blob` set with bytes) first upload
|
|
526
|
+
the bytes to `/v1/blobs` and go in as `content.kind="blob_ref"` so the
|
|
527
|
+
server's content processors extract them. A status probe on the
|
|
528
|
+
episode's idempotency key skips the upload when the experience already
|
|
529
|
+
exists (re-sync), avoiding orphaned duplicate blobs. Bytes-less blob
|
|
530
|
+
episodes (oversized skips) fall back to their text content.
|
|
531
|
+
|
|
532
|
+
Returns the server-side event_id on success ("exists" when the
|
|
533
|
+
status probe short-circuited a replay).
|
|
429
534
|
"""
|
|
430
535
|
import httpx
|
|
431
536
|
|
|
@@ -441,13 +546,23 @@ class CortexConnector(ABC):
|
|
|
441
546
|
"explicitly. The actor must match the token's `sub` claim."
|
|
442
547
|
)
|
|
443
548
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
549
|
+
blob_id: str | None = None
|
|
550
|
+
if episode.blob is not None and episode.blob.data:
|
|
551
|
+
scope = self._resolve_scope(episode)
|
|
552
|
+
key = episode.idempotency_key or self._fallback_idem(episode, scope)
|
|
553
|
+
if await self.experience_exists(key, scope):
|
|
554
|
+
logger.info(
|
|
555
|
+
"attachment already ingested (idempotency_key=%s); "
|
|
556
|
+
"skipping blob upload",
|
|
557
|
+
key,
|
|
558
|
+
)
|
|
559
|
+
return "exists"
|
|
560
|
+
blob_id = await self.upload_blob(
|
|
561
|
+
episode.blob.data, episode.blob.mime_type
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
envelope = self._envelope(episode, blob_id=blob_id)
|
|
565
|
+
headers = self._headers()
|
|
451
566
|
|
|
452
567
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
453
568
|
resp = await client.post(
|
|
@@ -432,16 +432,41 @@ def _cmd_sync(args: argparse.Namespace) -> None:
|
|
|
432
432
|
|
|
433
433
|
def _cmd_serve(args: argparse.Namespace) -> None:
|
|
434
434
|
"""Run the webhook receiver for real-time create/update/delete."""
|
|
435
|
-
|
|
435
|
+
# C3: this used to import a `build_handlers` that never existed —
|
|
436
|
+
# `cortexdb-sync serve` ImportError'd on startup, killing every
|
|
437
|
+
# connector's webhook mode. WebhookServer wires its own handlers from
|
|
438
|
+
# env secrets; the CLI only supplies CortexDB creds + bind address.
|
|
439
|
+
import uvicorn
|
|
440
|
+
|
|
441
|
+
from cortexdb_connectors.webhooks import WebhookServer
|
|
436
442
|
|
|
437
443
|
creds = _load_cortex_creds(args)
|
|
438
|
-
|
|
439
|
-
|
|
444
|
+
secret_envs = [
|
|
445
|
+
"SLACK_SIGNING_SECRET",
|
|
446
|
+
"GITHUB_WEBHOOK_SECRET",
|
|
447
|
+
"JIRA_WEBHOOK_SECRET",
|
|
448
|
+
"FRESHDESK_WEBHOOK_SECRET",
|
|
449
|
+
"TLDV_WEBHOOK_SECRET",
|
|
450
|
+
]
|
|
451
|
+
if not any(os.environ.get(v) for v in secret_envs):
|
|
440
452
|
logger.warning(
|
|
441
|
-
"no webhook
|
|
442
|
-
"
|
|
453
|
+
"no webhook signing secrets configured — every delivery will be "
|
|
454
|
+
"rejected as unsigned. Set one or more of: %s",
|
|
455
|
+
", ".join(secret_envs),
|
|
443
456
|
)
|
|
444
|
-
|
|
457
|
+
server = WebhookServer(
|
|
458
|
+
cortexdb_url=creds["api_url"] or None,
|
|
459
|
+
cortexdb_api_key=creds["api_key"] or None,
|
|
460
|
+
scope=creds["scope_template"] or "user:default",
|
|
461
|
+
actor=creds["actor"] or None,
|
|
462
|
+
)
|
|
463
|
+
logger.info(
|
|
464
|
+
"webhook receiver listening on %s:%d (routes: /webhooks/{slack,github,"
|
|
465
|
+
"jira,freshdesk,tldv}/events, /health)",
|
|
466
|
+
args.host,
|
|
467
|
+
args.port,
|
|
468
|
+
)
|
|
469
|
+
uvicorn.run(server.build_app(), host=args.host, port=args.port, log_level="info")
|
|
445
470
|
|
|
446
471
|
|
|
447
472
|
def _cmd_watch(args: argparse.Namespace) -> None:
|