@
feat(ai): add agent loop with tool calling, fix connection routing and Gemini compatibility ## 背景说明 将 dbx AI 助手从单次 SQL 文本生成器升级为具备 tool calling 的数据库 Agent。 Phase 1 实现 Agent Loop 框架 + 只读工具(list_tables、get_columns)。 参考:Pi agent-core,.trellis/tasks/06-11-feat-ai-agent-loop-tool-calling-upgrade ## 修改内容 - 新增 agent_loop.rs:Agent Loop 核心,支持 OpenAI/Claude/Gemini Provider 的 tool calling,最大 10 轮,Ollama 自动降级为 text-only - 新增 agent_tools.rs:read_only_tools (list_tables, get_columns) 的工具定义与执行器 - 新增 agent_events.rs:AgentEvent/ToolCall/ToolResult/ToolDefinition 类型定义 - 修改 ai.rs:AiMessage 新增 tool_call_id 字段 - 修改 lib.rs:注册 agent_events、agent_loop、agent_tools 模块 - 修改 routes/ai.rs & commands/ai.rs:新增 ai_agent_stream SSE 端点 - 修改 tauri.ts & http.ts:新增 AgentEvent 类型与 aiAgentStream 函数 - 修改 api.ts:注册 aiAgentStream 转发 - 修改 ai.ts:新增 runAgentStream,AiContext 增加 connectionId 字段 - 修改 AiAssistant.vue:接入 runAgentStream,解析 AgentEvent 渲染工具调用步骤 - 修改 en.ts / zh-CN.ts:新增 agentSteps 国际化文案 ## 修复的问题 - [#I01] connectionName 误传为 connectionId → add connectionId to AiContext - [#I02] agent loop 取消操作无效 → add cancel checks before each turn - [#C01] TODO 缺少 issue 引用 → add PRD reference to TODO - [#C02] stream_with_tools 命名不准确 → add clarifying doc comment - [#C03] Gemini functionResponse.name 解析错误 → fix tool_call_id format and extraction ## 测试建议 - execute_get_columns:合法表名/空表名/SQL 注入字符拒绝 - call_openai_with_tools:空 choices/空 tool_calls/非 JSON arguments - call_gemini_with_tools:空 candidates/parts 无 functionCall - runAgentStream:有效 connectionId 正常调用 @
This commit is contained in:
1 parent
759cab9088
commit
9ab41795d6
17 files changed
+1253
-122
No files matched your search
@@ -42,11 +42,53 @@ pub async fn ai_stream(app: AppHandle, session_id: String, request: AiCompletion
|
||||
result
|
||||
}
|
||||
|
||||
use dbx_core::agent_events::AgentEvent;
|
||||
use dbx_core::agent_loop::{run_agent_loop, AgentLoopContext};
|
||||
use dbx_core::models::connection::DatabaseType;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn ai_cancel_stream(session_id: String) -> Result<bool, String> {
|
||||
Ok(dbx_core::ai::cancel_stream(&session_id).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn ai_agent_stream(
|
||||
app: AppHandle,
|
||||
state: State<'_, Arc<AppState>>,
|
||||
session_id: String,
|
||||
request: AiCompletionRequest,
|
||||
connection_id: String,
|
||||
database: String,
|
||||
db_type: String,
|
||||
) -> Result<String, String> {
|
||||
let cancelled = dbx_core::ai::register_stream(&session_id).await;
|
||||
|
||||
let parsed_db_type: DatabaseType =
|
||||
serde_json::from_str(&format!("\"{}\"", db_type)).map_err(|_| format!("Unknown database type: {db_type}"))?;
|
||||
|
||||
let agent_ctx = AgentLoopContext { state: state.inner().clone(), connection_id, database, db_type: parsed_db_type };
|
||||
|
||||
let result = run_agent_loop(
|
||||
&request.config,
|
||||
&request.system_prompt,
|
||||
&request.messages,
|
||||
&agent_ctx,
|
||||
{
|
||||
let app = app.clone();
|
||||
move |event: AgentEvent| {
|
||||
let _ = app.emit("ai-agent-event", &event);
|
||||
}
|
||||
},
|
||||
&cancelled,
|
||||
request.max_tokens,
|
||||
request.temperature,
|
||||
)
|
||||
.await;
|
||||
|
||||
dbx_core::ai::unregister_stream(&session_id).await;
|
||||
result
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_ai_conversation(state: State<'_, Arc<AppState>>, conversation: AiConversation) -> Result<(), String> {
|
||||
state.storage.save_ai_conversation(&conversation).await
|
||||
|
||||
Reference in new issue
Block a user