fix(wiki): preserve agent id tool inputs

This commit is contained in:
matevip 2026-08-13 03:56:03 -04:00
parent b1f3332ae1
commit 7bf18e6af8
4 changed files with 120 additions and 45 deletions

View File

@ -154,9 +154,10 @@ public class WikiTool {
- boundToAgent true if the KB is explicitly bound to this agent - boundToAgent true if the KB is explicitly bound to this agent
""") """)
public String wiki_list_kbs( public String wiki_list_kbs(
@ToolParam(description = "Agent ID") Long agentId) { @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId) {
List<WikiKnowledgeBaseEntity> kbs = kbService.listByAgentId(agentId); Long parsedAgentId = parseAgentId(agentId);
WikiKnowledgeBaseEntity primary = kbService.resolvePrimaryKb(agentId); List<WikiKnowledgeBaseEntity> kbs = kbService.listByAgentId(parsedAgentId);
WikiKnowledgeBaseEntity primary = kbService.resolvePrimaryKb(parsedAgentId);
Long primaryId = primary == null ? null : primary.getId(); Long primaryId = primary == null ? null : primary.getId();
JSONArray arr = new JSONArray(); JSONArray arr = new JSONArray();
@ -195,7 +196,7 @@ public class WikiTool {
consult that page first call this tool with the bare slug before answering. consult that page first call this tool with the bare slug before answering.
""") """)
public String wiki_read_page( public String wiki_read_page(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Page slug") String slug, @ToolParam(description = "Page slug") String slug,
@ToolParam(description = "Max characters to return (null = full page)", required = false) Integer maxChars, @ToolParam(description = "Max characters to return (null = full page)", required = false) Integer maxChars,
@ToolParam(description = "Section heading to extract (null = all sections)", required = false) String sectionHeading, @ToolParam(description = "Section heading to extract (null = all sections)", required = false) String sectionHeading,
@ -247,7 +248,7 @@ public class WikiTool {
Without query returns all pages (use only for small KBs). Without query returns all pages (use only for small KBs).
""") """)
public String wiki_list_pages( public String wiki_list_pages(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Title keyword filter (optional)", required = false) String query, @ToolParam(description = "Title keyword filter (optional)", required = false) String query,
@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 = "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) { @ToolParam(description = "Numeric KB id from wiki_list_kbs. Use when `kbName` returns an ambiguous-name error.", required = false) Long kbId) {
@ -307,7 +308,7 @@ public class WikiTool {
When using wiki information in your answer, always cite the source page title. When using wiki information in your answer, always cite the source page title.
""") """)
public String wiki_search_pages( public String wiki_search_pages(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Search query") String query, @ToolParam(description = "Search query") String query,
@ToolParam(description = "Mode: keyword|semantic|hybrid (default: hybrid)", required = false) String mode, @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 = "Max results (default 5, max 20)", required = false) Integer topK,
@ -374,7 +375,7 @@ public class WikiTool {
When using retrieved content in your answer, cite the source page title shown in each result. When using retrieved content in your answer, cite the source page title shown in each result.
""") """)
public String wiki_semantic_search( public String wiki_semantic_search(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Natural language query") String query, @ToolParam(description = "Natural language query") String query,
@ToolParam(description = "Max results (default 5)", required = false) Integer topK, @ToolParam(description = "Max results (default 5)", required = false) Integer topK,
@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 = "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,
@ -441,7 +442,7 @@ public class WikiTool {
Returns file names, types, and paths of the original documents. Returns file names, types, and paths of the original documents.
""") """)
public String wiki_trace_source( public String wiki_trace_source(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Page slug") String slug, @ToolParam(description = "Page slug") String slug,
@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 = "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) { @ToolParam(description = "Numeric KB id from wiki_list_kbs. Use when `kbName` returns an ambiguous-name error.", required = false) Long kbId) {
@ -474,7 +475,7 @@ public class WikiTool {
Content should be Markdown. Slug is auto-generated from title. Content should be Markdown. Slug is auto-generated from title.
""") """)
public String wiki_create_page( public String wiki_create_page(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Page title") String title, @ToolParam(description = "Page title") String title,
@ToolParam(description = "Page content (Markdown)") String content, @ToolParam(description = "Page content (Markdown)") String content,
@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 = "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,
@ -561,7 +562,7 @@ public class WikiTool {
Set slug to control the page slug; otherwise it's derived from the topic. Set slug to control the page slug; otherwise it's derived from the topic.
""") """)
public String wiki_compile_page( public String wiki_compile_page(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Topic to compile a page about (natural language)") String topic, @ToolParam(description = "Topic to compile a page about (natural language)") String topic,
@ToolParam(description = "Optional explicit slug for the page", required = false) String slug, @ToolParam(description = "Optional explicit slug for the page", required = false) String slug,
@ToolParam(description = "Max evidence chunks (default 8, max 20)", required = false) Integer maxEvidenceChunks, @ToolParam(description = "Max evidence chunks (default 8, max 20)", required = false) Integer maxEvidenceChunks,
@ -618,7 +619,7 @@ public class WikiTool {
page; protected/system pages can still be read explicitly here. page; protected/system pages can still be read explicitly here.
""") """)
public String wiki_read_many( public String wiki_read_many(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Comma-separated slugs (max 10)") String slugs, @ToolParam(description = "Comma-separated slugs (max 10)") String slugs,
@ToolParam(description = "Max chars returned per page (default 2000, max 8000)", required = false) Integer maxCharsPerPage, @ToolParam(description = "Max chars returned per page (default 2000, max 8000)", required = false) Integer maxCharsPerPage,
@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 = "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,
@ -670,7 +671,7 @@ public class WikiTool {
System pages (overview / log) cannot be archived. System pages (overview / log) cannot be archived.
""") """)
public String wiki_archive_page( public String wiki_archive_page(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Page slug to archive") String slug, @ToolParam(description = "Page slug to archive") String slug,
@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 = "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) { @ToolParam(description = "Numeric KB id from wiki_list_kbs. Use when `kbName` returns an ambiguous-name error.", required = false) Long kbId) {
@ -682,14 +683,14 @@ public class WikiTool {
list / search / related results again. No-op when the page wasn't archived. list / search / related results again. No-op when the page wasn't archived.
""") """)
public String wiki_unarchive_page( public String wiki_unarchive_page(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Page slug to unarchive") String slug, @ToolParam(description = "Page slug to unarchive") String slug,
@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 = "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) { @ToolParam(description = "Numeric KB id from wiki_list_kbs. Use when `kbName` returns an ambiguous-name error.", required = false) Long kbId) {
return setArchivedTool(agentId, slug, false, "unarchived", kbName, kbId); return setArchivedTool(agentId, slug, false, "unarchived", kbName, kbId);
} }
private String setArchivedTool(Long agentId, String slug, boolean archive, String verb, String kbName, Long kbId) { private String setArchivedTool(String agentId, String slug, boolean archive, String verb, String kbName, Long kbId) {
if (slug == null || slug.isBlank()) return error("slug is required"); if (slug == null || slug.isBlank()) return error("slug is required");
KbResolution kbRes = resolveKb(agentId, kbName, kbId); KbResolution kbRes = resolveKb(agentId, kbName, kbId);
if (kbRes.hasError()) return kbRes.errorJson(); if (kbRes.hasError()) return kbRes.errorJson();
@ -725,7 +726,7 @@ public class WikiTool {
Delete an AI-generated wiki page. Cannot delete manually curated pages. Delete an AI-generated wiki page. Cannot delete manually curated pages.
""") """)
public String wiki_delete_page( public String wiki_delete_page(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Page slug to delete") String slug, @ToolParam(description = "Page slug to delete") String slug,
@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 = "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) { @ToolParam(description = "Numeric KB id from wiki_list_kbs. Use when `kbName` returns an ambiguous-name error.", required = false) Long kbId) {
@ -781,7 +782,7 @@ public class WikiTool {
semantic similarity). More reliable than keyword search for discovering connected knowledge. semantic similarity). More reliable than keyword search for discovering connected knowledge.
""") """)
public String wiki_related_pages( public String wiki_related_pages(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Page slug") String slug, @ToolParam(description = "Page slug") String slug,
@ToolParam(description = "Max results (default 5, max 10)", required = false) Integer topK, @ToolParam(description = "Max results (default 5, max 10)", required = false) Integer topK,
@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 = "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,
@ -819,7 +820,7 @@ public class WikiTool {
Explain why two wiki pages are related. Returns signal breakdown with scores. Explain why two wiki pages are related. Returns signal breakdown with scores.
""") """)
public String wiki_explain_relation( public String wiki_explain_relation(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "First page slug") String slugA, @ToolParam(description = "First page slug") String slugA,
@ToolParam(description = "Second page slug") String slugB, @ToolParam(description = "Second page slug") String slugB,
@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 = "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,
@ -854,7 +855,7 @@ public class WikiTool {
Does NOT regenerate content only adds [[wikilink]] cross-references. Does NOT regenerate content only adds [[wikilink]] cross-references.
""") """)
public String wiki_enrich_page( public String wiki_enrich_page(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Page slug") String slug, @ToolParam(description = "Page slug") String slug,
@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 = "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) { @ToolParam(description = "Numeric KB id from wiki_list_kbs. Use when `kbName` returns an ambiguous-name error.", required = false) Long kbId) {
@ -898,7 +899,7 @@ public class WikiTool {
is re-derived from the new content unless you pass one explicitly. is re-derived from the new content unless you pass one explicitly.
""") """)
public String wiki_update_page( public String wiki_update_page(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Slug of the page to update (from wiki_list_pages / wiki_read_page)") String slug, @ToolParam(description = "Slug of the page to update (from wiki_list_pages / wiki_read_page)") String slug,
@ToolParam(description = "New full Markdown content for the page body") String content, @ToolParam(description = "New full Markdown content for the page body") String content,
@ToolParam(description = "New one-line summary (optional; omit to auto-derive from content)", required = false) String summary, @ToolParam(description = "New one-line summary (optional; omit to auto-derive from content)", required = false) String summary,
@ -944,7 +945,7 @@ public class WikiTool {
relying on experience/analysis pages. relying on experience/analysis pages.
""") """)
public String wiki_stale_pages( public String wiki_stale_pages(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@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 = "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) { @ToolParam(description = "Numeric KB id from wiki_list_kbs. Use when `kbName` returns an ambiguous-name error.", required = false) Long kbId) {
@ -980,7 +981,7 @@ public class WikiTool {
human title, and a description of what the prompt produces. human title, and a description of what the prompt produces.
""") """)
public String wiki_list_transformations( public String wiki_list_transformations(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@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 = "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) { @ToolParam(description = "Numeric KB id from wiki_list_kbs. Use when `kbName` returns an ambiguous-name error.", required = false) Long kbId) {
KbResolution kbRes = resolveKb(agentId, kbName, kbId); KbResolution kbRes = resolveKb(agentId, kbName, kbId);
@ -1010,7 +1011,7 @@ public class WikiTool {
The run is also persisted so the result is visible in the wiki UI. The run is also persisted so the result is visible in the wiki UI.
""") """)
public String wiki_apply_transformation( public String wiki_apply_transformation(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Transformation name (from wiki_list_transformations)") String name, @ToolParam(description = "Transformation name (from wiki_list_transformations)") String name,
@ToolParam(description = "Raw material ID to run the transformation against") Long rawId, @ToolParam(description = "Raw material ID to run the transformation against") Long rawId,
@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 = "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,
@ -1065,7 +1066,7 @@ public class WikiTool {
is persisted in the wiki UI; pass slug (not page id) for convenience. is persisted in the wiki UI; pass slug (not page id) for convenience.
""") """)
public String wiki_apply_transformation_to_page( public String wiki_apply_transformation_to_page(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Transformation name (from wiki_list_transformations)") String name, @ToolParam(description = "Transformation name (from wiki_list_transformations)") String name,
@ToolParam(description = "Source wiki page slug to run the transformation against") String slug, @ToolParam(description = "Source wiki page slug to run the transformation against") String slug,
@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 = "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,
@ -1127,7 +1128,7 @@ public class WikiTool {
account). Idempotent re-running upserts the same slug. account). Idempotent re-running upserts the same slug.
""") """)
public String wiki_aggregate_transformation( public String wiki_aggregate_transformation(
@ToolParam(description = "Agent ID") Long agentId, @ToolParam(description = "Agent ID. Must be passed as a string to preserve large integer precision") String agentId,
@ToolParam(description = "Transformation name (from wiki_list_transformations)") String name, @ToolParam(description = "Transformation name (from wiki_list_transformations)") String name,
@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 = "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) { @ToolParam(description = "Numeric KB id from wiki_list_kbs. Use when `kbName` returns an ambiguous-name error.", required = false) Long kbId) {
@ -1183,6 +1184,18 @@ public class WikiTool {
return resolveKbId(agentId, null, null); return resolveKbId(agentId, null, null);
} }
private Long parseAgentId(String agentId) {
String trimmed = agentId != null ? agentId.trim() : "";
if (trimmed.isEmpty()) {
throw new IllegalArgumentException("agentId is required");
}
try {
return Long.parseLong(trimmed);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("agentId must be a numeric string");
}
}
/** /**
* Outcome of resolving a KB for a tool call. Exactly one of * Outcome of resolving a KB for a tool call. Exactly one of
* {@code kbId} / {@code errorJson} is non-null: * {@code kbId} / {@code errorJson} is non-null:
@ -1210,6 +1223,10 @@ public class WikiTool {
return pageTypePermissionService.resolve(agentId, kbId); return pageTypePermissionService.resolve(agentId, kbId);
} }
private WikiPageTypePermissionService.Access pageTypeAccess(String agentId, Long kbId) {
return pageTypeAccess(parseAgentId(agentId), kbId);
}
/** Whether the resolved access permits reading {@code page}. Null-safe. */ /** Whether the resolved access permits reading {@code page}. Null-safe. */
private boolean canRead(WikiPageTypePermissionService.Access access, WikiPageEntity page) { private boolean canRead(WikiPageTypePermissionService.Access access, WikiPageEntity page) {
return access == null || page == null || access.canRead(page.getPageType()); return access == null || page == null || access.canRead(page.getPageType());
@ -1273,6 +1290,11 @@ public class WikiTool {
}; };
} }
private String checkWrite(String agentId, Long kbId, String pageType,
WikiPageTypePermissionService.WriteOp op) {
return checkWrite(parseAgentId(agentId), kbId, pageType, op);
}
/** /**
* Record a pending approval for an {@code APPROVAL_REQUIRED} wiki write, * Record a pending approval for an {@code APPROVAL_REQUIRED} wiki write,
* keyed to the current conversation via {@link ChatOriginHolder}. Best-effort: * keyed to the current conversation via {@link ChatOriginHolder}. Best-effort:
@ -1390,6 +1412,10 @@ public class WikiTool {
return KbResolution.ok(primary.getId()); return KbResolution.ok(primary.getId());
} }
private KbResolution resolveKb(String agentId, String kbName, Long kbId) {
return resolveKb(parseAgentId(agentId), kbName, kbId);
}
/** /**
* Legacy 2-arg routing kept for the tool methods that haven't been * Legacy 2-arg routing kept for the tool methods that haven't been
* widened to accept {@code kbId} yet. Always returns null when the * widened to accept {@code kbId} yet. Always returns null when the

View File

@ -0,0 +1,47 @@
package vip.mate.wiki.tool;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.ai.support.ToolCallbacks;
import org.springframework.ai.tool.ToolCallback;
import vip.mate.wiki.service.HybridRetriever;
import vip.mate.wiki.service.WikiKnowledgeBaseService;
import vip.mate.wiki.service.WikiPageService;
import vip.mate.wiki.service.WikiPageTypePermissionService;
import vip.mate.wiki.service.WikiRawMaterialService;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
class WikiToolIdSchemaTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
@Test
@DisplayName("wiki tools publish agentId as a string parameter so LLM tool calls preserve precision")
void wikiToolAgentIdSchemasAreString() throws Exception {
WikiTool tool = new WikiTool(
mock(WikiPageService.class),
mock(WikiKnowledgeBaseService.class),
mock(WikiRawMaterialService.class),
mock(HybridRetriever.class),
new ObjectMapper(),
mock(WikiPageTypePermissionService.class));
int checked = 0;
for (ToolCallback callback : ToolCallbacks.from(tool)) {
JsonNode root = MAPPER.readTree(callback.getToolDefinition().inputSchema());
JsonNode agentId = root.at("/properties/agentId/type");
if (!agentId.isMissingNode()) {
assertThat(agentId.asText())
.as(callback.getToolDefinition().name())
.isEqualTo("string");
checked++;
}
}
assertThat(checked).isGreaterThan(0);
}
}

View File

@ -51,6 +51,7 @@ import static org.mockito.Mockito.when;
class WikiToolKbNameRoutingTest { class WikiToolKbNameRoutingTest {
private static final Long AGENT = 7L; private static final Long AGENT = 7L;
private static final String AGENT_PARAM = String.valueOf(AGENT);
private static final long PRIMARY_KB = 100L; private static final long PRIMARY_KB = 100L;
private static final long OTHER_KB = 200L; private static final long OTHER_KB = 200L;
private static final long DUP_BOUND_KB = 300L; private static final long DUP_BOUND_KB = 300L;
@ -109,7 +110,7 @@ class WikiToolKbNameRoutingTest {
wirePages(); wirePages();
when(kbService.resolvePrimaryKb(AGENT)).thenReturn(kb(PRIMARY_KB, "Primary", AGENT)); when(kbService.resolvePrimaryKb(AGENT)).thenReturn(kb(PRIMARY_KB, "Primary", AGENT));
String json = tool.wiki_list_pages(AGENT, null, null, null); String json = tool.wiki_list_pages(AGENT_PARAM, null, null, null);
JSONObject obj = JSONUtil.parseObj(json); JSONObject obj = JSONUtil.parseObj(json);
JSONArray pages = obj.getJSONArray("pages"); JSONArray pages = obj.getJSONArray("pages");
@ -126,7 +127,7 @@ class WikiToolKbNameRoutingTest {
when(kbService.resolvePrimaryKb(AGENT)).thenReturn(kb(PRIMARY_KB, "Primary", AGENT)); when(kbService.resolvePrimaryKb(AGENT)).thenReturn(kb(PRIMARY_KB, "Primary", AGENT));
when(kbService.findAllByName(AGENT, "Other")).thenReturn(List.of(kb(OTHER_KB, "Other", null))); when(kbService.findAllByName(AGENT, "Other")).thenReturn(List.of(kb(OTHER_KB, "Other", null)));
String json = tool.wiki_list_pages(AGENT, null, "Other", null); String json = tool.wiki_list_pages(AGENT_PARAM, null, "Other", null);
JSONObject obj = JSONUtil.parseObj(json); JSONObject obj = JSONUtil.parseObj(json);
JSONArray pages = obj.getJSONArray("pages"); JSONArray pages = obj.getJSONArray("pages");
@ -141,7 +142,7 @@ class WikiToolKbNameRoutingTest {
when(kbService.resolvePrimaryKb(AGENT)).thenReturn(kb(PRIMARY_KB, "Primary", AGENT)); when(kbService.resolvePrimaryKb(AGENT)).thenReturn(kb(PRIMARY_KB, "Primary", AGENT));
when(kbService.findVisibleById(AGENT, OTHER_KB)).thenReturn(kb(OTHER_KB, "Other", null)); when(kbService.findVisibleById(AGENT, OTHER_KB)).thenReturn(kb(OTHER_KB, "Other", null));
String json = tool.wiki_list_pages(AGENT, null, null, OTHER_KB); String json = tool.wiki_list_pages(AGENT_PARAM, null, null, OTHER_KB);
JSONObject obj = JSONUtil.parseObj(json); JSONObject obj = JSONUtil.parseObj(json);
assertThat(obj.getJSONArray("pages").getJSONObject(0).getStr("slug")) assertThat(obj.getJSONArray("pages").getJSONObject(0).getStr("slug"))
@ -156,7 +157,7 @@ class WikiToolKbNameRoutingTest {
// Deliberately do NOT stub findAllByName if the tool consulted // Deliberately do NOT stub findAllByName if the tool consulted
// kbName at all (or fell back to primary), the call would NPE. // kbName at all (or fell back to primary), the call would NPE.
String json = tool.wiki_list_pages(AGENT, null, "anything", OTHER_KB); String json = tool.wiki_list_pages(AGENT_PARAM, null, "anything", OTHER_KB);
JSONObject obj = JSONUtil.parseObj(json); JSONObject obj = JSONUtil.parseObj(json);
assertThat(obj.getJSONArray("pages").getJSONObject(0).getStr("slug")) assertThat(obj.getJSONArray("pages").getJSONObject(0).getStr("slug"))
.isEqualTo("other-only-slug"); .isEqualTo("other-only-slug");
@ -171,7 +172,7 @@ class WikiToolKbNameRoutingTest {
// Primary still mockable; the routing must NOT silently fall through. // Primary still mockable; the routing must NOT silently fall through.
when(kbService.resolvePrimaryKb(AGENT)).thenReturn(kb(PRIMARY_KB, "Primary", AGENT)); when(kbService.resolvePrimaryKb(AGENT)).thenReturn(kb(PRIMARY_KB, "Primary", AGENT));
String json = tool.wiki_list_pages(AGENT, null, "Bogus", null); String json = tool.wiki_list_pages(AGENT_PARAM, null, "Bogus", null);
JSONObject obj = JSONUtil.parseObj(json); JSONObject obj = JSONUtil.parseObj(json);
assertThat(obj.getStr("error")) assertThat(obj.getStr("error"))
@ -188,7 +189,7 @@ class WikiToolKbNameRoutingTest {
kb(DUP_SHARED_KB, "Docs", null))); kb(DUP_SHARED_KB, "Docs", null)));
when(kbService.resolvePrimaryKb(AGENT)).thenReturn(kb(PRIMARY_KB, "Primary", AGENT)); when(kbService.resolvePrimaryKb(AGENT)).thenReturn(kb(PRIMARY_KB, "Primary", AGENT));
String json = tool.wiki_list_pages(AGENT, null, "Docs", null); String json = tool.wiki_list_pages(AGENT_PARAM, null, "Docs", null);
JSONObject obj = JSONUtil.parseObj(json); JSONObject obj = JSONUtil.parseObj(json);
// Error must be ambiguity-flavoured so the LLM knows to retry with kbId. // Error must be ambiguity-flavoured so the LLM knows to retry with kbId.
@ -217,7 +218,7 @@ class WikiToolKbNameRoutingTest {
// would return null and surface a spurious "kbId=0 not visible" error, // would return null and surface a spurious "kbId=0 not visible" error,
// which is exactly the production regression this test prevents. // which is exactly the production regression this test prevents.
String json = tool.wiki_list_pages(AGENT, null, null, 0L); String json = tool.wiki_list_pages(AGENT_PARAM, null, null, 0L);
JSONObject obj = JSONUtil.parseObj(json); JSONObject obj = JSONUtil.parseObj(json);
assertThat(obj.getStr("error")) assertThat(obj.getStr("error"))
@ -234,7 +235,7 @@ class WikiToolKbNameRoutingTest {
when(kbService.findVisibleById(AGENT, 99999L)).thenReturn(null); when(kbService.findVisibleById(AGENT, 99999L)).thenReturn(null);
when(kbService.resolvePrimaryKb(AGENT)).thenReturn(kb(PRIMARY_KB, "Primary", AGENT)); when(kbService.resolvePrimaryKb(AGENT)).thenReturn(kb(PRIMARY_KB, "Primary", AGENT));
String json = tool.wiki_list_pages(AGENT, null, null, 99999L); String json = tool.wiki_list_pages(AGENT_PARAM, null, null, 99999L);
JSONObject obj = JSONUtil.parseObj(json); JSONObject obj = JSONUtil.parseObj(json);
assertThat(obj.getStr("error")) assertThat(obj.getStr("error"))
@ -248,7 +249,7 @@ class WikiToolKbNameRoutingTest {
void noResolvableKbReturnsLegacyError() { void noResolvableKbReturnsLegacyError() {
when(kbService.resolvePrimaryKb(AGENT)).thenReturn(null); when(kbService.resolvePrimaryKb(AGENT)).thenReturn(null);
String json = tool.wiki_list_pages(AGENT, null, null, null); String json = tool.wiki_list_pages(AGENT_PARAM, null, null, null);
JSONObject obj = JSONUtil.parseObj(json); JSONObject obj = JSONUtil.parseObj(json);
assertThat(obj.getStr("error")).contains("No wiki knowledge base found"); assertThat(obj.getStr("error")).contains("No wiki knowledge base found");
@ -264,7 +265,7 @@ class WikiToolKbNameRoutingTest {
kb(PRIMARY_KB, "Primary", AGENT))); kb(PRIMARY_KB, "Primary", AGENT)));
when(kbService.resolvePrimaryKb(AGENT)).thenReturn(kb(PRIMARY_KB, "Primary", AGENT)); when(kbService.resolvePrimaryKb(AGENT)).thenReturn(kb(PRIMARY_KB, "Primary", AGENT));
String json = tool.wiki_list_kbs(AGENT); String json = tool.wiki_list_kbs(AGENT_PARAM);
JSONObject obj = JSONUtil.parseObj(json); JSONObject obj = JSONUtil.parseObj(json);
assertThat(obj.getInt("kbCount")).isEqualTo(2); assertThat(obj.getInt("kbCount")).isEqualTo(2);

View File

@ -33,6 +33,7 @@ import static org.mockito.Mockito.when;
class WikiToolPermissionTest { class WikiToolPermissionTest {
private static final long AGENT = 11L; private static final long AGENT = 11L;
private static final String AGENT_PARAM = String.valueOf(AGENT);
private static final long KB = 7L; private static final long KB = 7L;
private record Harness(WikiTool tool, WikiPageService pageService, private record Harness(WikiTool tool, WikiPageService pageService,
@ -90,7 +91,7 @@ class WikiToolPermissionTest {
Harness h = harness(List.of(row("*", 0, 0, 0, 0, "deny"))); Harness h = harness(List.of(row("*", 0, 0, 0, 0, "deny")));
when(h.pageService().getBySlug(KB, "secret")).thenReturn(page("secret", "analysis")); when(h.pageService().getBySlug(KB, "secret")).thenReturn(page("secret", "analysis"));
String out = h.tool().wiki_read_page(AGENT, "secret", null, null, null, KB); String out = h.tool().wiki_read_page(AGENT_PARAM, "secret", null, null, null, KB);
assertTrue(out.contains("Page not found"), out); assertTrue(out.contains("Page not found"), out);
} }
@ -100,7 +101,7 @@ class WikiToolPermissionTest {
Harness h = harness(List.of(row("*", 1, 0, 0, 0, "deny"))); Harness h = harness(List.of(row("*", 1, 0, 0, 0, "deny")));
when(h.pageService().getBySlug(KB, "ok")).thenReturn(page("ok", "concept")); when(h.pageService().getBySlug(KB, "ok")).thenReturn(page("ok", "concept"));
String out = h.tool().wiki_read_page(AGENT, "ok", null, null, null, KB); String out = h.tool().wiki_read_page(AGENT_PARAM, "ok", null, null, null, KB);
assertTrue(out.contains("\"content\""), out); assertTrue(out.contains("\"content\""), out);
assertFalse(out.contains("Page not found"), out); assertFalse(out.contains("Page not found"), out);
@ -112,7 +113,7 @@ class WikiToolPermissionTest {
Harness h = harness(List.of(row("concept", 1, 0, 0, 0, "deny"))); Harness h = harness(List.of(row("concept", 1, 0, 0, 0, "deny")));
when(h.pageService().getBySlug(KB, "p")).thenReturn(page("p", "concept")); when(h.pageService().getBySlug(KB, "p")).thenReturn(page("p", "concept"));
String out = h.tool().wiki_delete_page(AGENT, "p", null, KB); String out = h.tool().wiki_delete_page(AGENT_PARAM, "p", null, KB);
assertTrue(out.contains("Not permitted"), out); assertTrue(out.contains("Not permitted"), out);
verify(h.pageService(), never()).delete(anyLong(), any()); verify(h.pageService(), never()).delete(anyLong(), any());
@ -123,7 +124,7 @@ class WikiToolPermissionTest {
Harness h = harness(List.of(row("concept", 1, 0, 0, 1, "approval_required"))); Harness h = harness(List.of(row("concept", 1, 0, 0, 1, "approval_required")));
when(h.pageService().getBySlug(KB, "p")).thenReturn(page("p", "concept")); when(h.pageService().getBySlug(KB, "p")).thenReturn(page("p", "concept"));
String out = h.tool().wiki_delete_page(AGENT, "p", null, KB); String out = h.tool().wiki_delete_page(AGENT_PARAM, "p", null, KB);
assertTrue(out.contains("Approval required"), out); assertTrue(out.contains("Approval required"), out);
verify(h.pageService(), never()).delete(anyLong(), any()); verify(h.pageService(), never()).delete(anyLong(), any());
@ -134,7 +135,7 @@ class WikiToolPermissionTest {
Harness h = harness(List.of(row("concept", 1, 1, 1, 1, "allow"))); Harness h = harness(List.of(row("concept", 1, 1, 1, 1, "allow")));
when(h.pageService().getBySlug(KB, "p")).thenReturn(page("p", "concept")); when(h.pageService().getBySlug(KB, "p")).thenReturn(page("p", "concept"));
String out = h.tool().wiki_delete_page(AGENT, "p", null, KB); String out = h.tool().wiki_delete_page(AGENT_PARAM, "p", null, KB);
assertTrue(out.contains("\"ok\":true"), out); assertTrue(out.contains("\"ok\":true"), out);
verify(h.pageService(), times(1)).delete(eq(KB), eq("p")); verify(h.pageService(), times(1)).delete(eq(KB), eq("p"));
@ -145,7 +146,7 @@ class WikiToolPermissionTest {
// a row exists for 'episode' only KB is gated, wildcard create not granted // a row exists for 'episode' only KB is gated, wildcard create not granted
Harness h = harness(List.of(row("episode", 1, 1, 1, 1, "allow"))); Harness h = harness(List.of(row("episode", 1, 1, 1, 1, "allow")));
String out = h.tool().wiki_create_page(AGENT, "New Page", "content here", null, KB); String out = h.tool().wiki_create_page(AGENT_PARAM, "New Page", "content here", null, KB);
assertTrue(out.contains("Not permitted"), out); assertTrue(out.contains("Not permitted"), out);
verify(h.pageService(), never()).createPage(anyLong(), any(), any(), any(), any(), any()); verify(h.pageService(), never()).createPage(anyLong(), any(), any(), any(), any(), any());
@ -161,7 +162,7 @@ class WikiToolPermissionTest {
updated.setVersion(2); updated.setVersion(2);
when(h.pageService().updatePageManually(eq(KB), eq("p"), any(), any())).thenReturn(updated); when(h.pageService().updatePageManually(eq(KB), eq("p"), any(), any())).thenReturn(updated);
String out = h.tool().wiki_update_page(AGENT, "p", "new body", null, null, KB); String out = h.tool().wiki_update_page(AGENT_PARAM, "p", "new body", null, null, KB);
assertTrue(out.contains("\"ok\":true"), out); assertTrue(out.contains("\"ok\":true"), out);
assertTrue(out.contains("updated in place"), out); assertTrue(out.contains("updated in place"), out);
@ -177,7 +178,7 @@ class WikiToolPermissionTest {
Harness h = harness(List.of(row("concept", 1, 1, 0, 0, "allow"))); Harness h = harness(List.of(row("concept", 1, 1, 0, 0, "allow")));
when(h.pageService().getBySlug(KB, "p")).thenReturn(page("p", "concept")); when(h.pageService().getBySlug(KB, "p")).thenReturn(page("p", "concept"));
String out = h.tool().wiki_update_page(AGENT, "p", "new body", null, null, KB); String out = h.tool().wiki_update_page(AGENT_PARAM, "p", "new body", null, null, KB);
assertTrue(out.contains("Not permitted"), out); assertTrue(out.contains("Not permitted"), out);
verify(h.pageService(), never()).updatePageManually(anyLong(), any(), any(), any()); verify(h.pageService(), never()).updatePageManually(anyLong(), any(), any(), any());
@ -188,7 +189,7 @@ class WikiToolPermissionTest {
Harness h = harness(List.of(row("*", 1, 1, 1, 1, "allow"))); Harness h = harness(List.of(row("*", 1, 1, 1, 1, "allow")));
when(h.pageService().getBySlug(KB, "ghost")).thenReturn(null); when(h.pageService().getBySlug(KB, "ghost")).thenReturn(null);
String out = h.tool().wiki_update_page(AGENT, "ghost", "body", null, null, KB); String out = h.tool().wiki_update_page(AGENT_PARAM, "ghost", "body", null, null, KB);
assertTrue(out.contains("Page not found"), out); assertTrue(out.contains("Page not found"), out);
verify(h.pageService(), never()).updatePageManually(anyLong(), any(), any(), any()); verify(h.pageService(), never()).updatePageManually(anyLong(), any(), any(), any());
@ -212,7 +213,7 @@ class WikiToolPermissionTest {
WikiPageEntity staleHidden = stalePage("classified", "secret", "{\"reason\":\"x\"}"); WikiPageEntity staleHidden = stalePage("classified", "secret", "{\"reason\":\"x\"}");
when(h.pageService().listByKbId(KB)).thenReturn(List.of(fresh, staleOk, staleHidden)); when(h.pageService().listByKbId(KB)).thenReturn(List.of(fresh, staleOk, staleHidden));
String out = h.tool().wiki_stale_pages(AGENT, null, KB); String out = h.tool().wiki_stale_pages(AGENT_PARAM, null, KB);
assertTrue(out.contains("\"staleCount\":1"), out); assertTrue(out.contains("\"staleCount\":1"), out);
assertTrue(out.contains("aged"), out); assertTrue(out.contains("aged"), out);
@ -225,7 +226,7 @@ class WikiToolPermissionTest {
Harness h = harness(List.of()); Harness h = harness(List.of());
when(h.pageService().listByKbId(KB)).thenReturn(List.of(page("a", "concept"), page("b", "episode"))); when(h.pageService().listByKbId(KB)).thenReturn(List.of(page("a", "concept"), page("b", "episode")));
String out = h.tool().wiki_stale_pages(AGENT, null, KB); String out = h.tool().wiki_stale_pages(AGENT_PARAM, null, KB);
assertTrue(out.contains("\"staleCount\":0"), out); assertTrue(out.contains("\"staleCount\":0"), out);
} }