fix assistant widget configuration
This commit is contained in:
1 parent
8d06201b41
commit
1f60b4b4be
16 files changed
+696
-11
No files matched your search
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { withOpenRouterWebSearch } from "./assistant-openrouter";
|
||||
|
||||
describe("withOpenRouterWebSearch", () => {
|
||||
test("adds the current OpenRouter server tool alongside Homarr function tools", () => {
|
||||
expect(
|
||||
withOpenRouterWebSearch({
|
||||
model: "deepseek/deepseek-v4-flash-latest",
|
||||
tools: [{ type: "function", function: { name: "board_addItem", parameters: {} } }],
|
||||
}),
|
||||
).toMatchObject({
|
||||
tools: [
|
||||
{ type: "function", function: { name: "board_addItem" } },
|
||||
{ type: "openrouter:web_search", parameters: { max_results: 5 } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("does not register the web search tool twice", () => {
|
||||
const body = { tools: [{ type: "openrouter:web_search", parameters: { max_results: 3 } }] };
|
||||
|
||||
expect(withOpenRouterWebSearch(body)).toBe(body);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
const openRouterWebSearchTool = {
|
||||
type: "openrouter:web_search",
|
||||
parameters: { max_results: 5 },
|
||||
} as const;
|
||||
|
||||
export const withOpenRouterWebSearch = (body: Record<string, unknown>) => {
|
||||
const tools = Array.isArray(body.tools) ? body.tools : [];
|
||||
if (
|
||||
tools.some(
|
||||
(candidate) =>
|
||||
typeof candidate === "object" &&
|
||||
candidate !== null &&
|
||||
"type" in candidate &&
|
||||
candidate.type === openRouterWebSearchTool.type,
|
||||
)
|
||||
) {
|
||||
return body;
|
||||
}
|
||||
|
||||
return { ...body, tools: [...tools, openRouterWebSearchTool] };
|
||||
};
|
||||
@@ -87,6 +87,46 @@ describe("getForcedAssistantToolName", () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test("continues directly from reviewed widget settings to board placement", () => {
|
||||
expect(
|
||||
getForcedAssistantToolName([
|
||||
assistantMessage({
|
||||
type: "dynamic-tool",
|
||||
toolName: "configure_widget",
|
||||
toolCallId: "widget-1",
|
||||
input: { boardId: "board-1", boardName: "Home", kind: "notebook" },
|
||||
state: "output-available",
|
||||
output: {
|
||||
boardId: "board-1",
|
||||
kind: "notebook",
|
||||
options: { content: "<h2>Plex</h2>" },
|
||||
integrationIds: [],
|
||||
},
|
||||
}),
|
||||
]),
|
||||
).toBe("board_addItem");
|
||||
});
|
||||
|
||||
test("does not add a widget after native configuration was cancelled", () => {
|
||||
expect(
|
||||
getForcedAssistantToolName([
|
||||
assistantMessage({
|
||||
type: "dynamic-tool",
|
||||
toolName: "configure_widget",
|
||||
toolCallId: "widget-1",
|
||||
input: { boardId: "board-1", boardName: "Home", kind: "mediaServer" },
|
||||
state: "output-available",
|
||||
output: {
|
||||
boardId: "board-1",
|
||||
kind: "mediaServer",
|
||||
cancelled: true,
|
||||
reason: "no-compatible-integration",
|
||||
},
|
||||
}),
|
||||
]),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test("does not repeat app creation after the mutation tool has been called", () => {
|
||||
expect(
|
||||
getForcedAssistantToolName([
|
||||
|
||||
@@ -19,10 +19,11 @@ export const getForcedAssistantToolName = (messages: UIMessage[]) => {
|
||||
const nextToolByHumanTool = {
|
||||
configure_app: "app_create",
|
||||
configure_board_settings: "board_savePartialBoardSettings",
|
||||
configure_widget: "board_addItem",
|
||||
} as const;
|
||||
const toolName = getToolName(latestToolPart);
|
||||
if (
|
||||
toolName === "configure_board_settings" &&
|
||||
(toolName === "configure_board_settings" || toolName === "configure_widget") &&
|
||||
typeof latestToolPart.output === "object" &&
|
||||
latestToolPart.output !== null &&
|
||||
"cancelled" in latestToolPart.output &&
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
import { extractMcpTools } from "../../mcp/_extract-tools";
|
||||
import { getRequestedMentionIds, sanitizeAttachmentFilename } from "./assistant-chat-input";
|
||||
import { getAssistantModelLookupStatus } from "./assistant-model-lookup";
|
||||
import { withOpenRouterWebSearch } from "./assistant-openrouter";
|
||||
import { assistantExecutionPolicy } from "./assistant-execution-policy";
|
||||
import { getAssistantStreamErrorMessage } from "./assistant-stream-error";
|
||||
import { getForcedAssistantToolName, withAssistantToolPolicy } from "./assistant-tool-policy";
|
||||
@@ -99,11 +100,13 @@ Action rules:
|
||||
- Mutating Homarr tools already pause for native user approval. Calling a mutation only proposes the action; it cannot execute until the user selects Approve and run. Once the requested change is sufficiently specified, call the mutation immediately so the approval UI appears.
|
||||
- A prose response such as "Please confirm if you would like me to proceed", "Would you like me to proceed?", or a parameter summary that asks for confirmation is incorrect. Never ask for a second textual confirmation before an approval-gated tool call.
|
||||
- Do not retry a denied action.
|
||||
- Before creating an app, call configure_app with the app name and every useful default you can infer, including its description, href, pingUrl, and an icon URL returned by icon_findIcons. The user reviews Homarr's native app form. Its icon picker searches Homarr's local icon repository. Use the returned values for app_create. If the user asked to place it on a board, use the appId returned by app_create in a board_addItem call with kind "app" and options { appId } before reporting success.
|
||||
- Before creating an app, call configure_app with the app name and every useful default you can infer, including its description, href, pingUrl, and an icon URL returned by icon_findIcons. The user reviews Homarr's native app form. Its icon picker searches Homarr's local icon repository. Use the returned values for app_create. If the user asked to place it on a board, call configure_widget afterward with kind "app" and options { appId: the returned appId }.
|
||||
- Before changing a board's custom CSS or visual and behavior settings, call board_getBoardSettings to read the current values, then call configure_board_settings with only the requested proposed changes. The user can edit the complete CSS and settings in Homarr's native form. Use its returned flat object for board_savePartialBoardSettings, which then shows the mutation approval. Never overwrite existing CSS rules unless the user explicitly requests replacement.
|
||||
- To create a formatted note on a dashboard, call board_addItem with kind "notebook". Put valid Tiptap-compatible HTML in options.content and set options.showToolbar and options.allowReadOnlyCheck when useful. Use semantic paragraphs, headings, lists, task lists, blockquotes, tables, links, emphasis, and images as appropriate. Never include scripts, styles, iframes, event handlers, or unsafe URL protocols.
|
||||
- Before adding any widget to a board, call configure_widget with the target board, widget kind, proposed options, and any integration IDs selected from integration_all. Homarr's native widget editor applies defaults, validates the options, and restricts the picker to compatible integrations the user can access. Use the returned boardId, kind, options, and integrationIds exactly as the input to board_addItem. If configure_widget returns cancelled, do not add the widget.
|
||||
- To create a formatted note on a dashboard, call configure_widget with kind "notebook". Put valid Tiptap-compatible HTML in options.content and set options.showToolbar and options.allowReadOnlyCheck when useful. Use semantic paragraphs, headings, lists, task lists, blockquotes, tables, links, emphasis, and images as appropriate. Never include scripts, styles, iframes, event handlers, or unsafe URL protocols.
|
||||
- When choosing an app icon without configure_app, call the Homarr icon findIcons tool first and use one of its returned local icon URLs. Never invent a third-party icon CDN URL.
|
||||
- When a Homarr tool returns a usable image or icon URL and a visual summary helps, embed that exact URL with Markdown image syntax. App summary tables must use an Icon column such as . Never replace an available returned icon with emoji, and never invent or transform image URLs.
|
||||
- When the configured provider is OpenRouter and the user asks for live web research or current external information, use OpenRouter's web search server tool. Ground the answer in its results and preserve useful source links in the response or generated note. Do not claim web search is unavailable when that tool is present. For other providers, only claim live research when an actual search tool is available.
|
||||
- Complete batch requests in the same run. Use parallel independent read-only calls when supported, reuse results, continue calling tools until every requested item has been attempted, and summarize only after the batch is complete. Do not stop after a partial batch or ask the user to type "Continue".
|
||||
- Browser tools can navigate within Homarr, open existing Homarr UI, or refresh the active view after a completed change. Never navigate to an arbitrary external URL and never refresh before a mutation finishes.
|
||||
- Keep responses concise and lead with the result. Summarize tool output instead of dumping JSON.
|
||||
@@ -499,6 +502,7 @@ export async function POST(request: Request) {
|
||||
apiKey: providerApiKey,
|
||||
headers: providerHeaders,
|
||||
includeUsage: true,
|
||||
transformRequestBody: configuration.provider === "openrouter" ? withOpenRouterWebSearch : undefined,
|
||||
metadataExtractor: createProviderTelemetryExtractor(),
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user