mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-29 16:18:55 +00:00
feat(ai): add agent planning and skill prompts
This commit is contained in:
@@ -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)" />
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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.)";
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user