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,


Reply via email to