@startsimpli/integrations 0.2.13 → 0.2.14
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 +1 -1
- 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-pages.test.tsx +11 -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/ConnectionsPage.tsx +142 -5
- package/src/components/pages/LibraryPage.tsx +44 -3
- package/src/components/pages/TrackerPage.tsx +17 -0
- package/src/corpus/ask.ts +12 -8
- package/src/corpus/entities.ts +18 -1
- 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 +80 -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. */
|
|
@@ -515,9 +533,56 @@ export interface CommitmentApi {
|
|
|
515
533
|
*/
|
|
516
534
|
export interface CorpusExtractorLlm {
|
|
517
535
|
extract(input: ExtractionRequest): Promise<ExtractionDraft>;
|
|
536
|
+
/**
|
|
537
|
+
* CONFIRM the model's own candidates against the records they came from.
|
|
538
|
+
*
|
|
539
|
+
* OPTIONAL, and the run reports `verified: false` when it is missing rather
|
|
540
|
+
* than pretending a check happened. Present on the shipped adapter.
|
|
541
|
+
*
|
|
542
|
+
* WHY IT EXISTS. More text raises recall and lowers precision, and transcripts
|
|
543
|
+
* are roughly ten times the text of a summary. Measured on the summaries
|
|
544
|
+
* alone, adding the model pass took recall 56.1% → 65.9% and precision 95.8% →
|
|
545
|
+
* 67.5%; a corpus of verbatim speech makes that trade worse, not better,
|
|
546
|
+
* because a transcript is full of sentences that SOUND like commitments and
|
|
547
|
+
* are hedges, jokes, or somebody reading an agenda aloud.
|
|
548
|
+
*
|
|
549
|
+
* ONE BATCHED CALL over every candidate with echoed ids
|
|
550
|
+
* (feedback_batch_llm_calls) — never a call per item.
|
|
551
|
+
*/
|
|
552
|
+
verify?(input: VerificationRequest): Promise<VerificationDraft>;
|
|
518
553
|
describe(): LlmDescription;
|
|
519
554
|
}
|
|
520
555
|
|
|
556
|
+
/**
|
|
557
|
+
* What the verifier is asked. The candidates AND the records they claim to come
|
|
558
|
+
* from, so the check is against the source rather than against the candidate's
|
|
559
|
+
* own plausibility.
|
|
560
|
+
*/
|
|
561
|
+
export interface VerificationRequest {
|
|
562
|
+
documents: Array<{ id: string; title: string; text: string }>;
|
|
563
|
+
candidates: Array<{
|
|
564
|
+
/** Echoed back on the verdict. Stable only within this call. */
|
|
565
|
+
id: string;
|
|
566
|
+
documentId: string;
|
|
567
|
+
subtype: string;
|
|
568
|
+
title: string;
|
|
569
|
+
owner?: string;
|
|
570
|
+
/** The span the extractor said this came from. */
|
|
571
|
+
quote: string;
|
|
572
|
+
}>;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export interface VerificationDraft {
|
|
576
|
+
verdicts: Array<{
|
|
577
|
+
id: string;
|
|
578
|
+
confirmed: boolean;
|
|
579
|
+
/** The supporting span, which the verifier may correct. */
|
|
580
|
+
quote?: string;
|
|
581
|
+
/** Why not, in the verifier's words. Surfaced, never silently swallowed. */
|
|
582
|
+
reason?: string;
|
|
583
|
+
}>;
|
|
584
|
+
}
|
|
585
|
+
|
|
521
586
|
export interface ExtractionRequest {
|
|
522
587
|
documents: Array<{
|
|
523
588
|
id: string;
|
|
@@ -591,8 +656,21 @@ export interface ExtractionResult {
|
|
|
591
656
|
nextOffset: number | null;
|
|
592
657
|
/** Items the vendor's own structure produced, with no model involved. */
|
|
593
658
|
fromRules: number;
|
|
594
|
-
/** Items only the model found. */
|
|
659
|
+
/** Items only the model found, AFTER verification. */
|
|
595
660
|
fromModel: number;
|
|
661
|
+
/**
|
|
662
|
+
* Model candidates thrown away because the span they quoted does not appear in
|
|
663
|
+
* the record. Free to check, and it is the signature of an invented item.
|
|
664
|
+
*/
|
|
665
|
+
ungrounded: number;
|
|
666
|
+
/** Grounded candidates the verification pass declined to confirm. */
|
|
667
|
+
unconfirmed: number;
|
|
668
|
+
/**
|
|
669
|
+
* Whether the model verification pass actually ran. False means the surviving
|
|
670
|
+
* model items passed the deterministic grounding check and nothing else — the
|
|
671
|
+
* number is still reported, it is just not called verified.
|
|
672
|
+
*/
|
|
673
|
+
verified: boolean;
|
|
596
674
|
created: number;
|
|
597
675
|
/** Existing items a new mention was attached to — the dedupe, counted. */
|
|
598
676
|
merged: number;
|