@odla-ai/kg 0.3.2 → 0.4.1

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.js CHANGED
@@ -1,6 +1,12 @@
1
1
  import {
2
- parseDateMs
3
- } from "./chunk-V3N2GRYU.js";
2
+ blendPairs,
3
+ bridgingScores,
4
+ labelPropagation,
5
+ parseDateMs,
6
+ projectBridges,
7
+ reifiedBridgeTargets,
8
+ topBridges
9
+ } from "./chunk-T6SYIVST.js";
4
10
 
5
11
  // src/types.ts
6
12
  function emptyUsage() {
@@ -292,6 +298,235 @@ async function canonicalizeFragment(db, fragment, ontology) {
292
298
  };
293
299
  }
294
300
 
301
+ // src/graph/connectors.ts
302
+ function findConnectors(fragment, config) {
303
+ const nameOf = /* @__PURE__ */ new Map();
304
+ for (const n of fragment.nodes) {
305
+ const label = n.attrs.name ?? n.attrs.title;
306
+ if (typeof label === "string" && label) nameOf.set(`${n.type}:${n.key}`, label);
307
+ }
308
+ const sets = config.lenses.map((l) => ({
309
+ pairs: projectBridges(reifiedBridgeTargets(fragment, { reified: l.reified, bridgeLabel: l.bridge, targetLabel: l.of }), {
310
+ maxPerBridge: config.maxPerBridge ?? 12
311
+ }),
312
+ weight: l.weight ?? 1
313
+ }));
314
+ const pairs = blendPairs([...sets, ...config.extraPairs ?? []]);
315
+ if (pairs.length === 0) return { connectors: [], communities: 0, nodes: 0 };
316
+ const nodes = [...new Set(pairs.flatMap((p) => [p.a, p.b]))].sort();
317
+ const edges = pairs.map((p) => ({ a: p.a, b: p.b, weight: p.w }));
318
+ const labels = labelPropagation(nodes, edges, { seed: 1 });
319
+ const groupName = /* @__PURE__ */ new Map();
320
+ for (const id of nodes) {
321
+ const g = labels.get(id);
322
+ if (!groupName.has(g)) groupName.set(g, nameOf.get(id) ?? id);
323
+ }
324
+ const bridging = bridgingScores(edges, labels);
325
+ const viaOf = (id) => {
326
+ const names = /* @__PURE__ */ new Set();
327
+ for (const p of pairs) {
328
+ if (p.a !== id && p.b !== id) continue;
329
+ for (const v of p.via) names.add(nameOf.get(v) ?? v.split(":").pop());
330
+ }
331
+ return [...names].slice(0, 6);
332
+ };
333
+ const connectors = topBridges(bridging, config.top ?? 12).map((b) => ({
334
+ ref: b.id,
335
+ name: nameOf.get(b.id) ?? b.id,
336
+ score: b.score,
337
+ groups: b.groups.map((g) => groupName.get(g)),
338
+ // every label has an anchor name
339
+ via: viaOf(b.id)
340
+ }));
341
+ return { connectors, communities: new Set(labels.values()).size, nodes: nodes.length };
342
+ }
343
+
344
+ // src/util/identity.ts
345
+ function slugify(input) {
346
+ return input.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
347
+ }
348
+ function normalizeUrl(raw) {
349
+ try {
350
+ const u = new URL(raw);
351
+ u.hash = "";
352
+ const drop = [/^utm_/i, /^ref$/i, /^ref_/i, /^fbclid$/i, /^gclid$/i, /^mc_/i];
353
+ for (const key of [...u.searchParams.keys()]) {
354
+ if (drop.some((re) => re.test(key))) u.searchParams.delete(key);
355
+ }
356
+ let s = u.toString();
357
+ s = s.replace(/\/$/, "");
358
+ return s;
359
+ } catch {
360
+ return raw.trim().replace(/#.*$/, "").replace(/\/$/, "");
361
+ }
362
+ }
363
+ function urlHash(raw) {
364
+ const s = normalizeUrl(raw);
365
+ let h = 2166136261;
366
+ for (let i = 0; i < s.length; i++) {
367
+ h ^= s.charCodeAt(i);
368
+ h = Math.imul(h, 16777619);
369
+ }
370
+ return (h >>> 0).toString(36);
371
+ }
372
+ function isLikelyPersonName(name) {
373
+ const n = name.trim();
374
+ if (!n || /\d/.test(n)) return false;
375
+ const parts = n.split(/\s+/);
376
+ if (parts.length < 2 || parts.length > 4) return false;
377
+ return parts.every((p) => /^[A-Z][a-zA-Z'’.-]*$/.test(p));
378
+ }
379
+
380
+ // src/enrich/facts.ts
381
+ var LOCATE_SYSTEM = "You resolve US business locations to counties. For each company you are given a city and/or a free-text region. Name the US county containing that city (for independent cities, use the city's own county-equivalent name) plus its 2-letter state. Use nulls when the location is ambiguous, outside the US, or unknown.";
382
+ var LOCATE_TOOL = {
383
+ name: "resolve_counties",
384
+ description: "County name + state per company",
385
+ parameters: {
386
+ type: "object",
387
+ properties: {
388
+ items: {
389
+ type: "array",
390
+ items: {
391
+ type: "object",
392
+ properties: {
393
+ slug: { type: "string" },
394
+ county: { type: ["string", "null"], description: 'county name WITHOUT the word "County", e.g. "Santa Clara"' },
395
+ state: { type: ["string", "null"], description: "2-letter USPS state" }
396
+ },
397
+ required: ["slug", "county", "state"]
398
+ }
399
+ }
400
+ },
401
+ required: ["items"]
402
+ }
403
+ };
404
+ function locatePrompt(batch) {
405
+ return "Companies:\n" + batch.map((c) => `- slug ${c.slug}: city=${c.city ?? "?"} region=${c.region ?? "?"}`).join("\n");
406
+ }
407
+ var FACTS_SEARCH_SYSTEM = "You are a research assistant with web search. Find concrete facts about the company: founding year/date, headquarters city and state, founders and key executives (names, roles), and funding (rounds, dates, amounts, investor names). Prefer official/reputable sources. Report dense factual notes; say 'not found' for anything you cannot establish. Do not speculate.";
408
+ var FACTS_EXTRACT_SYSTEM = "Extract structured facts from the research notes. Use null for anything the notes do not establish.";
409
+ var FACTS_EXTRACT_TOOL = {
410
+ name: "company_facts",
411
+ description: "Structured company facts from research notes",
412
+ parameters: {
413
+ type: "object",
414
+ properties: {
415
+ founded: { type: ["string", "null"], description: "YYYY or YYYY-MM or YYYY-MM-DD" },
416
+ city: { type: ["string", "null"] },
417
+ state: { type: ["string", "null"], description: "2-letter USPS" },
418
+ people: {
419
+ type: "array",
420
+ items: {
421
+ type: "object",
422
+ properties: { name: { type: "string" }, role: { type: "string" } },
423
+ required: ["name", "role"]
424
+ }
425
+ },
426
+ rounds: {
427
+ type: "array",
428
+ items: {
429
+ type: "object",
430
+ properties: {
431
+ round: { type: ["string", "null"] },
432
+ date: { type: ["string", "null"] },
433
+ amount: { type: ["string", "null"] },
434
+ investors: { type: "array", items: { type: "string" } }
435
+ },
436
+ required: ["investors"]
437
+ }
438
+ }
439
+ },
440
+ required: ["people", "rounds"]
441
+ }
442
+ };
443
+ var DATE_RE = /^\d{4}(-\d{2}){0,2}$/;
444
+ var isoDate = (v) => !DATE_RE.test(v) ? null : v.length === 4 ? `${v}-01-01` : v.length === 7 ? `${v}-01` : v;
445
+ var countyKey = (name, st) => `${String(name).toLowerCase().replace(/\bcounty\b|\bparish\b|\bborough\b/g, "").replace(/[^a-z]/g, "")}|${String(st).toUpperCase()}`;
446
+ function factsToFragment(company, draft) {
447
+ const nodes = [];
448
+ const edges = [];
449
+ const attrs = {};
450
+ const founded = draft.founded ? isoDate(draft.founded) : null;
451
+ if (founded) attrs.founded = founded;
452
+ if (draft.city && !company.city) attrs.city = String(draft.city).slice(0, 60);
453
+ if (Object.keys(attrs).length > 0) {
454
+ nodes.push({ type: "company", key: company.slug, attrs: { name: company.name, ...attrs } });
455
+ }
456
+ const anchorDate = attrs.founded ?? company.founded ?? company.first_seen ?? "";
457
+ for (const p of (draft.people ?? []).slice(0, 5)) {
458
+ const personSlug = slugify(p.name);
459
+ if (!personSlug || personSlug === company.slug) continue;
460
+ nodes.push({ type: "person", key: personSlug, attrs: { name: p.name } });
461
+ const key = `${personSlug}~${company.slug}`;
462
+ nodes.push({ type: "affiliation", key, attrs: { date: anchorDate, role: String(p.role).slice(0, 60) } });
463
+ edges.push({ from: { type: "affiliation", key }, to: { type: "person", key: personSlug }, label: "of_person" });
464
+ edges.push({ from: { type: "affiliation", key }, to: { type: "company", key: company.slug }, label: "of_company" });
465
+ }
466
+ (draft.rounds ?? []).slice(0, 4).forEach((r, i) => {
467
+ const investors = (r.investors ?? []).map(slugify).filter(Boolean).slice(0, 6);
468
+ const date = r.date ? isoDate(r.date) : null;
469
+ if (investors.length === 0 && !date) return;
470
+ const key = `${company.slug}~e${i}`;
471
+ const rAttrs = { date: date ?? anchorDate };
472
+ if (r.round) rAttrs.round = String(r.round).slice(0, 40);
473
+ if (r.amount) rAttrs.amount = String(r.amount).slice(0, 40);
474
+ nodes.push({ type: "funding_round", key, attrs: rAttrs });
475
+ edges.push({ from: { type: "funding_round", key }, to: { type: "company", key: company.slug }, label: "round_of" });
476
+ for (const [j, invSlug] of investors.entries()) {
477
+ nodes.push({ type: "investor", key: invSlug, attrs: { name: (r.investors ?? [])[j] } });
478
+ edges.push({ from: { type: "funding_round", key }, to: { type: "investor", key: invSlug }, label: "raised_from" });
479
+ }
480
+ });
481
+ return { nodes, edges };
482
+ }
483
+ function locationFragment(slug, name, fips) {
484
+ return {
485
+ nodes: [{ type: "company", key: slug, attrs: { name, fips } }],
486
+ edges: [{ from: { type: "company", key: slug }, to: { type: "county", key: fips }, label: "located_in" }]
487
+ };
488
+ }
489
+
490
+ // src/enrich/run.ts
491
+ async function enrichCompany(ai, models, company) {
492
+ const where = `${company.city ? `, ${company.city}` : ""}${company.region ? ` (${company.region})` : ""}`;
493
+ const { text } = await ai.search({
494
+ model: models.search,
495
+ system: FACTS_SEARCH_SYSTEM,
496
+ user: `Company: ${company.name}${where}. Research its founding, headquarters, key people, and funding.`,
497
+ maxTokens: 900
498
+ });
499
+ const extractOnce = () => ai.extract({
500
+ model: models.extract,
501
+ system: FACTS_EXTRACT_SYSTEM,
502
+ user: `Company: ${company.name}
503
+
504
+ Notes:
505
+ ${text}`,
506
+ tool: FACTS_EXTRACT_TOOL,
507
+ maxTokens: 4e3
508
+ // reasoning headroom before the forced tool call
509
+ });
510
+ const { value } = await extractOnce().catch(extractOnce);
511
+ return factsToFragment(company, value);
512
+ }
513
+ async function resolveCounties(ai, model, rows, gazetteer) {
514
+ const { value } = await ai.extract({
515
+ model,
516
+ system: LOCATE_SYSTEM,
517
+ user: locatePrompt(rows),
518
+ tool: LOCATE_TOOL,
519
+ maxTokens: 6e3
520
+ // reasoning models spend tokens before the tool call
521
+ });
522
+ const out = [];
523
+ for (const item of value.items ?? []) {
524
+ const fips = item.county && item.state ? gazetteer.get(countyKey(item.county, item.state)) : void 0;
525
+ if (fips) out.push({ slug: item.slug, fips });
526
+ }
527
+ return out;
528
+ }
529
+
295
530
  // src/llm/provider.ts
296
531
  import { init, extractText } from "@odla-ai/ai";
297
532
  var MODEL_TIERS = {
@@ -533,42 +768,6 @@ var gdeltConnector = {
533
768
  }
534
769
  };
535
770
 
536
- // src/util/identity.ts
537
- function slugify(input) {
538
- return input.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
539
- }
540
- function normalizeUrl(raw) {
541
- try {
542
- const u = new URL(raw);
543
- u.hash = "";
544
- const drop = [/^utm_/i, /^ref$/i, /^ref_/i, /^fbclid$/i, /^gclid$/i, /^mc_/i];
545
- for (const key of [...u.searchParams.keys()]) {
546
- if (drop.some((re) => re.test(key))) u.searchParams.delete(key);
547
- }
548
- let s = u.toString();
549
- s = s.replace(/\/$/, "");
550
- return s;
551
- } catch {
552
- return raw.trim().replace(/#.*$/, "").replace(/\/$/, "");
553
- }
554
- }
555
- function urlHash(raw) {
556
- const s = normalizeUrl(raw);
557
- let h = 2166136261;
558
- for (let i = 0; i < s.length; i++) {
559
- h ^= s.charCodeAt(i);
560
- h = Math.imul(h, 16777619);
561
- }
562
- return (h >>> 0).toString(36);
563
- }
564
- function isLikelyPersonName(name) {
565
- const n = name.trim();
566
- if (!n || /\d/.test(n)) return false;
567
- const parts = n.split(/\s+/);
568
- if (parts.length < 2 || parts.length > 4) return false;
569
- return parts.every((p) => /^[A-Z][a-zA-Z'’.-]*$/.test(p));
570
- }
571
-
572
771
  // src/connectors/fred.ts
573
772
  async function fetchSeries(seriesId, apiKey, start) {
574
773
  const url = `https://api.stlouisfed.org/fred/series/observations?series_id=${encodeURIComponent(seriesId)}&api_key=${encodeURIComponent(apiKey)}&file_type=json&observation_start=${encodeURIComponent(start)}`;
@@ -1008,8 +1207,13 @@ async function ingestFresh(step, deps, fresh, kind, resolve = false) {
1008
1207
  }
1009
1208
  export {
1010
1209
  COMMIT_STEP_CONFIG,
1210
+ FACTS_EXTRACT_SYSTEM,
1211
+ FACTS_EXTRACT_TOOL,
1212
+ FACTS_SEARCH_SYSTEM,
1011
1213
  IO_STEP_CONFIG,
1012
1214
  LLM_STEP_CONFIG,
1215
+ LOCATE_SYSTEM,
1216
+ LOCATE_TOOL,
1013
1217
  MODEL_TIERS,
1014
1218
  Store,
1015
1219
  addUsage,
@@ -1017,14 +1221,18 @@ export {
1017
1221
  asStepLike,
1018
1222
  canonicalizeFragment,
1019
1223
  connectorIds,
1224
+ countyKey,
1020
1225
  createProvider,
1021
1226
  emptyUsage,
1022
1227
  enabledSources,
1228
+ enrichCompany,
1023
1229
  exportFragment,
1024
1230
  extractJson,
1025
1231
  extractJsonArray,
1232
+ factsToFragment,
1026
1233
  fetchText,
1027
1234
  fetchUrl,
1235
+ findConnectors,
1028
1236
  fragmentToOps,
1029
1237
  getConnector,
1030
1238
  getProvider,
@@ -1033,6 +1241,8 @@ export {
1033
1241
  ingestFresh,
1034
1242
  isLikelyPersonName,
1035
1243
  jobsForHour,
1244
+ locatePrompt,
1245
+ locationFragment,
1036
1246
  makeContext,
1037
1247
  mergeAttrs,
1038
1248
  mergeRulesOf,
@@ -1040,6 +1250,7 @@ export {
1040
1250
  naturalKeysOf,
1041
1251
  normalizeUrl,
1042
1252
  resilientStep,
1253
+ resolveCounties,
1043
1254
  resolveName,
1044
1255
  searchEntities,
1045
1256
  slugify,