mateclaw/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java

271 lines
10 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package vip.mate.wiki.tool;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
import vip.mate.wiki.model.WikiPageEntity;
import vip.mate.wiki.model.WikiRawMaterialEntity;
import vip.mate.wiki.service.WikiKnowledgeBaseService;
import vip.mate.wiki.service.WikiPageService;
import vip.mate.wiki.service.WikiRawMaterialService;
import java.util.List;
/**
* Wiki 知识库工具
* <p>
* 供 Agent 在对话中按需读取 Wiki 页面内容。
* kbId 通过 agentId 自动解析LLM 无需传递。
*
* @author MateClaw Team
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class WikiTool {
private final WikiPageService pageService;
private final WikiKnowledgeBaseService kbService;
private final WikiRawMaterialService rawService;
@Tool(description = """
读取 Wiki 知识库中指定页面的完整内容。
当系统提示词中的 Wiki 页面摘要不够详细时,使用此工具获取完整内容。
返回 Markdown 格式的页面内容,包含 [[双向链接]] 和来源原始文件信息。
""")
public String wiki_read_page(
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
@ToolParam(description = "页面标识符 (slug)") String slug) {
if (slug == null || slug.isBlank()) {
return error("slug is required");
}
Long kbId = resolveKbId(agentId);
if (kbId == null) {
return error("No wiki knowledge base found for this agent");
}
WikiPageEntity page = pageService.getBySlug(kbId, slug);
if (page == null) {
return error("Page not found: " + slug);
}
JSONObject result = JSONUtil.createObj()
.set("title", page.getTitle())
.set("slug", page.getSlug())
.set("version", page.getVersion())
.set("lastUpdatedBy", page.getLastUpdatedBy())
.set("content", page.getContent())
.set("sourceFiles", resolveSourceFiles(page.getSourceRawIds()));
return result.toString();
}
@Tool(description = """
列出 Wiki 知识库中的所有页面。
返回页面列表包含标题、slug 和摘要。
""")
public String wiki_list_pages(
@ToolParam(description = "当前 Agent 的 ID") Long agentId) {
Long kbId = resolveKbId(agentId);
if (kbId == null) {
return error("No wiki knowledge base found for this agent");
}
List<WikiPageEntity> pages = pageService.listSummaries(kbId);
JSONArray arr = new JSONArray();
for (WikiPageEntity page : pages) {
arr.add(JSONUtil.createObj()
.set("title", page.getTitle())
.set("slug", page.getSlug())
.set("summary", page.getSummary()));
}
return JSONUtil.createObj()
.set("kbId", kbId)
.set("pageCount", pages.size())
.set("pages", arr)
.toString();
}
@Tool(description = """
在 Wiki 知识库中搜索页面。
按关键词搜索页面标题、摘要和正文内容,返回匹配的页面列表及其来源文件。
""")
public String wiki_search_pages(
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
@ToolParam(description = "搜索关键词") String query) {
if (query == null || query.isBlank()) {
return error("query is required");
}
Long kbId = resolveKbId(agentId);
if (kbId == null) {
return error("No wiki knowledge base found for this agent");
}
String queryLower = query.toLowerCase();
List<WikiPageEntity> allPages = pageService.listByKbIdWithContent(kbId);
List<WikiPageEntity> matched = allPages.stream()
.filter(p -> (p.getTitle() != null && p.getTitle().toLowerCase().contains(queryLower))
|| (p.getSummary() != null && p.getSummary().toLowerCase().contains(queryLower))
|| (p.getContent() != null && p.getContent().toLowerCase().contains(queryLower)))
.limit(20)
.toList();
JSONArray arr = new JSONArray();
for (WikiPageEntity page : matched) {
JSONObject obj = JSONUtil.createObj()
.set("title", page.getTitle())
.set("slug", page.getSlug())
.set("summary", page.getSummary())
.set("sourceFiles", resolveSourceFiles(page.getSourceRawIds()));
boolean titleMatch = page.getTitle() != null && page.getTitle().toLowerCase().contains(queryLower);
boolean contentMatch = page.getContent() != null && page.getContent().toLowerCase().contains(queryLower);
obj.set("matchIn", titleMatch ? "title" : contentMatch ? "content" : "summary");
arr.add(obj);
}
return JSONUtil.createObj()
.set("kbId", kbId)
.set("query", query)
.set("matchCount", matched.size())
.set("pages", arr)
.toString();
}
@Tool(description = """
追溯 Wiki 页面的来源原始文件。
查询指定页面是由哪些原始文档生成的,返回文件名、类型、路径等信息。
用于回答"这个内容出自哪篇文档"类的问题。
""")
public String wiki_trace_source(
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
@ToolParam(description = "页面标识符 (slug)") String slug) {
if (slug == null || slug.isBlank()) {
return error("slug is required");
}
Long kbId = resolveKbId(agentId);
if (kbId == null) {
return error("No wiki knowledge base found for this agent");
}
WikiPageEntity page = pageService.getBySlug(kbId, slug);
if (page == null) {
return error("Page not found: " + slug);
}
return JSONUtil.createObj()
.set("pageTitle", page.getTitle())
.set("pageSlug", page.getSlug())
.set("sourceFiles", resolveSourceFiles(page.getSourceRawIds()))
.toString();
}
@Tool(description = """
在 Wiki 知识库中创建新页面。
用于保存任务执行结果、分析报告、会议纪要等有价值的信息。
内容使用 Markdown 格式。页面标识符 (slug) 会从标题自动生成。
""")
public String wiki_create_page(
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
@ToolParam(description = "页面标题") String title,
@ToolParam(description = "页面内容 (Markdown 格式)") String content) {
if (title == null || title.isBlank()) {
return error("title is required");
}
if (content == null || content.isBlank()) {
return error("content is required");
}
Long kbId = resolveKbId(agentId);
if (kbId == null) {
return error("No wiki knowledge base found for this agent. Create one first.");
}
// 从标题生成 slug
String slug = title.toLowerCase()
.replaceAll("[^a-z0-9\\u4e00-\\u9fff]+", "-")
.replaceAll("^-|-$", "");
if (slug.isBlank()) {
slug = "page-" + System.currentTimeMillis();
}
// 检查 slug 是否已存在
WikiPageEntity existing = pageService.getBySlug(kbId, slug);
if (existing != null) {
slug = slug + "-" + System.currentTimeMillis() % 10000;
}
// 生成摘要(取前 200 字符)
String summary = content.length() > 200 ? content.substring(0, 200) + "..." : content;
WikiPageEntity page = pageService.createPage(kbId, slug, title, content, summary, null);
log.info("[WikiTool] Created page: {} (slug={}, kbId={})", title, slug, kbId);
return JSONUtil.createObj()
.set("ok", true)
.set("message", "Page created successfully")
.set("title", page.getTitle())
.set("slug", page.getSlug())
.set("kbId", kbId)
.toString();
}
/**
* 通过 agentId 自动解析关联的知识库 ID
* <p>
* 查找逻辑Agent 专属 KB + 公共 KBagent_id IS NULL取第一个。
*/
private Long resolveKbId(Long agentId) {
List<WikiKnowledgeBaseEntity> kbs = kbService.listByAgentId(agentId);
if (kbs.isEmpty()) {
// agentId 为 null 时也尝试查公共 KB
kbs = kbService.listAll();
}
return kbs.isEmpty() ? null : kbs.get(0).getId();
}
/**
* 将 sourceRawIds JSON 数组解析为原始文件信息列表
*/
private JSONArray resolveSourceFiles(String sourceRawIdsJson) {
JSONArray result = new JSONArray();
if (sourceRawIdsJson == null || sourceRawIdsJson.isBlank()) return result;
try {
List<Long> rawIds = new ObjectMapper().readValue(sourceRawIdsJson, new TypeReference<List<Long>>() {});
for (Long rawId : rawIds) {
WikiRawMaterialEntity raw = rawService.getById(rawId);
if (raw != null) {
result.add(JSONUtil.createObj()
.set("rawId", raw.getId())
.set("title", raw.getTitle())
.set("sourceType", raw.getSourceType())
.set("sourcePath", raw.getSourcePath()));
}
}
} catch (Exception e) {
log.warn("[WikiTool] Failed to resolve source files: {}", e.getMessage());
}
return result;
}
private String error(String message) {
return JSONUtil.createObj().set("error", message).toString();
}
}