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 f14353e1b6650b88a9034beda704bd94245d5256
Author: Wu Sheng <[email protected]>
AuthorDate: Wed Jul 8 15:41:36 2026 +0800

    refactor(ai): externalize 5 more skills' tool prompts to 
resources/tools/*.yaml
    
    Move each tool's LLM-facing description + param hints out of code into an 
editable
    per-skill YAML the loader reads at boot via toolPrompt — kubernetes, 
metric-catalog,
    rca, telemetry, triggers (12 tools, 30 params). Byte-identical to the prior 
code
    strings (verified verbatim against dd8526f), so the tool manifest is now one
    editable source with no code<->skills.md drift and the LLM sees the same 
text.
    Only the values are read (YAML comments incl. the license header never 
reach the
    model). The visualization skill (its make() widget family + shared 
renderInput
    schema) is the remaining one.
---
 apps/bff/src/ai/resources/tools/kubernetes.yaml    | 49 +++++++++++++++
 .../bff/src/ai/resources/tools/metric-catalog.yaml | 71 ++++++++++++++++++++++
 apps/bff/src/ai/resources/tools/rca.yaml           | 32 ++++++++++
 apps/bff/src/ai/resources/tools/telemetry.yaml     | 30 +++++++++
 apps/bff/src/ai/resources/tools/triggers.yaml      | 42 +++++++++++++
 apps/bff/src/ai/skill/kubernetes/tools.ts          | 37 +++++------
 apps/bff/src/ai/skill/metric-catalog/tools.ts      | 45 +++++++-------
 apps/bff/src/ai/skill/rca/tools.ts                 | 13 ++--
 apps/bff/src/ai/skill/telemetry/tools.ts           |  9 +--
 apps/bff/src/ai/skill/triggers/tools.ts            | 21 ++++---
 10 files changed, 283 insertions(+), 66 deletions(-)

diff --git a/apps/bff/src/ai/resources/tools/kubernetes.yaml 
b/apps/bff/src/ai/resources/tools/kubernetes.yaml
new file mode 100644
index 0000000..f8a0f95
--- /dev/null
+++ b/apps/bff/src/ai/resources/tools/kubernetes.yaml
@@ -0,0 +1,49 @@
+# 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 `kubernetes` 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/kubernetes/tools.ts (which reads these via toolPrompt). 
These
+# strings are part of the provider prompt-cache prefix — keep them byte-stable.
+
+list_pod_containers:
+  description: >-
+    List the containers of a Kubernetes pod so you can pick which one to read 
logs from. `serviceInstanceId` is the pod's ServiceInstance id (from 
kb_resolve_scope_drill(serviceId, 'instance')). On-demand pod logs are 
OAP-gated (off by default) — if unavailable, this says so.
+  params:
+    serviceInstanceId: >-
+      the pod's ServiceInstance id (from kb_resolve_scope_drill)
+
+fetch_pod_logs:
+  description: >-
+    Read a Kubernetes pod container's LIVE logs (on-demand, pulled from the 
K8s API through OAP, never stored) and show the fetched lines to the operator 
inline (a read-only result, not a live tail). Use this to find the ERROR STACK 
once metrics point at a pod, and as the main move for a middleware LEAF (a 
DB/MQ/cache has no downstream to walk — read its logs instead). 
`serviceInstanceId` is the pod's ServiceInstance id (from 
kb_resolve_scope_drill). Omit `container` to auto-pick the fir [...]
+  params:
+    layer: >-
+      OAP layer key of the pod, e.g. K8S_SERVICE
+    serviceInstanceId: >-
+      the pod's ServiceInstance id (from kb_resolve_scope_drill)
+    container: >-
+      container name (from list_pod_containers); omit to auto-pick the first
+    service: >-
+      service name (for the block heading)
+    pod: >-
+      pod / instance name (for the block heading)
+    windowSeconds: >-
+      trailing look-back seconds (default 300, max 1800). On-demand logs are 
collected over time — use a WIDE window (e.g. 1800) and widen before concluding 
a pod is silent; a narrow window on a quiet pod returns nothing.
+    keywordsOfContent: >-
+      include only lines matching these (OAP full-line regex) — use ONLY for a 
KNOWN error signature, never as a first-pass guess; fetch unfiltered otherwise
+    excludingKeywordsOfContent: >-
+      drop lines matching these (OAP full-line regex)
diff --git a/apps/bff/src/ai/resources/tools/metric-catalog.yaml 
b/apps/bff/src/ai/resources/tools/metric-catalog.yaml
new file mode 100644
index 0000000..f31f764
--- /dev/null
+++ b/apps/bff/src/ai/resources/tools/metric-catalog.yaml
@@ -0,0 +1,71 @@
+# 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 `metric-catalog` 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/metric-catalog/tools.ts (which reads these via 
toolPrompt). These
+# strings are part of the provider prompt-cache prefix — keep them byte-stable.
+
+kb_layer_capabilities:
+  description: >-
+    What a layer offers, from its template — load this to orient on a layer 
BEFORE reading its metrics/relations. Returns: the entity VOCABULARY (what 
Service/Instance/Endpoint MEAN here, e.g. Cluster/Node/Pod/Sidecar, plus any 
service-name grouping like namespace); which COMPONENTS the layer carries 
(topology/deployment/endpointDependency/traces/logs/…); the trace SOURCE 
(native | zipkin | both — use THIS to pick show_traces vs show_zipkin_traces, 
don't guess); per-scope metric COUNTS ( [...]
+  params:
+    layer: >-
+      OAP layer key, e.g. GENERAL, MESH, K8S_SERVICE
+
+kb_browse_catalog:
+  description: >-
+    List the metrics available on a layer dashboard page: the curated title, 
ready MQE expression(s), unit, widget type, and (when present) a human 
explanation. ALWAYS use these MQE expressions verbatim rather than inventing 
metric names — they are scope-correct. scope is one of service | instance | 
endpoint (default service).
+  params:
+    layer: >-
+      OAP layer key, e.g. GENERAL, MESH, K8S_SERVICE
+    scope: >-
+      service (default) | instance | endpoint
+
+kb_resolve_scope_drill:
+  description: >-
+    Drill DOWN from a service to its child entities so you can re-query the 
same metric at a finer scope. Returns the child instance/endpoint ids+names. 
Entity-scope is load-bearing: to see a Service metric per-instance you MUST 
re-query at instance scope with the child id — there is no automatic rollup.
+  params:
+    serviceId: >-
+      OAP service id (from list_services)
+    toScope: >-
+      drill target scope
+    keyword: >-
+      endpoint name filter (endpoint drill only)
+
+kb_describe_metric:
+  description: >-
+    Get the full catalog detail for ONE metric on a (layer, scope) page — 
title, MQE, unit, explanation, widget type — looked up by its widget id (from 
kb_browse_catalog) or a metric id it contains.
+  params:
+    id: >-
+      widget id (from kb_browse_catalog) or a raw metric id
+
+kb_search_metrics:
+  description: >-
+    Search the metric catalog ACROSS all layers by keyword (matches a metric 
title, id, or explanation) at a given scope (default service). Use when you do 
not already know which layer exposes the metric you need.
+  params:
+    keyword: >-
+      e.g. "gc", "heap", "latency", "cpu"
+
+kb_resolve_hierarchy:
+  description: >-
+    Resolve a service's CROSS-LAYER hierarchy — the same entity projected into 
other layers (a K8S_SERVICE linked DOWN to its backing PostgreSQL / MongoDB 
infra layer, or a MESH service to its GENERAL / K8S mirror). Use to continue 
root-cause ACROSS a layer boundary: when the root service depends on an 
infra/database layer, follow the hierarchy there and check that layer's metrics 
(memory, disk, connections). Distinct from kb_resolve_scope_drill (which stays 
within a layer, drilling to i [...]
+  params:
+    serviceId: >-
+      OAP service id (from list_services)
+    layer: >-
+      the service's current layer, e.g. K8S_SERVICE
diff --git a/apps/bff/src/ai/resources/tools/rca.yaml 
b/apps/bff/src/ai/resources/tools/rca.yaml
new file mode 100644
index 0000000..6c29b09
--- /dev/null
+++ b/apps/bff/src/ai/resources/tools/rca.yaml
@@ -0,0 +1,32 @@
+# 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 `rca` 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/rca/tools.ts (which reads these via toolPrompt). These
+# strings are part of the provider prompt-cache prefix — keep them byte-stable.
+
+list_playbooks:
+  description: >-
+    List the available root-cause investigation playbooks (id + title + 
when-to-use). Call this at the START of any "why is X wrong / what is the root 
cause" investigation, then get_playbook the best-matching one and FOLLOW it.
+
+get_playbook:
+  description: >-
+    Fetch a root-cause playbook by id (from list_playbooks) — a step-by-step 
method that sequences the data tools. Load the matching playbook before 
investigating, then follow its steps in order.
+  params:
+    id: >-
+      playbook id, e.g. root-cause, latency, errors-sla, saturation, 
middleware-remote, k8s, mesh
diff --git a/apps/bff/src/ai/resources/tools/telemetry.yaml 
b/apps/bff/src/ai/resources/tools/telemetry.yaml
new file mode 100644
index 0000000..8e63fdf
--- /dev/null
+++ b/apps/bff/src/ai/resources/tools/telemetry.yaml
@@ -0,0 +1,30 @@
+# 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 `telemetry` 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/telemetry/tools.ts (which reads these via toolPrompt). 
These
+# strings are part of the provider prompt-cache prefix — keep them byte-stable.
+
+list_alarms:
+  description: >-
+    List recently firing alarms — SkyWalking's health signal. Start here for 
'what is unhealthy / abnormal?': each alarm names the alarmed entity, scope, 
and the rule message. Active (unrecovered) alarms are listed first. Then drill 
into the alarmed entity's metrics to explain the anomaly. Optionally filter by 
layer or a keyword.
+  params:
+    layer: >-
+      OAP layer key filter, e.g. GENERAL
+    keyword: >-
+      alarm-name / entity keyword filter
diff --git a/apps/bff/src/ai/resources/tools/triggers.yaml 
b/apps/bff/src/ai/resources/tools/triggers.yaml
new file mode 100644
index 0000000..639713c
--- /dev/null
+++ b/apps/bff/src/ai/resources/tools/triggers.yaml
@@ -0,0 +1,42 @@
+# 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 `triggers` 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/triggers/tools.ts (which reads these via toolPrompt). 
These
+# strings are part of the provider prompt-cache prefix — keep them byte-stable.
+
+propose_profiling:
+  description: >-
+    PROPOSE a trace-profiling task (does NOT start it — the user approves a 
decision card first). Use only when metrics + traces cannot localise the cause 
and profiling would confirm a specific hypothesis (a hot method, lock 
contention, a slow call). You MUST supply cause (what you found), rationale 
(why profiling), and expectation (what it should reveal) — the user reads these 
before approving. Provide the service and, when possible, the specific endpoint 
to profile.
+  params:
+    layer: >-
+      OAP layer key, e.g. GENERAL
+    serviceId: >-
+      OAP service id (from list_services)
+    service: >-
+      service NAME
+    endpoint: >-
+      endpoint (API) name to profile, when known
+    durationMinutes: >-
+      collection window in minutes (e.g. 5)
+    cause: >-
+      the analyzed cause SO FAR — what the investigation found
+    rationale: >-
+      why profiling is the right next step
+    expectation: >-
+      what the profile is expected to reveal or confirm
diff --git a/apps/bff/src/ai/skill/kubernetes/tools.ts 
b/apps/bff/src/ai/skill/kubernetes/tools.ts
index b746441..0e00553 100644
--- a/apps/bff/src/ai/skill/kubernetes/tools.ts
+++ b/apps/bff/src/ai/skill/kubernetes/tools.ts
@@ -43,6 +43,7 @@ import type { AiRequestContext } from '../../context.js';
 import type { PodLogLine } from '../../types.js';
 import { graphqlPost } from '../../../client/graphql.js';
 import { fmtSecond, getServerOffsetMinutes } from '../../../util/window.js';
+import { toolPrompt } from '../../resources/loader.js';
 
 const DEFAULT_WINDOW_SEC = 300; // 5m trailing look-back for the agent's read
 const MAX_WINDOW_SEC = 30 * 60; // 30m — same ceiling as the Pod Logs tab
@@ -120,6 +121,7 @@ export function kubernetesTools(ctx: AiRequestContext): 
StructuredToolInterface[
     }
   }
 
+  const listPrompt = toolPrompt('kubernetes', 'list_pod_containers');
   const list = tool(
     async ({ serviceInstanceId }): Promise<string> => {
       if (!ctx.hasVerb('logs:read')) return denied();
@@ -137,14 +139,14 @@ export function kubernetesTools(ctx: AiRequestContext): 
StructuredToolInterface[
     },
     {
       name: 'list_pod_containers',
-      description:
-        "List the containers of a Kubernetes pod so you can pick which one to 
read logs from. `serviceInstanceId` is the pod's ServiceInstance id (from 
kb_resolve_scope_drill(serviceId, 'instance')). On-demand pod logs are 
OAP-gated (off by default) — if unavailable, this says so.",
+      description: listPrompt.description,
       schema: z.object({
-        serviceInstanceId: z.string().describe("the pod's ServiceInstance id 
(from kb_resolve_scope_drill)"),
+        serviceInstanceId: 
z.string().describe(listPrompt.p('serviceInstanceId')),
       }),
     },
   );
 
+  const fetchPrompt = toolPrompt('kubernetes', 'fetch_pod_logs');
   const fetchLogs = tool(
     async (input): Promise<string> => {
       if (!ctx.hasVerb('logs:read')) return denied();
@@ -236,33 +238,22 @@ export function kubernetesTools(ctx: AiRequestContext): 
StructuredToolInterface[
     },
     {
       name: 'fetch_pod_logs',
-      description:
-        'Read a Kubernetes pod container\'s LIVE logs (on-demand, pulled from 
the K8s API through OAP, never stored) and show the fetched lines to the 
operator inline (a read-only result, not a live tail). Use this to find the 
ERROR STACK once metrics point at a pod, and as the main move for a middleware 
LEAF (a DB/MQ/cache has no downstream to walk — read its logs instead). 
`serviceInstanceId` is the pod\'s ServiceInstance id (from 
kb_resolve_scope_drill). Omit `container` to auto-pick  [...]
+      description: fetchPrompt.description,
       schema: z.object({
-        layer: z.string().describe('OAP layer key of the pod, e.g. 
K8S_SERVICE'),
-        serviceInstanceId: z.string().describe("the pod's ServiceInstance id 
(from kb_resolve_scope_drill)"),
-        container: z
-          .string()
-          .optional()
-          .describe('container name (from list_pod_containers); omit to 
auto-pick the first'),
-        service: z.string().optional().describe('service name (for the block 
heading)'),
-        pod: z.string().optional().describe('pod / instance name (for the 
block heading)'),
-        windowSeconds: z
-          .number()
-          .int()
-          .positive()
-          .optional()
-          .describe(
-            'trailing look-back seconds (default 300, max 1800). On-demand 
logs are collected over time — use a WIDE window (e.g. 1800) and widen before 
concluding a pod is silent; a narrow window on a quiet pod returns nothing.',
-          ),
+        layer: z.string().describe(fetchPrompt.p('layer')),
+        serviceInstanceId: 
z.string().describe(fetchPrompt.p('serviceInstanceId')),
+        container: z.string().optional().describe(fetchPrompt.p('container')),
+        service: z.string().optional().describe(fetchPrompt.p('service')),
+        pod: z.string().optional().describe(fetchPrompt.p('pod')),
+        windowSeconds: 
z.number().int().positive().optional().describe(fetchPrompt.p('windowSeconds')),
         keywordsOfContent: z
           .array(z.string().min(1))
           .optional()
-          .describe('include only lines matching these (OAP full-line regex) — 
use ONLY for a KNOWN error signature, never as a first-pass guess; fetch 
unfiltered otherwise'),
+          .describe(fetchPrompt.p('keywordsOfContent')),
         excludingKeywordsOfContent: z
           .array(z.string().min(1))
           .optional()
-          .describe('drop lines matching these (OAP full-line regex)'),
+          .describe(fetchPrompt.p('excludingKeywordsOfContent')),
       }),
     },
   );
diff --git a/apps/bff/src/ai/skill/metric-catalog/tools.ts 
b/apps/bff/src/ai/skill/metric-catalog/tools.ts
index 878b4df..c0401d6 100644
--- a/apps/bff/src/ai/skill/metric-catalog/tools.ts
+++ b/apps/bff/src/ai/skill/metric-catalog/tools.ts
@@ -31,6 +31,7 @@ import { graphqlPost } from '../../../client/graphql.js';
 import { serviceLayerCatalog } from 
'../../../logic/services/service-layer-catalog.js';
 import { getServiceHierarchy } from '../../../logic/oap/hierarchy.js';
 import { layerCapabilities } from '../../../logic/layers/capabilities.js';
+import { toolPrompt } from '../../resources/loader.js';
 import { getLayerCatalog } from './catalog.js';
 
 const SEARCH_CAP = 40;
@@ -52,6 +53,7 @@ export function metricCatalogTools(ctx: AiRequestContext): 
StructuredToolInterfa
   const duration = () => ({ start: ctx.window.start, end: ctx.window.end, 
step: ctx.window.step });
   const denied = (): string => 'Permission denied: the current user lacks 
metrics:read.';
 
+  const cap0 = toolPrompt('metric-catalog', 'kb_layer_capabilities');
   const capabilities = tool(
     async ({ layer }): Promise<string> => {
       if (!ctx.hasVerb('metrics:read')) return denied();
@@ -61,12 +63,12 @@ export function metricCatalogTools(ctx: AiRequestContext): 
StructuredToolInterfa
     },
     {
       name: 'kb_layer_capabilities',
-      description:
-        "What a layer offers, from its template — load this to orient on a 
layer BEFORE reading its metrics/relations. Returns: the entity VOCABULARY 
(what Service/Instance/Endpoint MEAN here, e.g. Cluster/Node/Pod/Sidecar, plus 
any service-name grouping like namespace); which COMPONENTS the layer carries 
(topology/deployment/endpointDependency/traces/logs/…); the trace SOURCE 
(native | zipkin | both — use THIS to pick show_traces vs show_zipkin_traces, 
don't guess); per-scope metric COU [...]
-      schema: z.object({ layer: z.string().describe('OAP layer key, e.g. 
GENERAL, MESH, K8S_SERVICE') }),
+      description: cap0.description,
+      schema: z.object({ layer: z.string().describe(cap0.p('layer')) }),
     },
   );
 
+  const brw = toolPrompt('metric-catalog', 'kb_browse_catalog');
   const browse = tool(
     async ({ layer, scope }): Promise<string> => {
       if (!ctx.hasVerb('metrics:read')) return denied();
@@ -94,15 +96,15 @@ export function metricCatalogTools(ctx: AiRequestContext): 
StructuredToolInterfa
     },
     {
       name: 'kb_browse_catalog',
-      description:
-        'List the metrics available on a layer dashboard page: the curated 
title, ready MQE expression(s), unit, widget type, and (when present) a human 
explanation. ALWAYS use these MQE expressions verbatim rather than inventing 
metric names — they are scope-correct. scope is one of service | instance | 
endpoint (default service).',
+      description: brw.description,
       schema: z.object({
-        layer: z.string().describe('OAP layer key, e.g. GENERAL, MESH, 
K8S_SERVICE'),
-        scope: z.enum(SCOPES).optional().describe('service (default) | 
instance | endpoint'),
+        layer: z.string().describe(brw.p('layer')),
+        scope: z.enum(SCOPES).optional().describe(brw.p('scope')),
       }),
     },
   );
 
+  const drl = toolPrompt('metric-catalog', 'kb_resolve_scope_drill');
   const drill = tool(
     async ({ serviceId, toScope, keyword }): Promise<string> => {
       if (!ctx.hasVerb('metrics:read')) return denied();
@@ -135,16 +137,16 @@ export function metricCatalogTools(ctx: 
AiRequestContext): StructuredToolInterfa
     },
     {
       name: 'kb_resolve_scope_drill',
-      description:
-        'Drill DOWN from a service to its child entities so you can re-query 
the same metric at a finer scope. Returns the child instance/endpoint 
ids+names. Entity-scope is load-bearing: to see a Service metric per-instance 
you MUST re-query at instance scope with the child id — there is no automatic 
rollup.',
+      description: drl.description,
       schema: z.object({
-        serviceId: z.string().describe('OAP service id (from list_services)'),
-        toScope: z.enum(['instance', 'endpoint']).describe('drill target 
scope'),
-        keyword: z.string().optional().describe('endpoint name filter 
(endpoint drill only)'),
+        serviceId: z.string().describe(drl.p('serviceId')),
+        toScope: z.enum(['instance', 'endpoint']).describe(drl.p('toScope')),
+        keyword: z.string().optional().describe(drl.p('keyword')),
       }),
     },
   );
 
+  const dsc = toolPrompt('metric-catalog', 'kb_describe_metric');
   const describe = tool(
     async ({ layer, scope, id }): Promise<string> => {
       if (!ctx.hasVerb('metrics:read')) return denied();
@@ -169,16 +171,16 @@ export function metricCatalogTools(ctx: 
AiRequestContext): StructuredToolInterfa
     },
     {
       name: 'kb_describe_metric',
-      description:
-        'Get the full catalog detail for ONE metric on a (layer, scope) page — 
title, MQE, unit, explanation, widget type — looked up by its widget id (from 
kb_browse_catalog) or a metric id it contains.',
+      description: dsc.description,
       schema: z.object({
         layer: z.string(),
         scope: z.enum(SCOPES).optional(),
-        id: z.string().describe('widget id (from kb_browse_catalog) or a raw 
metric id'),
+        id: z.string().describe(dsc.p('id')),
       }),
     },
   );
 
+  const srch = toolPrompt('metric-catalog', 'kb_search_metrics');
   const search = tool(
     async ({ keyword, scope }): Promise<string> => {
       if (!ctx.hasVerb('metrics:read')) return denied();
@@ -216,15 +218,15 @@ export function metricCatalogTools(ctx: 
AiRequestContext): StructuredToolInterfa
     },
     {
       name: 'kb_search_metrics',
-      description:
-        'Search the metric catalog ACROSS all layers by keyword (matches a 
metric title, id, or explanation) at a given scope (default service). Use when 
you do not already know which layer exposes the metric you need.',
+      description: srch.description,
       schema: z.object({
-        keyword: z.string().describe('e.g. "gc", "heap", "latency", "cpu"'),
+        keyword: z.string().describe(srch.p('keyword')),
         scope: z.enum(SCOPES).optional(),
       }),
     },
   );
 
+  const hier = toolPrompt('metric-catalog', 'kb_resolve_hierarchy');
   const hierarchy = tool(
     async ({ serviceId, layer }): Promise<string> => {
       if (!ctx.hasVerb('topology:read')) return 'Permission denied: the 
current user lacks topology:read.';
@@ -252,11 +254,10 @@ export function metricCatalogTools(ctx: 
AiRequestContext): StructuredToolInterfa
     },
     {
       name: 'kb_resolve_hierarchy',
-      description:
-        'Resolve a service\'s CROSS-LAYER hierarchy — the same entity 
projected into other layers (a K8S_SERVICE linked DOWN to its backing 
PostgreSQL / MongoDB infra layer, or a MESH service to its GENERAL / K8S 
mirror). Use to continue root-cause ACROSS a layer boundary: when the root 
service depends on an infra/database layer, follow the hierarchy there and 
check that layer\'s metrics (memory, disk, connections). Distinct from 
kb_resolve_scope_drill (which stays within a layer, drilli [...]
+      description: hier.description,
       schema: z.object({
-        serviceId: z.string().describe('OAP service id (from list_services)'),
-        layer: z.string().describe("the service's current layer, e.g. 
K8S_SERVICE"),
+        serviceId: z.string().describe(hier.p('serviceId')),
+        layer: z.string().describe(hier.p('layer')),
       }),
     },
   );
diff --git a/apps/bff/src/ai/skill/rca/tools.ts 
b/apps/bff/src/ai/skill/rca/tools.ts
index 7360e85..74d900c 100644
--- a/apps/bff/src/ai/skill/rca/tools.ts
+++ b/apps/bff/src/ai/skill/rca/tools.ts
@@ -26,6 +26,7 @@ import { tool } from '@langchain/core/tools';
 import { z } from 'zod';
 import type { StructuredToolInterface } from '@langchain/core/tools';
 import { PLAYBOOKS } from './playbooks.js';
+import { toolPrompt } from '../../resources/loader.js';
 
 export function rcaTools(): StructuredToolInterface[] {
   const list = tool(
@@ -33,12 +34,13 @@ export function rcaTools(): StructuredToolInterface[] {
       JSON.stringify(PLAYBOOKS.map((p) => ({ id: p.id, title: p.title, 
whenToUse: p.whenToUse }))),
     {
       name: 'list_playbooks',
-      description:
-        'List the available root-cause investigation playbooks (id + title + 
when-to-use). Call this at the START of any "why is X wrong / what is the root 
cause" investigation, then get_playbook the best-matching one and FOLLOW it.',
+      description: toolPrompt('rca', 'list_playbooks').description,
       schema: z.object({}),
     },
   );
 
+  const getP = toolPrompt('rca', 'get_playbook');
+
   const get = tool(
     async ({ id }): Promise<string> => {
       const p = PLAYBOOKS.find((x) => x.id === id);
@@ -47,12 +49,9 @@ export function rcaTools(): StructuredToolInterface[] {
     },
     {
       name: 'get_playbook',
-      description:
-        'Fetch a root-cause playbook by id (from list_playbooks) — a 
step-by-step method that sequences the data tools. Load the matching playbook 
before investigating, then follow its steps in order.',
+      description: getP.description,
       schema: z.object({
-        id: z
-          .string()
-          .describe('playbook id, e.g. root-cause, latency, errors-sla, 
saturation, middleware-remote, k8s, mesh'),
+        id: z.string().describe(getP.p('id')),
       }),
     },
   );
diff --git a/apps/bff/src/ai/skill/telemetry/tools.ts 
b/apps/bff/src/ai/skill/telemetry/tools.ts
index 532e4bf..27320e8 100644
--- a/apps/bff/src/ai/skill/telemetry/tools.ts
+++ b/apps/bff/src/ai/skill/telemetry/tools.ts
@@ -29,6 +29,7 @@ import type { StructuredToolInterface } from 
'@langchain/core/tools';
 import type { AiRequestContext } from '../../context.js';
 import { graphqlPost } from '../../../client/graphql.js';
 import { fmtSecond, getServerOffsetMinutes } from '../../../util/window.js';
+import { toolPrompt } from '../../resources/loader.js';
 
 const ALARM_WINDOW_MS = 3 * 60 * 60_000; // under OAP's 4h alarm cap
 
@@ -51,6 +52,7 @@ interface AlarmMsg {
 }
 
 export function telemetryTools(ctx: AiRequestContext): 
StructuredToolInterface[] {
+  const alarms = toolPrompt('telemetry', 'list_alarms');
   const listAlarms = tool(
     async ({ layer, keyword }): Promise<string> => {
       if (!ctx.hasVerb('alarms:read')) {
@@ -94,11 +96,10 @@ export function telemetryTools(ctx: AiRequestContext): 
StructuredToolInterface[]
     },
     {
       name: 'list_alarms',
-      description:
-        "List recently firing alarms — SkyWalking's health signal. Start here 
for 'what is unhealthy / abnormal?': each alarm names the alarmed entity, 
scope, and the rule message. Active (unrecovered) alarms are listed first. Then 
drill into the alarmed entity's metrics to explain the anomaly. Optionally 
filter by layer or a keyword.",
+      description: alarms.description,
       schema: z.object({
-        layer: z.string().optional().describe('OAP layer key filter, e.g. 
GENERAL'),
-        keyword: z.string().optional().describe('alarm-name / entity keyword 
filter'),
+        layer: z.string().optional().describe(alarms.p('layer')),
+        keyword: z.string().optional().describe(alarms.p('keyword')),
       }),
     },
   );
diff --git a/apps/bff/src/ai/skill/triggers/tools.ts 
b/apps/bff/src/ai/skill/triggers/tools.ts
index 67cf7cb..fed83dc 100644
--- a/apps/bff/src/ai/skill/triggers/tools.ts
+++ b/apps/bff/src/ai/skill/triggers/tools.ts
@@ -28,8 +28,10 @@ import { tool } from '@langchain/core/tools';
 import { z } from 'zod';
 import type { StructuredToolInterface } from '@langchain/core/tools';
 import type { AiRequestContext } from '../../context.js';
+import { toolPrompt } from '../../resources/loader.js';
 
 export function triggerTools(ctx: AiRequestContext): StructuredToolInterface[] 
{
+  const t = toolPrompt('triggers', 'propose_profiling');
   const propose = tool(
     async ({ layer, serviceId, service, endpoint, durationMinutes, cause, 
rationale, expectation }): Promise<string> => {
       if (!ctx.hasVerb('profile:enable')) {
@@ -51,17 +53,16 @@ export function triggerTools(ctx: AiRequestContext): 
StructuredToolInterface[] {
     },
     {
       name: 'propose_profiling',
-      description:
-        'PROPOSE a trace-profiling task (does NOT start it — the user approves 
a decision card first). Use only when metrics + traces cannot localise the 
cause and profiling would confirm a specific hypothesis (a hot method, lock 
contention, a slow call). You MUST supply cause (what you found), rationale 
(why profiling), and expectation (what it should reveal) — the user reads these 
before approving. Provide the service and, when possible, the specific endpoint 
to profile.',
+      description: t.description,
       schema: z.object({
-        layer: z.string().describe('OAP layer key, e.g. GENERAL'),
-        serviceId: z.string().describe('OAP service id (from list_services)'),
-        service: z.string().describe('service NAME'),
-        endpoint: z.string().optional().describe('endpoint (API) name to 
profile, when known'),
-        durationMinutes: z.number().int().min(1).max(15).describe('collection 
window in minutes (e.g. 5)'),
-        cause: z.string().describe('the analyzed cause SO FAR — what the 
investigation found'),
-        rationale: z.string().describe('why profiling is the right next step'),
-        expectation: z.string().describe('what the profile is expected to 
reveal or confirm'),
+        layer: z.string().describe(t.p('layer')),
+        serviceId: z.string().describe(t.p('serviceId')),
+        service: z.string().describe(t.p('service')),
+        endpoint: z.string().optional().describe(t.p('endpoint')),
+        durationMinutes: 
z.number().int().min(1).max(15).describe(t.p('durationMinutes')),
+        cause: z.string().describe(t.p('cause')),
+        rationale: z.string().describe(t.p('rationale')),
+        expectation: z.string().describe(t.p('expectation')),
       }),
     },
   );

Reply via email to