Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple JSON blocks #251

Merged
merged 3 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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[];
};
Loading