feat(ai): add agent planning and skill prompts

This commit is contained in:
t8y2
2026-05-16 18:34:50 +08:00
parent 32aa7733f8
commit e9b32e4324
12 changed files with 1176 additions and 70 deletions
+56 -6
View File
@@ -6,9 +6,11 @@ import { translateBackendError } from "@/i18n/backend-errors";
import {
ArrowUp,
ArrowRightLeft,
AlertTriangle,
Bot,
Check,
ChevronRight,
CircleSlash,
Copy,
Database,
HelpCircle,
@@ -17,6 +19,7 @@ import {
MessageSquarePlus,
Replace,
Server,
ShieldCheck,
Table2,
Play,
Square,
@@ -44,7 +47,8 @@ import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import { useQueryStore } from "@/stores/queryStore";
import { useToast } from "@/composables/useToast";
import { buildAiContext, runAiStream, type AiAction } from "@/lib/ai";
import { extractFirstSqlCodeBlock, shouldAttemptAiAutoExecute } from "@/lib/aiSqlExecutionPolicy";
import { buildAiAgentPlan } from "@/lib/aiAgentPlan";
import { buildAiAgentStepItems, type AiAgentStepItem, type AiAgentStepTone } from "@/lib/aiAgentStepPresentation";
import { Marked } from "marked";
import {
aiCancelStream,
@@ -73,6 +77,7 @@ interface ChatMessage {
content: string;
reasoning?: string;
isThinking?: boolean;
agentSteps?: AiAgentStepItem[];
}
const props = defineProps<{
@@ -209,6 +214,33 @@ function appendAssistantReasoning(assistantIdx: number, delta: string) {
const expandedReasoning = ref<Set<number>>(new Set());
function agentStepIcon(tone: AiAgentStepTone) {
if (tone === "danger") return CircleSlash;
if (tone === "warning") return AlertTriangle;
if (tone === "active") return Play;
return ShieldCheck;
}
function agentStepClass(tone: AiAgentStepTone): string {
switch (tone) {
case "success":
return "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300";
case "active":
return "border-blue-500/30 bg-blue-500/10 text-blue-700 dark:text-blue-300";
case "warning":
return "border-amber-500/35 bg-amber-500/10 text-amber-700 dark:text-amber-300";
case "danger":
return "border-red-500/35 bg-red-500/10 text-red-700 dark:text-red-300";
default:
return "border-border bg-background/60 text-muted-foreground";
}
}
function agentStepTitle(step: AiAgentStepItem): string {
if (!step.titleKey) return t(step.labelKey);
return t(step.titleKey, step.titleParams || {});
}
function toggleReasoning(index: number) {
const next = new Set(expandedReasoning.value);
if (next.has(index)) {
@@ -448,7 +480,7 @@ async function send() {
scrollToBottom();
const requestedAction = activeAction.value;
const shouldAutoExecute = assistantMode.value === "agent" && shouldAttemptAiAutoExecute(displayText, requestedAction);
const requestedMode = assistantMode.value;
isGenerating.value = true;
messages.value.push({ role: "assistant", content: "" });
const assistantIdx = messages.value.length - 1;
@@ -466,6 +498,7 @@ async function send() {
{
config: settings.aiConfig,
action: activeAction.value,
mode: requestedMode,
instruction: displayText,
context,
},
@@ -484,10 +517,15 @@ async function send() {
const msg = messages.value[assistantIdx];
if (msg) msg.isThinking = false;
isGenerating.value = false;
if (shouldAutoExecute) {
const sql = extractFirstSqlCodeBlock(msg?.content || "");
if (sql) emit("requestAutoExecuteSql", sql);
}
const agentPlan = buildAiAgentPlan({
mode: requestedMode,
action: requestedAction,
instruction: displayText,
assistantContent: msg?.content || "",
connection: props.connection,
});
if (msg && requestedMode === "agent") msg.agentSteps = buildAiAgentStepItems(agentPlan);
if (agentPlan.handoffSql) emit("requestAutoExecuteSql", agentPlan.handoffSql);
activeAction.value = "generate";
currentSessionId.value = "";
persistConversation();
@@ -742,6 +780,18 @@ function formatInlineText(text: string): string {
</div>
</div>
</div>
<div v-if="msg.agentSteps?.length" class="mb-2 flex flex-wrap gap-1.5">
<span
v-for="step in msg.agentSteps"
:key="step.key"
class="inline-flex h-5 max-w-full items-center gap-1 rounded-full border px-1.5 text-[10px] font-medium"
:class="agentStepClass(step.tone)"
:title="agentStepTitle(step)"
>
<component :is="agentStepIcon(step.tone)" class="h-3 w-3 shrink-0" />
<span class="truncate">{{ t(step.labelKey) }}</span>
</span>
</div>
<template v-for="(seg, j) in parseMessage(msg.content)" :key="j">
<div v-if="seg.type === 'text'" class="ai-markdown whitespace-normal">
<div v-html="formatInlineText(seg.content)" />
+20
View File
@@ -564,6 +564,26 @@ export default {
truncated: "Context truncated",
contextSummary: "{database} · {tables} tables",
autoSqlBlocked: "The AI-generated SQL looked too risky to auto-execute. Review it manually before running.",
agentSteps: {
generated: "SQL generated",
noSql: "No SQL found",
safe: "Safety passed",
needsConfirm: "Needs confirmation",
blocked: "Blocked",
autoExecute: "Ready to run",
notRequested: "Run not requested",
skipped: "Not run",
},
agentStepTitles: {
riskCheck: "Risk check: {action} · {category} · {environment} · {reasons}",
blocked: "Execution was blocked by the risk policy. Review the SQL manually.",
requiresConfirmation: "This SQL requires confirmation before execution.",
askMode: "Ask mode does not auto-run SQL.",
unsupportedAction: "The current AI action does not auto-run SQL.",
noSql: "The response did not include an executable SQL code block.",
notRequested: "The user did not ask to run the SQL.",
skipped: "This step was not run.",
},
proxy: "Proxy",
proxyEnable: "Send AI requests through proxy",
proxyUrl: "Proxy URL",
+20
View File
@@ -550,6 +550,26 @@ export default {
truncated: "上下文已截断",
contextSummary: "{database} · {tables} 张表",
autoSqlBlocked: "AI 生成的 SQL 风险较高,已阻止自动执行,请手动检查后再运行。",
agentSteps: {
generated: "已生成 SQL",
noSql: "未找到 SQL",
safe: "安全检查通过",
needsConfirm: "需要确认",
blocked: "已阻止",
autoExecute: "准备执行",
notRequested: "未请求执行",
skipped: "未执行",
},
agentStepTitles: {
riskCheck: "风险检查:{action} · {category} · {environment} · {reasons}",
blocked: "风险策略阻止执行,请手动检查 SQL。",
requiresConfirmation: "此 SQL 需要确认后执行。",
askMode: "Ask 模式不会自动执行 SQL。",
unsupportedAction: "当前 AI 动作不会自动执行 SQL。",
noSql: "回复中没有可执行的 SQL 代码块。",
notRequested: "用户没有表达执行意图。",
skipped: "此步骤未执行。",
},
proxy: "代理",
proxyEnable: "AI 请求通过代理发送",
proxyUrl: "代理地址",
+76 -64
View File
@@ -12,9 +12,11 @@ import type {
import * as api from "@/lib/api";
import { currentLocale } from "@/i18n";
import { aiTableMentionKey, type AiTableMention } from "@/lib/aiTableMentions";
import { aiSkillForAction } from "@/lib/aiSkills";
import { isSchemaAware } from "@/lib/databaseCapabilities";
export type AiAction = "generate" | "explain" | "optimize" | "fix" | "convert" | "sampleData";
export type AiAssistantMode = "ask" | "agent";
export interface AiSchemaTable {
schema?: string;
@@ -39,41 +41,16 @@ export interface AiContext {
export interface AiRequestInput {
config: AiConfig;
action: AiAction;
mode?: AiAssistantMode;
instruction: string;
context: AiContext;
}
const ACTION_INSTRUCTIONS: Record<AiAction, { en: string; zh: string }> = {
generate: {
en: "Generate a SQL query that satisfies the user's request. Return the SQL in a ```sql code block first, followed by a brief note if needed. Use foreign key relationships from the schema to infer correct JOIN conditions.",
zh: "根据用户请求生成 SQL。先在 ```sql 代码块中返回 SQL,必要时附简短说明。利用 Schema 中的外键关系推断正确的 JOIN 条件。",
},
explain: {
en: "Explain the current SQL step by step. Point out risky operations, implicit assumptions, and potential performance issues. Reference index and foreign key info from the schema when relevant.",
zh: "逐步解释当前 SQL。指出危险操作、隐含假设和潜在性能问题。结合 Schema 中的索引和外键信息分析。",
},
optimize: {
en: "Rewrite or suggest improvements for the current SQL. Return the improved SQL in a ```sql code block first, followed by short notes explaining the changes. Use the index information in the schema to suggest index-aware optimizations (e.g., avoid full table scans, leverage existing indexes).",
zh: "重写或优化当前 SQL。先在 ```sql 代码块中返回优化后的 SQL,然后简要说明改动。利用 Schema 中的索引信息建议索引友好的优化(如避免全表扫描、利用现有索引)。",
},
fix: {
en: "Fix the current SQL using the provided error message and result context. Return the corrected SQL in a ```sql code block first, followed by a brief explanation of the root cause.",
zh: "根据报错信息和结果上下文修复当前 SQL。先在 ```sql 代码块中返回修正后的 SQL,再简要说明根因。",
},
convert: {
en: "Convert the current SQL to the target dialect requested by the user. Return the converted SQL in a ```sql code block first. Note any syntax differences or incompatibilities.",
zh: "将当前 SQL 转换为用户指定的目标方言。先在 ```sql 代码块中返回转换后的 SQL,再说明语法差异。",
},
sampleData: {
en: "Generate safe sample INSERT statements or mock data for the current schema. Do not use real production data. Return SQL in a ```sql code block.",
zh: "为当前 Schema 生成安全的示例 INSERT 语句或模拟数据。不使用真实生产数据。在 ```sql 代码块中返回 SQL。",
},
};
export async function runAiAction(input: AiRequestInput, history?: api.AiMessage[]): Promise<string> {
const isZh = currentLocale() === "zh-CN";
const systemPrompt = buildSystemPrompt(input.action, input.context);
const instruction = isZh ? ACTION_INSTRUCTIONS[input.action].zh : ACTION_INSTRUCTIONS[input.action].en;
const skill = aiSkillForAction(input.action);
const systemPrompt = buildSystemPrompt(input.action, input.context, input.mode);
const instruction = isZh ? skill.userInstruction.zh : skill.userInstruction.en;
const userPrompt = [
`Action: ${input.action}`,
instruction,
@@ -102,8 +79,9 @@ export async function runAiStream(
onReasoningDelta?: (delta: string) => void,
): Promise<void> {
const isZh = currentLocale() === "zh-CN";
const systemPrompt = buildSystemPrompt(input.action, input.context);
const instruction = isZh ? ACTION_INSTRUCTIONS[input.action].zh : ACTION_INSTRUCTIONS[input.action].en;
const skill = aiSkillForAction(input.action);
const systemPrompt = buildSystemPrompt(input.action, input.context, input.mode);
const instruction = isZh ? skill.userInstruction.zh : skill.userInstruction.en;
const userPrompt = [
`Action: ${input.action}`,
instruction,
@@ -152,7 +130,7 @@ export function extractSql(text: string): string {
return text.trim();
}
export function buildSystemPrompt(action: AiAction, context: AiContext): string {
export function buildSystemPrompt(action: AiAction, context: AiContext, mode: AiAssistantMode = "ask"): string {
const schema = formatSchema(context);
const resultPreview = context.lastResultPreview ? `\nLast result preview:\n${context.lastResultPreview}\n` : "";
const lastError = context.lastError ? `\nLast error:\n${context.lastError}\n` : "";
@@ -160,42 +138,16 @@ export function buildSystemPrompt(action: AiAction, context: AiContext): string
const isZh = currentLocale() === "zh-CN";
const lines: string[] = [
isZh ? "你是 DBX 内置的数据库助手。用中文回复。" : "You are DBX's built-in database assistant. Reply in English.",
isZh
? "精确、保守,根据当前数据库方言生成 SQL。"
: "Be precise, conservative, and adapt SQL to the active database dialect.",
isZh
? "下面的 Schema 上下文已包含表、列、索引和外键信息,直接使用即可。不要查询 information_schema 或系统表来获取结构信息。"
: "The schema context below already contains tables, columns, indexes, and foreign keys — use it directly. Do NOT query information_schema or system tables.",
isZh
? "当用户要求分析或查看某个表时,生成 SELECT 查询获取数据,而不是查询元数据。"
: "When the user asks to 'analyze' or 'look at' a table, generate a SELECT query to retrieve data, not a metadata query.",
isZh ? "不要编造 Schema 中不存在的表或列。" : "Never invent tables or columns that are not in the schema context.",
isZh
? "用户输入中的 @schema.table 或 @table 表示用户明确提到的表;这些表已优先放入 Schema 上下文。"
: "User input may contain @schema.table or @table mentions. Treat them as explicit table references; mentioned tables are prioritized in the schema context.",
isZh
? "对于 DROP、DELETE、TRUNCATE、ALTER 或没有 WHERE 的 UPDATE,简要警告并优先提供安全的 SELECT 预览。"
: "For destructive statements (DROP, DELETE, TRUNCATE, ALTER, UPDATE without WHERE), warn briefly and prefer a safer SELECT preview.",
...buildBasePromptLines(isZh),
...buildModePromptLines(mode, isZh),
...buildActionPromptLines(action, isZh),
];
if (action === "optimize") {
if (context.truncated) {
lines.push(
isZh
? "利用 Schema 中的索引信息建议优化。指出哪些查询条件可以命中索引、哪些会导致全表扫描。"
: "Use the index information in the schema to suggest optimizations. Point out which conditions hit indexes and which cause full table scans.",
);
} else if (action === "generate") {
lines.push(
isZh
? "利用外键关系推断 JOIN 条件。生成操作优先返回 SQL,避免长篇解释。"
: "Use foreign key relationships to infer JOIN conditions. Return the SQL first and avoid long explanations.",
);
} else if (action === "fix") {
lines.push(
isZh
? "仔细分析错误信息,定位根因。先返回修正后的 SQL,再简要解释。"
: "Carefully analyze the error message to identify the root cause. Return the corrected SQL first, then briefly explain.",
? "Schema 已截断:如果请求可能涉及未出现的表或字段,不要猜测。请让用户用 @table 指定相关表,或先生成只读探索查询。"
: "Schema is truncated: if the request may involve tables or columns not shown, do not guess. Ask the user to mention the relevant @table, or generate a read-only exploration query first.",
);
}
@@ -218,6 +170,66 @@ export function buildSystemPrompt(action: AiAction, context: AiContext): string
return lines.filter(Boolean).join("\n");
}
function buildBasePromptLines(isZh: boolean): string[] {
return [
isZh ? "你是 DBX 内置的数据库助手。用中文回复。" : "You are DBX's built-in database assistant. Reply in English.",
isZh
? "精确、保守,根据当前数据库方言生成 SQL。"
: "Be precise, conservative, and adapt SQL to the active database dialect.",
isZh
? "严格使用当前数据库方言;标识符引用、分页、日期函数、字符串拼接、LIMIT/TOP/OFFSET 语法必须匹配数据库类型。"
: "Strictly use the active database dialect; identifier quoting, pagination, date functions, string concatenation, and LIMIT/TOP/OFFSET syntax must match the database type.",
isZh
? "下面的 Schema 上下文已包含表、列、索引和外键信息,直接使用即可。不要查询 information_schema 或系统表来获取结构信息。"
: "The schema context below already contains tables, columns, indexes, and foreign keys — use it directly. Do NOT query information_schema or system tables.",
isZh
? "当用户要求分析或查看某个表时,生成 SELECT 查询获取数据,而不是查询元数据。"
: "When the user asks to 'analyze' or 'look at' a table, generate a SELECT query to retrieve data, not a metadata query.",
isZh ? "不要编造 Schema 中不存在的表或列。" : "Never invent tables or columns that are not in the schema context.",
isZh
? "用户输入中的 @schema.table 或 @table 表示用户明确提到的表;这些表已优先放入 Schema 上下文。"
: "User input may contain @schema.table or @table mentions. Treat them as explicit table references; mentioned tables are prioritized in the schema context.",
isZh
? "不要生成多语句 SQL,除非用户明确要求。不要在同一个回答里混合 SELECT 和写操作。"
: "Do not generate multi-statement SQL unless the user explicitly asks for it. Do not mix SELECT statements and write operations in the same answer.",
isZh
? "对于 DROP、DELETE、TRUNCATE、ALTER 或没有 WHERE 的 UPDATE,简要警告并优先提供安全的 SELECT 预览。"
: "For destructive statements (DROP, DELETE, TRUNCATE, ALTER, UPDATE without WHERE), warn briefly and prefer a safer SELECT preview.",
isZh
? "对于 UPDATE 或 DELETE,必须带 WHERE 并说明影响范围;生产库写操作只给建议,不主动建议执行。"
: "For UPDATE or DELETE, require a WHERE clause and explain the affected scope; for production writes, provide guidance but do not proactively suggest execution.",
];
}
function buildModePromptLines(mode: AiAssistantMode, isZh: boolean): string[] {
if (mode === "agent") {
return [
isZh
? "你处于 Agent 模式。用户表达查询意图时,优先生成一个可直接执行的只读 SQL。"
: "You are in Agent mode. When the user expresses query intent, prioritize one directly executable read-only SQL statement.",
isZh
? "第一个 ```sql 代码块只能包含最终推荐执行的 SQL;不要把解释性 SQL、备选 SQL、危险 SQL 放在第一个代码块。"
: "The first ```sql code block must contain only the final SQL recommended for execution; do not put explanatory SQL, alternatives, or risky SQL in the first code block.",
isZh
? "如果安全执行条件不满足,先说明原因,再给只读预览或澄清问题。"
: "If safe execution requirements are not met, explain why first, then provide a read-only preview or a clarifying question.",
];
}
return [
isZh
? "你处于 Ask 模式。只生成 SQL 和说明,不要暗示已经执行或即将自动执行。"
: "You are in Ask mode. Generate SQL and explanations only; do not imply that anything has run or will auto-run.",
];
}
function buildActionPromptLines(action: AiAction, isZh: boolean): string[] {
const skill = aiSkillForAction(action);
return isZh
? [...skill.systemRules.zh, ...skill.outputContract.zh]
: [...skill.systemRules.en, ...skill.outputContract.en];
}
function formatSchema(context: AiContext): string {
if (!context.tables.length) return "(No table schema loaded.)";
+95
View File
@@ -0,0 +1,95 @@
import type { AiAction, AiAssistantMode } from "@/lib/ai";
import {
classifyAiSqlExecution,
shouldAttemptAiAutoExecute,
stripAiSqlComments,
type AiSqlExecutionDecision,
} from "@/lib/aiSqlExecutionPolicy";
import type { ConnectionConfig } from "@/types/database";
export type AiAgentStep =
| { kind: "generate_sql"; status: "done"; sql: string }
| { kind: "generate_sql"; status: "skipped"; reason: "no_sql" }
| ({
kind: "risk_check";
status: "done";
} & AiSqlExecutionDecision)
| { kind: "execute_sql"; status: "pending"; sql: string }
| {
kind: "execute_sql";
status: "skipped";
reason:
| "ask_mode"
| "no_sql"
| "no_execution_intent"
| "unsupported_action"
| "blocked_by_policy"
| "requires_confirmation";
};
export interface AiAgentPlanInput {
mode: AiAssistantMode;
action: AiAction;
instruction: string;
assistantContent: string;
connection?: ConnectionConfig;
}
export interface AiAgentPlan {
steps: AiAgentStep[];
executableSql?: string;
handoffSql?: string;
}
export function buildAiAgentPlan(input: AiAgentPlanInput): AiAgentPlan {
const sql = extractFirstExecutableSqlCodeBlock(input.assistantContent);
if (!sql) {
return {
steps: [
{ kind: "generate_sql", status: "skipped", reason: "no_sql" },
{ kind: "execute_sql", status: "skipped", reason: "no_sql" },
],
};
}
const steps: AiAgentStep[] = [{ kind: "generate_sql", status: "done", sql }];
if (input.mode !== "agent") {
steps.push({ kind: "execute_sql", status: "skipped", reason: "ask_mode" });
return { steps };
}
if (input.action !== "generate") {
steps.push({ kind: "execute_sql", status: "skipped", reason: "unsupported_action" });
return { steps };
}
if (!shouldAttemptAiAutoExecute(input.instruction, input.action)) {
steps.push({ kind: "execute_sql", status: "skipped", reason: "no_execution_intent" });
return { steps };
}
const decision = classifyAiSqlExecution(sql, input.connection);
steps.push({ kind: "risk_check", status: "done", ...decision });
if (decision.action === "auto_execute") {
steps.push({ kind: "execute_sql", status: "pending", sql });
return { steps, executableSql: sql, handoffSql: sql };
}
steps.push({
kind: "execute_sql",
status: "skipped",
reason: decision.action === "block" ? "blocked_by_policy" : "requires_confirmation",
});
return { steps, handoffSql: sql };
}
function extractFirstExecutableSqlCodeBlock(content: string): string | undefined {
const blocks = content.matchAll(/```(?:sql|mysql|postgresql|sqlite|tsql|clickhouse)?\s*\n([\s\S]*?)```/gi);
for (const block of blocks) {
const sql = block[1]?.trim();
if (sql && stripAiSqlComments(sql).trim()) return sql;
}
return undefined;
}
+80
View File
@@ -0,0 +1,80 @@
import type { AiAgentPlan, AiAgentStep } from "@/lib/aiAgentPlan";
export type AiAgentStepTone = "success" | "active" | "warning" | "danger" | "muted";
export interface AiAgentStepItem {
key: string;
labelKey: string;
tone: AiAgentStepTone;
titleKey?: string;
titleParams?: Record<string, string>;
}
export function buildAiAgentStepItems(plan: AiAgentPlan): AiAgentStepItem[] {
return plan.steps.map(presentStep);
}
function presentStep(step: AiAgentStep): AiAgentStepItem {
if (step.kind === "generate_sql") {
if (step.status === "done") {
return { key: "generated", labelKey: "ai.agentSteps.generated", tone: "success" };
}
return { key: "noSql", labelKey: "ai.agentSteps.noSql", tone: "muted" };
}
if (step.kind === "risk_check") {
const title = {
titleKey: "ai.agentStepTitles.riskCheck",
titleParams: {
action: step.action,
category: step.category,
environment: step.environment,
reasons: step.reasons.length ? step.reasons.join(", ") : "-",
},
};
if (step.action === "auto_execute") {
return { key: "safe", labelKey: "ai.agentSteps.safe", tone: "success", ...title };
}
if (step.action === "confirm") {
return { key: "needsConfirm", labelKey: "ai.agentSteps.needsConfirm", tone: "warning", ...title };
}
return { key: "blocked", labelKey: "ai.agentSteps.blocked", tone: "danger", ...title };
}
if (step.status === "pending") {
return { key: "autoExecute", labelKey: "ai.agentSteps.autoExecute", tone: "active" };
}
if (step.reason === "no_execution_intent") {
return {
key: "notRequested",
labelKey: "ai.agentSteps.notRequested",
titleKey: "ai.agentStepTitles.notRequested",
tone: "muted",
};
}
return {
key: "skipped",
labelKey: "ai.agentSteps.skipped",
titleKey: skippedTitleKey(step.reason),
tone: "muted",
};
}
function skippedTitleKey(reason: string): string {
switch (reason) {
case "blocked_by_policy":
return "ai.agentStepTitles.blocked";
case "requires_confirmation":
return "ai.agentStepTitles.requiresConfirmation";
case "ask_mode":
return "ai.agentStepTitles.askMode";
case "unsupported_action":
return "ai.agentStepTitles.unsupportedAction";
case "no_sql":
return "ai.agentStepTitles.noSql";
default:
return "ai.agentStepTitles.skipped";
}
}
+189
View File
@@ -0,0 +1,189 @@
import type { AiAction } from "@/lib/ai";
export type AiSkillRiskPolicy = "readonly" | "readonly_preferred" | "confirmed_write" | "sample_write";
export type AiSkillContextNeed =
| "currentSql"
| "schema"
| "indexes"
| "foreignKeys"
| "lastError"
| "lastResultPreview"
| "databaseDialect";
export interface LocalizedAiSkillText {
en: string;
zh: string;
}
export interface LocalizedAiSkillLines {
en: string[];
zh: string[];
}
export interface AiSkillDefinition {
id: string;
action: AiAction;
title: LocalizedAiSkillText;
riskPolicy: AiSkillRiskPolicy;
contextNeeds: AiSkillContextNeed[];
userInstruction: LocalizedAiSkillText;
systemRules: LocalizedAiSkillLines;
outputContract: LocalizedAiSkillLines;
}
export const AI_SKILL_DEFINITIONS: AiSkillDefinition[] = [
{
id: "generate_sql",
action: "generate",
title: {
en: "Generate SQL",
zh: "生成 SQL",
},
riskPolicy: "readonly_preferred",
contextNeeds: ["schema", "indexes", "foreignKeys", "databaseDialect"],
userInstruction: {
en: "Generate a SQL query that satisfies the user's request. Return the SQL in a ```sql code block first, followed by a brief note if needed. Use foreign key relationships from the schema to infer correct JOIN conditions.",
zh: "根据用户请求生成 SQL。先在 ```sql 代码块中返回 SQL,必要时附简短说明。利用 Schema 中的外键关系推断正确的 JOIN 条件。",
},
systemRules: {
en: ["Use foreign key relationships to infer JOIN conditions. Return the SQL first and avoid long explanations."],
zh: ["利用外键关系推断 JOIN 条件。生成操作优先返回 SQL,避免长篇解释。"],
},
outputContract: {
en: [
"Output format: put only the final recommended SQL in the first ```sql code block; add at most 3 practical notes after it. If required information is missing, ask one clarifying question first.",
],
zh: ["输出格式:第一个 ```sql 代码块只放最终推荐 SQL;SQL 后最多 3 条实用说明。信息不足时先提出一个澄清问题。"],
},
},
{
id: "explain_sql",
action: "explain",
title: {
en: "Explain SQL",
zh: "解释 SQL",
},
riskPolicy: "readonly",
contextNeeds: ["currentSql", "schema", "indexes", "foreignKeys", "lastResultPreview"],
userInstruction: {
en: "Explain the current SQL step by step. Point out risky operations, implicit assumptions, and potential performance issues. Reference index and foreign key info from the schema when relevant.",
zh: "逐步解释当前 SQL。指出危险操作、隐含假设和潜在性能问题。结合 Schema 中的索引和外键信息分析。",
},
systemRules: {
en: [
"Explain what the SQL does without changing it. Reference schema, indexes, foreign keys, result preview, and risky assumptions when relevant.",
],
zh: ["解释当前 SQL 的作用,不要改写 SQL。必要时结合 Schema、索引、外键、结果预览和风险假设说明。"],
},
outputContract: {
en: [
"Output format: summarize the SQL purpose first, then explain execution logic, risks, and performance notes step by step.",
],
zh: ["输出格式:先概括 SQL 目的,再按步骤解释执行逻辑、风险点和性能注意事项。"],
},
},
{
id: "optimize_sql",
action: "optimize",
title: {
en: "Optimize SQL",
zh: "优化 SQL",
},
riskPolicy: "readonly",
contextNeeds: ["currentSql", "schema", "indexes", "foreignKeys"],
userInstruction: {
en: "Rewrite or suggest improvements for the current SQL. Return the improved SQL in a ```sql code block first, followed by short notes explaining the changes. Use the index information in the schema to suggest index-aware optimizations (e.g., avoid full table scans, leverage existing indexes).",
zh: "重写或优化当前 SQL。先在 ```sql 代码块中返回优化后的 SQL,然后简要说明改动。利用 Schema 中的索引信息建议索引友好的优化(如避免全表扫描、利用现有索引)。",
},
systemRules: {
en: [
"Use the index information in the schema to suggest optimizations. Point out which conditions hit indexes and which cause full table scans.",
],
zh: ["利用 Schema 中的索引信息建议优化。指出哪些查询条件可以命中索引、哪些会导致全表扫描。"],
},
outputContract: {
en: ["Output format: provide the optimized SQL first, then explain the key changes in at most 3 notes."],
zh: ["输出格式:先给优化后的 SQL,再用最多 3 条说明解释关键改动。"],
},
},
{
id: "fix_sql",
action: "fix",
title: {
en: "Fix SQL",
zh: "修复 SQL",
},
riskPolicy: "readonly_preferred",
contextNeeds: ["currentSql", "schema", "lastError", "lastResultPreview", "databaseDialect"],
userInstruction: {
en: "Fix the current SQL using the provided error message and result context. Return the corrected SQL in a ```sql code block first, followed by a brief explanation of the root cause.",
zh: "根据报错信息和结果上下文修复当前 SQL。先在 ```sql 代码块中返回修正后的 SQL,再简要说明根因。",
},
systemRules: {
en: [
"Carefully analyze the error message to identify the root cause. Return the corrected SQL first, then briefly explain.",
],
zh: ["仔细分析错误信息,定位根因。先返回修正后的 SQL,再简要解释。"],
},
outputContract: {
en: ["Output format: corrected SQL, error cause, change notes."],
zh: ["输出格式:修复后的 SQL、错误原因、改动说明。"],
},
},
{
id: "convert_sql",
action: "convert",
title: {
en: "Convert SQL Dialect",
zh: "转换 SQL 方言",
},
riskPolicy: "readonly_preferred",
contextNeeds: ["currentSql", "schema", "databaseDialect"],
userInstruction: {
en: "Convert the current SQL to the target dialect requested by the user. Return the converted SQL in a ```sql code block first. Note any syntax differences or incompatibilities.",
zh: "将当前 SQL 转换为用户指定的目标方言。先在 ```sql 代码块中返回转换后的 SQL,再说明语法差异。",
},
systemRules: {
en: [
"Convert only to the target dialect requested by the user. Preserve the query intent and call out syntax that cannot be converted safely.",
],
zh: ["只转换到用户指定的目标方言。保持查询意图,并指出无法安全转换的语法。"],
},
outputContract: {
en: [
"Output format: provide the converted SQL first, then note important target-dialect syntax differences or incompatibilities.",
],
zh: ["输出格式:先给转换后的 SQL,再说明目标方言下的重要语法差异或不兼容点。"],
},
},
{
id: "sample_data",
action: "sampleData",
title: {
en: "Generate Sample Data",
zh: "生成样例数据",
},
riskPolicy: "sample_write",
contextNeeds: ["schema", "databaseDialect"],
userInstruction: {
en: "Generate safe sample INSERT statements or mock data for the current schema. Do not use real production data. Return SQL in a ```sql code block.",
zh: "为当前 Schema 生成安全的示例 INSERT 语句或模拟数据。不使用真实生产数据。在 ```sql 代码块中返回 SQL。",
},
systemRules: {
en: [
"Generate mock data only. Do not use or imply real production data, credentials, personal data, or secrets.",
],
zh: ["只生成模拟数据。不要使用或暗示真实生产数据、凭据、个人数据或密钥。"],
},
outputContract: {
en: ["Output format: provide safe sample SQL first, then explain which values are mock data."],
zh: ["输出格式:先给安全的示例 SQL,再说明哪些值是模拟数据。"],
},
},
];
export function aiSkillForAction(action: AiAction): AiSkillDefinition {
const skill = AI_SKILL_DEFINITIONS.find((item) => item.action === action);
if (!skill) throw new Error(`Missing AI skill definition for action: ${action}`);
return skill;
}