refactor(assistant): generate MCP tool staging

This commit is contained in:
ajnart committed 2026-08-30 18:41:05 +02:00
1 parent 0fbe1e3fa3
commit 05ae366fe1
17 files changed
+212 -380

No files matched your search

@@ -22,7 +22,7 @@ describe("createCustomWidgetToolStepGate", () => {
gate.begin(0);
expect(gate.claim("customWidget_getSkill")).toBe(true);
expect(gate.claim("web_search")).toBe(true);
expect(gate.claim("homarr_findTools")).toBe(true);
expect(gate.claim("homarr_enableToolGroups")).toBe(true);
expect(gate.claim("customWidget_getReference")).toBe(true);
expect(gate.claim("customWidget_getComponent")).toBe(true);
expect(gate.claim("customWidget_getComponent")).toBe(false);
@@ -30,7 +30,7 @@ describe("createCustomWidgetToolStepGate", () => {
gate.begin(1);
expect(gate.claim("customWidget_validateTemplate")).toBe(true);
expect(gate.claim("homarr_findTools")).toBe(false);
expect(gate.claim("homarr_enableToolGroups")).toBe(false);
expect(gate.claim("customWidget_previewCreate")).toBe(false);
gate.begin(2);
@@ -1,10 +1,7 @@
import { describe, expect, test } from "vitest";
import type { ModelMessage, UIMessage } from "ai";
import {
compactAssistantStepMessages,
convertAssistantMessagesToModelMessages,
} from "./assistant-message-conversion";
import { compactAssistantStepMessages, convertAssistantMessagesToModelMessages } from "./assistant-message-conversion";
describe("convertAssistantMessagesToModelMessages", () => {
test("drops an interrupted tool call while preserving completed tool results", async () => {
@@ -205,8 +202,8 @@ describe("convertAssistantMessagesToModelMessages", () => {
{
type: "tool-call",
toolCallId: "regular-tool",
toolName: "homarr_findTools",
input: { query: "boards" },
toolName: "homarr_enableToolGroups",
input: { groups: ["board"] },
},
],
},
@@ -228,8 +225,8 @@ describe("convertAssistantMessagesToModelMessages", () => {
{
type: "tool-result",
toolCallId: "regular-tool",
toolName: "homarr_findTools",
output: { type: "json", value: { matches: [] } },
toolName: "homarr_enableToolGroups",
output: { type: "json", value: { enabledGroups: ["board"] } },
},
],
},
@@ -1,58 +0,0 @@
import { describe, expect, test } from "vitest";
import { z } from "zod/v4";
import type { McpTool } from "@homarr/api/mcp";
import { filterAssistantMcpToolMatches, findAssistantMcpTools } from "./assistant-tool-discovery";
const createTool = (name: string, description: string, pathInRouter = name.split("_")): McpTool => ({
name,
description,
pathInRouter,
inputSchema: { type: "object", properties: {} },
inputValidator: z.object({}),
});
describe("Assistant tool discovery", () => {
const tools = [
createTool("customWidget_previewCreate", "Validate a complete Custom Widget and create its preview."),
createTool("customWidget_previewQuery", "Test a query from a Custom Widget preview."),
createTool("customWidget_createFromPreview", "Persist the exact tested Custom Widget preview."),
createTool("mediaRequests_search", "Search Seerr media requests."),
createTool("board_getAllBoards", "List Homarr boards."),
];
test("finds only the small relevant lifecycle subset", () => {
expect(findAssistantMcpTools(tools, "custom widget preview and persist", 3).map(({ name }) => name)).toEqual([
"customWidget_createFromPreview",
"customWidget_previewCreate",
"customWidget_previewQuery",
]);
});
test("uses service aliases without loading unrelated schemas", () => {
expect(findAssistantMcpTools(tools, "Seerr movie requests").map(({ name }) => name)).toEqual([
"mediaRequests_search",
]);
});
test("returns no tools when a query contains only ignored terms", () => {
expect(findAssistantMcpTools(tools, "a an and the to")).toEqual([]);
});
test("keeps heavyweight authoring resources hidden unless explicitly requested", () => {
const matches = [
createTool("customWidget_getComponentCatalog", "Get the complete component catalog."),
createTool("customWidget_schema", "Get the complete JSON Schema."),
createTool("customWidget_findComponents", "Find focused components."),
];
expect(filterAssistantMcpToolMatches(matches, "custom widget", true).map(({ name }) => name)).toEqual([
"customWidget_findComponents",
]);
expect(filterAssistantMcpToolMatches(matches, "full component catalog and live schema", true).map(({ name }) =>
name,
)).toEqual(["customWidget_getComponentCatalog", "customWidget_findComponents"]);
expect(filterAssistantMcpToolMatches(matches, "custom widget", false)).toEqual(matches);
});
});
@@ -1,110 +0,0 @@
import type { UIMessage } from "ai";
import type { McpTool } from "@homarr/api/mcp";
const ignoredSearchTerms = new Set([
"a",
"an",
"and",
"can",
"create",
"for",
"from",
"get",
"in",
"list",
"make",
"my",
"of",
"on",
"please",
"the",
"to",
"want",
"with",
]);
const searchTermAliases: Record<string, readonly string[]> = {
account: ["user"],
dashboard: ["board"],
k8s: ["kubernetes"],
movie: ["media", "radarr", "request"],
seerr: ["overseerr", "jellyseerr", "media", "request"],
show: ["media", "sonarr", "request"],
streaming: ["stream", "media", "server"],
};
const normalizeSearchText = (value: string) =>
value
.replaceAll(/([a-z\d])([A-Z])/gu, "$1 $2")
.normalize("NFKD")
.toLowerCase()
.replaceAll(/[^a-z\d]+/gu, " ")
.trim();
const getSearchTerms = (query: string) => {
const terms = normalizeSearchText(query)
.split(" ")
.filter((term) => term.length > 1 && !ignoredSearchTerms.has(term));
return [...new Set(terms.flatMap((term) => [term, ...(searchTermAliases[term] ?? [])]))];
};
const scoreTool = (tool: McpTool, query: string, terms: readonly string[]) => {
const normalizedName = normalizeSearchText(tool.name);
const normalizedPath = normalizeSearchText(tool.pathInRouter.join(" "));
const normalizedDescription = normalizeSearchText(tool.description);
const searchable = `${normalizedName} ${normalizedPath} ${normalizedDescription}`;
let score = 0;
if (normalizedName.includes(query)) score += 40;
if (normalizedDescription.includes(query)) score += 20;
for (const term of terms) {
if (normalizedName.split(" ").includes(term)) score += 12;
else if (normalizedName.includes(term)) score += 7;
if (normalizedPath.split(" ").includes(term)) score += 6;
if (normalizedDescription.split(" ").includes(term)) score += 4;
else if (searchable.includes(term)) score += 1;
}
return score;
};
export const findAssistantMcpTools = (tools: readonly McpTool[], query: string, limit = 8) => {
const normalizedQuery = normalizeSearchText(query);
const terms = getSearchTerms(query);
if (!normalizedQuery || terms.length === 0) return [];
return tools
.map((tool) => ({ tool, score: scoreTool(tool, normalizedQuery, terms) }))
.filter(({ score }) => score > 0)
.toSorted((left, right) => right.score - left.score || left.tool.name.localeCompare(right.tool.name))
.slice(0, limit)
.map(({ tool }) => tool);
};
export const filterAssistantMcpToolMatches = <TTool extends Pick<McpTool, "name">>(
tools: readonly TTool[],
query: string,
customWidgetAuthoringActive: boolean,
) => {
if (!customWidgetAuthoringActive) return [...tools];
const normalizedQuery = normalizeSearchText(query);
const requestsCompleteCatalog =
normalizedQuery.includes("all components") ||
["full component catalog", "complete component catalog"].some((phrase) => normalizedQuery.includes(phrase));
return tools.filter((tool) => {
if (tool.name === "customWidget_getComponentCatalog") return requestsCompleteCatalog;
if (tool.name === "customWidget_schema") return false;
return true;
});
};
export const getLatestAssistantUserText = (messages: UIMessage[]) => {
const latestUserMessage = messages.toReversed().find((message) => message.role === "user");
if (!latestUserMessage) return "";
return latestUserMessage.parts
.flatMap((part) => {
if (typeof part !== "object" || part === null || !("type" in part) || part.type !== "text") return [];
if (!("text" in part) || typeof part.text !== "string") return [];
return [part.text];
})
.join("\n");
};
@@ -0,0 +1,40 @@
import { describe, expect, test } from "vitest";
import { createAssistantMcpToolGroups } from "./assistant-tool-groups";
const createTool = (name: string, group: string) => ({
name,
description: `${name} description`,
pathInRouter: [group, name],
});
describe("Assistant MCP tool groups", () => {
test("derives deterministic groups from router paths", () => {
const groups = createAssistantMcpToolGroups([
createTool("board_search", "board"),
createTool("mediaRequests_search", "mediaRequests"),
createTool("board_getAll", "board"),
]);
expect(groups.ids).toEqual(["board", "mediaRequests"]);
expect(groups.resolve(["board"])[0]?.tools.map(({ name }) => name)).toEqual(["board_getAll", "board_search"]);
});
test("deduplicates requested groups while preserving request order", () => {
const groups = createAssistantMcpToolGroups([
createTool("board_getAll", "board"),
createTool("docker_getContainers", "docker"),
]);
expect(groups.resolve(["docker", "board", "docker"]).map(({ id }) => id)).toEqual(["docker", "board"]);
});
test("rejects tools without a router group and unknown group IDs", () => {
expect(() => createAssistantMcpToolGroups([{ ...createTool("info_get", "info"), pathInRouter: [] }])).toThrow(
"has no router group",
);
const groups = createAssistantMcpToolGroups([createTool("info_get", "info")]);
expect(() => groups.resolve(["missing"])).toThrow("Unknown Assistant MCP tool group 'missing'");
});
});
@@ -0,0 +1,38 @@
import type { McpTool } from "@homarr/api/mcp";
type AssistantMcpTool = Pick<McpTool, "name" | "pathInRouter">;
export interface AssistantMcpToolGroup<TTool extends AssistantMcpTool = AssistantMcpTool> {
id: string;
tools: readonly TTool[];
}
export const createAssistantMcpToolGroups = <TTool extends AssistantMcpTool>(tools: readonly TTool[]) => {
const toolsByGroup = new Map<string, TTool[]>();
for (const tool of tools) {
const groupId = tool.pathInRouter[0];
if (!groupId) throw new Error(`Assistant MCP tool '${tool.name}' has no router group`);
const groupedTools = toolsByGroup.get(groupId) ?? [];
groupedTools.push(tool);
toolsByGroup.set(groupId, groupedTools);
}
const groups = [...toolsByGroup.entries()]
.toSorted(([left], [right]) => left.localeCompare(right))
.map(([id, groupedTools]) => ({
id,
tools: groupedTools.toSorted((left, right) => left.name.localeCompare(right.name)),
}));
const groupsById = new Map(groups.map((group) => [group.id, group]));
return {
ids: groups.map(({ id }) => id),
resolve(groupIds: readonly string[]): AssistantMcpToolGroup<TTool>[] {
return [...new Set(groupIds)].map((groupId) => {
const group = groupsById.get(groupId);
if (!group) throw new Error(`Unknown Assistant MCP tool group '${groupId}'`);
return group;
});
},
};
};
@@ -30,7 +30,7 @@ describe("customWidgetAssistantInstructions", () => {
test("loads authoring resources lazily and verifies every final preview", () => {
expect(customWidgetAssistantInstructions.length).toBeLessThan(4_100);
expect(customWidgetAssistantInstructions).toContain("customWidget_getSkill");
expect(customWidgetAssistantInstructions).toContain("homarr_findTools");
expect(customWidgetAssistantInstructions).toContain("staged by the authoring lifecycle");
expect(customWidgetAssistantInstructions).toContain("task-needed");
expect(customWidgetAssistantInstructions).toContain("Reuse loaded context");
expect(customWidgetAssistantInstructions).toContain("customWidget_findComponents");
@@ -3,13 +3,13 @@ import type { UIMessage } from "ai";
import {
CUSTOM_WIDGET_ASSISTANT_POLICY,
CUSTOM_WIDGET_LAZY_TOOL_DISCOVERY_INSTRUCTION,
CUSTOM_WIDGET_TOOL_STAGING_INSTRUCTION,
} from "@homarr/custom-widgets/authoring-prompt";
const mutationApprovalInstruction =
"Uses Homarr's native approval UI; call when inputs are ready without separate prose confirmation.";
export const customWidgetAssistantInstructions = `\n\n${CUSTOM_WIDGET_LAZY_TOOL_DISCOVERY_INSTRUCTION}\n\n${CUSTOM_WIDGET_ASSISTANT_POLICY}`;
export const customWidgetAssistantInstructions = `\n\n${CUSTOM_WIDGET_TOOL_STAGING_INSTRUCTION}\n\n${CUSTOM_WIDGET_ASSISTANT_POLICY}`;
export const withAssistantToolPolicy = (description: string | undefined, requiresApproval: boolean) => {
if (!requiresApproval) return description;
@@ -26,6 +26,13 @@ describe("Custom Widget authoring context", () => {
expect(needsCustomWidgetAuthoringContext([userMessage(text)])).toBe(true);
});
test.each(["List my custom widgets", "Delete a custom widget", "Explain custom widgets"])(
"leaves management intent to the general Custom Widget MCP group: %s",
(text) => {
expect(needsCustomWidgetAuthoringContext([userMessage(text)])).toBe(false);
},
);
test("continues after a Custom Widget tool call", () => {
const messages: UIMessage[] = [
userMessage("Continue"),
@@ -68,6 +75,8 @@ describe("Custom Widget authoring context", () => {
"customWidget_schema",
"customWidget_getComponentCatalog",
"customWidget_getReference",
"customWidget_findComponents",
"customWidget_getComponents",
"customWidget_getComponent",
"customWidget_getSharedProps",
"customWidget_getExample",
@@ -78,7 +87,6 @@ describe("Custom Widget authoring context", () => {
expect(getActiveCustomWidgetToolNames(tools, [userMessage("Create two custom widgets")], true)).toEqual([
"customWidget_getSkill",
"customWidget_validateTemplate",
]);
expect(getActiveCustomWidgetToolNames(tools, [userMessage("Create a custom widget")], false)).toEqual([]);
});
@@ -100,11 +108,7 @@ describe("Custom Widget authoring context", () => {
getCustomWidgetPhaseToolNames(tools, [
{ toolResults: [{ toolName: "customWidget_validateTemplate", output: { valid: true } }] },
]),
).toEqual([
"customWidget_validateTemplate",
"customWidget_previewCreate",
"customWidget_previewReviseTemplate",
]);
).toEqual(["customWidget_validateTemplate", "customWidget_previewCreate", "customWidget_previewReviseTemplate"]);
expect(
getCustomWidgetPhaseToolNames(tools, [
{ toolResults: [{ toolName: "customWidget_previewCreate", output: { success: true } }] },
@@ -135,6 +139,30 @@ describe("Custom Widget authoring context", () => {
).toBeNull();
});
test("exposes focused context tools only after the skill entrypoint is loaded", () => {
const tools = [
"customWidget_getSkill",
"customWidget_getReference",
"customWidget_findComponents",
"customWidget_getComponents",
"customWidget_getExample",
"customWidget_validateTemplate",
"customWidget_previewCreate",
];
expect(
getCustomWidgetPhaseToolNames(tools, [
{ toolResults: [{ toolName: "customWidget_getSkill", output: { content: "skill" } }] },
]),
).toEqual([
"customWidget_getReference",
"customWidget_findComponents",
"customWidget_getComponents",
"customWidget_getExample",
"customWidget_validateTemplate",
]);
});
test("keeps one concrete component repair path open for validation warnings", () => {
const tools = [
"customWidget_findComponents",
@@ -179,16 +207,10 @@ describe("Custom Widget authoring context", () => {
],
},
{
toolResults: [
{ toolName: "customWidget_getComponent", output: { name: "RefreshButton", props: [] } },
],
toolResults: [{ toolName: "customWidget_getComponent", output: { name: "RefreshButton", props: [] } }],
},
]),
).toEqual([
"customWidget_validateTemplate",
"customWidget_previewCreate",
"customWidget_previewReviseTemplate",
]);
).toEqual(["customWidget_validateTemplate", "customWidget_previewCreate", "customWidget_previewReviseTemplate"]);
});
test("keeps evidence tools until every preview request succeeds, then allows correction or persistence", () => {
@@ -257,11 +279,7 @@ describe("Custom Widget authoring context", () => {
...completeEvidence,
{ toolResults: [{ toolName: "customWidget_validateTemplate", output: { valid: true } }] },
]),
).toEqual([
"customWidget_validateTemplate",
"customWidget_previewCreate",
"customWidget_previewReviseTemplate",
]);
).toEqual(["customWidget_validateTemplate", "customWidget_previewCreate", "customWidget_previewReviseTemplate"]);
const revisedPreview = [
...completeEvidence,
@@ -310,9 +328,7 @@ describe("Custom Widget authoring context", () => {
],
},
];
expect(getCustomWidgetPhaseToolNames(tools, completeRevisedEvidence)).toEqual([
"customWidget_createFromPreview",
]);
expect(getCustomWidgetPhaseToolNames(tools, completeRevisedEvidence)).toEqual(["customWidget_createFromPreview"]);
});
test("closes focused discovery after four searches until validation", () => {
@@ -5,9 +5,9 @@ import { getCustomWidgetPhaseToolNames } from "@homarr/custom-widgets/core";
export { getCustomWidgetPhaseToolNames };
const customWidgetIntentPattern =
/(?:\bcustom[\s-]+widgets?\b|\bcustom\s+jsx\b|\bhomarr-custom-widget-v\d+\b|\bcustomWidget_[A-Za-z\d_]+\b|\b(?:build|create|design|make)\b[^\n]{0,80}\bwidgets?\s+(?:for|using|with)\b|\b(?:i|we)\s+(?:need|want)\b[^\n]{0,60}\bwidgets?\s+(?:for|using|with)\b)/iu;
/(?:\bcustom\s+jsx\b|\bhomarr-custom-widget-v\d+\b|\b(?:build|create|design|edit|fix|make|repair|update|validate)\b[^\n]{0,80}\bcustom[\s-]+widgets?\b|\b(?:build|create|design|make)\b[^\n]{0,80}\bwidgets?\s+(?:for|using|with)\b|\b(?:i|we)\s+(?:need|want)\b[^\n]{0,60}\bwidgets?\s+(?:for|using|with)\b)/iu;
const customWidgetBootstrapToolNames = new Set(["customWidget_getSkill", "customWidget_validateTemplate"]);
const customWidgetBootstrapToolNames = new Set(["customWidget_getSkill"]);
const maxFocusedComponentSearchesPerPhase = 4;
const customWidgetContextToolBudgets: Readonly<Record<string, number>> = {
customWidget_findComponents: maxFocusedComponentSearchesPerPhase,
+34 -53
View File
@@ -49,10 +49,7 @@ import {
sanitizeAttachmentFilename,
} from "./assistant-chat-input";
import { getAssistantModelLookupStatus } from "./assistant-model-lookup";
import {
compactAssistantStepMessages,
convertAssistantMessagesToModelMessages,
} from "./assistant-message-conversion";
import { compactAssistantStepMessages, convertAssistantMessagesToModelMessages } from "./assistant-message-conversion";
import {
getOpenRouterWebSearchRequests,
getOpenRouterWebSearchSources,
@@ -77,11 +74,7 @@ import {
getCustomWidgetPhaseToolNames,
needsCustomWidgetAuthoringContext,
} from "./custom-widget-authoring-context";
import {
filterAssistantMcpToolMatches,
findAssistantMcpTools,
getLatestAssistantUserText,
} from "./assistant-tool-discovery";
import { createAssistantMcpToolGroups } from "./assistant-tool-groups";
import {
customWidgetAssistantInstructions,
getForcedAssistantToolName,
@@ -157,20 +150,12 @@ const requestSchema = z.object({
.optional(),
});
const assistantToolDiscoverySchema = z.object({
query: z
.string()
.trim()
.min(2)
.max(240)
.describe("A concise Homarr capability or action, such as 'search media requests' or 'list Kubernetes pods'."),
});
const assistantToolDiscoveryName = "homarr_findTools";
const assistantToolGroupActivationName = "homarr_enableToolGroups";
const maxAssistantToolGroupsPerActivation = 4;
const assistantInstructions = `You are Homarr Assistant, embedded in the user's self-hosted Homarr dashboard.
Use Homarr tools for live instance data or actions; never invent resources, IDs, state, or results. Only a small relevant tool set is initially visible. Call ${assistantToolDiscoveryName} with a concise capability when a needed Homarr tool is not visible, then use the activated tool. Follow each tool's description and use integration IDs returned by discovery tools.
Use Homarr tools for live instance data or actions; never invent resources, IDs, state, or results. Homarr tools are grouped by their MCP router namespace. Call ${assistantToolGroupActivationName} with the task-needed group when its typed tools are not visible, then use the activated tool. Follow each tool's description and use integration IDs returned by Homarr tools.
Homarr permissions are authoritative. Explain denied access without suggesting a bypass. Read before changing when current state matters. Mutations use Homarr's native approval UI: when inputs are sufficient, call the mutation immediately and never ask for duplicate prose confirmation or retry a denial. Use ask_user only when a missing choice blocks the next action; do not end with a prose question expecting a reply.
@@ -547,10 +532,7 @@ export async function POST(request: Request) {
input !== null &&
!Array.isArray(input)
) {
executionInput = normalizeCustomWidgetLifecycleToolInput(
mcpTool.name,
input as Record<string, unknown>,
);
executionInput = normalizeCustomWidgetLifecycleToolInput(mcpTool.name, input as Record<string, unknown>);
}
const contextRequestKey = getCustomWidgetContextRequestKey(mcpTool.name, executionInput);
if (contextRequestKey !== null && loadedCustomWidgetContextRequests.has(contextRequestKey)) {
@@ -621,45 +603,41 @@ export async function POST(request: Request) {
];
}),
) satisfies ToolSet;
const discoveredToolNames = new Set<string>();
const latestUserText = getLatestAssistantUserText(incomingMessages);
const automaticToolCandidates = customWidgetAuthoringActive
const groupedToolCandidates = customWidgetAuthoringActive
? mcpTools.filter((mcpTool) => !mcpTool.name.startsWith("customWidget_"))
: mcpTools;
const automaticallySelectedTools = findAssistantMcpTools(automaticToolCandidates, latestUserText, 6).map(
({ name }) => name,
);
const toolDiscovery = tool({
const assistantToolGroups = createAssistantMcpToolGroups(groupedToolCandidates);
const assistantToolGroupIds = assistantToolGroups.ids as [string, ...string[]];
const assistantToolGroupActivationSchema = z.object({
groups: z
.array(z.enum(assistantToolGroupIds))
.min(1)
.max(maxAssistantToolGroupsPerActivation)
.describe("MCP router groups whose typed Homarr tools are needed for the current task."),
});
const enabledToolGroupIds = new Set<string>();
const toolGroupActivation = tool({
description:
"Find Homarr tools by capability without loading the complete tool catalog. Matching tools become callable on the next step. Use a focused query and call again with a different capability when needed.",
inputSchema: jsonSchema(z.toJSONSchema(assistantToolDiscoverySchema) as Parameters<typeof jsonSchema>[0]),
"Enable task-needed typed Homarr tools by MCP router group. Enabled groups remain available on later steps.",
inputSchema: jsonSchema(z.toJSONSchema(assistantToolGroupActivationSchema) as Parameters<typeof jsonSchema>[0]),
execute: (value) => {
if (customWidgetAuthoringActive && !customWidgetToolStepGate.claim(assistantToolDiscoveryName)) {
if (customWidgetAuthoringActive && !customWidgetToolStepGate.claim(assistantToolGroupActivationName)) {
return {
error:
"A Custom Widget tool must run in its own model step. Use that result before calling another tool.",
error: "A Custom Widget tool must run in its own model step. Use that result before calling another tool.",
};
}
const input = assistantToolDiscoverySchema.parse(value);
const matches = filterAssistantMcpToolMatches(
findAssistantMcpTools(mcpTools, input.query),
input.query,
customWidgetAuthoringActive,
);
for (const match of matches) discoveredToolNames.add(match.name);
const input = assistantToolGroupActivationSchema.parse(value);
const groups = assistantToolGroups.resolve(input.groups);
for (const group of groups) enabledToolGroupIds.add(group.id);
return {
query: input.query,
matches: matches.map((match) => ({ name: match.name, description: match.description })),
nextStep:
matches.length > 0
? "Use one or more matching tools now. Search again only for a different capability."
: "No matching Homarr tool was found. Refine the capability or explain that it is unavailable.",
enabledGroups: groups.map(({ id }) => id),
nextStep: "Use one or more enabled typed tools now. Enable another group only when the task needs it.",
};
},
});
const availableTools: ToolSet = {
...homarrTools,
[assistantToolDiscoveryName]: toolDiscovery,
[assistantToolGroupActivationName]: toolGroupActivation,
...frontendTools,
};
const frontendToolNames = Object.keys(frontendTools);
@@ -669,14 +647,17 @@ export async function POST(request: Request) {
canAuthorCustomWidgets,
);
const getActiveToolNames = (steps: Parameters<typeof getCustomWidgetPhaseToolNames>[1] = []) => {
const enabledToolNames = assistantToolGroups
.resolve([...enabledToolGroupIds])
.flatMap((group) => group.tools.map(({ name }) => name));
const phaseToolNames = customWidgetAuthoringActive
? getCustomWidgetPhaseToolNames(Object.keys(homarrTools), steps)
: null;
if (phaseToolNames) return [...frontendToolNames, ...phaseToolNames];
if (phaseToolNames) return [...frontendToolNames, ...enabledToolNames, ...phaseToolNames];
return [
assistantToolDiscoveryName,
assistantToolGroupActivationName,
...frontendToolNames,
...new Set([...automaticallySelectedTools, ...discoveredToolNames, ...activeCustomWidgetToolNames]),
...new Set([...enabledToolNames, ...activeCustomWidgetToolNames]),
];
};
const forcedToolName = getForcedAssistantToolName(incomingMessages);