feat(rag): replace LiteParse flows with LightRAG provider

This commit is contained in:
lix-2026
2026-06-07 01:10:31 +08:00
parent f46c2fb5d0
commit 1f25364374
40 changed files with 7232 additions and 2350 deletions
@@ -0,0 +1,157 @@
use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::hermes_tools::ToolCallInput;
use crate::routes::knowledge_rag::{
KnowledgeRagOpenReferenceRequest, KnowledgeRagQueryRequest, KnowledgeRagStatusQuery,
};
use axum::extract::{Extension, Json, Query, State};
use serde_json::{json, Value};
pub async fn status(
state: &AppState,
context: &RequestContext,
input: &ToolCallInput,
) -> Result<Value, WebError> {
let args = input.args.clone().unwrap_or_else(|| json!({}));
let body = KnowledgeRagStatusQuery {
workspace_id: input.effective_workspace_id().or_else(|| {
args.get("workspaceId")
.and_then(Value::as_str)
.map(ToOwned::to_owned)
}),
root_uri: input.effective_root_uri().or_else(|| {
args.get("rootUri")
.and_then(Value::as_str)
.map(ToOwned::to_owned)
}),
};
let Json(payload) = crate::routes::knowledge_rag::status(
State(state.clone()),
Extension(context.clone()),
Query(body),
)
.await?;
Ok(payload)
}
pub async fn query(
state: &AppState,
context: &RequestContext,
input: &ToolCallInput,
) -> Result<Value, WebError> {
let mut args = input.args.clone().unwrap_or_else(|| json!({}));
if args.get("workspaceId").is_none() {
if let Some(workspace_id) = input.effective_workspace_id() {
args["workspaceId"] = json!(workspace_id);
}
}
if args.get("rootUri").is_none() {
if let Some(root_uri) = input.effective_root_uri() {
args["rootUri"] = json!(root_uri);
}
}
let body = serde_json::from_value::<KnowledgeRagQueryRequest>(args).map_err(|error| {
WebError::bad_request_code(
"mnote_knowledge_rag_query_payload_invalid",
format!("资料库问答参数无效: {error}"),
)
.with_context(context)
})?;
let Json(payload) = crate::routes::knowledge_rag::query_rag(
State(state.clone()),
Extension(context.clone()),
Json(body),
)
.await?;
Ok(compact_query_result_for_agent(payload))
}
pub async fn open_reference(
state: &AppState,
context: &RequestContext,
input: &ToolCallInput,
) -> Result<Value, WebError> {
let mut args = input.args.clone().unwrap_or_else(|| json!({}));
if args.get("workspaceId").is_none() {
if let Some(workspace_id) = input.effective_workspace_id() {
args["workspaceId"] = json!(workspace_id);
}
}
if args.get("rootUri").is_none() {
if let Some(root_uri) = input.effective_root_uri() {
args["rootUri"] = json!(root_uri);
}
}
let body =
serde_json::from_value::<KnowledgeRagOpenReferenceRequest>(args).map_err(|error| {
WebError::bad_request_code(
"mnote_knowledge_rag_open_reference_payload_invalid",
format!("资料库引用打开参数无效: {error}"),
)
.with_context(context)
})?;
let Json(payload) = crate::routes::knowledge_rag::open_reference(
State(state.clone()),
Extension(context.clone()),
Json(body),
)
.await?;
Ok(payload)
}
fn compact_query_result_for_agent(payload: Value) -> Value {
let references = payload
.get("references")
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.take(8)
.map(compact_reference_for_agent)
.collect::<Vec<_>>()
})
.unwrap_or_default();
let citations = references
.iter()
.filter_map(|reference| {
reference
.get("citationMarkdown")
.and_then(Value::as_str)
.filter(|value| !value.trim().is_empty())
.map(ToOwned::to_owned)
})
.collect::<Vec<_>>();
json!({
"ok": payload.get("ok").cloned().unwrap_or_else(|| json!(true)),
"schema": "mnote.knowledge_rag.agent_query_result.v1",
"provider": payload.get("provider").cloned().unwrap_or_else(|| json!("lightrag")),
"answerGuidance": "Final answers must cite at least one returned citationMarkdown verbatim. If locatorDegraded is true, say the source location is degraded instead of inventing page or bbox.",
"references": references,
"citations": citations,
"sourceScope": payload.get("sourceScope").cloned().unwrap_or(Value::Array(Vec::new())),
"rawStatus": payload.pointer("/raw/status").cloned().unwrap_or(Value::Null),
"rawMessage": payload.pointer("/raw/message").cloned().unwrap_or(Value::Null),
"rawMetadata": payload.pointer("/raw/metadata").cloned().unwrap_or(Value::Null),
})
}
fn compact_reference_for_agent(reference: &Value) -> Value {
let quote = reference
.get("quote")
.and_then(Value::as_str)
.map(|value| value.chars().take(700).collect::<String>())
.unwrap_or_default();
json!({
"schema": reference.get("schema").cloned().unwrap_or_else(|| json!("mnote.knowledge_rag.reference.v1")),
"provider": reference.get("provider").cloned().unwrap_or_else(|| json!("lightrag")),
"filePath": reference.get("filePath").cloned().unwrap_or(Value::Null),
"sourceRootRelativePath": reference.get("sourceRootRelativePath").cloned().unwrap_or(Value::Null),
"chunkId": reference.get("chunkId").cloned().unwrap_or(Value::Null),
"quote": quote,
"locator": reference.get("locator").cloned().unwrap_or(Value::Null),
"locatorDegraded": reference.get("locatorDegraded").cloned().unwrap_or(Value::Bool(true)),
"citationMarkdown": reference.get("citationMarkdown").cloned().unwrap_or(Value::Null),
"citationUrl": reference.get("citationUrl").cloned().unwrap_or(Value::Null),
})
}
@@ -12,12 +12,9 @@ pub fn manifest() -> Value {
context_resolve_target_tool(),
doc_fetch_tool(),
doc_find_tool(),
evidence_search_tool(),
evidence_read_tool(),
evidence_open_tool(),
index_status_tool(),
index_refresh_tool(),
index_update_settings_tool(),
knowledge_rag_status_tool(),
knowledge_rag_query_tool(),
knowledge_rag_open_reference_tool(),
block_fetch_tool(),
doc_plan_update_tool(),
block_replace_tool(),
@@ -293,6 +290,7 @@ fn doc_find_tool() -> Value {
})
}
#[allow(dead_code)]
fn evidence_search_tool() -> Value {
let mut properties = base_identity_properties();
if let Value::Object(map) = &mut properties {
@@ -340,6 +338,7 @@ fn evidence_search_tool() -> Value {
})
}
#[allow(dead_code)]
fn evidence_read_tool() -> Value {
let mut properties = base_identity_properties();
if let Value::Object(map) = &mut properties {
@@ -371,6 +370,7 @@ fn evidence_read_tool() -> Value {
})
}
#[allow(dead_code)]
fn evidence_open_tool() -> Value {
let mut properties = base_identity_properties();
if let Value::Object(map) = &mut properties {
@@ -391,6 +391,93 @@ fn evidence_open_tool() -> Value {
})
}
fn knowledge_rag_status_tool() -> Value {
let mut properties = base_identity_properties();
if let Value::Object(map) = &mut properties {
map.insert("rootUri".into(), json!({ "type": "string" }));
}
json!({
"name": "mnote.knowledge_rag.status",
"description": "查看 LightRAG 资料库 provider 状态、dashboard 地址、source registry 和同步状态。",
"schemaVersion": TOOL_SCHEMA_VERSION,
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
"status": "available",
"annotations": tool_annotations(true, false, true, false),
"inputSchema": {
"type": "object",
"properties": properties
}
})
}
fn knowledge_rag_query_tool() -> Value {
let mut properties = base_identity_properties();
if let Value::Object(map) = &mut properties {
map.insert("rootUri".into(), json!({ "type": "string" }));
map.insert("query".into(), json!({ "type": "string" }));
map.insert("question".into(), json!({ "type": "string" }));
map.insert(
"mode".into(),
json!({ "type": "string", "enum": ["local", "global", "hybrid", "naive", "mix", "bypass"], "default": "mix" }),
);
map.insert("topK".into(), json!({ "type": "integer", "default": 40 }));
map.insert(
"chunkTopK".into(),
json!({ "type": "integer", "default": 20 }),
);
map.insert(
"includeChunkContent".into(),
json!({ "type": "boolean", "default": true }),
);
map.insert(
"sourcePaths".into(),
json!({
"type": "array",
"items": { "type": "string" },
"description": "可选 MNote workspace 相对路径范围;可传文件或目录,返回 references 会限制在这些来源内。"
}),
);
}
json!({
"name": "mnote.knowledge_rag.query",
"description": "向 LightRAG 资料库提问,返回 provider 原始结果和经 MNote registry 映射后的引用。",
"schemaVersion": TOOL_SCHEMA_VERSION,
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
"status": "available",
"annotations": tool_annotations(true, false, true, false),
"inputSchema": {
"type": "object",
"required": ["workspaceId", "rootUri", "query"],
"properties": properties
}
})
}
fn knowledge_rag_open_reference_tool() -> Value {
let mut properties = base_identity_properties();
if let Value::Object(map) = &mut properties {
map.insert("rootUri".into(), json!({ "type": "string" }));
map.insert("reference".into(), json!({ "type": "object" }));
map.insert("referenceId".into(), json!({ "type": "string" }));
map.insert("filePath".into(), json!({ "type": "string" }));
map.insert("chunkId".into(), json!({ "type": "string" }));
}
json!({
"name": "mnote.knowledge_rag.open_reference",
"description": "把 LightRAG reference 映射为 MNote 可打开的本地 resource action;没有 registry 命中时明确返回定位降级。",
"schemaVersion": TOOL_SCHEMA_VERSION,
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
"status": "available",
"annotations": tool_annotations(true, false, true, false),
"inputSchema": {
"type": "object",
"required": ["workspaceId", "rootUri", "filePath"],
"properties": properties
}
})
}
#[allow(dead_code)]
fn index_status_tool() -> Value {
let mut properties = base_identity_properties();
if let Value::Object(map) = &mut properties {
@@ -411,6 +498,7 @@ fn index_status_tool() -> Value {
})
}
#[allow(dead_code)]
fn index_refresh_tool() -> Value {
let mut properties = base_identity_properties();
if let Value::Object(map) = &mut properties {
@@ -431,6 +519,7 @@ fn index_refresh_tool() -> Value {
})
}
#[allow(dead_code)]
fn index_update_settings_tool() -> Value {
let mut properties = base_identity_properties();
if let Value::Object(map) = &mut properties {
@@ -744,7 +833,7 @@ fn page_get_tool() -> Value {
fn page_save_tool() -> Value {
json!({
"name": "mnote.page.save",
"description": "粗粒度兼容兜底:保存当前页面正文;本地 Markdown 普通编辑优先使用 agent 原生 patch/diff,只有整页覆盖/追加且其它工具无法表达时使用",
"description": "粗粒度 compat / cloud 兜底:保存当前页面正文;local-first 本地 Markdown 普通编辑禁止使用该工具,必须优先让 agent 原生 patch/diff 直接编辑授权文件;仅在用户明确要求整页覆盖/追加且其它工具无法表达时使用",
"schemaVersion": TOOL_SCHEMA_VERSION,
"capabilityScope": ["page.write"],
"status": "available",
@@ -795,7 +884,7 @@ fn available_tool(
fn doc_markdown_edit_tool() -> Value {
let mut tool = write_tool(
"mnote.doc.markdown_edit",
"兼容 / 远端代理 fallback:通过文本级搜索替换编辑 markdown 内容。local-first 本地 workspace 默认优先让 agent 原生 patch/diff 直接编辑授权文件;仅在需要 MNote 兼容工具、远端代理或结构校验时使用。",
"compat / remote / cloud fallback:通过文本级搜索替换编辑 markdown 内容。local-first 本地 workspace 的普通 Markdown 编辑禁止使用该工具,必须优先让 agent 原生 patch/diff 直接编辑授权文件;仅在远端代理、cloud/compat 或需要 MNote 结构校验时使用。",
["block.write", "page.write"],
json!({
"operations": {
@@ -4,6 +4,7 @@ pub mod context_tools;
pub mod doc;
pub mod evidence;
pub mod index;
pub mod knowledge_rag;
pub mod manifest;
pub mod onlyoffice_live;
pub mod page;
+20 -24
View File
@@ -36,24 +36,22 @@ const CAPABILITY_PACKS: &[MnoteCapabilityPack] = &[
content: include_str!("../../../../../skills/mnote-current-page/SKILL.md"),
},
MnoteCapabilityPack {
id: "mnote-local-index",
title: "本地索引与证据检索",
description: "检索本地文档证据,并管理本地索引范围、刷新和删除。",
id: "mnote-knowledge-rag",
title: "资料库问答",
description:
"通过 LightRAG provider 检索多本书、论文、PDF 和附件资料库,并返回可回跳来源。",
category: "knowledge",
agent_ids: &["hermes", "reasonix"],
read_only: false,
read_only: true,
requires_context_refs: &["folder"],
tool_names: &[
"mnote.context.snapshot",
"mnote.context.resolve_target",
"mnote.evidence.search",
"mnote.evidence.read",
"mnote.evidence.open",
"mnote.index.status",
"mnote.index.refresh",
"mnote.index.update_settings",
"mnote.knowledge_rag.status",
"mnote.knowledge_rag.query",
"mnote.knowledge_rag.open_reference",
],
content: include_str!("../../../../../skills/mnote-local-index/SKILL.md"),
content: include_str!("../../../../../skills/mnote-knowledge-rag/SKILL.md"),
},
MnoteCapabilityPack {
id: "mnote-local-file",
@@ -208,7 +206,7 @@ pub fn manifest_capabilities() -> Vec<Value> {
fn canonical_skill_id(skill_id: &str) -> &str {
match skill_id {
"mnote-document-evidence" => "mnote-local-index",
"mnote-document-evidence" | "mnote-local-index" => "mnote-knowledge-rag",
other => other,
}
}
@@ -364,33 +362,31 @@ mod tests {
}
#[test]
fn skill_registry_exposes_local_index_skill_to_agents() {
fn skill_registry_retired_local_index_in_favor_of_lightrag() {
let hermes_skills = skill_summaries_for_agent(Some("hermes"));
assert!(!hermes_skills
.iter()
.any(|skill| skill["id"] == "mnote-local-index"));
let skill = hermes_skills
.iter()
.find(|skill| skill["id"] == "mnote-local-index")
.expect("hermes should see local index skill");
assert_eq!(skill["readOnly"], false);
.find(|skill| skill["id"] == "mnote-knowledge-rag")
.expect("hermes should see LightRAG skill");
assert_eq!(skill["readOnly"], true);
assert!(skill["toolNames"]
.as_array()
.expect("tool names")
.iter()
.any(|name| name == "mnote.evidence.search"));
assert!(skill["toolNames"]
.as_array()
.expect("tool names")
.iter()
.any(|name| name == "mnote.index.update_settings"));
.any(|name| name == "mnote.knowledge_rag.query"));
assert!(!hermes_skills
.iter()
.any(|skill| skill["id"] == "mnote-document-evidence"));
}
#[test]
fn skill_read_keeps_document_evidence_compat_alias() {
fn skill_read_maps_document_evidence_alias_to_lightrag() {
let skill = find_skill("mnote-document-evidence", Some("reasonix"))
.expect("compat alias should resolve");
assert_eq!(skill.id, "mnote-local-index");
assert_eq!(skill.id, "mnote-knowledge-rag");
}
#[tokio::test]