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')), }), }, );
