feat(ai): add general action mode
This commit is contained in:
parent
dafa3e0f79
commit
ac4b98c97c
|
|
@ -52,7 +52,7 @@ import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
|||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useNavigationTargets } from "@/composables/useNavigationTargets";
|
||||
import { buildAiContext, runAgentStream, isVectorDbType, defaultActionForMode, isValidActionForMode, type AiAction, type AiAssistantMode, type AiSqlFileContext } from "@/lib/ai/ai";
|
||||
import { buildAiContext, runAgentStream, isVectorDbType, isValidActionForMode, defaultActionForMode, type AiAction, type AiAssistantMode, type AiSqlFileContext } from "@/lib/ai/ai";
|
||||
import { formatAiModelOption } from "@/lib/ai/aiModelPresentation";
|
||||
import type { AgentEvent } from "@/lib/backend/tauri";
|
||||
import { buildAiAgentPlan } from "@/lib/ai/aiAgentPlan";
|
||||
|
|
@ -132,7 +132,7 @@ const prompt = ref("");
|
|||
const messages = ref<ChatMessage[]>([]);
|
||||
const isGenerating = ref(false);
|
||||
const scrollRef = ref<InstanceType<typeof ScrollArea> | null>(null);
|
||||
const activeAction = ref<AiAction>("generate");
|
||||
const activeAction = ref<AiAction>("general");
|
||||
const assistantMode = ref<"ask" | "agent">("ask");
|
||||
const currentSessionId = ref("");
|
||||
const conversationId = ref("");
|
||||
|
|
@ -335,6 +335,16 @@ const selectedSqlFileMentions = ref<AiSqlFileMention[]>([]);
|
|||
let mentionTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let mentionRequestId = 0;
|
||||
|
||||
// Slash command menu
|
||||
const commandOpen = ref(false);
|
||||
const commandSelectedIndex = ref(0);
|
||||
const commandStart = ref(0);
|
||||
|
||||
const filteredCommands = computed(() => {
|
||||
const query = prompt.value.slice(commandStart.value + 1).toLowerCase();
|
||||
return actionButtons.value.filter((cmd) => cmd.action.toLowerCase().includes(query) || t(cmd.key).toLowerCase().includes(query));
|
||||
});
|
||||
|
||||
const AI_SQL_FILE_MENTION_CANDIDATE_LIMIT = 50;
|
||||
const AI_SQL_FILE_CONTEXT_MAX_CHARS = 12_000;
|
||||
|
||||
|
|
@ -347,6 +357,7 @@ interface AiActionButton {
|
|||
|
||||
/** Ask-mode actions: SQL-producing, never auto-run. */
|
||||
const askActionButtons: AiActionButton[] = [
|
||||
{ action: "general", icon: MessageSquarePlus, key: "ai.actions.general" },
|
||||
{ action: "generate", icon: Wand2, key: "ai.actions.generate" },
|
||||
{ action: "explain", icon: HelpCircle, key: "ai.actions.explain" },
|
||||
{ action: "optimize", icon: Zap, key: "ai.actions.optimize" },
|
||||
|
|
@ -357,6 +368,7 @@ const askActionButtons: AiActionButton[] = [
|
|||
|
||||
/** Agent-mode actions: task-oriented, drive tool use and real results. */
|
||||
const agentActionButtons: AiActionButton[] = [
|
||||
{ action: "general", icon: MessageSquarePlus, key: "ai.actions.general" },
|
||||
{ action: "query", icon: Search, key: "ai.actions.query" },
|
||||
{ action: "exploreSchema", icon: Table2, key: "ai.actions.exploreSchema" },
|
||||
{ action: "executeAndExplain", icon: Play, key: "ai.actions.executeAndExplain" },
|
||||
|
|
@ -366,18 +378,15 @@ const agentActionButtons: AiActionButton[] = [
|
|||
|
||||
const actionButtons = computed<AiActionButton[]>(() => (assistantMode.value === "agent" ? agentActionButtons : askActionButtons));
|
||||
|
||||
// Vector DBs hide the action menu and only expose collection tools (list_collections /
|
||||
// browse_collection), never SQL tools. Keep their action at `generate` so the task contract
|
||||
// doesn't tell the LLM to call execute_query / produce SQL — neither applies to vector stores.
|
||||
// Vector DBs hide the action menu and only expose collection tools.
|
||||
// Keep their action at `generate` so the task contract doesn't tell the LLM to call execute_query.
|
||||
function resolveDefaultAction(mode: AiAssistantMode): AiAction {
|
||||
if (props.connection && isVectorDbType(props.connection.db_type)) return "generate";
|
||||
return defaultActionForMode(mode);
|
||||
}
|
||||
|
||||
// Switching mode is a deliberate context change: land on that mode's default action so the
|
||||
// menu and behavior match the new intent (Ask → generate, Agent → query). The shared
|
||||
// `generate` action is not carried across because its label/semantics differ per mode
|
||||
// ("生成 SQL" vs "生成但不执行").
|
||||
// menu and behavior match the new intent. The shared `general` action is the default.
|
||||
//
|
||||
// `triggerAction` may set the action itself after programmatically switching mode (e.g. "Fix
|
||||
// with AI" invoked from Agent mode); `suppressModeActionReset` tells this watch to skip the
|
||||
|
|
@ -391,6 +400,18 @@ watch(assistantMode, (mode) => {
|
|||
activeAction.value = resolveDefaultAction(mode);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.connection?.db_type,
|
||||
() => {
|
||||
// Vector DBs hide the action picker, so keep the hidden action aligned with
|
||||
// the collection-oriented prompt contract on initial render and connection changes.
|
||||
if (props.connection && isVectorDbType(props.connection.db_type)) {
|
||||
activeAction.value = "generate";
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function selectAction(action: AiAction) {
|
||||
activeAction.value = action;
|
||||
if (action === "fix" && props.tab?.result) {
|
||||
|
|
@ -1137,6 +1158,23 @@ function scrollMentionSelectedIntoView() {
|
|||
|
||||
function refreshMentionState() {
|
||||
clearTimeout(mentionTimer);
|
||||
|
||||
// 优先检测斜杠命令(仅在输入内容为空时触发)
|
||||
const textarea = promptTextareaRef.value;
|
||||
const cursor = textarea?.selectionStart ?? prompt.value.length;
|
||||
const beforeCursor = prompt.value.slice(0, cursor);
|
||||
const slashMatch = /^\/([^\s]*)$/.exec(beforeCursor.trimStart());
|
||||
|
||||
if (slashMatch) {
|
||||
mentionOpen.value = false;
|
||||
commandOpen.value = true;
|
||||
commandStart.value = beforeCursor.length - slashMatch[1].length - 1;
|
||||
commandSelectedIndex.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
commandOpen.value = false;
|
||||
|
||||
const mention = activeMentionAtCursor();
|
||||
if (!mention || !props.connection || !props.tab?.database) {
|
||||
mentionOpen.value = false;
|
||||
|
|
@ -1155,6 +1193,21 @@ function onPromptKeyup(event: KeyboardEvent) {
|
|||
refreshMentionState();
|
||||
}
|
||||
|
||||
function selectCommand(command: AiActionButton) {
|
||||
const before = prompt.value.slice(0, commandStart.value);
|
||||
const after = prompt.value.slice(promptTextareaRef.value?.selectionStart ?? prompt.value.length);
|
||||
prompt.value = `${before}${after}`.replace(/\s{2,}/g, " ").trim();
|
||||
commandOpen.value = false;
|
||||
activeAction.value = command.action;
|
||||
nextTick(() => {
|
||||
const textarea = promptTextareaRef.value;
|
||||
if (textarea) {
|
||||
textarea.selectionStart = textarea.selectionEnd = before.length;
|
||||
textarea.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function insertMention(candidate: AiMentionCandidate) {
|
||||
const textarea = promptTextareaRef.value;
|
||||
const cursor = textarea?.selectionStart ?? prompt.value.length;
|
||||
|
|
@ -1173,6 +1226,30 @@ function insertMention(candidate: AiMentionCandidate) {
|
|||
function onPromptKeydown(event: KeyboardEvent) {
|
||||
if (isAiPromptImeCompositionEvent(event, promptCompositionActive.value)) return;
|
||||
|
||||
// 斜杠命令菜单键盘导航
|
||||
if (commandOpen.value) {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
commandSelectedIndex.value = Math.min(commandSelectedIndex.value + 1, filteredCommands.value.length - 1);
|
||||
return;
|
||||
}
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
commandSelectedIndex.value = Math.max(commandSelectedIndex.value - 1, 0);
|
||||
return;
|
||||
}
|
||||
if ((event.key === "Enter" || event.key === "Tab") && filteredCommands.value[commandSelectedIndex.value]) {
|
||||
event.preventDefault();
|
||||
selectCommand(filteredCommands.value[commandSelectedIndex.value]);
|
||||
return;
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
commandOpen.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mentionOpen.value) {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
|
|
@ -1304,7 +1381,7 @@ async function send() {
|
|||
await runAgentStream(
|
||||
{
|
||||
config: settings.aiConfig,
|
||||
action: activeAction.value,
|
||||
action: requestedAction,
|
||||
mode: requestedMode,
|
||||
instruction: modelInstruction,
|
||||
context,
|
||||
|
|
@ -1373,7 +1450,6 @@ async function send() {
|
|||
if (msg && requestedMode === "agent") msg.agentSteps = buildAiAgentStepItems(agentPlan);
|
||||
if (agentPlan.handoffSql) emit("requestAutoExecuteSql", agentPlan.handoffSql);
|
||||
}
|
||||
activeAction.value = resolveDefaultAction(assistantMode.value);
|
||||
currentSessionId.value = "";
|
||||
// Apply deferred context compaction after streaming so assistantIdx stays stable.
|
||||
// Visible chat history is kept for the user; future LLM history starts from this hidden summary.
|
||||
|
|
@ -1910,6 +1986,23 @@ async function openExternalUrl(url: string) {
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="commandOpen && filteredCommands.length" class="absolute bottom-full left-2 right-2 z-20 mb-1 max-h-56 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md">
|
||||
<div class="max-h-56 overflow-auto p-1">
|
||||
<button
|
||||
v-for="(cmd, index) in filteredCommands"
|
||||
:key="cmd.action"
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs hover:bg-muted"
|
||||
:class="{ 'bg-muted': index === commandSelectedIndex }"
|
||||
@mousedown.prevent="selectCommand(cmd)"
|
||||
@mouseenter="commandSelectedIndex = index"
|
||||
>
|
||||
<component :is="cmd.icon" class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span class="font-medium">/{{ cmd.action }}</span>
|
||||
<span class="ml-auto text-[11px] text-muted-foreground">{{ t(cmd.key) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="promptMentionChips.length" class="mb-1.5 flex flex-wrap gap-1">
|
||||
<button
|
||||
v-for="mention in promptMentionChips"
|
||||
|
|
|
|||
|
|
@ -1304,6 +1304,7 @@ export default {
|
|||
reasoningLevelHigh: "High",
|
||||
reasoningLevelHint: "Controls Codex CLI model_reasoning_effort. Default uses your Codex config.",
|
||||
actions: {
|
||||
general: "General",
|
||||
generate: "Generate SQL",
|
||||
explain: "Explain SQL",
|
||||
optimize: "Optimize SQL",
|
||||
|
|
@ -1316,6 +1317,7 @@ export default {
|
|||
generateNoExec: "Generate (no run)",
|
||||
},
|
||||
placeholders: {
|
||||
general: "Ask me anything...",
|
||||
generate: "Describe what you want to query, e.g. orders per user in the last 7 days",
|
||||
explain: "Optional: add what you want to understand",
|
||||
optimize: "Optional: add a goal, e.g. reduce full table scans",
|
||||
|
|
|
|||
|
|
@ -1250,6 +1250,7 @@ export default withEnglishFallback({
|
|||
codexCliPath: "Ruta de Codex CLI",
|
||||
codexCliPathHint: "Déjalo vacío para usar codex desde PATH. Inicia sesión por separado con codex login.",
|
||||
actions: {
|
||||
general: "General",
|
||||
generate: "Generar SQL",
|
||||
explain: "Explicar SQL",
|
||||
optimize: "Optimizar SQL",
|
||||
|
|
@ -1262,6 +1263,7 @@ export default withEnglishFallback({
|
|||
generateNoExec: "Generar (sin ejecutar)",
|
||||
},
|
||||
placeholders: {
|
||||
general: "Pregunta lo que quieras...",
|
||||
generate: "Describe lo que quieres consultar, p. ej. pedidos por usuario en los últimos 7 días",
|
||||
explain: "Opcional: indica qué quieres entender",
|
||||
optimize: "Opcional: indica un objetivo, p. ej. reducir los escaneos completos de tabla",
|
||||
|
|
|
|||
|
|
@ -1248,6 +1248,7 @@ export default withEnglishFallback({
|
|||
reasoningLevelHigh: "Alto",
|
||||
reasoningLevelHint: "Controlla model_reasoning_effort di Codex CLI. Predefinito usa la configurazione Codex.",
|
||||
actions: {
|
||||
general: "Generale",
|
||||
generate: "Genera SQL",
|
||||
explain: "Spiega SQL",
|
||||
optimize: "Ottimizza SQL",
|
||||
|
|
@ -1260,6 +1261,7 @@ export default withEnglishFallback({
|
|||
generateNoExec: "Genera (senza eseguire)",
|
||||
},
|
||||
placeholders: {
|
||||
general: "Chiedi qualsiasi cosa...",
|
||||
generate: "Descrivi cosa desideri interrogare, es. ordini per utente negli ultimi 7 giorni",
|
||||
explain: "Opzionale: aggiungi cosa desideri comprendere",
|
||||
optimize: "Opzionale: aggiungi un obiettivo, es. ridurre le scansioni complete della tabella",
|
||||
|
|
|
|||
|
|
@ -1248,6 +1248,7 @@ export default withEnglishFallback({
|
|||
enableThinkingHint: "このオプションは/chat/completions APIとサポートされているモデルでのみ有効です。無効にするとトークン使用量を大幅に削減できますが、生成結果の品質が若干低下する可能性があります。",
|
||||
anthropicMessagesHint: "Anthropic Messages 互換 API は通常 /v1/messages を使用します。",
|
||||
actions: {
|
||||
general: "一般的な質問",
|
||||
generate: "SQLを生成",
|
||||
explain: "SQLを説明",
|
||||
optimize: "SQLを最適化",
|
||||
|
|
@ -1260,6 +1261,7 @@ export default withEnglishFallback({
|
|||
generateNoExec: "生成のみ(実行しない)",
|
||||
},
|
||||
placeholders: {
|
||||
general: "何でも聞いてください...",
|
||||
generate: "クエリしたい内容を説明してください(例: 過去7日間のユーザーごとの注文数)",
|
||||
explain: "任意: 理解したい内容を追加",
|
||||
optimize: "任意: 目標を追加(例: フルテーブルスキャンを減らす)",
|
||||
|
|
|
|||
|
|
@ -1249,6 +1249,7 @@ export default withEnglishFallback({
|
|||
enableThinkingOff: "Desativado",
|
||||
enableThinkingHint: "Esta opção só tem efeito em APIs /chat/completions e modelos compatíveis. Quando desativada, pode reduzir significativamente o uso de tokens, mas a qualidade dos resultados gerados pode diminuir ligeiramente.",
|
||||
actions: {
|
||||
general: "Geral",
|
||||
generate: "Gerar SQL",
|
||||
explain: "Explicar SQL",
|
||||
optimize: "Otimizar SQL",
|
||||
|
|
@ -1261,6 +1262,7 @@ export default withEnglishFallback({
|
|||
generateNoExec: "Gerar (sem executar)",
|
||||
},
|
||||
placeholders: {
|
||||
general: "Pergunte qualquer coisa...",
|
||||
generate: "Descreva o que você quer consultar, por exemplo, pedidos por usuário nos últimos 7 dias",
|
||||
explain: "Opcional: adicione o que você quer entender",
|
||||
optimize: "Opcional: adicione um objetivo, por exemplo, reduzir varreduras completas de tabela",
|
||||
|
|
|
|||
|
|
@ -1306,6 +1306,7 @@ export default withEnglishFallback({
|
|||
reasoningLevelHigh: "高",
|
||||
reasoningLevelHint: "控制 Codex CLI 的 model_reasoning_effort。默认使用你的 Codex 配置。",
|
||||
actions: {
|
||||
general: "通用问答",
|
||||
generate: "生成 SQL",
|
||||
explain: "解释 SQL",
|
||||
optimize: "优化 SQL",
|
||||
|
|
@ -1318,6 +1319,7 @@ export default withEnglishFallback({
|
|||
generateNoExec: "生成但不执行",
|
||||
},
|
||||
placeholders: {
|
||||
general: "问我任何问题...",
|
||||
generate: "描述你想查询什么,例如:统计最近 7 天每个用户的订单数",
|
||||
explain: "可留空,或补充你关心的点",
|
||||
optimize: "可留空,或说明优化目标,例如:减少全表扫描",
|
||||
|
|
|
|||
|
|
@ -1249,6 +1249,7 @@ export default withEnglishFallback({
|
|||
reasoningLevelHigh: "高",
|
||||
reasoningLevelHint: "控制 Codex CLI 的 model_reasoning_effort。預設會使用你的 Codex 設定。",
|
||||
actions: {
|
||||
general: "通用問答",
|
||||
generate: "產生 SQL",
|
||||
explain: "解釋 SQL",
|
||||
optimize: "最佳化 SQL",
|
||||
|
|
@ -1261,6 +1262,7 @@ export default withEnglishFallback({
|
|||
generateNoExec: "產生但不執行",
|
||||
},
|
||||
placeholders: {
|
||||
general: "問我任何問題...",
|
||||
generate: "描述你想查詢什麼,例如:統計最近 7 天每個使用者的訂單數",
|
||||
explain: "可留空,或補充你關心的點",
|
||||
optimize: "可留空,或說明最佳化目標,例如:減少全資料表掃描",
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@ import { ASK_ACTIONS, AGENT_ACTIONS, defaultActionForMode, isValidActionForMode
|
|||
|
||||
describe("AI action mode mapping", () => {
|
||||
describe("defaultActionForMode", () => {
|
||||
it("defaults Ask to generate", () => {
|
||||
expect(defaultActionForMode("ask")).toBe("generate");
|
||||
it("defaults Ask to general", () => {
|
||||
expect(defaultActionForMode("ask")).toBe("general");
|
||||
});
|
||||
|
||||
it("defaults Agent to query (not generate)", () => {
|
||||
// The whole point of the feature: Agent mode must not default to SQL generation.
|
||||
expect(defaultActionForMode("agent")).toBe("query");
|
||||
it("defaults Agent to general", () => {
|
||||
expect(defaultActionForMode("agent")).toBe("general");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -47,15 +46,15 @@ describe("AI action mode mapping", () => {
|
|||
});
|
||||
|
||||
describe("action sets", () => {
|
||||
it("Ask menu keeps the SQL-producing actions", () => {
|
||||
expect(ASK_ACTIONS).toEqual(["generate", "explain", "optimize", "fix", "convert", "sampleData"]);
|
||||
it("Ask menu starts with general, then SQL-producing actions", () => {
|
||||
expect(ASK_ACTIONS).toEqual(["general", "generate", "explain", "optimize", "fix", "convert", "sampleData"]);
|
||||
});
|
||||
|
||||
it("Agent menu is task-oriented, starts with query, and still offers generate", () => {
|
||||
expect(AGENT_ACTIONS[0]).toBe("query");
|
||||
// generate is shared so users can still request SQL-only output ("生成但不执行").
|
||||
it("Agent menu starts with general, then task-oriented actions", () => {
|
||||
expect(AGENT_ACTIONS[0]).toBe("general");
|
||||
// generate is shared so users can still request SQL-only output.
|
||||
expect(AGENT_ACTIONS).toContain("generate");
|
||||
expect(AGENT_ACTIONS).toEqual(["query", "exploreSchema", "executeAndExplain", "generate"]);
|
||||
expect(AGENT_ACTIONS).toEqual(["general", "query", "exploreSchema", "executeAndExplain", "generate"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,20 +32,20 @@ function dbLabel(dbType: DatabaseType): string {
|
|||
return labels[dbType] || dbType;
|
||||
}
|
||||
|
||||
export type AiAction = "generate" | "explain" | "optimize" | "fix" | "convert" | "sampleData" | "query" | "exploreSchema" | "executeAndExplain";
|
||||
export type AiAction = "general" | "generate" | "explain" | "optimize" | "fix" | "convert" | "sampleData" | "query" | "exploreSchema" | "executeAndExplain";
|
||||
export type AiAssistantMode = "ask" | "agent";
|
||||
|
||||
/** Actions shown in the Ask mode menu: SQL-producing, never auto-run. */
|
||||
export const ASK_ACTIONS: AiAction[] = ["generate", "explain", "optimize", "fix", "convert", "sampleData"];
|
||||
export const ASK_ACTIONS: AiAction[] = ["general", "generate", "explain", "optimize", "fix", "convert", "sampleData"];
|
||||
|
||||
/**
|
||||
* Actions shown in the Agent mode menu: task-oriented, drive tool use.
|
||||
* `generate` is shared with Ask so users can still request SQL-only output without execution.
|
||||
*/
|
||||
export const AGENT_ACTIONS: AiAction[] = ["query", "exploreSchema", "executeAndExplain", "generate"];
|
||||
export const AGENT_ACTIONS: AiAction[] = ["general", "query", "exploreSchema", "executeAndExplain", "generate"];
|
||||
|
||||
export function defaultActionForMode(mode: AiAssistantMode): AiAction {
|
||||
return mode === "agent" ? "query" : "generate";
|
||||
export function defaultActionForMode(_mode: AiAssistantMode): AiAction {
|
||||
return "general";
|
||||
}
|
||||
|
||||
export function isValidActionForMode(action: AiAction, mode: AiAssistantMode): boolean {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,28 @@ export interface AiSkillDefinition {
|
|||
}
|
||||
|
||||
export const AI_SKILL_DEFINITIONS: AiSkillDefinition[] = [
|
||||
{
|
||||
id: "general",
|
||||
action: "general",
|
||||
title: {
|
||||
en: "General",
|
||||
zh: "通用问答",
|
||||
},
|
||||
riskPolicy: "readonly",
|
||||
contextNeeds: [],
|
||||
userInstruction: {
|
||||
en: "Answer the user's question directly and naturally. Use your general knowledge and the database schema context when relevant.",
|
||||
zh: "直接、自然地回答用户的问题。使用你的通用知识,涉及数据库时可参考 Schema 上下文。",
|
||||
},
|
||||
systemRules: {
|
||||
en: ["Answer naturally and helpfully. Adapt to the user's intent — whether that's a greeting, a conceptual question, or a database-related inquiry."],
|
||||
zh: ["自然、有帮助地回答。根据用户意图灵活应对——无论是问候、概念性问题还是数据库相关咨询。"],
|
||||
},
|
||||
outputContract: {
|
||||
en: ["Provide a clear, helpful answer adapted to the user's question."],
|
||||
zh: ["根据用户问题提供清晰、有帮助的回答。"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "generate_sql",
|
||||
action: "generate",
|
||||
|
|
|
|||
|
|
@ -479,6 +479,7 @@ fn augment_system_prompt_with_task_contract(
|
|||
"This is a SQL-producing action: produce the final SQL in a fenced ```sql code block. Use tools only as intermediate evidence for schema/dialect; do not stop at a tool-result summary. In Agent mode, execute a query only when the original request explicitly asks for real data/results, not when it merely asks to generate SQL."
|
||||
} else {
|
||||
match action.to_ascii_lowercase().as_str() {
|
||||
"general" => "This is a general Q&A mode. Answer the user's question directly and naturally using your knowledge and any available database context. Adapt to the user's intent.",
|
||||
"query" => "This is a data-query task: call execute_query to obtain real results, then answer based on the actual data. Do not stop after merely outputting SQL text.",
|
||||
"exploreschema" => "This is a schema-inspection task: use list_tables/get_columns to obtain authoritative structure, then summarize. Do not execute data queries unless the user explicitly asks for data.",
|
||||
"executeandexplain" => "This is an execute-and-explain task: call execute_query to run the current SQL, then explain the real results.",
|
||||
|
|
@ -530,6 +531,7 @@ fn build_contract_repair_prompt(task_contract: Option<&AiTaskContract>, is_agent
|
|||
"For this SQL-producing action, produce SQL in a fenced ```sql code block. Tool results are evidence only; do not answer by summarizing schema/tool output. Execute a query only when the original request explicitly asks for real data/results."
|
||||
} else {
|
||||
match action.to_ascii_lowercase().as_str() {
|
||||
"general" => "For this general Q&A, answer the user's question directly and naturally.",
|
||||
"query" => "For this data-query task, call execute_query and answer based on real data; do not stop at SQL text or a schema summary.",
|
||||
"exploreschema" => "For this schema-inspection task, summarize real structure from list_tables/get_columns; do not invent columns.",
|
||||
"executeandexplain" => "For this execute-and-explain task, run the current SQL via execute_query and explain the real results.",
|
||||
|
|
@ -1375,4 +1377,15 @@ mod tests {
|
|||
assert!(repair.contains("data-query task"));
|
||||
assert!(repair.contains("call execute_query"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn general_action_skips_sql_validation() {
|
||||
let contract = AiTaskContract {
|
||||
action: Some("general".to_string()),
|
||||
mode: Some("ask".to_string()),
|
||||
user_request: Some("你好".to_string()),
|
||||
};
|
||||
let answer = "你好!我是 DBX 的数据库助手。有什么可以帮你的吗?";
|
||||
assert_eq!(validate_final_answer(Some(&contract), answer), FinalAnswerCheck::Satisfied);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { AiAction } from "../../apps/desktop/src/lib/ai/ai.ts";
|
|||
import { AI_SKILL_DEFINITIONS, aiSkillForAction } from "../../apps/desktop/src/lib/ai/aiSkills.ts";
|
||||
|
||||
const actions: AiAction[] = [
|
||||
"general",
|
||||
"generate",
|
||||
"explain",
|
||||
"optimize",
|
||||
|
|
@ -25,7 +26,10 @@ test("defines one internal AI skill per assistant action", () => {
|
|||
assert.match(skill.id, /^[a-z][a-z0-9_]*$/);
|
||||
assert.ok(skill.title.zh);
|
||||
assert.ok(skill.title.en);
|
||||
assert.ok(skill.contextNeeds.length > 0);
|
||||
// general skill has empty contextNeeds, others must have at least one
|
||||
if (action !== "general") {
|
||||
assert.ok(skill.contextNeeds.length > 0);
|
||||
}
|
||||
assert.ok(skill.systemRules.zh.length > 0);
|
||||
assert.ok(skill.systemRules.en.length > 0);
|
||||
assert.ok(skill.userInstruction.zh.length > 0);
|
||||
|
|
|
|||
Loading…
Reference in New Issue