diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/MemorySearchTool.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/MemorySearchTool.java index d1da96ee45..3c0ac392c2 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/MemorySearchTool.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/tool/MemorySearchTool.java @@ -19,7 +19,9 @@ import io.agentscope.core.tool.Tool; import io.agentscope.core.tool.ToolParam; import io.agentscope.harness.agent.workspace.WorkspaceManager; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.StringJoiner; import java.util.regex.Pattern; import org.slf4j.Logger; @@ -62,11 +64,21 @@ public String memorySearch( } private String keywordSearch(RuntimeContext rc, String query) { - StringJoiner results = new StringJoiner("\n"); - int matchCount = 0; + String[] rawTokens = query.split("[\\s,,;;||、/\\\\]+"); + List patterns = new ArrayList<>(); + for (String t : rawTokens) { + if (!t.isEmpty()) { + patterns.add(Pattern.compile(Pattern.quote(t), Pattern.CASE_INSENSITIVE)); + } + } + if (patterns.isEmpty()) { + return "No matching memories found for: " + query; + } List memoryPaths = workspaceManager.listMemoryFilePaths(rc); - Pattern pattern = Pattern.compile(Pattern.quote(query), Pattern.CASE_INSENSITIVE); + StringJoiner andResults = new StringJoiner("\n"); + int andCount = 0; + List> orCandidates = new ArrayList<>(); for (String relativePath : memoryPaths) { String content = workspaceManager.readManagedWorkspaceFileUtf8(rc, relativePath); @@ -75,16 +87,41 @@ private String keywordSearch(RuntimeContext rc, String query) { } String[] lines = content.split("\n", -1); for (int i = 0; i < lines.length; i++) { - if (pattern.matcher(lines[i]).find()) { - results.add(String.format("Source: %s#%d: %s", relativePath, i + 1, lines[i])); - matchCount++; + int hits = hitCount(lines[i], patterns); + if (hits == 0) { + continue; + } + String label = String.format("Source: %s#%d: %s", relativePath, i + 1, lines[i]); + if (hits == patterns.size()) { + andResults.add(label); + andCount++; + } else { + orCandidates.add(Map.entry(hits, label)); } } } - if (matchCount == 0) { + if (andCount > 0) { + return "Found " + andCount + " matches:\n\n" + andResults; + } + if (orCandidates.isEmpty()) { return "No matching memories found for: " + query; } - return "Found " + matchCount + " matches:\n\n" + results; + orCandidates.sort((a, b) -> Integer.compare(b.getKey(), a.getKey())); + StringJoiner orResults = new StringJoiner("\n"); + for (Map.Entry e : orCandidates) { + orResults.add(e.getValue()); + } + return "Found " + orCandidates.size() + " matches:\n\n" + orResults; + } + + private int hitCount(String line, List patterns) { + int count = 0; + for (Pattern p : patterns) { + if (p.matcher(line).find()) { + count++; + } + } + return count; } } diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/MemorySearchToolTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/MemorySearchToolTest.java new file mode 100644 index 0000000000..df38951e0c --- /dev/null +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/tool/MemorySearchToolTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.harness.agent.tool; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.agentscope.harness.agent.workspace.WorkspaceManager; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** Tests for multi-keyword search in {@link MemorySearchTool}. */ +class MemorySearchToolTest { + + @TempDir Path workspace; + + private MemorySearchTool tool; + + @BeforeEach + void setUp() { + tool = new MemorySearchTool(new WorkspaceManager(workspace)); + } + + @Test + void singleKeywordMatches() throws IOException { + Files.writeString(workspace.resolve("MEMORY.md"), "- Contains Markdown formatting\n"); + String result = tool.memorySearch(null, "Markdown"); + assertTrue(result.startsWith("Found "), result); + } + + @Test + void multiKeywordAndPassMatches() throws IOException { + Files.writeString( + workspace.resolve("MEMORY.md"), "- 用户要求创作诗歌并以 Markdown 文件形式交付,交付物为《秋日书怀》(七言律诗)\n"); + String result = tool.memorySearch(null, "秋日书怀 七言律诗"); + assertTrue(result.startsWith("Found "), result); + } + + @Test + void multiKeywordThreeTokensAndPass() throws IOException { + Files.writeString( + workspace.resolve("MEMORY.md"), "- 用户要求创作诗歌并以 Markdown 文件形式交付,交付物为《秋日书怀》(七言律诗)\n"); + String result = tool.memorySearch(null, "诗歌 Markdown 交付"); + assertTrue(result.startsWith("Found "), result); + } + + @Test + void orFallbackWhenNotAllTokensOnOneLine() throws IOException { + Files.writeString( + workspace.resolve("MEMORY.md"), + "- line about 诗歌 creation\n- another line about 交付\n"); + String result = tool.memorySearch(null, "诗歌 交付"); + // No single line has both; OR fallback returns lines with any match + assertTrue(result.startsWith("Found "), result); + } + + @Test + void noMatchReturnsMessage() throws IOException { + Files.writeString(workspace.resolve("MEMORY.md"), "- some unrelated content\n"); + String result = tool.memorySearch(null, "absent keyword"); + assertTrue(result.startsWith("No matching"), result); + } + + @Test + void nullQueryReturnsNoQuery() { + String result = tool.memorySearch(null, null); + assertTrue(result.contains("No query"), result); + } + + @Test + void blankQueryReturnsNoQuery() { + String result = tool.memorySearch(null, " "); + assertTrue(result.contains("No query"), result); + } +}