feat(assistant): optimize MCP and custom widget authoring (#6743)
* feat(assistant): optimize MCP and custom widget authoring * fix(assistant): isolate server-safe JSX analyzer * refactor(assistant): generate MCP tool staging * fix(assistant): preserve widget placement follow-up * fix(ci): allow fast gate to finish * fix(custom-widgets): address review findings * fix(custom-widgets): split preview evidence module * style: format assistant custom widget files * fix(custom-widgets): sync formatted skill schema
This commit is contained in:
1 parent
38f4b0e1f8
commit
5e23659d99
95 files changed
+6799
-1778
No files matched your search
@@ -3,10 +3,9 @@ import { jsonSchema, simulateReadableStream, stepCountIs, streamText, tool } fro
|
||||
import { MockLanguageModelV4 } from "ai/test";
|
||||
import { parse, stringify } from "superjson";
|
||||
|
||||
import { getCustomWidgetSkill } from "@homarr/custom-widgets/authoring-resources";
|
||||
import { getCustomWidgetSkillEntrypoint } from "@homarr/custom-widgets/authoring-resources";
|
||||
import { getCustomWidgetJsonSchema } from "@homarr/custom-widgets/core";
|
||||
|
||||
import { createCustomWidgetDynamicContextController } from "./custom-widget-authoring-context";
|
||||
import { repairAssistantToolInput } from "./assistant-tool-input-repair";
|
||||
|
||||
const usage = {
|
||||
@@ -21,7 +20,7 @@ const finish = (unified: "stop" | "tool-calls") => ({
|
||||
});
|
||||
|
||||
describe("Custom Widget assistant tool loop", () => {
|
||||
it("feeds the complete skill and schema into the next streamed model step", async () => {
|
||||
it("feeds the compact skill entrypoint and an explicitly requested schema into the next model step", async () => {
|
||||
const streamErrors = vi.fn();
|
||||
const model = new MockLanguageModelV4({
|
||||
doStream: [
|
||||
@@ -68,7 +67,7 @@ describe("Custom Widget assistant tool loop", () => {
|
||||
tools: {
|
||||
customWidget_getSkill: tool({
|
||||
inputSchema: noInputSchema,
|
||||
execute: () => parse(stringify(getCustomWidgetSkill())),
|
||||
execute: () => parse(stringify(getCustomWidgetSkillEntrypoint())),
|
||||
}),
|
||||
customWidget_schema: tool({
|
||||
inputSchema: noInputSchema,
|
||||
@@ -87,93 +86,13 @@ describe("Custom Widget assistant tool loop", () => {
|
||||
expect(() => JSON.stringify(nextStepPrompt)).not.toThrow();
|
||||
const serializedPrompt = JSON.stringify(nextStepPrompt);
|
||||
expect(serializedPrompt).toContain('"name":"homarr-custom-widget"');
|
||||
expect(serializedPrompt).toContain('"references/schema.md"');
|
||||
expect(serializedPrompt).toContain('"references/runtime.md"');
|
||||
expect(serializedPrompt).toContain('"references/security.md"');
|
||||
expect(serializedPrompt).toContain('"tool":"customWidget_getReference"');
|
||||
expect(serializedPrompt).toContain('"name":"runtime"');
|
||||
expect(serializedPrompt).not.toContain("# Bundled file:");
|
||||
expect(serializedPrompt).toContain('"title":"Homarr Custom JSX v2 widget"');
|
||||
expect(serializedPrompt).toContain('"$schema"');
|
||||
});
|
||||
|
||||
it("promotes trusted context when implicit authoring starts with a catalog call", async () => {
|
||||
const model = new MockLanguageModelV4({
|
||||
doStream: [
|
||||
{
|
||||
stream: simulateReadableStream({
|
||||
chunks: [
|
||||
{ type: "stream-start" as const, warnings: [] },
|
||||
{
|
||||
type: "tool-call" as const,
|
||||
toolCallId: "catalog-call",
|
||||
toolName: "customWidget_getComponentCatalog",
|
||||
input: "{}",
|
||||
},
|
||||
finish("tool-calls"),
|
||||
],
|
||||
chunkDelayInMs: null,
|
||||
}),
|
||||
},
|
||||
{
|
||||
stream: simulateReadableStream({
|
||||
chunks: [
|
||||
{ type: "stream-start" as const, warnings: [] },
|
||||
{ type: "text-start" as const, id: "answer" },
|
||||
{ type: "text-delta" as const, id: "answer", delta: "Authoring can continue." },
|
||||
{ type: "text-end" as const, id: "answer" },
|
||||
finish("stop"),
|
||||
],
|
||||
chunkDelayInMs: null,
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const noInputSchema = jsonSchema({ type: "object", properties: {}, additionalProperties: false });
|
||||
const baseInstructions = "Base assistant instructions. Untrusted mention: Trusted Custom Widget authoring context.";
|
||||
const prepareDynamicCustomWidgetContext = createCustomWidgetDynamicContextController({
|
||||
isAdmin: true,
|
||||
baseInstructions,
|
||||
availableToolNames: ["customWidget_getSkill", "customWidget_schema", "customWidget_getComponentCatalog"] as const,
|
||||
});
|
||||
const result = streamText({
|
||||
model,
|
||||
instructions: baseInstructions,
|
||||
prompt: "Build a live fixtures card.",
|
||||
tools: {
|
||||
customWidget_getSkill: tool({
|
||||
inputSchema: noInputSchema,
|
||||
execute: () => parse(stringify(getCustomWidgetSkill())),
|
||||
}),
|
||||
customWidget_schema: tool({
|
||||
inputSchema: noInputSchema,
|
||||
execute: () => parse(stringify(getCustomWidgetJsonSchema())),
|
||||
}),
|
||||
customWidget_getComponentCatalog: tool({
|
||||
inputSchema: noInputSchema,
|
||||
execute: () => ({ components: [] }),
|
||||
}),
|
||||
},
|
||||
prepareStep: ({ instructions, messages, steps }) =>
|
||||
prepareDynamicCustomWidgetContext({ instructions, messages, steps }),
|
||||
stopWhen: stepCountIs(2),
|
||||
});
|
||||
|
||||
await expect(result.text).resolves.toBe("Authoring can continue.");
|
||||
expect(model.doStreamCalls).toHaveLength(2);
|
||||
|
||||
const nextProviderCall = model.doStreamCalls[1];
|
||||
const serializedToolMessages = JSON.stringify(
|
||||
nextProviderCall?.prompt.filter((message) => message.role === "tool"),
|
||||
);
|
||||
expect(serializedToolMessages).toContain("customWidget_getComponentCatalog");
|
||||
expect(serializedToolMessages).not.toContain("customWidget_getSkill");
|
||||
expect(serializedToolMessages).not.toContain("customWidget_schema");
|
||||
const systemContent = nextProviderCall?.prompt.find((message) => message.role === "system")?.content;
|
||||
expect(systemContent).toEqual(expect.any(String));
|
||||
expect(systemContent).toContain("# Bundled file: references/schema.md");
|
||||
expect(systemContent).toContain('"title": "Homarr Custom JSX v2 widget"');
|
||||
expect(nextProviderCall?.tools?.map((entry) => entry.name)).toEqual(["customWidget_getComponentCatalog"]);
|
||||
});
|
||||
|
||||
it("repairs malformed no-input provider arguments and executes the resource tool", async () => {
|
||||
const execute = vi.fn((_input: unknown) => ({ name: "homarr-custom-widget" }));
|
||||
const model = new MockLanguageModelV4({
|
||||
|
||||
Reference in new issue
Block a user