feat(wiki): knowledge-layer filter on wiki search

This commit is contained in:
matevip 2026-05-31 07:57:18 +08:00
parent ad1f5b4a15
commit cc8c9cf951
2 changed files with 62 additions and 4 deletions

View File

@ -275,6 +275,7 @@ public class WikiTool {
@ToolParam(description = "Search query") String query,
@ToolParam(description = "Mode: keyword|semantic|hybrid (default: hybrid)", required = false) String mode,
@ToolParam(description = "Max results (default 5, max 20)", required = false) Integer topK,
@ToolParam(description = "Knowledge layer filter: fact | experience | all (default all). 'fact' = factual pages (and unlayered pages); 'experience' = synthesis/analysis pages.", required = false) String layer,
@ToolParam(description = "Target knowledge base name (from wiki_list_kbs). Omit to use the agent's primary KB; switch to `kbId` when two KBs share the name.", required = false) String kbName,
@ToolParam(description = "Numeric KB id from wiki_list_kbs. Use when `kbName` returns an ambiguous-name error.", required = false) Long kbId) {
@ -289,15 +290,17 @@ public class WikiTool {
int k = (topK != null && topK > 0) ? Math.min(topK, 20) : 5;
List<PageSearchResult> results = hybridRetriever.search(kbId, query, mode, k);
// Drop hits whose page type this agent may not read, so search cannot
// surface pages a direct read would refuse.
// Drop hits this agent may not read (by page type) or that fall outside
// the requested knowledge layer. Both need the page, so fetch it once.
WikiPageTypePermissionService.Access access = pageTypeAccess(agentId, kbId);
if (access != null) {
boolean layerFilter = layer != null && !layer.isBlank() && !"all".equalsIgnoreCase(layer.trim());
if (access != null || layerFilter) {
final Long resolvedKbId = kbId;
final String layerWanted = layer;
results = results.stream()
.filter(r -> {
WikiPageEntity p = pageService.getBySlug(resolvedKbId, r.slug());
return canRead(access, p);
return canRead(access, p) && matchesLayer(p == null ? null : p.getKnowledgeLayer(), layerWanted);
})
.toList();
}
@ -1069,6 +1072,20 @@ public class WikiTool {
return access == null || access.canRead(pageType);
}
/**
* Whether a page's knowledge layer matches a retrieval filter. A null/blank
* or {@code all} filter matches everything; an unlayered page counts as
* {@code fact} (the RFC default), so {@code layer=fact} includes legacy
* pages while {@code layer=experience} returns only experience pages.
*/
static boolean matchesLayer(String pageLayer, String filter) {
if (filter == null || filter.isBlank() || "all".equalsIgnoreCase(filter.trim())) {
return true;
}
String effective = (pageLayer == null || pageLayer.isBlank()) ? "fact" : pageLayer.trim().toLowerCase();
return effective.equals(filter.trim().toLowerCase());
}
/**
* Gate a write/mutate operation by pageType permission. Returns an error
* JSON string to short-circuit the tool when the write is not permitted, or

View File

@ -0,0 +1,41 @@
package vip.mate.wiki.tool;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Unit tests for the retrieval knowledge-layer filter ({@link WikiTool#matchesLayer}).
*/
class WikiToolLayerFilterTest {
@Test
void noFilterOrAll_matchesEverything() {
assertTrue(WikiTool.matchesLayer("experience", null));
assertTrue(WikiTool.matchesLayer("experience", ""));
assertTrue(WikiTool.matchesLayer("experience", "all"));
assertTrue(WikiTool.matchesLayer(null, "all"));
}
@Test
void factFilter_includesUnlayeredPages() {
assertTrue(WikiTool.matchesLayer(null, "fact")); // legacy / unlayered counts as fact
assertTrue(WikiTool.matchesLayer("", "fact"));
assertTrue(WikiTool.matchesLayer("fact", "fact"));
assertFalse(WikiTool.matchesLayer("experience", "fact"));
}
@Test
void experienceFilter_excludesFactAndUnlayered() {
assertTrue(WikiTool.matchesLayer("experience", "experience"));
assertFalse(WikiTool.matchesLayer("fact", "experience"));
assertFalse(WikiTool.matchesLayer(null, "experience"));
}
@Test
void caseInsensitive() {
assertTrue(WikiTool.matchesLayer("Experience", "EXPERIENCE"));
assertTrue(WikiTool.matchesLayer("FACT", "fact"));
}
}