diff --git a/.changeset/selfish-jars-mix.md b/.changeset/selfish-jars-mix.md new file mode 100644 index 00000000..e3932a1a --- /dev/null +++ b/.changeset/selfish-jars-mix.md @@ -0,0 +1,5 @@ +--- +"@llm-ui/csv": minor +--- + +@llm-ui/csv: Added type option to distinguish between custom blocks diff --git a/packages/csv/src/block.tsx b/packages/csv/src/block.tsx index a9f4a447..00b59d02 100644 --- a/packages/csv/src/block.tsx +++ b/packages/csv/src/block.tsx @@ -3,13 +3,11 @@ import { csvBlockLookBack } from "./lookback"; import { findCompleteCsvBlock, findPartialCsvBlock } from "./matchers"; import { CsvBlockOptions } from "./options"; -export const csvBlock = ( - userOptions?: Partial, -): LLMOutputBlock => { +export const csvBlock = (options: CsvBlockOptions): LLMOutputBlock => { return { - findCompleteMatch: findCompleteCsvBlock(userOptions), - findPartialMatch: findPartialCsvBlock(userOptions), - lookBack: csvBlockLookBack(userOptions), + findCompleteMatch: findCompleteCsvBlock(options), + findPartialMatch: findPartialCsvBlock(options), + lookBack: csvBlockLookBack(options), component: ({ blockMatch }) => (
diff --git a/packages/csv/src/csvBlockExample.test.ts b/packages/csv/src/csvBlockExample.test.ts index ec3a124a..cb3de91d 100644 --- a/packages/csv/src/csvBlockExample.test.ts +++ b/packages/csv/src/csvBlockExample.test.ts @@ -5,7 +5,7 @@ import { CsvBlockOptions } from "./options"; type TestCase = { name: string; example: string[]; - options?: Partial; + options: CsvBlockOptions; expected: string; }; @@ -14,24 +14,32 @@ describe("csvBlockExample", () => { { name: "single", example: ["abc"], - expected: "⦅abc⦆", + options: { type: "t" }, + expected: "⦅t,abc⦆", }, { name: "multiple", example: ["abc", "def"], - expected: "⦅abc,def⦆", + options: { type: "t" }, + expected: "⦅t,abc,def⦆", + }, + { + name: "custom type", + example: ["abc", "def"], + options: { type: "type", delimiter: ";" }, + expected: "⦅type;abc;def⦆", }, { name: "custom delimiter", example: ["abc", "def"], - options: { delimiter: ";" }, - expected: "⦅abc;def⦆", + options: { type: "t", delimiter: ";" }, + expected: "⦅t;abc;def⦆", }, { name: "custom start and end chars", example: ["abc", "def"], - options: { startChar: "x", endChar: "y" }, - expected: "xabc,defy", + options: { type: "t", startChar: "x", endChar: "y" }, + expected: "xt,abc,defy", }, ]; diff --git a/packages/csv/src/csvBlockExample.ts b/packages/csv/src/csvBlockExample.ts index 6780c682..d55f9be6 100644 --- a/packages/csv/src/csvBlockExample.ts +++ b/packages/csv/src/csvBlockExample.ts @@ -2,8 +2,8 @@ import { CsvBlockOptions, getOptions } from "./options"; export const csvBlockExample = ( example: string[], - userOptions?: Partial, + options: CsvBlockOptions, ): string => { - const { startChar, endChar, delimiter } = getOptions(userOptions); - return `${startChar}${example.join(delimiter)}${endChar}`; + const { startChar, endChar, delimiter, type } = getOptions(options); + return `${startChar}${[type, ...example].join(delimiter)}${endChar}`; }; diff --git a/packages/csv/src/csvBlockPrompt.test.ts b/packages/csv/src/csvBlockPrompt.test.ts index b83aa37b..225cfe67 100644 --- a/packages/csv/src/csvBlockPrompt.test.ts +++ b/packages/csv/src/csvBlockPrompt.test.ts @@ -6,13 +6,14 @@ describe("csvBlockPrompt", () => { expect( csvBlockPrompt({ name: "simple", + options: { type: "buttons" }, examples: [["abc"]], }), ).toMatchInlineSnapshot(` - "You can respond with a simple component by wrapping a , separated string in ⦅⦆ tags. + "You can respond with a simple component using the following , delimited syntax: ⦅buttons,⦆ Examples: - ⦅abc⦆" + ⦅buttons,abc⦆" `); }); @@ -20,18 +21,18 @@ describe("csvBlockPrompt", () => { expect( csvBlockPrompt({ name: "complex", - + options: { type: "buttons" }, examples: [ ["abc", "def"], ["ghi", "jkl"], ], }), ).toMatchInlineSnapshot(` - "You can respond with a complex component by wrapping a , separated string in ⦅⦆ tags. + "You can respond with a complex component using the following , delimited syntax: ⦅buttons,⦆ Examples: - ⦅abc,def⦆ - ⦅ghi,jkl⦆" + ⦅buttons,abc,def⦆ + ⦅buttons,ghi,jkl⦆" `); }); @@ -39,38 +40,36 @@ describe("csvBlockPrompt", () => { expect( csvBlockPrompt({ name: "complex", - + options: { type: "buttons", delimiter: ";" }, examples: [ ["abc", "def"], ["ghi", "jkl"], ], - userOptions: { delimiter: ";" }, }), ).toMatchInlineSnapshot(` - "You can respond with a complex component by wrapping a ; separated string in ⦅⦆ tags. + "You can respond with a complex component using the following ; delimited syntax: ⦅buttons;⦆ Examples: - ⦅abc;def⦆ - ⦅ghi;jkl⦆" + ⦅buttons;abc;def⦆ + ⦅buttons;ghi;jkl⦆" `); }); it("custom start and end chars", () => { expect( csvBlockPrompt({ name: "complex", - + options: { type: "type", startChar: "x", endChar: "y" }, examples: [ ["abc", "def"], ["ghi", "jkl"], ], - userOptions: { startChar: "x", endChar: "y" }, }), ).toMatchInlineSnapshot(` - "You can respond with a complex component by wrapping a , separated string in xy tags. + "You can respond with a complex component using the following , delimited syntax: xtype,y Examples: - xabc,defy - xghi,jkly" + xtype,abc,defy + xtype,ghi,jkly" `); }); }); diff --git a/packages/csv/src/csvBlockPrompt.ts b/packages/csv/src/csvBlockPrompt.ts index ef088e74..26394bd4 100644 --- a/packages/csv/src/csvBlockPrompt.ts +++ b/packages/csv/src/csvBlockPrompt.ts @@ -4,15 +4,15 @@ import { CsvBlockOptions, getOptions } from "./options"; export const csvBlockPrompt = ({ name, examples, - userOptions, + options, }: { name: string; examples: string[][]; - userOptions?: Partial; + options: CsvBlockOptions; }): string => { - const { startChar, endChar, delimiter } = getOptions(userOptions); + const { startChar, endChar, delimiter, type } = getOptions(options); const examplePrompts = examples.map((example) => - csvBlockExample(example, userOptions), + csvBlockExample(example, options), ); - return `You can respond with a ${name} component by wrapping a ${delimiter} separated string in ${startChar}${endChar} tags.\n\nExamples: \n${examplePrompts.join(`\n`)}`; + return `You can respond with a ${name} component using the following ${delimiter} delimited syntax: ${startChar}${type}${delimiter}${endChar}\n\nExamples: \n${examplePrompts.join(`\n`)}`; }; diff --git a/packages/csv/src/lookback.test.ts b/packages/csv/src/lookback.test.ts index a81c02f5..bc8d862f 100644 --- a/packages/csv/src/lookback.test.ts +++ b/packages/csv/src/lookback.test.ts @@ -6,7 +6,7 @@ import { CsvBlockOptions } from "./options"; type TestCase = { name: string; output: string; - options?: Partial; + options: CsvBlockOptions; isStreamFinished: boolean; isComplete: boolean; visibleTextLengthTarget: number; @@ -17,171 +17,195 @@ describe("csvBlockLookBack", () => { const testCases: TestCase[] = [ { name: "full", - output: "⦅a⦆", + output: "⦅t,a⦆", isStreamFinished: true, isComplete: true, visibleTextLengthTarget: 1, + options: { type: "t" }, expected: { - output: "a", + output: "t,a", visibleText: "a", }, }, { name: "delimited", - output: "⦅abc,def⦆", + output: "⦅t,abc,def⦆", isStreamFinished: true, isComplete: true, visibleTextLengthTarget: 6, + options: { type: "t" }, expected: { - output: "abc,def", + output: "t,abc,def", visibleText: "abcdef", }, }, { name: "delimited custom char", - output: "⦅abc;def⦆", + output: "⦅t;abc;def⦆", isStreamFinished: true, isComplete: true, visibleTextLengthTarget: 6, - options: { delimiter: ";" }, + options: { type: "t", delimiter: ";" }, expected: { - output: "abc;def", + output: "t;abc;def", visibleText: "abcdef", }, }, { name: "custom start and end char", - output: "xabc,defy", + output: "xt,abc,defy", isStreamFinished: true, isComplete: true, visibleTextLengthTarget: 6, - options: { startChar: "x", endChar: "y" }, + options: { type: "t", startChar: "x", endChar: "y" }, expected: { - output: "abc,def", + output: "t,abc,def", + visibleText: "abcdef", + }, + }, + { + name: "custom type", + output: "⦅type,abc,def⦆", + isStreamFinished: true, + isComplete: true, + visibleTextLengthTarget: 6, + options: { type: "type" }, + expected: { + output: "type,abc,def", visibleText: "abcdef", }, }, { name: "visible text limited", - output: "⦅abc,def⦆", + output: "⦅t,abc,def⦆", isStreamFinished: true, isComplete: true, visibleTextLengthTarget: 5, + options: { type: "t" }, expected: { - output: "abc,de", + output: "t,abc,de", visibleText: "abcde", }, }, { name: "full no visible text", - output: "⦅abc,def⦆", + output: "⦅t,abc,def⦆", isStreamFinished: true, isComplete: true, visibleTextLengthTarget: 0, + options: { type: "t" }, expected: { - output: ",", + output: "t,,", visibleText: "", }, }, { name: "started", - output: "⦅", + output: "⦅t,", isStreamFinished: false, isComplete: false, visibleTextLengthTarget: 3, + options: { type: "t" }, expected: { - output: "", + output: "t", visibleText: "", }, }, { name: "partial", - output: "⦅abc", + output: "⦅t,abc", isStreamFinished: false, isComplete: false, visibleTextLengthTarget: 4, + options: { type: "t" }, expected: { - output: "abc", + output: "t,abc", visibleText: "abc", }, }, { name: "partial with delimiter", - output: "⦅abc,", + output: "⦅t;abc;", isStreamFinished: false, isComplete: false, visibleTextLengthTarget: 4, + options: { type: "t", delimiter: ";" }, expected: { - output: "abc", + output: "t;abc", visibleText: "abc", }, }, { name: "partial with second element", - output: "⦅abc,d", + output: "⦅t,abc,d", isStreamFinished: false, isComplete: false, visibleTextLengthTarget: 4, + options: { type: "t" }, expected: { - output: "abc,d", + output: "t,abc,d", visibleText: "abcd", }, }, { name: "allIndexesVisible: false", - output: "⦅abc,def⦆", + output: "⦅t,abc,def⦆", options: { + type: "t", allIndexesVisible: false, }, isStreamFinished: true, isComplete: true, visibleTextLengthTarget: 5, expected: { - output: "abc,def", + output: "t,abc,def", visibleText: " ", }, }, { name: "allIndexesVisible: false + partial", - output: "⦅abc,de", + output: "⦅t,abc,de", options: { + type: "t", allIndexesVisible: false, }, isStreamFinished: false, isComplete: false, visibleTextLengthTarget: 5, expected: { - output: "abc,de", + output: "t,abc,de", visibleText: "", }, }, { name: "visibleIndexes 1st only", - output: "⦅abc,def⦆", + output: "⦅t,abc,def⦆", options: { + type: "t", allIndexesVisible: false, - visibleIndexes: [0], + visibleIndexes: [1], }, isStreamFinished: true, isComplete: true, visibleTextLengthTarget: 5, expected: { - output: "abc,def", + output: "t,abc,def", visibleText: "abc", }, }, { name: "visibleIndexes 0th and 2nd", - output: "⦅abc,def,ghi⦆", + output: "⦅t,abc,def,ghi⦆", options: { + type: "t", allIndexesVisible: false, - visibleIndexes: [0, 2], + visibleIndexes: [1, 3], }, isStreamFinished: true, isComplete: true, visibleTextLengthTarget: 5, expected: { - output: "abc,def,gh", + output: "t,abc,def,gh", visibleText: "abcgh", }, }, diff --git a/packages/csv/src/lookback.ts b/packages/csv/src/lookback.ts index 0f1ce7a6..1f60cf1c 100644 --- a/packages/csv/src/lookback.ts +++ b/packages/csv/src/lookback.ts @@ -4,19 +4,22 @@ import { CsvBlockOptions, getOptions } from "./options"; import { parseCsv } from "./parseCsv"; export const csvBlockLookBack = ( - userOptions?: Partial, + options: CsvBlockOptions, ): LookBackFunction => { - const options = getOptions(userOptions); + const completeOptions = getOptions(options); const { allIndexesVisible, visibleIndexes: visibleIndexesOption, delimiter, - } = options; + } = completeOptions; return ({ output, isComplete, visibleTextLengthTarget }) => { - const array = parseCsv(removeStartEndChars(output, options), options); + const array = parseCsv( + removeStartEndChars(output, completeOptions), + completeOptions, + ); const visibleIndexes = allIndexesVisible - ? array.map((_, i) => i) + ? array.map((_, i) => i).slice(1) // Skip index 0 as it is the type : visibleIndexesOption; let charsRemaining = visibleTextLengthTarget; diff --git a/packages/csv/src/matchers.test.ts b/packages/csv/src/matchers.test.ts index f80f545e..797e01f8 100644 --- a/packages/csv/src/matchers.test.ts +++ b/packages/csv/src/matchers.test.ts @@ -7,72 +7,90 @@ type TestCase = { name: string; input: string; expected: MaybeLLMOutputMatch; - options?: CsvBlockOptions; + options: CsvBlockOptions; }; describe("findCompleteCsvBlock", () => { const testCases: TestCase[] = [ { name: "single char", - input: "⦅a⦆", + input: "⦅t,a⦆", + options: { type: "t" }, expected: { startIndex: 0, - endIndex: 3, - outputRaw: "⦅a⦆", + endIndex: 5, + outputRaw: "⦅t,a⦆", }, }, { name: "multiple chars", - input: "⦅abc⦆", + input: "⦅t,abc⦆", + options: { type: "t" }, expected: { startIndex: 0, - endIndex: 5, - outputRaw: "⦅abc⦆", + endIndex: 7, + outputRaw: "⦅t,abc⦆", }, }, { name: "delimited", - input: "⦅a,b,c⦆", + input: "⦅t,a,b,c⦆", + options: { type: "t" }, expected: { startIndex: 0, - endIndex: 7, - outputRaw: "⦅a,b,c⦆", + endIndex: 9, + outputRaw: "⦅t,a,b,c⦆", + }, + }, + { + name: "custom type", + input: "⦅type,a,b,c⦆", + options: { type: "type" }, + expected: { + startIndex: 0, + endIndex: 12, + outputRaw: "⦅type,a,b,c⦆", }, }, { name: "text before", - input: "abc⦅a,b,c⦆", + input: "abc⦅t,a,b,c⦆", + options: { type: "t" }, expected: { startIndex: 3, - endIndex: 10, - outputRaw: "⦅a,b,c⦆", + endIndex: 12, + outputRaw: "⦅t,a,b,c⦆", }, }, { name: "text after", - input: "⦅a,b,c⦆ def", + input: "⦅t,a,b,c⦆ def", + options: { type: "t" }, expected: { startIndex: 0, - endIndex: 7, - outputRaw: "⦅a,b,c⦆", + endIndex: 9, + outputRaw: "⦅t,a,b,c⦆", }, }, { name: "text before and after", - input: "abc⦅a,b,c⦆ def", + input: "abc⦅t,a,b,c⦆ def", + options: { type: "t" }, expected: { startIndex: 3, - endIndex: 10, - outputRaw: "⦅a,b,c⦆", + endIndex: 12, + outputRaw: "⦅t,a,b,c⦆", }, }, { name: "not a block", input: "```\nhello\n```", + options: { type: "t" }, expected: undefined, }, { name: "unfinished block", - input: "⦅a,b,c", + input: "⦅t,a,b,c", + options: { type: "t" }, expected: undefined, }, ]; @@ -88,44 +106,61 @@ describe("findCompleteCsvBlock", () => { describe("findPartialCsvBlock", () => { const testCases: TestCase[] = [ { - name: "single char", - input: "⦅a", + name: "opening bracket", + input: "⦅", + options: { type: "t" }, + expected: undefined, + }, + { + name: "opening bracket + type", + input: "⦅t", + options: { type: "t" }, + expected: undefined, + }, + { + name: "opening bracket + type + delimiter", + input: "⦅t,", + options: { type: "t" }, expected: { startIndex: 0, - endIndex: 2, - outputRaw: "⦅a", + endIndex: 3, + outputRaw: "⦅t,", }, }, { - name: "multiple chars", - input: "⦅abc", + name: "custom type", + input: "⦅type,", + options: { type: "type" }, expected: { startIndex: 0, - endIndex: 4, - outputRaw: "⦅abc", + endIndex: 6, + outputRaw: "⦅type,", }, }, { name: "delimited", - input: "⦅a,b,c", + input: "⦅t,a,b,c", + options: { type: "t" }, expected: { startIndex: 0, - endIndex: 6, - outputRaw: "⦅a,b,c", + endIndex: 8, + outputRaw: "⦅t,a,b,c", }, }, { name: "text before", - input: "abc⦅a,b,c", + input: "abc⦅t,a,b,c", + options: { type: "t" }, expected: { startIndex: 3, - endIndex: 9, - outputRaw: "⦅a,b,c", + endIndex: 11, + outputRaw: "⦅t,a,b,c", }, }, { name: "not a block", input: "```\nhello\n```", + options: { type: "t" }, expected: undefined, }, ]; diff --git a/packages/csv/src/matchers.ts b/packages/csv/src/matchers.ts index 6b43e9e1..a39ac30c 100644 --- a/packages/csv/src/matchers.ts +++ b/packages/csv/src/matchers.ts @@ -4,19 +4,21 @@ import { CsvBlockOptions, getOptions } from "./options"; import { regexMatcher } from "@llm-ui/shared"; export const findCompleteCsvBlock = ( - userOptions?: Partial, + options: CsvBlockOptions, ): LLMOutputMatcher => { - const { startChar, endChar } = getOptions(userOptions); + const { type, startChar, endChar, delimiter } = getOptions(options); - const regex = new RegExp(`${startChar}([\\s\\S]*?)${endChar}`); + const regex = new RegExp( + `${startChar}${type}${delimiter}([\\s\\S]*?)${endChar}`, + ); return regexMatcher(regex); }; export const findPartialCsvBlock = ( - userOptions?: Partial, + options: CsvBlockOptions, ): LLMOutputMatcher => { - const { startChar } = getOptions(userOptions); + const { type, startChar, delimiter } = getOptions(options); - const regex = new RegExp(`${startChar}([\\s\\S]*)`); + const regex = new RegExp(`${startChar}${type}${delimiter}([\\s\\S]*)`); return regexMatcher(regex); }; diff --git a/packages/csv/src/options.test.ts b/packages/csv/src/options.test.ts index 3e537758..3bdca2fd 100644 --- a/packages/csv/src/options.test.ts +++ b/packages/csv/src/options.test.ts @@ -1,9 +1,10 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ import { describe, expect, it } from "vitest"; import { CsvBlockOptions, getOptions } from "./options"; type TestCase = { name: string; - options?: Partial; + options: CsvBlockOptions; expectError: boolean; }; @@ -11,27 +12,41 @@ describe("getOptions", () => { const testCases: TestCase[] = [ { name: "valid options do not error 1 ", - options: undefined, + options: { type: "buttons" }, expectError: false, }, { name: "valid options do not error 2 ", - options: { startChar: "a", endChar: "b" }, + options: { type: "buttons", startChar: "a", endChar: "b" }, expectError: false, }, { name: "valid options do not error 3", - options: { allIndexesVisible: true }, + options: { type: "buttons", allIndexesVisible: true }, expectError: false, }, + { + name: "no type errors", + // @ts-expect-error + options: {}, + expectError: true, + }, { name: "allIndexesVisible: true, visibleIndexes: [1] errors", - options: { allIndexesVisible: true, visibleIndexes: [1] }, + options: { + type: "buttons", + allIndexesVisible: true, + visibleIndexes: [1], + }, expectError: true, }, { name: "allIndexesVisible: false, visibleIndexes: [1] ok", - options: { allIndexesVisible: false, visibleIndexes: [1] }, + options: { + type: "buttons", + allIndexesVisible: false, + visibleIndexes: [1], + }, expectError: false, }, ]; diff --git a/packages/csv/src/options.ts b/packages/csv/src/options.ts index 77ca8b12..4e3112ba 100644 --- a/packages/csv/src/options.ts +++ b/packages/csv/src/options.ts @@ -1,4 +1,5 @@ -export type CsvBlockOptions = { +export type CsvBlockOptionsComplete = { + type: string; startChar: string; endChar: string; delimiter: string; @@ -6,7 +7,7 @@ export type CsvBlockOptions = { visibleIndexes: number[]; }; -export const defaultOptions: CsvBlockOptions = { +export const defaultOptions: Omit = { startChar: "⦅", endChar: "⦆", delimiter: ",", @@ -14,9 +15,17 @@ export const defaultOptions: CsvBlockOptions = { visibleIndexes: [], }; -export const getOptions = (userOptions?: Partial) => { +export type CsvBlockOptions = Partial> & + Pick; + +export const getOptions = ( + userOptions: CsvBlockOptions, +): CsvBlockOptionsComplete => { const result = { ...defaultOptions, ...userOptions }; - const { allIndexesVisible, visibleIndexes } = result; + const { type, allIndexesVisible, visibleIndexes } = result; + if (!type) { + throw new Error("type option is required"); + } if (allIndexesVisible && visibleIndexes.length > 0) { throw new Error( "visibleIndexes should be [] when allIndexesVisible is true", diff --git a/packages/csv/src/parseCsv.test.ts b/packages/csv/src/parseCsv.test.ts index a2a3faf6..f0ff19db 100644 --- a/packages/csv/src/parseCsv.test.ts +++ b/packages/csv/src/parseCsv.test.ts @@ -6,34 +6,52 @@ type TestCase = { name: string; input: string; expected: string[]; - options?: Partial; + options: CsvBlockOptions; }; describe("parseCsv", () => { const testCases: TestCase[] = [ - { name: "single char", input: "a", expected: ["a"] }, - { name: "multiple chars", input: "abc", expected: ["abc"] }, - { name: "separated", input: "abc,def", expected: ["abc", "def"] }, + { + name: "single char", + input: "t", + options: { type: "t" }, + expected: ["t"], + }, + { + name: "multiple chars", + input: "type", + options: { type: "type" }, + expected: ["type"], + }, + { + name: "separated", + input: "t,abc,def", + options: { type: "t" }, + expected: ["t", "abc", "def"], + }, { name: "leading whitespace", - input: " abc,def", - expected: [" abc", "def"], + input: "t, abc,def", + options: { type: "t" }, + expected: ["t", " abc", "def"], }, { name: "trailing whitespace", - input: "abc,def ", - expected: ["abc", "def "], + input: "t,abc,def ", + options: { type: "t" }, + expected: ["t", "abc", "def "], }, { name: "trailing delimiter", - input: "abc,", - expected: ["abc"], + input: "t,abc,", + options: { type: "t" }, + expected: ["t", "abc"], }, { name: "separated custom delimiter", - input: "abc;def", - options: { delimiter: ";" }, - expected: ["abc", "def"], + input: "t;abc;def", + options: { type: "t", delimiter: ";" }, + expected: ["t", "abc", "def"], }, ]; diff --git a/packages/csv/src/parseCsv.ts b/packages/csv/src/parseCsv.ts index d9c33f70..4c4d1f92 100644 --- a/packages/csv/src/parseCsv.ts +++ b/packages/csv/src/parseCsv.ts @@ -1,9 +1,6 @@ import { CsvBlockOptions, getOptions } from "./options"; -export const parseCsv = ( - csv: string, - options?: Partial, -): string[] => { +export const parseCsv = (csv: string, options: CsvBlockOptions): string[] => { const { delimiter } = getOptions(options); return csv.split(delimiter).filter((item) => item !== ""); };