mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 19:40:43 +00:00
160 lines
4.8 KiB
TypeScript
160 lines
4.8 KiB
TypeScript
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
|
import {
|
|
createAssistantMessageEventStream,
|
|
type AssistantMessage,
|
|
type Context,
|
|
type Model,
|
|
} from "@mariozechner/pi-ai";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
applyPluginTextReplacements,
|
|
mergePluginTextTransforms,
|
|
transformStreamContextText,
|
|
wrapStreamFnTextTransforms,
|
|
} from "./plugin-text-transforms.js";
|
|
|
|
const model = {
|
|
api: "openai-responses",
|
|
provider: "test",
|
|
id: "test-model",
|
|
} as Model<"openai-responses">;
|
|
|
|
function makeAssistantMessage(text: string): AssistantMessage {
|
|
return {
|
|
role: "assistant",
|
|
content: [{ type: "text", text }],
|
|
stopReason: "stop",
|
|
api: "openai-responses",
|
|
provider: "test",
|
|
model: "test-model",
|
|
usage: {
|
|
input: 1,
|
|
output: 1,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 2,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
},
|
|
timestamp: 0,
|
|
};
|
|
}
|
|
|
|
describe("plugin text transforms", () => {
|
|
it("merges registered transform groups in order", () => {
|
|
const merged = mergePluginTextTransforms(
|
|
{ input: [{ from: /red basket/g, to: "blue basket" }] },
|
|
{ output: [{ from: /blue basket/g, to: "red basket" }] },
|
|
{ input: [{ from: /paper ticket/g, to: "digital ticket" }] },
|
|
);
|
|
|
|
expect(merged?.input).toHaveLength(2);
|
|
expect(merged?.output).toHaveLength(1);
|
|
expect(applyPluginTextReplacements("red basket paper ticket", merged?.input)).toBe(
|
|
"blue basket digital ticket",
|
|
);
|
|
});
|
|
|
|
it("applies ordered string and regexp replacements", () => {
|
|
expect(
|
|
applyPluginTextReplacements("paper ticket on the left shelf", [
|
|
{ from: /paper ticket/g, to: "digital ticket" },
|
|
{ from: /left shelf/g, to: "right shelf" },
|
|
{ from: "digital ticket", to: "counter receipt" },
|
|
]),
|
|
).toBe("counter receipt on the right shelf");
|
|
});
|
|
|
|
it("rewrites system prompt and message text content before transport", () => {
|
|
const context = transformStreamContextText(
|
|
{
|
|
systemPrompt: "Use orchid mailbox inside north tower",
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "text", text: "Please use the red basket" },
|
|
{ type: "image", url: "data:image/png;base64,abc" },
|
|
],
|
|
},
|
|
],
|
|
} as Context,
|
|
[
|
|
{
|
|
from: /orchid mailbox/g,
|
|
to: "pine mailbox",
|
|
},
|
|
{ from: /red basket/g, to: "blue basket" },
|
|
],
|
|
) as unknown as { systemPrompt: string; messages: Array<{ content: unknown[] }> };
|
|
|
|
expect(context.systemPrompt).toBe("Use pine mailbox inside north tower");
|
|
expect(context.messages[0]?.content[0]).toMatchObject({
|
|
type: "text",
|
|
text: "Please use the blue basket",
|
|
});
|
|
expect(context.messages[0]?.content[1]).toMatchObject({
|
|
type: "image",
|
|
url: "data:image/png;base64,abc",
|
|
});
|
|
});
|
|
|
|
it("wraps stream functions with inbound and outbound replacements", async () => {
|
|
let capturedContext: Context | undefined;
|
|
const baseStreamFn: StreamFn = (_model, context) => {
|
|
capturedContext = context;
|
|
const stream = createAssistantMessageEventStream();
|
|
queueMicrotask(() => {
|
|
const partial = makeAssistantMessage("blue basket on the right shelf");
|
|
stream.push({
|
|
type: "text_delta",
|
|
contentIndex: 0,
|
|
delta: "blue basket on the right shelf",
|
|
partial,
|
|
});
|
|
stream.push({
|
|
type: "done",
|
|
reason: "stop",
|
|
message: makeAssistantMessage("final blue basket on the right shelf"),
|
|
});
|
|
stream.end();
|
|
});
|
|
return stream;
|
|
};
|
|
|
|
const wrapped = wrapStreamFnTextTransforms({
|
|
streamFn: baseStreamFn,
|
|
input: [{ from: /red basket/g, to: "blue basket" }],
|
|
output: [
|
|
{ from: /blue basket/g, to: "red basket" },
|
|
{ from: /right shelf/g, to: "left shelf" },
|
|
],
|
|
transformSystemPrompt: false,
|
|
});
|
|
const stream = await Promise.resolve(
|
|
wrapped(
|
|
model,
|
|
{
|
|
systemPrompt: "Keep red basket untouched here",
|
|
messages: [{ role: "user", content: "Use red basket" }],
|
|
} as Context,
|
|
undefined,
|
|
),
|
|
);
|
|
const events = [];
|
|
for await (const event of stream) {
|
|
events.push(event);
|
|
}
|
|
const result = await stream.result();
|
|
|
|
expect(capturedContext?.systemPrompt).toBe("Keep red basket untouched here");
|
|
expect(capturedContext?.messages).toMatchObject([{ role: "user", content: "Use blue basket" }]);
|
|
expect(events[0]).toMatchObject({
|
|
type: "text_delta",
|
|
delta: "red basket on the left shelf",
|
|
});
|
|
expect(result.content).toMatchObject([
|
|
{ type: "text", text: "final red basket on the left shelf" },
|
|
]);
|
|
});
|
|
});
|