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:
53
src/api/kb.rs
Normal file
53
src/api/kb.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
extract::State,
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::AppState;
|
||||
use super::{ApiResult, db_err};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct KbContent {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
pub fn router(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/kb", get(get_kb).put(put_kb))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn get_kb(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> ApiResult<KbContent> {
|
||||
let content: String = sqlx::query_scalar("SELECT content FROM kb_content WHERE id = 1")
|
||||
.fetch_one(&state.db.pool)
|
||||
.await
|
||||
.map_err(db_err)?;
|
||||
|
||||
Ok(Json(KbContent { content }))
|
||||
}
|
||||
|
||||
async fn put_kb(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(input): Json<KbContent>,
|
||||
) -> ApiResult<KbContent> {
|
||||
sqlx::query("UPDATE kb_content SET content = ?, updated_at = datetime('now') WHERE id = 1")
|
||||
.bind(&input.content)
|
||||
.execute(&state.db.pool)
|
||||
.await
|
||||
.map_err(db_err)?;
|
||||
|
||||
// Re-index
|
||||
if let Some(kb) = &state.kb {
|
||||
if let Err(e) = kb.index(&input.content).await {
|
||||
tracing::error!("KB indexing failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(KbContent {
|
||||
content: input.content,
|
||||
}))
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod kb;
|
||||
mod projects;
|
||||
mod timers;
|
||||
mod workflows;
|
||||
@@ -9,16 +10,24 @@ use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, any},
|
||||
Router,
|
||||
Json, Router,
|
||||
};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
pub(crate) type ApiResult<T> = Result<Json<T>, Response>;
|
||||
|
||||
pub(crate) fn db_err(e: sqlx::Error) -> Response {
|
||||
tracing::error!("Database error: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
|
||||
}
|
||||
|
||||
pub fn router(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.merge(projects::router(state.clone()))
|
||||
.merge(workflows::router(state.clone()))
|
||||
.merge(timers::router(state.clone()))
|
||||
.merge(kb::router(state.clone()))
|
||||
.route("/projects/{id}/files/{*path}", get(serve_project_file))
|
||||
.route("/projects/{id}/app/{*path}", any(proxy_to_service).with_state(state.clone()))
|
||||
.route("/projects/{id}/app/", any(proxy_to_service_root).with_state(state))
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use crate::AppState;
|
||||
use crate::db::Project;
|
||||
|
||||
type ApiResult<T> = Result<Json<T>, Response>;
|
||||
|
||||
fn db_err(e: sqlx::Error) -> Response {
|
||||
tracing::error!("Database error: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
|
||||
}
|
||||
use super::{ApiResult, db_err};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateProject {
|
||||
|
||||
@@ -9,13 +9,7 @@ use axum::{
|
||||
use serde::Deserialize;
|
||||
use crate::AppState;
|
||||
use crate::db::Timer;
|
||||
|
||||
type ApiResult<T> = Result<Json<T>, Response>;
|
||||
|
||||
fn db_err(e: sqlx::Error) -> Response {
|
||||
tracing::error!("Database error: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
|
||||
}
|
||||
use super::{ApiResult, db_err};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateTimer {
|
||||
|
||||
@@ -10,19 +10,13 @@ use serde::Deserialize;
|
||||
use crate::AppState;
|
||||
use crate::agent::AgentEvent;
|
||||
use crate::db::{Workflow, PlanStep, Comment};
|
||||
use super::{ApiResult, db_err};
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct ReportResponse {
|
||||
report: String,
|
||||
}
|
||||
|
||||
type ApiResult<T> = Result<Json<T>, Response>;
|
||||
|
||||
fn db_err(e: sqlx::Error) -> Response {
|
||||
tracing::error!("Database error: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateWorkflow {
|
||||
pub requirement: String,
|
||||
|
||||
Reference in New Issue
Block a user