@odla-ai/kg 0.4.1 → 0.5.0
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/README.md +31 -0
- package/dist/index.cjs +197 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +144 -2
- package/dist/index.d.ts +144 -2
- package/dist/index.js +197 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { G as GraphFragment, W as WeightedPair, U as Usage, R as RawItem, E as ExtractResult } from './lenses-C7Q7Gvnx.cjs';
|
|
2
2
|
export { a as GraphEdge, b as GraphNode, c as addUsage, e as emptyUsage } from './lenses-C7Q7Gvnx.cjs';
|
|
3
3
|
import { Op, AdminDb, SerializedAttr, Cardinality, SerializedSchema, Value, Entity } from '@odla-ai/db';
|
|
4
|
-
import { ExtractTool, Ai } from '@odla-ai/ai';
|
|
4
|
+
import { ExtractTool, SearchCitation, Ai } from '@odla-ai/ai';
|
|
5
5
|
|
|
6
6
|
/** node type → unique natural-key attribute name. */
|
|
7
7
|
type NaturalKeys = Record<string, string>;
|
|
@@ -237,6 +237,120 @@ declare function factsToFragment(company: CompanyRow, draft: FactsDraft): GraphF
|
|
|
237
237
|
/** The `located_in` write linking a company to its resolved county. */
|
|
238
238
|
declare function locationFragment(slug: string, name: string, fips: string): GraphFragment;
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* What a research pass concluded about the entity it was asked to find.
|
|
242
|
+
*
|
|
243
|
+
* `wrong_entity` is deliberately distinct from `not_found`: a pass that
|
|
244
|
+
* confidently describes a *different* person with the same name is a much worse
|
|
245
|
+
* failure than one that admits it found nothing, and a benchmark that collapses
|
|
246
|
+
* the two cannot see it.
|
|
247
|
+
*/
|
|
248
|
+
type Verdict = "confirmed" | "probable" | "ambiguous" | "not_found" | "wrong_entity";
|
|
249
|
+
/** Verdicts that permit writing enriched facts to the graph. */
|
|
250
|
+
declare const WRITABLE_VERDICTS: readonly Verdict[];
|
|
251
|
+
/** One supporting claim tied to the source that backs it. */
|
|
252
|
+
interface Evidence {
|
|
253
|
+
/** What the source establishes, in the model's words. */
|
|
254
|
+
claim: string;
|
|
255
|
+
/** The URL the claim came from. */
|
|
256
|
+
sourceUrl: string;
|
|
257
|
+
/** When the source was read, as an ISO date. */
|
|
258
|
+
retrievedAt?: string;
|
|
259
|
+
}
|
|
260
|
+
/** A verdict plus the confidence and evidence offered for it. */
|
|
261
|
+
interface Verification {
|
|
262
|
+
verdict: Verdict;
|
|
263
|
+
/** The model's stated confidence, clamped to 0..1. Scored for calibration, never trusted. */
|
|
264
|
+
confidence: number;
|
|
265
|
+
evidence: Evidence[];
|
|
266
|
+
}
|
|
267
|
+
/** Whether a value is one of the closed verdict members. */
|
|
268
|
+
declare function isVerdict(value: unknown): value is Verdict;
|
|
269
|
+
/** Whether a verdict permits writing the enriched facts it came with. */
|
|
270
|
+
declare function isWritable(verdict: Verdict): boolean;
|
|
271
|
+
/**
|
|
272
|
+
* Normalize a model's self-reported verification into the closed shape.
|
|
273
|
+
*
|
|
274
|
+
* Fails safe in both directions: an unrecognized verdict becomes `ambiguous`
|
|
275
|
+
* (never `confirmed`), a missing or out-of-range confidence is clamped, and
|
|
276
|
+
* evidence without a usable http(s) source URL is dropped — an unsourced claim
|
|
277
|
+
* is exactly the thing this package exists to stop treating as a fact.
|
|
278
|
+
*/
|
|
279
|
+
declare function normalizeVerification(draft: unknown, retrievedAt?: string): Verification;
|
|
280
|
+
/** Whether a string is an absolute http(s) URL — the only kind evidence may cite. */
|
|
281
|
+
declare function isHttpUrl(value: string): boolean;
|
|
282
|
+
|
|
283
|
+
/** The minimal person row enrichment reads. */
|
|
284
|
+
interface PersonRow {
|
|
285
|
+
slug: string;
|
|
286
|
+
name: string;
|
|
287
|
+
/** Natural key of a company the person is already linked to — the strongest disambiguator. */
|
|
288
|
+
companySlug?: string;
|
|
289
|
+
companyName?: string;
|
|
290
|
+
role?: string;
|
|
291
|
+
city?: string;
|
|
292
|
+
region?: string;
|
|
293
|
+
}
|
|
294
|
+
/** Where a person publishes. Kinds with one canonical host are validated against it. */
|
|
295
|
+
type PersonLinkKind = "website" | "blog" | "substack" | "linkedin" | "x" | "bluesky" | "github" | "other";
|
|
296
|
+
/** System prompt for the grounded person research pass. */
|
|
297
|
+
declare const PERSON_SEARCH_SYSTEM: string;
|
|
298
|
+
/** System prompt for structuring the research notes into {@link PersonDraft}. */
|
|
299
|
+
declare const PERSON_EXTRACT_SYSTEM: string;
|
|
300
|
+
/** Forced-tool schema for a person's verified public profile. */
|
|
301
|
+
declare const PERSON_EXTRACT_TOOL: ExtractTool;
|
|
302
|
+
/** The model's structured person profile, before validation into a fragment. */
|
|
303
|
+
interface PersonDraft {
|
|
304
|
+
verdict?: string;
|
|
305
|
+
confidence?: number;
|
|
306
|
+
links?: Array<{
|
|
307
|
+
kind?: string;
|
|
308
|
+
url?: string;
|
|
309
|
+
}>;
|
|
310
|
+
employer?: string | null;
|
|
311
|
+
role?: string | null;
|
|
312
|
+
businessEmail?: string | null;
|
|
313
|
+
businessPhone?: string | null;
|
|
314
|
+
city?: string | null;
|
|
315
|
+
region?: string | null;
|
|
316
|
+
almaMater?: string | null;
|
|
317
|
+
otherCompanies?: string[];
|
|
318
|
+
evidence?: Array<{
|
|
319
|
+
claim?: string;
|
|
320
|
+
sourceUrl?: string;
|
|
321
|
+
}>;
|
|
322
|
+
}
|
|
323
|
+
/** A validated person link. */
|
|
324
|
+
interface PersonLink {
|
|
325
|
+
kind: PersonLinkKind;
|
|
326
|
+
url: string;
|
|
327
|
+
}
|
|
328
|
+
/** The validated result of one person enrichment pass. */
|
|
329
|
+
interface PersonResult {
|
|
330
|
+
verification: Verification;
|
|
331
|
+
links: PersonLink[];
|
|
332
|
+
fragment: GraphFragment;
|
|
333
|
+
}
|
|
334
|
+
/** The user prompt for a person research pass. */
|
|
335
|
+
declare function personPrompt(person: PersonRow): string;
|
|
336
|
+
/**
|
|
337
|
+
* Keep only links that are plausibly real: absolute https URLs, on the canonical
|
|
338
|
+
* host when the kind has one, and never a placeholder domain. Models invent
|
|
339
|
+
* `linkedin.com/in/first-last` from a name constantly; a kind/host mismatch is
|
|
340
|
+
* the cheapest way to catch it before it becomes a stored "fact".
|
|
341
|
+
*/
|
|
342
|
+
declare function validateLinks(raw: PersonDraft["links"], limit?: number): PersonLink[];
|
|
343
|
+
/**
|
|
344
|
+
* Turn one person's structured draft into a verdict plus an idempotent fragment.
|
|
345
|
+
*
|
|
346
|
+
* The fragment is empty unless the verdict is writable — an `ambiguous`,
|
|
347
|
+
* `not_found`, or `wrong_entity` pass contributes no graph, however confident
|
|
348
|
+
* the model sounded. The verdict and its evidence are still returned, because
|
|
349
|
+
* "this lane confidently found the wrong person" is exactly what a benchmark
|
|
350
|
+
* needs to record.
|
|
351
|
+
*/
|
|
352
|
+
declare function personToFragment(person: PersonRow, draft: PersonDraft, retrievedAt?: string): PersonResult;
|
|
353
|
+
|
|
240
354
|
/** Which model serves each verb (search must be a web-grounded model). */
|
|
241
355
|
interface EnrichModels {
|
|
242
356
|
/** Web-search-grounded research model. */
|
|
@@ -251,6 +365,23 @@ interface EnrichModels {
|
|
|
251
365
|
* fragment (possibly empty — nothing established); the caller writes it.
|
|
252
366
|
*/
|
|
253
367
|
declare function enrichCompany(ai: Ai, models: EnrichModels, company: CompanyRow): Promise<GraphFragment>;
|
|
368
|
+
/** One person enrichment pass, with the sources the provider actually cited. */
|
|
369
|
+
interface PersonEnrichment extends PersonResult {
|
|
370
|
+
/** Sources the search model attributed. Empty when the provider grounded nothing. */
|
|
371
|
+
citations: SearchCitation[];
|
|
372
|
+
/** The research prose the extraction was taken from — useful for adjudication. */
|
|
373
|
+
notes: string;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Enrich one person: a grounded web search for their public professional
|
|
377
|
+
* presence, then a structured extraction into a validated verdict + fragment.
|
|
378
|
+
*
|
|
379
|
+
* Unlike {@link enrichCompany}, this always returns a verdict — including
|
|
380
|
+
* `not_found` and `wrong_entity` — because "the lane confidently identified the
|
|
381
|
+
* wrong person" is a result worth recording, not an error. The fragment is empty
|
|
382
|
+
* unless the verdict permits writing.
|
|
383
|
+
*/
|
|
384
|
+
declare function enrichPerson(ai: Ai, models: EnrichModels, person: PersonRow, retrievedAt?: string): Promise<PersonEnrichment>;
|
|
254
385
|
/**
|
|
255
386
|
* Resolve a batch of companies' free-text locations to county FIPS codes via a
|
|
256
387
|
* single structured extraction, joined against a caller-supplied gazetteer
|
|
@@ -541,6 +672,17 @@ declare function ingestFresh(step: StepLike, deps: PipelineDeps, fresh: RawItem[
|
|
|
541
672
|
*/
|
|
542
673
|
/** NFKD-normalize, strip accents, lowercase, non-alphanumerics → "-", cap 60. */
|
|
543
674
|
declare function slugify(input: string): string;
|
|
675
|
+
/**
|
|
676
|
+
* Collapse a name to a comparison key: accents stripped, `&` spelled out,
|
|
677
|
+
* punctuation and spacing removed, and trailing legal suffixes dropped.
|
|
678
|
+
*
|
|
679
|
+
* Stronger than comparing {@link slugify} output, because it matches
|
|
680
|
+
* "Acme Corp.", "Acme, Inc." and "Acme & Co" onto the same key — the shapes the
|
|
681
|
+
* same organization is actually written in across sources. Use it to decide
|
|
682
|
+
* whether two records are the same entity; use `slugify` for the natural key
|
|
683
|
+
* that gets stored.
|
|
684
|
+
*/
|
|
685
|
+
declare function identityKey(value: string): string;
|
|
544
686
|
/** Drop tracking params, hash, and trailing slash so the same page maps to one key. */
|
|
545
687
|
declare function normalizeUrl(raw: string): string;
|
|
546
688
|
/** Stable short key for a URL (FNV-1a 32-bit, base36). Used as the `signal` natural key. */
|
|
@@ -561,4 +703,4 @@ declare function htmlToText(html: string, maxChars?: number): string;
|
|
|
561
703
|
/** Fetch a URL and return readable text. Best-effort: returns "" on any failure. */
|
|
562
704
|
declare function fetchText(url: string, maxChars?: number): Promise<string>;
|
|
563
705
|
|
|
564
|
-
export { type AttrSpec, type AttrType, COMMIT_STEP_CONFIG, type CompanyRow, type Connector, type ConnectorConfig, type ConnectorContext, type ConnectorJob, type ConnectorLens, type ConnectorReport, type ContextConfig, type EdgeSpec, type EnrichModels, type EntitySpec, type ExportEntitySpec, type ExportSpec, type ExtractCall, ExtractResult, FACTS_EXTRACT_SYSTEM, FACTS_EXTRACT_TOOL, FACTS_SEARCH_SYSTEM, type FactsDraft, GraphFragment, IO_STEP_CONFIG, type KVLike, LLM_STEP_CONFIG, LOCATE_SYSTEM, LOCATE_TOOL, type LlmConfig, type LlmEnv, MODEL_TIERS, type MergeRule, type NaturalKeys, type Ontology, type PipelineDeps, type Provider, type ProviderFamily, RawItem, type SearchCall, type SourceConnector, type SourceEntry, type StepConfig, type StepLike, Store, Usage, type WriteCall, allJobs, asStepLike, canonicalizeFragment, connectorIds, countyKey, createProvider, enabledSources, enrichCompany, exportFragment, extractJson, extractJsonArray, factsToFragment, fetchText, fetchUrl, findConnectors, fragmentToOps, getConnector, getProvider, htmlToText, humanAttrsOf, ingestFresh, isLikelyPersonName, jobsForHour, locatePrompt, locationFragment, makeContext, mergeAttrs, mergeRulesOf, mergeScalar, naturalKeysOf, normalizeUrl, resilientStep, resolveCounties, resolveName, searchEntities, slugify, toSerializedSchema, urlHash, writeFragment };
|
|
706
|
+
export { type AttrSpec, type AttrType, COMMIT_STEP_CONFIG, type CompanyRow, type Connector, type ConnectorConfig, type ConnectorContext, type ConnectorJob, type ConnectorLens, type ConnectorReport, type ContextConfig, type EdgeSpec, type EnrichModels, type EntitySpec, type Evidence, type ExportEntitySpec, type ExportSpec, type ExtractCall, ExtractResult, FACTS_EXTRACT_SYSTEM, FACTS_EXTRACT_TOOL, FACTS_SEARCH_SYSTEM, type FactsDraft, GraphFragment, IO_STEP_CONFIG, type KVLike, LLM_STEP_CONFIG, LOCATE_SYSTEM, LOCATE_TOOL, type LlmConfig, type LlmEnv, MODEL_TIERS, type MergeRule, type NaturalKeys, type Ontology, PERSON_EXTRACT_SYSTEM, PERSON_EXTRACT_TOOL, PERSON_SEARCH_SYSTEM, type PersonDraft, type PersonEnrichment, type PersonLink, type PersonLinkKind, type PersonResult, type PersonRow, type PipelineDeps, type Provider, type ProviderFamily, RawItem, type SearchCall, type SourceConnector, type SourceEntry, type StepConfig, type StepLike, Store, Usage, type Verdict, type Verification, WRITABLE_VERDICTS, type WriteCall, allJobs, asStepLike, canonicalizeFragment, connectorIds, countyKey, createProvider, enabledSources, enrichCompany, enrichPerson, exportFragment, extractJson, extractJsonArray, factsToFragment, fetchText, fetchUrl, findConnectors, fragmentToOps, getConnector, getProvider, htmlToText, humanAttrsOf, identityKey, ingestFresh, isHttpUrl, isLikelyPersonName, isVerdict, isWritable, jobsForHour, locatePrompt, locationFragment, makeContext, mergeAttrs, mergeRulesOf, mergeScalar, naturalKeysOf, normalizeUrl, normalizeVerification, personPrompt, personToFragment, resilientStep, resolveCounties, resolveName, searchEntities, slugify, toSerializedSchema, urlHash, validateLinks, writeFragment };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { G as GraphFragment, W as WeightedPair, U as Usage, R as RawItem, E as ExtractResult } from './lenses-C7Q7Gvnx.js';
|
|
2
2
|
export { a as GraphEdge, b as GraphNode, c as addUsage, e as emptyUsage } from './lenses-C7Q7Gvnx.js';
|
|
3
3
|
import { Op, AdminDb, SerializedAttr, Cardinality, SerializedSchema, Value, Entity } from '@odla-ai/db';
|
|
4
|
-
import { ExtractTool, Ai } from '@odla-ai/ai';
|
|
4
|
+
import { ExtractTool, SearchCitation, Ai } from '@odla-ai/ai';
|
|
5
5
|
|
|
6
6
|
/** node type → unique natural-key attribute name. */
|
|
7
7
|
type NaturalKeys = Record<string, string>;
|
|
@@ -237,6 +237,120 @@ declare function factsToFragment(company: CompanyRow, draft: FactsDraft): GraphF
|
|
|
237
237
|
/** The `located_in` write linking a company to its resolved county. */
|
|
238
238
|
declare function locationFragment(slug: string, name: string, fips: string): GraphFragment;
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* What a research pass concluded about the entity it was asked to find.
|
|
242
|
+
*
|
|
243
|
+
* `wrong_entity` is deliberately distinct from `not_found`: a pass that
|
|
244
|
+
* confidently describes a *different* person with the same name is a much worse
|
|
245
|
+
* failure than one that admits it found nothing, and a benchmark that collapses
|
|
246
|
+
* the two cannot see it.
|
|
247
|
+
*/
|
|
248
|
+
type Verdict = "confirmed" | "probable" | "ambiguous" | "not_found" | "wrong_entity";
|
|
249
|
+
/** Verdicts that permit writing enriched facts to the graph. */
|
|
250
|
+
declare const WRITABLE_VERDICTS: readonly Verdict[];
|
|
251
|
+
/** One supporting claim tied to the source that backs it. */
|
|
252
|
+
interface Evidence {
|
|
253
|
+
/** What the source establishes, in the model's words. */
|
|
254
|
+
claim: string;
|
|
255
|
+
/** The URL the claim came from. */
|
|
256
|
+
sourceUrl: string;
|
|
257
|
+
/** When the source was read, as an ISO date. */
|
|
258
|
+
retrievedAt?: string;
|
|
259
|
+
}
|
|
260
|
+
/** A verdict plus the confidence and evidence offered for it. */
|
|
261
|
+
interface Verification {
|
|
262
|
+
verdict: Verdict;
|
|
263
|
+
/** The model's stated confidence, clamped to 0..1. Scored for calibration, never trusted. */
|
|
264
|
+
confidence: number;
|
|
265
|
+
evidence: Evidence[];
|
|
266
|
+
}
|
|
267
|
+
/** Whether a value is one of the closed verdict members. */
|
|
268
|
+
declare function isVerdict(value: unknown): value is Verdict;
|
|
269
|
+
/** Whether a verdict permits writing the enriched facts it came with. */
|
|
270
|
+
declare function isWritable(verdict: Verdict): boolean;
|
|
271
|
+
/**
|
|
272
|
+
* Normalize a model's self-reported verification into the closed shape.
|
|
273
|
+
*
|
|
274
|
+
* Fails safe in both directions: an unrecognized verdict becomes `ambiguous`
|
|
275
|
+
* (never `confirmed`), a missing or out-of-range confidence is clamped, and
|
|
276
|
+
* evidence without a usable http(s) source URL is dropped — an unsourced claim
|
|
277
|
+
* is exactly the thing this package exists to stop treating as a fact.
|
|
278
|
+
*/
|
|
279
|
+
declare function normalizeVerification(draft: unknown, retrievedAt?: string): Verification;
|
|
280
|
+
/** Whether a string is an absolute http(s) URL — the only kind evidence may cite. */
|
|
281
|
+
declare function isHttpUrl(value: string): boolean;
|
|
282
|
+
|
|
283
|
+
/** The minimal person row enrichment reads. */
|
|
284
|
+
interface PersonRow {
|
|
285
|
+
slug: string;
|
|
286
|
+
name: string;
|
|
287
|
+
/** Natural key of a company the person is already linked to — the strongest disambiguator. */
|
|
288
|
+
companySlug?: string;
|
|
289
|
+
companyName?: string;
|
|
290
|
+
role?: string;
|
|
291
|
+
city?: string;
|
|
292
|
+
region?: string;
|
|
293
|
+
}
|
|
294
|
+
/** Where a person publishes. Kinds with one canonical host are validated against it. */
|
|
295
|
+
type PersonLinkKind = "website" | "blog" | "substack" | "linkedin" | "x" | "bluesky" | "github" | "other";
|
|
296
|
+
/** System prompt for the grounded person research pass. */
|
|
297
|
+
declare const PERSON_SEARCH_SYSTEM: string;
|
|
298
|
+
/** System prompt for structuring the research notes into {@link PersonDraft}. */
|
|
299
|
+
declare const PERSON_EXTRACT_SYSTEM: string;
|
|
300
|
+
/** Forced-tool schema for a person's verified public profile. */
|
|
301
|
+
declare const PERSON_EXTRACT_TOOL: ExtractTool;
|
|
302
|
+
/** The model's structured person profile, before validation into a fragment. */
|
|
303
|
+
interface PersonDraft {
|
|
304
|
+
verdict?: string;
|
|
305
|
+
confidence?: number;
|
|
306
|
+
links?: Array<{
|
|
307
|
+
kind?: string;
|
|
308
|
+
url?: string;
|
|
309
|
+
}>;
|
|
310
|
+
employer?: string | null;
|
|
311
|
+
role?: string | null;
|
|
312
|
+
businessEmail?: string | null;
|
|
313
|
+
businessPhone?: string | null;
|
|
314
|
+
city?: string | null;
|
|
315
|
+
region?: string | null;
|
|
316
|
+
almaMater?: string | null;
|
|
317
|
+
otherCompanies?: string[];
|
|
318
|
+
evidence?: Array<{
|
|
319
|
+
claim?: string;
|
|
320
|
+
sourceUrl?: string;
|
|
321
|
+
}>;
|
|
322
|
+
}
|
|
323
|
+
/** A validated person link. */
|
|
324
|
+
interface PersonLink {
|
|
325
|
+
kind: PersonLinkKind;
|
|
326
|
+
url: string;
|
|
327
|
+
}
|
|
328
|
+
/** The validated result of one person enrichment pass. */
|
|
329
|
+
interface PersonResult {
|
|
330
|
+
verification: Verification;
|
|
331
|
+
links: PersonLink[];
|
|
332
|
+
fragment: GraphFragment;
|
|
333
|
+
}
|
|
334
|
+
/** The user prompt for a person research pass. */
|
|
335
|
+
declare function personPrompt(person: PersonRow): string;
|
|
336
|
+
/**
|
|
337
|
+
* Keep only links that are plausibly real: absolute https URLs, on the canonical
|
|
338
|
+
* host when the kind has one, and never a placeholder domain. Models invent
|
|
339
|
+
* `linkedin.com/in/first-last` from a name constantly; a kind/host mismatch is
|
|
340
|
+
* the cheapest way to catch it before it becomes a stored "fact".
|
|
341
|
+
*/
|
|
342
|
+
declare function validateLinks(raw: PersonDraft["links"], limit?: number): PersonLink[];
|
|
343
|
+
/**
|
|
344
|
+
* Turn one person's structured draft into a verdict plus an idempotent fragment.
|
|
345
|
+
*
|
|
346
|
+
* The fragment is empty unless the verdict is writable — an `ambiguous`,
|
|
347
|
+
* `not_found`, or `wrong_entity` pass contributes no graph, however confident
|
|
348
|
+
* the model sounded. The verdict and its evidence are still returned, because
|
|
349
|
+
* "this lane confidently found the wrong person" is exactly what a benchmark
|
|
350
|
+
* needs to record.
|
|
351
|
+
*/
|
|
352
|
+
declare function personToFragment(person: PersonRow, draft: PersonDraft, retrievedAt?: string): PersonResult;
|
|
353
|
+
|
|
240
354
|
/** Which model serves each verb (search must be a web-grounded model). */
|
|
241
355
|
interface EnrichModels {
|
|
242
356
|
/** Web-search-grounded research model. */
|
|
@@ -251,6 +365,23 @@ interface EnrichModels {
|
|
|
251
365
|
* fragment (possibly empty — nothing established); the caller writes it.
|
|
252
366
|
*/
|
|
253
367
|
declare function enrichCompany(ai: Ai, models: EnrichModels, company: CompanyRow): Promise<GraphFragment>;
|
|
368
|
+
/** One person enrichment pass, with the sources the provider actually cited. */
|
|
369
|
+
interface PersonEnrichment extends PersonResult {
|
|
370
|
+
/** Sources the search model attributed. Empty when the provider grounded nothing. */
|
|
371
|
+
citations: SearchCitation[];
|
|
372
|
+
/** The research prose the extraction was taken from — useful for adjudication. */
|
|
373
|
+
notes: string;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Enrich one person: a grounded web search for their public professional
|
|
377
|
+
* presence, then a structured extraction into a validated verdict + fragment.
|
|
378
|
+
*
|
|
379
|
+
* Unlike {@link enrichCompany}, this always returns a verdict — including
|
|
380
|
+
* `not_found` and `wrong_entity` — because "the lane confidently identified the
|
|
381
|
+
* wrong person" is a result worth recording, not an error. The fragment is empty
|
|
382
|
+
* unless the verdict permits writing.
|
|
383
|
+
*/
|
|
384
|
+
declare function enrichPerson(ai: Ai, models: EnrichModels, person: PersonRow, retrievedAt?: string): Promise<PersonEnrichment>;
|
|
254
385
|
/**
|
|
255
386
|
* Resolve a batch of companies' free-text locations to county FIPS codes via a
|
|
256
387
|
* single structured extraction, joined against a caller-supplied gazetteer
|
|
@@ -541,6 +672,17 @@ declare function ingestFresh(step: StepLike, deps: PipelineDeps, fresh: RawItem[
|
|
|
541
672
|
*/
|
|
542
673
|
/** NFKD-normalize, strip accents, lowercase, non-alphanumerics → "-", cap 60. */
|
|
543
674
|
declare function slugify(input: string): string;
|
|
675
|
+
/**
|
|
676
|
+
* Collapse a name to a comparison key: accents stripped, `&` spelled out,
|
|
677
|
+
* punctuation and spacing removed, and trailing legal suffixes dropped.
|
|
678
|
+
*
|
|
679
|
+
* Stronger than comparing {@link slugify} output, because it matches
|
|
680
|
+
* "Acme Corp.", "Acme, Inc." and "Acme & Co" onto the same key — the shapes the
|
|
681
|
+
* same organization is actually written in across sources. Use it to decide
|
|
682
|
+
* whether two records are the same entity; use `slugify` for the natural key
|
|
683
|
+
* that gets stored.
|
|
684
|
+
*/
|
|
685
|
+
declare function identityKey(value: string): string;
|
|
544
686
|
/** Drop tracking params, hash, and trailing slash so the same page maps to one key. */
|
|
545
687
|
declare function normalizeUrl(raw: string): string;
|
|
546
688
|
/** Stable short key for a URL (FNV-1a 32-bit, base36). Used as the `signal` natural key. */
|
|
@@ -561,4 +703,4 @@ declare function htmlToText(html: string, maxChars?: number): string;
|
|
|
561
703
|
/** Fetch a URL and return readable text. Best-effort: returns "" on any failure. */
|
|
562
704
|
declare function fetchText(url: string, maxChars?: number): Promise<string>;
|
|
563
705
|
|
|
564
|
-
export { type AttrSpec, type AttrType, COMMIT_STEP_CONFIG, type CompanyRow, type Connector, type ConnectorConfig, type ConnectorContext, type ConnectorJob, type ConnectorLens, type ConnectorReport, type ContextConfig, type EdgeSpec, type EnrichModels, type EntitySpec, type ExportEntitySpec, type ExportSpec, type ExtractCall, ExtractResult, FACTS_EXTRACT_SYSTEM, FACTS_EXTRACT_TOOL, FACTS_SEARCH_SYSTEM, type FactsDraft, GraphFragment, IO_STEP_CONFIG, type KVLike, LLM_STEP_CONFIG, LOCATE_SYSTEM, LOCATE_TOOL, type LlmConfig, type LlmEnv, MODEL_TIERS, type MergeRule, type NaturalKeys, type Ontology, type PipelineDeps, type Provider, type ProviderFamily, RawItem, type SearchCall, type SourceConnector, type SourceEntry, type StepConfig, type StepLike, Store, Usage, type WriteCall, allJobs, asStepLike, canonicalizeFragment, connectorIds, countyKey, createProvider, enabledSources, enrichCompany, exportFragment, extractJson, extractJsonArray, factsToFragment, fetchText, fetchUrl, findConnectors, fragmentToOps, getConnector, getProvider, htmlToText, humanAttrsOf, ingestFresh, isLikelyPersonName, jobsForHour, locatePrompt, locationFragment, makeContext, mergeAttrs, mergeRulesOf, mergeScalar, naturalKeysOf, normalizeUrl, resilientStep, resolveCounties, resolveName, searchEntities, slugify, toSerializedSchema, urlHash, writeFragment };
|
|
706
|
+
export { type AttrSpec, type AttrType, COMMIT_STEP_CONFIG, type CompanyRow, type Connector, type ConnectorConfig, type ConnectorContext, type ConnectorJob, type ConnectorLens, type ConnectorReport, type ContextConfig, type EdgeSpec, type EnrichModels, type EntitySpec, type Evidence, type ExportEntitySpec, type ExportSpec, type ExtractCall, ExtractResult, FACTS_EXTRACT_SYSTEM, FACTS_EXTRACT_TOOL, FACTS_SEARCH_SYSTEM, type FactsDraft, GraphFragment, IO_STEP_CONFIG, type KVLike, LLM_STEP_CONFIG, LOCATE_SYSTEM, LOCATE_TOOL, type LlmConfig, type LlmEnv, MODEL_TIERS, type MergeRule, type NaturalKeys, type Ontology, PERSON_EXTRACT_SYSTEM, PERSON_EXTRACT_TOOL, PERSON_SEARCH_SYSTEM, type PersonDraft, type PersonEnrichment, type PersonLink, type PersonLinkKind, type PersonResult, type PersonRow, type PipelineDeps, type Provider, type ProviderFamily, RawItem, type SearchCall, type SourceConnector, type SourceEntry, type StepConfig, type StepLike, Store, Usage, type Verdict, type Verification, WRITABLE_VERDICTS, type WriteCall, allJobs, asStepLike, canonicalizeFragment, connectorIds, countyKey, createProvider, enabledSources, enrichCompany, enrichPerson, exportFragment, extractJson, extractJsonArray, factsToFragment, fetchText, fetchUrl, findConnectors, fragmentToOps, getConnector, getProvider, htmlToText, humanAttrsOf, identityKey, ingestFresh, isHttpUrl, isLikelyPersonName, isVerdict, isWritable, jobsForHour, locatePrompt, locationFragment, makeContext, mergeAttrs, mergeRulesOf, mergeScalar, naturalKeysOf, normalizeUrl, normalizeVerification, personPrompt, personToFragment, resilientStep, resolveCounties, resolveName, searchEntities, slugify, toSerializedSchema, urlHash, validateLinks, writeFragment };
|
package/dist/index.js
CHANGED
|
@@ -345,6 +345,15 @@ function findConnectors(fragment, config) {
|
|
|
345
345
|
function slugify(input) {
|
|
346
346
|
return input.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
|
|
347
347
|
}
|
|
348
|
+
var LEGAL_SUFFIXES = /* @__PURE__ */ new Set(["co", "company", "corp", "corporation", "inc", "incorporated", "llc", "llp", "ltd", "limited", "pllc"]);
|
|
349
|
+
function identityKey(value) {
|
|
350
|
+
const tokens = value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/&/g, " and ").match(/[a-z0-9]+/g) ?? [];
|
|
351
|
+
while (tokens.length > 1 && LEGAL_SUFFIXES.has(tokens.at(-1) ?? "")) {
|
|
352
|
+
tokens.pop();
|
|
353
|
+
if (tokens.at(-1) === "and") tokens.pop();
|
|
354
|
+
}
|
|
355
|
+
return tokens.join("");
|
|
356
|
+
}
|
|
348
357
|
function normalizeUrl(raw) {
|
|
349
358
|
try {
|
|
350
359
|
const u = new URL(raw);
|
|
@@ -487,6 +496,160 @@ function locationFragment(slug, name, fips) {
|
|
|
487
496
|
};
|
|
488
497
|
}
|
|
489
498
|
|
|
499
|
+
// src/enrich/verify.ts
|
|
500
|
+
var WRITABLE_VERDICTS = ["confirmed", "probable"];
|
|
501
|
+
var ALL = ["confirmed", "probable", "ambiguous", "not_found", "wrong_entity"];
|
|
502
|
+
function isVerdict(value) {
|
|
503
|
+
return typeof value === "string" && ALL.includes(value);
|
|
504
|
+
}
|
|
505
|
+
function isWritable(verdict) {
|
|
506
|
+
return WRITABLE_VERDICTS.includes(verdict);
|
|
507
|
+
}
|
|
508
|
+
function normalizeVerification(draft, retrievedAt) {
|
|
509
|
+
const d = draft ?? {};
|
|
510
|
+
const verdict = isVerdict(d.verdict) ? d.verdict : "ambiguous";
|
|
511
|
+
const raw = typeof d.confidence === "number" && Number.isFinite(d.confidence) ? d.confidence : 0;
|
|
512
|
+
const confidence = Math.min(1, Math.max(0, raw));
|
|
513
|
+
const evidence = [];
|
|
514
|
+
for (const item of Array.isArray(d.evidence) ? d.evidence.slice(0, 12) : []) {
|
|
515
|
+
const e = item ?? {};
|
|
516
|
+
const sourceUrl = typeof e.sourceUrl === "string" ? e.sourceUrl.trim() : "";
|
|
517
|
+
const claim = typeof e.claim === "string" ? e.claim.trim() : "";
|
|
518
|
+
if (!claim || !isHttpUrl(sourceUrl)) continue;
|
|
519
|
+
evidence.push({ claim: claim.slice(0, 300), sourceUrl, ...retrievedAt ? { retrievedAt } : {} });
|
|
520
|
+
}
|
|
521
|
+
if (evidence.length === 0 && isWritable(verdict)) return { verdict: "ambiguous", confidence, evidence };
|
|
522
|
+
return { verdict, confidence, evidence };
|
|
523
|
+
}
|
|
524
|
+
function isHttpUrl(value) {
|
|
525
|
+
try {
|
|
526
|
+
const u = new URL(value);
|
|
527
|
+
return u.protocol === "https:" || u.protocol === "http:";
|
|
528
|
+
} catch {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/enrich/person.ts
|
|
534
|
+
var LINK_KINDS = ["website", "blog", "substack", "linkedin", "x", "bluesky", "github", "other"];
|
|
535
|
+
var CANONICAL_HOSTS = {
|
|
536
|
+
linkedin: ["linkedin.com"],
|
|
537
|
+
github: ["github.com"],
|
|
538
|
+
x: ["x.com", "twitter.com"],
|
|
539
|
+
bluesky: ["bsky.app"]
|
|
540
|
+
};
|
|
541
|
+
var PLACEHOLDER_HOSTS = ["example.com", "example.org", "example.net", "yourdomain.com", "domain.com"];
|
|
542
|
+
var PERSON_SEARCH_SYSTEM = "You are a research assistant with web search, confirming the identity of a specific named person and finding their PUBLIC PROFESSIONAL presence. Look for: a personal website, blog, or Substack; LinkedIn, X/Bluesky, or GitHub profiles; their current employer and role; and a business email or phone that they or their employer have published openly (a company contact page, a conference bio). Do NOT look for or report home addresses, personal phone numbers, or personal email addresses. Also report disambiguating facts: city or region, alma mater, and other companies named alongside them. Names are not unique. If the results describe more than one person with this name and you cannot tell which is meant, say so explicitly. If you find a different person with the same name, say that too. Cite the URL for every claim. Never guess a profile URL from a name pattern \u2014 report only URLs you actually saw. If you found nothing, say so plainly.";
|
|
543
|
+
var PERSON_EXTRACT_SYSTEM = "Extract the structured profile from the research notes. Use null for anything the notes do not establish. Set verdict to 'confirmed' only when the notes identify this exact person beyond doubt; 'probable' when the evidence is strong but circumstantial; 'ambiguous' when several same-named people cannot be told apart; 'not_found' when nothing was established; 'wrong_entity' when the notes describe a different person with the same name. Every claim in evidence must carry the source URL it came from.";
|
|
544
|
+
var PERSON_EXTRACT_TOOL = {
|
|
545
|
+
name: "person_profile",
|
|
546
|
+
description: "Verified identity plus public professional profile for one person",
|
|
547
|
+
parameters: {
|
|
548
|
+
type: "object",
|
|
549
|
+
properties: {
|
|
550
|
+
verdict: { type: "string", enum: ["confirmed", "probable", "ambiguous", "not_found", "wrong_entity"] },
|
|
551
|
+
confidence: { type: "number", description: "0..1 \u2014 how sure you are this is the right person" },
|
|
552
|
+
links: {
|
|
553
|
+
type: "array",
|
|
554
|
+
description: "Only URLs actually seen in the results; never a guessed profile pattern.",
|
|
555
|
+
items: {
|
|
556
|
+
type: "object",
|
|
557
|
+
properties: {
|
|
558
|
+
kind: { type: "string", enum: [...LINK_KINDS] },
|
|
559
|
+
url: { type: "string" }
|
|
560
|
+
},
|
|
561
|
+
required: ["kind", "url"]
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
employer: { type: ["string", "null"], description: "Current employer name" },
|
|
565
|
+
role: { type: ["string", "null"], description: "Current title at that employer" },
|
|
566
|
+
businessEmail: { type: ["string", "null"], description: "Publicly published work email only" },
|
|
567
|
+
businessPhone: { type: ["string", "null"], description: "Publicly published work phone only" },
|
|
568
|
+
city: { type: ["string", "null"] },
|
|
569
|
+
region: { type: ["string", "null"], description: "State, province, or country" },
|
|
570
|
+
almaMater: { type: ["string", "null"] },
|
|
571
|
+
otherCompanies: { type: "array", items: { type: "string" }, description: "Companies named alongside them" },
|
|
572
|
+
evidence: {
|
|
573
|
+
type: "array",
|
|
574
|
+
items: {
|
|
575
|
+
type: "object",
|
|
576
|
+
properties: { claim: { type: "string" }, sourceUrl: { type: "string" } },
|
|
577
|
+
required: ["claim", "sourceUrl"]
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
required: ["verdict", "confidence", "links", "evidence"]
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
function personPrompt(person) {
|
|
585
|
+
const at = person.companyName ? ` (${person.role ? `${person.role}, ` : ""}${person.companyName})` : "";
|
|
586
|
+
const where = [person.city, person.region].filter(Boolean).join(", ");
|
|
587
|
+
return `Person: ${person.name}${at}${where ? `
|
|
588
|
+
Known location: ${where}` : ""}
|
|
589
|
+
|
|
590
|
+
Confirm this is a real, identifiable person and find their public professional presence.`;
|
|
591
|
+
}
|
|
592
|
+
function validateLinks(raw, limit = 8) {
|
|
593
|
+
const out = [];
|
|
594
|
+
const seen = /* @__PURE__ */ new Set();
|
|
595
|
+
for (const item of (raw ?? []).slice(0, 24)) {
|
|
596
|
+
const kind = LINK_KINDS.includes(String(item?.kind)) ? item.kind : "other";
|
|
597
|
+
const url = String(item?.url ?? "").trim();
|
|
598
|
+
const parsed = httpsUrl(url);
|
|
599
|
+
if (!parsed) continue;
|
|
600
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
|
|
601
|
+
if (PLACEHOLDER_HOSTS.includes(host)) continue;
|
|
602
|
+
const required = CANONICAL_HOSTS[kind];
|
|
603
|
+
if (required && !required.some((h) => host === h || host.endsWith(`.${h}`))) continue;
|
|
604
|
+
if (seen.has(url)) continue;
|
|
605
|
+
seen.add(url);
|
|
606
|
+
out.push({ kind, url });
|
|
607
|
+
if (out.length >= limit) break;
|
|
608
|
+
}
|
|
609
|
+
return out;
|
|
610
|
+
}
|
|
611
|
+
function personToFragment(person, draft, retrievedAt) {
|
|
612
|
+
const verification = normalizeVerification(draft, retrievedAt);
|
|
613
|
+
const links = validateLinks(draft.links);
|
|
614
|
+
if (!isWritable(verification.verdict)) return { verification, links, fragment: { nodes: [], edges: [] } };
|
|
615
|
+
const attrs = { name: person.name };
|
|
616
|
+
for (const link of links) if (!attrs[link.kind]) attrs[link.kind] = link.url;
|
|
617
|
+
const city = str(draft.city, 60);
|
|
618
|
+
if (city && !person.city) attrs.city = city;
|
|
619
|
+
const region = str(draft.region, 60);
|
|
620
|
+
if (region && !person.region) attrs.region = region;
|
|
621
|
+
const email = str(draft.businessEmail, 120);
|
|
622
|
+
if (email && /^[^@\s]+@[^@\s.]+\.[^@\s]+$/.test(email)) attrs.businessEmail = email;
|
|
623
|
+
const phone = str(draft.businessPhone, 40);
|
|
624
|
+
if (phone) attrs.businessPhone = phone;
|
|
625
|
+
const almaMater = str(draft.almaMater, 120);
|
|
626
|
+
if (almaMater) attrs.almaMater = almaMater;
|
|
627
|
+
const nodes = [{ type: "person", key: person.slug, attrs }];
|
|
628
|
+
const edges = [];
|
|
629
|
+
const employer = str(draft.employer, 120);
|
|
630
|
+
const known = person.companySlug && person.companyName && identityKey(person.companyName) === identityKey(employer);
|
|
631
|
+
const companySlug = employer ? known ? person.companySlug : slugify(employer) : "";
|
|
632
|
+
if (employer && companySlug) {
|
|
633
|
+
const key = `${person.slug}~${companySlug}`;
|
|
634
|
+
nodes.push({ type: "company", key: companySlug, attrs: { name: known ? person.companyName : employer } });
|
|
635
|
+
nodes.push({ type: "affiliation", key, attrs: { role: str(draft.role, 60) || person.role || "" } });
|
|
636
|
+
edges.push({ from: { type: "affiliation", key }, to: { type: "person", key: person.slug }, label: "of_person" });
|
|
637
|
+
edges.push({ from: { type: "affiliation", key }, to: { type: "company", key: companySlug }, label: "of_company" });
|
|
638
|
+
}
|
|
639
|
+
return { verification, links, fragment: { nodes, edges } };
|
|
640
|
+
}
|
|
641
|
+
function str(value, max) {
|
|
642
|
+
return typeof value === "string" ? value.trim().slice(0, max) : "";
|
|
643
|
+
}
|
|
644
|
+
function httpsUrl(value) {
|
|
645
|
+
try {
|
|
646
|
+
const url = new URL(value);
|
|
647
|
+
return url.protocol === "https:" ? url : null;
|
|
648
|
+
} catch {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
490
653
|
// src/enrich/run.ts
|
|
491
654
|
async function enrichCompany(ai, models, company) {
|
|
492
655
|
const where = `${company.city ? `, ${company.city}` : ""}${company.region ? ` (${company.region})` : ""}`;
|
|
@@ -510,6 +673,27 @@ ${text}`,
|
|
|
510
673
|
const { value } = await extractOnce().catch(extractOnce);
|
|
511
674
|
return factsToFragment(company, value);
|
|
512
675
|
}
|
|
676
|
+
async function enrichPerson(ai, models, person, retrievedAt) {
|
|
677
|
+
const { text, citations } = await ai.search({
|
|
678
|
+
model: models.search,
|
|
679
|
+
system: PERSON_SEARCH_SYSTEM,
|
|
680
|
+
user: personPrompt(person),
|
|
681
|
+
maxTokens: 1200
|
|
682
|
+
});
|
|
683
|
+
const extractOnce = () => ai.extract({
|
|
684
|
+
model: models.extract,
|
|
685
|
+
system: PERSON_EXTRACT_SYSTEM,
|
|
686
|
+
user: `Person: ${person.name}
|
|
687
|
+
|
|
688
|
+
Notes:
|
|
689
|
+
${text}`,
|
|
690
|
+
tool: PERSON_EXTRACT_TOOL,
|
|
691
|
+
maxTokens: 4e3
|
|
692
|
+
// reasoning headroom before the forced tool call
|
|
693
|
+
});
|
|
694
|
+
const { value } = await extractOnce().catch(extractOnce);
|
|
695
|
+
return { ...personToFragment(person, value, retrievedAt), citations, notes: text };
|
|
696
|
+
}
|
|
513
697
|
async function resolveCounties(ai, model, rows, gazetteer) {
|
|
514
698
|
const { value } = await ai.extract({
|
|
515
699
|
model,
|
|
@@ -1215,7 +1399,11 @@ export {
|
|
|
1215
1399
|
LOCATE_SYSTEM,
|
|
1216
1400
|
LOCATE_TOOL,
|
|
1217
1401
|
MODEL_TIERS,
|
|
1402
|
+
PERSON_EXTRACT_SYSTEM,
|
|
1403
|
+
PERSON_EXTRACT_TOOL,
|
|
1404
|
+
PERSON_SEARCH_SYSTEM,
|
|
1218
1405
|
Store,
|
|
1406
|
+
WRITABLE_VERDICTS,
|
|
1219
1407
|
addUsage,
|
|
1220
1408
|
allJobs,
|
|
1221
1409
|
asStepLike,
|
|
@@ -1226,6 +1414,7 @@ export {
|
|
|
1226
1414
|
emptyUsage,
|
|
1227
1415
|
enabledSources,
|
|
1228
1416
|
enrichCompany,
|
|
1417
|
+
enrichPerson,
|
|
1229
1418
|
exportFragment,
|
|
1230
1419
|
extractJson,
|
|
1231
1420
|
extractJsonArray,
|
|
@@ -1238,8 +1427,12 @@ export {
|
|
|
1238
1427
|
getProvider,
|
|
1239
1428
|
htmlToText,
|
|
1240
1429
|
humanAttrsOf,
|
|
1430
|
+
identityKey,
|
|
1241
1431
|
ingestFresh,
|
|
1432
|
+
isHttpUrl,
|
|
1242
1433
|
isLikelyPersonName,
|
|
1434
|
+
isVerdict,
|
|
1435
|
+
isWritable,
|
|
1243
1436
|
jobsForHour,
|
|
1244
1437
|
locatePrompt,
|
|
1245
1438
|
locationFragment,
|
|
@@ -1249,6 +1442,9 @@ export {
|
|
|
1249
1442
|
mergeScalar,
|
|
1250
1443
|
naturalKeysOf,
|
|
1251
1444
|
normalizeUrl,
|
|
1445
|
+
normalizeVerification,
|
|
1446
|
+
personPrompt,
|
|
1447
|
+
personToFragment,
|
|
1252
1448
|
resilientStep,
|
|
1253
1449
|
resolveCounties,
|
|
1254
1450
|
resolveName,
|
|
@@ -1256,6 +1452,7 @@ export {
|
|
|
1256
1452
|
slugify,
|
|
1257
1453
|
toSerializedSchema,
|
|
1258
1454
|
urlHash,
|
|
1455
|
+
validateLinks,
|
|
1259
1456
|
writeFragment
|
|
1260
1457
|
};
|
|
1261
1458
|
//# sourceMappingURL=index.js.map
|