chore: checkpoint pi lab rust integration work

This commit is contained in:
Agent Board
2026-07-11 20:39:18 +08:00
parent d47f6447fd
commit d16eccfe10
39 changed files with 4843 additions and 455 deletions
+72 -4
View File
@@ -2622,10 +2622,14 @@ impl ControlPlaneStore for SqliteControlPlaneStore {
let conn = self.lock_conn()?;
let mut stmt = conn.prepare(
"SELECT id, user_id, workspace_id, document_id, session_id, run_id, profile, acp_runtime, event_type, payload_json, created_at
FROM ai_runtime_events
WHERE user_id = ?1 AND run_id = ?2
ORDER BY created_at ASC
LIMIT ?3",
FROM (
SELECT id, user_id, workspace_id, document_id, session_id, run_id, profile, acp_runtime, event_type, payload_json, created_at, rowid AS event_rowid
FROM ai_runtime_events
WHERE user_id = ?1 AND run_id = ?2
ORDER BY created_at DESC, rowid DESC
LIMIT ?3
)
ORDER BY created_at ASC, event_rowid ASC",
)?;
let rows = stmt
.query_map(
@@ -4225,6 +4229,70 @@ mod tests {
.is_empty());
}
#[test]
fn ai_runtime_events_preserve_insert_order_with_same_timestamp() {
let store = store();
create_user(&store, "ai_runtime_order_user");
store
.upsert_ai_runtime_run(UpsertAiRuntimeRunInput {
id: None,
user_id: "ai_runtime_order_user".to_string(),
workspace_id: Some("ws_order".to_string()),
document_id: Some("doc_order".to_string()),
session_id: "sess_order".to_string(),
run_id: "run_order".to_string(),
title: Some("order test".to_string()),
profile: "pi_lab".to_string(),
acp_runtime: "pi".to_string(),
trace_id: None,
status: "running".to_string(),
runtime_json: "{}".to_string(),
payload_json: "{}".to_string(),
})
.expect("insert runtime run");
for (id, payload) in [
("are_z_first", "{\"seq\":1}"),
("are_m_second", "{\"seq\":2}"),
("are_a_third", "{\"seq\":3}"),
] {
store
.append_ai_runtime_event(AppendAiRuntimeEventInput {
id: Some(id.to_string()),
user_id: "ai_runtime_order_user".to_string(),
workspace_id: Some("ws_order".to_string()),
document_id: Some("doc_order".to_string()),
session_id: "sess_order".to_string(),
run_id: "run_order".to_string(),
profile: "pi_lab".to_string(),
acp_runtime: "pi".to_string(),
event_type: "pi_rpc_event".to_string(),
payload_json: payload.to_string(),
})
.expect("append ordered runtime event");
}
let events = store
.list_ai_runtime_events("ai_runtime_order_user", "run_order", 10)
.expect("list runtime events");
assert_eq!(
events
.iter()
.map(|event| event.id.as_str())
.collect::<Vec<_>>(),
vec!["are_z_first", "are_m_second", "are_a_third"]
);
let tail = store
.list_ai_runtime_events("ai_runtime_order_user", "run_order", 2)
.expect("list runtime tail events");
assert_eq!(
tail.iter()
.map(|event| event.id.as_str())
.collect::<Vec<_>>(),
vec!["are_m_second", "are_a_third"]
);
}
#[test]
fn ai_runtime_session_management_renames_auto_titles_and_soft_deletes() {
let store = store();
+8 -4
View File
@@ -3146,10 +3146,14 @@ impl ControlPlaneStore for TursoControlPlaneStore {
let conn = self.lock_conn()?;
let mut stmt = conn.prepare(
"SELECT id, user_id, workspace_id, document_id, session_id, run_id, profile, acp_runtime, event_type, payload_json, created_at
FROM ai_runtime_events
WHERE user_id = ?1 AND run_id = ?2
ORDER BY created_at ASC
LIMIT ?3",
FROM (
SELECT id, user_id, workspace_id, document_id, session_id, run_id, profile, acp_runtime, event_type, payload_json, created_at
FROM ai_runtime_events
WHERE user_id = ?1 AND run_id = ?2
ORDER BY created_at DESC, id DESC
LIMIT ?3
)
ORDER BY created_at ASC, id ASC",
)?;
let rows = stmt
.query_map(
File diff suppressed because it is too large Load Diff
@@ -33,7 +33,7 @@ pub async fn status(
Query(body),
)
.await?;
Ok(payload)
Ok(compact_status_result_for_agent(payload))
}
pub async fn search(
@@ -235,18 +235,17 @@ fn ensure_weknora_scope(
.with_context(context))
}
fn compact_query_result_for_agent(payload: Value) -> Value {
let references = payload
pub(crate) fn compact_query_result_for_agent(payload: Value) -> Value {
let payload_references = payload
.get("references")
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.take(8)
.map(compact_reference_for_agent)
.collect::<Vec<_>>()
})
.cloned()
.unwrap_or_default();
let references = payload_references
.iter()
.take(8)
.map(compact_reference_for_agent)
.collect::<Vec<_>>();
let payload_citations = payload
.get("citations")
.and_then(Value::as_array)
@@ -255,16 +254,33 @@ fn compact_query_result_for_agent(payload: Value) -> Value {
let citations = if payload_citations.is_empty() {
citation_references_for_ui(&references)
.into_iter()
.take(8)
.map(compact_citation_for_agent)
.collect::<Vec<_>>()
} else {
citation_values_for_ui(&payload_citations)
.into_iter()
.take(8)
.map(compact_citation_for_agent)
.collect::<Vec<_>>()
};
let citation_markdowns = citations
let ui_citations = if payload_citations.is_empty() {
citation_references_for_ui(&payload_references)
} else {
citation_values_for_ui(&payload_citations)
}
.into_iter()
.take(8)
.map(compact_ui_citation_for_agent)
.collect::<Vec<_>>();
let citation_count = if payload_citations.is_empty() {
payload_references.len()
} else {
payload_citations.len()
};
let citation_markdowns = ui_citations
.iter()
.take(2)
.filter_map(|citation| {
citation
.get("citationMarkdown")
@@ -273,7 +289,6 @@ fn compact_query_result_for_agent(payload: Value) -> Value {
.map(ToOwned::to_owned)
})
.collect::<Vec<_>>();
let ui_citations = citations.clone();
let document_structure_index = payload
.get("documentStructureIndex")
.cloned()
@@ -285,8 +300,13 @@ fn compact_query_result_for_agent(payload: Value) -> Value {
"provider": payload.get("provider").cloned().unwrap_or_else(|| json!("weknora")),
"answerGuidance": "Final answers should answer the user in plain text. Do not copy citationMarkdown, citationUrl, /documents, mnote://, or search-engine wrapped local links into the answer. MNote UI will render citations[] / references[].citationMarkdown after the answer as clickable source locators. Treat rawMetadata.keywords and entity/relation counts only as query analysis, not proof that a source contains those words. Use references[].quote/contentDiagnostics as evidence. For book-like or skip-KG documents, call the tool with sourcePaths and includeDocumentStructureIndex=true; MNote fixes their effective retrieval mode to naive because they intentionally do not build KG. If documentStructureIndex is present, use it as a section map and call mnote.knowledge_rag.section_context with the section range when you need bounded chapter text for second-pass reading; do not cite the map itself unless the same claim appears in references[].quote or section_context text. If locatorDegraded is true, say the source location is degraded instead of inventing page or bbox. If contentDiagnostics.ocrTextExposed is false, say OCR text was not exposed in the returned reference instead of claiming OCR succeeded.",
"references": references,
"referenceCount": payload_references.len(),
"referencesTruncated": payload_references.len() > 8,
"citations": citations,
"citationCount": citation_count,
"citationsTruncated": citation_count > 8,
"citationMarkdowns": citation_markdowns,
"citationMarkdownsTruncated": ui_citations.len() > 2,
"uiCitations": ui_citations,
"documentStructureIndex": compact_document_structure_index_for_agent(&document_structure_index),
"citationRendering": "MNote UI appends uiCitations/citations after the final answer; agent must not hand-write citation links.",
@@ -303,6 +323,177 @@ fn compact_query_result_for_agent(payload: Value) -> Value {
})
}
pub(crate) fn compact_status_result_for_agent(payload: Value) -> Value {
let document_values = payload
.pointer("/documents/documents")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let documents = document_values
.iter()
.take(12)
.map(|document| {
json!({
"id": document.get("id").cloned().unwrap_or(Value::Null),
"filePath": document.get("filePath").cloned().unwrap_or(Value::Null),
"status": document.get("status").cloned().unwrap_or(Value::Null),
"statusGroup": document.get("statusGroup").cloned().unwrap_or(Value::Null),
"summary": compact_text_value(document.get("summary"), 240),
"chunksCount": document.get("chunksCount").cloned().unwrap_or(Value::Null),
"updatedAt": document.get("updatedAt").cloned().unwrap_or(Value::Null),
})
})
.collect::<Vec<_>>();
let registry_entries = payload
.pointer("/registry/entries")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let sources = registry_entries
.iter()
.take(16)
.map(|source| {
json!({
"sourceId": source.get("sourceId").cloned().unwrap_or(Value::Null),
"sourceRootRelativePath": source.get("sourceRootRelativePath").cloned().unwrap_or(Value::Null),
"sourceKind": source.get("sourceKind").cloned().unwrap_or(Value::Null),
"status": source.get("status").cloned().unwrap_or(Value::Null),
"provider": source.get("provider").cloned().unwrap_or(Value::Null),
"providerStatus": source.get("providerStatus").cloned().unwrap_or(Value::Null),
"lightRagStatus": source.get("lightRagStatus").cloned().unwrap_or(Value::Null),
"stale": source.get("stale").cloned().unwrap_or(Value::Bool(false)),
"retryRequired": source.get("retryRequired").cloned().unwrap_or(Value::Bool(false)),
"lastError": compact_text_value(source.get("lastError"), 320),
"updatedAtMs": source.get("updatedAtMs").cloned().unwrap_or(Value::Null),
})
})
.collect::<Vec<_>>();
let indexed_roots = payload
.pointer("/registry/indexedRoots")
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.take(12)
.map(|root| {
json!({
"rootRelativePath": root.get("rootRelativePath").cloned().unwrap_or(Value::Null),
"recursive": root.get("recursive").cloned().unwrap_or(Value::Bool(false)),
"runOnChange": root.get("runOnChange").cloned().unwrap_or(Value::Null),
"updatedAtMs": root.get("updatedAtMs").cloned().unwrap_or(Value::Null),
})
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
let knowledge_base_values = payload
.get("knowledgeBases")
.and_then(|value| {
value
.get("bases")
.and_then(Value::as_array)
.or_else(|| value.as_array())
})
.cloned()
.unwrap_or_default();
let knowledge_bases = knowledge_base_values
.iter()
.take(12)
.map(|base| {
json!({
"baseId": base.get("baseId").cloned().unwrap_or(Value::Null),
"name": base.get("name").cloned().unwrap_or(Value::Null),
"provider": base.get("provider").cloned().unwrap_or(Value::Null),
"providerKbId": base.get("providerKbId").cloned().unwrap_or(Value::Null),
"defaultToolEnabled": base.get("defaultToolEnabled").cloned().unwrap_or(Value::Bool(false)),
"canWrite": base.get("canWrite").cloned().unwrap_or(Value::Bool(false)),
"sourceCount": base.get("sourceCount").cloned().unwrap_or(Value::Null),
"chunkCount": base.get("chunkCount").cloned().unwrap_or(Value::Null),
"status": base.get("status").cloned().unwrap_or(Value::Null),
"updatedAtMs": base.get("updatedAtMs").cloned().unwrap_or(Value::Null),
})
})
.collect::<Vec<_>>();
let pipeline = payload.get("pipeline").unwrap_or(&Value::Null);
let rerank = payload.get("rerank").unwrap_or(&Value::Null);
json!({
"ok": payload.get("ok").cloned().unwrap_or_else(|| json!(true)),
"schema": "mnote.knowledge_rag.agent_status_result.v1",
"provider": payload.get("provider").cloned().unwrap_or(Value::Null),
"endpoint": payload.get("endpoint").cloned().unwrap_or(Value::Null),
"health": compact_health_for_agent(payload.get("health")),
"rerank": {
"enabled": rerank.get("enabled").cloned().unwrap_or(Value::Bool(false)),
"available": rerank.get("available").cloned().unwrap_or(Value::Bool(false)),
"status": rerank.get("status").cloned().unwrap_or(Value::Null),
"binding": rerank.get("binding").cloned().unwrap_or(Value::Null),
"model": rerank.get("model").or_else(|| rerank.get("rerankModel")).cloned().unwrap_or(Value::Null),
"minScore": rerank.get("minScore").cloned().unwrap_or(Value::Null),
},
"documents": {
"ok": payload.pointer("/documents/ok").cloned().unwrap_or(Value::Bool(false)),
"total": document_values.len(),
"statusGroups": payload.pointer("/documents/rawStatusGroups").cloned().unwrap_or_else(|| json!({})),
"items": documents,
"truncated": document_values.len() > 12,
},
"pipeline": {
"ok": pipeline.get("ok").cloned().unwrap_or(Value::Bool(false)),
"busy": pipeline.get("busy").cloned().unwrap_or(Value::Bool(false)),
"scanning": pipeline.get("scanning").cloned().unwrap_or(Value::Bool(false)),
"requestPending": pipeline.get("requestPending").cloned().unwrap_or(Value::Bool(false)),
"pendingEnqueues": pipeline.get("pendingEnqueues").cloned().unwrap_or(Value::Null),
"docs": pipeline.get("docs").cloned().unwrap_or(Value::Null),
"batches": pipeline.get("batches").cloned().unwrap_or(Value::Null),
"currentBatch": pipeline.get("currentBatch").cloned().unwrap_or(Value::Null),
"jobName": pipeline.get("jobName").cloned().unwrap_or(Value::Null),
"latestMessage": compact_text_value(pipeline.get("latestMessage"), 400),
"progress": pipeline.get("progress").cloned().unwrap_or(Value::Null),
},
"sourceRegistry": {
"workspaceId": payload.pointer("/registry/workspaceId").cloned().unwrap_or(Value::Null),
"rootUri": payload.pointer("/registry/rootUri").cloned().unwrap_or(Value::Null),
"updatedAtMs": payload.pointer("/registry/updatedAtMs").cloned().unwrap_or(Value::Null),
"sourceCount": registry_entries.len(),
"indexedRootCount": payload.pointer("/registry/indexedRoots").and_then(Value::as_array).map(Vec::len).unwrap_or(0),
"indexedRoots": indexed_roots,
"sources": sources,
"truncated": registry_entries.len() > 16,
},
"knowledgeBases": {
"total": knowledge_base_values.len(),
"items": knowledge_bases,
"truncated": knowledge_base_values.len() > 12,
},
"agentGuidance": "This is a compact provider/status summary for the agent. The full provider configuration, raw health payload, registry providerResponse, and complete document lists remain available to the MNote UI/API and are intentionally omitted from the model context.",
})
}
fn compact_health_for_agent(value: Option<&Value>) -> Value {
let nested = value.and_then(|health| health.get("health"));
json!({
"ok": value.and_then(|health| health.get("ok")).cloned().unwrap_or(Value::Bool(false)),
"status": nested.and_then(|health| health.get("status")).or_else(|| value.and_then(|health| health.get("status"))).cloned().unwrap_or(Value::Null),
"healthy": nested.and_then(|health| health.get("healthy")).or_else(|| value.and_then(|health| health.get("healthy"))).cloned().unwrap_or(Value::Null),
"provider": nested.and_then(|health| health.get("provider")).or_else(|| value.and_then(|health| health.get("provider"))).cloned().unwrap_or(Value::Null),
"version": nested.and_then(|health| health.get("version")).or_else(|| value.and_then(|health| health.get("version"))).cloned().unwrap_or(Value::Null),
"code": value.and_then(|health| health.get("code")).cloned().unwrap_or(Value::Null),
"message": compact_text_value(value.and_then(|health| health.get("message")), 320),
})
}
fn compact_text_value(value: Option<&Value>, max_chars: usize) -> Value {
value
.and_then(Value::as_str)
.map(|text| text.chars().take(max_chars).collect::<String>())
.map(Value::String)
.unwrap_or(Value::Null)
}
fn compact_document_structure_index_for_agent(value: &Value) -> Value {
if value.is_null() {
return Value::Null;
@@ -393,7 +584,7 @@ fn compact_structure_section_for_agent(section: &Value) -> Value {
})
}
fn compact_section_context_for_agent(payload: Value) -> Value {
pub(crate) fn compact_section_context_for_agent(payload: Value) -> Value {
let blocks = payload
.get("blocks")
.and_then(Value::as_array)
@@ -470,12 +661,7 @@ fn compact_reference_for_agent(reference: &Value) -> Value {
.get("displayQuote")
.and_then(Value::as_str)
.or_else(|| reference.get("quote").and_then(Value::as_str))
.map(|value| value.chars().take(700).collect::<String>())
.unwrap_or_default();
let locator_evidence_text = reference
.get("locatorEvidenceText")
.and_then(Value::as_str)
.map(|value| value.chars().take(700).collect::<String>())
.map(|value| value.chars().take(560).collect::<String>())
.unwrap_or_default();
let quote_diagnostics = reference
.get("contentDiagnostics")
@@ -490,16 +676,11 @@ fn compact_reference_for_agent(reference: &Value) -> Value {
"sourceRootRelativePath": reference.get("sourceRootRelativePath").cloned().unwrap_or(Value::Null),
"chunkId": reference.get("chunkId").cloned().unwrap_or(Value::Null),
"quote": quote,
"displayQuote": reference.get("displayQuote").cloned().unwrap_or(Value::Null),
"locatorEvidenceText": locator_evidence_text,
"quoteSource": reference.get("quoteSource").cloned().unwrap_or(Value::Null),
"locator": reference.get("locator").cloned().unwrap_or(Value::Null),
"locatorPrecision": reference.get("locatorPrecision").cloned().unwrap_or(Value::Null),
"locatorDegraded": reference.get("locatorDegraded").cloned().unwrap_or(Value::Bool(true)),
"contentDiagnostics": quote_diagnostics,
"citationDiagnostics": reference.get("citationDiagnostics").cloned().unwrap_or(Value::Null),
"citationMarkdown": reference.get("citationMarkdown").cloned().unwrap_or(Value::Null),
"citationUrl": reference.get("citationUrl").cloned().unwrap_or(Value::Null),
})
}
@@ -508,12 +689,7 @@ fn compact_citation_for_agent(citation: &Value) -> Value {
.get("displayQuote")
.and_then(Value::as_str)
.or_else(|| citation.get("quote").and_then(Value::as_str))
.map(|value| value.chars().take(420).collect::<String>())
.unwrap_or_default();
let locator_evidence_text = citation
.get("locatorEvidenceText")
.and_then(Value::as_str)
.map(|value| value.chars().take(420).collect::<String>())
.map(|value| value.chars().take(360).collect::<String>())
.unwrap_or_default();
json!({
"schema": citation.get("schema").cloned().unwrap_or_else(|| json!("mnote.knowledge_rag.citation.v1")),
@@ -521,21 +697,25 @@ fn compact_citation_for_agent(citation: &Value) -> Value {
"citationId": citation.get("citationId").cloned().unwrap_or(Value::Null),
"citationLabel": citation.get("citationLabel").cloned().unwrap_or(Value::Null),
"sourceId": citation.get("sourceId").cloned().unwrap_or(Value::Null),
"sourcePath": citation.get("sourcePath").cloned().unwrap_or(Value::Null),
"sourceRootRelativePath": citation.get("sourceRootRelativePath").cloned().unwrap_or(Value::Null),
"filePath": citation.get("filePath").or_else(|| citation.get("lightRagFilePath")).cloned().unwrap_or(Value::Null),
"chunkId": citation.get("chunkId").or_else(|| citation.get("lightRagChunkId")).cloned().unwrap_or(Value::Null),
"blockId": citation.get("blockId").cloned().unwrap_or(Value::Null),
"headingPath": citation.get("headingPath").cloned().unwrap_or_else(|| json!([])),
"quote": display_quote,
"displayQuote": citation.get("displayQuote").cloned().unwrap_or(Value::Null),
"locatorEvidenceText": locator_evidence_text,
"searchQuery": citation.get("searchQuery").cloned().unwrap_or(Value::Null),
"locatorPrecision": citation.get("locatorPrecision").cloned().unwrap_or(Value::Null),
"locatorDegraded": citation.get("locatorDegraded").cloned().unwrap_or(Value::Bool(true)),
})
}
fn compact_ui_citation_for_agent(citation: &Value) -> Value {
json!({
"citationId": citation.get("citationId").cloned().unwrap_or(Value::Null),
"citationLabel": citation.get("citationLabel").cloned().unwrap_or(Value::Null),
"sourceRootRelativePath": citation.get("sourceRootRelativePath").cloned().unwrap_or(Value::Null),
"headingPath": citation.get("headingPath").cloned().unwrap_or_else(|| json!([])),
"locatorPrecision": citation.get("locatorPrecision").cloned().unwrap_or(Value::Null),
"locatorDegraded": citation.get("locatorDegraded").cloned().unwrap_or(Value::Bool(true)),
"citationMarkdown": citation.get("citationMarkdown").cloned().unwrap_or(Value::Null),
"citationUrl": citation.get("citationUrl").cloned().unwrap_or(Value::Null),
"diagnostics": citation.get("diagnostics").or_else(|| citation.get("citationDiagnostics")).cloned().unwrap_or(Value::Null),
})
}
@@ -669,8 +849,9 @@ mod tests {
assert_eq!(compact["citations"][0]["citationId"], "c0de");
assert_eq!(compact["citations"][0]["headingPath"][0], "保护基");
assert_eq!(compact["citations"][0]["displayQuote"], "吡咯烷,5 h,90%");
assert_eq!(compact["citations"][0]["quote"], "吡咯烷,5 h,90%");
assert!(compact["citations"][0].get("rawQuote").is_none());
assert!(compact["citations"][0].get("citationMarkdown").is_none());
assert_eq!(
compact["uiCitations"][0]["citationMarkdown"].as_str(),
Some("[docs/a.docx](/documents/a)")
@@ -701,4 +882,132 @@ mod tests {
Some("chunk")
);
}
#[test]
fn compact_status_result_keeps_capability_summary_without_large_payloads() {
let documents = (0..20)
.map(|index| {
json!({
"id": format!("doc-{index}"),
"filePath": format!("docs/{index}.md"),
"status": "processed",
"statusGroup": "processed",
"summary": "x".repeat(2_000),
"chunksCount": 10,
})
})
.collect::<Vec<_>>();
let sources = (0..24)
.map(|index| {
json!({
"sourceId": format!("source-{index}"),
"sourceRootRelativePath": format!("docs/{index}.md"),
"providerStatus": "indexed",
"providerResponse": {"raw": "y".repeat(4_000)},
"lastError": "z".repeat(1_000),
})
})
.collect::<Vec<_>>();
let payload = json!({
"ok": true,
"provider": "lightrag",
"providerConfig": {"apiKey": "must-not-reach-agent"},
"endpoint": "http://127.0.0.1:9621",
"health": {"ok": true, "health": {"status": "healthy", "raw": "h".repeat(8_000)}},
"rerank": {"enabled": true, "available": true, "status": "ready", "model": "reranker"},
"documents": {
"ok": true,
"rawStatusGroups": {"processed": 20},
"documents": documents,
},
"pipeline": {
"ok": true,
"busy": false,
"historyMessages": vec!["history".repeat(1_000); 100],
"latestMessage": "latest".repeat(500),
},
"registry": {
"workspaceId": "workspace",
"rootUri": "file:///workspace",
"indexedRoots": [{"rootRelativePath": "docs", "recursive": true}],
"entries": sources,
},
});
let compact = compact_status_result_for_agent(payload);
assert_eq!(
compact["schema"],
"mnote.knowledge_rag.agent_status_result.v1"
);
assert_eq!(compact["documents"]["total"], 20);
assert_eq!(compact["documents"]["items"].as_array().unwrap().len(), 12);
assert_eq!(compact["sourceRegistry"]["sourceCount"], 24);
assert_eq!(
compact["sourceRegistry"]["sources"]
.as_array()
.unwrap()
.len(),
16
);
assert!(compact.get("providerConfig").is_none());
assert!(compact["health"].get("raw").is_none());
assert!(compact["sourceRegistry"]["sources"][0]
.get("providerResponse")
.is_none());
assert!(
serde_json::to_vec(&compact)
.expect("serialize compact status")
.len()
< 40_000
);
}
#[test]
fn compact_query_result_caps_repeated_evidence_and_ui_locators() {
let references = (0..40)
.map(|index| {
json!({
"citationId": format!("ref-{index}"),
"sourceRootRelativePath": format!("docs/{index}.md"),
"displayQuote": "evidence".repeat(500),
"locatorEvidenceText": "duplicate evidence".repeat(500),
"citationMarkdown": format!("[docs/{index}.md](/documents/{})", "x".repeat(4_000)),
})
})
.collect::<Vec<_>>();
let citations = (0..40)
.map(|index| {
json!({
"citationId": format!("citation-{index}"),
"citationLabel": format!("[{index}]"),
"sourceRootRelativePath": format!("docs/{index}.md"),
"displayQuote": "citation evidence".repeat(500),
"sourcePath": format!("/very/long/{}", "path".repeat(1_000)),
"citationMarkdown": format!("[docs/{index}.md](/documents/{})", "y".repeat(4_000)),
})
})
.collect::<Vec<_>>();
let compact = compact_query_result_for_agent(json!({
"references": references,
"citations": citations,
}));
assert_eq!(compact["references"].as_array().unwrap().len(), 8);
assert_eq!(compact["citations"].as_array().unwrap().len(), 8);
assert_eq!(compact["uiCitations"].as_array().unwrap().len(), 8);
assert_eq!(compact["referenceCount"], 40);
assert_eq!(compact["citationCount"], 40);
assert_eq!(compact["referencesTruncated"], true);
assert_eq!(compact["citationsTruncated"], true);
assert!(compact["references"][0].get("citationMarkdown").is_none());
assert!(compact["citations"][0].get("citationMarkdown").is_none());
assert!(
serde_json::to_vec(&compact)
.expect("serialize compact query")
.len()
< 60_000
);
}
}
@@ -1177,7 +1177,8 @@ pub async fn admin_put_user_settings(
// ─── Constants ──────────────────────────────────────────────────────────
const DEFAULT_PI_MODEL: &str = "omniroute/freefirst";
const DEFAULT_PI_MODEL: &str = "omniroute/gpt-5.4-mini";
const FREEFIRST_PI_MODEL: &str = "omniroute/freefirst";
const LIGHTRAG_PROVIDER: &str = "lightrag";
const LIGHTRAG_PROVIDER_DESCRIPTION: &str = "MNote 知识库默认提供者(LightRAG)";
const SOURCE_OF_TRUTH: &str = "directory_grants";
@@ -1904,6 +1905,12 @@ pub(crate) fn load_effective_ai_runtime_policy(
.into_iter()
.filter_map(|entry| normalize_model_ref(Some(&default_provider), &entry.id))
.collect::<Vec<_>>();
if !allowed_models.iter().any(|model| model == FREEFIRST_PI_MODEL) {
allowed_models.push(FREEFIRST_PI_MODEL.to_string());
}
if !allowed_models.iter().any(|model| model == DEFAULT_PI_MODEL) {
allowed_models.push(DEFAULT_PI_MODEL.to_string());
}
allowed_models.sort();
allowed_models.dedup();
+16
View File
@@ -601,6 +601,13 @@ pub fn build_router(state: AppState) -> Router {
.route("/api/page-ai/pi/start", post(page_ai_pi::start))
.route("/api/page-ai/pi/send", post(page_ai_pi::send))
.route("/api/page-ai/pi/abort", post(page_ai_pi::abort))
.route("/api/page-ai/pi/configure", post(page_ai_pi::configure))
.route("/api/page-ai/pi/state", post(page_ai_pi::state))
.route("/api/page-ai/pi/compact", post(page_ai_pi::compact))
.route(
"/api/page-ai/pi/queue-config",
post(page_ai_pi::queue_config),
)
.route("/api/page-ai/pi/ui-response", post(page_ai_pi::ui_response))
.route(
"/api/page-ai/pi/ui-request-bridge",
@@ -631,6 +638,15 @@ pub fn build_router(state: AppState) -> Router {
post(page_ai_pi::mcp_call_bridge),
)
.route("/page-ai/pi", get(page_ai_pi::shell))
.route(
"/api/page-ai/pi/sessions/{session_id}/tree",
get(page_ai_pi::session_tree),
)
.route("/api/page-ai/pi/fork", post(page_ai_pi::fork_pi_session))
.route(
"/api/page-ai/pi/artifacts/{tool_event_id}/diff",
get(page_ai_pi::artifact_diff),
)
.route(
"/api/sidebar/shortcuts",
get(sidebar_shortcuts::list_shortcuts).post(sidebar_shortcuts::upsert_shortcut),