feat(recipe-studio): add support for managing tools by provider in tool profiles
This commit is contained in:
parent
992e07495f
commit
d31420acc3
7 changed files with 119 additions and 10 deletions
|
|
@ -14,7 +14,7 @@ import {
|
|||
PlusSignIcon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { type ReactElement, useEffect, useMemo, useState } from "react";
|
||||
import { type ReactElement, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { listMcpTools } from "../../api";
|
||||
import { ChipInput } from "../../components/chip-input";
|
||||
import type { LlmMcpProviderConfig, McpEnvVar, ToolProfileConfig } from "../../types";
|
||||
|
|
@ -351,11 +351,12 @@ export function ToolProfileDialog({
|
|||
const providers = config.mcp_providers;
|
||||
const [loadingTools, setLoadingTools] = useState(false);
|
||||
const [toolsByProvider, setToolsByProvider] = useState<Record<string, string[]>>(
|
||||
{},
|
||||
config.fetched_tools_by_provider ?? {},
|
||||
);
|
||||
const [providerErrors, setProviderErrors] = useState<Record<string, string>>({});
|
||||
const [duplicateTools, setDuplicateTools] = useState<Record<string, string[]>>({});
|
||||
const [openProviders, setOpenProviders] = useState<Record<string, boolean>>({});
|
||||
const previousProviderSignatureRef = useRef<string | null>(null);
|
||||
|
||||
const providerSignature = useMemo(
|
||||
() =>
|
||||
|
|
@ -378,10 +379,30 @@ export function ToolProfileDialog({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
const previousSignature = previousProviderSignatureRef.current;
|
||||
previousProviderSignatureRef.current = providerSignature;
|
||||
if (previousSignature === null) {
|
||||
setToolsByProvider(config.fetched_tools_by_provider ?? {});
|
||||
return;
|
||||
}
|
||||
if (previousSignature === providerSignature) {
|
||||
return;
|
||||
}
|
||||
setToolsByProvider({});
|
||||
setProviderErrors({});
|
||||
setDuplicateTools({});
|
||||
}, [providerSignature]);
|
||||
if (Object.keys(config.fetched_tools_by_provider ?? {}).length > 0) {
|
||||
onUpdate({
|
||||
// biome-ignore lint/style/useNamingConvention: ui schema
|
||||
fetched_tools_by_provider: {},
|
||||
});
|
||||
}
|
||||
}, [config.fetched_tools_by_provider, onUpdate, providerSignature]);
|
||||
|
||||
useEffect(() => {
|
||||
const tools = config.fetched_tools_by_provider ?? {};
|
||||
setToolsByProvider(tools);
|
||||
}, [config.fetched_tools_by_provider]);
|
||||
|
||||
useEffect(() => {
|
||||
setOpenProviders((current) => {
|
||||
|
|
@ -523,13 +544,16 @@ export function ToolProfileDialog({
|
|||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
timeout_sec: timeoutSec,
|
||||
});
|
||||
setToolsByProvider(
|
||||
Object.fromEntries(
|
||||
response.providers
|
||||
.filter((provider) => provider.name.trim())
|
||||
.map((provider) => [provider.name.trim(), provider.tools]),
|
||||
),
|
||||
const nextToolsByProvider = Object.fromEntries(
|
||||
response.providers
|
||||
.filter((provider) => provider.name.trim())
|
||||
.map((provider) => [provider.name.trim(), provider.tools]),
|
||||
);
|
||||
setToolsByProvider(nextToolsByProvider);
|
||||
onUpdate({
|
||||
// biome-ignore lint/style/useNamingConvention: ui schema
|
||||
fetched_tools_by_provider: nextToolsByProvider,
|
||||
});
|
||||
setProviderErrors(
|
||||
Object.fromEntries(
|
||||
response.providers
|
||||
|
|
|
|||
|
|
@ -61,6 +61,16 @@ function stripApiKeys(value: unknown): unknown {
|
|||
}
|
||||
output[key] = stripApiKeys(entry);
|
||||
}
|
||||
if (
|
||||
output.provider_type === "stdio" &&
|
||||
output.env &&
|
||||
typeof output.env === "object" &&
|
||||
!Array.isArray(output.env)
|
||||
) {
|
||||
output.env = Object.fromEntries(
|
||||
Object.keys(output.env as Record<string, unknown>).map((envKey) => [envKey, ""]),
|
||||
);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -181,6 +181,8 @@ export type ToolProfileConfig = {
|
|||
name: string;
|
||||
// biome-ignore lint/style/useNamingConvention: ui schema
|
||||
mcp_providers: LlmMcpProviderConfig[];
|
||||
// biome-ignore lint/style/useNamingConvention: ui schema
|
||||
fetched_tools_by_provider?: Record<string, string[]>;
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
allow_tools?: string[];
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
|
|
|
|||
|
|
@ -275,6 +275,8 @@ export function makeToolProfileConfig(
|
|||
name: nextName(existing, "tools"),
|
||||
// biome-ignore lint/style/useNamingConvention: ui schema
|
||||
mcp_providers: [],
|
||||
// biome-ignore lint/style/useNamingConvention: ui schema
|
||||
fetched_tools_by_provider: {},
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
allow_tools: [],
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
|
|
|
|||
|
|
@ -252,6 +252,46 @@ function parseUiMarkdownNoteNodes(input: unknown): UiMarkdownNoteNode[] {
|
|||
return noteNodes;
|
||||
}
|
||||
|
||||
function parseUiToolProfileNodes(input: unknown): Map<string, Record<string, string[]>> {
|
||||
const toolProfiles = new Map<string, Record<string, string[]>>();
|
||||
if (!Array.isArray(input)) {
|
||||
return toolProfiles;
|
||||
}
|
||||
for (const node of input) {
|
||||
if (!isRecord(node)) {
|
||||
continue;
|
||||
}
|
||||
const nodeType = readString(node.node_type) ?? readString(node.type);
|
||||
if (nodeType !== "tool_config") {
|
||||
continue;
|
||||
}
|
||||
const name = readString(node.name) ?? readString(node.id);
|
||||
if (!name?.trim()) {
|
||||
continue;
|
||||
}
|
||||
const rawToolsByProvider = isRecord(node.tools_by_provider)
|
||||
? node.tools_by_provider
|
||||
: null;
|
||||
if (!rawToolsByProvider) {
|
||||
continue;
|
||||
}
|
||||
const toolsByProvider = Object.fromEntries(
|
||||
Object.entries(rawToolsByProvider).flatMap(([providerName, tools]) => {
|
||||
const trimmedName = providerName.trim();
|
||||
if (!trimmedName || !Array.isArray(tools)) {
|
||||
return [];
|
||||
}
|
||||
const values = Array.from(
|
||||
new Set(tools.map((value) => String(value).trim()).filter(Boolean)),
|
||||
);
|
||||
return values.length > 0 ? [[trimmedName, values]] : [];
|
||||
}),
|
||||
);
|
||||
toolProfiles.set(name.trim(), toolsByProvider);
|
||||
}
|
||||
return toolProfiles;
|
||||
}
|
||||
|
||||
function parseAdvancedOpenByNode(input: unknown): Record<string, boolean> {
|
||||
if (!isRecord(input)) {
|
||||
return {};
|
||||
|
|
@ -292,6 +332,7 @@ function buildToolProfileConfig(
|
|||
toolConfig: LlmToolConfig,
|
||||
toolConfigsByAlias: Map<string, LlmToolConfig>,
|
||||
mcpProvidersByName: Map<string, LlmMcpProviderConfig>,
|
||||
fetchedToolsByProfileName: Map<string, Record<string, string[]>>,
|
||||
id: string,
|
||||
): ToolProfileConfig {
|
||||
const canonical = toolConfigsByAlias.get(toolConfig.tool_alias) ?? toolConfig;
|
||||
|
|
@ -303,6 +344,8 @@ function buildToolProfileConfig(
|
|||
mcp_providers: canonical.providers
|
||||
.map((providerName) => mcpProvidersByName.get(providerName))
|
||||
.flatMap((provider) => (provider ? [cloneMcpProvider(provider)] : [])),
|
||||
// biome-ignore lint/style/useNamingConvention: ui schema
|
||||
fetched_tools_by_provider: fetchedToolsByProfileName.get(canonical.tool_alias) ?? {},
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
allow_tools: [...(canonical.allow_tools ?? [])],
|
||||
// biome-ignore lint/style/useNamingConvention: api schema
|
||||
|
|
@ -370,6 +413,7 @@ export function importRecipePayload(input: string): ImportResult {
|
|||
);
|
||||
const uiAdvancedOpenByNode = parseAdvancedOpenByNode(ui?.advanced_open_by_node);
|
||||
const uiMarkdownNotes = parseUiMarkdownNoteNodes(ui?.nodes);
|
||||
const uiToolProfilesByName = parseUiToolProfileNodes(ui?.nodes);
|
||||
|
||||
for (const note of uiMarkdownNotes) {
|
||||
const id = `n${nextId}`;
|
||||
|
|
@ -470,6 +514,7 @@ export function importRecipePayload(input: string): ImportResult {
|
|||
toolConfig,
|
||||
toolConfigsByAlias,
|
||||
mcpProvidersByName,
|
||||
uiToolProfilesByName,
|
||||
id,
|
||||
);
|
||||
if (nameToId.has(config.name)) {
|
||||
|
|
|
|||
|
|
@ -267,6 +267,31 @@ export function buildRecipePayload(
|
|||
},
|
||||
];
|
||||
}
|
||||
if (config.kind === "tool_config") {
|
||||
const toolsByProvider = Object.fromEntries(
|
||||
Object.entries(config.fetched_tools_by_provider ?? {}).flatMap(
|
||||
([providerName, tools]) => {
|
||||
const name = providerName.trim();
|
||||
const values = Array.from(
|
||||
new Set(tools.map((tool) => tool.trim()).filter(Boolean)),
|
||||
);
|
||||
return name && values.length > 0 ? [[name, values]] : [];
|
||||
},
|
||||
),
|
||||
);
|
||||
return [
|
||||
{
|
||||
id: config.name,
|
||||
x: node.position.x,
|
||||
y: node.position.y,
|
||||
...(width !== null ? { width } : {}),
|
||||
node_type: "tool_config" as const,
|
||||
...(Object.keys(toolsByProvider).length > 0 && {
|
||||
tools_by_provider: toolsByProvider,
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: config.name,
|
||||
|
|
|
|||
|
|
@ -37,11 +37,12 @@ export type RecipePayload = {
|
|||
x: number;
|
||||
y: number;
|
||||
width?: number;
|
||||
node_type?: "markdown_note";
|
||||
node_type?: "markdown_note" | "tool_config";
|
||||
name?: string;
|
||||
markdown?: string;
|
||||
note_color?: string;
|
||||
note_opacity?: string;
|
||||
tools_by_provider?: Record<string, string[]>;
|
||||
}>;
|
||||
edges: {
|
||||
from: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue