@pentatonic-ai/ai-agent-sdk 0.10.21 → 0.10.22
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.
package/dist/index.cjs
CHANGED
|
@@ -878,7 +878,7 @@ function fireAndForgetEmit(clientConfig, sessionOpts, messages, result, model) {
|
|
|
878
878
|
}
|
|
879
879
|
|
|
880
880
|
// src/telemetry.js
|
|
881
|
-
var VERSION = "0.10.
|
|
881
|
+
var VERSION = "0.10.22";
|
|
882
882
|
var TELEMETRY_URL = "https://sdk-telemetry.philip-134.workers.dev";
|
|
883
883
|
function machineId() {
|
|
884
884
|
const raw = typeof process !== "undefined" ? `${process.env?.USER || process.env?.USERNAME || "u"}:${process.platform || "x"}:${process.arch || "x"}` : "browser";
|
package/dist/index.js
CHANGED
|
@@ -847,7 +847,7 @@ function fireAndForgetEmit(clientConfig, sessionOpts, messages, result, model) {
|
|
|
847
847
|
}
|
|
848
848
|
|
|
849
849
|
// src/telemetry.js
|
|
850
|
-
var VERSION = "0.10.
|
|
850
|
+
var VERSION = "0.10.22";
|
|
851
851
|
var TELEMETRY_URL = "https://sdk-telemetry.philip-134.workers.dev";
|
|
852
852
|
function machineId() {
|
|
853
853
|
const raw = typeof process !== "undefined" ? `${process.env?.USER || process.env?.USERNAME || "u"}:${process.platform || "x"}:${process.arch || "x"}` : "browser";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pentatonic-ai/ai-agent-sdk",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.22",
|
|
4
4
|
"description": "TES SDK — LLM observability and lifecycle tracking via Pentatonic Thing Event System. Track token usage, tool calls, and conversations. Manage things through event-sourced lifecycle stages with AI enrichment and vector search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -327,3 +327,45 @@ def test_external_id_keys_a_domainless_record_node() -> None:
|
|
|
327
327
|
assert worker.entity_id("arena1", "org", ext) != worker.entity_id(
|
|
328
328
|
"arena1", "org", "Acme Corp"
|
|
329
329
|
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# ----------------------------------------------------------------------
|
|
333
|
+
# _person_email_domain — person email/domain stamped as a node attribute
|
|
334
|
+
# (SEE-192). Pure helper; the upsert SQL that consumes it is covered by the
|
|
335
|
+
# engine import check, same posture as the org-domain stamp above.
|
|
336
|
+
# ----------------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
def test_person_email_domain_plain() -> None:
|
|
339
|
+
assert worker._person_email_domain(
|
|
340
|
+
["Johann Boedecker", "johann@pentatonic.com"]
|
|
341
|
+
) == ("johann@pentatonic.com", "pentatonic.com")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def test_person_email_domain_lowercases() -> None:
|
|
345
|
+
assert worker._person_email_domain(["J@PENTATONIC.com"]) == (
|
|
346
|
+
"j@pentatonic.com",
|
|
347
|
+
"pentatonic.com",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_person_email_domain_none_without_an_email() -> None:
|
|
352
|
+
# name-only aliases (a fragmented person with no email) → nothing to stamp.
|
|
353
|
+
assert worker._person_email_domain(["Johann Boedecker", "Bödecker, Johann"]) == (
|
|
354
|
+
None,
|
|
355
|
+
None,
|
|
356
|
+
)
|
|
357
|
+
assert worker._person_email_domain([]) == (None, None)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def test_person_email_domain_only_bare_addresses() -> None:
|
|
361
|
+
# mirrors the id-key selection: a display-name form ("X <a@b.com>", has a
|
|
362
|
+
# space) is NOT treated as the person's bare address — graph aliases carry
|
|
363
|
+
# the bare email, and this keeps the attribute aligned with person_id_key.
|
|
364
|
+
assert worker._person_email_domain(["Bob Smith <bob@lego.com>"]) == (None, None)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def test_person_email_domain_subdomain_preserved() -> None:
|
|
368
|
+
assert worker._person_email_domain(["a@mail.acme.co.uk"]) == (
|
|
369
|
+
"a@mail.acme.co.uk",
|
|
370
|
+
"mail.acme.co.uk",
|
|
371
|
+
)
|
|
@@ -1098,6 +1098,18 @@ def _domain_of(value: str) -> str | None:
|
|
|
1098
1098
|
return m.group(1).lower() if m else None
|
|
1099
1099
|
|
|
1100
1100
|
|
|
1101
|
+
def _person_email_domain(aliases: list[str]) -> tuple[str | None, str | None]:
|
|
1102
|
+
"""A person's own email (first alias that looks like a bare address) and its
|
|
1103
|
+
domain, for stamping as queryable node attributes (SEE-192). Returns
|
|
1104
|
+
`(lowercased_email, domain)` or `(None, None)`. Callers run this AFTER the
|
|
1105
|
+
email-alias guard, so a surviving address is the person's own — not a
|
|
1106
|
+
bystander. Mirrors the org-domain stamp; reuses `_domain_of`."""
|
|
1107
|
+
email = next((a for a in aliases if a and "@" in a and " " not in a), None)
|
|
1108
|
+
if not email:
|
|
1109
|
+
return None, None
|
|
1110
|
+
return email.strip().lower(), _domain_of(email)
|
|
1111
|
+
|
|
1112
|
+
|
|
1101
1113
|
def _bare_domain(value: str) -> str | None:
|
|
1102
1114
|
"""Normalise a structured domain field to a bare host — 'acme.com' from
|
|
1103
1115
|
'Acme.com', 'https://www.acme.com/about', etc. Used for system-of-record
|
|
@@ -1416,6 +1428,21 @@ def upsert_entities(
|
|
|
1416
1428
|
if subject_ext_id not in aliases:
|
|
1417
1429
|
aliases.append(subject_ext_id)
|
|
1418
1430
|
attrs_patch["external_id"] = subject_ext_id
|
|
1431
|
+
# Person email/domain as a QUERYABLE node attribute (SEE-192). The
|
|
1432
|
+
# email already mints the id (person_id_key) and survives the alias
|
|
1433
|
+
# guard above, but was never persisted as a discrete attribute — so
|
|
1434
|
+
# retrieval / context-aware mention resolution had to dig it out of
|
|
1435
|
+
# the aliases array (text-search, not indexed) and 0/37,648 person
|
|
1436
|
+
# nodes carried `attributes.email`/`domain`. Stamp it (lowercased) +
|
|
1437
|
+
# its domain, mirroring the org-domain stamp, so a person node is
|
|
1438
|
+
# directly matchable by email/domain. Merge-only (`|| jsonb`); the
|
|
1439
|
+
# guard already dropped bystander emails, so this is the person's own.
|
|
1440
|
+
if etype == "person":
|
|
1441
|
+
_pemail, _pdomain = _person_email_domain(aliases)
|
|
1442
|
+
if _pemail:
|
|
1443
|
+
attrs_patch["email"] = _pemail
|
|
1444
|
+
if _pdomain:
|
|
1445
|
+
attrs_patch["domain"] = _pdomain
|
|
1419
1446
|
attrs_update = json.dumps(attrs_patch)
|
|
1420
1447
|
|
|
1421
1448
|
# Sort (don't `list(set(...))`) so lock acquisition order
|
|
@@ -36,6 +36,8 @@ from entity_id import entity_id, normalize_surface_form, person_id_key # noqa:
|
|
|
36
36
|
# Source-time parsing — byte-identical copy in extractor-async (source_time.py).
|
|
37
37
|
from source_time import event_source_time
|
|
38
38
|
|
|
39
|
+
import json
|
|
40
|
+
|
|
39
41
|
import psycopg
|
|
40
42
|
import psycopg.rows
|
|
41
43
|
from fastapi import FastAPI, HTTPException
|
|
@@ -270,6 +272,18 @@ def _person_entity(
|
|
|
270
272
|
if a:
|
|
271
273
|
aliases_set.add(a)
|
|
272
274
|
|
|
275
|
+
# Email/domain as a QUERYABLE node attribute (SEE-192). The sync pass keys
|
|
276
|
+
# the node on the ENVELOPE email (the most reliable source — not LLM-
|
|
277
|
+
# promoted), so stamp it (lowercased) + its domain here so a person node is
|
|
278
|
+
# directly matchable by email/domain instead of only via the aliases array.
|
|
279
|
+
attrs: dict[str, str] = {}
|
|
280
|
+
if email:
|
|
281
|
+
em = email.strip().lower()
|
|
282
|
+
attrs["email"] = em
|
|
283
|
+
dom = em.rsplit("@", 1)[-1] if "@" in em else ""
|
|
284
|
+
if dom and "." in dom and " " not in dom:
|
|
285
|
+
attrs["domain"] = dom
|
|
286
|
+
|
|
273
287
|
return {
|
|
274
288
|
"id": _entity_id(req.arena, "person", id_key),
|
|
275
289
|
"arena": req.arena,
|
|
@@ -279,6 +293,7 @@ def _person_entity(
|
|
|
279
293
|
"provenance_event_ids": [event_id],
|
|
280
294
|
"participant_set": req.attributes.get("participant_set", [req.arena]),
|
|
281
295
|
"disclosure_class": req.attributes.get("disclosure_class", "private"),
|
|
296
|
+
"attributes": attrs,
|
|
282
297
|
}
|
|
283
298
|
|
|
284
299
|
|
|
@@ -550,10 +565,10 @@ async def _upsert_entities(
|
|
|
550
565
|
INSERT INTO entities (
|
|
551
566
|
id, arena, entity_type, canonical_name, aliases,
|
|
552
567
|
provenance_event_ids, participant_set, disclosure_class,
|
|
553
|
-
first_seen, last_seen, salience
|
|
568
|
+
first_seen, last_seen, salience, attributes
|
|
554
569
|
) VALUES (
|
|
555
570
|
%s, %s, %s, %s, %s, %s, %s, %s::disclosure_class,
|
|
556
|
-
COALESCE(%s, NOW()), COALESCE(%s, NOW()), %s
|
|
571
|
+
COALESCE(%s, NOW()), COALESCE(%s, NOW()), %s, %s::jsonb
|
|
557
572
|
)
|
|
558
573
|
ON CONFLICT (id) DO UPDATE SET
|
|
559
574
|
aliases = (
|
|
@@ -562,6 +577,9 @@ async def _upsert_entities(
|
|
|
562
577
|
provenance_event_ids = (
|
|
563
578
|
SELECT ARRAY(SELECT DISTINCT UNNEST(entities.provenance_event_ids || EXCLUDED.provenance_event_ids))
|
|
564
579
|
),
|
|
580
|
+
-- merge the email/domain hard key (no-op for the `{}` default
|
|
581
|
+
-- on entity types that don't stamp it); never clobbers.
|
|
582
|
+
attributes = entities.attributes || EXCLUDED.attributes,
|
|
565
583
|
salience = GREATEST(entities.salience, EXCLUDED.salience),
|
|
566
584
|
last_seen = GREATEST(entities.last_seen, EXCLUDED.last_seen),
|
|
567
585
|
first_seen = LEAST(entities.first_seen, EXCLUDED.first_seen)
|
|
@@ -569,7 +587,8 @@ async def _upsert_entities(
|
|
|
569
587
|
(e["id"], e["arena"], e["entity_type"], e["canonical_name"],
|
|
570
588
|
e["aliases"], e["provenance_event_ids"],
|
|
571
589
|
e["participant_set"], e["disclosure_class"],
|
|
572
|
-
event_time, event_time, _sal
|
|
590
|
+
event_time, event_time, _sal,
|
|
591
|
+
json.dumps(e.get("attributes") or {})),
|
|
573
592
|
)
|
|
574
593
|
|
|
575
594
|
|