@startsimpli/integrations 0.2.13 → 0.2.16
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/package.json +2 -2
- package/src/__tests__/connections.test.tsx +165 -0
- package/src/__tests__/corpus-ask.test.ts +19 -1
- package/src/__tests__/corpus-copy.test.tsx +4 -1
- package/src/__tests__/corpus-field-gate.test.ts +246 -0
- package/src/__tests__/corpus-pages.test.tsx +58 -2
- package/src/__tests__/corpus-tracker.test.tsx +43 -0
- package/src/__tests__/corpus-transcript.test.ts +476 -0
- package/src/components/CredentialSchemaForm.tsx +55 -8
- package/src/components/pages/AskPage.tsx +11 -0
- package/src/components/pages/ConnectionsPage.tsx +142 -5
- package/src/components/pages/LibraryPage.tsx +56 -3
- package/src/components/pages/TrackerPage.tsx +17 -0
- package/src/corpus/ask.ts +33 -8
- package/src/corpus/entities.ts +56 -2
- package/src/corpus/extract.ts +340 -15
- package/src/corpus/fixtures.ts +9 -1
- package/src/corpus/llm.ts +80 -1
- package/src/corpus/retrieval.ts +28 -4
- package/src/corpus/text.ts +76 -0
- package/src/corpus/types.ts +108 -2
package/src/corpus/text.ts
CHANGED
|
@@ -281,3 +281,79 @@ const OUTCOME_HEADING = /\b(next\s*steps?|action\s*items?|follow[\s-]?ups?|to[\s
|
|
|
281
281
|
export function outcomeSections(text: string): TextSection[] {
|
|
282
282
|
return splitSections(text).filter((section) => OUTCOME_HEADING.test(section.heading));
|
|
283
283
|
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// assembling a document's text
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
/** The text-bearing shape, so this file still knows nothing about the corpus. */
|
|
290
|
+
interface TextBearing {
|
|
291
|
+
body: string;
|
|
292
|
+
summary?: string;
|
|
293
|
+
sections: Array<{ label: string; text: string }>;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Every distinct piece of text a document carries, summary FIRST.
|
|
298
|
+
*
|
|
299
|
+
* DE-DUPLICATED, because `body` deliberately falls back to `summary` on a corpus
|
|
300
|
+
* that has never seen a full text — and sending the same 5,000 characters to a
|
|
301
|
+
* model twice is a bill, not a feature.
|
|
302
|
+
*
|
|
303
|
+
* SUMMARY FIRST is not cosmetic. It is the piece a person already curated, the
|
|
304
|
+
* piece the deterministic pass reads, and the piece that must survive when the
|
|
305
|
+
* rest has to be cut.
|
|
306
|
+
*/
|
|
307
|
+
export function documentParts(document: TextBearing): string[] {
|
|
308
|
+
const summary = (document.summary ?? '').trim();
|
|
309
|
+
const body = (document.body ?? '').trim();
|
|
310
|
+
const parts = [summary, body === summary ? '' : body];
|
|
311
|
+
for (const section of document.sections ?? []) {
|
|
312
|
+
const text = (section.text ?? '').trim();
|
|
313
|
+
if (!text || parts.includes(text)) continue;
|
|
314
|
+
parts.push(`## ${section.label}\n${text}`);
|
|
315
|
+
}
|
|
316
|
+
return parts.filter(Boolean);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Trim to a budget while KEEPING THE END.
|
|
321
|
+
*
|
|
322
|
+
* A prefix `slice(0, maxChars)` is the wrong cut for a conversation. People
|
|
323
|
+
* agree things in the last five minutes of a meeting — "I'll send it Friday",
|
|
324
|
+
* "put that on Megan" — so a head-first truncation deletes precisely the half
|
|
325
|
+
* the extractor exists to read, and does it silently. Keeping a quarter from the
|
|
326
|
+
* front preserves who is in the room and what it is about; the rest of the
|
|
327
|
+
* budget goes to the end.
|
|
328
|
+
*/
|
|
329
|
+
export function trimKeepingTheEnd(text: string, maxChars: number): string {
|
|
330
|
+
if (maxChars <= 0) return '';
|
|
331
|
+
if (text.length <= maxChars) return text;
|
|
332
|
+
const head = Math.floor(maxChars * 0.25);
|
|
333
|
+
const tail = maxChars - head;
|
|
334
|
+
return [
|
|
335
|
+
text.slice(0, head),
|
|
336
|
+
`…[truncated ${text.length - maxChars} characters]…`,
|
|
337
|
+
text.slice(text.length - tail),
|
|
338
|
+
].join('\n');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* A document as one string for a prompt, inside a character budget.
|
|
343
|
+
*
|
|
344
|
+
* THE SUMMARY IS PAID FOR FIRST and only trimmed if it alone does not fit. Once
|
|
345
|
+
* a transcript lands, `body` is ten times the size of the summary; a naive join
|
|
346
|
+
* plus a prefix cut spends the entire budget on the opening minutes of the
|
|
347
|
+
* meeting and never reaches the outcome block at all — on the Ask surface, whose
|
|
348
|
+
* per-document budget is 6,000 characters, the summary would simply stop
|
|
349
|
+
* reaching the model.
|
|
350
|
+
*/
|
|
351
|
+
export function documentText(document: TextBearing, maxChars: number): string {
|
|
352
|
+
const [first = '', ...rest] = documentParts(document);
|
|
353
|
+
const summary = trimKeepingTheEnd(first, maxChars);
|
|
354
|
+
const verbatim = rest.join('\n\n');
|
|
355
|
+
if (!verbatim) return summary;
|
|
356
|
+
const remaining = maxChars - summary.length - 2;
|
|
357
|
+
const kept = trimKeepingTheEnd(verbatim, remaining);
|
|
358
|
+
return [summary, kept].filter(Boolean).join('\n\n');
|
|
359
|
+
}
|
package/src/corpus/types.ts
CHANGED
|
@@ -92,8 +92,26 @@ export interface DocumentSection {
|
|
|
92
92
|
export interface SourceDocument {
|
|
93
93
|
id: string;
|
|
94
94
|
title: string;
|
|
95
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* The FULL text — a transcript, a PDF's extracted text, a document's contents.
|
|
97
|
+
*
|
|
98
|
+
* Falls back to `summary` when the source has never delivered a full text, so
|
|
99
|
+
* that every consumer of `body` keeps working on a corpus of summaries. When
|
|
100
|
+
* both exist they are DIFFERENT strings and both survive; see `summary`.
|
|
101
|
+
*/
|
|
96
102
|
body: string;
|
|
103
|
+
/**
|
|
104
|
+
* The short form somebody or something already WROTE — Granola's AI summary, a
|
|
105
|
+
* file's description. Two fields and not one, exactly as the tenant's canonical
|
|
106
|
+
* schema declares them, because the day a transcript arrives is the day a
|
|
107
|
+
* single field would silently lose the summary.
|
|
108
|
+
*
|
|
109
|
+
* THAT IS NOT HYPOTHETICAL. This mapper collapsed them for the whole of the
|
|
110
|
+
* epic so far, and the collapse would have deleted the deterministic rules pass
|
|
111
|
+
* — the `## Next Steps` block lives in the summary and nowhere else — on the
|
|
112
|
+
* exact deploy that first switched transcripts on.
|
|
113
|
+
*/
|
|
114
|
+
summary?: string;
|
|
97
115
|
/** Additional long-form the source provided. See `DocumentSection`. */
|
|
98
116
|
sections: DocumentSection[];
|
|
99
117
|
/** When the thing HAPPENED, which is not when it was ingested. */
|
|
@@ -109,6 +127,19 @@ export interface SourceDocument {
|
|
|
109
127
|
scope: CorpusScope;
|
|
110
128
|
/** When the connector last wrote this row. Drives the freshness claim. */
|
|
111
129
|
ingestedAt?: string | null;
|
|
130
|
+
/**
|
|
131
|
+
* Attribute names the SERVER withheld from this reader
|
|
132
|
+
* (bd startsim-768w.38.33). The tenant's per-field read gate strips them from
|
|
133
|
+
* `data` and names them back in `redacted_fields`; this carries that fact all
|
|
134
|
+
* the way to the screen and into the prompt.
|
|
135
|
+
*
|
|
136
|
+
* IT HAS TO TRAVEL, and that is the whole reason it is on the document rather
|
|
137
|
+
* than swallowed at the mapper. A record whose transcript was withheld is NOT
|
|
138
|
+
* a record with no transcript. Anything that cannot tell the two apart — a
|
|
139
|
+
* reader, or a model asked "was this discussed?" — answers "no" with total
|
|
140
|
+
* confidence about text it was simply never shown.
|
|
141
|
+
*/
|
|
142
|
+
withheldFields?: string[];
|
|
112
143
|
}
|
|
113
144
|
|
|
114
145
|
/**
|
|
@@ -281,6 +312,14 @@ export interface AskResult {
|
|
|
281
312
|
* returned something it should not have, and is worth seeing.
|
|
282
313
|
*/
|
|
283
314
|
droppedOutOfScope: number;
|
|
315
|
+
/**
|
|
316
|
+
* Attribute names the server withheld from this reader on at least one record
|
|
317
|
+
* that was considered (bd startsim-768w.38.33). Reported for exactly the
|
|
318
|
+
* reason `droppedOutOfScope` is: an answer built from a partial record has to
|
|
319
|
+
* say so, or "your corpus has nothing about that" and "you were not shown the
|
|
320
|
+
* part that does" become the same sentence.
|
|
321
|
+
*/
|
|
322
|
+
withheldFields?: string[];
|
|
284
323
|
/** Which provider/model actually answered, and whether it was a real one. */
|
|
285
324
|
provider: LlmDescription;
|
|
286
325
|
/** Freshness at answer time — an answer over a stale corpus says so. */
|
|
@@ -305,6 +344,13 @@ export interface PromptDocument {
|
|
|
305
344
|
occurredAt: string | null;
|
|
306
345
|
participants: string[];
|
|
307
346
|
text: string;
|
|
347
|
+
/**
|
|
348
|
+
* Fields the reader was not allowed to see (bd startsim-768w.38.33). Rendered
|
|
349
|
+
* into the document block so the model states a limit instead of inventing a
|
|
350
|
+
* certainty: without it, a summary-only view of a meeting whose transcript was
|
|
351
|
+
* withheld reads as proof the thing was never discussed.
|
|
352
|
+
*/
|
|
353
|
+
withheldFields?: string[];
|
|
308
354
|
}
|
|
309
355
|
|
|
310
356
|
export interface AnswerRequest {
|
|
@@ -515,9 +561,56 @@ export interface CommitmentApi {
|
|
|
515
561
|
*/
|
|
516
562
|
export interface CorpusExtractorLlm {
|
|
517
563
|
extract(input: ExtractionRequest): Promise<ExtractionDraft>;
|
|
564
|
+
/**
|
|
565
|
+
* CONFIRM the model's own candidates against the records they came from.
|
|
566
|
+
*
|
|
567
|
+
* OPTIONAL, and the run reports `verified: false` when it is missing rather
|
|
568
|
+
* than pretending a check happened. Present on the shipped adapter.
|
|
569
|
+
*
|
|
570
|
+
* WHY IT EXISTS. More text raises recall and lowers precision, and transcripts
|
|
571
|
+
* are roughly ten times the text of a summary. Measured on the summaries
|
|
572
|
+
* alone, adding the model pass took recall 56.1% → 65.9% and precision 95.8% →
|
|
573
|
+
* 67.5%; a corpus of verbatim speech makes that trade worse, not better,
|
|
574
|
+
* because a transcript is full of sentences that SOUND like commitments and
|
|
575
|
+
* are hedges, jokes, or somebody reading an agenda aloud.
|
|
576
|
+
*
|
|
577
|
+
* ONE BATCHED CALL over every candidate with echoed ids
|
|
578
|
+
* (feedback_batch_llm_calls) — never a call per item.
|
|
579
|
+
*/
|
|
580
|
+
verify?(input: VerificationRequest): Promise<VerificationDraft>;
|
|
518
581
|
describe(): LlmDescription;
|
|
519
582
|
}
|
|
520
583
|
|
|
584
|
+
/**
|
|
585
|
+
* What the verifier is asked. The candidates AND the records they claim to come
|
|
586
|
+
* from, so the check is against the source rather than against the candidate's
|
|
587
|
+
* own plausibility.
|
|
588
|
+
*/
|
|
589
|
+
export interface VerificationRequest {
|
|
590
|
+
documents: Array<{ id: string; title: string; text: string }>;
|
|
591
|
+
candidates: Array<{
|
|
592
|
+
/** Echoed back on the verdict. Stable only within this call. */
|
|
593
|
+
id: string;
|
|
594
|
+
documentId: string;
|
|
595
|
+
subtype: string;
|
|
596
|
+
title: string;
|
|
597
|
+
owner?: string;
|
|
598
|
+
/** The span the extractor said this came from. */
|
|
599
|
+
quote: string;
|
|
600
|
+
}>;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export interface VerificationDraft {
|
|
604
|
+
verdicts: Array<{
|
|
605
|
+
id: string;
|
|
606
|
+
confirmed: boolean;
|
|
607
|
+
/** The supporting span, which the verifier may correct. */
|
|
608
|
+
quote?: string;
|
|
609
|
+
/** Why not, in the verifier's words. Surfaced, never silently swallowed. */
|
|
610
|
+
reason?: string;
|
|
611
|
+
}>;
|
|
612
|
+
}
|
|
613
|
+
|
|
521
614
|
export interface ExtractionRequest {
|
|
522
615
|
documents: Array<{
|
|
523
616
|
id: string;
|
|
@@ -591,8 +684,21 @@ export interface ExtractionResult {
|
|
|
591
684
|
nextOffset: number | null;
|
|
592
685
|
/** Items the vendor's own structure produced, with no model involved. */
|
|
593
686
|
fromRules: number;
|
|
594
|
-
/** Items only the model found. */
|
|
687
|
+
/** Items only the model found, AFTER verification. */
|
|
595
688
|
fromModel: number;
|
|
689
|
+
/**
|
|
690
|
+
* Model candidates thrown away because the span they quoted does not appear in
|
|
691
|
+
* the record. Free to check, and it is the signature of an invented item.
|
|
692
|
+
*/
|
|
693
|
+
ungrounded: number;
|
|
694
|
+
/** Grounded candidates the verification pass declined to confirm. */
|
|
695
|
+
unconfirmed: number;
|
|
696
|
+
/**
|
|
697
|
+
* Whether the model verification pass actually ran. False means the surviving
|
|
698
|
+
* model items passed the deterministic grounding check and nothing else — the
|
|
699
|
+
* number is still reported, it is just not called verified.
|
|
700
|
+
*/
|
|
701
|
+
verified: boolean;
|
|
596
702
|
created: number;
|
|
597
703
|
/** Existing items a new mention was attached to — the dedupe, counted. */
|
|
598
704
|
merged: number;
|