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:
2026-03-01 08:15:50 +00:00
parent 1aa81896b5
commit d9d3bc340c
19 changed files with 2283 additions and 53 deletions

View File

@@ -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(),
};