mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-15 20:10:42 +00:00
feat: integrate tools catalog functionality into agent management
- Added support for loading and displaying a tools catalog in the agent management interface. - Enhanced the AppViewState to include loading, error, and result states for the tools catalog. - Implemented loadToolsCatalog function to fetch tools based on the selected agent. - Updated UI components to reflect tools catalog loading states and errors. - Refactored agent tools rendering logic to utilize the new tools catalog data structure.
This commit is contained in:
@@ -16,7 +16,7 @@ import type { AppViewState } from "./app-view-state.ts";
|
||||
import { loadAgentFileContent, loadAgentFiles, saveAgentFile } from "./controllers/agent-files.ts";
|
||||
import { loadAgentIdentities, loadAgentIdentity } from "./controllers/agent-identity.ts";
|
||||
import { loadAgentSkills } from "./controllers/agent-skills.ts";
|
||||
import { loadAgents } from "./controllers/agents.ts";
|
||||
import { loadAgents, loadToolsCatalog } from "./controllers/agents.ts";
|
||||
import { loadChannels } from "./controllers/channels.ts";
|
||||
import { loadChatHistory } from "./controllers/chat.ts";
|
||||
import {
|
||||
@@ -860,12 +860,25 @@ export function renderApp(state: AppViewState) {
|
||||
agentId: state.agentSkillsAgentId,
|
||||
filter: state.skillsFilter,
|
||||
},
|
||||
toolsCatalog: {
|
||||
loading: state.toolsCatalogLoading,
|
||||
error: state.toolsCatalogError,
|
||||
result: state.toolsCatalogResult,
|
||||
},
|
||||
onRefresh: async () => {
|
||||
await loadAgents(state);
|
||||
const agentIds = state.agentsList?.agents?.map((entry) => entry.id) ?? [];
|
||||
if (agentIds.length > 0) {
|
||||
void loadAgentIdentities(state, agentIds);
|
||||
}
|
||||
const refreshedAgentId =
|
||||
state.agentsSelectedId ??
|
||||
state.agentsList?.defaultId ??
|
||||
state.agentsList?.agents?.[0]?.id ??
|
||||
null;
|
||||
if (state.agentsPanel === "tools" && refreshedAgentId) {
|
||||
void loadToolsCatalog(state, refreshedAgentId);
|
||||
}
|
||||
},
|
||||
onSelectAgent: (agentId) => {
|
||||
if (state.agentsSelectedId === agentId) {
|
||||
@@ -881,10 +894,16 @@ export function renderApp(state: AppViewState) {
|
||||
state.agentSkillsReport = null;
|
||||
state.agentSkillsError = null;
|
||||
state.agentSkillsAgentId = null;
|
||||
state.toolsCatalogResult = null;
|
||||
state.toolsCatalogError = null;
|
||||
state.toolsCatalogLoading = false;
|
||||
void loadAgentIdentity(state, agentId);
|
||||
if (state.agentsPanel === "files") {
|
||||
void loadAgentFiles(state, agentId);
|
||||
}
|
||||
if (state.agentsPanel === "tools") {
|
||||
void loadToolsCatalog(state, agentId);
|
||||
}
|
||||
if (state.agentsPanel === "skills") {
|
||||
void loadAgentSkills(state, agentId);
|
||||
}
|
||||
@@ -906,6 +925,14 @@ export function renderApp(state: AppViewState) {
|
||||
void loadAgentSkills(state, resolvedAgentId);
|
||||
}
|
||||
}
|
||||
if (panel === "tools" && resolvedAgentId) {
|
||||
if (
|
||||
state.toolsCatalogResult?.agentId !== resolvedAgentId ||
|
||||
state.toolsCatalogError
|
||||
) {
|
||||
void loadToolsCatalog(state, resolvedAgentId);
|
||||
}
|
||||
}
|
||||
if (panel === "channels") {
|
||||
void loadChannels(state, false);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
SessionsListResult,
|
||||
SkillStatusReport,
|
||||
StatusSummary,
|
||||
ToolsCatalogResult,
|
||||
} from "./types.ts";
|
||||
import type { ChatAttachment, ChatQueueItem } from "./ui-types.ts";
|
||||
import type { NostrProfileFormState } from "./views/channels.nostr-profile-form.ts";
|
||||
@@ -152,6 +153,9 @@ export type AppViewState = {
|
||||
agentsList: AgentsListResult | null;
|
||||
agentsError: string | null;
|
||||
agentsSelectedId: string | null;
|
||||
toolsCatalogLoading: boolean;
|
||||
toolsCatalogError: string | null;
|
||||
toolsCatalogResult: ToolsCatalogResult | null;
|
||||
agentsPanel: "overview" | "files" | "tools" | "skills" | "channels" | "cron";
|
||||
agentFilesLoading: boolean;
|
||||
agentFilesError: string | null;
|
||||
|
||||
@@ -81,6 +81,7 @@ import type {
|
||||
SkillStatusReport,
|
||||
StatusSummary,
|
||||
NostrProfile,
|
||||
ToolsCatalogResult,
|
||||
} from "./types.ts";
|
||||
import { type ChatAttachment, type ChatQueueItem, type CronFormState } from "./ui-types.ts";
|
||||
import type { NostrProfileFormState } from "./views/channels.nostr-profile-form.ts";
|
||||
@@ -267,6 +268,9 @@ export class OpenClawApp extends LitElement {
|
||||
@state() agentsList: AgentsListResult | null = null;
|
||||
@state() agentsError: string | null = null;
|
||||
@state() agentsSelectedId: string | null = null;
|
||||
@state() toolsCatalogLoading = false;
|
||||
@state() toolsCatalogError: string | null = null;
|
||||
@state() toolsCatalogResult: ToolsCatalogResult | null = null;
|
||||
@state() agentsPanel: "overview" | "files" | "tools" | "skills" | "channels" | "cron" =
|
||||
"overview";
|
||||
@state() agentFilesLoading = false;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { GatewayBrowserClient } from "../gateway.ts";
|
||||
import type { AgentsListResult } from "../types.ts";
|
||||
import type { AgentsListResult, ToolsCatalogResult } from "../types.ts";
|
||||
|
||||
export type AgentsState = {
|
||||
client: GatewayBrowserClient | null;
|
||||
@@ -8,6 +8,9 @@ export type AgentsState = {
|
||||
agentsError: string | null;
|
||||
agentsList: AgentsListResult | null;
|
||||
agentsSelectedId: string | null;
|
||||
toolsCatalogLoading: boolean;
|
||||
toolsCatalogError: string | null;
|
||||
toolsCatalogResult: ToolsCatalogResult | null;
|
||||
};
|
||||
|
||||
export async function loadAgents(state: AgentsState) {
|
||||
@@ -35,3 +38,36 @@ export async function loadAgents(state: AgentsState) {
|
||||
state.agentsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadToolsCatalog(state: AgentsState, agentId: string) {
|
||||
const resolvedAgentId = agentId.trim();
|
||||
if (!state.client || !state.connected || !resolvedAgentId) {
|
||||
return;
|
||||
}
|
||||
if (state.toolsCatalogLoading) {
|
||||
return;
|
||||
}
|
||||
state.toolsCatalogLoading = true;
|
||||
state.toolsCatalogError = null;
|
||||
state.toolsCatalogResult = null;
|
||||
try {
|
||||
const res = await state.client.request<ToolsCatalogResult>("tools.catalog", {
|
||||
agentId: resolvedAgentId,
|
||||
includePlugins: true,
|
||||
});
|
||||
if (state.agentsSelectedId && state.agentsSelectedId !== resolvedAgentId) {
|
||||
return;
|
||||
}
|
||||
state.toolsCatalogResult = res;
|
||||
} catch (err) {
|
||||
if (state.agentsSelectedId && state.agentsSelectedId !== resolvedAgentId) {
|
||||
return;
|
||||
}
|
||||
state.toolsCatalogResult = null;
|
||||
state.toolsCatalogError = String(err);
|
||||
} finally {
|
||||
if (!state.agentsSelectedId || state.agentsSelectedId === resolvedAgentId) {
|
||||
state.toolsCatalogLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,6 +573,15 @@ export type ModelCatalogEntry = {
|
||||
input?: Array<"text" | "image">;
|
||||
};
|
||||
|
||||
export type ToolCatalogProfile =
|
||||
import("../../../src/gateway/protocol/schema/types.js").ToolCatalogProfile;
|
||||
export type ToolCatalogEntry =
|
||||
import("../../../src/gateway/protocol/schema/types.js").ToolCatalogEntry;
|
||||
export type ToolCatalogGroup =
|
||||
import("../../../src/gateway/protocol/schema/types.js").ToolCatalogGroup;
|
||||
export type ToolsCatalogResult =
|
||||
import("../../../src/gateway/protocol/schema/types.js").ToolsCatalogResult;
|
||||
|
||||
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
||||
|
||||
export type LogEntry = {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { html, nothing } from "lit";
|
||||
import { normalizeToolName } from "../../../../src/agents/tool-policy-shared.js";
|
||||
import type { SkillStatusEntry, SkillStatusReport } from "../types.ts";
|
||||
import type { SkillStatusEntry, SkillStatusReport, ToolsCatalogResult } from "../types.ts";
|
||||
import {
|
||||
type AgentToolEntry,
|
||||
type AgentToolSection,
|
||||
isAllowedByPolicy,
|
||||
matchesList,
|
||||
PROFILE_OPTIONS,
|
||||
resolveAgentConfig,
|
||||
resolveToolProfileOptions,
|
||||
resolveToolProfile,
|
||||
TOOL_SECTIONS,
|
||||
resolveToolSections,
|
||||
} from "./agents-utils.ts";
|
||||
import type { SkillGroup } from "./skills-grouping.ts";
|
||||
import { groupSkills } from "./skills-grouping.ts";
|
||||
@@ -17,12 +19,37 @@ import {
|
||||
renderSkillStatusChips,
|
||||
} from "./skills-shared.ts";
|
||||
|
||||
function renderToolBadges(section: AgentToolSection, tool: AgentToolEntry) {
|
||||
const source = tool.source ?? section.source;
|
||||
const pluginId = tool.pluginId ?? section.pluginId;
|
||||
const badges: string[] = [];
|
||||
if (source === "plugin" && pluginId) {
|
||||
badges.push(`plugin:${pluginId}`);
|
||||
} else if (source === "core") {
|
||||
badges.push("core");
|
||||
}
|
||||
if (tool.optional) {
|
||||
badges.push("optional");
|
||||
}
|
||||
if (badges.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px;">
|
||||
${badges.map((badge) => html`<span class="agent-pill">${badge}</span>`)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderAgentTools(params: {
|
||||
agentId: string;
|
||||
configForm: Record<string, unknown> | null;
|
||||
configLoading: boolean;
|
||||
configSaving: boolean;
|
||||
configDirty: boolean;
|
||||
toolsCatalogLoading: boolean;
|
||||
toolsCatalogError: string | null;
|
||||
toolsCatalogResult: ToolsCatalogResult | null;
|
||||
onProfileChange: (agentId: string, profile: string | null, clearAllow: boolean) => void;
|
||||
onOverridesChange: (agentId: string, alsoAllow: string[], deny: string[]) => void;
|
||||
onConfigReload: () => void;
|
||||
@@ -32,6 +59,8 @@ export function renderAgentTools(params: {
|
||||
const agentTools = config.entry?.tools ?? {};
|
||||
const globalTools = config.globalTools ?? {};
|
||||
const profile = agentTools.profile ?? globalTools.profile ?? "full";
|
||||
const profileOptions = resolveToolProfileOptions(params.toolsCatalogResult);
|
||||
const toolSections = resolveToolSections(params.toolsCatalogResult);
|
||||
const profileSource = agentTools.profile
|
||||
? "agent override"
|
||||
: globalTools.profile
|
||||
@@ -40,7 +69,11 @@ export function renderAgentTools(params: {
|
||||
const hasAgentAllow = Array.isArray(agentTools.allow) && agentTools.allow.length > 0;
|
||||
const hasGlobalAllow = Array.isArray(globalTools.allow) && globalTools.allow.length > 0;
|
||||
const editable =
|
||||
Boolean(params.configForm) && !params.configLoading && !params.configSaving && !hasAgentAllow;
|
||||
Boolean(params.configForm) &&
|
||||
!params.configLoading &&
|
||||
!params.configSaving &&
|
||||
!hasAgentAllow &&
|
||||
!(params.toolsCatalogLoading && !params.toolsCatalogResult && !params.toolsCatalogError);
|
||||
const alsoAllow = hasAgentAllow
|
||||
? []
|
||||
: Array.isArray(agentTools.alsoAllow)
|
||||
@@ -50,7 +83,7 @@ export function renderAgentTools(params: {
|
||||
const basePolicy = hasAgentAllow
|
||||
? { allow: agentTools.allow ?? [], deny: agentTools.deny ?? [] }
|
||||
: (resolveToolProfile(profile) ?? undefined);
|
||||
const toolIds = TOOL_SECTIONS.flatMap((section) => section.tools.map((tool) => tool.id));
|
||||
const toolIds = toolSections.flatMap((section) => section.tools.map((tool) => tool.id));
|
||||
|
||||
const resolveAllowed = (toolId: string) => {
|
||||
const baseAllowed = isAllowedByPolicy(toolId, basePolicy);
|
||||
@@ -166,6 +199,22 @@ export function renderAgentTools(params: {
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
params.toolsCatalogLoading && !params.toolsCatalogResult && !params.toolsCatalogError
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">Loading runtime tool catalog…</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
params.toolsCatalogError
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Could not load runtime tool catalog. Showing built-in fallback list instead.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="agent-tools-meta" style="margin-top: 16px;">
|
||||
<div class="agent-kv">
|
||||
@@ -191,7 +240,7 @@ export function renderAgentTools(params: {
|
||||
<div class="agent-tools-presets" style="margin-top: 16px;">
|
||||
<div class="label">Quick Presets</div>
|
||||
<div class="agent-tools-buttons">
|
||||
${PROFILE_OPTIONS.map(
|
||||
${profileOptions.map(
|
||||
(option) => html`
|
||||
<button
|
||||
class="btn btn--sm ${profile === option.id ? "active" : ""}"
|
||||
@@ -213,11 +262,18 @@ export function renderAgentTools(params: {
|
||||
</div>
|
||||
|
||||
<div class="agent-tools-grid" style="margin-top: 20px;">
|
||||
${TOOL_SECTIONS.map(
|
||||
${toolSections.map(
|
||||
(section) =>
|
||||
html`
|
||||
<div class="agent-tools-section">
|
||||
<div class="agent-tools-header">${section.label}</div>
|
||||
<div class="agent-tools-header">
|
||||
${section.label}
|
||||
${
|
||||
section.source === "plugin" && section.pluginId
|
||||
? html`<span class="agent-pill" style="margin-left: 8px;">plugin:${section.pluginId}</span>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<div class="agent-tools-list">
|
||||
${section.tools.map((tool) => {
|
||||
const { allowed } = resolveAllowed(tool.id);
|
||||
@@ -226,6 +282,7 @@ export function renderAgentTools(params: {
|
||||
<div>
|
||||
<div class="agent-tool-title mono">${tool.label}</div>
|
||||
<div class="agent-tool-sub">${tool.description}</div>
|
||||
${renderToolBadges(section, tool)}
|
||||
</div>
|
||||
<label class="cfg-toggle">
|
||||
<input
|
||||
|
||||
@@ -4,9 +4,33 @@ import {
|
||||
normalizeToolName,
|
||||
resolveToolProfilePolicy,
|
||||
} from "../../../../src/agents/tool-policy-shared.js";
|
||||
import type { AgentIdentityResult, AgentsFilesListResult, AgentsListResult } from "../types.ts";
|
||||
import type {
|
||||
AgentIdentityResult,
|
||||
AgentsFilesListResult,
|
||||
AgentsListResult,
|
||||
ToolCatalogProfile,
|
||||
ToolsCatalogResult,
|
||||
} from "../types.ts";
|
||||
|
||||
export const TOOL_SECTIONS = [
|
||||
export type AgentToolEntry = {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
source?: "core" | "plugin";
|
||||
pluginId?: string;
|
||||
optional?: boolean;
|
||||
defaultProfiles?: string[];
|
||||
};
|
||||
|
||||
export type AgentToolSection = {
|
||||
id: string;
|
||||
label: string;
|
||||
source?: "core" | "plugin";
|
||||
pluginId?: string;
|
||||
tools: AgentToolEntry[];
|
||||
};
|
||||
|
||||
export const FALLBACK_TOOL_SECTIONS: AgentToolSection[] = [
|
||||
{
|
||||
id: "fs",
|
||||
label: "Files",
|
||||
@@ -97,6 +121,38 @@ export const PROFILE_OPTIONS = [
|
||||
{ id: "full", label: "Full" },
|
||||
] as const;
|
||||
|
||||
export function resolveToolSections(
|
||||
toolsCatalogResult: ToolsCatalogResult | null,
|
||||
): AgentToolSection[] {
|
||||
if (toolsCatalogResult?.groups?.length) {
|
||||
return toolsCatalogResult.groups.map((group) => ({
|
||||
id: group.id,
|
||||
label: group.label,
|
||||
source: group.source,
|
||||
pluginId: group.pluginId,
|
||||
tools: group.tools.map((tool) => ({
|
||||
id: tool.id,
|
||||
label: tool.label,
|
||||
description: tool.description,
|
||||
source: tool.source,
|
||||
pluginId: tool.pluginId,
|
||||
optional: tool.optional,
|
||||
defaultProfiles: [...tool.defaultProfiles],
|
||||
})),
|
||||
}));
|
||||
}
|
||||
return FALLBACK_TOOL_SECTIONS;
|
||||
}
|
||||
|
||||
export function resolveToolProfileOptions(
|
||||
toolsCatalogResult: ToolsCatalogResult | null,
|
||||
): readonly ToolCatalogProfile[] | typeof PROFILE_OPTIONS {
|
||||
if (toolsCatalogResult?.profiles?.length) {
|
||||
return toolsCatalogResult.profiles;
|
||||
}
|
||||
return PROFILE_OPTIONS;
|
||||
}
|
||||
|
||||
type ToolPolicy = {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
CronJob,
|
||||
CronStatus,
|
||||
SkillStatusReport,
|
||||
ToolsCatalogResult,
|
||||
} from "../types.ts";
|
||||
import { renderAgentOverview } from "./agents-panels-overview.ts";
|
||||
import {
|
||||
@@ -58,6 +59,12 @@ export type AgentSkillsState = {
|
||||
filter: string;
|
||||
};
|
||||
|
||||
export type ToolsCatalogState = {
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
result: ToolsCatalogResult | null;
|
||||
};
|
||||
|
||||
export type AgentsProps = {
|
||||
basePath: string;
|
||||
loading: boolean;
|
||||
@@ -73,6 +80,7 @@ export type AgentsProps = {
|
||||
agentIdentityError: string | null;
|
||||
agentIdentityById: Record<string, AgentIdentityResult>;
|
||||
agentSkills: AgentSkillsState;
|
||||
toolsCatalog: ToolsCatalogState;
|
||||
onRefresh: () => void;
|
||||
onSelectAgent: (agentId: string) => void;
|
||||
onSelectPanel: (panel: AgentsPanel) => void;
|
||||
@@ -257,6 +265,9 @@ export function renderAgents(props: AgentsProps) {
|
||||
configLoading: props.config.loading,
|
||||
configSaving: props.config.saving,
|
||||
configDirty: props.config.dirty,
|
||||
toolsCatalogLoading: props.toolsCatalog.loading,
|
||||
toolsCatalogError: props.toolsCatalog.error,
|
||||
toolsCatalogResult: props.toolsCatalog.result,
|
||||
onProfileChange: props.onToolsProfileChange,
|
||||
onOverridesChange: props.onToolsOverridesChange,
|
||||
onConfigReload: props.onConfigReload,
|
||||
|
||||
Reference in New Issue
Block a user