@agent-cards/commerce-sdk 0.16.0 → 0.18.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 +21 -20
- package/dist/ack.d.ts +14 -4
- package/dist/ack.js +29 -3
- package/dist/buy-tool.d.ts +28 -7
- package/dist/buy-tool.js +358 -34
- package/dist/client.d.ts +33 -0
- package/dist/client.js +5 -1
- package/dist/definition.js +1 -0
- package/dist/direct.d.ts +11 -1
- package/dist/direct.js +34 -7
- package/dist/index.d.ts +1 -1
- package/dist/messages.d.ts +27 -6
- package/dist/messages.js +68 -8
- package/dist/model-view.d.ts +5 -2
- package/dist/model-view.js +192 -23
- package/dist/sentences.d.ts +13 -0
- package/dist/sentences.js +28 -0
- package/dist/texting.d.ts +2 -0
- package/dist/texting.js +6 -1
- package/package.json +1 -1
package/dist/model-view.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { SENTENCE_END, lastSentenceStart } from './sentences.js';
|
|
1
|
+
import { SENTENCE_END, lastSentenceStart, leadsIn, withoutLeadIn } from './sentences.js';
|
|
2
|
+
import { toMessages } from './messages.js';
|
|
2
3
|
/** The steps the shopper takes on a page a link opens: the message that asks for one is one short line. */
|
|
3
4
|
const STEP_ACTIONS = new Set(['store_login', 'add_card', 'approve']);
|
|
4
5
|
const MODEL_STATUSES = new Set(['needs_input', 'needs_card', 'approval_sent', 'placing', 'order_placed', 'declined']);
|
|
@@ -373,6 +374,18 @@ export function replyAsWritten(result) {
|
|
|
373
374
|
const LIST_ITEM = /^\s*(?:[-*•]|\d{1,2}[.)])\s+/;
|
|
374
375
|
/** A price, in any of the currencies a store writes one in. */
|
|
375
376
|
const PRICE = /[$£€]\s?\d/;
|
|
377
|
+
/**
|
|
378
|
+
* A mark that numbers or bullets an item in the middle of a line, such as the `2)` in `1) a mug; 2) a soap`, or a `•`.
|
|
379
|
+
* A number counts only in a run that goes 1, 2, 3, so a size such as `(Pack of 12) and` marks nothing.
|
|
380
|
+
*/
|
|
381
|
+
const INLINE_MARK = /(?<=^|[\s:;,(])(?:(\d{1,2})([.)])|•)[ \t]+/g;
|
|
382
|
+
/**
|
|
383
|
+
* A sentence that asks the shopper something: a question, `let me know ...` or `tell me ...` anywhere in it, as in
|
|
384
|
+
* `amazon has the best price, so let me know how many you want.`, or one that opens with `pick ...` or `reply ...`.
|
|
385
|
+
*/
|
|
386
|
+
const ASKS = /\?[\s"'*_)\]]*$|\b(?:let me know|tell me)\b|^[\s*_"'(]*(?:just (?:say|tell|reply|pick)|reply|pick|choose)\b/i;
|
|
387
|
+
/** A clause that turns from a product to the shopper, after a comma, a semicolon or a dash: `, which one would you like?`. */
|
|
388
|
+
const ASKING_CLAUSE = /(?:[,;]|\s[-–])\s+(?=(?:(?:and|or|so|but)\s+)?(?:which|what|want|would|do|does|should|shall|can|could|how|any|let me know|tell me|just)\b)/gi;
|
|
376
389
|
/** The place a sentence points at in a numbered list, such as `#8` or `number 3`, where `a #2 Phillips head` names a kind of thing. */
|
|
377
390
|
const LIST_NUMBER = /(?<!\ba )(?<!\ban )(?:#(\d{1,2})\b|\bnumber (\d{1,2})\b)/i;
|
|
378
391
|
/**
|
|
@@ -503,13 +516,123 @@ function cardsNamed(text, cards, inList) {
|
|
|
503
516
|
.filter(({ hit }) => hit.size === 0 || !saidOnce(hit) || !found.some((other) => other.hit.size > hit.size && [...hit].every((word) => other.hit.has(word))))
|
|
504
517
|
.map(({ index }) => index);
|
|
505
518
|
}
|
|
519
|
+
/** The sentences of a text from `from` on, each with the place in the text where it starts. */
|
|
520
|
+
function sentencesAt(text, from) {
|
|
521
|
+
const out = [];
|
|
522
|
+
let at = from;
|
|
523
|
+
for (const part of text.slice(from).split(SENTENCE_END)) {
|
|
524
|
+
const start = text.indexOf(part, at);
|
|
525
|
+
if (part !== '')
|
|
526
|
+
out.push({ text: part, at: start });
|
|
527
|
+
at = start + part.length;
|
|
528
|
+
}
|
|
529
|
+
return out;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Where the clause that asks the shopper starts in a sentence that also gives a product, as `which one would you like?`
|
|
533
|
+
* does in `2) Mini Soaps Gift Box ($58.00), which one would you like?`, or null when the sentence asks nothing apart.
|
|
534
|
+
*/
|
|
535
|
+
function askingClause(sentence) {
|
|
536
|
+
if (!ASKS.test(sentence))
|
|
537
|
+
return null;
|
|
538
|
+
for (const turn of sentence.matchAll(ASKING_CLAUSE)) {
|
|
539
|
+
const rest = sentence.slice(turn.index + turn[0].length);
|
|
540
|
+
if (ASKS.test(rest) && !PRICE.test(rest))
|
|
541
|
+
return turn.index + turn[0].length;
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
/** Where the sentence that holds `at` starts: right after the sentence before it, or at the line's start. */
|
|
546
|
+
function sentenceStart(line, at) {
|
|
547
|
+
return lastSentenceStart(line.slice(0, at));
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Where a list on one line stops, given where its last item starts: past that item's first period, through the
|
|
551
|
+
* sentences that only describe it, to the first that asks the shopper or names a card itself, so `8.4 Fl. Oz.` never
|
|
552
|
+
* cuts the item short. A question the item's own sentence asks after a comma, `..., which one?`, stays out of it.
|
|
553
|
+
*/
|
|
554
|
+
function inlineEnd(line, from, cards) {
|
|
555
|
+
const [own, ...rest] = sentencesAt(line, from);
|
|
556
|
+
const clause = own ? askingClause(own.text) : null;
|
|
557
|
+
if (own && clause !== null)
|
|
558
|
+
return own.at + clause;
|
|
559
|
+
const stop = rest.find((sentence) => ASKS.test(sentence.text) || cardsNamed(sentence.text, cards, false).length > 0);
|
|
560
|
+
return stop ? stop.at : line.length;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* A list a reply writes on one line, mid-sentence: `i found a few options: 1) a mug ($19.00); 2) a gift set ($115.00);
|
|
564
|
+
* 3) a soap box ($58.00). which one?`. Its items are numbered `1)` or `1.`, bulleted with `•`, or set apart by `;` with
|
|
565
|
+
* each naming a card, after a colon or three of them at least. The list may open the line, `1) a mug; 2) a candle`, and
|
|
566
|
+
* ends where inlineEnd says. `lead` is where the sentence that leads into the list starts, `end` where the list stops.
|
|
567
|
+
* Null when the line holds no such list.
|
|
568
|
+
*/
|
|
569
|
+
function inlineList(line, cards) {
|
|
570
|
+
const marks = [...line.matchAll(INLINE_MARK)];
|
|
571
|
+
// A numbered run, 1, 2, 3, each marked the same way, or two or more bullets.
|
|
572
|
+
const run = [];
|
|
573
|
+
for (const mark of marks) {
|
|
574
|
+
if (mark[1] !== undefined && Number(mark[1]) === run.length + 1 && (run.length === 0 || run[0][2] === mark[2]))
|
|
575
|
+
run.push(mark);
|
|
576
|
+
}
|
|
577
|
+
const bullets = marks.filter((mark) => mark[1] === undefined);
|
|
578
|
+
const chosen = run.length >= 2 ? run : bullets.length >= 2 ? bullets : [];
|
|
579
|
+
if (chosen.length >= 2) {
|
|
580
|
+
const last = chosen[chosen.length - 1];
|
|
581
|
+
const end = inlineEnd(line, last.index + last[0].length, cards);
|
|
582
|
+
const items = chosen.map((mark, k) => {
|
|
583
|
+
const start = mark.index;
|
|
584
|
+
const raw = line.slice(start, k + 1 < chosen.length ? chosen[k + 1].index : end);
|
|
585
|
+
return { start, raw, body: raw.slice(mark[0].length), number: mark[1] === undefined ? null : Number(mark[1]) };
|
|
586
|
+
});
|
|
587
|
+
return { lead: sentenceStart(line, items[0].start), items, end };
|
|
588
|
+
}
|
|
589
|
+
// Parts set apart by `;` run from the colon that leads into them, found by its place rather than by sentences, so a
|
|
590
|
+
// name such as `8.4 Fl. Oz.` before the first `;` stays whole.
|
|
591
|
+
const semicolon = line.indexOf(';');
|
|
592
|
+
if (semicolon === -1)
|
|
593
|
+
return null;
|
|
594
|
+
const colon = line.lastIndexOf(':', semicolon);
|
|
595
|
+
const led = colon !== -1 && !/[?!]/.test(line.slice(colon, semicolon));
|
|
596
|
+
const opens = led ? colon + 1 : sentenceStart(line, semicolon);
|
|
597
|
+
// The list's last `;` is the last one with an item on its right and no question between, found before the list's end
|
|
598
|
+
// is, so a sentence inside an item, `The Lavender Candle ships free;`, never ends the list early, and a `;` in a
|
|
599
|
+
// question after it, `which one would you like; or should i search again?`, is no part of it. An item names a card
|
|
600
|
+
// the list has not named yet, or names one again with a price, `Travel Mug ($19.00)`; a part that names a listed
|
|
601
|
+
// card with no price is a recommendation after the list, `i'd pick Travel Mug; Lavender Candle is nicer.`, and stays.
|
|
602
|
+
let last = semicolon;
|
|
603
|
+
for (let next = line.indexOf(';', last + 1); next !== -1; next = line.indexOf(';', last + 1)) {
|
|
604
|
+
const after = line.indexOf(';', next + 1);
|
|
605
|
+
const asks = sentencesAt(line.slice(0, next), last + 1).some((sentence) => ASKS.test(sentence.text));
|
|
606
|
+
const listed = new Set(cardsNamed(line.slice(opens, next), cards, true));
|
|
607
|
+
const text = line.slice(next + 1, after === -1 ? line.length : after);
|
|
608
|
+
const part = cardsNamed(text, cards, true);
|
|
609
|
+
const item = part.some((index) => !listed.has(index)) || (part.length > 0 && PRICE.test(sentencesAt(text, 0)[0]?.text ?? ''));
|
|
610
|
+
if (asks || !item)
|
|
611
|
+
break;
|
|
612
|
+
last = next;
|
|
613
|
+
}
|
|
614
|
+
const end = inlineEnd(line, last + 1, cards);
|
|
615
|
+
const parts = line.slice(opens, end).replace(/[\s,;]+$/, '').split(';');
|
|
616
|
+
if ((!led && parts.length < 3) || !parts.every((part) => cardsNamed(part, cards, true).length > 0))
|
|
617
|
+
return null;
|
|
618
|
+
let start = opens;
|
|
619
|
+
const items = parts.map((part) => {
|
|
620
|
+
const item = { start, raw: part, body: part, number: null };
|
|
621
|
+
start += part.length + 1;
|
|
622
|
+
return item;
|
|
623
|
+
});
|
|
624
|
+
return { lead: led ? sentenceStart(line, colon) : opens, items, end };
|
|
625
|
+
}
|
|
506
626
|
/**
|
|
507
627
|
* Agentcard's reply without the list its cards already show the shopper: each list line that names a product or
|
|
508
628
|
* store whose card went out, the line that led into that list (`here are a few:`, `other choices:`), a sentence that
|
|
509
629
|
* points at a number the list gave (`the dishwand in #8`), a sentence that goes on to more of the list (`other 6-packs
|
|
510
630
|
* are ...`, `there's also ...`) and names a card, and, past the one or two products a reply recommends in words, each
|
|
511
|
-
* further sentence that names another card.
|
|
512
|
-
*
|
|
631
|
+
* further sentence that names another card. A list written on one line goes the same way, item by item (inlineList),
|
|
632
|
+
* and so does a list line written without a bullet, whole, up to the question on it. What is left is the reply around
|
|
633
|
+
* the list, such as `found these at amazon.` and `which one do you want?`. A reply whose cards went out names none of
|
|
634
|
+
* them twice over. A list that keeps an item for a product no card shows keeps its lead-in, and the two read as one
|
|
635
|
+
* message (toMessages), so the shopper never reads `two options came in under $20:` on its own.
|
|
513
636
|
*
|
|
514
637
|
* Each card is its name, or its name and its price as the model reads it (`{ name, price: '$22.89' }`): with the price,
|
|
515
638
|
* a reply's short name for the product, such as `Planet Oat Extra Creamy ($22.89)`, reads as its card (cardsNamed).
|
|
@@ -528,25 +651,61 @@ export function withoutCardList(reply, cardNames) {
|
|
|
528
651
|
const words = wordsOf(text);
|
|
529
652
|
return PRICE.test(text) && cards.some((card) => card.words.length >= 2 && card.words.slice(0, 3).every((word, i) => words[i] === word));
|
|
530
653
|
};
|
|
531
|
-
|
|
654
|
+
// The numbers the dropped list gave, so a number of a product's own, `a #2 Phillips head`, keeps its sentence.
|
|
655
|
+
const numbers = new Set();
|
|
656
|
+
// A list a line writes inline goes the way a list of lines does: each item that names a card, and the sentence that
|
|
657
|
+
// led into the list once every item went. An item for a product no card shows stays, with the lead, and so does a
|
|
658
|
+
// list that opens the line, `1) a mug; 2) a candle; 3) a soap box`, item by item rather than as one list line.
|
|
659
|
+
const unlisted = (line) => {
|
|
660
|
+
const list = inlineList(line, cards);
|
|
661
|
+
const gone = list?.items.map((item) => cardsNamed(item.body, cards, true).length > 0) ?? [];
|
|
662
|
+
if (!list || !gone.some(Boolean))
|
|
663
|
+
return line;
|
|
664
|
+
list.items.forEach((item, k) => {
|
|
665
|
+
if (gone[k] && item.number !== null)
|
|
666
|
+
numbers.add(item.number);
|
|
667
|
+
});
|
|
668
|
+
const after = line.slice(list.end).trimStart();
|
|
669
|
+
if (gone.every(Boolean))
|
|
670
|
+
return `${line.slice(0, list.lead).trimEnd()} ${after}`.trim();
|
|
671
|
+
const left = list.items.filter((_, k) => !gone[k]).map((item) => item.raw.trim().replace(/[\s,;]*(?:\b(?:and|or))?[\s,;]*$/i, ''));
|
|
672
|
+
const close = /[.!?]$/.test(left[left.length - 1]) ? '' : '.';
|
|
673
|
+
return [line.slice(0, list.items[0].start).trimEnd(), `${left.join('; ')}${close}`, after].filter((part) => part !== '').join(' ');
|
|
674
|
+
};
|
|
675
|
+
const lines = reply.split('\n').map(unlisted);
|
|
532
676
|
const kind = lines.map((line) => {
|
|
533
677
|
if (LIST_ITEM.test(line))
|
|
534
678
|
return cardsNamed(line.replace(LIST_ITEM, ''), cards, true).length > 0 ? 'item' : 'kept';
|
|
535
679
|
return opensCard(line) ? 'named' : 'kept';
|
|
536
680
|
});
|
|
537
681
|
const dropped = kind.map((of) => of !== 'kept');
|
|
538
|
-
|
|
539
|
-
|
|
682
|
+
lines.forEach((line, i) => {
|
|
683
|
+
const number = dropped[i] ? Number(/^\s*(\d{1,2})[.)]\s/.exec(line)?.[1]) : NaN;
|
|
684
|
+
if (Number.isInteger(number))
|
|
685
|
+
numbers.add(number);
|
|
686
|
+
});
|
|
540
687
|
// The list a line leads into: the lines from the next one with words on it to the first that is not a list line, so a
|
|
541
|
-
// question on the line after the last item, `Which one?`, is no part of the list.
|
|
688
|
+
// question on the line after the last item, `Which one?`, is no part of the list. A blank line between two list lines
|
|
689
|
+
// is no end of it, as toMessages reads one, so a list that keeps an item past the blank keeps its lead.
|
|
690
|
+
const listLine = (j) => LIST_ITEM.test(lines[j]) || kind[j] === 'named';
|
|
542
691
|
const listAfter = (line) => {
|
|
543
692
|
const start = lines.findIndex((after, j) => j > line && after.trim() !== '');
|
|
544
693
|
if (start === -1)
|
|
545
694
|
return [];
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
695
|
+
const list = [];
|
|
696
|
+
for (let j = start; j < lines.length; j += 1) {
|
|
697
|
+
if (listLine(j)) {
|
|
698
|
+
list.push(dropped[j]);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (lines[j].trim() !== '')
|
|
702
|
+
break;
|
|
703
|
+
// A blank line: the list goes on when the next line with words on it is a list line.
|
|
704
|
+
const next = lines.findIndex((after, k) => k > j && after.trim() !== '');
|
|
705
|
+
if (next === -1 || !listLine(next))
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
return list;
|
|
550
709
|
};
|
|
551
710
|
const named = new Set();
|
|
552
711
|
const kept = [];
|
|
@@ -557,34 +716,44 @@ export function withoutCardList(reply, cardNames) {
|
|
|
557
716
|
// The line that leads into a list whose every line went loses the sentence that leads in: `Two I'd suggest:` or
|
|
558
717
|
// `Other choices:`. A list that keeps a line, for a product no card shows, keeps its lead too.
|
|
559
718
|
const list = listAfter(i);
|
|
560
|
-
if (text
|
|
561
|
-
text =
|
|
562
|
-
|
|
719
|
+
if (leadsIn(text) && list.length > 0 && list.every(Boolean))
|
|
720
|
+
text = withoutLeadIn(text);
|
|
721
|
+
// A list line written without a bullet goes whole, with what trails its card's price (`$16.12. amazon, and the
|
|
722
|
+
// cheapest multipack.`), up to the sentence that asks the shopper something, which stays with what follows it. A
|
|
723
|
+
// question its card's own sentence asks after a comma, `..., $16.12, want it?`, stays too.
|
|
724
|
+
let asked = false;
|
|
725
|
+
const sentences = text.split(SENTENCE_END).flatMap((sentence) => {
|
|
563
726
|
const place = LIST_NUMBER.exec(sentence);
|
|
564
727
|
if (place && numbers.has(Number(place[1] ?? place[2])))
|
|
565
|
-
return
|
|
728
|
+
return [];
|
|
566
729
|
const found = cardsNamed(sentence, cards, false);
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
730
|
+
if (kind[i] === 'named') {
|
|
731
|
+
const card = found.length > 0 || opensCard(sentence);
|
|
732
|
+
asked ||= ASKS.test(sentence);
|
|
733
|
+
if (!card)
|
|
734
|
+
return asked ? [sentence] : [];
|
|
735
|
+
const clause = asked ? askingClause(sentence) : null;
|
|
736
|
+
return clause === null ? [] : [sentence.slice(clause)];
|
|
737
|
+
}
|
|
570
738
|
if (found.length === 0)
|
|
571
|
-
return
|
|
739
|
+
return [sentence];
|
|
572
740
|
// `other 6-packs are ...` and `there's also ...` recommend nothing: they are the list again, in a sentence.
|
|
573
741
|
if (MORE_OF_LIST.test(sentence))
|
|
574
|
-
return
|
|
742
|
+
return [];
|
|
575
743
|
const fresh = found.filter((card) => !named.has(card));
|
|
576
744
|
// A reply recommends one or two products in words; a sentence that names a third is the list again.
|
|
577
745
|
if (named.size + fresh.length > 2)
|
|
578
|
-
return
|
|
746
|
+
return [];
|
|
579
747
|
for (const card of fresh)
|
|
580
748
|
named.add(card);
|
|
581
|
-
return
|
|
749
|
+
return [sentence];
|
|
582
750
|
});
|
|
583
751
|
if (kind[i] === 'named' && sentences.length === 0)
|
|
584
752
|
return;
|
|
585
753
|
kept.push(sentences.join(' ').trimEnd());
|
|
586
754
|
});
|
|
587
|
-
|
|
755
|
+
// A lead-in kept for a list that lost an item to a card goes with what is left of the list, as one message (toMessages).
|
|
756
|
+
return toMessages(kept.join('\n')).join('\n\n');
|
|
588
757
|
}
|
|
589
758
|
/**
|
|
590
759
|
* A thrown error as the model may read it: the same message with every link
|
package/dist/sentences.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
1
|
export declare const SENTENCE_END: RegExp;
|
|
2
2
|
/** Where the last sentence of `text` begins: 0 when the text is one sentence. */
|
|
3
3
|
export declare function lastSentenceStart(text: string): number;
|
|
4
|
+
/** True when `text` ends with a colon that leads into what follows it, so it never stands alone as a message. */
|
|
5
|
+
export declare function leadsIn(text: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* The text less every sentence at its end that leads in with a colon, for lead-ins nothing follows: the list they led
|
|
8
|
+
* into went (its cards show it), and a colon would point at nothing. Lead-ins in a row go together, so `two things:`
|
|
9
|
+
* above `first:` leaves no colon behind. A text that leads into nothing stays as it is.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* withoutLeadIn('found these at amazon. two came in under $20:'); // 'found these at amazon.'
|
|
13
|
+
* withoutLeadIn('two things:\nfirst:'); // ''
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function withoutLeadIn(text: string): string;
|
package/dist/sentences.js
CHANGED
|
@@ -27,3 +27,31 @@ export function lastSentenceStart(text) {
|
|
|
27
27
|
start = end.index + end[0].length;
|
|
28
28
|
return start;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* A text that leads into what comes after it with a colon, as `two options came in under $20:` or `here are a few:` does,
|
|
32
|
+
* a closing quote or bracket allowed after the colon.
|
|
33
|
+
*/
|
|
34
|
+
const LEAD_IN = /:["')\]]*$/;
|
|
35
|
+
/** True when `text` ends with a colon that leads into what follows it, so it never stands alone as a message. */
|
|
36
|
+
export function leadsIn(text) {
|
|
37
|
+
return LEAD_IN.test(String(text ?? '').trim());
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The text less every sentence at its end that leads in with a colon, for lead-ins nothing follows: the list they led
|
|
41
|
+
* into went (its cards show it), and a colon would point at nothing. Lead-ins in a row go together, so `two things:`
|
|
42
|
+
* above `first:` leaves no colon behind. A text that leads into nothing stays as it is.
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* withoutLeadIn('found these at amazon. two came in under $20:'); // 'found these at amazon.'
|
|
46
|
+
* withoutLeadIn('two things:\nfirst:'); // ''
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function withoutLeadIn(text) {
|
|
50
|
+
let rest = String(text ?? '').trim();
|
|
51
|
+
while (rest !== '' && leadsIn(rest)) {
|
|
52
|
+
const at = rest.lastIndexOf('\n') + 1;
|
|
53
|
+
const last = rest.slice(at);
|
|
54
|
+
rest = (rest.slice(0, at) + last.slice(0, lastSentenceStart(last))).trim();
|
|
55
|
+
}
|
|
56
|
+
return rest;
|
|
57
|
+
}
|
package/dist/texting.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface ReplyAsWrittenOptions {
|
|
|
41
41
|
* 6"), and Agentcard and `directCall` read that number against the list. Null for any other answer, which your model
|
|
42
42
|
* puts in its own words, for a line the tool wrote itself for your model, and once the shopper read the reply through
|
|
43
43
|
* `onText` (`shopper_already_read_reply`). `{ textingStyle: true }` hands it over in the texting style (`textingStyle`).
|
|
44
|
+
* A paragraph that ends with a colon, such as `two options came in under $20:`, shares its message with the paragraph
|
|
45
|
+
* after it, and one that nothing follows loses its colon sentence, so the shopper never reads a message ending on a colon.
|
|
44
46
|
*
|
|
45
47
|
* ```ts
|
|
46
48
|
* replyAsWritten(result); // 'which bread do you want?\n\n• Sourdough\n• Whole wheat'
|
package/dist/texting.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { forModel, replyAsWritten as replyWithoutCardList } from './model-view.js';
|
|
2
|
+
import { toMessages } from './messages.js';
|
|
2
3
|
/** The mark every list line starts with. */
|
|
3
4
|
const BULLET = '• ';
|
|
4
5
|
/** A line that is a list item: `- `, `* `, `• `, `1. ` or `1) `, then text. The number, when it has one, is group 2. */
|
|
@@ -184,6 +185,8 @@ function namesOne(item, names) {
|
|
|
184
185
|
* 6"), and Agentcard and `directCall` read that number against the list. Null for any other answer, which your model
|
|
185
186
|
* puts in its own words, for a line the tool wrote itself for your model, and once the shopper read the reply through
|
|
186
187
|
* `onText` (`shopper_already_read_reply`). `{ textingStyle: true }` hands it over in the texting style (`textingStyle`).
|
|
188
|
+
* A paragraph that ends with a colon, such as `two options came in under $20:`, shares its message with the paragraph
|
|
189
|
+
* after it, and one that nothing follows loses its colon sentence, so the shopper never reads a message ending on a colon.
|
|
187
190
|
*
|
|
188
191
|
* ```ts
|
|
189
192
|
* replyAsWritten(result); // 'which bread do you want?\n\n• Sourdough\n• Whole wheat'
|
|
@@ -196,7 +199,9 @@ export function replyAsWritten(result, options = {}) {
|
|
|
196
199
|
const view = forModel(result);
|
|
197
200
|
const names = [...view.products.map((product) => product.name), ...view.stores.map((store) => store.name)].map(wordsOf).filter((name) => name.length > 0);
|
|
198
201
|
const bulleted = withChoice(bulletLists(reply, { keepNumbers: (items) => items.some((item) => namesOne(item, names)) }), result.choice ?? null);
|
|
199
|
-
|
|
202
|
+
// Read as the shopper's chat cuts it (toMessages), so a paragraph that leads in with a colon shares its message with
|
|
203
|
+
// the one after it, and one nothing follows loses its colon sentence: no message of it ends on a colon.
|
|
204
|
+
return toMessages(options.textingStyle ? styleLines(bulleted) : bulleted).join('\n\n') || null;
|
|
200
205
|
}
|
|
201
206
|
/**
|
|
202
207
|
* One message of Agentcard's, as `onText` hands it to the app: its lists as `• ` bullet lines and, with `texting`, in
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-cards/commerce-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Give your agent one tool that buys for your shoppers at real stores, and post each purchase update Agentcard sends into the shopper's chat, over a connection your server opens or a webhook endpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|