This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch feat/ai-history-and-enhancement in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit 83a37ad20fd3445dac6c3b36dcafdca85da1bc9f Author: Wu Sheng <[email protected]> AuthorDate: Wed Jul 8 15:53:48 2026 +0800 refactor(ai): externalize the visualization skill's tool prompts (rollout complete) The last skill — its make() widget family + shared renderInput schema + the explicit map/trace/log tools + the service-list sub-page — moves out of code into resources/tools/visualization.yaml, read via toolPrompt. The shared 5-widget description template and the render-input param hints are shared entries (`_widget` with {type}/{when} placeholders, `_render_input`); make() interpolates them at build time. Byte-identical to f14353e (verified verbatim, including the two template-built descriptions reconstructed). All 7 skills now read their LLM-facing manifest (tool descriptions + param hints) from editable resources/tools/*.yaml — no tool-prompt text left in code, and the skills.md<->code drift is gone. Only the parsed values reach the model; the YAML comments (incl. the license header) never do. --- apps/bff/src/ai/resources/tools/visualization.yaml | 217 +++++++++++++++++++++ apps/bff/src/ai/skill/visualization/tools.ts | 192 +++++++++--------- 2 files changed, 310 insertions(+), 99 deletions(-) diff --git a/apps/bff/src/ai/resources/tools/visualization.yaml b/apps/bff/src/ai/resources/tools/visualization.yaml new file mode 100644 index 0000000..5390d61 --- /dev/null +++ b/apps/bff/src/ai/resources/tools/visualization.yaml @@ -0,0 +1,217 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# LLM-facing tool manifest for the `visualization` skill: each tool's description + +# per-param hints the model reads to pick and call the tool. Edit the prompt text +# HERE, not in skill/visualization/tools.ts (which reads these via toolPrompt). These +# strings are part of the provider prompt-cache prefix — keep them byte-stable. + +# (internal) The 5 show_<type> tools share ONE description template; {type} and +# {when} are filled in per widget type at build time from the entries below. +_widget: + description: >- + Render a {type} figure in the chat. {when} Provide the layer, service name, and the MQE expression(s) (use catalog MQE verbatim). Pass instance/endpoint to render at that finer scope. Give several related figures the same group label to cluster them into one tabbed block. + +show_line: + description: >- + Use for a time series (a metric that varies over the window). + +show_card: + description: >- + Use ONLY when the MQE collapses to a single scalar (latest/max/min/avg-of-plain/sum). + +show_top: + description: >- + Use for a sorted top-N list (an MQE wrapped in top_n(...)). + +show_table: + description: >- + Use for a labeled table (e.g. latest(...) of a labeled metric). + +show_record: + description: >- + Use for RECORD-typed rows (e.g. sampled records / slow traces list). + +# (internal) shared render-input schema used by the 5 show_<type> tools. +_render_input: + description: >- + (internal) shared render-input schema + params: + title: >- + Human title shown above the figure + layer: >- + OAP layer key, e.g. GENERAL + service: >- + Service NAME (from list_services) + expressions: >- + MQE expression(s) — use catalog MQE verbatim + labels: >- + One short human label PER expression. REQUIRED when you pass 2+ expressions on one chart (e.g. ["read","write"]) — without it every line inherits the title and the operator cannot tell them apart. Omit for a single expression. + instance: >- + ServiceInstance name → renders at instance scope + endpoint: >- + Endpoint name → renders at endpoint scope + unit: >- + Unit suffix, e.g. ms, %, cpm + group: >- + Optional group label. Give several related figures the SAME group to cluster them into ONE tabbed block (e.g. an entity breakdown); omit for a standalone figure. + +show_widget: + description: >- + PREFERRED way to show a catalog metric: render an EXISTING dashboard widget by its id with the template's FULL config (explanation/tip, unit, format, thresholds, per-rank legends) — identical to the dashboard, no reconstruction. Pass the layer, the scope you browsed, the service NAME, and the widgetId from kb_browse_catalog. Add instance/endpoint (a NAME) for a finer scope. Use show_line/show_card/etc. ONLY for a DERIVED MQE with no catalog widget (e.g. a drilled child-scope query). + params: + layer: >- + OAP layer key, e.g. GENERAL + scope: >- + the catalog scope you browsed (default service) + service: >- + service NAME + widgetId: >- + the widgetId from kb_browse_catalog + instance: >- + ServiceInstance NAME (instance scope) + endpoint: >- + Endpoint NAME (endpoint scope) + group: >- + cluster related figures into one tabbed block + +show_service_list: + description: >- + Render the services in a layer with their key metrics inline in the chat as an interactive view (the same component the dashboards use). Provide the layer (and optionally a service to focus). + params: + layer: >- + OAP layer key, e.g. GENERAL + service: >- + optional service NAME to focus on + title: >- + optional heading for the view + +show_hierarchy: + description: >- + Render a service's CROSS-LAYER hierarchy inline (the topology page's Smartscape overlay): the focused service plus the same logical service projected into its upper layers (e.g. a GENERAL service's MESH / K8S_SERVICE mirrors) and lower layers (backing infrastructure). Use this to show how one service maps across layers — NOT for same-layer dependencies (use show_topology for those). Provide the layer and service name. + params: + layer: >- + OAP layer key of the focused service, e.g. GENERAL + service: >- + service NAME (from list_services) + title: >- + optional heading for the block + +show_topology: + description: >- + Render a service's FOCUSED one-hop dependency topology inline (the ego graph): the service plus its DIRECT upstream callers (services that call it) and DIRECT downstream dependencies (services it calls). This is one hop each way — NOT the whole-layer map. Use it to show who a service talks to / walk the dependency step of an investigation. Provide the layer and service name. + params: + layer: >- + OAP layer key, e.g. GENERAL + service: >- + service NAME (from list_services) + title: >- + optional heading for the block + +show_deployment: + description: >- + Mount the per-service DEPLOYMENT view inline (read-only): the instance-to-instance call graph WITHIN one service (its instances/pods and the intra-service relations between them), the same view as the layer Deployment tab. Use it to show how a service's own instances talk to each other — NOT the cross-service topology (use show_topology for that). NOTE: it reads the SAME OAP instance-relation API as show_instance_topology, but with the one service on BOTH sides — so despite the share [...] + params: + layer: >- + OAP layer key, e.g. GENERAL + service: >- + service NAME (from list_services) + title: >- + optional heading for the block + +show_instance_topology: + description: >- + Mount the per-PAIR INSTANCE-TOPOLOGY view inline (read-only): the instances of a SOURCE (client) service and a DEST (server) service as two columns, with the instance-to-instance calls BETWEEN them (the same instance-map drill-down the Topology tab opens from a call edge). Requires BOTH a source and a dest service that have a call relationship (source calls dest) — use it to show how one service's instances talk to another's. It reads the SAME OAP instance-relation API as show_deploy [...] + params: + layer: >- + OAP layer key, e.g. GENERAL + sourceService: >- + SOURCE (client) service NAME — the caller (from list_services) + destService: >- + DEST (server) service NAME — the callee it calls (from list_services) + title: >- + optional heading for the block + +show_endpoint_dependency: + description: >- + Mount the per-endpoint API-DEPENDENCY view inline (read-only): the focused endpoint's upstream callers and downstream callees as a dependency chain (the same view as the layer API-dependency tab). It draws the service's PRIMARY endpoint (its most-recently-active one) and pins that chain; the operator can expand any node. This is API-DEPENDENCY detection (endpoint→endpoint). It reads EndpointRelation, which is SERVER-SIDE ONLY (no client-side edge metric) and is produced by NATIVE Sky [...] + params: + layer: >- + OAP layer key, e.g. GENERAL + service: >- + service NAME (from list_services) + title: >- + optional heading for the block + +show_traces: + description: >- + Mount the native distributed-tracing view inline for a service — the real trace LIST plus the span WATERFALL, which the operator browses (click a trace to open its spans). Use it to surface slow / erroring traces for a service so a human can inspect them; there is no tool to read individual span data yourself. Provide the layer and service name; optionally a look-back windowMinutes (default 30). ONLY for a layer whose traces run NATIVE (SkyWalking segments) — for a Zipkin-tracing lay [...] + params: + layer: >- + OAP layer key, e.g. GENERAL + service: >- + service NAME (from list_services) + title: >- + optional heading for the block + windowMinutes: >- + look-back window in minutes (default 30) + +list_zipkin_services: + description: >- + List the ZIPKIN service names (the span localEndpoint.serviceName universe — GLOBAL, not per-layer, and DIFFERENT from the SkyWalking service names). Use this for a Zipkin-tracing layer to find the Zipkin-side service name that matches the SkyWalking service (or the user's phrasing), THEN pass the matched name to show_zipkin_traces. Optionally pass a keyword to narrow the list. + params: + keyword: >- + case-insensitive substring to narrow the Zipkin service list + +show_zipkin_traces: + description: >- + Mount the Zipkin distributed-tracing view inline — the real trace LIST + span WATERFALL for a ZIPKIN service, which the operator browses. `service` MUST be a Zipkin service name from list_zipkin_services (NOT the SkyWalking name — they differ). Use this for a layer whose traces run on Zipkin (Envoy ALS / rover) after matching the service. Provide the layer (for context) and the matched Zipkin service name; optionally a look-back windowMinutes (default 30). + params: + layer: >- + OAP layer key, e.g. MESH + service: >- + ZIPKIN service name (from list_zipkin_services) + title: >- + optional heading for the block + windowMinutes: >- + look-back window in minutes (default 30) + +show_logs: + description: >- + Mount the layer LOGS view inline for a service — the real log stream the operator browses (click a row for its detail). Use it to surface a service's logs for a human to read. This is the layer Logs tab (stored logs); it is NOT fetch_pod_logs (that is the Kubernetes on-demand live tail). Provide the layer and service name; optionally a look-back windowMinutes (default 30). + params: + layer: >- + OAP layer key, e.g. GENERAL + service: >- + service NAME (from list_services) + title: >- + optional heading for the block + windowMinutes: >- + look-back window in minutes (default 30) + +show_browser_logs: + description: >- + Mount the browser-monitoring ERROR list inline for a browser app — the client-side JS error stream the operator browses (click a row for its stack trace). Use it for a BROWSER-family layer's app to surface front-end errors for a human. Provide the layer and service (browser-app) name; optionally a look-back windowMinutes (default 30). + params: + layer: >- + OAP browser-layer key + service: >- + browser-app service NAME (from list_services) + title: >- + optional heading for the block + windowMinutes: >- + look-back window in minutes (default 30) diff --git a/apps/bff/src/ai/skill/visualization/tools.ts b/apps/bff/src/ai/skill/visualization/tools.ts index 6cdc716..c8f27c2 100644 --- a/apps/bff/src/ai/skill/visualization/tools.ts +++ b/apps/bff/src/ai/skill/visualization/tools.ts @@ -54,27 +54,19 @@ import { buildDeployment } from '../../../logic/oap/deployment.js'; import { buildInstanceTopology } from '../../../logic/oap/instance-topology.js'; import { buildEndpointDependency } from '../../../logic/oap/endpoint-dependency.js'; import { zipkinFetchServices } from '../../../client/zipkin.js'; +import { toolPrompt } from '../../resources/loader.js'; +const ri = toolPrompt('visualization', '_render_input'); const renderInput = z.object({ - title: z.string().describe('Human title shown above the figure'), - layer: z.string().describe('OAP layer key, e.g. GENERAL'), - service: z.string().describe('Service NAME (from list_services)'), - expressions: z.array(z.string()).min(1).describe('MQE expression(s) — use catalog MQE verbatim'), - labels: z - .array(z.string()) - .optional() - .describe( - 'One short human label PER expression. REQUIRED when you pass 2+ expressions on one chart (e.g. ["read","write"]) — without it every line inherits the title and the operator cannot tell them apart. Omit for a single expression.', - ), - instance: z.string().optional().describe('ServiceInstance name → renders at instance scope'), - endpoint: z.string().optional().describe('Endpoint name → renders at endpoint scope'), - unit: z.string().optional().describe('Unit suffix, e.g. ms, %, cpm'), - group: z - .string() - .optional() - .describe( - 'Optional group label. Give several related figures the SAME group to cluster them into ONE tabbed block (e.g. an entity breakdown); omit for a standalone figure.', - ), + title: z.string().describe(ri.p('title')), + layer: z.string().describe(ri.p('layer')), + service: z.string().describe(ri.p('service')), + expressions: z.array(z.string()).min(1).describe(ri.p('expressions')), + labels: z.array(z.string()).optional().describe(ri.p('labels')), + instance: z.string().optional().describe(ri.p('instance')), + endpoint: z.string().optional().describe(ri.p('endpoint')), + unit: z.string().optional().describe(ri.p('unit')), + group: z.string().optional().describe(ri.p('group')), }); type RenderInput = z.infer<typeof renderInput>; @@ -292,10 +284,12 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa return `Rendered ${type} "${input.title}" (${summarize(type, result)}).`; } - const make = (type: DashboardWidgetType, when: string): StructuredToolInterface => + const make = (type: DashboardWidgetType): StructuredToolInterface => tool((input: RenderInput) => render(type, input), { name: `show_${type}`, - description: `Render a ${type} figure in the chat. ${when} Provide the layer, service name, and the MQE expression(s) (use catalog MQE verbatim). Pass instance/endpoint to render at that finer scope. Give several related figures the same group label to cluster them into one tabbed block.`, + description: toolPrompt('visualization', '_widget') + .description.replace('{type}', type) + .replace('{when}', toolPrompt('visualization', `show_${type}`).description), schema: renderInput, }); @@ -303,6 +297,7 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa // FULL config (tip/explanation, unit, format, valueMap, thresholds, per-rank // legends) — captured whole, so the figure persists mqe + explanation + // response + config. Preferred over show_line/etc. for a catalog metric. + const widgetPrompt = toolPrompt('visualization', 'show_widget'); const widgetTool = tool( async ({ layer, @@ -356,16 +351,15 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa }, { name: 'show_widget', - description: - "PREFERRED way to show a catalog metric: render an EXISTING dashboard widget by its id with the template's FULL config (explanation/tip, unit, format, thresholds, per-rank legends) — identical to the dashboard, no reconstruction. Pass the layer, the scope you browsed, the service NAME, and the widgetId from kb_browse_catalog. Add instance/endpoint (a NAME) for a finer scope. Use show_line/show_card/etc. ONLY for a DERIVED MQE with no catalog widget (e.g. a drilled child-scope query).", + description: widgetPrompt.description, schema: z.object({ - layer: z.string().describe('OAP layer key, e.g. GENERAL'), - scope: z.enum(['service', 'instance', 'endpoint']).optional().describe('the catalog scope you browsed (default service)'), - service: z.string().describe('service NAME'), - widgetId: z.string().describe('the widgetId from kb_browse_catalog'), - instance: z.string().optional().describe('ServiceInstance NAME (instance scope)'), - endpoint: z.string().optional().describe('Endpoint NAME (endpoint scope)'), - group: z.string().optional().describe('cluster related figures into one tabbed block'), + layer: z.string().describe(widgetPrompt.p('layer')), + scope: z.enum(['service', 'instance', 'endpoint']).optional().describe(widgetPrompt.p('scope')), + service: z.string().describe(widgetPrompt.p('service')), + widgetId: z.string().describe(widgetPrompt.p('widgetId')), + instance: z.string().optional().describe(widgetPrompt.p('instance')), + endpoint: z.string().optional().describe(widgetPrompt.p('endpoint')), + group: z.string().optional().describe(widgetPrompt.p('group')), }), }, ); @@ -375,13 +369,13 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa const SUBPAGES: Array<{ kind: SubPageKind; verb: string; - when: string; needsService: boolean; }> = [ - { kind: 'service-list', verb: 'metrics:read', when: 'the services in a layer with their key metrics', needsService: false }, + { kind: 'service-list', verb: 'metrics:read', needsService: false }, ]; - const subPageTools = SUBPAGES.map(({ kind, verb, when, needsService }) => - tool( + const subPageTools = SUBPAGES.map(({ kind, verb, needsService }) => { + const t = toolPrompt('visualization', `show_${kind.replace(/-/g, '_')}`); + return tool( async ({ layer, service, title }: { layer: string; service?: string; title?: string }): Promise<string> => { if (!ctx.hasVerb(verb)) return `Permission denied: the current user lacks ${verb}.`; if (needsService && !service) return `${kind} needs a service name.`; @@ -391,22 +385,23 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa }, { name: `show_${kind.replace(/-/g, '_')}`, - description: `Render ${when} inline in the chat as an interactive view (the same component the dashboards use). Provide the layer${needsService ? ' and the service name' : ' (and optionally a service to focus)'}.`, + description: t.description, schema: z.object({ - layer: z.string().describe('OAP layer key, e.g. GENERAL'), + layer: z.string().describe(t.p('layer')), service: needsService - ? z.string().describe('service NAME') - : z.string().optional().describe('optional service NAME to focus on'), - title: z.string().optional().describe('optional heading for the view'), + ? z.string().describe(t.p('service')) + : z.string().optional().describe(t.p('service')), + title: z.string().optional().describe(t.p('title')), }), }, - ), - ); + ); + }); // show_hierarchy renders the topology page's cross-layer Smartscape overlay // inline: the focus service + the same logical service projected into upper // (K8S_SERVICE ← MESH ← GENERAL) and lower (→ infra) layers. Params only — the // BFF resolves the peers from getServiceHierarchy; the UI draws the ribbon. + const hierarchyPrompt = toolPrompt('visualization', 'show_hierarchy'); const hierarchyTool = tool( async ({ layer, service, title }: { layer: string; service: string; title?: string }): Promise<string> => { if (!ctx.hasVerb('topology:read')) return 'Permission denied: the current user lacks topology:read.'; @@ -448,12 +443,11 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa }, { name: 'show_hierarchy', - description: - "Render a service's CROSS-LAYER hierarchy inline (the topology page's Smartscape overlay): the focused service plus the same logical service projected into its upper layers (e.g. a GENERAL service's MESH / K8S_SERVICE mirrors) and lower layers (backing infrastructure). Use this to show how one service maps across layers — NOT for same-layer dependencies (use show_topology for those). Provide the layer and service name.", + description: hierarchyPrompt.description, schema: z.object({ - layer: z.string().describe('OAP layer key of the focused service, e.g. GENERAL'), - service: z.string().describe('service NAME (from list_services)'), - title: z.string().optional().describe('optional heading for the block'), + layer: z.string().describe(hierarchyPrompt.p('layer')), + service: z.string().describe(hierarchyPrompt.p('service')), + title: z.string().optional().describe(hierarchyPrompt.p('title')), }), }, ); @@ -464,6 +458,7 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa // the Topology tab uses (depth 1), so the block carries the WHOLE graph — // nodes+edges WITH metric values + edge series — as a snapshot: the embedded // view seeds from it (static on reload) and the model reads real values. + const topologyPrompt = toolPrompt('visualization', 'show_topology'); const topologyTool = tool( async ({ layer, service, title }: { layer: string; service: string; title?: string }): Promise<string> => { if (!ctx.hasVerb('topology:read')) return 'Permission denied: the current user lacks topology:read.'; @@ -528,12 +523,11 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa }, { name: 'show_topology', - description: - "Render a service's FOCUSED one-hop dependency topology inline (the ego graph): the service plus its DIRECT upstream callers (services that call it) and DIRECT downstream dependencies (services it calls). This is one hop each way — NOT the whole-layer map. Use it to show who a service talks to / walk the dependency step of an investigation. Provide the layer and service name.", + description: topologyPrompt.description, schema: z.object({ - layer: z.string().describe('OAP layer key, e.g. GENERAL'), - service: z.string().describe('service NAME (from list_services)'), - title: z.string().optional().describe('optional heading for the block'), + layer: z.string().describe(topologyPrompt.p('layer')), + service: z.string().describe(topologyPrompt.p('service')), + title: z.string().optional().describe(topologyPrompt.p('title')), }), }, ); @@ -543,6 +537,7 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa // scoped, so the tool resolves the serviceId (the deployment query keys on it) // and hands the UI a frozen window; the UI view fetches its own graph and owns // the pan/zoom + node/edge detail. + const deploymentPrompt = toolPrompt('visualization', 'show_deployment'); const deploymentTool = tool( async ({ layer, service, title }: { layer: string; service: string; title?: string }): Promise<string> => { if (!ctx.hasVerb('topology:read')) return 'Permission denied: the current user lacks topology:read.'; @@ -584,12 +579,11 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa }, { name: 'show_deployment', - description: - "Mount the per-service DEPLOYMENT view inline (read-only): the instance-to-instance call graph WITHIN one service (its instances/pods and the intra-service relations between them), the same view as the layer Deployment tab. Use it to show how a service's own instances talk to each other — NOT the cross-service topology (use show_topology for that). NOTE: it reads the SAME OAP instance-relation API as show_instance_topology, but with the one service on BOTH sides — so despite the [...] + description: deploymentPrompt.description, schema: z.object({ - layer: z.string().describe('OAP layer key, e.g. GENERAL'), - service: z.string().describe('service NAME (from list_services)'), - title: z.string().optional().describe('optional heading for the block'), + layer: z.string().describe(deploymentPrompt.p('layer')), + service: z.string().describe(deploymentPrompt.p('service')), + title: z.string().optional().describe(deploymentPrompt.p('title')), }), }, ); @@ -599,6 +593,7 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa // as two columns, with the instance-to-instance calls between them. The tool // resolves BOTH service ids; the two must have a call relationship (client → // server) or the map is empty. The UI owns pan/zoom + node/edge detail. + const instanceTopologyPrompt = toolPrompt('visualization', 'show_instance_topology'); const instanceTopologyTool = tool( async ({ layer, sourceService, destService, title }: { layer: string; sourceService: string; destService: string; title?: string }): Promise<string> => { if (!ctx.hasVerb('topology:read')) return 'Permission denied: the current user lacks topology:read.'; @@ -643,13 +638,12 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa }, { name: 'show_instance_topology', - description: - "Mount the per-PAIR INSTANCE-TOPOLOGY view inline (read-only): the instances of a SOURCE (client) service and a DEST (server) service as two columns, with the instance-to-instance calls BETWEEN them (the same instance-map drill-down the Topology tab opens from a call edge). Requires BOTH a source and a dest service that have a call relationship (source calls dest) — use it to show how one service's instances talk to another's. It reads the SAME OAP instance-relation API as show_d [...] + description: instanceTopologyPrompt.description, schema: z.object({ - layer: z.string().describe('OAP layer key, e.g. GENERAL'), - sourceService: z.string().describe('SOURCE (client) service NAME — the caller (from list_services)'), - destService: z.string().describe('DEST (server) service NAME — the callee it calls (from list_services)'), - title: z.string().optional().describe('optional heading for the block'), + layer: z.string().describe(instanceTopologyPrompt.p('layer')), + sourceService: z.string().describe(instanceTopologyPrompt.p('sourceService')), + destService: z.string().describe(instanceTopologyPrompt.p('destService')), + title: z.string().optional().describe(instanceTopologyPrompt.p('title')), }), }, ); @@ -658,6 +652,7 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa // inline (read-only). Service-scoped: the tool resolves the serviceId; the // embedded view auto-picks the service's top endpoint and draws its upstream/ // downstream dependency chain. The UI owns the expand + node/edge detail. + const endpointDependencyPrompt = toolPrompt('visualization', 'show_endpoint_dependency'); const endpointDependencyTool = tool( async ({ layer, service, title }: { layer: string; service: string; title?: string }): Promise<string> => { if (!ctx.hasVerb('topology:read')) return 'Permission denied: the current user lacks topology:read.'; @@ -695,12 +690,11 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa }, { name: 'show_endpoint_dependency', - description: - "Mount the per-endpoint API-DEPENDENCY view inline (read-only): the focused endpoint's upstream callers and downstream callees as a dependency chain (the same view as the layer API-dependency tab). It draws the service's PRIMARY endpoint (its most-recently-active one) and pins that chain; the operator can expand any node. This is API-DEPENDENCY detection (endpoint→endpoint). It reads EndpointRelation, which is SERVER-SIDE ONLY (no client-side edge metric) and is produced by NATIV [...] + description: endpointDependencyPrompt.description, schema: z.object({ - layer: z.string().describe('OAP layer key, e.g. GENERAL'), - service: z.string().describe('service NAME (from list_services)'), - title: z.string().optional().describe('optional heading for the block'), + layer: z.string().describe(endpointDependencyPrompt.p('layer')), + service: z.string().describe(endpointDependencyPrompt.p('service')), + title: z.string().optional().describe(endpointDependencyPrompt.p('title')), }), }, ); @@ -709,6 +703,7 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa // on the service — the operator gets the actual trace LIST + span WATERFALL to // browse. Params only; the UI view fetches its own traces + owns the // list→detail interaction. + const tracesPrompt = toolPrompt('visualization', 'show_traces'); const tracesTool = tool( async ({ layer, service, title, windowMinutes }: { layer: string; service: string; title?: string; windowMinutes?: number }): Promise<string> => { if (!ctx.hasVerb('traces:read')) return 'Permission denied: the current user lacks traces:read.'; @@ -726,13 +721,12 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa }, { name: 'show_traces', - description: - "Mount the native distributed-tracing view inline for a service — the real trace LIST plus the span WATERFALL, which the operator browses (click a trace to open its spans). Use it to surface slow / erroring traces for a service so a human can inspect them; there is no tool to read individual span data yourself. Provide the layer and service name; optionally a look-back windowMinutes (default 30). ONLY for a layer whose traces run NATIVE (SkyWalking segments) — for a Zipkin-tracin [...] + description: tracesPrompt.description, schema: z.object({ - layer: z.string().describe('OAP layer key, e.g. GENERAL'), - service: z.string().describe('service NAME (from list_services)'), - title: z.string().optional().describe('optional heading for the block'), - windowMinutes: z.number().optional().describe('look-back window in minutes (default 30)'), + layer: z.string().describe(tracesPrompt.p('layer')), + service: z.string().describe(tracesPrompt.p('service')), + title: z.string().optional().describe(tracesPrompt.p('title')), + windowMinutes: z.number().optional().describe(tracesPrompt.p('windowMinutes')), }), }, ); @@ -741,6 +735,7 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa // localEndpoint.serviceName), which is GLOBAL and differs from the SkyWalking // service names. So the assistant first LISTS the Zipkin services (this tool), // matches the intended service by name, then renders with show_zipkin_traces. + const listZipkinServicesPrompt = toolPrompt('visualization', 'list_zipkin_services'); const listZipkinServicesTool = tool( async ({ keyword }: { keyword?: string }): Promise<string> => { if (!ctx.hasVerb('traces:read')) return 'Permission denied: the current user lacks traces:read.'; @@ -764,16 +759,16 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa }, { name: 'list_zipkin_services', - description: - "List the ZIPKIN service names (the span localEndpoint.serviceName universe — GLOBAL, not per-layer, and DIFFERENT from the SkyWalking service names). Use this for a Zipkin-tracing layer to find the Zipkin-side service name that matches the SkyWalking service (or the user's phrasing), THEN pass the matched name to show_zipkin_traces. Optionally pass a keyword to narrow the list.", + description: listZipkinServicesPrompt.description, schema: z.object({ - keyword: z.string().optional().describe('case-insensitive substring to narrow the Zipkin service list'), + keyword: z.string().optional().describe(listZipkinServicesPrompt.p('keyword')), }), }, ); // show_zipkin_traces mounts the real Zipkin Traces view inline (read-only), // focused on a ZIPKIN service name the model matched via list_zipkin_services. + const zipkinTracesPrompt = toolPrompt('visualization', 'show_zipkin_traces'); const zipkinTracesTool = tool( async ({ layer, service, title, windowMinutes }: { layer: string; service: string; title?: string; windowMinutes?: number }): Promise<string> => { if (!ctx.hasVerb('traces:read')) return 'Permission denied: the current user lacks traces:read.'; @@ -787,13 +782,12 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa }, { name: 'show_zipkin_traces', - description: - "Mount the Zipkin distributed-tracing view inline — the real trace LIST + span WATERFALL for a ZIPKIN service, which the operator browses. `service` MUST be a Zipkin service name from list_zipkin_services (NOT the SkyWalking name — they differ). Use this for a layer whose traces run on Zipkin (Envoy ALS / rover) after matching the service. Provide the layer (for context) and the matched Zipkin service name; optionally a look-back windowMinutes (default 30).", + description: zipkinTracesPrompt.description, schema: z.object({ - layer: z.string().describe('OAP layer key, e.g. MESH'), - service: z.string().describe('ZIPKIN service name (from list_zipkin_services)'), - title: z.string().optional().describe('optional heading for the block'), - windowMinutes: z.number().optional().describe('look-back window in minutes (default 30)'), + layer: z.string().describe(zipkinTracesPrompt.p('layer')), + service: z.string().describe(zipkinTracesPrompt.p('service')), + title: z.string().optional().describe(zipkinTracesPrompt.p('title')), + windowMinutes: z.number().optional().describe(zipkinTracesPrompt.p('windowMinutes')), }), }, ); @@ -801,6 +795,7 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa // show_logs mounts the real layer Logs view inline (read-only), focused on the // service — the operator gets the actual log stream + row→detail. Distinct // from fetch_pod_logs (the k8s on-demand live tail); this is the layer Logs tab. + const logsPrompt = toolPrompt('visualization', 'show_logs'); const logsTool = tool( async ({ layer, service, title, windowMinutes }: { layer: string; service: string; title?: string; windowMinutes?: number }): Promise<string> => { if (!ctx.hasVerb('logs:read')) return 'Permission denied: the current user lacks logs:read.'; @@ -818,13 +813,12 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa }, { name: 'show_logs', - description: - "Mount the layer LOGS view inline for a service — the real log stream the operator browses (click a row for its detail). Use it to surface a service's logs for a human to read. This is the layer Logs tab (stored logs); it is NOT fetch_pod_logs (that is the Kubernetes on-demand live tail). Provide the layer and service name; optionally a look-back windowMinutes (default 30).", + description: logsPrompt.description, schema: z.object({ - layer: z.string().describe('OAP layer key, e.g. GENERAL'), - service: z.string().describe('service NAME (from list_services)'), - title: z.string().optional().describe('optional heading for the block'), - windowMinutes: z.number().optional().describe('look-back window in minutes (default 30)'), + layer: z.string().describe(logsPrompt.p('layer')), + service: z.string().describe(logsPrompt.p('service')), + title: z.string().optional().describe(logsPrompt.p('title')), + windowMinutes: z.number().optional().describe(logsPrompt.p('windowMinutes')), }), }, ); @@ -832,6 +826,7 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa // show_browser_logs mounts the real browser-monitoring error list inline // (read-only), focused on the browser app — the operator gets the client-side // error stream + its row→stack-trace detail (BROWSER-family layers only). + const browserErrorsPrompt = toolPrompt('visualization', 'show_browser_logs'); const browserErrorsTool = tool( async ({ layer, service, title, windowMinutes }: { layer: string; service: string; title?: string; windowMinutes?: number }): Promise<string> => { if (!ctx.hasVerb('browser-errors:read')) return 'Permission denied: the current user lacks browser-errors:read.'; @@ -849,23 +844,22 @@ export function visualizationTools(ctx: AiRequestContext): StructuredToolInterfa }, { name: 'show_browser_logs', - description: - "Mount the browser-monitoring ERROR list inline for a browser app — the client-side JS error stream the operator browses (click a row for its stack trace). Use it for a BROWSER-family layer's app to surface front-end errors for a human. Provide the layer and service (browser-app) name; optionally a look-back windowMinutes (default 30).", + description: browserErrorsPrompt.description, schema: z.object({ - layer: z.string().describe('OAP browser-layer key'), - service: z.string().describe('browser-app service NAME (from list_services)'), - title: z.string().optional().describe('optional heading for the block'), - windowMinutes: z.number().optional().describe('look-back window in minutes (default 30)'), + layer: z.string().describe(browserErrorsPrompt.p('layer')), + service: z.string().describe(browserErrorsPrompt.p('service')), + title: z.string().optional().describe(browserErrorsPrompt.p('title')), + windowMinutes: z.number().optional().describe(browserErrorsPrompt.p('windowMinutes')), }), }, ); return [ - make('line', 'Use for a time series (a metric that varies over the window).'), - make('card', 'Use ONLY when the MQE collapses to a single scalar (latest/max/min/avg-of-plain/sum).'), - make('top', 'Use for a sorted top-N list (an MQE wrapped in top_n(...)).'), - make('table', 'Use for a labeled table (e.g. latest(...) of a labeled metric).'), - make('record', 'Use for RECORD-typed rows (e.g. sampled records / slow traces list).'), + make('line'), + make('card'), + make('top'), + make('table'), + make('record'), widgetTool, ...subPageTools, hierarchyTool,
