Add global knowledge base with RAG search
- KB module: fastembed (AllMiniLML6V2) for CPU embedding, SQLite for vector storage with brute-force cosine similarity search - Chunking by ## headings, embeddings stored as BLOB in kb_chunks table - API: GET/PUT /api/kb for full-text read/write with auto re-indexing - Agent tools: kb_search (top-5 semantic search) and kb_read (full text) available in both planning and execution phases - Frontend: Settings menu in sidebar footer, KB editor as independent view with markdown textarea and save button - Also: extract shared db_err/ApiResult to api/mod.rs, add context management design doc Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
80
src/agent.rs
80
src/agent.rs
@@ -87,10 +87,11 @@ pub struct AgentManager {
|
||||
next_port: AtomicU16,
|
||||
pool: SqlitePool,
|
||||
llm_config: LlmConfig,
|
||||
kb: Option<Arc<crate::kb::KbManager>>,
|
||||
}
|
||||
|
||||
impl AgentManager {
|
||||
pub fn new(pool: SqlitePool, llm_config: LlmConfig) -> Arc<Self> {
|
||||
pub fn new(pool: SqlitePool, llm_config: LlmConfig, kb: Option<Arc<crate::kb::KbManager>>) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
agents: RwLock::new(HashMap::new()),
|
||||
broadcast: RwLock::new(HashMap::new()),
|
||||
@@ -98,6 +99,7 @@ impl AgentManager {
|
||||
next_port: AtomicU16::new(9100),
|
||||
pool,
|
||||
llm_config,
|
||||
kb,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -146,6 +148,14 @@ impl AgentManager {
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_venv(exec: &LocalExecutor, workdir: &str) {
|
||||
let _ = tokio::fs::create_dir_all(workdir).await;
|
||||
let venv_path = format!("{}/.venv", workdir);
|
||||
if !std::path::Path::new(&venv_path).exists() {
|
||||
let _ = exec.execute("uv venv .venv", workdir).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn agent_loop(
|
||||
project_id: String,
|
||||
mut rx: mpsc::Receiver<AgentEvent>,
|
||||
@@ -196,11 +206,7 @@ async fn agent_loop(
|
||||
.await;
|
||||
|
||||
// Ensure workspace and venv exist
|
||||
let _ = tokio::fs::create_dir_all(&workdir).await;
|
||||
let venv_path = format!("{}/.venv", workdir);
|
||||
if !std::path::Path::new(&venv_path).exists() {
|
||||
let _ = exec.execute("uv venv .venv", &workdir).await;
|
||||
}
|
||||
ensure_venv(&exec, &workdir).await;
|
||||
let _ = tokio::fs::write(format!("{}/requirement.md", workdir), &requirement).await;
|
||||
|
||||
tracing::info!("Starting agent loop for workflow {}", workflow_id);
|
||||
@@ -264,11 +270,7 @@ async fn agent_loop(
|
||||
let Some(wf) = wf else { continue };
|
||||
|
||||
// Ensure venv exists for comment re-runs too
|
||||
let _ = tokio::fs::create_dir_all(&workdir).await;
|
||||
let venv_path = format!("{}/.venv", workdir);
|
||||
if !std::path::Path::new(&venv_path).exists() {
|
||||
let _ = exec.execute("uv venv .venv", &workdir).await;
|
||||
}
|
||||
ensure_venv(&exec, &workdir).await;
|
||||
|
||||
// Clear old plan steps (keep log entries for history)
|
||||
let _ = sqlx::query("DELETE FROM plan_steps WHERE workflow_id = ? AND kind = 'plan'")
|
||||
@@ -375,6 +377,23 @@ fn tool_list_files() -> Tool {
|
||||
}))
|
||||
}
|
||||
|
||||
fn tool_kb_search() -> Tool {
|
||||
make_tool("kb_search", "搜索知识库中与查询相关的内容片段。返回最相关的 top-5 片段。", serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": { "type": "string", "description": "搜索查询" }
|
||||
},
|
||||
"required": ["query"]
|
||||
}))
|
||||
}
|
||||
|
||||
fn tool_kb_read() -> Tool {
|
||||
make_tool("kb_read", "读取知识库全文内容。", serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_planning_tools() -> Vec<Tool> {
|
||||
vec![
|
||||
make_tool("update_plan", "设置高层执行计划。分析需求后调用此工具提交计划。每个步骤应是一个逻辑阶段(不是具体命令),包含简短标题和详细描述。调用后自动进入执行阶段。", serde_json::json!({
|
||||
@@ -397,6 +416,8 @@ fn build_planning_tools() -> Vec<Tool> {
|
||||
})),
|
||||
tool_list_files(),
|
||||
tool_read_file(),
|
||||
tool_kb_search(),
|
||||
tool_kb_read(),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -451,6 +472,8 @@ fn build_execution_tools() -> Vec<Tool> {
|
||||
},
|
||||
"required": ["content"]
|
||||
})),
|
||||
tool_kb_search(),
|
||||
tool_kb_read(),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -481,6 +504,7 @@ fn build_planning_prompt(project_id: &str) -> String {
|
||||
- 因此前端 HTML 中的所有 API 请求必须使用【不带开头 / 的相对路径】\n\
|
||||
- 正确示例:fetch('todos') 或 fetch('./todos') 错误示例:fetch('/todos') 或 fetch('/api/todos')\n\
|
||||
- HTML 中的 <base> 标签不需要设置,只要不用绝对路径就行\n\
|
||||
- 知识库工具:kb_search(query) 搜索相关片段,kb_read() 读取全文\n\
|
||||
\n\
|
||||
请使用中文回复。",
|
||||
project_id,
|
||||
@@ -511,6 +535,7 @@ fn build_execution_prompt(project_id: &str) -> String {
|
||||
- 静态文件访问:/api/projects/{0}/files/{{filename}}\n\
|
||||
- 后台服务访问:/api/projects/{0}/app/(启动命令需监听 0.0.0.0:$PORT)\n\
|
||||
- 【重要】应用通过反向代理访问,前端 HTML/JS 中的 fetch/XHR 请求必须使用相对路径(如 fetch('todos')),绝对不能用 / 开头的路径(如 fetch('/todos')),否则会 404\n\
|
||||
- 知识库工具:kb_search(query) 搜索相关片段,kb_read() 读取全文\n\
|
||||
\n\
|
||||
请使用中文回复。",
|
||||
project_id,
|
||||
@@ -951,6 +976,37 @@ async fn run_agent_loop(
|
||||
}
|
||||
}
|
||||
|
||||
"kb_search" => {
|
||||
let query = args["query"].as_str().unwrap_or("");
|
||||
let result = if let Some(kb) = &mgr.kb {
|
||||
match kb.search(query).await {
|
||||
Ok(results) if results.is_empty() => "知识库为空或没有匹配结果。".to_string(),
|
||||
Ok(results) => {
|
||||
results.iter().enumerate().map(|(i, r)| {
|
||||
format!("--- 片段 {} (相似度: {:.2}) ---\n{}", i + 1, r.score, r.content)
|
||||
}).collect::<Vec<_>>().join("\n\n")
|
||||
}
|
||||
Err(e) => format!("Error: {}", e),
|
||||
}
|
||||
} else {
|
||||
"知识库未初始化。".to_string()
|
||||
};
|
||||
state.step_messages.push(ChatMessage::tool_result(&tc.id, &result));
|
||||
}
|
||||
|
||||
"kb_read" => {
|
||||
let result: String = match sqlx::query_scalar::<_, String>("SELECT content FROM kb_content WHERE id = 1")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
{
|
||||
Ok(content) => {
|
||||
if content.is_empty() { "知识库为空。".to_string() } else { content }
|
||||
}
|
||||
Err(e) => format!("Error: {}", e),
|
||||
};
|
||||
state.step_messages.push(ChatMessage::tool_result(&tc.id, &result));
|
||||
}
|
||||
|
||||
// IO tools: execute, read_file, write_file, list_files
|
||||
_ => {
|
||||
let current_plan_step_id = match &state.phase {
|
||||
@@ -968,6 +1024,8 @@ async fn run_agent_loop(
|
||||
"read_file" => format!("Read: {}", args["path"].as_str().unwrap_or("?")),
|
||||
"write_file" => format!("Write: {}", args["path"].as_str().unwrap_or("?")),
|
||||
"list_files" => format!("List: {}", args["path"].as_str().unwrap_or(".")),
|
||||
"kb_search" => format!("KB Search: {}", args["query"].as_str().unwrap_or("?")),
|
||||
"kb_read" => "KB Read".to_string(),
|
||||
other => other.to_string(),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user