mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(memory): preserve agent id in tool inputs
This commit is contained in:
parent
49d31d0847
commit
06105faa4d
@ -60,7 +60,7 @@ public class StructuredMemoryTool {
|
||||
key 用 snake_case 标识符,例如 preferred_language, no_mock_db
|
||||
""")
|
||||
public String remember_structured(
|
||||
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
|
||||
@ToolParam(description = "当前 Agent 的 ID。必须作为字符串传入,避免大整数精度丢失") String agentId,
|
||||
@ToolParam(description = "记忆类型:user / feedback / project / reference") String type,
|
||||
@ToolParam(description = "条目标识符(snake_case),例如 preferred_language") String key,
|
||||
@ToolParam(description = "条目内容") String content,
|
||||
@ -71,7 +71,8 @@ public class StructuredMemoryTool {
|
||||
}
|
||||
|
||||
try {
|
||||
structuredMemoryService.remember(agentId, type.trim().toLowerCase(),
|
||||
Long parsedAgentId = parseAgentId(agentId);
|
||||
structuredMemoryService.remember(parsedAgentId, type.trim().toLowerCase(),
|
||||
key.trim(), content.trim(), "agent", writeOwner(toolContext));
|
||||
|
||||
JSONObject result = new JSONObject();
|
||||
@ -94,7 +95,7 @@ public class StructuredMemoryTool {
|
||||
type 为空时搜索所有类型。
|
||||
""")
|
||||
public String recall_structured(
|
||||
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
|
||||
@ToolParam(description = "当前 Agent 的 ID。必须作为字符串传入,避免大整数精度丢失") String agentId,
|
||||
@ToolParam(description = "记忆类型过滤(可选):user / feedback / project / reference", required = false) String type,
|
||||
@ToolParam(description = "搜索关键词(可选),匹配 key 和内容", required = false) String keyword,
|
||||
ToolContext toolContext) {
|
||||
@ -104,8 +105,9 @@ public class StructuredMemoryTool {
|
||||
}
|
||||
|
||||
try {
|
||||
Long parsedAgentId = parseAgentId(agentId);
|
||||
List<Map<String, String>> results = structuredMemoryService.recall(
|
||||
agentId,
|
||||
parsedAgentId,
|
||||
type != null && !type.isBlank() ? type.trim().toLowerCase() : null,
|
||||
keyword,
|
||||
readOwner(toolContext));
|
||||
@ -128,7 +130,7 @@ public class StructuredMemoryTool {
|
||||
需要指定类型和 key。
|
||||
""")
|
||||
public String forget_structured(
|
||||
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
|
||||
@ToolParam(description = "当前 Agent 的 ID。必须作为字符串传入,避免大整数精度丢失") String agentId,
|
||||
@ToolParam(description = "记忆类型:user / feedback / project / reference") String type,
|
||||
@ToolParam(description = "要删除的条目标识符") String key,
|
||||
ToolContext toolContext) {
|
||||
@ -138,7 +140,8 @@ public class StructuredMemoryTool {
|
||||
}
|
||||
|
||||
try {
|
||||
boolean removed = structuredMemoryService.forget(agentId,
|
||||
Long parsedAgentId = parseAgentId(agentId);
|
||||
boolean removed = structuredMemoryService.forget(parsedAgentId,
|
||||
type.trim().toLowerCase(), key.trim(), writeOwner(toolContext));
|
||||
|
||||
JSONObject result = new JSONObject();
|
||||
@ -159,4 +162,16 @@ public class StructuredMemoryTool {
|
||||
result.set("message", message);
|
||||
return JSONUtil.toJsonPrettyStr(result);
|
||||
}
|
||||
|
||||
private Long parseAgentId(String agentId) {
|
||||
String trimmed = agentId != null ? agentId.trim() : "";
|
||||
if (trimmed.isEmpty()) {
|
||||
throw new IllegalArgumentException("agentId 不能为空");
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(trimmed);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("agentId 必须是数字字符串");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,15 +60,14 @@ public class WorkspaceMemoryTool {
|
||||
返回结构化 JSON,包括文件名、是否启用为系统提示词、更新时间和大小。
|
||||
""")
|
||||
public String list_workspace_memory_files(
|
||||
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
|
||||
@ToolParam(description = "当前 Agent 的 ID。必须作为字符串传入,避免大整数精度丢失") String agentId,
|
||||
@ToolParam(description = "可选:按文件名前缀过滤,例如 memory/ 或 MEM", required = false) String filenamePrefix,
|
||||
ToolContext toolContext) {
|
||||
|
||||
if (agentId == null) {
|
||||
return error("agentId 不能为空");
|
||||
}
|
||||
Long parsedAgentId = parseAgentIdOrNull(agentId);
|
||||
if (parsedAgentId == null) return error("agentId 不能为空");
|
||||
|
||||
List<WorkspaceFileEntity> files = workspaceFileService.listVisibleFiles(agentId, readOwner(toolContext)).stream()
|
||||
List<WorkspaceFileEntity> files = workspaceFileService.listVisibleFiles(parsedAgentId, readOwner(toolContext)).stream()
|
||||
.filter(file -> filenamePrefix == null || filenamePrefix.isBlank()
|
||||
|| (file.getFilename() != null && file.getFilename().startsWith(filenamePrefix)))
|
||||
.sorted(Comparator
|
||||
@ -99,23 +98,24 @@ public class WorkspaceMemoryTool {
|
||||
返回结构化 JSON,包括文件名、是否启用、内容和字节数。
|
||||
""")
|
||||
public String read_workspace_memory_file(
|
||||
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
|
||||
@ToolParam(description = "当前 Agent 的 ID。必须作为字符串传入,避免大整数精度丢失") String agentId,
|
||||
@ToolParam(description = "工作区文件名,例如 MEMORY.md、PROFILE.md、memory/2026-03-31.md") String filename,
|
||||
ToolContext toolContext) {
|
||||
|
||||
String validation = validate(agentId, filename);
|
||||
Long parsedAgentId = parseAgentIdOrNull(agentId);
|
||||
String validation = validate(parsedAgentId, filename);
|
||||
if (validation != null) {
|
||||
return error(validation);
|
||||
}
|
||||
|
||||
WorkspaceFileEntity file = workspaceFileService.getVisibleFile(agentId, filename, readOwner(toolContext));
|
||||
WorkspaceFileEntity file = workspaceFileService.getVisibleFile(parsedAgentId, filename, readOwner(toolContext));
|
||||
if (file == null) {
|
||||
return error("工作区文件不存在: " + filename);
|
||||
}
|
||||
|
||||
// 追踪主动检索信号(比被动注入更强的"真实需要"指标)
|
||||
String content = file.getContent() != null ? file.getContent() : "";
|
||||
memoryRecallTracker.trackActiveRetrieval(agentId, filename, content);
|
||||
memoryRecallTracker.trackActiveRetrieval(parsedAgentId, filename, content);
|
||||
|
||||
JSONObject result = new JSONObject();
|
||||
result.set("agentId", String.valueOf(agentId));
|
||||
@ -139,19 +139,20 @@ public class WorkspaceMemoryTool {
|
||||
不会出现在管理页的共享文件列表);TEAM 表示所有使用该 Agent 的用户共享的文件。向用户说明写入结果时请如实区分。
|
||||
""")
|
||||
public String write_workspace_memory_file(
|
||||
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
|
||||
@ToolParam(description = "当前 Agent 的 ID。必须作为字符串传入,避免大整数精度丢失") String agentId,
|
||||
@ToolParam(description = "工作区文件名,例如 MEMORY.md、PROFILE.md、memory/2026-03-31.md") String filename,
|
||||
@ToolParam(description = "要写入的完整 Markdown 内容") String content,
|
||||
ToolContext toolContext) {
|
||||
|
||||
String validation = validate(agentId, filename);
|
||||
Long parsedAgentId = parseAgentIdOrNull(agentId);
|
||||
String validation = validate(parsedAgentId, filename);
|
||||
if (validation != null) {
|
||||
return error(validation);
|
||||
}
|
||||
|
||||
String ownerKey = writeOwner(toolContext);
|
||||
WorkspaceFileEntity before = workspaceFileService.getVisibleFile(agentId, filename, ownerKey);
|
||||
WorkspaceFileEntity saved = workspaceFileService.saveVisibleFile(agentId, filename, content != null ? content : "", ownerKey);
|
||||
WorkspaceFileEntity before = workspaceFileService.getVisibleFile(parsedAgentId, filename, ownerKey);
|
||||
WorkspaceFileEntity saved = workspaceFileService.saveVisibleFile(parsedAgentId, filename, content != null ? content : "", ownerKey);
|
||||
|
||||
JSONObject result = new JSONObject();
|
||||
result.set("agentId", String.valueOf(agentId));
|
||||
@ -175,14 +176,15 @@ public class WorkspaceMemoryTool {
|
||||
默认只替换第一处匹配,replaceAll=true 时替换全部。
|
||||
""")
|
||||
public String edit_workspace_memory_file(
|
||||
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
|
||||
@ToolParam(description = "当前 Agent 的 ID。必须作为字符串传入,避免大整数精度丢失") String agentId,
|
||||
@ToolParam(description = "工作区文件名,例如 MEMORY.md、PROFILE.md、memory/2026-03-31.md") String filename,
|
||||
@ToolParam(description = "要查找的原始文本,要求精确匹配") String oldText,
|
||||
@ToolParam(description = "替换后的新文本") String newText,
|
||||
@ToolParam(description = "是否替换全部匹配项,默认 false", required = false) Boolean replaceAll,
|
||||
ToolContext toolContext) {
|
||||
|
||||
String validation = validate(agentId, filename);
|
||||
Long parsedAgentId = parseAgentIdOrNull(agentId);
|
||||
String validation = validate(parsedAgentId, filename);
|
||||
if (validation != null) {
|
||||
return error(validation);
|
||||
}
|
||||
@ -197,7 +199,7 @@ public class WorkspaceMemoryTool {
|
||||
}
|
||||
|
||||
String ownerKey = writeOwner(toolContext);
|
||||
WorkspaceFileEntity existing = workspaceFileService.getVisibleFile(agentId, filename, ownerKey);
|
||||
WorkspaceFileEntity existing = workspaceFileService.getVisibleFile(parsedAgentId, filename, ownerKey);
|
||||
if (existing == null) {
|
||||
return error("工作区文件不存在: " + filename);
|
||||
}
|
||||
@ -219,7 +221,7 @@ public class WorkspaceMemoryTool {
|
||||
replacements = 1;
|
||||
}
|
||||
|
||||
WorkspaceFileEntity saved = workspaceFileService.saveVisibleFile(agentId, filename, updated, ownerKey);
|
||||
WorkspaceFileEntity saved = workspaceFileService.saveVisibleFile(parsedAgentId, filename, updated, ownerKey);
|
||||
|
||||
JSONObject result = new JSONObject();
|
||||
result.set("agentId", String.valueOf(agentId));
|
||||
@ -241,16 +243,15 @@ public class WorkspaceMemoryTool {
|
||||
many memory entries. Returns ranked hits with filename, line number, and snippet \
|
||||
(matched terms wrapped in [[...]]).""")
|
||||
public String search_workspace_memory(
|
||||
@ToolParam(description = "当前 Agent 的 ID") Long agentId,
|
||||
@ToolParam(description = "当前 Agent 的 ID。必须作为字符串传入,避免大整数精度丢失") String agentId,
|
||||
@ToolParam(description = "关键词或短语,2-64 字符") String query,
|
||||
@ToolParam(description = "搜索范围:all(全部)/ memory(MEMORY.md 与 memory/)/ profile / persona,默认 all",
|
||||
required = false) String scope,
|
||||
@ToolParam(description = "返回的最大命中数,默认 10,上限 30", required = false) Integer limit,
|
||||
ToolContext toolContext) {
|
||||
|
||||
if (agentId == null) {
|
||||
return error("agentId 不能为空");
|
||||
}
|
||||
Long parsedAgentId = parseAgentIdOrNull(agentId);
|
||||
if (parsedAgentId == null) return error("agentId 不能为空");
|
||||
if (query == null || query.isBlank()) {
|
||||
return error("query 不能为空");
|
||||
}
|
||||
@ -269,7 +270,7 @@ public class WorkspaceMemoryTool {
|
||||
// plus this owner's PERSONAL memory only.
|
||||
String ownerKey = readOwner(toolContext);
|
||||
List<MemorySearchHit> hits = workspaceFileService.searchSnippets(
|
||||
agentId, trimmed, prefixes, effectiveLimit, ownerKey);
|
||||
parsedAgentId, trimmed, prefixes, effectiveLimit, ownerKey);
|
||||
|
||||
// Treat each unique file in the results as an active retrieval signal —
|
||||
// boosts that file's weight in the dream-consolidation ranker the same
|
||||
@ -279,9 +280,9 @@ public class WorkspaceMemoryTool {
|
||||
if (retrieved.add(hit.filename())) {
|
||||
// Read the same visible row the hit came from (the owner's
|
||||
// PERSONAL row when present) so PERSONAL hits track correctly.
|
||||
WorkspaceFileEntity file = workspaceFileService.getVisibleFile(agentId, hit.filename(), ownerKey);
|
||||
WorkspaceFileEntity file = workspaceFileService.getVisibleFile(parsedAgentId, hit.filename(), ownerKey);
|
||||
if (file != null && file.getContent() != null) {
|
||||
memoryRecallTracker.trackActiveRetrieval(agentId, hit.filename(), file.getContent());
|
||||
memoryRecallTracker.trackActiveRetrieval(parsedAgentId, hit.filename(), file.getContent());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -351,6 +352,18 @@ public class WorkspaceMemoryTool {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Long parseAgentIdOrNull(String agentId) {
|
||||
String trimmed = agentId != null ? agentId.trim() : "";
|
||||
if (trimmed.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(trimmed);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("agentId 必须是数字字符串");
|
||||
}
|
||||
}
|
||||
|
||||
private int countOccurrences(String text, String target) {
|
||||
int count = 0;
|
||||
int idx = 0;
|
||||
|
||||
@ -2,6 +2,10 @@ package vip.mate.memory.tool;
|
||||
|
||||
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 com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import vip.mate.memory.MemoryProperties;
|
||||
import vip.mate.memory.identity.MemoryOwnerResolver;
|
||||
import vip.mate.memory.service.StructuredMemoryService;
|
||||
@ -14,6 +18,7 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class StructuredMemoryToolIdSerializationTest {
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@DisplayName("recall_structured returns agentId as a JSON string to preserve snowflake precision")
|
||||
@ -26,9 +31,32 @@ class StructuredMemoryToolIdSerializationTest {
|
||||
new MemoryOwnerResolver(),
|
||||
new MemoryProperties());
|
||||
|
||||
String json = tool.recall_structured(2079862124134313986L, "reference", "meeting", null);
|
||||
String json = tool.recall_structured("2079862124134313986", "reference", "meeting", null);
|
||||
|
||||
assertThat(json).contains("\"agentId\": \"2079862124134313986\"");
|
||||
assertThat(json).doesNotContain("\"agentId\": 2079862124134313986");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("recall_structured publishes agentId as a string parameter so LLM tool calls preserve precision")
|
||||
void recallStructuredAgentIdSchemaIsString() throws Exception {
|
||||
StructuredMemoryTool tool = new StructuredMemoryTool(
|
||||
mock(StructuredMemoryService.class),
|
||||
new MemoryOwnerResolver(),
|
||||
new MemoryProperties());
|
||||
|
||||
String schema = callback(tool, "recall_structured").getToolDefinition().inputSchema();
|
||||
JsonNode root = MAPPER.readTree(schema);
|
||||
|
||||
assertThat(root.at("/properties/agentId/type").asText()).isEqualTo("string");
|
||||
}
|
||||
|
||||
private static ToolCallback callback(Object tool, String name) {
|
||||
for (ToolCallback callback : ToolCallbacks.from(tool)) {
|
||||
if (name.equals(callback.getToolDefinition().name())) {
|
||||
return callback;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Missing tool callback: " + name);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,10 @@ package vip.mate.tool.builtin;
|
||||
|
||||
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 com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import vip.mate.memory.MemoryProperties;
|
||||
import vip.mate.memory.identity.MemoryOwnerResolver;
|
||||
import vip.mate.memory.service.MemoryRecallTracker;
|
||||
@ -15,6 +19,7 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class WorkspaceMemoryToolIdSerializationTest {
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
@DisplayName("search_workspace_memory returns agentId as a JSON string to preserve snowflake precision")
|
||||
@ -28,9 +33,33 @@ class WorkspaceMemoryToolIdSerializationTest {
|
||||
new MemoryOwnerResolver(),
|
||||
new MemoryProperties());
|
||||
|
||||
String json = tool.search_workspace_memory(2079862124134313986L, "meeting", "all", 10, null);
|
||||
String json = tool.search_workspace_memory("2079862124134313986", "meeting", "all", 10, null);
|
||||
|
||||
assertThat(json).contains("\"agentId\": \"2079862124134313986\"");
|
||||
assertThat(json).doesNotContain("\"agentId\": 2079862124134313986");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("search_workspace_memory publishes agentId as a string parameter so LLM tool calls preserve precision")
|
||||
void searchWorkspaceMemoryAgentIdSchemaIsString() throws Exception {
|
||||
WorkspaceMemoryTool tool = new WorkspaceMemoryTool(
|
||||
mock(WorkspaceFileService.class),
|
||||
mock(MemoryRecallTracker.class),
|
||||
new MemoryOwnerResolver(),
|
||||
new MemoryProperties());
|
||||
|
||||
String schema = callback(tool, "search_workspace_memory").getToolDefinition().inputSchema();
|
||||
JsonNode root = MAPPER.readTree(schema);
|
||||
|
||||
assertThat(root.at("/properties/agentId/type").asText()).isEqualTo("string");
|
||||
}
|
||||
|
||||
private static ToolCallback callback(Object tool, String name) {
|
||||
for (ToolCallback callback : ToolCallbacks.from(tool)) {
|
||||
if (name.equals(callback.getToolDefinition().name())) {
|
||||
return callback;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Missing tool callback: " + name);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user