@startsimpli/integrations 0.2.11 → 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 +70 -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 +40 -2
- 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
|
@@ -48,6 +48,7 @@ import type {
|
|
|
48
48
|
ConnectionTarget,
|
|
49
49
|
ConnectorAction,
|
|
50
50
|
ConnectorCatalogEntry,
|
|
51
|
+
ConnectorFieldSpec,
|
|
51
52
|
ConnectionsApi,
|
|
52
53
|
} from './types';
|
|
53
54
|
|
|
@@ -267,7 +268,7 @@ function CatalogCard({
|
|
|
267
268
|
onConnected: () => void;
|
|
268
269
|
}) {
|
|
269
270
|
const [open, setOpen] = React.useState(false);
|
|
270
|
-
const [values, setValues] = React.useState<Record<string,
|
|
271
|
+
const [values, setValues] = React.useState<Record<string, unknown>>({});
|
|
271
272
|
const [busy, setBusy] = React.useState(false);
|
|
272
273
|
const [error, setError] = React.useState('');
|
|
273
274
|
// Every declared capability starts ON. Not because "on" is a safe default in
|
|
@@ -296,7 +297,11 @@ function CatalogCard({
|
|
|
296
297
|
}
|
|
297
298
|
for (const spec of configFields) {
|
|
298
299
|
const v = values[isSecret(spec) ? `${spec.key}__draft` : spec.key];
|
|
299
|
-
if (v)
|
|
300
|
+
// `if (v)` DROPPED A FALSE, which for a checkbox makes "off" and
|
|
301
|
+
// "untouched" the same request. Harmless while every config field was a
|
|
302
|
+
// string; not harmless once one of them decides whether verbatim
|
|
303
|
+
// recordings are stored.
|
|
304
|
+
if (typeof v === 'boolean' || typeof v === 'number' || v) config[spec.key] = v;
|
|
300
305
|
}
|
|
301
306
|
await api.connect(foundrySlug, {
|
|
302
307
|
connector_key: entry.key, name: entry.name, credentials, config,
|
|
@@ -428,7 +433,7 @@ function CatalogCard({
|
|
|
428
433
|
* a secret never means re-typing the settings around it.
|
|
429
434
|
*
|
|
430
435
|
* Only the CREDENTIAL fields render. `config` is not a credential and is edited
|
|
431
|
-
* through
|
|
436
|
+
* through `ConnectionSettingsForm` below; mixing them here would make a
|
|
432
437
|
* rotation quietly a config change.
|
|
433
438
|
*/
|
|
434
439
|
function ReplaceKeyForm({
|
|
@@ -441,7 +446,7 @@ function ReplaceKeyForm({
|
|
|
441
446
|
classNames: typeof DEFAULTS;
|
|
442
447
|
onDone: () => void;
|
|
443
448
|
}) {
|
|
444
|
-
const [values, setValues] = React.useState<Record<string,
|
|
449
|
+
const [values, setValues] = React.useState<Record<string, unknown>>({});
|
|
445
450
|
const [busy, setBusy] = React.useState(false);
|
|
446
451
|
const [error, setError] = React.useState('');
|
|
447
452
|
|
|
@@ -514,6 +519,101 @@ function ReplaceKeyForm({
|
|
|
514
519
|
// a connected account: discover -> wire -> confirm -> fire
|
|
515
520
|
// --------------------------------------------------------------------------
|
|
516
521
|
|
|
522
|
+
/**
|
|
523
|
+
* The connection's own settings — its connector's `config_schema`, for an
|
|
524
|
+
* account that already exists.
|
|
525
|
+
*
|
|
526
|
+
* WHY IT IS A SEPARATE FORM FROM THE CONNECT CARD. Connecting is "here are my
|
|
527
|
+
* credentials, does this work"; this is "change how it behaves". Rotating a key
|
|
528
|
+
* must not quietly change a setting, and changing a setting must not ask for a
|
|
529
|
+
* key. `ReplaceKeyForm` has said as much in its own docstring since it shipped,
|
|
530
|
+
* pointing at a surface that did not exist.
|
|
531
|
+
*
|
|
532
|
+
* IT SENDS ONLY WHAT CHANGED, and the API merges. Two reasons, and the second is
|
|
533
|
+
* the one that bites: resending untouched fields turns "I ticked one box" into
|
|
534
|
+
* "I reassert every setting", which silently overwrites an edit made in another
|
|
535
|
+
* tab — and it is exactly how the retention bound gets reset to whatever this
|
|
536
|
+
* form happened to be showing.
|
|
537
|
+
*
|
|
538
|
+
* A FALSE IS SENT, not omitted. The connect card drops falsy values, which for a
|
|
539
|
+
* checkbox makes "off" indistinguishable from "untouched" — and against a
|
|
540
|
+
* merging API that would make the off position literally unreachable. For
|
|
541
|
+
* `ingest_transcripts` the off position is the whole privacy posture.
|
|
542
|
+
*/
|
|
543
|
+
function ConnectionSettingsForm({
|
|
544
|
+
api, foundrySlug, connection, fields, classNames: cn, onDone,
|
|
545
|
+
}: {
|
|
546
|
+
api: ConnectionsApi;
|
|
547
|
+
foundrySlug: string;
|
|
548
|
+
connection: Connection;
|
|
549
|
+
fields: ConnectorFieldSpec[];
|
|
550
|
+
classNames: typeof DEFAULTS;
|
|
551
|
+
onDone: () => void;
|
|
552
|
+
}) {
|
|
553
|
+
const stored = React.useMemo(
|
|
554
|
+
() => (connection.config ?? {}) as Record<string, unknown>,
|
|
555
|
+
[connection.config],
|
|
556
|
+
);
|
|
557
|
+
const [edits, setEdits] = React.useState<Record<string, string | boolean>>({});
|
|
558
|
+
const [busy, setBusy] = React.useState(false);
|
|
559
|
+
const [error, setError] = React.useState('');
|
|
560
|
+
|
|
561
|
+
const values = React.useMemo(() => ({ ...stored, ...edits }), [stored, edits]);
|
|
562
|
+
const warnings = patternWarnings(fields, values);
|
|
563
|
+
const changed = Object.keys(edits);
|
|
564
|
+
|
|
565
|
+
async function save() {
|
|
566
|
+
setBusy(true);
|
|
567
|
+
setError('');
|
|
568
|
+
try {
|
|
569
|
+
// Numbers go back as numbers. A `number` input hands back a string, and a
|
|
570
|
+
// retention bound stored as "0" rather than 0 is a comparison that quietly
|
|
571
|
+
// stops being true.
|
|
572
|
+
const config: Record<string, unknown> = {};
|
|
573
|
+
for (const key of changed) {
|
|
574
|
+
const spec = fields.find((field) => field.key === key);
|
|
575
|
+
const value = edits[key];
|
|
576
|
+
config[key] =
|
|
577
|
+
spec?.type === 'number' && typeof value === 'string' && value.trim() !== ''
|
|
578
|
+
? Number(value)
|
|
579
|
+
: value;
|
|
580
|
+
}
|
|
581
|
+
await api.updateConnection(foundrySlug, connection.id, { config });
|
|
582
|
+
onDone();
|
|
583
|
+
} catch (err) {
|
|
584
|
+
setError((err as { message?: string })?.message || 'Those settings were not saved.');
|
|
585
|
+
} finally {
|
|
586
|
+
setBusy(false);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return (
|
|
591
|
+
<div className="mt-3 space-y-3 border-t pt-3" role="group" aria-label="Connection settings">
|
|
592
|
+
<CredentialSchemaForm
|
|
593
|
+
fields={fields}
|
|
594
|
+
values={values}
|
|
595
|
+
onChange={(key, value) => setEdits((current) => ({ ...current, [key]: value }))}
|
|
596
|
+
warnings={warnings}
|
|
597
|
+
idPrefix={`settings-${connection.id}`}
|
|
598
|
+
/>
|
|
599
|
+
{error ? <p className={cn.error} role="alert">{error}</p> : null}
|
|
600
|
+
<div className="flex items-center gap-2">
|
|
601
|
+
<button
|
|
602
|
+
type="button"
|
|
603
|
+
className={cn.button}
|
|
604
|
+
disabled={busy || changed.length === 0}
|
|
605
|
+
onClick={() => void save()}
|
|
606
|
+
>
|
|
607
|
+
{busy ? 'Saving…' : 'Save settings'}
|
|
608
|
+
</button>
|
|
609
|
+
{changed.length === 0 ? (
|
|
610
|
+
<span className={cn.muted}>Nothing changed yet.</span>
|
|
611
|
+
) : null}
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
517
617
|
function ConnectionCard({
|
|
518
618
|
api, foundrySlug, connection, entry, canManage, classNames: cn, onChanged,
|
|
519
619
|
}: {
|
|
@@ -533,6 +633,7 @@ function ConnectionCard({
|
|
|
533
633
|
const [error, setError] = React.useState('');
|
|
534
634
|
const [notice, setNotice] = React.useState('');
|
|
535
635
|
const [replacing, setReplacing] = React.useState(false);
|
|
636
|
+
const [settingsOpen, setSettingsOpen] = React.useState(false);
|
|
536
637
|
const [confirmingDisconnect, setConfirmingDisconnect] = React.useState(false);
|
|
537
638
|
|
|
538
639
|
const declared = entry?.capabilities;
|
|
@@ -568,8 +669,10 @@ function ConnectionCard({
|
|
|
568
669
|
}
|
|
569
670
|
}
|
|
570
671
|
|
|
672
|
+
const configFields = schemaFields(entry?.config_schema);
|
|
673
|
+
|
|
571
674
|
return (
|
|
572
|
-
<div className={cn.card}>
|
|
675
|
+
<div className={cn.card} data-connection={connection.id}>
|
|
573
676
|
<div className="flex items-start justify-between gap-4">
|
|
574
677
|
<div>
|
|
575
678
|
<p className="font-medium text-gray-900">{connection.name}</p>
|
|
@@ -624,12 +727,35 @@ function ConnectionCard({
|
|
|
624
727
|
>
|
|
625
728
|
{busy === 'discover' ? 'Loading…' : 'Workflows'}
|
|
626
729
|
</button>
|
|
730
|
+
{/* SETTINGS, which this card has never had. A connector's
|
|
731
|
+
`config_schema` ships on every catalog row and was rendered
|
|
732
|
+
exactly once — on the connect form, before the account existed.
|
|
733
|
+
After that a setting could not be changed from the product at
|
|
734
|
+
all, which for Granola means the switch deciding whether verbatim
|
|
735
|
+
recordings of people's conversations are stored was reachable
|
|
736
|
+
only by editing the database. `ReplaceKeyForm` already pointed at
|
|
737
|
+
"the connection's own settings" as if this existed. */}
|
|
738
|
+
{configFields.length ? (
|
|
739
|
+
<button
|
|
740
|
+
type="button"
|
|
741
|
+
className={cn.button}
|
|
742
|
+
disabled={Boolean(busy) || confirmingDisconnect}
|
|
743
|
+
onClick={() => {
|
|
744
|
+
setReplacing(false);
|
|
745
|
+
setError('');
|
|
746
|
+
setSettingsOpen((v) => !v);
|
|
747
|
+
}}
|
|
748
|
+
>
|
|
749
|
+
{settingsOpen ? 'Close settings' : 'Settings'}
|
|
750
|
+
</button>
|
|
751
|
+
) : null}
|
|
627
752
|
<button
|
|
628
753
|
type="button"
|
|
629
754
|
className={cn.button}
|
|
630
755
|
disabled={Boolean(busy) || confirmingDisconnect}
|
|
631
756
|
onClick={() => {
|
|
632
757
|
setReplacing(false);
|
|
758
|
+
setSettingsOpen(false);
|
|
633
759
|
setError('');
|
|
634
760
|
setConfirmingDisconnect(true);
|
|
635
761
|
}}
|
|
@@ -640,6 +766,17 @@ function ConnectionCard({
|
|
|
640
766
|
) : null}
|
|
641
767
|
</div>
|
|
642
768
|
|
|
769
|
+
{settingsOpen ? (
|
|
770
|
+
<ConnectionSettingsForm
|
|
771
|
+
api={api}
|
|
772
|
+
foundrySlug={foundrySlug}
|
|
773
|
+
connection={connection}
|
|
774
|
+
fields={configFields}
|
|
775
|
+
classNames={cn}
|
|
776
|
+
onDone={() => { setSettingsOpen(false); setNotice('Settings saved.'); onChanged(); }}
|
|
777
|
+
/>
|
|
778
|
+
) : null}
|
|
779
|
+
|
|
643
780
|
{replacing ? (
|
|
644
781
|
<ReplaceKeyForm
|
|
645
782
|
api={api}
|
|
@@ -114,11 +114,31 @@ function relative(value: string | null | undefined, now: number): string {
|
|
|
114
114
|
* and the row is a summary, not the document.
|
|
115
115
|
*/
|
|
116
116
|
function preview(document: SourceDocument, length = 180): string {
|
|
117
|
-
const text = stripMarkdown(
|
|
117
|
+
const text = stripMarkdown(
|
|
118
|
+
summaryOf(document) || document.body || document.sections[0]?.text || '',
|
|
119
|
+
);
|
|
118
120
|
if (!text) return '';
|
|
119
121
|
return text.length > length ? `${text.slice(0, length).trim()}…` : text;
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
/**
|
|
125
|
+
* The short form, when the source wrote one.
|
|
126
|
+
*
|
|
127
|
+
* `body` falls back to the summary on a corpus that has never delivered a full
|
|
128
|
+
* text, so on today's tenant these are the same string and this returns it. When
|
|
129
|
+
* both exist they are different, and only this one belongs under the heading
|
|
130
|
+
* "Summary".
|
|
131
|
+
*/
|
|
132
|
+
function summaryOf(document: SourceDocument): string {
|
|
133
|
+
return document.summary || (document.body ?? '');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The verbatim text, when it is genuinely different from the summary. */
|
|
137
|
+
function fullTextOf(document: SourceDocument): string {
|
|
138
|
+
const body = document.body ?? '';
|
|
139
|
+
return body && body !== summaryOf(document) ? body : '';
|
|
140
|
+
}
|
|
141
|
+
|
|
122
142
|
// ---------------------------------------------------------------------------
|
|
123
143
|
|
|
124
144
|
export function LibraryPage({
|
|
@@ -589,15 +609,36 @@ function DocumentDetail({
|
|
|
589
609
|
</div>
|
|
590
610
|
) : null}
|
|
591
611
|
|
|
592
|
-
{document.body
|
|
612
|
+
{/* THE SUMMARY, UNDER THE WORD SUMMARY. This used to render `document.body`
|
|
613
|
+
here — which was the summary only for as long as nothing had ever
|
|
614
|
+
delivered a full text. The moment `ingest_transcripts` lands, `body` is
|
|
615
|
+
forty minutes of verbatim speech, and it would have appeared on this
|
|
616
|
+
screen labelled "Summary" while the actual summary appeared nowhere. */}
|
|
617
|
+
{summaryOf(document) ? (
|
|
593
618
|
<div className="mt-3">
|
|
594
619
|
<p className="text-sm font-medium text-gray-900">Summary</p>
|
|
595
620
|
{/* Sources write Markdown — a literal "## Decisions" on the screen
|
|
596
621
|
reads as a broken product, not as the source's punctuation. */}
|
|
597
|
-
<Markdown text={document
|
|
622
|
+
<Markdown text={summaryOf(document)} className="mt-1 text-sm text-gray-700" />
|
|
598
623
|
</div>
|
|
599
624
|
) : null}
|
|
600
625
|
|
|
626
|
+
{/* The FULL text, collapsed, next to the source's other long-form. Behind
|
|
627
|
+
a disclosure and not open by default, for the same reason a transcript
|
|
628
|
+
is opt-in at all: verbatim speech is a different data class from a
|
|
629
|
+
summary, and a screen that puts it in front of whoever opened the
|
|
630
|
+
record has quietly decided that on the reader's behalf. */}
|
|
631
|
+
{fullTextOf(document) ? (
|
|
632
|
+
<details className="mt-3">
|
|
633
|
+
<summary className="cursor-pointer text-sm font-medium text-gray-900">
|
|
634
|
+
Full text
|
|
635
|
+
</summary>
|
|
636
|
+
<pre className="mt-2 max-h-96 overflow-auto whitespace-pre-wrap rounded bg-gray-50 p-2 text-xs text-gray-700">
|
|
637
|
+
{fullTextOf(document)}
|
|
638
|
+
</pre>
|
|
639
|
+
</details>
|
|
640
|
+
) : null}
|
|
641
|
+
|
|
601
642
|
{/* Long-form under the SOURCE's own name for it — "Transcript" for a
|
|
602
643
|
meeting, "Extracted text" for a PDF. This component does not know
|
|
603
644
|
which is which, and must not. */}
|
|
@@ -113,6 +113,18 @@ export function TrackerPage({
|
|
|
113
113
|
const [error, setError] = React.useState('');
|
|
114
114
|
const [run, setRun] = React.useState<ExtractionResult | null>(null);
|
|
115
115
|
const [running, setRunning] = React.useState(false);
|
|
116
|
+
/**
|
|
117
|
+
* Does this workspace hold ANY commitments at all?
|
|
118
|
+
*
|
|
119
|
+
* Now that the server really filters, an empty page means one of two very
|
|
120
|
+
* different things and the screen owes the difference: "nothing has been
|
|
121
|
+
* pulled in yet" is advice, and saying it to somebody with ninety-seven open
|
|
122
|
+
* items and nothing overdue is simply false. Browser-verified on the deployed
|
|
123
|
+
* tenant — the Overdue tab said "Nothing has been pulled out of your meetings
|
|
124
|
+
* yet" over a corpus of ninety-seven. Resolved with ONE unfiltered
|
|
125
|
+
* count-only read, and only when a view comes back empty.
|
|
126
|
+
*/
|
|
127
|
+
const [workspaceHasAny, setWorkspaceHasAny] = React.useState<boolean | null>(null);
|
|
116
128
|
|
|
117
129
|
const query = React.useMemo<CommitmentQuery>(
|
|
118
130
|
() => ({
|
|
@@ -136,6 +148,15 @@ export function TrackerPage({
|
|
|
136
148
|
const page = await apiRef.current.listCommitments(query);
|
|
137
149
|
setRows(page.results);
|
|
138
150
|
setApplied(page.appliedFilters ?? []);
|
|
151
|
+
if (page.results.length > 0) {
|
|
152
|
+
setWorkspaceHasAny(true);
|
|
153
|
+
} else {
|
|
154
|
+
const any = await apiRef.current
|
|
155
|
+
.listCommitments({ pageSize: 1 })
|
|
156
|
+
.then((probe) => probe.count > 0)
|
|
157
|
+
.catch(() => null);
|
|
158
|
+
setWorkspaceHasAny(any);
|
|
159
|
+
}
|
|
139
160
|
} catch (err) {
|
|
140
161
|
setRows([]);
|
|
141
162
|
setError(failureMessage(err));
|
|
@@ -249,9 +270,9 @@ export function TrackerPage({
|
|
|
249
270
|
|
|
250
271
|
{!loading && visible.length === 0 ? (
|
|
251
272
|
<p className={cn.muted}>
|
|
252
|
-
{
|
|
273
|
+
{workspaceHasAny === false
|
|
253
274
|
? 'Nothing has been pulled out of your meetings yet. Connect a source, then run “Pull from meetings”.'
|
|
254
|
-
: 'Nothing matches this view.
|
|
275
|
+
: 'Nothing matches this view. Your other tabs may still have items.'}
|
|
255
276
|
</p>
|
|
256
277
|
) : null}
|
|
257
278
|
|
|
@@ -386,6 +407,23 @@ function RunSummary({
|
|
|
386
407
|
out of the notes by {result.provider.live ? `${result.provider.provider} · ${result.provider.model}` : 'no live model'}.
|
|
387
408
|
</p>
|
|
388
409
|
|
|
410
|
+
{/* WHAT THE CHECK THREW AWAY, on the screen.
|
|
411
|
+
Measured over the sixteen real meetings: the model proposes about
|
|
412
|
+
ninety items and roughly two thirds of them do not survive. That is not
|
|
413
|
+
a bug to hide — it is the number that keeps the list believable, and
|
|
414
|
+
hiding it would leave "the model read 30 out of the notes" looking like
|
|
415
|
+
the model is simply better than it is. If `verified` is ever false the
|
|
416
|
+
run says so rather than implying a check that did not happen. */}
|
|
417
|
+
{result.fromModel || result.ungrounded || result.unconfirmed ? (
|
|
418
|
+
<p className={cn.muted}>
|
|
419
|
+
{result.verified
|
|
420
|
+
? `Checked against the meetings themselves: ${result.unconfirmed + result.ungrounded} more were proposed and dropped — ` +
|
|
421
|
+
`${result.ungrounded} quoted something the record does not say, ` +
|
|
422
|
+
`${result.unconfirmed} could not be confirmed as something anyone actually took on.`
|
|
423
|
+
: 'These were NOT checked against the meetings — the verification step did not run, so treat them as suggestions.'}
|
|
424
|
+
</p>
|
|
425
|
+
) : null}
|
|
426
|
+
|
|
389
427
|
{result.needsReview.length ? (
|
|
390
428
|
// Weak resolution matches are shown, never applied. A to-do list that
|
|
391
429
|
// quietly empties itself is the one failure with no symptom.
|
package/src/corpus/ask.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { moneyEventAsDocument } from './entities';
|
|
22
22
|
import { retrieveForQuestion } from './retrieval';
|
|
23
|
+
import { documentText } from './text';
|
|
23
24
|
import type {
|
|
24
25
|
AnswerRequest,
|
|
25
26
|
AskRequest,
|
|
@@ -103,15 +104,18 @@ export function buildAnswerRequest(
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
/**
|
|
106
|
-
* A document's text for the prompt:
|
|
107
|
-
* a budget. Sections are labelled with the source's
|
|
108
|
-
* "Transcript" or "Extracted text" without this
|
|
107
|
+
* A document's text for the prompt: the summary first, then the full text, then
|
|
108
|
+
* each named section, cut at a budget. Sections are labelled with the source's
|
|
109
|
+
* own words, so the model sees "Transcript" or "Extracted text" without this
|
|
110
|
+
* package knowing what either is.
|
|
111
|
+
*
|
|
112
|
+
* ONE IMPLEMENTATION, in `./text`, shared with extraction. It used to be two
|
|
113
|
+
* near-identical private copies with different budgets, which is how the Ask
|
|
114
|
+
* surface came within one config flag of spending its entire 6,000-character
|
|
115
|
+
* per-document budget on the opening minutes of a transcript and never showing
|
|
116
|
+
* the model the summary at all.
|
|
109
117
|
*/
|
|
110
|
-
export
|
|
111
|
-
const parts = [document.body, ...document.sections.map((s) => `## ${s.label}\n${s.text}`)];
|
|
112
|
-
const joined = parts.filter(Boolean).join('\n\n');
|
|
113
|
-
return joined.length > maxChars ? `${joined.slice(0, maxChars)}\n…[truncated]` : joined;
|
|
114
|
-
}
|
|
118
|
+
export { documentText };
|
|
115
119
|
|
|
116
120
|
/** Render the batched request as the single user message. */
|
|
117
121
|
export function renderAnswerPrompt(input: AnswerRequest): string {
|
package/src/corpus/entities.ts
CHANGED
|
@@ -391,6 +391,17 @@ export function toSourceDocument(
|
|
|
391
391
|
const data = bag(entity);
|
|
392
392
|
const source = asText(pick(data, 'source', 'source_key', 'sourceKey')) || 'unknown';
|
|
393
393
|
|
|
394
|
+
// TWO TEXT FIELDS, NOT ONE, because the canonical schema declares two and says
|
|
395
|
+
// why: "Collapsing them means the transcript clobbers the summary the first
|
|
396
|
+
// time both arrive". This mapper collapsed them anyway —
|
|
397
|
+
// `pick(data,'body',…,'summary')` returns the first present key — so a
|
|
398
|
+
// document carrying both would have surfaced ONLY the transcript, and the
|
|
399
|
+
// `## Next Steps` block that the entire deterministic rules pass reads lives
|
|
400
|
+
// in the summary. Switching `ingest_transcripts` on would have taken a
|
|
401
|
+
// 95.8%-precision, 100%-owner-accuracy pass to zero, silently, on deploy.
|
|
402
|
+
const summary = asText(pick(data, 'summary', 'description'));
|
|
403
|
+
const fullText = asText(pick(data, 'body', 'text', 'content'));
|
|
404
|
+
|
|
394
405
|
const sections = toSections(pick(data, 'sections'));
|
|
395
406
|
// A source that stored its long-form under a bare attribute rather than in
|
|
396
407
|
// `sections` still contributes one. Listed keys are storage spellings, not
|
|
@@ -407,7 +418,13 @@ export function toSourceDocument(
|
|
|
407
418
|
asText(pick(data, 'title', 'subject', 'headline')) ||
|
|
408
419
|
entity.name ||
|
|
409
420
|
`Untitled ${source} record`,
|
|
410
|
-
body
|
|
421
|
+
// `body` still ANSWERS for the summary when the source has never delivered a
|
|
422
|
+
// full text, which is every row on the live tenant today. Every existing
|
|
423
|
+
// consumer of `.body` — retrieval, the Ask prompt, the Library preview —
|
|
424
|
+
// keeps working unchanged on a corpus of summaries; only a corpus that has
|
|
425
|
+
// both sees a difference, and that is the corpus this distinction is for.
|
|
426
|
+
body: fullText || summary,
|
|
427
|
+
...(summary ? { summary } : {}),
|
|
411
428
|
sections,
|
|
412
429
|
occurredAt: asIso(pick(data, 'occurred_at', 'occurredAt', 'happened_at', 'date')),
|
|
413
430
|
source,
|