Skip to content

Commit

Permalink
Support multiple JSON blocks (#251)
Browse files Browse the repository at this point in the history
Multiple JSON blocks bug fixes

Sometimes two JSON blocks of different types would not be found
properly. This has been fixed.
  • Loading branch information
richardgill authored May 31, 2024
1 parent b264096 commit 88eb937
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 25 deletions.
7 changes: 7 additions & 0 deletions .changeset/curly-peaches-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@llm-ui/json": patch
---

Multiple JSON blocks bug fixes

Sometimes two JSON blocks of different types would not be found properly. This has been fixed.
30 changes: 30 additions & 0 deletions packages/csv/src/matchers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,36 @@ describe("findCompleteCsvBlock", () => {
outputRaw: "⦅t,a,b,c⦆",
},
},
{
name: "two same type blocks",
input: "⦅t,a,b,c⦆⦅t,a,b,c⦆",
options: { type: "t" },
expected: {
startIndex: 0,
endIndex: 9,
outputRaw: "⦅t,a,b,c⦆",
},
},
{
name: "two different blocks",
input: "⦅t,a,b,c⦆⦅z,a,b,c⦆",
options: { type: "t" },
expected: {
startIndex: 0,
endIndex: 9,
outputRaw: "⦅t,a,b,c⦆",
},
},
{
name: "two different blocks reversed",
input: "⦅z,a,b,c⦆⦅t,a,b,c⦆",
options: { type: "t" },
expected: {
startIndex: 9,
endIndex: 18,
outputRaw: "⦅t,a,b,c⦆",
},
},
{
name: "not a block",
input: "```\nhello\n```",
Expand Down
30 changes: 30 additions & 0 deletions packages/json/src/matchers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,36 @@ describe("findCompleteJsonBlock", () => {
outputRaw: '【{type:"buttons"}】',
},
},
{
name: "full custom same component twice",
input: '【{type:"buttons"}】【{type:"buttons"}】',
options: { type: "buttons" },
expected: {
startIndex: 0,
endIndex: 18,
outputRaw: '【{type:"buttons"}】',
},
},
{
name: "full custom 2 different components",
input: '【{type:"buttons"}】【{type:"somethingelse"}】',
options: { type: "buttons" },
expected: {
startIndex: 0,
endIndex: 18,
outputRaw: '【{type:"buttons"}】',
},
},
{
name: "full custom 2 different components reversed",
input: '【{type:"somethingelse"}】【{type:"buttons"}】',
options: { type: "buttons" },
expected: {
startIndex: 24,
endIndex: 42,
outputRaw: '【{type:"buttons"}】',
},
},
{
name: "full custom component with fields",
input: '【{type:"buttons", something: "something", else: "else"}】',
Expand Down
24 changes: 13 additions & 11 deletions packages/json/src/matchers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LLMOutputMatcher } from "@llm-ui/react";
import { regexMatcher, removeStartEndChars } from "@llm-ui/shared";
import { regexMatcherGlobal, removeStartEndChars } from "@llm-ui/shared";
import {
JsonBlockOptions,
JsonBlockOptionsComplete,
Expand All @@ -12,18 +12,20 @@ const findJsonBlock = (
options: JsonBlockOptionsComplete,
): LLMOutputMatcher => {
const { type } = options;
const matcher = regexMatcher(regex);
const matcher = regexMatcherGlobal(regex);
return (llmOutput: string) => {
const match = matcher(llmOutput);
if (!match) {
const matches = matcher(llmOutput);
if (matches.length === 0) {
return undefined;
}
const block = parseJson5(removeStartEndChars(match.outputRaw, options));
return matches.find((match) => {
const block = parseJson5(removeStartEndChars(match.outputRaw, options));

if (!block || block[options.typeKey] !== type) {
return undefined;
}
return match;
if (!block || block[options.typeKey] !== type) {
return undefined;
}
return match;
});
};
};

Expand All @@ -32,7 +34,7 @@ export const findCompleteJsonBlock = (
): LLMOutputMatcher => {
const options = getOptions(userOptions);
const { startChar, endChar } = options;
const regex = new RegExp(`${startChar}([\\s\\S]*?)${endChar}`);
const regex = new RegExp(`${startChar}([\\s\\S]*?)${endChar}`, "g");
return findJsonBlock(regex, options);
};

Expand All @@ -41,6 +43,6 @@ export const findPartialJsonBlock = (
): LLMOutputMatcher => {
const options = getOptions(userOptions);
const { startChar } = options;
const regex = new RegExp(`${startChar}([\\s\\S]*)`);
const regex = new RegExp(`${startChar}([\\s\\S]*)`, "g");
return findJsonBlock(regex, options);
};
2 changes: 1 addition & 1 deletion packages/shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { regexMatcher } from "./regexMatcher";
export { regexMatcher, regexMatcherGlobal } from "./regexMatcher";
export { removeStartEndChars } from "./removeStartEndChars";
68 changes: 67 additions & 1 deletion packages/shared/src/regexMatcher.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { regexMatcher } from "./regexMatcher";
import { regexMatcher, regexMatcherGlobal } from "./regexMatcher";

describe("regexMatcher", () => {
const testCases = [
Expand Down Expand Up @@ -44,3 +44,69 @@ describe("regexMatcher", () => {
});
});
});

describe("regexMatcherGlobal", () => {
const testCases = [
{
input: "hello",
regex: /hello/g,
expected: [
{
startIndex: 0,
endIndex: 5,
outputRaw: "hello",
},
],
},
{
input: "abc hello",
regex: /hello/g,
expected: [
{
startIndex: 4,
endIndex: 9,
outputRaw: "hello",
},
],
},
{
input: "abc hello def",
regex: /hello/g,
expected: [
{
startIndex: 4,
endIndex: 9,
outputRaw: "hello",
},
],
},
{
input: "abc hello def hello",
regex: /hello/g,
expected: [
{
startIndex: 4,
endIndex: 9,
outputRaw: "hello",
},
{
startIndex: 14,
endIndex: 19,
outputRaw: "hello",
},
],
},
{
input: "abc yellow def",
regex: /hello/g,
expected: [],
},
];

testCases.forEach(({ input, regex, expected }) => {
it(`should match ${input} with ${regex}`, () => {
const result = regexMatcherGlobal(regex)(input);
expect(result).toEqual(expected);
});
});
});
47 changes: 35 additions & 12 deletions packages/shared/src/regexMatcher.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,41 @@
import { MaybeLLMOutputMatch } from "@llm-ui/react";
import { LLMOutputMatch, MaybeLLMOutputMatch } from "@llm-ui/react";

const regexMatchToLLmOutputMatch = (
regexMatch: RegExpMatchArray | null,
): MaybeLLMOutputMatch => {
if (regexMatch) {
const matchString = regexMatch[0];
const startIndex = regexMatch.index!;
const endIndex = startIndex + matchString.length;
return {
startIndex,
endIndex,
outputRaw: matchString,
};
}
return undefined;
};

export const regexMatcher =
(regex: RegExp) =>
(llmOutput: string): MaybeLLMOutputMatch => {
const regexMatch = llmOutput.match(regex);
if (regexMatch) {
const matchString = regexMatch[0];
const startIndex = regexMatch.index!;
const endIndex = startIndex + matchString.length;
return {
startIndex,
endIndex,
outputRaw: matchString,
};
if (regex.global) {
throw new Error("regexMatcher does not support global regexes");
}
return regexMatchToLLmOutputMatch(llmOutput.match(regex));
};

export const regexMatcherGlobal =
(regex: RegExp) =>
(llmOutput: string): LLMOutputMatch[] => {
if (!regex.global) {
throw new Error("regexMatcherGlobal does not support non-global regexes");
}
const matches = Array.from(llmOutput.matchAll(regex));
if (!matches) {
return [];
}
return undefined;
return matches
.map((m) => regexMatchToLLmOutputMatch(m))
.filter((m) => m !== undefined) as LLMOutputMatch[];
};

0 comments on commit 88eb937

Please sign in to comment.