From b5146ccdc796ca437f91b19fbffa124e9a95057a Mon Sep 17 00:00:00 2001 From: Gina <1317462541@qq.com> Date: Thu, 6 Aug 2026 16:14:40 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dgraph-memory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../graph/extraction/ExtractionPrompts.java | 34 ++++++++++++ .../graph/graph_memory/GraphMemory.java | 34 +++++++++++- .../graph/extraction/GraphExtractionTest.java | 18 +++++++ .../graph_memory/GraphMemoryBaseTest.java | 54 +++++++++++++++++++ 4 files changed, 138 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/openjiuwen/core/memory/graph/extraction/ExtractionPrompts.java b/src/main/java/com/openjiuwen/core/memory/graph/extraction/ExtractionPrompts.java index a95faf856..dc3e610fd 100644 --- a/src/main/java/com/openjiuwen/core/memory/graph/extraction/ExtractionPrompts.java +++ b/src/main/java/com/openjiuwen/core/memory/graph/extraction/ExtractionPrompts.java @@ -13,6 +13,7 @@ import com.openjiuwen.core.memory.graph.extraction.prompts.TemplateManager; import com.openjiuwen.core.memory.graph.extraction.prompts.entity_extraction.ExtractionPromptLanguageBase; +import java.math.BigInteger; import java.time.Instant; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -105,10 +106,43 @@ public static PromptCall extractEntityAttributes(Entity entity, String content, if (extras != null) { kwargs.putAll(extras); } + applyHumanSummaryTarget(entity, kwargs); return new PromptCall(kwargs, TemplateManager.getInstance().get(templateName), MultilingualBaseModel.responseFormat(EntitySummary.class, language)); } + private static void applyHumanSummaryTarget(Entity entity, Map kwargs) { + if (!"Human".equalsIgnoreCase(entity.getObjType())) { + return; + } + Number doubledTarget = doubleSummaryTarget(kwargs.get("summary_target")); + if (doubledTarget != null) { + kwargs.put("summary_target", doubledTarget); + } + } + + private static Number doubleSummaryTarget(Object summaryTarget) { + BigInteger target; + if (summaryTarget instanceof BigInteger bigInteger) { + target = bigInteger; + } else if (summaryTarget instanceof Number number) { + target = BigInteger.valueOf(number.longValue()); + } else if (summaryTarget instanceof String value + && !value.isEmpty() && value.codePoints().allMatch(Character::isDigit)) { + target = new BigInteger(value); + } else { + return null; + } + BigInteger doubledTarget = target.shiftLeft(1); + if (doubledTarget.bitLength() < Integer.SIZE) { + return Integer.valueOf(doubledTarget.intValue()); + } + if (doubledTarget.bitLength() < Long.SIZE) { + return Long.valueOf(doubledTarget.longValue()); + } + return doubledTarget; + } + /** * extractRelationDeclaration. * diff --git a/src/main/java/com/openjiuwen/core/memory/graph/graph_memory/GraphMemory.java b/src/main/java/com/openjiuwen/core/memory/graph/graph_memory/GraphMemory.java index 94bb451ae..dbddcb4f2 100644 --- a/src/main/java/com/openjiuwen/core/memory/graph/graph_memory/GraphMemory.java +++ b/src/main/java/com/openjiuwen/core/memory/graph/graph_memory/GraphMemory.java @@ -5,6 +5,10 @@ package com.openjiuwen.core.memory.graph.graph_memory; import com.openjiuwen.core.common.concurrent.OpenJiuwenExecutors; +import com.openjiuwen.core.common.exception.ErrorHelper; +import com.openjiuwen.core.common.exception.StatusCode; +import com.openjiuwen.core.common.logging.LoggerProtocol; +import com.openjiuwen.core.common.logging.Loggers; import com.openjiuwen.core.foundation.llm.Model; import com.openjiuwen.core.foundation.llm.schema.AssistantMessage; import com.openjiuwen.core.foundation.store.base_embedding.Embedding; @@ -60,6 +64,7 @@ */ public class GraphMemory { private static final String STORE_TYPE = "graph mem store"; + private static final LoggerProtocol MEMORY_LOGGER = Loggers.MEMORY; /** * Public record SearchHit used by the Java parity implementation. @@ -244,7 +249,9 @@ public void registerSearchStrategy(String name, SearchConfig searchEntity, Searc throw new IllegalArgumentException("Search config cannot be registered as an empty value."); } if (searchStrategies.containsKey(name) && !isForceRegister) { - throw new IllegalArgumentException("Search config with name [" + name + "] already exists."); + throw ErrorHelper.buildError(StatusCode.MEMORY_STORE_VALIDATION_INVALID, + "store_type", STORE_TYPE, + "error_msg", "Search config with name [" + name + "] already exists."); } searchStrategies.put(name, List.of(searchEntity != null ? copySearchConfig(searchEntity) : new SearchConfig(), @@ -785,12 +792,35 @@ private AssistantMessage invokeLlm(ExtractionPrompts.PromptCall promptCall, Map< if (extra != null) { params.putAll(extra); } + AssistantMessage response; semaphore.acquire(); try { - return llmClient.invoke(params.get("messages"), null, null, null, null, null, null, null, null, params); + response = llmClient.invoke(params.get("messages"), null, null, null, null, null, null, null, null, params); } finally { semaphore.release(); } + if (isDebugEnabled) { + logLlmInvocation(promptCall.template().getName(), params.get("messages"), response); + } + return response; + } + + private static void logLlmInvocation(String templateName, Object messages, AssistantMessage response) { + String separator = System.lineSeparator() + "=".repeat(60) + System.lineSeparator(); + String debugMessage = "TEMPLATE " + templateName + separator + lastMessageContent(messages) + + separator + String.valueOf(response.getContent()); + MEMORY_LOGGER.info("Graph Memory LLM Invoke: {}", debugMessage); + } + + private static String lastMessageContent(Object messages) { + if (!(messages instanceof List messageList) || messageList.isEmpty()) { + return ""; + } + Object lastMessage = messageList.get(messageList.size() - 1); + if (lastMessage instanceof Map messageMap) { + return String.valueOf(messageMap.get("content")); + } + return String.valueOf(lastMessage); } /** diff --git a/src/test/java/com/openjiuwen/core/memory/graph/extraction/GraphExtractionTest.java b/src/test/java/com/openjiuwen/core/memory/graph/extraction/GraphExtractionTest.java index 55e6cbe9b..9df602c67 100644 --- a/src/test/java/com/openjiuwen/core/memory/graph/extraction/GraphExtractionTest.java +++ b/src/test/java/com/openjiuwen/core/memory/graph/extraction/GraphExtractionTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.openjiuwen.core.foundation.store.graph.Entity; import com.openjiuwen.core.memory.config.graph.AddMemStrategy; import com.openjiuwen.core.memory.config.graph.EpisodeType; import com.openjiuwen.core.memory.config.graph.GraphDefaults; @@ -62,4 +63,21 @@ void templateManagerShouldLoadPromptResources() { assertThat(template.toMessages()).isNotEmpty(); assertThat(manager.contains("entity_extraction_relation_en")).isTrue(); } + + @Test + void humanSummaryTargetShouldBeTwiceTheConfiguredLimit() { + Entity entity = new Entity(); + entity.setName("A公司"); + Entity human = new Entity(); + human.setName("张明"); + human.setObjType("Human"); + + ExtractionPrompts.PromptCall entityPrompt = ExtractionPrompts.extractEntityAttributes( + entity, "content", "", "cn", Map.of("summary_target", "110"), 2); + ExtractionPrompts.PromptCall humanPrompt = ExtractionPrompts.extractEntityAttributes( + human, "content", "", "cn", Map.of("summary_target", "110"), 2); + + assertThat(entityPrompt.kwargs()).containsEntry("summary_target", "110"); + assertThat(humanPrompt.kwargs()).containsEntry("summary_target", 220); + } } diff --git a/src/test/java/com/openjiuwen/core/memory/graph/graph_memory/GraphMemoryBaseTest.java b/src/test/java/com/openjiuwen/core/memory/graph/graph_memory/GraphMemoryBaseTest.java index f9281ba8b..35939e306 100644 --- a/src/test/java/com/openjiuwen/core/memory/graph/graph_memory/GraphMemoryBaseTest.java +++ b/src/test/java/com/openjiuwen/core/memory/graph/graph_memory/GraphMemoryBaseTest.java @@ -4,6 +4,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.openjiuwen.core.common.exception.BaseError; +import com.openjiuwen.core.common.exception.StatusCode; import com.openjiuwen.core.foundation.llm.Model; import com.openjiuwen.core.foundation.llm.schema.AssistantMessage; import com.openjiuwen.core.foundation.llm.schema.ModelClientConfig; @@ -17,8 +19,14 @@ import com.openjiuwen.core.foundation.store.graph.Relation; import com.openjiuwen.core.memory.config.graph.SearchConfig; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.slf4j.LoggerFactory; import java.lang.reflect.Method; import java.nio.file.Path; @@ -101,6 +109,21 @@ void shouldRejectUnknownStrategy() { .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("Strategy [missing] not found"); } + @Test + void shouldRejectDuplicateStrategyWithMemoryValidationError() { + GraphConfig config = + GraphConfig.builder().uri(tempDir.resolve("graph.db").toString()).backend("in_memory").build(); + GraphMemory memory = new GraphMemory(config); + + assertThatThrownBy(() -> memory.registerSearchStrategy( + "default", new SearchConfig(), new SearchConfig(), new SearchConfig(), false)) + .isInstanceOfSatisfying(BaseError.class, error -> { + assertThat(error.getStatus()).isEqualTo(StatusCode.MEMORY_STORE_VALIDATION_INVALID); + assertThat(error.getCode()).isEqualTo(StatusCode.MEMORY_STORE_VALIDATION_INVALID.getCode()); + }) + .hasMessageContaining("Search config with name [default] already exists."); + } + @Test void shouldPrepareConversationEpisodesAndBuildHistory() throws Exception { GraphConfig config = @@ -156,6 +179,37 @@ void shouldAddMemoryThroughBasicMainFlow() throws Exception { assertThat(entity.getContent()).contains("Alice likes coffee"); } + @Test + void shouldLogLlmInvocationWhenDebugEnabled() throws Exception { + Logger memoryLogger = (Logger) LoggerFactory.getLogger("memory"); + ListAppender appender = new ListAppender<>(); + appender.start(); + Level previousLevel = memoryLogger.getLevel(); + memoryLogger.setLevel(Level.INFO); + memoryLogger.addAppender(appender); + try { + GraphConfig config = + GraphConfig.builder().uri(tempDir.resolve("debug-graph.db").toString()).backend("in_memory").build(); + FakeModel fakeModel = new FakeModel("{\"extracted_relations\":[]}", + "{\"extracted_entities\":[{\"name\":\"Alice\",\"entityTypeId\":0}]}", + "{\"extracted_relations\":[]}", + "{\"summary\":\"Alice likes coffee\",\"attributes\":{}}"); + GraphMemory memory = new GraphMemory(config, fakeModel, true, null, null, Map.of(), null, "cn", true); + memory.attachEmbedder(new DummyEmbedding()); + + memory.addMemory(com.openjiuwen.core.memory.config.graph.EpisodeType.DOCUMENT, "user-1", + "Alice likes coffee", null, OffsetDateTime.now()); + + assertThat(appender.list).extracting(ILoggingEvent::getFormattedMessage) + .anySatisfy(message -> assertThat(message) + .contains("Graph Memory LLM Invoke: TEMPLATE", "Alice likes coffee", + "extracted_relations")); + } finally { + memoryLogger.detachAppender(appender); + memoryLogger.setLevel(previousLevel); + } + } + private static final class DummyEmbedding extends Embedding { @Override public List embedQuery(String text) { From 4389be0d1118299c898b4e0e9dbd934be14d2e76 Mon Sep 17 00:00:00 2001 From: Gina <1317462541@qq.com> Date: Thu, 6 Aug 2026 16:33:34 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=B8=85codecheck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../graph/extraction/ExtractionPrompts.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/openjiuwen/core/memory/graph/extraction/ExtractionPrompts.java b/src/main/java/com/openjiuwen/core/memory/graph/extraction/ExtractionPrompts.java index dc3e610fd..71a13093f 100644 --- a/src/main/java/com/openjiuwen/core/memory/graph/extraction/ExtractionPrompts.java +++ b/src/main/java/com/openjiuwen/core/memory/graph/extraction/ExtractionPrompts.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; /** * Prompt building and entity extraction orchestration by episode type. @@ -115,13 +116,11 @@ private static void applyHumanSummaryTarget(Entity entity, Map k if (!"Human".equalsIgnoreCase(entity.getObjType())) { return; } - Number doubledTarget = doubleSummaryTarget(kwargs.get("summary_target")); - if (doubledTarget != null) { - kwargs.put("summary_target", doubledTarget); - } + doubleSummaryTarget(kwargs.get("summary_target")) + .ifPresent(doubledTarget -> kwargs.put("summary_target", doubledTarget)); } - private static Number doubleSummaryTarget(Object summaryTarget) { + private static Optional doubleSummaryTarget(Object summaryTarget) { BigInteger target; if (summaryTarget instanceof BigInteger bigInteger) { target = bigInteger; @@ -131,16 +130,16 @@ private static Number doubleSummaryTarget(Object summaryTarget) { && !value.isEmpty() && value.codePoints().allMatch(Character::isDigit)) { target = new BigInteger(value); } else { - return null; + return Optional.empty(); } BigInteger doubledTarget = target.shiftLeft(1); if (doubledTarget.bitLength() < Integer.SIZE) { - return Integer.valueOf(doubledTarget.intValue()); + return Optional.of(Integer.valueOf(doubledTarget.intValue())); } if (doubledTarget.bitLength() < Long.SIZE) { - return Long.valueOf(doubledTarget.longValue()); + return Optional.of(Long.valueOf(doubledTarget.longValue())); } - return doubledTarget; + return Optional.of(doubledTarget); } /**