diff --git a/memind-core/pom.xml b/memind-core/pom.xml index dc0744e9..fdb90560 100644 --- a/memind-core/pom.xml +++ b/memind-core/pom.xml @@ -27,6 +27,15 @@ Memind - Core + + io.micrometer + micrometer-observation + + + io.projectreactor + reactor-core-micrometer + + io.projectreactor diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/DefaultMemoryBuilder.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/DefaultMemoryBuilder.java index 8c4452fd..36b34640 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/DefaultMemoryBuilder.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/DefaultMemoryBuilder.java @@ -16,27 +16,20 @@ import com.openmemind.ai.memory.core.DefaultMemory; import com.openmemind.ai.memory.core.Memory; import com.openmemind.ai.memory.core.buffer.MemoryBuffer; -import com.openmemind.ai.memory.core.extraction.MemoryExtractor; import com.openmemind.ai.memory.core.extraction.insight.tree.BubbleTrackerStore; import com.openmemind.ai.memory.core.llm.ChatClientRegistry; import com.openmemind.ai.memory.core.llm.ChatClientSlot; import com.openmemind.ai.memory.core.llm.StructuredChatClient; import com.openmemind.ai.memory.core.llm.rerank.NoopReranker; import com.openmemind.ai.memory.core.llm.rerank.Reranker; -import com.openmemind.ai.memory.core.metrics.MemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.NoopMemoryMetricsRecorder; import com.openmemind.ai.memory.core.plugin.RawDataPlugin; import com.openmemind.ai.memory.core.prompt.PromptRegistry; import com.openmemind.ai.memory.core.resource.ContentParserRegistry; import com.openmemind.ai.memory.core.resource.ResourceFetcher; -import com.openmemind.ai.memory.core.retrieval.MemoryRetriever; import com.openmemind.ai.memory.core.store.MemoryStore; import com.openmemind.ai.memory.core.textsearch.MemoryTextSearch; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.NoopMemoryObserver; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryExtractor; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryRetriever; import com.openmemind.ai.memory.core.vector.MemoryVector; +import io.micrometer.observation.ObservationRegistry; import java.util.ArrayList; import java.util.EnumMap; import java.util.IdentityHashMap; @@ -64,8 +57,7 @@ public final class DefaultMemoryBuilder implements MemoryBuilder { private BubbleTrackerStore bubbleTrackerStore; private final List rawDataPlugins = new ArrayList<>(); private MemoryBuildOptions options = MemoryBuildOptions.defaults(); - private MemoryObserver memoryObserver = new NoopMemoryObserver(); - private MemoryMetricsRecorder memoryMetricsRecorder = NoopMemoryMetricsRecorder.INSTANCE; + private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; private boolean externallyManaged; @Override @@ -155,14 +147,9 @@ public MemoryBuilder options(MemoryBuildOptions options) { } @Override - public MemoryBuilder memoryObserver(MemoryObserver observer) { - this.memoryObserver = Objects.requireNonNull(observer, "observer"); - return this; - } - - @Override - public MemoryBuilder memoryMetricsRecorder(MemoryMetricsRecorder recorder) { - this.memoryMetricsRecorder = Objects.requireNonNull(recorder, "recorder"); + public MemoryBuilder observationRegistry(ObservationRegistry observationRegistry) { + this.observationRegistry = + Objects.requireNonNull(observationRegistry, "observationRegistry"); return this; } @@ -194,21 +181,12 @@ public Memory build() { resourceFetcher, List.copyOf(rawDataPlugins), bubbleTrackerStore, - memoryObserver, - memoryMetricsRecorder, + observationRegistry, sanitization.memoryThreadForcedDisableReason()); MemoryExtractionAssembly extractionAssembly = new MemoryExtractionAssembler().assemble(context); - MemoryExtractor pipeline = - tracingExtractor( - extractionAssembly.pipeline(), - context.memoryObserver(), - context.memoryMetricsRecorder()); - MemoryRetriever memoryRetriever = - tracingRetriever( - new MemoryRetrievalAssembler().assemble(context), - context.memoryObserver(), - context.memoryMetricsRecorder()); + var pipeline = extractionAssembly.pipeline(); + var memoryRetriever = new MemoryRetrievalAssembler().assemble(context); AutoCloseable lifecycle = externallyManaged ? lifecycle(extractionAssembly.lifecycle()) @@ -231,27 +209,6 @@ public Memory build() { extractionAssembly.memoryThreadLayer()); } - private MemoryExtractor tracingExtractor( - MemoryExtractor extractor, MemoryObserver observer, MemoryMetricsRecorder recorder) { - if (!hasObservability(observer, recorder) || extractor instanceof TracingMemoryExtractor) { - return extractor; - } - return new TracingMemoryExtractor(extractor, observer, recorder); - } - - private MemoryRetriever tracingRetriever( - MemoryRetriever retriever, MemoryObserver observer, MemoryMetricsRecorder recorder) { - if (!hasObservability(observer, recorder) || retriever instanceof TracingMemoryRetriever) { - return retriever; - } - return new TracingMemoryRetriever(retriever, observer); - } - - private boolean hasObservability(MemoryObserver observer, MemoryMetricsRecorder recorder) { - return !(observer instanceof NoopMemoryObserver) - || !(recorder instanceof NoopMemoryMetricsRecorder); - } - MemoryBuildOptions buildOptions() { return options; } diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/MemoryAssemblyContext.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/MemoryAssemblyContext.java index 65f195a6..9c03c817 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/MemoryAssemblyContext.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/MemoryAssemblyContext.java @@ -20,17 +20,14 @@ import com.openmemind.ai.memory.core.extraction.insight.tree.BubbleTrackerStore; import com.openmemind.ai.memory.core.llm.ChatClientRegistry; import com.openmemind.ai.memory.core.llm.rerank.Reranker; -import com.openmemind.ai.memory.core.metrics.MemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.NoopMemoryMetricsRecorder; import com.openmemind.ai.memory.core.plugin.RawDataPlugin; import com.openmemind.ai.memory.core.prompt.PromptRegistry; import com.openmemind.ai.memory.core.resource.ContentParserRegistry; import com.openmemind.ai.memory.core.resource.ResourceFetcher; import com.openmemind.ai.memory.core.store.MemoryStore; import com.openmemind.ai.memory.core.textsearch.MemoryTextSearch; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.NoopMemoryObserver; import com.openmemind.ai.memory.core.vector.MemoryVector; +import io.micrometer.observation.ObservationRegistry; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -48,8 +45,7 @@ record MemoryAssemblyContext( ResourceFetcher resourceFetcher, List rawDataPlugins, BubbleTrackerStore bubbleTrackerStore, - MemoryObserver memoryObserver, - MemoryMetricsRecorder memoryMetricsRecorder, + ObservationRegistry observationRegistry, Optional memoryThreadForcedDisableReason) { MemoryAssemblyContext { @@ -70,50 +66,14 @@ record MemoryAssemblyContext( Objects.requireNonNull(promptRegistry, "promptRegistry"); Objects.requireNonNull(options, "options"); rawDataPlugins = List.copyOf(Objects.requireNonNull(rawDataPlugins, "rawDataPlugins")); - memoryObserver = memoryObserver != null ? memoryObserver : new NoopMemoryObserver(); - memoryMetricsRecorder = - memoryMetricsRecorder != null - ? memoryMetricsRecorder - : NoopMemoryMetricsRecorder.INSTANCE; + observationRegistry = + observationRegistry != null ? observationRegistry : ObservationRegistry.NOOP; memoryThreadForcedDisableReason = memoryThreadForcedDisableReason != null ? memoryThreadForcedDisableReason : Optional.empty(); } - MemoryAssemblyContext( - ChatClientRegistry chatClientRegistry, - MemoryStore memoryStore, - MemoryBuffer memoryBuffer, - MemoryTextSearch textSearch, - MemoryVector memoryVector, - Reranker reranker, - PromptRegistry promptRegistry, - MemoryBuildOptions options, - ContentParserRegistry contentParserRegistry, - ResourceFetcher resourceFetcher, - List rawDataPlugins, - BubbleTrackerStore bubbleTrackerStore, - MemoryObserver memoryObserver, - Optional memoryThreadForcedDisableReason) { - this( - chatClientRegistry, - memoryStore, - memoryBuffer, - textSearch, - memoryVector, - reranker, - promptRegistry, - options, - contentParserRegistry, - resourceFetcher, - rawDataPlugins, - bubbleTrackerStore, - memoryObserver, - NoopMemoryMetricsRecorder.INSTANCE, - memoryThreadForcedDisableReason); - } - InsightBuffer insightBuffer() { return memoryBuffer.insightBuffer(); } diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/MemoryBuilder.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/MemoryBuilder.java index e12c9152..3b61ee5f 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/MemoryBuilder.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/MemoryBuilder.java @@ -19,15 +19,14 @@ import com.openmemind.ai.memory.core.llm.ChatClientSlot; import com.openmemind.ai.memory.core.llm.StructuredChatClient; import com.openmemind.ai.memory.core.llm.rerank.Reranker; -import com.openmemind.ai.memory.core.metrics.MemoryMetricsRecorder; import com.openmemind.ai.memory.core.plugin.RawDataPlugin; import com.openmemind.ai.memory.core.prompt.PromptRegistry; import com.openmemind.ai.memory.core.resource.ContentParserRegistry; import com.openmemind.ai.memory.core.resource.ResourceFetcher; import com.openmemind.ai.memory.core.store.MemoryStore; import com.openmemind.ai.memory.core.textsearch.MemoryTextSearch; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; import com.openmemind.ai.memory.core.vector.MemoryVector; +import io.micrometer.observation.ObservationRegistry; /** * Builds a {@link Memory} instance from runtime components. @@ -60,11 +59,7 @@ public interface MemoryBuilder { MemoryBuilder options(MemoryBuildOptions options); - default MemoryBuilder memoryObserver(MemoryObserver observer) { - return this; - } - - default MemoryBuilder memoryMetricsRecorder(MemoryMetricsRecorder recorder) { + default MemoryBuilder observationRegistry(ObservationRegistry observationRegistry) { return this; } diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/MemoryExtractionAssembler.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/MemoryExtractionAssembler.java index 7b257b9a..d51aac83 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/MemoryExtractionAssembler.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/MemoryExtractionAssembler.java @@ -95,7 +95,6 @@ import com.openmemind.ai.memory.core.store.graph.NoOpItemGraphCommitOperations; import com.openmemind.ai.memory.core.store.thread.NoOpThreadEnrichmentInputStore; import com.openmemind.ai.memory.core.store.thread.ThreadEnrichmentInputStore; -import com.openmemind.ai.memory.core.tracing.decorator.TracingItemGraphMaterializer; import com.openmemind.ai.memory.core.utils.IdUtils; import java.util.ArrayList; import java.util.HashMap; @@ -141,13 +140,15 @@ MemoryExtractionAssembly assemble(MemoryAssemblyContext context) { captionGenerator, context.memoryStore(), context.memoryVector(), - context.options().extraction().rawdata().vectorBatchSize()); + context.options().extraction().rawdata().vectorBatchSize(), + context.observationRegistry()); MemoryItemExtractor itemExtractor = createMemoryItemExtractor(registry, processors, context.promptRegistry()); MemoryItemDeduplicator deduplicator = new CompositeDeduplicator( - List.of(new HashBasedDeduplicator(context.memoryStore()))); + List.of(new HashBasedDeduplicator(context.memoryStore())), + context.observationRegistry()); ItemGraphMaterializer graphMaterializer = graphMaterializer(context); MemoryItemLayer memoryItemLayer = new MemoryItemLayer( @@ -157,19 +158,22 @@ MemoryExtractionAssembly assemble(MemoryAssemblyContext context) { context.memoryVector(), IdUtils.snowflake(), null, - graphMaterializer); + graphMaterializer, + context.observationRegistry()); MemoryItemExtractStep memoryItemStep = memoryItemLayer; MemoryThreadLayer memoryThreadLayer = null; InsightGenerator insightGenerator = new LlmInsightGenerator( registry.resolve(ChatClientSlot.INSIGHT_GENERATOR), - context.promptRegistry()); + context.promptRegistry(), + context.observationRegistry()); InsightGraphAssistant insightGraphAssistant = insightGraphAssistant(context); InsightGroupClassifier insightGroupClassifier = new LlmInsightGroupClassifier( registry.resolve(ChatClientSlot.INSIGHT_GROUP_CLASSIFIER), - context.promptRegistry()); + context.promptRegistry(), + context.observationRegistry()); var identityManager = new InsightPointIdentityManager(); var evidenceNormalizer = new InsightPointEvidenceNormalizer(); BubbleTrackerStore bubbleTrackerStore = @@ -201,12 +205,13 @@ MemoryExtractionAssembly assemble(MemoryAssemblyContext context) { identityManager, evidenceNormalizer, insightGraphAssistant, - null); + context.observationRegistry()); InsightLayer insightLayer = new InsightLayer( context.memoryStore(), insightBuildScheduler, - unsupportedInsightTypes(processors)); + unsupportedInsightTypes(processors), + context.observationRegistry()); ContextCommitDetector contextCommitDetector = new LlmContextCommitDetector( @@ -320,7 +325,8 @@ MemoryExtractionAssembly assemble(MemoryAssemblyContext context) { resolveResourceFetcher(context.resourceFetcher()), ingestionPolicyRegistry, context.options().extraction().rawdata(), - context.options().extraction().item()); + context.options().extraction().item(), + context.observationRegistry()); return new MemoryExtractionAssembly( pipeline, insightLayer, extractionLifecycle, memoryThreadLayer); } @@ -405,8 +411,9 @@ yield new ConservativeHeuristicEntityResolutionStrategy( planner, context.memoryStore().itemGraphCommitOperations(), derivedMaintainer, - context.options().extraction().item().graph()); - return new TracingItemGraphMaterializer(graphMaterializer, context.memoryObserver()); + context.options().extraction().item().graph(), + context.observationRegistry()); + return graphMaterializer; } private ConversationContentProcessor conversationProcessor( diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/MemoryRetrievalAssembler.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/MemoryRetrievalAssembler.java index 38c38ed5..594accc9 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/MemoryRetrievalAssembler.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/builder/MemoryRetrievalAssembler.java @@ -15,8 +15,9 @@ import com.openmemind.ai.memory.core.llm.ChatClientRegistry; import com.openmemind.ai.memory.core.llm.ChatClientSlot; -import com.openmemind.ai.memory.core.metrics.MemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.NoopMemoryMetricsRecorder; +import com.openmemind.ai.memory.core.llm.rerank.LlmReranker; +import com.openmemind.ai.memory.core.llm.rerank.Reranker; +import com.openmemind.ai.memory.core.llm.rerank.observation.LlmRerankerObservation; import com.openmemind.ai.memory.core.retrieval.DefaultMemoryRetriever; import com.openmemind.ai.memory.core.retrieval.admission.DefaultRetrievalAdmissionPolicy; import com.openmemind.ai.memory.core.retrieval.deep.LlmTypedQueryExpander; @@ -38,7 +39,6 @@ import com.openmemind.ai.memory.core.retrieval.sufficiency.SufficiencyGate; import com.openmemind.ai.memory.core.retrieval.temporal.DefaultTemporalConstraintExtractor; import com.openmemind.ai.memory.core.retrieval.temporal.DefaultTemporalItemChannel; -import com.openmemind.ai.memory.core.retrieval.temporal.TemporalItemChannel; import com.openmemind.ai.memory.core.retrieval.thread.DefaultMemoryThreadAssistant; import com.openmemind.ai.memory.core.retrieval.thread.MemoryThreadAssistConfigMapper; import com.openmemind.ai.memory.core.retrieval.thread.MemoryThreadAssistant; @@ -49,15 +49,7 @@ import com.openmemind.ai.memory.core.retrieval.tier.ItemTierRetriever; import com.openmemind.ai.memory.core.retrieval.tier.ItemTierSearch; import com.openmemind.ai.memory.core.retrieval.tier.LlmInsightTypeRouter; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.NoopMemoryObserver; -import com.openmemind.ai.memory.core.tracing.decorator.TracingGraphItemChannel; -import com.openmemind.ai.memory.core.tracing.decorator.TracingInsightTierRetriever; -import com.openmemind.ai.memory.core.tracing.decorator.TracingItemTierRetriever; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryThreadAssistant; -import com.openmemind.ai.memory.core.tracing.decorator.TracingRetrievalGraphAssistant; -import com.openmemind.ai.memory.core.tracing.decorator.TracingRetrievalResultMerger; -import com.openmemind.ai.memory.core.tracing.decorator.TracingTemporalItemChannel; +import io.micrometer.observation.ObservationRegistry; final class MemoryRetrievalAssembler { @@ -66,55 +58,54 @@ DefaultMemoryRetriever assemble(MemoryAssemblyContext context) { InsightTypeRouter insightTypeRouter = new LlmInsightTypeRouter( registry.resolve(ChatClientSlot.INSIGHT_TYPE_ROUTER), - context.promptRegistry()); + context.promptRegistry(), + context.observationRegistry()); InsightTierSearch insightTierRetriever = - tracingInsightTierRetriever( - new InsightTierRetriever( - context.memoryStore(), context.memoryVector(), insightTypeRouter), - context.memoryObserver(), - context.memoryMetricsRecorder()); + new InsightTierRetriever( + context.memoryStore(), + context.memoryVector(), + insightTypeRouter, + context.observationRegistry()); ItemTierSearch itemTierRetriever = - tracingItemTierRetriever( - new ItemTierRetriever( - context.memoryStore(), - context.memoryVector(), - context.textSearch()), - context.memoryObserver(), - context.memoryMetricsRecorder()); + new ItemTierRetriever( + context.memoryStore(), + context.memoryVector(), + context.textSearch(), + context.observationRegistry()); SufficiencyGate sufficiencyGate = new LlmSufficiencyGate( registry.resolve(ChatClientSlot.SUFFICIENCY_GATE), - context.promptRegistry()); + context.promptRegistry(), + context.observationRegistry()); TypedQueryExpander typedQueryExpander = new LlmTypedQueryExpander( - registry.resolve(ChatClientSlot.QUERY_EXPANDER), context.promptRegistry()); + registry.resolve(ChatClientSlot.QUERY_EXPANDER), + context.promptRegistry(), + context.observationRegistry()); var graphExpansionEngine = new GraphExpansionEngine(context.memoryStore()); RetrievalGraphAssistant graphAssistant = buildGraphAssistant(context, graphExpansionEngine); GraphItemChannel graphItemChannel = - tracingGraphItemChannel( - new DefaultGraphItemChannel(graphExpansionEngine), - context.memoryObserver(), - context.memoryMetricsRecorder()); + new DefaultGraphItemChannel(graphExpansionEngine, context.observationRegistry()); MemoryThreadAssistant memoryThreadAssistant = buildMemoryThreadAssistant(context); RetrievalResultMerger resultMerger = - tracingRetrievalResultMerger( - DefaultRetrievalResultMerger.INSTANCE, - context.memoryObserver(), - context.memoryMetricsRecorder()); + new DefaultRetrievalResultMerger(context.observationRegistry()); SimpleStrategyConfig simpleStrategyConfig = simpleStrategyConfig(context.options()); DeepStrategyConfig deepStrategyConfig = deepStrategyConfig(context.options()); + Reranker reranker = + observeCustomReranker(context.reranker(), context.observationRegistry()); DeepRetrievalStrategy deepRetrievalStrategy = new DeepRetrievalStrategy( insightTierRetriever, itemTierRetriever, sufficiencyGate, typedQueryExpander, - context.reranker(), + reranker, context.memoryStore(), deepStrategyConfig, graphAssistant, memoryThreadAssistant, - resultMerger); + resultMerger, + context.observationRegistry()); SimpleRetrievalStrategy simpleRetrievalStrategy = new SimpleRetrievalStrategy( insightTierRetriever, @@ -125,13 +116,12 @@ DefaultMemoryRetriever assemble(MemoryAssemblyContext context) { graphAssistant, memoryThreadAssistant, new DefaultTemporalConstraintExtractor(), - tracingTemporalItemChannel( - new DefaultTemporalItemChannel(context.memoryStore()), - context.memoryObserver(), - context.memoryMetricsRecorder()), + new DefaultTemporalItemChannel( + context.memoryStore(), context.observationRegistry()), graphItemChannel, resultMerger, - java.time.Clock.systemDefaultZone()); + java.time.Clock.systemDefaultZone(), + context.observationRegistry()); var admissionOptions = context.options().retrieval().common().admission(); var admissionPolicy = new DefaultRetrievalAdmissionPolicy(admissionOptions); @@ -146,76 +136,41 @@ DefaultMemoryRetriever assemble(MemoryAssemblyContext context) { null, admissionPolicy, admissionOptions, - longQueryCondenser); + longQueryCondenser, + context.observationRegistry()); memoryRetriever.registerStrategy(simpleRetrievalStrategy); memoryRetriever.registerStrategy(deepRetrievalStrategy); return memoryRetriever; } - private RetrievalGraphAssistant buildGraphAssistant( - MemoryAssemblyContext context, GraphExpansionEngine graphExpansionEngine) { - return new TracingRetrievalGraphAssistant( - new DefaultRetrievalGraphAssistant(graphExpansionEngine), context.memoryObserver()); - } - - private InsightTierSearch tracingInsightTierRetriever( - InsightTierSearch retriever, MemoryObserver observer, MemoryMetricsRecorder recorder) { - if (!hasObservability(observer, recorder) - || retriever instanceof TracingInsightTierRetriever) { - return retriever; - } - return new TracingInsightTierRetriever(retriever, observer, recorder); - } - - private ItemTierSearch tracingItemTierRetriever( - ItemTierSearch retriever, MemoryObserver observer, MemoryMetricsRecorder recorder) { - if (!hasObservability(observer, recorder) - || retriever instanceof TracingItemTierRetriever) { - return retriever; + static Reranker observeCustomReranker( + Reranker reranker, ObservationRegistry observationRegistry) { + if (reranker == null || reranker instanceof LlmReranker) { + return reranker; } - return new TracingItemTierRetriever(retriever, observer, recorder); + return (query, results, topK) -> + LlmRerankerObservation.observe( + observationRegistry, + query, + results, + topK, + () -> reranker.rerank(query, results, topK)); } - private GraphItemChannel tracingGraphItemChannel( - GraphItemChannel channel, MemoryObserver observer, MemoryMetricsRecorder recorder) { - if (!hasObservability(observer, recorder) || channel instanceof TracingGraphItemChannel) { - return channel; - } - return new TracingGraphItemChannel(channel, observer, recorder); - } - - private TemporalItemChannel tracingTemporalItemChannel( - TemporalItemChannel channel, MemoryObserver observer, MemoryMetricsRecorder recorder) { - if (!hasObservability(observer, recorder) - || channel instanceof TracingTemporalItemChannel) { - return channel; - } - return new TracingTemporalItemChannel(channel, observer, recorder); - } - - private RetrievalResultMerger tracingRetrievalResultMerger( - RetrievalResultMerger merger, MemoryObserver observer, MemoryMetricsRecorder recorder) { - if (!hasObservability(observer, recorder) - || merger instanceof TracingRetrievalResultMerger) { - return merger; - } - return new TracingRetrievalResultMerger(merger, observer, recorder); - } - - private boolean hasObservability(MemoryObserver observer, MemoryMetricsRecorder recorder) { - return !(observer instanceof NoopMemoryObserver) - || !(recorder instanceof NoopMemoryMetricsRecorder); + private RetrievalGraphAssistant buildGraphAssistant( + MemoryAssemblyContext context, GraphExpansionEngine graphExpansionEngine) { + return new DefaultRetrievalGraphAssistant( + graphExpansionEngine, context.observationRegistry()); } private MemoryThreadAssistant buildMemoryThreadAssistant(MemoryAssemblyContext context) { if (!context.options().memoryThread().enabled()) { return NoOpMemoryThreadAssistant.INSTANCE; } - return new TracingMemoryThreadAssistant( - new DefaultMemoryThreadAssistant( - context.memoryStore(), - context.options().memoryThread().lifecycle().dormantAfter()), - context.memoryObserver()); + return new DefaultMemoryThreadAssistant( + context.memoryStore(), + context.options().memoryThread().lifecycle().dormantAfter(), + context.observationRegistry()); } private SimpleStrategyConfig simpleStrategyConfig(MemoryBuildOptions options) { diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/DefaultMemoryExtractor.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/DefaultMemoryExtractor.java index b3124a17..3e821036 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/DefaultMemoryExtractor.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/DefaultMemoryExtractor.java @@ -25,6 +25,7 @@ import com.openmemind.ai.memory.core.extraction.context.ContextCommitDetector; import com.openmemind.ai.memory.core.extraction.item.ItemExtractionConfig; import com.openmemind.ai.memory.core.extraction.item.SegmentBudgetEnforcer; +import com.openmemind.ai.memory.core.extraction.observation.DefaultMemoryExtractorObservation; import com.openmemind.ai.memory.core.extraction.rawdata.RawContentProcessor; import com.openmemind.ai.memory.core.extraction.rawdata.RawContentProcessorRegistry; import com.openmemind.ai.memory.core.extraction.rawdata.content.ConversationContent; @@ -46,6 +47,7 @@ import com.openmemind.ai.memory.core.resource.ResourceRef; import com.openmemind.ai.memory.core.resource.ResourceStore; import com.openmemind.ai.memory.core.utils.HashUtils; +import io.micrometer.observation.ObservationRegistry; import java.time.Duration; import java.time.Instant; import java.util.HashMap; @@ -86,6 +88,7 @@ public class DefaultMemoryExtractor implements MemoryExtractor { private final ItemExtractionOptions itemExtractionOptions; private final RawContentProcessorRegistry rawContentProcessorRegistry; private final SegmentBudgetEnforcer segmentBudgetEnforcer; + private final ObservationRegistry observationRegistry; public DefaultMemoryExtractor( RawDataExtractStep rawDataStep, @@ -110,7 +113,8 @@ public DefaultMemoryExtractor( null, RawDataIngestionPolicyRegistry.empty(), RawDataExtractionOptions.defaults(), - ItemExtractionOptions.defaults()); + ItemExtractionOptions.defaults(), + ObservationRegistry.NOOP); } public DefaultMemoryExtractor( @@ -128,6 +132,40 @@ public DefaultMemoryExtractor( RawDataIngestionPolicyRegistry ingestionPolicyRegistry, RawDataExtractionOptions rawDataExtractionOptions, ItemExtractionOptions itemExtractionOptions) { + this( + rawDataStep, + memoryItemStep, + insightStep, + segmentProcessor, + contextCommitDetector, + pendingConversationBuffer, + recentConversationBuffer, + rawContentProcessorRegistry, + contentParserRegistry, + resourceStore, + resourceFetcher, + ingestionPolicyRegistry, + rawDataExtractionOptions, + itemExtractionOptions, + ObservationRegistry.NOOP); + } + + public DefaultMemoryExtractor( + RawDataExtractStep rawDataStep, + MemoryItemExtractStep memoryItemStep, + InsightExtractStep insightStep, + SegmentProcessor segmentProcessor, + ContextCommitDetector contextCommitDetector, + PendingConversationBuffer pendingConversationBuffer, + RecentConversationBuffer recentConversationBuffer, + RawContentProcessorRegistry rawContentProcessorRegistry, + ContentParserRegistry contentParserRegistry, + ResourceStore resourceStore, + ResourceFetcher resourceFetcher, + RawDataIngestionPolicyRegistry ingestionPolicyRegistry, + RawDataExtractionOptions rawDataExtractionOptions, + ItemExtractionOptions itemExtractionOptions, + ObservationRegistry observationRegistry) { this( rawDataStep, memoryItemStep, @@ -143,7 +181,8 @@ public DefaultMemoryExtractor( null, ingestionPolicyRegistry, rawDataExtractionOptions, - itemExtractionOptions); + itemExtractionOptions, + observationRegistry); } DefaultMemoryExtractor( @@ -172,7 +211,8 @@ public DefaultMemoryExtractor( requestResolver, RawDataIngestionPolicyRegistry.empty(), rawDataExtractionOptions, - itemExtractionOptions); + itemExtractionOptions, + ObservationRegistry.NOOP); } private DefaultMemoryExtractor( @@ -190,7 +230,8 @@ private DefaultMemoryExtractor( ExtractionRequestResolver requestResolver, RawDataIngestionPolicyRegistry ingestionPolicyRegistry, RawDataExtractionOptions rawDataExtractionOptions, - ItemExtractionOptions itemExtractionOptions) { + ItemExtractionOptions itemExtractionOptions, + ObservationRegistry observationRegistry) { this.rawDataStep = Objects.requireNonNull(rawDataStep, "rawDataStep is required"); this.memoryItemStep = Objects.requireNonNull(memoryItemStep, "memoryItemStep is required"); this.insightStep = Objects.requireNonNull(insightStep, "insightStep is required"); @@ -208,6 +249,8 @@ private DefaultMemoryExtractor( this.itemExtractionOptions = Objects.requireNonNull(itemExtractionOptions, "itemExtractionOptions"); this.rawContentProcessorRegistry = rawContentProcessorRegistry; + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; this.requestResolver = requestResolver != null ? requestResolver @@ -230,11 +273,15 @@ public Mono extract(ExtractionRequest request) { Objects.requireNonNull(request, "request is required"); Objects.requireNonNull(request.memoryId(), "memoryId is required"); - var startTime = Instant.now(); - - return Mono.defer(() -> requestResolver.resolve(request)) - .flatMap(resolved -> executeResolvedRequest(resolved, startTime)) - .onErrorResume(e -> toErrorResult(request.memoryId(), e, startTime)); + return DefaultMemoryExtractorObservation.observe( + observationRegistry, + request.memoryId(), + () -> { + var startTime = Instant.now(); + return Mono.defer(() -> requestResolver.resolve(request)) + .flatMap(resolved -> executeResolvedRequest(resolved, startTime)) + .onErrorResume(e -> toErrorResult(request.memoryId(), e, startTime)); + }); } /** @@ -337,66 +384,70 @@ public Mono addMessage( var bufferKey = memoryId.toIdentifier(); - return Mono.fromCallable( - () -> - ConversationBufferLocks.withLock( - bufferKey, - () -> { - recentConversationBuffer.append(bufferKey, message); - if (message.role() == Message.Role.ASSISTANT) { - appendToPendingBuffer(bufferKey, message); - return Optional.empty(); - } - - var snapshot = - pendingConversationBuffer.load(bufferKey); - if (snapshot.isEmpty()) { - appendToPendingBuffer(bufferKey, message); - return Optional.empty(); - } - - var detectionInput = - new CommitDetectionInput( - snapshot, - List.of(message), - CommitDetectionContext.empty()); - var decision = - contextCommitDetector - .shouldCommit(detectionInput) - .defaultIfEmpty(CommitDecision.hold()) - .block(); - - if (decision == null || !decision.shouldSeal()) { - appendToPendingBuffer(bufferKey, message); - return Optional.empty(); - } - - log.debug( - "Boundary detection triggered sealing:" - + " memoryId={}, reason={}, confidence={}", - bufferKey, - decision.reason(), - decision.confidence()); - - var sealedMessages = List.copyOf(snapshot); - pendingConversationBuffer.clear(bufferKey); - pendingConversationBuffer.append(bufferKey, message); - - return Optional.of( - new PendingExtraction( - sealedMessages, new HashMap<>())); - })) - .subscribeOn(Schedulers.boundedElastic()) - .flatMap( - pending -> - pending.map( - extraction -> - extractConversationSegment( - memoryId, - extraction.messages(), - config, - extraction.sealMetadata())) - .orElseGet(Mono::empty)); + return DefaultMemoryExtractorObservation.observe( + observationRegistry, + memoryId, + () -> + Mono.fromCallable( + () -> + ConversationBufferLocks.withLock( + bufferKey, + () -> + pendingExtraction( + bufferKey, message))) + .subscribeOn(Schedulers.boundedElastic()) + .flatMap( + pending -> + pending.map( + extraction -> + extractConversationSegment( + memoryId, + extraction + .messages(), + config, + extraction + .sealMetadata())) + .orElseGet(Mono::empty))); + } + + private Optional pendingExtraction(String bufferKey, Message message) { + recentConversationBuffer.append(bufferKey, message); + if (message.role() == Message.Role.ASSISTANT) { + appendToPendingBuffer(bufferKey, message); + return Optional.empty(); + } + + var snapshot = pendingConversationBuffer.load(bufferKey); + if (snapshot.isEmpty()) { + appendToPendingBuffer(bufferKey, message); + return Optional.empty(); + } + + var detectionInput = + new CommitDetectionInput( + snapshot, List.of(message), CommitDetectionContext.empty()); + var decision = + contextCommitDetector + .shouldCommit(detectionInput) + .defaultIfEmpty(CommitDecision.hold()) + .block(); + + if (decision == null || !decision.shouldSeal()) { + appendToPendingBuffer(bufferKey, message); + return Optional.empty(); + } + + log.debug( + "Boundary detection triggered sealing: memoryId={}, reason={}, confidence={}", + bufferKey, + decision.reason(), + decision.confidence()); + + var sealedMessages = List.copyOf(snapshot); + pendingConversationBuffer.clear(bufferKey); + pendingConversationBuffer.append(bufferKey, message); + + return Optional.of(new PendingExtraction(sealedMessages, new HashMap<>())); } private void appendToPendingBuffer(String bufferKey, Message message) { diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/MemoryExtractor.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/MemoryExtractor.java index ce72f54a..5ea6c865 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/MemoryExtractor.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/MemoryExtractor.java @@ -23,8 +23,8 @@ *

Defines the memory extraction contract: batch extraction via {@link #extract(ExtractionRequest)} * and context single-message extraction via {@link #addMessage(MemoryId, Message, ExtractionConfig)}. * - *

The primary implementation is {@link DefaultMemoryExtractor}. Decorators (e.g., tracing) wrap this - * interface to add cross-cutting concerns. + *

The primary implementation is {@link DefaultMemoryExtractor}. Implementations publish observability signals at + * the relevant pipeline stages. */ public interface MemoryExtractor { diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/InsightLayer.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/InsightLayer.java index f8df7092..1a535c5b 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/InsightLayer.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/InsightLayer.java @@ -18,11 +18,13 @@ import com.openmemind.ai.memory.core.data.MemoryItem; import com.openmemind.ai.memory.core.data.enums.InsightAnalysisMode; import com.openmemind.ai.memory.core.data.enums.MemoryItemType; +import com.openmemind.ai.memory.core.extraction.insight.observation.InsightLayerObservation; import com.openmemind.ai.memory.core.extraction.insight.scheduler.InsightBuildScheduler; import com.openmemind.ai.memory.core.extraction.result.InsightResult; import com.openmemind.ai.memory.core.extraction.result.MemoryItemResult; import com.openmemind.ai.memory.core.extraction.step.InsightExtractStep; import com.openmemind.ai.memory.core.store.MemoryStore; +import io.micrometer.observation.ObservationRegistry; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -48,11 +50,19 @@ public class InsightLayer implements InsightExtractStep { private final MemoryStore memoryStore; private final InsightBuildScheduler scheduler; private final Set unsupportedContentTypes; + private final ObservationRegistry observationRegistry; public InsightLayer(MemoryStore memoryStore, InsightBuildScheduler scheduler) { this(memoryStore, scheduler, Set.of()); } + public InsightLayer( + MemoryStore memoryStore, + InsightBuildScheduler scheduler, + ObservationRegistry observationRegistry) { + this(memoryStore, scheduler, Set.of(), observationRegistry); + } + /** * @param memoryStore memory store * @param scheduler insight build scheduler @@ -63,10 +73,20 @@ public InsightLayer( MemoryStore memoryStore, InsightBuildScheduler scheduler, Set unsupportedContentTypes) { + this(memoryStore, scheduler, unsupportedContentTypes, ObservationRegistry.NOOP); + } + + public InsightLayer( + MemoryStore memoryStore, + InsightBuildScheduler scheduler, + Set unsupportedContentTypes, + ObservationRegistry observationRegistry) { this.memoryStore = Objects.requireNonNull(memoryStore); this.scheduler = Objects.requireNonNull(scheduler); this.unsupportedContentTypes = unsupportedContentTypes != null ? Set.copyOf(unsupportedContentTypes) : Set.of(); + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } @Override @@ -77,6 +97,14 @@ public Mono extract(MemoryId memoryId, MemoryItemResult memoryIte @Override public Mono extract( MemoryId memoryId, MemoryItemResult memoryItemResult, String language) { + return InsightLayerObservation.observe( + observationRegistry, + memoryId, + () -> extractInternal(memoryId, memoryItemResult, language)); + } + + private Mono extractInternal( + MemoryId memoryId, MemoryItemResult memoryItemResult, String language) { // Only take FACT type items whose content type supports insight building var factItems = memoryItemResult.newItems().stream() diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/generator/LlmInsightGenerator.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/generator/LlmInsightGenerator.java index 6dce10b3..9c4fe2fd 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/generator/LlmInsightGenerator.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/generator/LlmInsightGenerator.java @@ -17,6 +17,8 @@ import com.openmemind.ai.memory.core.data.MemoryInsight; import com.openmemind.ai.memory.core.data.MemoryInsightType; import com.openmemind.ai.memory.core.data.MemoryItem; +import com.openmemind.ai.memory.core.extraction.insight.generator.observation.LlmInsightGeneratorObservation; +import com.openmemind.ai.memory.core.extraction.insight.generator.observation.LlmInsightGeneratorObservation.InsightGenerateDocument; import com.openmemind.ai.memory.core.llm.ChatMessages; import com.openmemind.ai.memory.core.llm.StructuredChatClient; import com.openmemind.ai.memory.core.prompt.PromptRegistry; @@ -24,6 +26,7 @@ import com.openmemind.ai.memory.core.prompt.extraction.insight.InsightLeafPrompts; import com.openmemind.ai.memory.core.prompt.extraction.insight.InteractionGuideSynthesisPrompts; import com.openmemind.ai.memory.core.prompt.extraction.insight.RootSynthesisPrompts; +import io.micrometer.observation.ObservationRegistry; import java.time.Duration; import java.util.List; import java.util.Objects; @@ -45,18 +48,33 @@ public class LlmInsightGenerator implements InsightGenerator { private final StructuredChatClient structuredChatClient; private final PromptRegistry promptRegistry; + private final ObservationRegistry observationRegistry; public LlmInsightGenerator(StructuredChatClient structuredChatClient) { this(structuredChatClient, PromptRegistry.EMPTY); } + public LlmInsightGenerator( + StructuredChatClient structuredChatClient, ObservationRegistry observationRegistry) { + this(structuredChatClient, PromptRegistry.EMPTY, observationRegistry); + } + public LlmInsightGenerator( StructuredChatClient structuredChatClient, PromptRegistry promptRegistry) { + this(structuredChatClient, promptRegistry, ObservationRegistry.NOOP); + } + + public LlmInsightGenerator( + StructuredChatClient structuredChatClient, + PromptRegistry promptRegistry, + ObservationRegistry observationRegistry) { this.structuredChatClient = Objects.requireNonNull( structuredChatClient, "structuredChatClient must not be null"); this.promptRegistry = Objects.requireNonNull(promptRegistry, "promptRegistry must not be null"); + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } @Override @@ -68,6 +86,29 @@ public Mono generatePoints( int targetTokens, String additionalContext, String language) { + return LlmInsightGeneratorObservation.observeLeafPointGeneration( + observationRegistry, + insightType, + groupName, + () -> + generatePointsInternal( + insightType, + groupName, + existingPoints, + newItems, + targetTokens, + additionalContext, + language)); + } + + private Mono generatePointsInternal( + MemoryInsightType insightType, + String groupName, + List existingPoints, + List newItems, + int targetTokens, + String additionalContext, + String language) { var template = InsightLeafPrompts.build( @@ -109,6 +150,29 @@ public Mono generateLeafPointOps( int targetTokens, String additionalContext, String language) { + return LlmInsightGeneratorObservation.observeLeafPointOperations( + observationRegistry, + insightType, + groupName, + () -> + generateLeafPointOpsInternal( + insightType, + groupName, + existingPoints, + newItems, + targetTokens, + additionalContext, + language)); + } + + private Mono generateLeafPointOpsInternal( + MemoryInsightType insightType, + String groupName, + List existingPoints, + List newItems, + int targetTokens, + String additionalContext, + String language) { var template = InsightLeafPrompts.buildPointOps( @@ -160,6 +224,28 @@ public Mono generateBranchSummary( int targetTokens, String additionalContext, String language) { + return LlmInsightGeneratorObservation.observeAggregatePointGeneration( + observationRegistry, + InsightGenerateDocument.BRANCH, + insightType, + leafInsights, + () -> + generateBranchSummaryInternal( + insightType, + existingPoints, + leafInsights, + targetTokens, + additionalContext, + language)); + } + + private Mono generateBranchSummaryInternal( + MemoryInsightType insightType, + List existingPoints, + List leafInsights, + int targetTokens, + String additionalContext, + String language) { var promptResult = BranchAggregationPrompts.build( @@ -209,6 +295,28 @@ public Mono generateBranchPointOps( int targetTokens, String additionalContext, String language) { + return LlmInsightGeneratorObservation.observeAggregatePointOperations( + observationRegistry, + InsightGenerateDocument.BRANCH, + insightType, + leafInsights, + () -> + generateBranchPointOpsInternal( + insightType, + existingPoints, + leafInsights, + targetTokens, + additionalContext, + language)); + } + + private Mono generateBranchPointOpsInternal( + MemoryInsightType insightType, + List existingPoints, + List leafInsights, + int targetTokens, + String additionalContext, + String language) { var promptResult = BranchAggregationPrompts.buildPointOps( @@ -258,6 +366,28 @@ public Mono generateRootSynthesis( int targetTokens, String additionalContext, String language) { + return LlmInsightGeneratorObservation.observeAggregatePointGeneration( + observationRegistry, + InsightGenerateDocument.ROOT, + rootInsightType, + branchInsights, + () -> + generateRootSynthesisInternal( + rootInsightType, + existingPoints, + branchInsights, + targetTokens, + additionalContext, + language)); + } + + private Mono generateRootSynthesisInternal( + MemoryInsightType rootInsightType, + List existingPoints, + List branchInsights, + int targetTokens, + String additionalContext, + String language) { var template = switch (rootInsightType.name()) { diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/generator/observation/LlmInsightGeneratorObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/generator/observation/LlmInsightGeneratorObservation.java new file mode 100644 index 00000000..970b5caf --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/generator/observation/LlmInsightGeneratorObservation.java @@ -0,0 +1,390 @@ +/* + * 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 com.openmemind.ai.memory.core.extraction.insight.generator.observation; + +import com.openmemind.ai.memory.core.data.MemoryInsight; +import com.openmemind.ai.memory.core.data.MemoryInsightType; +import com.openmemind.ai.memory.core.data.PointOperation; +import com.openmemind.ai.memory.core.extraction.insight.generator.InsightPointGenerateResponse; +import com.openmemind.ai.memory.core.extraction.insight.generator.InsightPointOpsResponse; +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; + +/** Observation contracts for LlmInsightGenerator. */ +public final class LlmInsightGeneratorObservation { + + private LlmInsightGeneratorObservation() {} + + public static Mono observeLeafPointGeneration( + ObservationRegistry observationRegistry, + MemoryInsightType insightType, + String groupName, + Supplier> operation) { + return observeLeafPointGeneration( + observationRegistry, insightType, groupName, ignored -> operation.get()); + } + + public static Mono observeLeafPointGeneration( + ObservationRegistry observationRegistry, + MemoryInsightType insightType, + String groupName, + Function> + operation) { + return observePointGeneration( + observationRegistry, + InsightGenerateDocument.LEAF, + () -> InsightGenerateObservationContext.leaf(insightType, groupName), + operation); + } + + public static Mono observeLeafPointOperations( + ObservationRegistry observationRegistry, + MemoryInsightType insightType, + String groupName, + Supplier> operation) { + return observeLeafPointOperations( + observationRegistry, insightType, groupName, ignored -> operation.get()); + } + + public static Mono observeLeafPointOperations( + ObservationRegistry observationRegistry, + MemoryInsightType insightType, + String groupName, + Function> operation) { + return observePointOperations( + observationRegistry, + InsightGenerateDocument.LEAF, + () -> InsightGenerateObservationContext.leaf(insightType, groupName), + operation); + } + + public static Mono observeAggregatePointGeneration( + ObservationRegistry observationRegistry, + InsightGenerateDocument document, + MemoryInsightType insightType, + List insights, + Supplier> operation) { + return observeAggregatePointGeneration( + observationRegistry, document, insightType, insights, ignored -> operation.get()); + } + + public static Mono observeAggregatePointGeneration( + ObservationRegistry observationRegistry, + InsightGenerateDocument document, + MemoryInsightType insightType, + List insights, + Function> + operation) { + return observePointGeneration( + observationRegistry, + document, + () -> InsightGenerateObservationContext.aggregate(document, insightType, insights), + operation); + } + + public static Mono observeAggregatePointOperations( + ObservationRegistry observationRegistry, + InsightGenerateDocument document, + MemoryInsightType insightType, + List insights, + Supplier> operation) { + return observeAggregatePointOperations( + observationRegistry, document, insightType, insights, ignored -> operation.get()); + } + + public static Mono observeAggregatePointOperations( + ObservationRegistry observationRegistry, + InsightGenerateDocument document, + MemoryInsightType insightType, + List insights, + Function> operation) { + return observePointOperations( + observationRegistry, + document, + () -> InsightGenerateObservationContext.aggregate(document, insightType, insights), + operation); + } + + private static Mono observePointGeneration( + ObservationRegistry observationRegistry, + InsightGenerateDocument document, + Supplier contextFactory, + Function> + operation) { + return MemoryObservation.mono( + observationRegistry, + document, + InsightGenerateConvention.of(document), + contextFactory, + context -> operation.apply(context).doOnNext(context::recordPointResponse)); + } + + private static Mono observePointOperations( + ObservationRegistry observationRegistry, + InsightGenerateDocument document, + Supplier contextFactory, + Function> operation) { + return MemoryObservation.mono( + observationRegistry, + document, + InsightGenerateConvention.of(document), + contextFactory, + context -> operation.apply(context).doOnNext(context::recordOpsResponse)); + } + + public enum InsightGenerateDocument implements ObservationDocumentation { + LEAF("memind.extraction.insight.generate.leaf"), + BRANCH("memind.extraction.insight.generate.branch"), + ROOT("memind.extraction.insight.generate.root"); + + private final String name; + + InsightGenerateDocument(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public Class> + getDefaultConvention() { + return InsightGenerateConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return switch (this) { + case LEAF -> + new KeyName[] { + HighCardinalityKeyNames.INSIGHT_TYPE, + HighCardinalityKeyNames.GROUP_NAME, + HighCardinalityKeyNames.POINT_COUNT, + HighCardinalityKeyNames.ADD_COUNT, + HighCardinalityKeyNames.UPDATE_COUNT, + HighCardinalityKeyNames.DELETE_COUNT + }; + case BRANCH -> + new KeyName[] { + HighCardinalityKeyNames.INSIGHT_TYPE, + HighCardinalityKeyNames.LEAF_COUNT, + HighCardinalityKeyNames.POINT_COUNT, + HighCardinalityKeyNames.ADD_COUNT, + HighCardinalityKeyNames.UPDATE_COUNT, + HighCardinalityKeyNames.DELETE_COUNT + }; + case ROOT -> + new KeyName[] { + HighCardinalityKeyNames.INSIGHT_TYPE, + HighCardinalityKeyNames.LEAF_COUNT, + HighCardinalityKeyNames.POINT_COUNT + }; + }; + } + } + + public enum HighCardinalityKeyNames implements KeyName { + INSIGHT_TYPE { + @Override + public String asString() { + return "memind.extraction.insight_type"; + } + }, + GROUP_NAME { + @Override + public String asString() { + return "memind.extraction.insight_group_name"; + } + }, + LEAF_COUNT { + @Override + public String asString() { + return "memind.extraction.insight_leaf_count"; + } + }, + POINT_COUNT { + @Override + public String asString() { + return "memind.extraction.insight_point_count"; + } + }, + ADD_COUNT { + @Override + public String asString() { + return "memind.extraction.insight_add_count"; + } + }, + UPDATE_COUNT { + @Override + public String asString() { + return "memind.extraction.insight_update_count"; + } + }, + DELETE_COUNT { + @Override + public String asString() { + return "memind.extraction.insight_delete_count"; + } + }; + } + + public static final class InsightGenerateObservationContext extends Observation.Context { + + private final InsightGenerateDocument document; + private final String insightTypeName; + private final String groupName; + private final Integer leafCount; + private Integer pointCount; + private Integer addCount; + private Integer updateCount; + private Integer deleteCount; + + public InsightGenerateObservationContext( + InsightGenerateDocument document, + String insightTypeName, + String groupName, + Integer leafCount) { + this.document = document; + this.insightTypeName = insightTypeName; + this.groupName = groupName; + this.leafCount = leafCount; + } + + public static InsightGenerateObservationContext leaf( + MemoryInsightType insightType, String groupName) { + return new InsightGenerateObservationContext( + InsightGenerateDocument.LEAF, + insightType == null ? "" : insightType.name(), + groupName == null ? "" : groupName, + null); + } + + public static InsightGenerateObservationContext aggregate( + InsightGenerateDocument document, + MemoryInsightType insightType, + List insights) { + return new InsightGenerateObservationContext( + document, + insightType == null ? "" : insightType.name(), + null, + insights == null ? 0 : insights.size()); + } + + public void recordPointResponse(InsightPointGenerateResponse response) { + this.pointCount = + response == null || response.points() == null ? 0 : response.points().size(); + } + + public void recordOpsResponse(InsightPointOpsResponse response) { + var operations = response == null ? List.of() : response.operations(); + this.addCount = countOperations(operations, PointOperation.OpType.ADD); + this.updateCount = countOperations(operations, PointOperation.OpType.UPDATE); + this.deleteCount = countOperations(operations, PointOperation.OpType.DELETE); + } + + private static int countOperations( + List operations, PointOperation.OpType opType) { + return (int) operations.stream().filter(operation -> operation.op() == opType).count(); + } + } + + public static final class InsightGenerateConvention + implements ObservationConvention { + + public static final InsightGenerateConvention LEAF = + new InsightGenerateConvention(InsightGenerateDocument.LEAF); + public static final InsightGenerateConvention BRANCH = + new InsightGenerateConvention(InsightGenerateDocument.BRANCH); + public static final InsightGenerateConvention ROOT = + new InsightGenerateConvention(InsightGenerateDocument.ROOT); + + private final InsightGenerateDocument document; + + public InsightGenerateConvention(InsightGenerateDocument document) { + this.document = document; + } + + public static InsightGenerateConvention of(InsightGenerateDocument document) { + return switch (document) { + case LEAF -> LEAF; + case BRANCH -> BRANCH; + case ROOT -> ROOT; + }; + } + + @Override + public String getName() { + return document.getName(); + } + + @Override + public String getContextualName(InsightGenerateObservationContext context) { + return context.document.getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(InsightGenerateObservationContext context) { + var values = + KeyValues.of( + HighCardinalityKeyNames.INSIGHT_TYPE.withValue( + context.insightTypeName)); + if (context.groupName != null) { + values = + values.and(HighCardinalityKeyNames.GROUP_NAME.withValue(context.groupName)); + } + if (context.leafCount != null) { + values = + values.and( + HighCardinalityKeyNames.LEAF_COUNT.withValue( + String.valueOf(context.leafCount))); + } + if (context.pointCount != null) { + values = + values.and( + HighCardinalityKeyNames.POINT_COUNT.withValue( + String.valueOf(context.pointCount))); + } + if (context.addCount != null) { + values = + values.and( + HighCardinalityKeyNames.ADD_COUNT.withValue( + String.valueOf(context.addCount))) + .and( + HighCardinalityKeyNames.UPDATE_COUNT.withValue( + String.valueOf(context.updateCount))) + .and( + HighCardinalityKeyNames.DELETE_COUNT.withValue( + String.valueOf(context.deleteCount))); + } + return values; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof InsightGenerateObservationContext generateContext + && generateContext.document == document; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/group/LlmInsightGroupClassifier.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/group/LlmInsightGroupClassifier.java index c5eb0c86..fb0eea20 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/group/LlmInsightGroupClassifier.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/group/LlmInsightGroupClassifier.java @@ -15,10 +15,12 @@ import com.openmemind.ai.memory.core.data.MemoryInsightType; import com.openmemind.ai.memory.core.data.MemoryItem; +import com.openmemind.ai.memory.core.extraction.insight.group.observation.LlmInsightGroupClassifierObservation; import com.openmemind.ai.memory.core.llm.ChatMessages; import com.openmemind.ai.memory.core.llm.StructuredChatClient; import com.openmemind.ai.memory.core.prompt.PromptRegistry; import com.openmemind.ai.memory.core.prompt.extraction.insight.InsightGroupPrompts; +import io.micrometer.observation.ObservationRegistry; import java.time.Duration; import java.util.ArrayList; import java.util.Comparator; @@ -81,18 +83,33 @@ public class LlmInsightGroupClassifier implements InsightGroupClassifier { private final StructuredChatClient structuredChatClient; private final PromptRegistry promptRegistry; + private final ObservationRegistry observationRegistry; public LlmInsightGroupClassifier(StructuredChatClient structuredChatClient) { this(structuredChatClient, PromptRegistry.EMPTY); } + public LlmInsightGroupClassifier( + StructuredChatClient structuredChatClient, ObservationRegistry observationRegistry) { + this(structuredChatClient, PromptRegistry.EMPTY, observationRegistry); + } + public LlmInsightGroupClassifier( StructuredChatClient structuredChatClient, PromptRegistry promptRegistry) { + this(structuredChatClient, promptRegistry, ObservationRegistry.NOOP); + } + + public LlmInsightGroupClassifier( + StructuredChatClient structuredChatClient, + PromptRegistry promptRegistry, + ObservationRegistry observationRegistry) { this.structuredChatClient = Objects.requireNonNull( structuredChatClient, "structuredChatClient must not be null"); this.promptRegistry = Objects.requireNonNull(promptRegistry, "promptRegistry must not be null"); + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } @Override @@ -119,6 +136,25 @@ public Mono>> classify( List existingGroupNames, String additionalContext, String language) { + return LlmInsightGroupClassifierObservation.observe( + observationRegistry, + insightType, + items, + () -> + classifyInternal( + insightType, + items, + existingGroupNames, + additionalContext, + language)); + } + + private Mono>> classifyInternal( + MemoryInsightType insightType, + List items, + List existingGroupNames, + String additionalContext, + String language) { if (items.isEmpty()) { return Mono.just(Map.of()); diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/group/observation/LlmInsightGroupClassifierObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/group/observation/LlmInsightGroupClassifierObservation.java new file mode 100644 index 00000000..9e4ebe6c --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/group/observation/LlmInsightGroupClassifierObservation.java @@ -0,0 +1,152 @@ +/* + * 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 com.openmemind.ai.memory.core.extraction.insight.group.observation; + +import com.openmemind.ai.memory.core.data.MemoryInsightType; +import com.openmemind.ai.memory.core.data.MemoryItem; +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; + +/** Observation contracts for LlmInsightGroupClassifier. */ +public final class LlmInsightGroupClassifierObservation { + + private LlmInsightGroupClassifierObservation() {} + + public static Mono>> observe( + ObservationRegistry observationRegistry, + MemoryInsightType insightType, + List items, + Supplier>>> operation) { + return observe(observationRegistry, insightType, items, ignored -> operation.get()); + } + + public static Mono>> observe( + ObservationRegistry observationRegistry, + MemoryInsightType insightType, + List items, + Function>>> + operation) { + return MemoryObservation.mono( + observationRegistry, + GroupClassifyDocument.CLASSIFY, + GroupClassifyConvention.INSTANCE, + () -> new GroupClassifyObservationContext(insightType, items), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum GroupClassifyDocument implements ObservationDocumentation { + CLASSIFY; + + @Override + public String getName() { + return "memind.extraction.insight.group.classify"; + } + + @Override + public Class> + getDefaultConvention() { + return GroupClassifyConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + INSIGHT_TYPE { + @Override + public String asString() { + return "memind.extraction.insight_type"; + } + }, + ITEM_COUNT { + @Override + public String asString() { + return "memind.extraction.item_count"; + } + }, + GROUP_COUNT { + @Override + public String asString() { + return "memind.extraction.insight_group_count"; + } + }; + } + + public static final class GroupClassifyObservationContext extends Observation.Context { + + private final String insightTypeName; + private final int itemCount; + private Integer groupCount; + + public GroupClassifyObservationContext( + MemoryInsightType insightType, List items) { + this.insightTypeName = insightType == null ? "" : insightType.name(); + this.itemCount = items == null ? 0 : items.size(); + } + + public void recordResult(Map> result) { + this.groupCount = result.size(); + } + } + + public static final class GroupClassifyConvention + implements ObservationConvention { + + public static final GroupClassifyConvention INSTANCE = new GroupClassifyConvention(); + + @Override + public String getName() { + return GroupClassifyDocument.CLASSIFY.getName(); + } + + @Override + public String getContextualName(GroupClassifyObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(GroupClassifyObservationContext context) { + var values = + KeyValues.of( + HighCardinalityKeyNames.INSIGHT_TYPE.withValue(context.insightTypeName), + HighCardinalityKeyNames.ITEM_COUNT.withValue( + String.valueOf(context.itemCount))); + if (context.groupCount != null) { + values = + values.and( + HighCardinalityKeyNames.GROUP_COUNT.withValue( + String.valueOf(context.groupCount))); + } + return values; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof GroupClassifyObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/observation/InsightLayerObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/observation/InsightLayerObservation.java new file mode 100644 index 00000000..bb64bc32 --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/observation/InsightLayerObservation.java @@ -0,0 +1,117 @@ +/* + * 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 com.openmemind.ai.memory.core.extraction.insight.observation; + +import com.openmemind.ai.memory.core.data.MemoryId; +import com.openmemind.ai.memory.core.extraction.result.InsightResult; +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; + +/** Observation contracts for InsightLayer. */ +public final class InsightLayerObservation { + + private InsightLayerObservation() {} + + public static Mono observe( + ObservationRegistry observationRegistry, + MemoryId memoryId, + Supplier> operation) { + return observe(observationRegistry, memoryId, ignored -> operation.get()); + } + + public static Mono observe( + ObservationRegistry observationRegistry, + MemoryId memoryId, + Function> operation) { + return MemoryObservation.mono( + observationRegistry, + InsightDocument.EXTRACT, + InsightConvention.INSTANCE, + () -> new InsightObservationContext(memoryId), + operation); + } + + public enum InsightDocument implements ObservationDocumentation { + EXTRACT; + + @Override + public String getName() { + return "memind.extraction.insight"; + } + + @Override + public Class> + getDefaultConvention() { + return InsightConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }; + } + + public static final class InsightObservationContext extends Observation.Context { + + private final MemoryId memoryId; + + public InsightObservationContext(MemoryId memoryId) { + this.memoryId = memoryId; + } + } + + public static final class InsightConvention + implements ObservationConvention { + + public static final InsightConvention INSTANCE = new InsightConvention(); + + @Override + public String getName() { + return InsightDocument.EXTRACT.getName(); + } + + @Override + public String getContextualName(InsightObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(InsightObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue(context.memoryId.toIdentifier())); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof InsightObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/reference/InsightReferences.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/reference/InsightReferences.java index 278044a9..a461629f 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/reference/InsightReferences.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/reference/InsightReferences.java @@ -21,7 +21,7 @@ * Insight reference parsing tool * *

Parse and manipulate the {@code [ref:ITEM_ID]} reference tags in the summary, - * supporting tracing the source item of each summary information. + * supporting tracking the source item of each summary information. * */ public final class InsightReferences { diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/scheduler/InsightBuildScheduler.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/scheduler/InsightBuildScheduler.java index d979aaba..9b63b5b3 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/scheduler/InsightBuildScheduler.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/scheduler/InsightBuildScheduler.java @@ -31,17 +31,14 @@ import com.openmemind.ai.memory.core.extraction.insight.group.InsightGroupClassifier; import com.openmemind.ai.memory.core.extraction.insight.group.InsightGroupRouter; import com.openmemind.ai.memory.core.extraction.insight.operation.PointOperationResolver; +import com.openmemind.ai.memory.core.extraction.insight.scheduler.observation.InsightBuildSchedulerObservation; import com.openmemind.ai.memory.core.extraction.insight.support.InsightPointEvidenceNormalizer; import com.openmemind.ai.memory.core.extraction.insight.support.InsightPointIdentityManager; import com.openmemind.ai.memory.core.extraction.insight.tree.InsightTreeReorganizer; import com.openmemind.ai.memory.core.store.MemoryStore; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.NoopMemoryObserver; -import com.openmemind.ai.memory.core.tracing.ObservationContext; import com.openmemind.ai.memory.core.utils.IdUtils; import com.openmemind.ai.memory.core.vector.MemoryVector; +import io.micrometer.observation.ObservationRegistry; import java.io.Closeable; import java.time.Instant; import java.util.ArrayList; @@ -62,7 +59,6 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; /** * Insight Build Scheduler @@ -87,7 +83,7 @@ public class InsightBuildScheduler implements Closeable { private final InsightBuildConfig config; private final InsightPointIdentityManager pointIdentityManager; private final InsightPointEvidenceNormalizer evidenceNormalizer; - private final MemoryObserver observer; + private final ObservationRegistry observationRegistry; private final ExecutorService executor; private final Semaphore semaphore; @@ -157,7 +153,7 @@ public InsightBuildScheduler( MemoryVector memoryVector, IdUtils.SnowflakeIdGenerator idGenerator, InsightBuildConfig config, - MemoryObserver observer) { + ObservationRegistry observationRegistry) { this( bufferStore, store, @@ -171,7 +167,7 @@ public InsightBuildScheduler( new InsightPointIdentityManager(), new InsightPointEvidenceNormalizer(), NoOpInsightGraphAssistant.INSTANCE, - observer); + observationRegistry); } public InsightBuildScheduler( @@ -185,7 +181,7 @@ public InsightBuildScheduler( IdUtils.SnowflakeIdGenerator idGenerator, InsightBuildConfig config, InsightGraphAssistant graphAssistant, - MemoryObserver observer) { + ObservationRegistry observationRegistry) { this( bufferStore, store, @@ -199,7 +195,7 @@ public InsightBuildScheduler( new InsightPointIdentityManager(), new InsightPointEvidenceNormalizer(), graphAssistant, - observer); + observationRegistry); } public InsightBuildScheduler( @@ -214,7 +210,7 @@ public InsightBuildScheduler( InsightBuildConfig config, InsightPointIdentityManager pointIdentityManager, InsightPointEvidenceNormalizer evidenceNormalizer, - MemoryObserver observer) { + ObservationRegistry observationRegistry) { this( bufferStore, store, @@ -228,7 +224,7 @@ public InsightBuildScheduler( pointIdentityManager, evidenceNormalizer, NoOpInsightGraphAssistant.INSTANCE, - observer); + observationRegistry); } public InsightBuildScheduler( @@ -244,7 +240,7 @@ public InsightBuildScheduler( InsightPointIdentityManager pointIdentityManager, InsightPointEvidenceNormalizer evidenceNormalizer, InsightGraphAssistant graphAssistant, - MemoryObserver observer) { + ObservationRegistry observationRegistry) { this.bufferStore = Objects.requireNonNull(bufferStore); this.store = Objects.requireNonNull(store); this.generator = Objects.requireNonNull(generator); @@ -258,7 +254,8 @@ public InsightBuildScheduler( this.config = Objects.requireNonNull(config); this.pointIdentityManager = Objects.requireNonNull(pointIdentityManager); this.evidenceNormalizer = Objects.requireNonNull(evidenceNormalizer); - this.observer = observer != null ? observer : new NoopMemoryObserver(); + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; this.executor = Executors.newVirtualThreadPerTaskExecutor(); this.semaphore = new Semaphore(config.concurrency()); } @@ -405,23 +402,11 @@ private void runPipeline( List itemIds, boolean force, String language) { - observer.observeMono( - ObservationContext.of( - MemorySpanNames.EXTRACTION_INSIGHT_PIPELINE, - Map.of( - MemoryAttributes.MEMORY_ID, - memoryId.toIdentifier(), - MemoryAttributes.EXTRACTION_INSIGHT_TYPE, - insightTypeName)), - () -> - Mono.fromRunnable( - () -> - doRunPipeline( - memoryId, - insightTypeName, - itemIds, - force, - language))) + InsightBuildSchedulerObservation.observePipeline( + observationRegistry, + memoryId, + insightTypeName, + () -> doRunPipeline(memoryId, insightTypeName, itemIds, force, language)) .block(); } @@ -894,28 +879,21 @@ private void phaseTreeReorganize( List builtLeafs, String language) { try { - observer.observeMono( - ObservationContext.of( - MemorySpanNames.EXTRACTION_INSIGHT_TREE_REORGANIZE, - Map.of( - MemoryAttributes.MEMORY_ID, - memoryId.toIdentifier(), - MemoryAttributes.EXTRACTION_INSIGHT_TYPE, - insightTypeName, - MemoryAttributes.EXTRACTION_INSIGHT_LEAF_COUNT, - builtLeafs.size())), - () -> - Mono.fromRunnable( - () -> { - var treeConfig = insightType.resolveTreeConfig(); - treeReorganizer.onLeafsUpdated( - memoryId, - insightTypeName, - insightType, - builtLeafs, - treeConfig, - language); - })) + InsightBuildSchedulerObservation.observeTreeReorganize( + observationRegistry, + memoryId, + insightTypeName, + builtLeafs.size(), + () -> { + var treeConfig = insightType.resolveTreeConfig(); + treeReorganizer.onLeafsUpdated( + memoryId, + insightTypeName, + insightType, + builtLeafs, + treeConfig, + language); + }) .block(); log.debug( "Phase 4 Tree Reorganize completed [type={}, leafs={}]", diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/scheduler/observation/InsightBuildSchedulerObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/scheduler/observation/InsightBuildSchedulerObservation.java new file mode 100644 index 00000000..72980b0e --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/insight/scheduler/observation/InsightBuildSchedulerObservation.java @@ -0,0 +1,215 @@ +/* + * 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 com.openmemind.ai.memory.core.extraction.insight.scheduler.observation; + +import com.openmemind.ai.memory.core.data.MemoryId; +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import reactor.core.publisher.Mono; + +/** Observation contracts for InsightBuildScheduler. */ +public final class InsightBuildSchedulerObservation { + + private InsightBuildSchedulerObservation() {} + + public static Mono observePipeline( + ObservationRegistry observationRegistry, + MemoryId memoryId, + String insightTypeName, + Runnable operation) { + return MemoryObservation.mono( + observationRegistry, + PipelineDocument.RUN, + PipelineConvention.INSTANCE, + () -> new PipelineObservationContext(memoryId, insightTypeName), + ignored -> Mono.fromRunnable(operation).then()); + } + + public static Mono observeTreeReorganize( + ObservationRegistry observationRegistry, + MemoryId memoryId, + String insightTypeName, + int leafCount, + Runnable operation) { + return MemoryObservation.mono( + observationRegistry, + TreeReorganizeDocument.RUN, + TreeReorganizeConvention.INSTANCE, + () -> new TreeReorganizeObservationContext(memoryId, insightTypeName, leafCount), + ignored -> Mono.fromRunnable(operation).then()); + } + + public enum PipelineDocument implements ObservationDocumentation { + RUN("memind.extraction.insight.pipeline"); + + private final String name; + + PipelineDocument(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public Class> + getDefaultConvention() { + return PipelineConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return new KeyName[] { + HighCardinalityKeyNames.MEMORY_ID, HighCardinalityKeyNames.INSIGHT_TYPE + }; + } + } + + public enum TreeReorganizeDocument implements ObservationDocumentation { + RUN("memind.extraction.insight.tree.reorganize"); + + private final String name; + + TreeReorganizeDocument(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public Class> + getDefaultConvention() { + return TreeReorganizeConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }, + INSIGHT_TYPE { + @Override + public String asString() { + return "memind.extraction.insight_type"; + } + }, + LEAF_COUNT { + @Override + public String asString() { + return "memind.extraction.insight_leaf_count"; + } + }; + } + + public static final class PipelineObservationContext extends Observation.Context { + + private final String memoryId; + private final String insightTypeName; + + public PipelineObservationContext(MemoryId memoryId, String insightTypeName) { + this.memoryId = memoryId == null ? "" : memoryId.toIdentifier(); + this.insightTypeName = insightTypeName == null ? "" : insightTypeName; + } + } + + public static final class TreeReorganizeObservationContext extends Observation.Context { + + private final String memoryId; + private final String insightTypeName; + private final int leafCount; + + public TreeReorganizeObservationContext( + MemoryId memoryId, String insightTypeName, int leafCount) { + this.memoryId = memoryId == null ? "" : memoryId.toIdentifier(); + this.insightTypeName = insightTypeName == null ? "" : insightTypeName; + this.leafCount = leafCount; + } + } + + public static final class PipelineConvention + implements ObservationConvention { + + public static final PipelineConvention INSTANCE = new PipelineConvention(); + + @Override + public String getName() { + return PipelineDocument.RUN.getName(); + } + + @Override + public String getContextualName(PipelineObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(PipelineObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue(context.memoryId), + HighCardinalityKeyNames.INSIGHT_TYPE.withValue(context.insightTypeName)); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof PipelineObservationContext; + } + } + + public static final class TreeReorganizeConvention + implements ObservationConvention { + + public static final TreeReorganizeConvention INSTANCE = new TreeReorganizeConvention(); + + @Override + public String getName() { + return TreeReorganizeDocument.RUN.getName(); + } + + @Override + public String getContextualName(TreeReorganizeObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(TreeReorganizeObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue(context.memoryId), + HighCardinalityKeyNames.INSIGHT_TYPE.withValue(context.insightTypeName), + HighCardinalityKeyNames.LEAF_COUNT.withValue( + String.valueOf(context.leafCount))); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof TreeReorganizeObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayer.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayer.java index 9650cdd9..3e868a36 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayer.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayer.java @@ -23,6 +23,7 @@ import com.openmemind.ai.memory.core.extraction.item.extractor.MemoryItemExtractor; import com.openmemind.ai.memory.core.extraction.item.graph.ItemGraphMaterializer; import com.openmemind.ai.memory.core.extraction.item.graph.NoOpItemGraphMaterializer; +import com.openmemind.ai.memory.core.extraction.item.observation.MemoryItemLayerObservation; import com.openmemind.ai.memory.core.extraction.item.support.ExtractedMemoryEntry; import com.openmemind.ai.memory.core.extraction.item.support.ItemEmbeddingTextResolver; import com.openmemind.ai.memory.core.extraction.rawdata.ParsedSegment; @@ -32,6 +33,7 @@ import com.openmemind.ai.memory.core.store.MemoryStore; import com.openmemind.ai.memory.core.utils.IdUtils; import com.openmemind.ai.memory.core.vector.MemoryVector; +import io.micrometer.observation.ObservationRegistry; import java.time.Instant; import java.util.ArrayList; import java.util.Comparator; @@ -60,6 +62,7 @@ public class MemoryItemLayer implements MemoryItemExtractStep { private final IdUtils.SnowflakeIdGenerator idGenerator; private final LlmSelfVerificationStep selfVerificationStep; // nullable private final ItemGraphMaterializer graphMaterializer; + private final ObservationRegistry observationRegistry; public MemoryItemLayer( MemoryItemExtractor extractor, @@ -76,6 +79,23 @@ public MemoryItemLayer( NoOpItemGraphMaterializer.persistItemsOnly(memoryStore.itemOperations())); } + public MemoryItemLayer( + MemoryItemExtractor extractor, + MemoryItemDeduplicator deduplicator, + MemoryStore memoryStore, + MemoryVector vector, + ObservationRegistry observationRegistry) { + this( + extractor, + deduplicator, + memoryStore, + vector, + IdUtils.snowflake(), + null, + NoOpItemGraphMaterializer.persistItemsOnly(memoryStore.itemOperations()), + observationRegistry); + } + public MemoryItemLayer( MemoryItemExtractor extractor, MemoryItemDeduplicator deduplicator, @@ -89,7 +109,8 @@ public MemoryItemLayer( vector, IdUtils.snowflake(), selfVerificationStep, - NoOpItemGraphMaterializer.persistItemsOnly(memoryStore.itemOperations())); + NoOpItemGraphMaterializer.persistItemsOnly(memoryStore.itemOperations()), + ObservationRegistry.NOOP); } public MemoryItemLayer( @@ -105,7 +126,8 @@ public MemoryItemLayer( vector, IdUtils.snowflake(), null, - graphMaterializer); + graphMaterializer, + ObservationRegistry.NOOP); } public MemoryItemLayer( @@ -122,7 +144,8 @@ public MemoryItemLayer( vector, idGenerator, selfVerificationStep, - NoOpItemGraphMaterializer.persistItemsOnly(memoryStore.itemOperations())); + NoOpItemGraphMaterializer.persistItemsOnly(memoryStore.itemOperations()), + ObservationRegistry.NOOP); } public MemoryItemLayer( @@ -133,6 +156,26 @@ public MemoryItemLayer( IdUtils.SnowflakeIdGenerator idGenerator, LlmSelfVerificationStep selfVerificationStep, ItemGraphMaterializer graphMaterializer) { + this( + extractor, + deduplicator, + memoryStore, + vector, + idGenerator, + selfVerificationStep, + graphMaterializer, + ObservationRegistry.NOOP); + } + + public MemoryItemLayer( + MemoryItemExtractor extractor, + MemoryItemDeduplicator deduplicator, + MemoryStore memoryStore, + MemoryVector vector, + IdUtils.SnowflakeIdGenerator idGenerator, + LlmSelfVerificationStep selfVerificationStep, + ItemGraphMaterializer graphMaterializer, + ObservationRegistry observationRegistry) { this.extractor = extractor; this.deduplicator = deduplicator; this.memoryStore = memoryStore; @@ -143,11 +186,21 @@ public MemoryItemLayer( graphMaterializer != null ? graphMaterializer : NoOpItemGraphMaterializer.persistItemsOnly(memoryStore.itemOperations()); + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } @Override public Mono extract( MemoryId memoryId, RawDataResult rawDataResult, ItemExtractionConfig config) { + return MemoryItemLayerObservation.observe( + observationRegistry, + memoryId, + () -> extractInternal(memoryId, rawDataResult, config)); + } + + private Mono extractInternal( + MemoryId memoryId, RawDataResult rawDataResult, ItemExtractionConfig config) { List resolvedInsightTypes = resolveInsightTypes(); diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/CompositeDeduplicator.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/CompositeDeduplicator.java index dad6dd03..2e50f0fe 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/CompositeDeduplicator.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/CompositeDeduplicator.java @@ -14,7 +14,9 @@ package com.openmemind.ai.memory.core.extraction.item.dedup; import com.openmemind.ai.memory.core.data.MemoryId; +import com.openmemind.ai.memory.core.extraction.item.dedup.observation.CompositeDeduplicatorObservation; import com.openmemind.ai.memory.core.extraction.item.support.ExtractedMemoryEntry; +import io.micrometer.observation.ObservationRegistry; import java.util.ArrayList; import java.util.List; import reactor.core.publisher.Flux; @@ -30,14 +32,28 @@ public class CompositeDeduplicator implements MemoryItemDeduplicator { private final List deduplicators; + private final ObservationRegistry observationRegistry; public CompositeDeduplicator(List deduplicators) { + this(deduplicators, ObservationRegistry.NOOP); + } + + public CompositeDeduplicator( + List deduplicators, ObservationRegistry observationRegistry) { this.deduplicators = List.copyOf(deduplicators); + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } @Override public Mono deduplicate( MemoryId memoryId, List entries) { + return CompositeDeduplicatorObservation.observe( + observationRegistry, memoryId, () -> deduplicateInternal(memoryId, entries)); + } + + private Mono deduplicateInternal( + MemoryId memoryId, List entries) { var seed = Mono.just(new DeduplicationResult(entries, List.of())); return Flux.fromIterable(deduplicators) @@ -55,11 +71,6 @@ public Mono deduplicate( .flatMap(mono -> mono); } - @Override - public String spanName() { - return "memind.extraction.item.dedup"; - } - private DeduplicationResult merge(DeduplicationResult acc, DeduplicationResult stage) { var allMatched = new ArrayList<>(acc.matchedItems()); allMatched.addAll(stage.matchedItems()); diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/HashBasedDeduplicator.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/HashBasedDeduplicator.java index f72ae687..f350af24 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/HashBasedDeduplicator.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/HashBasedDeduplicator.java @@ -17,7 +17,6 @@ import com.openmemind.ai.memory.core.data.MemoryItem; import com.openmemind.ai.memory.core.extraction.item.support.ExtractedMemoryEntry; import com.openmemind.ai.memory.core.store.MemoryStore; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; import com.openmemind.ai.memory.core.utils.HashUtils; import java.util.ArrayList; import java.util.List; @@ -95,9 +94,4 @@ public Mono deduplicate( List.copyOf(newEntries), List.copyOf(matchedItems)); }); } - - @Override - public String spanName() { - return MemorySpanNames.EXTRACTION_ITEM_DEDUP; - } } diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/MemoryItemDeduplicator.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/MemoryItemDeduplicator.java index ff61fd98..1f769892 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/MemoryItemDeduplicator.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/MemoryItemDeduplicator.java @@ -32,9 +32,4 @@ public interface MemoryItemDeduplicator { * @return Deduplication result */ Mono deduplicate(MemoryId memoryId, List entries); - - /** - * The span name corresponding to this deduplicator - */ - String spanName(); } diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/SemanticDeduplicator.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/SemanticDeduplicator.java index d8eb133b..63a62f9b 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/SemanticDeduplicator.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/SemanticDeduplicator.java @@ -17,7 +17,6 @@ import com.openmemind.ai.memory.core.data.MemoryItem; import com.openmemind.ai.memory.core.extraction.item.support.ExtractedMemoryEntry; import com.openmemind.ai.memory.core.store.MemoryStore; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; import com.openmemind.ai.memory.core.vector.MemoryVector; import java.util.ArrayList; import java.util.List; @@ -106,10 +105,5 @@ public Mono deduplicate( }); } - @Override - public String spanName() { - return MemorySpanNames.EXTRACTION_ITEM_SEMANTIC_DEDUP; - } - private record EntryMatch(ExtractedMemoryEntry entry, String vectorId, float score) {} } diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/observation/CompositeDeduplicatorObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/observation/CompositeDeduplicatorObservation.java new file mode 100644 index 00000000..1a74499c --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/dedup/observation/CompositeDeduplicatorObservation.java @@ -0,0 +1,117 @@ +/* + * 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 com.openmemind.ai.memory.core.extraction.item.dedup.observation; + +import com.openmemind.ai.memory.core.data.MemoryId; +import com.openmemind.ai.memory.core.extraction.item.dedup.DeduplicationResult; +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; + +/** Observation contracts for CompositeDeduplicator. */ +public final class CompositeDeduplicatorObservation { + + private CompositeDeduplicatorObservation() {} + + public static Mono observe( + ObservationRegistry observationRegistry, + MemoryId memoryId, + Supplier> operation) { + return observe(observationRegistry, memoryId, ignored -> operation.get()); + } + + public static Mono observe( + ObservationRegistry observationRegistry, + MemoryId memoryId, + Function> operation) { + return MemoryObservation.mono( + observationRegistry, + DeduplicationDocument.DEDUPLICATE, + DeduplicationConvention.INSTANCE, + () -> new DeduplicationObservationContext(memoryId), + operation); + } + + public enum DeduplicationDocument implements ObservationDocumentation { + DEDUPLICATE; + + @Override + public String getName() { + return "memind.extraction.item.dedup"; + } + + @Override + public Class> + getDefaultConvention() { + return DeduplicationConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }; + } + + public static final class DeduplicationObservationContext extends Observation.Context { + + private final MemoryId memoryId; + + public DeduplicationObservationContext(MemoryId memoryId) { + this.memoryId = memoryId; + } + } + + public static final class DeduplicationConvention + implements ObservationConvention { + + public static final DeduplicationConvention INSTANCE = new DeduplicationConvention(); + + @Override + public String getName() { + return DeduplicationDocument.DEDUPLICATE.getName(); + } + + @Override + public String getContextualName(DeduplicationObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(DeduplicationObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue(context.memoryId.toIdentifier())); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof DeduplicationObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/graph/pipeline/DefaultItemGraphMaterializer.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/graph/pipeline/DefaultItemGraphMaterializer.java index 41056496..c7409159 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/graph/pipeline/DefaultItemGraphMaterializer.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/graph/pipeline/DefaultItemGraphMaterializer.java @@ -20,9 +20,11 @@ import com.openmemind.ai.memory.core.extraction.item.graph.ItemGraphMaterializer; import com.openmemind.ai.memory.core.extraction.item.graph.commit.ExtractionBatchId; import com.openmemind.ai.memory.core.extraction.item.graph.derived.GraphDerivedMaintainer; +import com.openmemind.ai.memory.core.extraction.item.graph.pipeline.observation.DefaultItemGraphMaterializerObservation; import com.openmemind.ai.memory.core.extraction.item.graph.plan.DefaultItemGraphPlanner; import com.openmemind.ai.memory.core.extraction.item.support.ExtractedMemoryEntry; import com.openmemind.ai.memory.core.store.graph.ItemGraphCommitOperations; +import io.micrometer.observation.ObservationRegistry; import java.util.List; import java.util.Objects; import reactor.core.publisher.Mono; @@ -36,21 +38,42 @@ public final class DefaultItemGraphMaterializer implements ItemGraphMaterializer private final ItemGraphCommitOperations commitOperations; private final GraphDerivedMaintainer derivedMaintainer; private final ItemGraphOptions options; + private final ObservationRegistry observationRegistry; public DefaultItemGraphMaterializer( DefaultItemGraphPlanner planner, ItemGraphCommitOperations commitOperations, GraphDerivedMaintainer derivedMaintainer, ItemGraphOptions options) { + this(planner, commitOperations, derivedMaintainer, options, ObservationRegistry.NOOP); + } + + public DefaultItemGraphMaterializer( + DefaultItemGraphPlanner planner, + ItemGraphCommitOperations commitOperations, + GraphDerivedMaintainer derivedMaintainer, + ItemGraphOptions options, + ObservationRegistry observationRegistry) { this.planner = Objects.requireNonNull(planner, "planner"); this.commitOperations = Objects.requireNonNull(commitOperations, "commitOperations"); this.derivedMaintainer = Objects.requireNonNull(derivedMaintainer, "derivedMaintainer"); this.options = Objects.requireNonNull(options, "options"); + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } @Override public Mono materialize( MemoryId memoryId, List items, List sourceEntries) { + return DefaultItemGraphMaterializerObservation.observe( + observationRegistry, + memoryId, + items, + () -> materializeInternal(memoryId, items, sourceEntries)); + } + + private Mono materializeInternal( + MemoryId memoryId, List items, List sourceEntries) { if (!options.enabled() || items == null || items.isEmpty()) { return Mono.just(ItemGraphMaterializationResult.empty()); } diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/graph/pipeline/observation/DefaultItemGraphMaterializerObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/graph/pipeline/observation/DefaultItemGraphMaterializerObservation.java new file mode 100644 index 00000000..67032c44 --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/graph/pipeline/observation/DefaultItemGraphMaterializerObservation.java @@ -0,0 +1,618 @@ +/* + * 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 com.openmemind.ai.memory.core.extraction.item.graph.pipeline.observation; + +import com.openmemind.ai.memory.core.data.MemoryId; +import com.openmemind.ai.memory.core.data.MemoryItem; +import com.openmemind.ai.memory.core.extraction.item.graph.ItemGraphMaterializationResult; +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; + +/** Observation contracts for DefaultItemGraphMaterializer. */ +public final class DefaultItemGraphMaterializerObservation { + + private DefaultItemGraphMaterializerObservation() {} + + public static Mono observe( + ObservationRegistry observationRegistry, + MemoryId memoryId, + List items, + Supplier> operation) { + return observe(observationRegistry, memoryId, items, ignored -> operation.get()); + } + + public static Mono observe( + ObservationRegistry observationRegistry, + MemoryId memoryId, + List items, + Function> + operation) { + return MemoryObservation.mono( + observationRegistry, + GraphMaterializeDocument.GRAPH_MATERIALIZE, + GraphMaterializeConvention.INSTANCE, + () -> new GraphMaterializeObservationContext(memoryId, items), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum GraphMaterializeDocument implements ObservationDocumentation { + GRAPH_MATERIALIZE; + + @Override + public String getName() { + return "memind.graph.materialize"; + } + + @Override + public Class> + getDefaultConvention() { + return GraphMaterializeConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }, + EXTRACTION_ITEM_COUNT { + @Override + public String asString() { + return "memind.extraction.item_count"; + } + }, + ENTITY_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.entity_count"; + } + }, + MENTION_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.mention_count"; + } + }, + STRUCTURED_LINK_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.structured_link_count"; + } + }, + TEMPORAL_SOURCE_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.temporal_source_count"; + } + }, + TEMPORAL_HISTORY_QUERY_BATCH_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.temporal_history_query_batch_count"; + } + }, + TEMPORAL_HISTORY_CANDIDATE_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.temporal_history_candidate_count"; + } + }, + TEMPORAL_INTRA_BATCH_CANDIDATE_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.temporal_intra_batch_candidate_count"; + } + }, + TEMPORAL_SELECTED_PAIR_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.temporal_selected_pair_count"; + } + }, + TEMPORAL_CREATED_LINK_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.temporal_created_link_count"; + } + }, + TEMPORAL_QUERY_DURATION_MS { + @Override + public String asString() { + return "memind.extraction.graph.temporal_query_duration_ms"; + } + }, + TEMPORAL_BUILD_DURATION_MS { + @Override + public String asString() { + return "memind.extraction.graph.temporal_build_duration_ms"; + } + }, + TEMPORAL_UPSERT_DURATION_MS { + @Override + public String asString() { + return "memind.extraction.graph.temporal_upsert_duration_ms"; + } + }, + TEMPORAL_BELOW_RETRIEVAL_FLOOR_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.temporal_below_retrieval_floor_count"; + } + }, + TEMPORAL_MIN_STRENGTH { + @Override + public String asString() { + return "memind.extraction.graph.temporal_min_strength"; + } + }, + TEMPORAL_MAX_STRENGTH { + @Override + public String asString() { + return "memind.extraction.graph.temporal_max_strength"; + } + }, + TEMPORAL_STRENGTH_BUCKET_SUMMARY { + @Override + public String asString() { + return "memind.extraction.graph.temporal_strength_bucket_summary"; + } + }, + TEMPORAL_DEGRADED { + @Override + public String asString() { + return "memind.extraction.graph.temporal_degraded"; + } + }, + RESOLUTION_CANDIDATE_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.resolution_candidate_count"; + } + }, + RESOLUTION_SOURCE_DISTRIBUTION { + @Override + public String asString() { + return "memind.extraction.graph.resolution_source_distribution"; + } + }, + RESOLUTION_SCORE_HISTOGRAM { + @Override + public String asString() { + return "memind.extraction.graph.resolution_score_histogram"; + } + }, + RESOLUTION_CANDIDATE_REJECTED_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.resolution_candidate_rejected_count"; + } + }, + RESOLUTION_MERGE_ACCEPTED_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.resolution_merge_accepted_count"; + } + }, + RESOLUTION_MERGE_REJECTED_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.resolution_merge_rejected_count"; + } + }, + RESOLUTION_CREATE_NEW_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.resolution_create_new_count"; + } + }, + RESOLUTION_EXACT_FALLBACK_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.resolution_exact_fallback_count"; + } + }, + RESOLUTION_CANDIDATE_CAP_HIT_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.resolution_candidate_cap_hit_count"; + } + }, + ALIAS_EVIDENCE_OBSERVED_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.alias_evidence_observed_count"; + } + }, + ALIAS_EVIDENCE_MERGED_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.alias_evidence_merged_count"; + } + }, + RESOLUTION_SPECIAL_BYPASS_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.resolution_special_bypass_count"; + } + }, + SEMANTIC_SEARCH_REQUEST_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.semantic_search_request_count"; + } + }, + SEMANTIC_SEARCH_INVOCATION_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.semantic_search_invocation_count"; + } + }, + SEMANTIC_SEARCH_HIT_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.semantic_search_hit_count"; + } + }, + SEMANTIC_RESOLVED_CANDIDATE_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.semantic_resolved_candidate_count"; + } + }, + SEMANTIC_LINK_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.semantic_link_count"; + } + }, + SEMANTIC_UPSERT_BATCH_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.semantic_upsert_batch_count"; + } + }, + SEMANTIC_SOURCE_WINDOW_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.semantic_source_window_count"; + } + }, + SEMANTIC_FAILED_RESOLVE_CHUNK_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.semantic_failed_resolve_chunk_count"; + } + }, + SEMANTIC_FAILED_WINDOW_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.semantic_failed_window_count"; + } + }, + SEMANTIC_FAILED_UPSERT_BATCH_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.semantic_failed_upsert_batch_count"; + } + }, + SEMANTIC_SAME_BATCH_HIT_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.semantic_same_batch_hit_count"; + } + }, + SEMANTIC_SEARCH_FALLBACK_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.semantic_search_fallback_count"; + } + }, + SEMANTIC_INTRA_BATCH_CANDIDATE_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.semantic_intra_batch_candidate_count"; + } + }, + SEMANTIC_SEARCH_PHASE_DURATION_MS { + @Override + public String asString() { + return "memind.extraction.graph.semantic_search_phase_duration_ms"; + } + }, + SEMANTIC_RESOLVE_PHASE_DURATION_MS { + @Override + public String asString() { + return "memind.extraction.graph.semantic_resolve_phase_duration_ms"; + } + }, + SEMANTIC_UPSERT_PHASE_DURATION_MS { + @Override + public String asString() { + return "memind.extraction.graph.semantic_upsert_phase_duration_ms"; + } + }, + SEMANTIC_INTRA_BATCH_PHASE_DURATION_MS { + @Override + public String asString() { + return "memind.extraction.graph.semantic_intra_batch_phase_duration_ms"; + } + }, + SEMANTIC_DEGRADED { + @Override + public String asString() { + return "memind.extraction.graph.semantic_degraded"; + } + }, + TYPE_FALLBACK_TO_OTHER_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.type_fallback_to_other_count"; + } + }, + TOP_UNRESOLVED_TYPE_LABELS { + @Override + public String asString() { + return "memind.extraction.graph.top_unresolved_type_labels"; + } + }, + DROPPED_BLANK_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.dropped_blank_count"; + } + }, + DROPPED_PUNCTUATION_ONLY_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.dropped_punctuation_only_count"; + } + }, + DROPPED_PRONOUN_LIKE_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.dropped_pronoun_like_count"; + } + }, + DROPPED_TEMPORAL_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.dropped_temporal_count"; + } + }, + DROPPED_DATE_LIKE_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.dropped_date_like_count"; + } + }, + DROPPED_RESERVED_SPECIAL_COLLISION_COUNT { + @Override + public String asString() { + return "memind.extraction.graph.dropped_reserved_special_collision_count"; + } + }, + STRUCTURED_BATCH_DEGRADED { + @Override + public String asString() { + return "memind.extraction.graph.structured_batch_degraded"; + } + }; + } + + public static final class GraphMaterializeObservationContext extends Observation.Context { + + private final MemoryId memoryId; + private final int itemCount; + + public GraphMaterializeObservationContext(MemoryId memoryId, List items) { + this.memoryId = memoryId; + this.itemCount = items == null ? 0 : items.size(); + } + + public void recordResult(ItemGraphMaterializationResult result) { + var stats = result.stats(); + add(HighCardinalityKeyNames.ENTITY_COUNT, stats.entityCount()); + add(HighCardinalityKeyNames.MENTION_COUNT, stats.mentionCount()); + add(HighCardinalityKeyNames.STRUCTURED_LINK_COUNT, stats.structuredItemLinkCount()); + add(HighCardinalityKeyNames.TEMPORAL_SOURCE_COUNT, stats.temporalSourceCount()); + add( + HighCardinalityKeyNames.TEMPORAL_HISTORY_QUERY_BATCH_COUNT, + stats.temporalHistoryQueryBatchCount()); + add( + HighCardinalityKeyNames.TEMPORAL_HISTORY_CANDIDATE_COUNT, + stats.temporalHistoryCandidateCount()); + add( + HighCardinalityKeyNames.TEMPORAL_INTRA_BATCH_CANDIDATE_COUNT, + stats.temporalIntraBatchCandidateCount()); + add( + HighCardinalityKeyNames.TEMPORAL_SELECTED_PAIR_COUNT, + stats.temporalSelectedPairCount()); + add( + HighCardinalityKeyNames.TEMPORAL_CREATED_LINK_COUNT, + stats.temporalCreatedLinkCount()); + add( + HighCardinalityKeyNames.TEMPORAL_QUERY_DURATION_MS, + stats.temporalQueryDurationMs()); + add( + HighCardinalityKeyNames.TEMPORAL_BUILD_DURATION_MS, + stats.temporalBuildDurationMs()); + add( + HighCardinalityKeyNames.TEMPORAL_UPSERT_DURATION_MS, + stats.temporalUpsertDurationMs()); + add( + HighCardinalityKeyNames.TEMPORAL_BELOW_RETRIEVAL_FLOOR_COUNT, + stats.temporalBelowRetrievalFloorCount()); + add(HighCardinalityKeyNames.TEMPORAL_MIN_STRENGTH, stats.temporalMinStrength()); + add(HighCardinalityKeyNames.TEMPORAL_MAX_STRENGTH, stats.temporalMaxStrength()); + add( + HighCardinalityKeyNames.TEMPORAL_STRENGTH_BUCKET_SUMMARY, + stats.temporalStrengthBucketSummary()); + add(HighCardinalityKeyNames.TEMPORAL_DEGRADED, stats.temporalDegraded()); + add( + HighCardinalityKeyNames.RESOLUTION_CANDIDATE_COUNT, + stats.resolutionCandidateCount()); + add( + HighCardinalityKeyNames.RESOLUTION_SOURCE_DISTRIBUTION, + stats.resolutionCandidateSourceSummary()); + add( + HighCardinalityKeyNames.RESOLUTION_SCORE_HISTOGRAM, + stats.resolutionMergeScoreHistogramSummary()); + add( + HighCardinalityKeyNames.RESOLUTION_CANDIDATE_REJECTED_COUNT, + stats.resolutionCandidateRejectedCount()); + add( + HighCardinalityKeyNames.RESOLUTION_MERGE_ACCEPTED_COUNT, + stats.resolutionMergeAcceptedCount()); + add( + HighCardinalityKeyNames.RESOLUTION_MERGE_REJECTED_COUNT, + stats.resolutionMergeRejectedCount()); + add( + HighCardinalityKeyNames.RESOLUTION_CREATE_NEW_COUNT, + stats.resolutionCreateNewCount()); + add( + HighCardinalityKeyNames.RESOLUTION_EXACT_FALLBACK_COUNT, + stats.resolutionExactFallbackCount()); + add( + HighCardinalityKeyNames.RESOLUTION_CANDIDATE_CAP_HIT_COUNT, + stats.resolutionCandidateCapHitCount()); + add( + HighCardinalityKeyNames.ALIAS_EVIDENCE_OBSERVED_COUNT, + stats.aliasEvidenceObservedCount()); + add( + HighCardinalityKeyNames.ALIAS_EVIDENCE_MERGED_COUNT, + stats.aliasEvidenceMergedCount()); + add( + HighCardinalityKeyNames.RESOLUTION_SPECIAL_BYPASS_COUNT, + stats.resolutionSpecialBypassCount()); + add( + HighCardinalityKeyNames.SEMANTIC_SEARCH_REQUEST_COUNT, + stats.semanticSearchRequestCount()); + add( + HighCardinalityKeyNames.SEMANTIC_SEARCH_INVOCATION_COUNT, + stats.semanticSearchInvocationCount()); + add(HighCardinalityKeyNames.SEMANTIC_SEARCH_HIT_COUNT, stats.semanticSearchHitCount()); + add( + HighCardinalityKeyNames.SEMANTIC_RESOLVED_CANDIDATE_COUNT, + stats.semanticResolvedCandidateCount()); + add(HighCardinalityKeyNames.SEMANTIC_LINK_COUNT, stats.semanticLinkCount()); + add( + HighCardinalityKeyNames.SEMANTIC_UPSERT_BATCH_COUNT, + stats.semanticUpsertBatchCount()); + add( + HighCardinalityKeyNames.SEMANTIC_SOURCE_WINDOW_COUNT, + stats.semanticSourceWindowCount()); + add( + HighCardinalityKeyNames.SEMANTIC_FAILED_RESOLVE_CHUNK_COUNT, + stats.semanticFailedResolveChunkCount()); + add( + HighCardinalityKeyNames.SEMANTIC_FAILED_WINDOW_COUNT, + stats.semanticFailedWindowCount()); + add( + HighCardinalityKeyNames.SEMANTIC_FAILED_UPSERT_BATCH_COUNT, + stats.semanticFailedUpsertBatchCount()); + add( + HighCardinalityKeyNames.SEMANTIC_SAME_BATCH_HIT_COUNT, + stats.semanticSameBatchHitCount()); + add( + HighCardinalityKeyNames.SEMANTIC_SEARCH_FALLBACK_COUNT, + stats.semanticSearchFallbackCount()); + add( + HighCardinalityKeyNames.SEMANTIC_INTRA_BATCH_CANDIDATE_COUNT, + stats.semanticIntraBatchCandidateCount()); + add( + HighCardinalityKeyNames.SEMANTIC_SEARCH_PHASE_DURATION_MS, + stats.semanticSearchPhaseDurationMs()); + add( + HighCardinalityKeyNames.SEMANTIC_RESOLVE_PHASE_DURATION_MS, + stats.semanticResolvePhaseDurationMs()); + add( + HighCardinalityKeyNames.SEMANTIC_UPSERT_PHASE_DURATION_MS, + stats.semanticUpsertPhaseDurationMs()); + add( + HighCardinalityKeyNames.SEMANTIC_INTRA_BATCH_PHASE_DURATION_MS, + stats.semanticIntraBatchPhaseDurationMs()); + add(HighCardinalityKeyNames.SEMANTIC_DEGRADED, stats.semanticDegraded()); + add( + HighCardinalityKeyNames.TYPE_FALLBACK_TO_OTHER_COUNT, + stats.typeFallbackToOtherCount()); + add( + HighCardinalityKeyNames.TOP_UNRESOLVED_TYPE_LABELS, + stats.topUnresolvedTypeLabelsSummary()); + add(HighCardinalityKeyNames.DROPPED_BLANK_COUNT, stats.droppedBlankCount()); + add( + HighCardinalityKeyNames.DROPPED_PUNCTUATION_ONLY_COUNT, + stats.droppedPunctuationOnlyCount()); + add( + HighCardinalityKeyNames.DROPPED_PRONOUN_LIKE_COUNT, + stats.droppedPronounLikeCount()); + add(HighCardinalityKeyNames.DROPPED_TEMPORAL_COUNT, stats.droppedTemporalCount()); + add(HighCardinalityKeyNames.DROPPED_DATE_LIKE_COUNT, stats.droppedDateLikeCount()); + add( + HighCardinalityKeyNames.DROPPED_RESERVED_SPECIAL_COLLISION_COUNT, + stats.droppedReservedSpecialCollisionCount()); + add(HighCardinalityKeyNames.STRUCTURED_BATCH_DEGRADED, stats.structuredBatchDegraded()); + } + + private void add(HighCardinalityKeyNames key, Object value) { + if (value != null) { + addHighCardinalityKeyValue(key.withValue(String.valueOf(value))); + } + } + } + + public static final class GraphMaterializeConvention + implements ObservationConvention { + + public static final GraphMaterializeConvention INSTANCE = new GraphMaterializeConvention(); + + @Override + public String getName() { + return GraphMaterializeDocument.GRAPH_MATERIALIZE.getName(); + } + + @Override + public String getContextualName(GraphMaterializeObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(GraphMaterializeObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue(context.memoryId.toIdentifier()), + HighCardinalityKeyNames.EXTRACTION_ITEM_COUNT.withValue( + String.valueOf(context.itemCount))); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof GraphMaterializeObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/observation/MemoryItemLayerObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/observation/MemoryItemLayerObservation.java new file mode 100644 index 00000000..e6dd179a --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/item/observation/MemoryItemLayerObservation.java @@ -0,0 +1,151 @@ +/* + * 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 com.openmemind.ai.memory.core.extraction.item.observation; + +import com.openmemind.ai.memory.core.data.MemoryId; +import com.openmemind.ai.memory.core.extraction.result.MemoryItemResult; +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; + +/** Observation contracts for MemoryItemLayer. */ +public final class MemoryItemLayerObservation { + + private MemoryItemLayerObservation() {} + + public static Mono observe( + ObservationRegistry observationRegistry, + MemoryId memoryId, + Supplier> operation) { + return observe(observationRegistry, memoryId, ignored -> operation.get()); + } + + public static Mono observe( + ObservationRegistry observationRegistry, + MemoryId memoryId, + Function> operation) { + return MemoryObservation.mono( + observationRegistry, + MemoryItemDocument.EXTRACT, + MemoryItemConvention.INSTANCE, + () -> new MemoryItemObservationContext(memoryId), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum MemoryItemDocument implements ObservationDocumentation { + EXTRACT; + + @Override + public String getName() { + return "memind.extraction.item"; + } + + @Override + public Class> + getDefaultConvention() { + return MemoryItemConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }, + ITEM_COUNT { + @Override + public String asString() { + return "memind.extraction.item_count"; + } + }, + NEW_ITEM_COUNT { + @Override + public String asString() { + return "memind.extraction.new_item_count"; + } + }; + } + + public static final class MemoryItemObservationContext extends Observation.Context { + + private final MemoryId memoryId; + private Integer itemCount; + private Integer newItemCount; + + public MemoryItemObservationContext(MemoryId memoryId) { + this.memoryId = memoryId; + } + + public void recordResult(MemoryItemResult result) { + this.itemCount = result.newCount(); + this.newItemCount = result.newItems().size(); + } + } + + public static final class MemoryItemConvention + implements ObservationConvention { + + public static final MemoryItemConvention INSTANCE = new MemoryItemConvention(); + + @Override + public String getName() { + return MemoryItemDocument.EXTRACT.getName(); + } + + @Override + public String getContextualName(MemoryItemObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(MemoryItemObservationContext context) { + var values = + KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue( + context.memoryId.toIdentifier())); + if (context.itemCount != null) { + values = + values.and( + HighCardinalityKeyNames.ITEM_COUNT.withValue( + String.valueOf(context.itemCount))); + } + if (context.newItemCount != null) { + values = + values.and( + HighCardinalityKeyNames.NEW_ITEM_COUNT.withValue( + String.valueOf(context.newItemCount))); + } + return values; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof MemoryItemObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/observation/DefaultMemoryExtractorObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/observation/DefaultMemoryExtractorObservation.java new file mode 100644 index 00000000..4e91f8c9 --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/observation/DefaultMemoryExtractorObservation.java @@ -0,0 +1,224 @@ +/* + * 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 com.openmemind.ai.memory.core.extraction.observation; + +import com.openmemind.ai.memory.core.data.MemoryId; +import com.openmemind.ai.memory.core.extraction.ExtractionResult; +import com.openmemind.ai.memory.core.extraction.ExtractionStatus; +import com.openmemind.ai.memory.core.extraction.item.graph.ItemGraphMaterializationResult; +import com.openmemind.ai.memory.core.extraction.result.MemoryItemResult; +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; + +/** Observation contracts for DefaultMemoryExtractor. */ +public final class DefaultMemoryExtractorObservation { + + private DefaultMemoryExtractorObservation() {} + + public static Mono observe( + ObservationRegistry observationRegistry, + MemoryId memoryId, + Supplier> operation) { + return observe(observationRegistry, memoryId, ignored -> operation.get()); + } + + public static Mono observe( + ObservationRegistry observationRegistry, + MemoryId memoryId, + Function> operation) { + return MemoryObservation.mono( + observationRegistry, + ExtractionDocument.EXTRACTION, + ExtractionObservationConvention.INSTANCE, + () -> new ExtractionObservationContext(memoryId), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum ExtractionDocument implements ObservationDocumentation { + EXTRACTION; + + @Override + public String getName() { + return "memind.extraction"; + } + + @Override + public Class> + getDefaultConvention() { + return ExtractionObservationConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }; + } + + public static final class ExtractionObservationContext extends MemoryObservationContext { + + private final MemoryId memoryId; + private boolean hasResult; + private String resultStatus = "unknown"; + private int rawDataCount; + private Integer segmentCount; + private int newItemCount; + private Integer reinforcedItemCount; + private int insightCount; + private Integer graphEntityCount; + private Integer graphMentionCount; + private Integer graphRelationCount; + private String source = "core"; + + public ExtractionObservationContext(MemoryId memoryId) { + this.memoryId = memoryId; + } + + public void recordResult(ExtractionResult result) { + hasResult = true; + if (result == null) { + return; + } + resultStatus = status(result.status()); + rawDataCount = + result.rawDataResult() == null || result.rawDataResult().rawDataList() == null + ? 0 + : result.rawDataResult().rawDataList().size(); + segmentCount = + result.rawDataResult() == null || result.rawDataResult().segments() == null + ? null + : result.rawDataResult().segments().size(); + + MemoryItemResult itemResult = result.memoryItemResult(); + newItemCount = itemResult == null ? 0 : itemResult.newCount(); + insightCount = result.totalInsights(); + + ItemGraphMaterializationResult graph = + itemResult == null ? null : itemResult.graphMaterializationResult(); + ItemGraphMaterializationResult.Stats stats = graph == null ? null : graph.stats(); + graphEntityCount = stats == null ? null : stats.entityCount(); + graphMentionCount = stats == null ? null : stats.mentionCount(); + graphRelationCount = finalRelationCount(stats); + } + + public boolean hasResult() { + return hasResult; + } + + @Override + public String status() { + String terminalStatus = errorOrCancellationStatus(); + if (terminalStatus != null) { + return terminalStatus; + } + return resultStatus; + } + + public int rawDataCount() { + return rawDataCount; + } + + public Integer segmentCount() { + return segmentCount; + } + + public int newItemCount() { + return newItemCount; + } + + public Integer reinforcedItemCount() { + return reinforcedItemCount; + } + + public int insightCount() { + return insightCount; + } + + public Integer graphEntityCount() { + return graphEntityCount; + } + + public Integer graphMentionCount() { + return graphMentionCount; + } + + public Integer graphRelationCount() { + return graphRelationCount; + } + + public String source() { + return source; + } + + private static Integer finalRelationCount(ItemGraphMaterializationResult.Stats stats) { + if (stats == null || stats.finalRelationStats() == null) { + return null; + } + var finalStats = stats.finalRelationStats(); + return finalStats.semanticRelationCount() + + finalStats.temporalRelationCount() + + finalStats.causalRelationCount() + + finalStats.itemLinkCount(); + } + + private static String status(ExtractionStatus status) { + return status == null ? "unknown" : status.name().toLowerCase(); + } + } + + public static final class ExtractionObservationConvention + implements ObservationConvention { + + public static final ExtractionObservationConvention INSTANCE = + new ExtractionObservationConvention(); + + @Override + public String getName() { + return ExtractionDocument.EXTRACTION.getName(); + } + + @Override + public String getContextualName(ExtractionObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(ExtractionObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue(context.memoryId.toIdentifier())); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof ExtractionObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/rawdata/RawDataLayer.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/rawdata/RawDataLayer.java index f2cc68c5..2651131b 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/rawdata/RawDataLayer.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/rawdata/RawDataLayer.java @@ -19,6 +19,7 @@ import com.openmemind.ai.memory.core.extraction.rawdata.caption.CaptionGenerator; import com.openmemind.ai.memory.core.extraction.rawdata.content.ConversationContent; import com.openmemind.ai.memory.core.extraction.rawdata.content.RawContent; +import com.openmemind.ai.memory.core.extraction.rawdata.observation.RawDataLayerObservation; import com.openmemind.ai.memory.core.extraction.rawdata.segment.CharBoundary; import com.openmemind.ai.memory.core.extraction.rawdata.segment.MessageBoundary; import com.openmemind.ai.memory.core.extraction.rawdata.segment.Segment; @@ -29,6 +30,7 @@ import com.openmemind.ai.memory.core.store.MemoryStore; import com.openmemind.ai.memory.core.utils.HashUtils; import com.openmemind.ai.memory.core.vector.MemoryVector; +import io.micrometer.observation.ObservationRegistry; import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; @@ -54,6 +56,7 @@ public class RawDataLayer implements RawDataExtractStep, SegmentProcessor { private final MemoryStore memoryStore; private final MemoryVector vector; private final int vectorBatchSize; + private final ObservationRegistry observationRegistry; /** * Creates a RawDataLayer with a list of content processors. @@ -83,12 +86,29 @@ public RawDataLayer( MemoryStore memoryStore, MemoryVector vector, int vectorBatchSize) { + this( + processorList, + defaultCaptionGenerator, + memoryStore, + vector, + vectorBatchSize, + ObservationRegistry.NOOP); + } + + public RawDataLayer( + List> processorList, + CaptionGenerator defaultCaptionGenerator, + MemoryStore memoryStore, + MemoryVector vector, + int vectorBatchSize, + ObservationRegistry observationRegistry) { this( new RawContentProcessorRegistry(processorList), defaultCaptionGenerator, memoryStore, vector, - vectorBatchSize); + vectorBatchSize, + observationRegistry); } public RawDataLayer( @@ -97,6 +117,22 @@ public RawDataLayer( MemoryStore memoryStore, MemoryVector vector, int vectorBatchSize) { + this( + processorRegistry, + defaultCaptionGenerator, + memoryStore, + vector, + vectorBatchSize, + ObservationRegistry.NOOP); + } + + public RawDataLayer( + RawContentProcessorRegistry processorRegistry, + CaptionGenerator defaultCaptionGenerator, + MemoryStore memoryStore, + MemoryVector vector, + int vectorBatchSize, + ObservationRegistry observationRegistry) { this.processorRegistry = java.util.Objects.requireNonNull(processorRegistry, "processorRegistry"); this.defaultCaptionGenerator = defaultCaptionGenerator; @@ -106,6 +142,8 @@ public RawDataLayer( throw new IllegalArgumentException("vectorBatchSize must be > 0"); } this.vectorBatchSize = vectorBatchSize; + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } @Override @@ -124,6 +162,18 @@ public Mono extract( String contentType, Map metadata, String language) { + return RawDataLayerObservation.observe( + observationRegistry, + memoryId, + () -> extractInternal(memoryId, content, contentType, metadata, language)); + } + + private Mono extractInternal( + MemoryId memoryId, + RawContent content, + String contentType, + Map metadata, + String language) { RawDataInput input = new RawDataInput(memoryId, content, contentType, metadata); return process(input, language) .map( diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/rawdata/observation/RawDataLayerObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/rawdata/observation/RawDataLayerObservation.java new file mode 100644 index 00000000..acfd50d5 --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/extraction/rawdata/observation/RawDataLayerObservation.java @@ -0,0 +1,137 @@ +/* + * 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 com.openmemind.ai.memory.core.extraction.rawdata.observation; + +import com.openmemind.ai.memory.core.data.MemoryId; +import com.openmemind.ai.memory.core.extraction.result.RawDataResult; +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; + +/** Observation contracts for RawDataLayer. */ +public final class RawDataLayerObservation { + + private RawDataLayerObservation() {} + + public static Mono observe( + ObservationRegistry observationRegistry, + MemoryId memoryId, + Supplier> operation) { + return observe(observationRegistry, memoryId, ignored -> operation.get()); + } + + public static Mono observe( + ObservationRegistry observationRegistry, + MemoryId memoryId, + Function> operation) { + return MemoryObservation.mono( + observationRegistry, + RawDataDocument.EXTRACT, + RawDataConvention.INSTANCE, + () -> new RawDataObservationContext(memoryId), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum RawDataDocument implements ObservationDocumentation { + EXTRACT; + + @Override + public String getName() { + return "memind.extraction.rawdata"; + } + + @Override + public Class> + getDefaultConvention() { + return RawDataConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }, + SEGMENT_COUNT { + @Override + public String asString() { + return "memind.extraction.segment_count"; + } + }; + } + + public static final class RawDataObservationContext extends Observation.Context { + + private final MemoryId memoryId; + private Integer segmentCount; + + public RawDataObservationContext(MemoryId memoryId) { + this.memoryId = memoryId; + } + + public void recordResult(RawDataResult result) { + this.segmentCount = result.segments().size(); + } + } + + public static final class RawDataConvention + implements ObservationConvention { + + public static final RawDataConvention INSTANCE = new RawDataConvention(); + + @Override + public String getName() { + return RawDataDocument.EXTRACT.getName(); + } + + @Override + public String getContextualName(RawDataObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(RawDataObservationContext context) { + var values = + KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue( + context.memoryId.toIdentifier())); + if (context.segmentCount != null) { + values = + values.and( + HighCardinalityKeyNames.SEGMENT_COUNT.withValue( + String.valueOf(context.segmentCount))); + } + return values; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof RawDataObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/llm/rerank/LlmReranker.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/llm/rerank/LlmReranker.java index 1b77851c..fb6deda6 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/llm/rerank/LlmReranker.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/llm/rerank/LlmReranker.java @@ -15,8 +15,10 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import com.openmemind.ai.memory.core.llm.rerank.observation.LlmRerankerObservation; import com.openmemind.ai.memory.core.retrieval.RetrievalConfig.RerankConfig; import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; +import io.micrometer.observation.ObservationRegistry; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -49,12 +51,22 @@ public class LlmReranker implements Reranker { private final String rerankUrl; private final String apiKey; private final String model; + private final ObservationRegistry observationRegistry; public LlmReranker(String baseUrl, String apiKey) { this(baseUrl, apiKey, DEFAULT_MODEL); } + public LlmReranker(String baseUrl, String apiKey, ObservationRegistry observationRegistry) { + this(baseUrl, apiKey, DEFAULT_MODEL, observationRegistry); + } + public LlmReranker(String baseUrl, String apiKey, String model) { + this(baseUrl, apiKey, model, ObservationRegistry.NOOP); + } + + public LlmReranker( + String baseUrl, String apiKey, String model, ObservationRegistry observationRegistry) { Objects.requireNonNull(baseUrl, "baseUrl must not be null"); Objects.requireNonNull(apiKey, "apiKey must not be null"); this.rerankUrl = baseUrl.replaceAll("/+$", "") + "/v1/rerank"; @@ -62,10 +74,32 @@ public LlmReranker(String baseUrl, String apiKey, String model) { this.model = Objects.requireNonNull(model, "model must not be null"); this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); this.objectMapper = new ObjectMapper(); + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } @Override public Mono> rerank(String query, List results, int topK) { + return LlmRerankerObservation.observe( + observationRegistry, + query, + results, + topK, + context -> + rerankInternal(query, results, topK) + .onErrorResume( + e -> { + context.markDegraded(); + log.warn( + "Rerank failed after 3 retries, returning" + + " original sorted topK slice", + e); + return Mono.just(fallback(results, topK)); + })); + } + + private Mono> rerankInternal( + String query, List results, int topK) { if (results.isEmpty()) { return Mono.just(results); } @@ -73,15 +107,7 @@ public Mono> rerank(String query, List results, return Mono.fromCallable(() -> doRerank(query, results, topK)) .subscribeOn(Schedulers.boundedElastic()) .retryWhen( - Retry.backoff(3, Duration.ofSeconds(2)).maxBackoff(Duration.ofSeconds(10))) - .onErrorResume( - e -> { - log.warn( - "Rerank failed after 3 retries, returning original sorted topK" - + " slice", - e); - return Mono.just(fallback(results, topK)); - }); + Retry.backoff(3, Duration.ofSeconds(2)).maxBackoff(Duration.ofSeconds(10))); } private List doRerank(String query, List results, int topK) @@ -192,6 +218,27 @@ public static double blendScore( */ public Mono> rerank( String query, List results, RerankConfig rerankConfig) { + return LlmRerankerObservation.observe( + observationRegistry, + query, + results, + rerankConfig.topK(), + context -> + rerankInternal(query, results, rerankConfig) + .onErrorResume( + e -> { + context.markDegraded(); + log.warn( + "Rerank failed after 3 retries, returning" + + " original sorted topK slice", + e); + return Mono.just( + fallback(results, rerankConfig.topK())); + })); + } + + private Mono> rerankInternal( + String query, List results, RerankConfig rerankConfig) { if (results.isEmpty() || !rerankConfig.enabled()) { return Mono.just(results); } @@ -199,15 +246,7 @@ public Mono> rerank( return Mono.fromCallable(() -> doRerank(query, results, rerankConfig)) .subscribeOn(Schedulers.boundedElastic()) .retryWhen( - Retry.backoff(3, Duration.ofSeconds(2)).maxBackoff(Duration.ofSeconds(10))) - .onErrorResume( - e -> { - log.warn( - "Rerank failed after 3 retries, returning original sorted topK" - + " slice", - e); - return Mono.just(fallback(results, rerankConfig.topK())); - }); + Retry.backoff(3, Duration.ofSeconds(2)).maxBackoff(Duration.ofSeconds(10))); } private List doRerank( diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/llm/rerank/observation/LlmRerankerObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/llm/rerank/observation/LlmRerankerObservation.java new file mode 100644 index 00000000..b097ab24 --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/llm/rerank/observation/LlmRerankerObservation.java @@ -0,0 +1,253 @@ +/* + * 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 com.openmemind.ai.memory.core.llm.rerank.observation; + +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; +import com.openmemind.ai.memory.core.retrieval.trace.ObservationTiming; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEvent; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEventSource; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** Observation contracts for LlmReranker. */ +public final class LlmRerankerObservation { + + private LlmRerankerObservation() {} + + public static Mono> observe( + ObservationRegistry observationRegistry, + String query, + List results, + int topK, + Supplier>> operation) { + return observe(observationRegistry, query, results, topK, ignored -> operation.get()); + } + + public static Mono> observe( + ObservationRegistry observationRegistry, + String query, + List results, + int topK, + Function>> operation) { + return MemoryObservation.mono( + observationRegistry, + RerankDocument.RERANK, + RerankConvention.INSTANCE, + reactorContext -> + new RerankObservationContext(query, results, topK, reactorContext), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum RerankDocument implements ObservationDocumentation { + RERANK; + + @Override + public String getName() { + return "memind.retrieval.rerank"; + } + + @Override + public Class> + getDefaultConvention() { + return RerankConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + QUERY { + @Override + public String asString() { + return "memind.retrieval.query"; + } + }, + CANDIDATES { + @Override + public String asString() { + return "memind.retrieval.rerank.candidates"; + } + }, + TOP_K { + @Override + public String asString() { + return "memind.retrieval.top_k"; + } + }, + RESULT_COUNT { + @Override + public String asString() { + return "memind.retrieval.result_count"; + } + }; + } + + public static final class RerankObservationContext extends MemoryObservationContext + implements RetrievalTraceEventSource { + + private final String query; + private final int candidateCount; + private final int topK; + private List results = List.of(); + private boolean degraded; + + public RerankObservationContext(String query, List results, int topK) { + this(query, results, topK, null); + } + + public RerankObservationContext( + String query, List results, int topK, ContextView reactorContext) { + super(reactorContext); + this.query = query; + this.candidateCount = results == null ? 0 : results.size(); + this.topK = topK; + } + + public void recordResult(List results) { + this.results = results == null ? List.of() : List.copyOf(results); + addHighCardinalityKeyValue( + HighCardinalityKeyNames.RESULT_COUNT.withValue(String.valueOf(resultCount()))); + } + + public int inputCount() { + return candidateCount; + } + + public int candidateCount() { + return candidateCount; + } + + public int resultCount() { + return results.size(); + } + + public void markDegraded() { + degraded = true; + } + + public boolean degraded() { + return degraded; + } + + public boolean skipped() { + return topK <= 0 || candidateCount == 0; + } + + public String stage() { + return "rerank"; + } + + public String tier() { + return "item"; + } + + public String method() { + return "rerank"; + } + + public String source() { + return "core"; + } + + @Override + public String status() { + String terminalStatus = errorOrCancellationStatus(); + return terminalStatus == null && degraded ? "degraded" : super.status(); + } + + @Override + public Optional toRetrievalTraceEvent( + ObservationTiming timing, RetrievalTraceOptions options) { + return Optional.of( + new RetrievalTraceEvent( + RerankDocument.RERANK.getName(), + RerankDocument.RERANK.getName(), + status(), + timing.startedAt(), + timing.completedAt(), + timing.durationMillis(), + Map.of("operation", "retrieval", "stage", stage()), + Map.of( + HighCardinalityKeyNames.CANDIDATES.asString(), + String.valueOf(candidateCount), + HighCardinalityKeyNames.RESULT_COUNT.asString(), + String.valueOf(resultCount())), + new RetrievalTraceEvent.StagePayload( + stage(), + tier(), + method(), + inputCount(), + candidateCount(), + resultCount(), + degraded(), + skipped(), + Map.of("topK", topK), + RetrievalTraceEvent.candidates( + results, + options.maxCandidatesPerStage(), + options.maxTextLength())))); + } + } + + public static final class RerankConvention + implements ObservationConvention { + + public static final RerankConvention INSTANCE = new RerankConvention(); + + @Override + public String getName() { + return RerankDocument.RERANK.getName(); + } + + @Override + public String getContextualName(RerankObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(RerankObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.QUERY.withValue(safe(context.query)), + HighCardinalityKeyNames.CANDIDATES.withValue( + String.valueOf(context.candidateCount)), + HighCardinalityKeyNames.TOP_K.withValue(String.valueOf(context.topK))); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof RerankObservationContext; + } + } + + private static String safe(String value) { + return value == null ? "" : value; + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/ExtractionMetrics.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/ExtractionMetrics.java deleted file mode 100644 index 1fecbda8..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/ExtractionMetrics.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.metrics; - -public record ExtractionMetrics( - String status, - int rawDataCount, - Integer segmentCount, - int newItemCount, - Integer reinforcedItemCount, - int insightCount, - Integer graphEntityCount, - Integer graphMentionCount, - Integer graphRelationCount, - String source) {} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/ExtractionMetricsExtractor.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/ExtractionMetricsExtractor.java deleted file mode 100644 index e3b48367..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/ExtractionMetricsExtractor.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.metrics; - -import com.openmemind.ai.memory.core.extraction.ExtractionResult; -import com.openmemind.ai.memory.core.extraction.ExtractionStatus; -import com.openmemind.ai.memory.core.extraction.item.graph.ItemGraphMaterializationResult; -import com.openmemind.ai.memory.core.extraction.result.MemoryItemResult; - -public final class ExtractionMetricsExtractor { - - private ExtractionMetricsExtractor() {} - - public static ExtractionMetrics extract(ExtractionResult result, String source) { - if (result == null) { - return new ExtractionMetrics( - "unknown", 0, null, 0, null, 0, null, null, null, sourceOrCore(source)); - } - MemoryItemResult itemResult = result.memoryItemResult(); - ItemGraphMaterializationResult graph = - itemResult == null ? null : itemResult.graphMaterializationResult(); - ItemGraphMaterializationResult.Stats stats = graph == null ? null : graph.stats(); - return new ExtractionMetrics( - status(result.status()), - result.rawDataResult() == null ? 0 : result.rawDataResult().rawDataList().size(), - result.rawDataResult() == null ? null : result.rawDataResult().segments().size(), - itemResult == null ? 0 : itemResult.newCount(), - null, - result.totalInsights(), - stats == null ? null : stats.entityCount(), - stats == null ? null : stats.mentionCount(), - finalRelationCount(stats), - sourceOrCore(source)); - } - - private static Integer finalRelationCount(ItemGraphMaterializationResult.Stats stats) { - if (stats == null || stats.finalRelationStats() == null) { - return null; - } - var finalStats = stats.finalRelationStats(); - return finalStats.semanticRelationCount() - + finalStats.temporalRelationCount() - + finalStats.causalRelationCount() - + finalStats.itemLinkCount(); - } - - private static String status(ExtractionStatus status) { - return status == null ? "unknown" : status.name().toLowerCase(); - } - - private static String sourceOrCore(String source) { - return source == null || source.isBlank() ? "core" : source; - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/MemoryMetricsRecorder.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/MemoryMetricsRecorder.java deleted file mode 100644 index d3f76b5c..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/MemoryMetricsRecorder.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.metrics; - -public interface MemoryMetricsRecorder { - - void recordExtractionSummary(ExtractionMetrics metrics); - - void recordRetrievalStage(RetrievalStageMetrics metrics); - - void recordRetrievalMerge(RetrievalMergeMetrics metrics); - - void recordRetrievalSummary(RetrievalSummaryMetrics metrics); -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/NoopMemoryMetricsRecorder.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/NoopMemoryMetricsRecorder.java deleted file mode 100644 index ee15433b..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/NoopMemoryMetricsRecorder.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.metrics; - -public enum NoopMemoryMetricsRecorder implements MemoryMetricsRecorder { - INSTANCE; - - @Override - public void recordExtractionSummary(ExtractionMetrics metrics) {} - - @Override - public void recordRetrievalStage(RetrievalStageMetrics metrics) {} - - @Override - public void recordRetrievalMerge(RetrievalMergeMetrics metrics) {} - - @Override - public void recordRetrievalSummary(RetrievalSummaryMetrics metrics) {} -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/RetrievalMetricsSupport.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/RetrievalMetricsSupport.java deleted file mode 100644 index 4bc8382c..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/RetrievalMetricsSupport.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.metrics; - -import com.openmemind.ai.memory.core.retrieval.RetrievalResult; - -public final class RetrievalMetricsSupport { - - private RetrievalMetricsSupport() {} - - public static RetrievalSummaryMetrics summary( - String strategy, RetrievalResult result, String source) { - String status = result == null ? "empty" : result.status().name().toLowerCase(); - return new RetrievalSummaryMetrics( - strategyOrUnknown(strategy), - status, - result == null || result.items() == null ? 0 : result.items().size(), - result == null || result.insights() == null ? 0 : result.insights().size(), - result == null || result.rawData() == null ? 0 : result.rawData().size(), - result == null || result.evidences() == null ? 0 : result.evidences().size(), - sourceOrCore(source)); - } - - public static int deduplicatedCount(int inputCount, int outputCount) { - return Math.max(0, inputCount - outputCount); - } - - public static void safeRecord(Runnable recorder) { - try { - recorder.run(); - } catch (RuntimeException ignored) { - // Metrics collection must never affect memory operations. - } - } - - public static String strategyOrUnknown(String strategy) { - return strategy == null || strategy.isBlank() ? "unknown" : strategy; - } - - public static String sourceOrCore(String source) { - return source == null || source.isBlank() ? "core" : source; - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/RetrievalStageMetrics.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/RetrievalStageMetrics.java deleted file mode 100644 index d68ee4f0..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/RetrievalStageMetrics.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.metrics; - -public record RetrievalStageMetrics( - String strategy, - String stage, - String tier, - String method, - String status, - Integer inputCount, - Integer candidateCount, - Integer resultCount, - boolean degraded, - boolean skipped, - String source) {} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/RetrievalSummaryMetrics.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/RetrievalSummaryMetrics.java deleted file mode 100644 index 70e9e596..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/RetrievalSummaryMetrics.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.metrics; - -public record RetrievalSummaryMetrics( - String strategy, - String status, - int itemCount, - int insightCount, - int rawDataCount, - int evidenceCount, - String source) { - - public int countFor(RetrievalResultType type) { - return switch (type) { - case ITEM -> itemCount; - case INSIGHT -> insightCount; - case RAW_DATA -> rawDataCount; - case EVIDENCE -> evidenceCount; - }; - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/observation/MemoryObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/observation/MemoryObservation.java new file mode 100644 index 00000000..7291a936 --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/observation/MemoryObservation.java @@ -0,0 +1,138 @@ +/* + * 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 com.openmemind.ai.memory.core.observation; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.observability.micrometer.Micrometer; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** Shared Reactor + Micrometer Observation template for memory components. */ +public final class MemoryObservation { + + private MemoryObservation() {} + + public static Mono mono( + ObservationRegistry observationRegistry, + ObservationDocumentation document, + ObservationConvention convention, + Supplier contextFactory, + Function> operation) { + Objects.requireNonNull(contextFactory, "contextFactory"); + return mono( + observationRegistry, + document, + convention, + ignored -> contextFactory.get(), + operation); + } + + public static Mono mono( + ObservationRegistry observationRegistry, + ObservationDocumentation document, + ObservationConvention convention, + Function contextFactory, + Function> operation) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(convention, "convention"); + Objects.requireNonNull(contextFactory, "contextFactory"); + Objects.requireNonNull(operation, "operation"); + ObservationRegistry registry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; + return Mono.deferContextual( + reactorContext -> { + ContextT context = contextFactory.apply(reactorContext); + Mono publisher; + try { + publisher = + Objects.requireNonNull( + operation.apply(context), "operation returned null"); + } catch (Throwable error) { + publisher = Mono.error(error); + } + return publisher + .doOnError(context::setError) + .name(document.getName()) + .tap( + Micrometer.observation( + registry, + actualRegistry -> + document.observation( + null, + convention, + () -> context, + actualRegistry))); + }); + } + + public static Flux flux( + ObservationRegistry observationRegistry, + ObservationDocumentation document, + ObservationConvention convention, + Supplier contextFactory, + Function> operation) { + Objects.requireNonNull(contextFactory, "contextFactory"); + return flux( + observationRegistry, + document, + convention, + ignored -> contextFactory.get(), + operation); + } + + public static Flux flux( + ObservationRegistry observationRegistry, + ObservationDocumentation document, + ObservationConvention convention, + Function contextFactory, + Function> operation) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(convention, "convention"); + Objects.requireNonNull(contextFactory, "contextFactory"); + Objects.requireNonNull(operation, "operation"); + ObservationRegistry registry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; + return Flux.deferContextual( + reactorContext -> { + ContextT context = contextFactory.apply(reactorContext); + Flux publisher; + try { + publisher = + Objects.requireNonNull( + operation.apply(context), "operation returned null"); + } catch (Throwable error) { + publisher = Flux.error(error); + } + return publisher + .doOnError(context::setError) + .name(document.getName()) + .tap( + Micrometer.observation( + registry, + actualRegistry -> + document.observation( + null, + convention, + () -> context, + actualRegistry))); + }); + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/observation/MemoryObservationContext.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/observation/MemoryObservationContext.java new file mode 100644 index 00000000..b714e82a --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/observation/MemoryObservationContext.java @@ -0,0 +1,55 @@ +/* + * 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 com.openmemind.ai.memory.core.observation; + +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceRecorder; +import io.micrometer.observation.Observation; +import java.util.Optional; +import reactor.util.context.ContextView; + +/** Base observation context with request-scoped memory observability state. */ +public abstract class MemoryObservationContext extends Observation.Context { + + private static final String REACTOR_STATUS = "reactor.status"; + private static final String CANCELLED = "cancelled"; + + protected MemoryObservationContext() {} + + protected MemoryObservationContext(ContextView reactorContext) { + if (reactorContext != null) { + reactorContext + .getOrEmpty(RetrievalTraceRecorder.class) + .ifPresent(recorder -> put(RetrievalTraceRecorder.class, recorder)); + } + } + + public Optional traceRecorder() { + return Optional.ofNullable(get(RetrievalTraceRecorder.class)); + } + + public String status() { + String terminalStatus = errorOrCancellationStatus(); + return terminalStatus == null ? "success" : terminalStatus; + } + + protected String errorOrCancellationStatus() { + if (getError() != null) { + return "error"; + } + var reactorStatus = getLowCardinalityKeyValue(REACTOR_STATUS); + return reactorStatus != null && CANCELLED.equals(reactorStatus.getValue()) + ? CANCELLED + : null; + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/DefaultMemoryRetriever.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/DefaultMemoryRetriever.java index 0c08db8a..f2d2bca5 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/DefaultMemoryRetriever.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/DefaultMemoryRetriever.java @@ -19,12 +19,14 @@ import com.openmemind.ai.memory.core.retrieval.admission.RetrievalAdmissionOptions; import com.openmemind.ai.memory.core.retrieval.admission.RetrievalAdmissionPolicy; import com.openmemind.ai.memory.core.retrieval.admission.RetrievalAdmissionResult; +import com.openmemind.ai.memory.core.retrieval.observation.DefaultMemoryRetrieverObservation; import com.openmemind.ai.memory.core.retrieval.query.LongQueryCondenser; import com.openmemind.ai.memory.core.retrieval.query.QueryContext; import com.openmemind.ai.memory.core.retrieval.query.QueryRewriter; import com.openmemind.ai.memory.core.retrieval.strategy.RetrievalStrategy; import com.openmemind.ai.memory.core.store.MemoryStore; import com.openmemind.ai.memory.core.textsearch.MemoryTextSearch; +import io.micrometer.observation.ObservationRegistry; import java.util.List; import java.util.Map; import java.util.Objects; @@ -52,6 +54,7 @@ public class DefaultMemoryRetriever implements MemoryRetriever { private final RetrievalAdmissionPolicy admissionPolicy; private final RetrievalAdmissionOptions admissionOptions; private final LongQueryCondenser longQueryCondenser; // nullable for legacy constructors + private final ObservationRegistry observationRegistry; public DefaultMemoryRetriever(MemoryStore memoryStore) { this(memoryStore, null, null); @@ -93,6 +96,24 @@ public DefaultMemoryRetriever( RetrievalAdmissionPolicy admissionPolicy, RetrievalAdmissionOptions admissionOptions, LongQueryCondenser longQueryCondenser) { + this( + memoryStore, + textSearch, + queryRewriter, + admissionPolicy, + admissionOptions, + longQueryCondenser, + ObservationRegistry.NOOP); + } + + public DefaultMemoryRetriever( + MemoryStore memoryStore, + MemoryTextSearch textSearch, + QueryRewriter queryRewriter, + RetrievalAdmissionPolicy admissionPolicy, + RetrievalAdmissionOptions admissionOptions, + LongQueryCondenser longQueryCondenser, + ObservationRegistry observationRegistry) { this.memoryStore = Objects.requireNonNull(memoryStore, "memoryStore must not be null"); this.textSearch = textSearch; // nullable this.queryRewriter = queryRewriter; // nullable @@ -101,6 +122,8 @@ public DefaultMemoryRetriever( this.admissionOptions = Objects.requireNonNull(admissionOptions, "admissionOptions must not be null"); this.longQueryCondenser = longQueryCondenser; + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; this.strategies = new ConcurrentHashMap<>(); } @@ -131,6 +154,12 @@ public Mono retrieve(RetrievalRequest request) { + " query, strategy)"); } + return DefaultMemoryRetrieverObservation.observe( + observationRegistry, request.memoryId(), () -> retrieveInternal(request, config)); + } + + private Mono retrieveInternal( + RetrievalRequest request, RetrievalConfig config) { RetrievalAdmissionResult admission = admissionPolicy.evaluate(request); if (admission.decision() == RetrievalAdmissionDecision.SKIP || admission.decision() == RetrievalAdmissionDecision.REJECT) { diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/deep/LlmTypedQueryExpander.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/deep/LlmTypedQueryExpander.java index 7a5d8038..795a2c30 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/deep/LlmTypedQueryExpander.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/deep/LlmTypedQueryExpander.java @@ -18,6 +18,8 @@ import com.openmemind.ai.memory.core.prompt.PromptRegistry; import com.openmemind.ai.memory.core.prompt.retrieval.TypedQueryExpandPrompts; import com.openmemind.ai.memory.core.retrieval.deep.ExpandedQuery.QueryType; +import com.openmemind.ai.memory.core.retrieval.deep.observation.LlmTypedQueryExpanderObservation; +import io.micrometer.observation.ObservationRegistry; import java.time.Duration; import java.util.List; import java.util.Objects; @@ -40,6 +42,7 @@ public class LlmTypedQueryExpander implements TypedQueryExpander { private final StructuredChatClient structuredChatClient; private final PromptRegistry promptRegistry; + private final ObservationRegistry observationRegistry; public LlmTypedQueryExpander(StructuredChatClient structuredChatClient) { this(structuredChatClient, PromptRegistry.EMPTY); @@ -47,11 +50,20 @@ public LlmTypedQueryExpander(StructuredChatClient structuredChatClient) { public LlmTypedQueryExpander( StructuredChatClient structuredChatClient, PromptRegistry promptRegistry) { + this(structuredChatClient, promptRegistry, ObservationRegistry.NOOP); + } + + public LlmTypedQueryExpander( + StructuredChatClient structuredChatClient, + PromptRegistry promptRegistry, + ObservationRegistry observationRegistry) { this.structuredChatClient = Objects.requireNonNull( structuredChatClient, "structuredChatClient must not be null"); this.promptRegistry = Objects.requireNonNull(promptRegistry, "promptRegistry must not be null"); + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } @Override @@ -61,35 +73,51 @@ public Mono> expand( List keyInformation, List conversationHistory, int maxExpansions) { - return Mono.defer( - () -> { - var promptResult = - TypedQueryExpandPrompts.build( - promptRegistry, - query, - gaps, - keyInformation, - conversationHistory, - maxExpansions) - .render("English"); - var messages = - ChatMessages.systemUser( - promptResult.systemPrompt(), promptResult.userPrompt()); - return structuredChatClient - .call(messages, TypedExpandResponse.class) - .map(response -> toExpandedQueries(response, maxExpansions)) - .switchIfEmpty(Mono.just(List.of())); - }) - .subscribeOn(Schedulers.boundedElastic()) - .retryWhen( - Retry.backoff(3, Duration.ofSeconds(2)).maxBackoff(Duration.ofSeconds(10))) - .onErrorResume( - e -> { - log.warn( - "Type-annotated query expansion failed, returning empty list", - e); - return Mono.just(List.of()); - }); + Mono> operation = + Mono.defer( + () -> { + var promptResult = + TypedQueryExpandPrompts.build( + promptRegistry, + query, + gaps, + keyInformation, + conversationHistory, + maxExpansions) + .render("English"); + var messages = + ChatMessages.systemUser( + promptResult.systemPrompt(), + promptResult.userPrompt()); + return structuredChatClient + .call(messages, TypedExpandResponse.class) + .map( + response -> + toExpandedQueries( + response, maxExpansions)) + .switchIfEmpty(Mono.just(List.of())); + }) + .subscribeOn(Schedulers.boundedElastic()) + .retryWhen( + Retry.backoff(3, Duration.ofSeconds(2)) + .maxBackoff(Duration.ofSeconds(10))); + return LlmTypedQueryExpanderObservation.observe( + observationRegistry, + query, + gaps, + keyInformation, + conversationHistory, + maxExpansions, + context -> + operation.onErrorResume( + e -> { + context.markDegraded(); + log.warn( + "Type-annotated query expansion failed, returning empty" + + " list", + e); + return Mono.just(List.of()); + })); } private List toExpandedQueries(TypedExpandResponse response, int maxExpansions) { diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/deep/observation/LlmTypedQueryExpanderObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/deep/observation/LlmTypedQueryExpanderObservation.java new file mode 100644 index 00000000..b3e87c21 --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/deep/observation/LlmTypedQueryExpanderObservation.java @@ -0,0 +1,310 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.deep.observation; + +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import com.openmemind.ai.memory.core.retrieval.deep.ExpandedQuery; +import com.openmemind.ai.memory.core.retrieval.trace.ObservationTiming; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEvent; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEventSource; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** Observation contracts for LlmTypedQueryExpander. */ +public final class LlmTypedQueryExpanderObservation { + + private LlmTypedQueryExpanderObservation() {} + + public static Mono> observe( + ObservationRegistry observationRegistry, + String query, + List gaps, + List keyInformation, + List conversationHistory, + int maxExpansions, + Supplier>> operation) { + return observe( + observationRegistry, + query, + gaps, + keyInformation, + conversationHistory, + maxExpansions, + ignored -> operation.get()); + } + + public static Mono> observe( + ObservationRegistry observationRegistry, + String query, + List gaps, + List keyInformation, + List conversationHistory, + int maxExpansions, + Function>> operation) { + return MemoryObservation.mono( + observationRegistry, + MultiQueryExpandDocument.EXPAND, + MultiQueryExpandConvention.INSTANCE, + reactorContext -> + new MultiQueryExpandObservationContext( + query, + gaps, + keyInformation, + conversationHistory, + maxExpansions, + reactorContext), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum MultiQueryExpandDocument implements ObservationDocumentation { + EXPAND; + + @Override + public String getName() { + return "memind.retrieval.multi_query_expand"; + } + + @Override + public Class> + getDefaultConvention() { + return MultiQueryExpandConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + QUERY { + @Override + public String asString() { + return "memind.retrieval.query"; + } + }, + GAP_COUNT { + @Override + public String asString() { + return "memind.retrieval.gap_count"; + } + }, + KEY_INFORMATION_COUNT { + @Override + public String asString() { + return "memind.retrieval.key_information_count"; + } + }, + CONVERSATION_HISTORY_COUNT { + @Override + public String asString() { + return "memind.retrieval.conversation_history_count"; + } + }, + MAX_EXPANSIONS { + @Override + public String asString() { + return "memind.retrieval.max_expansions"; + } + }, + RESULT_COUNT { + @Override + public String asString() { + return "memind.retrieval.result_count"; + } + }; + } + + public static final class MultiQueryExpandObservationContext extends MemoryObservationContext + implements RetrievalTraceEventSource { + + private final String query; + private final int gapCount; + private final int keyInformationCount; + private final int conversationHistoryCount; + private final int maxExpansions; + private List results = List.of(); + private boolean degraded; + + public MultiQueryExpandObservationContext( + String query, + List gaps, + List keyInformation, + List conversationHistory, + int maxExpansions) { + this(query, gaps, keyInformation, conversationHistory, maxExpansions, null); + } + + public MultiQueryExpandObservationContext( + String query, + List gaps, + List keyInformation, + List conversationHistory, + int maxExpansions, + ContextView reactorContext) { + super(reactorContext); + this.query = query; + this.gapCount = gaps == null ? 0 : gaps.size(); + this.keyInformationCount = keyInformation == null ? 0 : keyInformation.size(); + this.conversationHistoryCount = + conversationHistory == null ? 0 : conversationHistory.size(); + this.maxExpansions = maxExpansions; + } + + public void recordResult(List results) { + this.results = results == null ? List.of() : List.copyOf(results); + addHighCardinalityKeyValue( + HighCardinalityKeyNames.RESULT_COUNT.withValue(String.valueOf(resultCount()))); + } + + public int inputCount() { + return gapCount; + } + + public int candidateCount() { + return maxExpansions; + } + + public int resultCount() { + return results.size(); + } + + public void markDegraded() { + degraded = true; + } + + public String stage() { + return "query_expand"; + } + + public String tier() { + return "none"; + } + + public String method() { + return "llm"; + } + + public boolean degraded() { + return degraded; + } + + public boolean skipped() { + return maxExpansions <= 0; + } + + public String source() { + return "core"; + } + + @Override + public String status() { + String terminalStatus = errorOrCancellationStatus(); + return terminalStatus == null && degraded ? "degraded" : super.status(); + } + + @Override + public Optional toRetrievalTraceEvent( + ObservationTiming timing, RetrievalTraceOptions options) { + return Optional.of( + new RetrievalTraceEvent( + MultiQueryExpandDocument.EXPAND.getName(), + MultiQueryExpandDocument.EXPAND.getName(), + status(), + timing.startedAt(), + timing.completedAt(), + timing.durationMillis(), + Map.of("operation", "retrieval", "stage", stage()), + Map.of( + HighCardinalityKeyNames.GAP_COUNT.asString(), + String.valueOf(gapCount), + HighCardinalityKeyNames.RESULT_COUNT.asString(), + String.valueOf(resultCount())), + new RetrievalTraceEvent.StagePayload( + stage(), + tier(), + method(), + inputCount(), + candidateCount(), + resultCount(), + degraded(), + skipped(), + attributes(), + List.of()))); + } + + private Map attributes() { + return Map.of( + "gapCount", + gapCount, + "keyInformationCount", + keyInformationCount, + "conversationHistoryCount", + conversationHistoryCount, + "maxExpansions", + maxExpansions); + } + } + + public static final class MultiQueryExpandConvention + implements ObservationConvention { + + public static final MultiQueryExpandConvention INSTANCE = new MultiQueryExpandConvention(); + + @Override + public String getName() { + return MultiQueryExpandDocument.EXPAND.getName(); + } + + @Override + public String getContextualName(MultiQueryExpandObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(MultiQueryExpandObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.QUERY.withValue(safe(context.query)), + HighCardinalityKeyNames.GAP_COUNT.withValue(String.valueOf(context.gapCount)), + HighCardinalityKeyNames.KEY_INFORMATION_COUNT.withValue( + String.valueOf(context.keyInformationCount)), + HighCardinalityKeyNames.CONVERSATION_HISTORY_COUNT.withValue( + String.valueOf(context.conversationHistoryCount)), + HighCardinalityKeyNames.MAX_EXPANSIONS.withValue( + String.valueOf(context.maxExpansions))); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof MultiQueryExpandObservationContext; + } + } + + private static String safe(String value) { + return value == null ? "" : value; + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/graph/DefaultGraphItemChannel.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/graph/DefaultGraphItemChannel.java index 82c51fc0..02df1e91 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/graph/DefaultGraphItemChannel.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/graph/DefaultGraphItemChannel.java @@ -14,9 +14,11 @@ package com.openmemind.ai.memory.core.retrieval.graph; import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; +import com.openmemind.ai.memory.core.retrieval.graph.observation.DefaultGraphItemChannelObservation; import com.openmemind.ai.memory.core.retrieval.query.QueryContext; import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; import com.openmemind.ai.memory.core.store.graph.GraphQueryBudgetContext; +import io.micrometer.observation.ObservationRegistry; import java.time.Duration; import java.util.List; import java.util.concurrent.TimeoutException; @@ -30,9 +32,17 @@ public final class DefaultGraphItemChannel implements GraphItemChannel { private static final Logger log = LoggerFactory.getLogger(DefaultGraphItemChannel.class); private final GraphExpansionEngine engine; + private final ObservationRegistry observationRegistry; public DefaultGraphItemChannel(GraphExpansionEngine engine) { + this(engine, ObservationRegistry.NOOP); + } + + public DefaultGraphItemChannel( + GraphExpansionEngine engine, ObservationRegistry observationRegistry) { this.engine = engine; + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } @Override @@ -41,6 +51,19 @@ public Mono retrieve( RetrievalConfig config, RetrievalGraphSettings settings, List seeds) { + return DefaultGraphItemChannelObservation.observe( + observationRegistry, + context, + settings, + seeds, + () -> retrieveInternal(context, config, settings, seeds)); + } + + private Mono retrieveInternal( + QueryContext context, + RetrievalConfig config, + RetrievalGraphSettings settings, + List seeds) { boolean enabled = settings != null && settings.enabled(); if (!enabled || seeds == null || seeds.isEmpty() || engine == null) { return Mono.just(GraphExpansionResult.empty(enabled)); diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/graph/DefaultRetrievalGraphAssistant.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/graph/DefaultRetrievalGraphAssistant.java index 76c2b226..5dd0d183 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/graph/DefaultRetrievalGraphAssistant.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/graph/DefaultRetrievalGraphAssistant.java @@ -14,10 +14,12 @@ package com.openmemind.ai.memory.core.retrieval.graph; import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; +import com.openmemind.ai.memory.core.retrieval.graph.observation.DefaultRetrievalGraphAssistantObservation; import com.openmemind.ai.memory.core.retrieval.query.QueryContext; import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; import com.openmemind.ai.memory.core.store.MemoryStore; import com.openmemind.ai.memory.core.store.graph.GraphQueryBudgetContext; +import io.micrometer.observation.ObservationRegistry; import java.time.Duration; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -37,14 +39,22 @@ public final class DefaultRetrievalGraphAssistant implements RetrievalGraphAssistant { private final GraphExpansionEngine graphExpansionEngine; + private final ObservationRegistry observationRegistry; public DefaultRetrievalGraphAssistant(MemoryStore store) { this(new GraphExpansionEngine(store)); } public DefaultRetrievalGraphAssistant(GraphExpansionEngine graphExpansionEngine) { + this(graphExpansionEngine, ObservationRegistry.NOOP); + } + + public DefaultRetrievalGraphAssistant( + GraphExpansionEngine graphExpansionEngine, ObservationRegistry observationRegistry) { this.graphExpansionEngine = Objects.requireNonNull(graphExpansionEngine, "graphExpansionEngine"); + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } @Override @@ -53,6 +63,19 @@ public Mono assist( RetrievalConfig config, RetrievalGraphSettings graphSettings, List directItems) { + return DefaultRetrievalGraphAssistantObservation.observe( + observationRegistry, + context, + graphSettings, + directItems, + () -> assistInternal(context, config, graphSettings, directItems)); + } + + private Mono assistInternal( + QueryContext context, + RetrievalConfig config, + RetrievalGraphSettings graphSettings, + List directItems) { boolean enabled = graphSettings != null && graphSettings.enabled(); if (!enabled || directItems == null || directItems.isEmpty()) { return Mono.just(RetrievalGraphAssistResult.directOnly(directItems, enabled)); diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/graph/observation/DefaultGraphItemChannelObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/graph/observation/DefaultGraphItemChannelObservation.java new file mode 100644 index 00000000..8cb8472d --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/graph/observation/DefaultGraphItemChannelObservation.java @@ -0,0 +1,352 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.graph.observation; + +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import com.openmemind.ai.memory.core.retrieval.graph.GraphExpansionResult; +import com.openmemind.ai.memory.core.retrieval.graph.RetrievalGraphSettings; +import com.openmemind.ai.memory.core.retrieval.query.QueryContext; +import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; +import com.openmemind.ai.memory.core.retrieval.trace.ObservationTiming; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEvent; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEventSource; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** Observation contracts for DefaultGraphItemChannel. */ +public final class DefaultGraphItemChannelObservation { + + private DefaultGraphItemChannelObservation() {} + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + RetrievalGraphSettings settings, + List seeds, + Supplier> operation) { + return observe( + observationRegistry, queryContext, settings, seeds, ignored -> operation.get()); + } + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + RetrievalGraphSettings settings, + List seeds, + Function> operation) { + return MemoryObservation.mono( + observationRegistry, + GraphItemChannelDocument.RETRIEVE, + GraphItemChannelConvention.INSTANCE, + reactorContext -> + new GraphItemChannelObservationContext( + queryContext, settings, seeds, reactorContext), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum GraphItemChannelDocument implements ObservationDocumentation { + RETRIEVE; + + @Override + public String getName() { + return "memind.retrieval.channel.graph"; + } + + @Override + public Class> + getDefaultConvention() { + return GraphItemChannelConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }, + CHANNEL { + @Override + public String asString() { + return "memind.retrieval.channel"; + } + }, + GRAPH_ENABLED { + @Override + public String asString() { + return "memind.retrieval.graph.enabled"; + } + }, + GRAPH_SEED_COUNT { + @Override + public String asString() { + return "memind.retrieval.graph.seed_count"; + } + }, + RESULT_COUNT { + @Override + public String asString() { + return "memind.retrieval.result_count"; + } + }, + GRAPH_LINK_EXPANSION_COUNT { + @Override + public String asString() { + return "memind.retrieval.graph.link_expansion_count"; + } + }, + GRAPH_ENTITY_EXPANSION_COUNT { + @Override + public String asString() { + return "memind.retrieval.graph.entity_expansion_count"; + } + }, + GRAPH_DEDUPED_CANDIDATE_COUNT { + @Override + public String asString() { + return "memind.retrieval.graph.deduped_candidate_count"; + } + }, + GRAPH_OVERLAP_COUNT { + @Override + public String asString() { + return "memind.retrieval.graph.overlap_count"; + } + }, + GRAPH_SKIPPED_OVERFANOUT_ENTITY_COUNT { + @Override + public String asString() { + return "memind.retrieval.graph.skipped_overfanout_entity_count"; + } + }, + GRAPH_TIMEOUT { + @Override + public String asString() { + return "memind.retrieval.graph.timeout"; + } + }, + GRAPH_DEGRADED { + @Override + public String asString() { + return "memind.retrieval.graph.degraded"; + } + }; + } + + public static final class GraphItemChannelObservationContext extends MemoryObservationContext + implements RetrievalTraceEventSource { + + private final QueryContext queryContext; + private final RetrievalGraphSettings settings; + private final int seedCount; + private GraphExpansionResult result; + + public GraphItemChannelObservationContext( + QueryContext queryContext, + RetrievalGraphSettings settings, + List seeds) { + this(queryContext, settings, seeds, null); + } + + public GraphItemChannelObservationContext( + QueryContext queryContext, + RetrievalGraphSettings settings, + List seeds, + ContextView reactorContext) { + super(reactorContext); + this.queryContext = queryContext; + this.settings = settings; + this.seedCount = seeds == null ? 0 : seeds.size(); + } + + public void recordResult(GraphExpansionResult result) { + this.result = result; + add(HighCardinalityKeyNames.RESULT_COUNT, resultCount(result)); + add( + HighCardinalityKeyNames.GRAPH_LINK_EXPANSION_COUNT, + result == null ? 0 : result.linkExpansionCount()); + add( + HighCardinalityKeyNames.GRAPH_ENTITY_EXPANSION_COUNT, + result == null ? 0 : result.entityExpansionCount()); + add( + HighCardinalityKeyNames.GRAPH_DEDUPED_CANDIDATE_COUNT, + result == null ? 0 : result.dedupedCandidateCount()); + add( + HighCardinalityKeyNames.GRAPH_OVERLAP_COUNT, + result == null ? 0 : result.overlapCount()); + add( + HighCardinalityKeyNames.GRAPH_SKIPPED_OVERFANOUT_ENTITY_COUNT, + result == null ? 0 : result.skippedOverFanoutEntityCount()); + add(HighCardinalityKeyNames.GRAPH_TIMEOUT, result != null && result.timedOut()); + add(HighCardinalityKeyNames.GRAPH_DEGRADED, result != null && result.degraded()); + } + + private int resultCount(GraphExpansionResult result) { + return result == null || result.graphItems() == null ? 0 : result.graphItems().size(); + } + + public int resultCount() { + return resultCount(result); + } + + public int candidateCount() { + return result == null ? 0 : result.dedupedCandidateCount(); + } + + public boolean degraded() { + return result != null && result.degraded(); + } + + public boolean skipped() { + return result == null || !result.enabled(); + } + + public String strategyName() { + return null; + } + + public String stage() { + return "channel"; + } + + public String tier() { + return "item"; + } + + public String method() { + return "graph"; + } + + public String source() { + return "core"; + } + + @Override + public String status() { + String terminalStatus = errorOrCancellationStatus(); + if (terminalStatus != null) { + return terminalStatus; + } + return degraded() ? "degraded" : "success"; + } + + @Override + public Optional toRetrievalTraceEvent( + ObservationTiming timing, RetrievalTraceOptions options) { + List results = result == null ? List.of() : result.graphItems(); + return Optional.of( + new RetrievalTraceEvent( + GraphItemChannelDocument.RETRIEVE.getName(), + GraphItemChannelDocument.RETRIEVE.getName(), + status(), + timing.startedAt(), + timing.completedAt(), + timing.durationMillis(), + Map.of("operation", "retrieval", "stage", "channel"), + Map.of( + HighCardinalityKeyNames.RESULT_COUNT.asString(), + String.valueOf(resultCount()), + HighCardinalityKeyNames.GRAPH_DEDUPED_CANDIDATE_COUNT + .asString(), + String.valueOf(candidateCount())), + new RetrievalTraceEvent.StagePayload( + "channel", + "item", + "graph", + seedCount, + candidateCount(), + resultCount(), + degraded(), + skipped(), + attributes(), + RetrievalTraceEvent.candidates( + results, + options.maxCandidatesPerStage(), + options.maxTextLength())))); + } + + private Map attributes() { + if (result == null) { + return Map.of(); + } + return Map.of( + "linkExpansionCount", + result.linkExpansionCount(), + "entityExpansionCount", + result.entityExpansionCount(), + "overlapCount", + result.overlapCount(), + "timedOut", + result.timedOut()); + } + + private void add(HighCardinalityKeyNames key, Object value) { + if (value != null) { + addHighCardinalityKeyValue(key.withValue(String.valueOf(value))); + } + } + } + + public static final class GraphItemChannelConvention + implements ObservationConvention { + + public static final GraphItemChannelConvention INSTANCE = new GraphItemChannelConvention(); + + @Override + public String getName() { + return GraphItemChannelDocument.RETRIEVE.getName(); + } + + @Override + public String getContextualName(GraphItemChannelObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(GraphItemChannelObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue( + context.queryContext.memoryId().toIdentifier()), + HighCardinalityKeyNames.CHANNEL.withValue("graph"), + HighCardinalityKeyNames.GRAPH_ENABLED.withValue( + String.valueOf(context.settings != null && context.settings.enabled())), + HighCardinalityKeyNames.GRAPH_SEED_COUNT.withValue( + String.valueOf(context.seedCount))); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof GraphItemChannelObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/graph/observation/DefaultRetrievalGraphAssistantObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/graph/observation/DefaultRetrievalGraphAssistantObservation.java new file mode 100644 index 00000000..d363475a --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/graph/observation/DefaultRetrievalGraphAssistantObservation.java @@ -0,0 +1,384 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.graph.observation; + +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import com.openmemind.ai.memory.core.retrieval.graph.RetrievalGraphAssistResult; +import com.openmemind.ai.memory.core.retrieval.graph.RetrievalGraphSettings; +import com.openmemind.ai.memory.core.retrieval.query.QueryContext; +import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; +import com.openmemind.ai.memory.core.retrieval.trace.ObservationTiming; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEvent; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEventSource; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** Observation contracts for DefaultRetrievalGraphAssistant. */ +public final class DefaultRetrievalGraphAssistantObservation { + + private DefaultRetrievalGraphAssistantObservation() {} + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + RetrievalGraphSettings graphSettings, + List directItems, + Supplier> operation) { + return observe( + observationRegistry, + queryContext, + graphSettings, + directItems, + ignored -> operation.get()); + } + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + RetrievalGraphSettings graphSettings, + List directItems, + Function> operation) { + return MemoryObservation.mono( + observationRegistry, + GraphAssistDocument.ASSIST, + GraphAssistConvention.INSTANCE, + reactorContext -> + new GraphAssistObservationContext( + queryContext, graphSettings, directItems, reactorContext), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum GraphAssistDocument implements ObservationDocumentation { + ASSIST; + + @Override + public String getName() { + return "memind.retrieval.graph.assist"; + } + + @Override + public Class> + getDefaultConvention() { + return GraphAssistConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }, + GRAPH_ENABLED { + @Override + public String asString() { + return "memind.retrieval.graph.enabled"; + } + }, + DIRECT_COUNT { + @Override + public String asString() { + return "memind.retrieval.graph.direct_count"; + } + }, + RESULT_COUNT { + @Override + public String asString() { + return "memind.retrieval.result_count"; + } + }, + GRAPH_SEED_COUNT { + @Override + public String asString() { + return "memind.retrieval.graph.seed_count"; + } + }, + GRAPH_LINK_EXPANSION_COUNT { + @Override + public String asString() { + return "memind.retrieval.graph.link_expansion_count"; + } + }, + GRAPH_ENTITY_EXPANSION_COUNT { + @Override + public String asString() { + return "memind.retrieval.graph.entity_expansion_count"; + } + }, + GRAPH_DEDUPED_CANDIDATE_COUNT { + @Override + public String asString() { + return "memind.retrieval.graph.deduped_candidate_count"; + } + }, + GRAPH_ADMITTED_CANDIDATE_COUNT { + @Override + public String asString() { + return "memind.retrieval.graph.admitted_candidate_count"; + } + }, + GRAPH_DISPLACED_DIRECT_COUNT { + @Override + public String asString() { + return "memind.retrieval.graph.displaced_direct_count"; + } + }, + GRAPH_OVERLAP_COUNT { + @Override + public String asString() { + return "memind.retrieval.graph.overlap_count"; + } + }, + GRAPH_SKIPPED_OVERFANOUT_ENTITY_COUNT { + @Override + public String asString() { + return "memind.retrieval.graph.skipped_overfanout_entity_count"; + } + }, + GRAPH_TIMEOUT { + @Override + public String asString() { + return "memind.retrieval.graph.timeout"; + } + }, + GRAPH_DEGRADED { + @Override + public String asString() { + return "memind.retrieval.graph.degraded"; + } + }; + } + + public static final class GraphAssistObservationContext extends MemoryObservationContext + implements RetrievalTraceEventSource { + + private final QueryContext queryContext; + private final RetrievalGraphSettings graphSettings; + private final int directCount; + private RetrievalGraphAssistResult result; + + public GraphAssistObservationContext( + QueryContext queryContext, RetrievalGraphSettings graphSettings) { + this(queryContext, graphSettings, null); + } + + public GraphAssistObservationContext( + QueryContext queryContext, + RetrievalGraphSettings graphSettings, + List directItems) { + this(queryContext, graphSettings, directItems, null); + } + + public GraphAssistObservationContext( + QueryContext queryContext, + RetrievalGraphSettings graphSettings, + List directItems, + ContextView reactorContext) { + super(reactorContext); + this.queryContext = queryContext; + this.graphSettings = graphSettings; + this.directCount = directItems == null ? 0 : directItems.size(); + } + + public void recordResult(RetrievalGraphAssistResult result) { + this.result = result; + if (result == null) { + add(HighCardinalityKeyNames.RESULT_COUNT, 0); + return; + } + var stats = result.stats(); + add(HighCardinalityKeyNames.RESULT_COUNT, resultCount()); + add(HighCardinalityKeyNames.GRAPH_SEED_COUNT, stats.seedCount()); + add(HighCardinalityKeyNames.GRAPH_LINK_EXPANSION_COUNT, stats.linkExpansionCount()); + add(HighCardinalityKeyNames.GRAPH_ENTITY_EXPANSION_COUNT, stats.entityExpansionCount()); + add( + HighCardinalityKeyNames.GRAPH_DEDUPED_CANDIDATE_COUNT, + stats.dedupedCandidateCount()); + add( + HighCardinalityKeyNames.GRAPH_ADMITTED_CANDIDATE_COUNT, + stats.admittedGraphCandidateCount()); + add(HighCardinalityKeyNames.GRAPH_DISPLACED_DIRECT_COUNT, stats.displacedDirectCount()); + add(HighCardinalityKeyNames.GRAPH_OVERLAP_COUNT, stats.overlapCount()); + add( + HighCardinalityKeyNames.GRAPH_SKIPPED_OVERFANOUT_ENTITY_COUNT, + stats.skippedOverFanoutEntityCount()); + add(HighCardinalityKeyNames.GRAPH_TIMEOUT, stats.timedOut()); + add(HighCardinalityKeyNames.GRAPH_DEGRADED, stats.degraded()); + } + + public int inputCount() { + return directCount; + } + + public int candidateCount() { + return result == null ? 0 : result.stats().dedupedCandidateCount(); + } + + public int resultCount() { + return result == null || result.items() == null ? 0 : result.items().size(); + } + + public boolean degraded() { + return result != null && result.stats().degraded(); + } + + public boolean skipped() { + return result == null || !result.stats().graphEnabled() || directCount == 0; + } + + public String stage() { + return "graph_assist"; + } + + public String tier() { + return "item"; + } + + public String method() { + return "graph"; + } + + public String source() { + return "core"; + } + + @Override + public String status() { + String terminalStatus = errorOrCancellationStatus(); + if (terminalStatus != null) { + return terminalStatus; + } + if (degraded()) { + return "degraded"; + } + return skipped() ? "skipped" : "success"; + } + + @Override + public Optional toRetrievalTraceEvent( + ObservationTiming timing, RetrievalTraceOptions options) { + List results = result == null ? List.of() : result.items(); + return Optional.of( + new RetrievalTraceEvent( + GraphAssistDocument.ASSIST.getName(), + GraphAssistDocument.ASSIST.getName(), + status(), + timing.startedAt(), + timing.completedAt(), + timing.durationMillis(), + Map.of("operation", "retrieval", "stage", stage()), + Map.of( + HighCardinalityKeyNames.DIRECT_COUNT.asString(), + String.valueOf(directCount), + HighCardinalityKeyNames.RESULT_COUNT.asString(), + String.valueOf(resultCount())), + new RetrievalTraceEvent.StagePayload( + stage(), + tier(), + method(), + inputCount(), + candidateCount(), + resultCount(), + degraded(), + skipped(), + attributes(), + RetrievalTraceEvent.candidates( + results, + options.maxCandidatesPerStage(), + options.maxTextLength())))); + } + + private Map attributes() { + if (result == null) { + return Map.of(); + } + var stats = result.stats(); + return Map.of( + "seedCount", + stats.seedCount(), + "linkExpansionCount", + stats.linkExpansionCount(), + "entityExpansionCount", + stats.entityExpansionCount(), + "admittedGraphCandidateCount", + stats.admittedGraphCandidateCount(), + "timedOut", + stats.timedOut()); + } + + private void add(HighCardinalityKeyNames key, Object value) { + if (value != null) { + addHighCardinalityKeyValue(key.withValue(String.valueOf(value))); + } + } + } + + public static final class GraphAssistConvention + implements ObservationConvention { + + public static final GraphAssistConvention INSTANCE = new GraphAssistConvention(); + + @Override + public String getName() { + return GraphAssistDocument.ASSIST.getName(); + } + + @Override + public String getContextualName(GraphAssistObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(GraphAssistObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue(memoryId(context.queryContext)), + HighCardinalityKeyNames.GRAPH_ENABLED.withValue( + String.valueOf( + context.graphSettings != null + && context.graphSettings.enabled())), + HighCardinalityKeyNames.DIRECT_COUNT.withValue( + String.valueOf(context.directCount))); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof GraphAssistObservationContext; + } + } + + private static String memoryId(QueryContext queryContext) { + return queryContext == null || queryContext.memoryId() == null + ? "unknown" + : queryContext.memoryId().toIdentifier(); + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/observation/DefaultMemoryRetrieverObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/observation/DefaultMemoryRetrieverObservation.java new file mode 100644 index 00000000..5ecd206c --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/observation/DefaultMemoryRetrieverObservation.java @@ -0,0 +1,214 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.observation; + +import com.openmemind.ai.memory.core.data.MemoryId; +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import com.openmemind.ai.memory.core.retrieval.RetrievalResult; +import com.openmemind.ai.memory.core.retrieval.trace.ObservationTiming; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEvent; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEventSource; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** Observation contracts for DefaultMemoryRetriever. */ +public final class DefaultMemoryRetrieverObservation { + + private DefaultMemoryRetrieverObservation() {} + + public static Mono observe( + ObservationRegistry observationRegistry, + MemoryId memoryId, + Supplier> operation) { + return observe(observationRegistry, memoryId, ignored -> operation.get()); + } + + public static Mono observe( + ObservationRegistry observationRegistry, + MemoryId memoryId, + Function> operation) { + return MemoryObservation.mono( + observationRegistry, + RetrievalDocument.RETRIEVAL, + RetrievalObservationConvention.INSTANCE, + reactorContext -> new RetrievalObservationContext(memoryId, reactorContext), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum RetrievalDocument implements ObservationDocumentation { + RETRIEVAL; + + @Override + public String getName() { + return "memind.retrieval"; + } + + @Override + public Class> + getDefaultConvention() { + return RetrievalObservationConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }, + RESULT_COUNT { + @Override + public String asString() { + return "memind.retrieval.result_count"; + } + }; + } + + public static final class RetrievalObservationContext extends MemoryObservationContext + implements RetrievalTraceEventSource { + + private final MemoryId memoryId; + private String strategyName = "unknown"; + private int itemCount; + private int insightCount; + private int rawDataCount; + private int evidenceCount; + private String resultStatus = "unknown"; + + public RetrievalObservationContext(MemoryId memoryId) { + this(memoryId, null); + } + + public RetrievalObservationContext(MemoryId memoryId, ContextView reactorContext) { + super(reactorContext); + this.memoryId = memoryId; + } + + public void recordResult(RetrievalResult result) { + strategyName = + result == null || result.strategy() == null ? "unknown" : result.strategy(); + itemCount = result == null || result.items() == null ? 0 : result.items().size(); + insightCount = + result == null || result.insights() == null ? 0 : result.insights().size(); + rawDataCount = result == null || result.rawData() == null ? 0 : result.rawData().size(); + evidenceCount = + result == null || result.evidences() == null ? 0 : result.evidences().size(); + resultStatus = + result == null || result.status() == null + ? "unknown" + : result.status().name().toLowerCase(); + addHighCardinalityKeyValue( + HighCardinalityKeyNames.RESULT_COUNT.withValue(String.valueOf(itemCount))); + } + + @Override + public String status() { + String terminalStatus = errorOrCancellationStatus(); + return terminalStatus == null ? resultStatus : terminalStatus; + } + + public String strategyName() { + return strategyName; + } + + public int itemCount() { + return itemCount; + } + + public int insightCount() { + return insightCount; + } + + public int rawDataCount() { + return rawDataCount; + } + + public int evidenceCount() { + return evidenceCount; + } + + public String source() { + return "core"; + } + + @Override + public Optional toRetrievalTraceEvent( + ObservationTiming timing, RetrievalTraceOptions options) { + return Optional.of( + new RetrievalTraceEvent( + RetrievalDocument.RETRIEVAL.getName(), + RetrievalDocument.RETRIEVAL.getName(), + status(), + timing.startedAt(), + timing.completedAt(), + timing.durationMillis(), + Map.of("operation", "retrieval", "strategy", strategyName), + Map.of( + HighCardinalityKeyNames.RESULT_COUNT.asString(), + String.valueOf(itemCount)), + new RetrievalTraceEvent.FinalPayload( + strategyName, + itemCount, + insightCount, + rawDataCount, + evidenceCount))); + } + } + + public static final class RetrievalObservationConvention + implements ObservationConvention { + + public static final RetrievalObservationConvention INSTANCE = + new RetrievalObservationConvention(); + + @Override + public String getName() { + return RetrievalDocument.RETRIEVAL.getName(); + } + + @Override + public String getContextualName(RetrievalObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(RetrievalObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue(context.memoryId.toIdentifier())); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof RetrievalObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/scoring/DefaultRetrievalResultMerger.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/scoring/DefaultRetrievalResultMerger.java index 31e87632..fc2291c5 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/scoring/DefaultRetrievalResultMerger.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/scoring/DefaultRetrievalResultMerger.java @@ -13,6 +13,8 @@ */ package com.openmemind.ai.memory.core.retrieval.scoring; +import com.openmemind.ai.memory.core.retrieval.scoring.observation.DefaultRetrievalResultMergerObservation; +import io.micrometer.observation.ObservationRegistry; import java.util.List; import reactor.core.publisher.Mono; @@ -21,11 +23,29 @@ public final class DefaultRetrievalResultMerger implements RetrievalResultMerger public static final DefaultRetrievalResultMerger INSTANCE = new DefaultRetrievalResultMerger(); - private DefaultRetrievalResultMerger() {} + private final ObservationRegistry observationRegistry; + + public DefaultRetrievalResultMerger() { + this(ObservationRegistry.NOOP); + } + + public DefaultRetrievalResultMerger(ObservationRegistry observationRegistry) { + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; + } @Override public Mono> merge( ScoringConfig scoring, List> rankedLists, double... weights) { + return DefaultRetrievalResultMergerObservation.observe( + observationRegistry, + rankedLists, + weights, + () -> mergeInternal(scoring, rankedLists, weights)); + } + + private Mono> mergeInternal( + ScoringConfig scoring, List> rankedLists, double... weights) { return Mono.fromSupplier(() -> ResultMerger.merge(scoring, rankedLists, weights)); } } diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/scoring/observation/DefaultRetrievalResultMergerObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/scoring/observation/DefaultRetrievalResultMergerObservation.java new file mode 100644 index 00000000..a8d5b8d6 --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/scoring/observation/DefaultRetrievalResultMergerObservation.java @@ -0,0 +1,248 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.scoring.observation; + +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; +import com.openmemind.ai.memory.core.retrieval.trace.ObservationTiming; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEvent; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEventSource; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** Observation contracts for DefaultRetrievalResultMerger. */ +public final class DefaultRetrievalResultMergerObservation { + + private DefaultRetrievalResultMergerObservation() {} + + public static Mono> observe( + ObservationRegistry observationRegistry, + List> rankedLists, + double[] weights, + Supplier>> operation) { + return observe(observationRegistry, rankedLists, weights, ignored -> operation.get()); + } + + public static Mono> observe( + ObservationRegistry observationRegistry, + List> rankedLists, + double[] weights, + Function>> operation) { + return MemoryObservation.mono( + observationRegistry, + ResultMergeDocument.MERGE, + ResultMergeConvention.INSTANCE, + reactorContext -> + new ResultMergeObservationContext(rankedLists, weights, reactorContext), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum ResultMergeDocument implements ObservationDocumentation { + MERGE; + + @Override + public String getName() { + return "memind.retrieval.result_merge"; + } + + @Override + public Class> + getDefaultConvention() { + return ResultMergeConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + SOURCE_LIST_COUNT { + @Override + public String asString() { + return "memind.retrieval.source_list_count"; + } + }, + CANDIDATE_COUNT { + @Override + public String asString() { + return "memind.retrieval.candidate_count"; + } + }, + DEDUPED_COUNT { + @Override + public String asString() { + return "memind.retrieval.deduped_count"; + } + }, + WEIGHT_COUNT { + @Override + public String asString() { + return "memind.retrieval.weight_count"; + } + }, + RESULT_COUNT { + @Override + public String asString() { + return "memind.retrieval.result_count"; + } + }; + } + + public static final class ResultMergeObservationContext extends MemoryObservationContext + implements RetrievalTraceEventSource { + + private final List> rankedLists; + private final double[] weights; + private int outputCount; + private int deduplicatedCount; + + public ResultMergeObservationContext( + List> rankedLists, double[] weights) { + this(rankedLists, weights, null); + } + + public ResultMergeObservationContext( + List> rankedLists, + double[] weights, + ContextView reactorContext) { + super(reactorContext); + this.rankedLists = rankedLists; + this.weights = weights; + } + + public void recordResult(List result) { + int before = candidateCount(rankedLists); + int after = result == null ? 0 : result.size(); + outputCount = after; + deduplicatedCount = Math.max(0, before - after); + add(HighCardinalityKeyNames.RESULT_COUNT, after); + add(HighCardinalityKeyNames.DEDUPED_COUNT, deduplicatedCount); + } + + public int inputCount() { + return candidateCount(rankedLists); + } + + public int outputCount() { + return outputCount; + } + + public int deduplicatedCount() { + return deduplicatedCount; + } + + public int sourceCount() { + return rankedLists == null ? 0 : rankedLists.size(); + } + + public int weightCount() { + return weights == null ? 0 : weights.length; + } + + public String strategyName() { + return null; + } + + public String source() { + return "core"; + } + + @Override + public Optional toRetrievalTraceEvent( + ObservationTiming timing, RetrievalTraceOptions options) { + return Optional.of( + new RetrievalTraceEvent( + ResultMergeDocument.MERGE.getName(), + ResultMergeDocument.MERGE.getName(), + status(), + timing.startedAt(), + timing.completedAt(), + timing.durationMillis(), + Map.of("operation", "retrieval", "stage", "merge"), + Map.of( + HighCardinalityKeyNames.CANDIDATE_COUNT.asString(), + String.valueOf(inputCount()), + HighCardinalityKeyNames.RESULT_COUNT.asString(), + String.valueOf(outputCount())), + new RetrievalTraceEvent.MergePayload( + inputCount(), + outputCount(), + deduplicatedCount(), + sourceCount()))); + } + + private void add(HighCardinalityKeyNames key, Object value) { + if (value != null) { + addHighCardinalityKeyValue(key.withValue(String.valueOf(value))); + } + } + } + + public static final class ResultMergeConvention + implements ObservationConvention { + + public static final ResultMergeConvention INSTANCE = new ResultMergeConvention(); + + @Override + public String getName() { + return ResultMergeDocument.MERGE.getName(); + } + + @Override + public String getContextualName(ResultMergeObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(ResultMergeObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.SOURCE_LIST_COUNT.withValue( + String.valueOf(context.sourceCount())), + HighCardinalityKeyNames.CANDIDATE_COUNT.withValue( + String.valueOf(context.inputCount())), + HighCardinalityKeyNames.DEDUPED_COUNT.withValue( + String.valueOf(context.deduplicatedCount())), + HighCardinalityKeyNames.WEIGHT_COUNT.withValue( + String.valueOf(context.weightCount()))); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof ResultMergeObservationContext; + } + } + + private static int candidateCount(List> rankedLists) { + if (rankedLists == null) { + return 0; + } + return rankedLists.stream().filter(Objects::nonNull).mapToInt(List::size).sum(); + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/strategy/DeepRetrievalStrategy.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/strategy/DeepRetrievalStrategy.java index 2d64553c..8cf4b3be 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/strategy/DeepRetrievalStrategy.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/strategy/DeepRetrievalStrategy.java @@ -29,6 +29,7 @@ import com.openmemind.ai.memory.core.retrieval.scoring.RetrievalResultMerger; import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; import com.openmemind.ai.memory.core.retrieval.scoring.TimeDecay; +import com.openmemind.ai.memory.core.retrieval.strategy.observation.DeepRetrievalStrategyObservation; import com.openmemind.ai.memory.core.retrieval.sufficiency.SufficiencyGate; import com.openmemind.ai.memory.core.retrieval.thread.MemoryThreadAssistResult; import com.openmemind.ai.memory.core.retrieval.thread.MemoryThreadAssistant; @@ -40,6 +41,7 @@ import com.openmemind.ai.memory.core.store.MemoryStore; import com.openmemind.ai.memory.core.textsearch.MemoryTextSearch; import com.openmemind.ai.memory.core.textsearch.TextSearchResult; +import io.micrometer.observation.ObservationRegistry; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -84,6 +86,7 @@ public class DeepRetrievalStrategy implements RetrievalStrategy { private final RetrievalGraphAssistant graphAssistant; private final MemoryThreadAssistant memoryThreadAssistant; private final RetrievalResultMerger resultMerger; + private final ObservationRegistry observationRegistry; /** Tier2 initial retrieval result */ private record Tier2InitResult( @@ -184,6 +187,32 @@ public DeepRetrievalStrategy( RetrievalGraphAssistant graphAssistant, MemoryThreadAssistant memoryThreadAssistant, RetrievalResultMerger resultMerger) { + this( + insightRetriever, + itemRetriever, + sufficiencyGate, + typedQueryExpander, + reranker, + memoryStore, + defaultStrategyConfig, + graphAssistant, + memoryThreadAssistant, + resultMerger, + ObservationRegistry.NOOP); + } + + public DeepRetrievalStrategy( + InsightTierSearch insightRetriever, + ItemTierSearch itemRetriever, + SufficiencyGate sufficiencyGate, + TypedQueryExpander typedQueryExpander, + Reranker reranker, + MemoryStore memoryStore, + DeepStrategyConfig defaultStrategyConfig, + RetrievalGraphAssistant graphAssistant, + MemoryThreadAssistant memoryThreadAssistant, + RetrievalResultMerger resultMerger, + ObservationRegistry observationRegistry) { this.insightRetriever = Objects.requireNonNull(insightRetriever, "insightRetriever must not be null"); this.itemRetriever = @@ -207,6 +236,8 @@ public DeepRetrievalStrategy( : NoOpMemoryThreadAssistant.INSTANCE; this.resultMerger = resultMerger != null ? resultMerger : DefaultRetrievalResultMerger.INSTANCE; + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } @Override @@ -218,7 +249,8 @@ public String name() { public Mono retrieve(QueryContext context, RetrievalConfig config) { log.debug("DeepRetrieval strategy started: query={}", context.searchQuery()); - return executePipeline(context, config); + return DeepRetrievalStrategyObservation.observe( + observationRegistry, context, name(), () -> executePipeline(context, config)); } private DeepStrategyConfig deepConfig(RetrievalConfig config) { diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/strategy/SimpleRetrievalStrategy.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/strategy/SimpleRetrievalStrategy.java index 09d782cf..a70429b2 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/strategy/SimpleRetrievalStrategy.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/strategy/SimpleRetrievalStrategy.java @@ -27,6 +27,7 @@ import com.openmemind.ai.memory.core.retrieval.scoring.RetrievalResultMerger; import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; import com.openmemind.ai.memory.core.retrieval.scoring.TimeDecay; +import com.openmemind.ai.memory.core.retrieval.strategy.observation.SimpleRetrievalStrategyObservation; import com.openmemind.ai.memory.core.retrieval.temporal.DefaultTemporalConstraintExtractor; import com.openmemind.ai.memory.core.retrieval.temporal.DefaultTemporalItemChannel; import com.openmemind.ai.memory.core.retrieval.temporal.TemporalConstraintExtractor; @@ -43,6 +44,7 @@ import com.openmemind.ai.memory.core.store.MemoryStore; import com.openmemind.ai.memory.core.textsearch.MemoryTextSearch; import com.openmemind.ai.memory.core.textsearch.TextSearchResult; +import io.micrometer.observation.ObservationRegistry; import java.time.Clock; import java.util.ArrayList; import java.util.Comparator; @@ -83,6 +85,7 @@ public class SimpleRetrievalStrategy implements RetrievalStrategy { graphItemChannel; // nullable: preserves legacy graph assistant behavior private final RetrievalResultMerger resultMerger; private final Clock clock; + private final ObservationRegistry observationRegistry; public SimpleRetrievalStrategy( InsightTierSearch insightRetriever, @@ -120,6 +123,36 @@ public SimpleRetrievalStrategy( GraphItemChannel graphItemChannel, RetrievalResultMerger resultMerger, Clock clock) { + this( + insightRetriever, + itemRetriever, + textSearch, + memoryStore, + defaultStrategyConfig, + graphAssistant, + memoryThreadAssistant, + temporalConstraintExtractor, + temporalItemChannel, + graphItemChannel, + resultMerger, + clock, + ObservationRegistry.NOOP); + } + + public SimpleRetrievalStrategy( + InsightTierSearch insightRetriever, + ItemTierSearch itemRetriever, + MemoryTextSearch textSearch, + MemoryStore memoryStore, + SimpleStrategyConfig defaultStrategyConfig, + RetrievalGraphAssistant graphAssistant, + MemoryThreadAssistant memoryThreadAssistant, + TemporalConstraintExtractor temporalConstraintExtractor, + TemporalItemChannel temporalItemChannel, + GraphItemChannel graphItemChannel, + RetrievalResultMerger resultMerger, + Clock clock, + ObservationRegistry observationRegistry) { this.insightRetriever = Objects.requireNonNull(insightRetriever, "insightRetriever must not be null"); this.itemRetriever = @@ -148,6 +181,8 @@ public SimpleRetrievalStrategy( this.resultMerger = resultMerger != null ? resultMerger : DefaultRetrievalResultMerger.INSTANCE; this.clock = clock != null ? clock : Clock.systemDefaultZone(); + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } public SimpleRetrievalStrategy( @@ -207,7 +242,8 @@ public String name() { public Mono retrieve(QueryContext context, RetrievalConfig config) { log.debug("Simple strategy started: query={}", context.searchQuery()); - return executePipeline(context, config); + return SimpleRetrievalStrategyObservation.observe( + observationRegistry, context, name(), () -> executePipeline(context, config)); } private Mono executePipeline(QueryContext context, RetrievalConfig config) { diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/strategy/observation/DeepRetrievalStrategyObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/strategy/observation/DeepRetrievalStrategyObservation.java new file mode 100644 index 00000000..dbe9099c --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/strategy/observation/DeepRetrievalStrategyObservation.java @@ -0,0 +1,197 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.strategy.observation; + +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import com.openmemind.ai.memory.core.retrieval.RetrievalResult; +import com.openmemind.ai.memory.core.retrieval.query.QueryContext; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** Observation contracts for DeepRetrievalStrategy. */ +public final class DeepRetrievalStrategyObservation { + + private DeepRetrievalStrategyObservation() {} + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + String strategyName, + Supplier> operation) { + return observe(observationRegistry, queryContext, strategyName, ignored -> operation.get()); + } + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + String strategyName, + Function> operation) { + return MemoryObservation.mono( + observationRegistry, + StrategyDocument.RETRIEVE, + StrategyConvention.INSTANCE, + reactorContext -> + new StrategyObservationContext(queryContext, strategyName, reactorContext), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum StrategyDocument implements ObservationDocumentation { + RETRIEVE; + + @Override + public String getName() { + return "memind.retrieval.strategy"; + } + + @Override + public Class> + getDefaultConvention() { + return StrategyConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }, + STRATEGY { + @Override + public String asString() { + return "memind.retrieval.strategy"; + } + }, + RESULT_COUNT { + @Override + public String asString() { + return "memind.retrieval.result_count"; + } + }; + } + + public static final class StrategyObservationContext extends MemoryObservationContext { + + private final QueryContext queryContext; + private final String strategyName; + private int itemCount; + private int insightCount; + private int rawDataCount; + private int evidenceCount; + private String resultStatus = "empty"; + + public StrategyObservationContext(QueryContext queryContext, String strategyName) { + this(queryContext, strategyName, null); + } + + public StrategyObservationContext( + QueryContext queryContext, String strategyName, ContextView reactorContext) { + super(reactorContext); + this.queryContext = queryContext; + this.strategyName = strategyName; + } + + public void recordResult(RetrievalResult result) { + itemCount = result == null || result.items() == null ? 0 : result.items().size(); + insightCount = + result == null || result.insights() == null ? 0 : result.insights().size(); + rawDataCount = result == null || result.rawData() == null ? 0 : result.rawData().size(); + evidenceCount = + result == null || result.evidences() == null ? 0 : result.evidences().size(); + resultStatus = + result == null || result.status() == null + ? "empty" + : result.status().name().toLowerCase(); + addHighCardinalityKeyValue( + HighCardinalityKeyNames.RESULT_COUNT.withValue(String.valueOf(itemCount))); + } + + @Override + public String status() { + String terminalStatus = errorOrCancellationStatus(); + if (terminalStatus != null) { + return terminalStatus; + } + return resultStatus; + } + + public String strategyName() { + return strategyName; + } + + public int itemCount() { + return itemCount; + } + + public int insightCount() { + return insightCount; + } + + public int rawDataCount() { + return rawDataCount; + } + + public int evidenceCount() { + return evidenceCount; + } + + public String source() { + return "core"; + } + } + + public static final class StrategyConvention + implements ObservationConvention { + + public static final StrategyConvention INSTANCE = new StrategyConvention(); + + @Override + public String getName() { + return StrategyDocument.RETRIEVE.getName(); + } + + @Override + public String getContextualName(StrategyObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(StrategyObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue( + context.queryContext.memoryId().toIdentifier()), + HighCardinalityKeyNames.STRATEGY.withValue(context.strategyName)); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof StrategyObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/strategy/observation/SimpleRetrievalStrategyObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/strategy/observation/SimpleRetrievalStrategyObservation.java new file mode 100644 index 00000000..219fb844 --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/strategy/observation/SimpleRetrievalStrategyObservation.java @@ -0,0 +1,197 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.strategy.observation; + +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import com.openmemind.ai.memory.core.retrieval.RetrievalResult; +import com.openmemind.ai.memory.core.retrieval.query.QueryContext; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** Observation contracts for SimpleRetrievalStrategy. */ +public final class SimpleRetrievalStrategyObservation { + + private SimpleRetrievalStrategyObservation() {} + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + String strategyName, + Supplier> operation) { + return observe(observationRegistry, queryContext, strategyName, ignored -> operation.get()); + } + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + String strategyName, + Function> operation) { + return MemoryObservation.mono( + observationRegistry, + StrategyDocument.RETRIEVE, + StrategyConvention.INSTANCE, + reactorContext -> + new StrategyObservationContext(queryContext, strategyName, reactorContext), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum StrategyDocument implements ObservationDocumentation { + RETRIEVE; + + @Override + public String getName() { + return "memind.retrieval.strategy"; + } + + @Override + public Class> + getDefaultConvention() { + return StrategyConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }, + STRATEGY { + @Override + public String asString() { + return "memind.retrieval.strategy"; + } + }, + RESULT_COUNT { + @Override + public String asString() { + return "memind.retrieval.result_count"; + } + }; + } + + public static final class StrategyObservationContext extends MemoryObservationContext { + + private final QueryContext queryContext; + private final String strategyName; + private int itemCount; + private int insightCount; + private int rawDataCount; + private int evidenceCount; + private String resultStatus = "empty"; + + public StrategyObservationContext(QueryContext queryContext, String strategyName) { + this(queryContext, strategyName, null); + } + + public StrategyObservationContext( + QueryContext queryContext, String strategyName, ContextView reactorContext) { + super(reactorContext); + this.queryContext = queryContext; + this.strategyName = strategyName; + } + + public void recordResult(RetrievalResult result) { + itemCount = result == null || result.items() == null ? 0 : result.items().size(); + insightCount = + result == null || result.insights() == null ? 0 : result.insights().size(); + rawDataCount = result == null || result.rawData() == null ? 0 : result.rawData().size(); + evidenceCount = + result == null || result.evidences() == null ? 0 : result.evidences().size(); + resultStatus = + result == null || result.status() == null + ? "empty" + : result.status().name().toLowerCase(); + addHighCardinalityKeyValue( + HighCardinalityKeyNames.RESULT_COUNT.withValue(String.valueOf(itemCount))); + } + + @Override + public String status() { + String terminalStatus = errorOrCancellationStatus(); + if (terminalStatus != null) { + return terminalStatus; + } + return resultStatus; + } + + public String strategyName() { + return strategyName; + } + + public int itemCount() { + return itemCount; + } + + public int insightCount() { + return insightCount; + } + + public int rawDataCount() { + return rawDataCount; + } + + public int evidenceCount() { + return evidenceCount; + } + + public String source() { + return "core"; + } + } + + public static final class StrategyConvention + implements ObservationConvention { + + public static final StrategyConvention INSTANCE = new StrategyConvention(); + + @Override + public String getName() { + return StrategyDocument.RETRIEVE.getName(); + } + + @Override + public String getContextualName(StrategyObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(StrategyObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue( + context.queryContext.memoryId().toIdentifier()), + HighCardinalityKeyNames.STRATEGY.withValue(context.strategyName)); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof StrategyObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/sufficiency/LlmSufficiencyGate.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/sufficiency/LlmSufficiencyGate.java index 3256374c..52d0c272 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/sufficiency/LlmSufficiencyGate.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/sufficiency/LlmSufficiencyGate.java @@ -19,6 +19,8 @@ import com.openmemind.ai.memory.core.prompt.retrieval.SufficiencyGatePrompts; import com.openmemind.ai.memory.core.retrieval.query.QueryContext; import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; +import com.openmemind.ai.memory.core.retrieval.sufficiency.observation.LlmSufficiencyGateObservation; +import io.micrometer.observation.ObservationRegistry; import java.time.Duration; import java.util.List; import java.util.Objects; @@ -38,6 +40,7 @@ public class LlmSufficiencyGate implements SufficiencyGate { private final StructuredChatClient structuredChatClient; private final PromptRegistry promptRegistry; + private final ObservationRegistry observationRegistry; public LlmSufficiencyGate(StructuredChatClient structuredChatClient) { this(structuredChatClient, PromptRegistry.EMPTY); @@ -45,15 +48,30 @@ public LlmSufficiencyGate(StructuredChatClient structuredChatClient) { public LlmSufficiencyGate( StructuredChatClient structuredChatClient, PromptRegistry promptRegistry) { + this(structuredChatClient, promptRegistry, ObservationRegistry.NOOP); + } + + public LlmSufficiencyGate( + StructuredChatClient structuredChatClient, + PromptRegistry promptRegistry, + ObservationRegistry observationRegistry) { this.structuredChatClient = Objects.requireNonNull( structuredChatClient, "structuredChatClient must not be null"); this.promptRegistry = Objects.requireNonNull(promptRegistry, "promptRegistry must not be null"); + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } @Override public Mono check(QueryContext context, List results) { + return LlmSufficiencyGateObservation.observe( + observationRegistry, context, results, () -> checkInternal(context, results)); + } + + private Mono checkInternal( + QueryContext context, List results) { if (results.isEmpty()) { return Mono.just(SufficiencyResult.fallbackInsufficient()); } diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/sufficiency/observation/LlmSufficiencyGateObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/sufficiency/observation/LlmSufficiencyGateObservation.java new file mode 100644 index 00000000..4cb16ff8 --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/sufficiency/observation/LlmSufficiencyGateObservation.java @@ -0,0 +1,300 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.sufficiency.observation; + +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import com.openmemind.ai.memory.core.retrieval.query.QueryContext; +import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; +import com.openmemind.ai.memory.core.retrieval.sufficiency.SufficiencyResult; +import com.openmemind.ai.memory.core.retrieval.trace.ObservationTiming; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEvent; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEventSource; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** Observation contracts for LlmSufficiencyGate. */ +public final class LlmSufficiencyGateObservation { + + private LlmSufficiencyGateObservation() {} + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + List results, + Supplier> operation) { + return observe(observationRegistry, queryContext, results, ignored -> operation.get()); + } + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + List results, + Function> operation) { + return MemoryObservation.mono( + observationRegistry, + SufficiencyDocument.SUFFICIENCY, + SufficiencyConvention.INSTANCE, + reactorContext -> + new SufficiencyObservationContext(queryContext, results, reactorContext), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum SufficiencyDocument implements ObservationDocumentation { + SUFFICIENCY; + + @Override + public String getName() { + return "memind.retrieval.sufficiency"; + } + + @Override + public Class> + getDefaultConvention() { + return SufficiencyConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }, + INPUT_COUNT { + @Override + public String asString() { + return "memind.retrieval.input_count"; + } + }, + SUFFICIENT { + @Override + public String asString() { + return "memind.retrieval.sufficient"; + } + }, + GAP_COUNT { + @Override + public String asString() { + return "memind.retrieval.gap_count"; + } + }, + EVIDENCE_COUNT { + @Override + public String asString() { + return "memind.retrieval.evidence_count"; + } + }, + KEY_INFORMATION_COUNT { + @Override + public String asString() { + return "memind.retrieval.key_information_count"; + } + }; + } + + public static final class SufficiencyObservationContext extends MemoryObservationContext + implements RetrievalTraceEventSource { + + private final QueryContext queryContext; + private final int inputCount; + private SufficiencyResult result; + + public SufficiencyObservationContext( + QueryContext queryContext, List results) { + this(queryContext, results, null); + } + + public SufficiencyObservationContext( + QueryContext queryContext, List results, ContextView reactorContext) { + super(reactorContext); + this.queryContext = queryContext; + this.inputCount = results == null ? 0 : results.size(); + } + + public void recordResult(SufficiencyResult result) { + this.result = result; + addHighCardinalityKeyValue( + HighCardinalityKeyNames.SUFFICIENT.withValue( + String.valueOf(result != null && result.sufficient()))); + add(HighCardinalityKeyNames.GAP_COUNT, gapCount()); + add(HighCardinalityKeyNames.EVIDENCE_COUNT, evidenceCount()); + add(HighCardinalityKeyNames.KEY_INFORMATION_COUNT, keyInformationCount()); + } + + public int inputCount() { + return inputCount; + } + + public int candidateCount() { + return inputCount; + } + + public int resultCount() { + return evidenceCount(); + } + + public boolean degraded() { + return result != null && "fallback".equals(result.reasoning()); + } + + public boolean skipped() { + return false; + } + + public String stage() { + return "sufficiency"; + } + + public String tier() { + return "item"; + } + + public String method() { + return "llm"; + } + + public String source() { + return "core"; + } + + @Override + public String status() { + String terminalStatus = errorOrCancellationStatus(); + if (terminalStatus != null) { + return terminalStatus; + } + return degraded() ? "degraded" : "success"; + } + + @Override + public Optional toRetrievalTraceEvent( + ObservationTiming timing, RetrievalTraceOptions options) { + return Optional.of( + new RetrievalTraceEvent( + SufficiencyDocument.SUFFICIENCY.getName(), + SufficiencyDocument.SUFFICIENCY.getName(), + status(), + timing.startedAt(), + timing.completedAt(), + timing.durationMillis(), + Map.of("operation", "retrieval", "stage", stage()), + Map.of( + HighCardinalityKeyNames.INPUT_COUNT.asString(), + String.valueOf(inputCount), + HighCardinalityKeyNames.SUFFICIENT.asString(), + String.valueOf(sufficient())), + new RetrievalTraceEvent.StagePayload( + stage(), + tier(), + method(), + inputCount(), + candidateCount(), + resultCount(), + degraded(), + skipped(), + attributes(), + List.of()))); + } + + private Map attributes() { + return Map.of( + "sufficient", + sufficient(), + "gapCount", + gapCount(), + "evidenceCount", + evidenceCount(), + "keyInformationCount", + keyInformationCount()); + } + + private boolean sufficient() { + return result != null && result.sufficient(); + } + + private int gapCount() { + return result == null || result.gaps() == null ? 0 : result.gaps().size(); + } + + private int evidenceCount() { + return result == null || result.evidences() == null ? 0 : result.evidences().size(); + } + + private int keyInformationCount() { + return result == null || result.keyInformation() == null + ? 0 + : result.keyInformation().size(); + } + + private void add(HighCardinalityKeyNames key, Object value) { + if (value != null) { + addHighCardinalityKeyValue(key.withValue(String.valueOf(value))); + } + } + } + + public static final class SufficiencyConvention + implements ObservationConvention { + + public static final SufficiencyConvention INSTANCE = new SufficiencyConvention(); + + @Override + public String getName() { + return SufficiencyDocument.SUFFICIENCY.getName(); + } + + @Override + public String getContextualName(SufficiencyObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(SufficiencyObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue(memoryId(context.queryContext)), + HighCardinalityKeyNames.INPUT_COUNT.withValue( + String.valueOf(context.inputCount))); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof SufficiencyObservationContext; + } + } + + private static String memoryId(QueryContext queryContext) { + return queryContext == null || queryContext.memoryId() == null + ? "unknown" + : queryContext.memoryId().toIdentifier(); + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/temporal/DefaultTemporalItemChannel.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/temporal/DefaultTemporalItemChannel.java index 5f16c154..4298b1fd 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/temporal/DefaultTemporalItemChannel.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/temporal/DefaultTemporalItemChannel.java @@ -18,9 +18,11 @@ import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; import com.openmemind.ai.memory.core.retrieval.query.QueryContext; import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; +import com.openmemind.ai.memory.core.retrieval.temporal.observation.DefaultTemporalItemChannelObservation; import com.openmemind.ai.memory.core.store.MemoryStore; import com.openmemind.ai.memory.core.store.item.TemporalItemLookupMatch; import com.openmemind.ai.memory.core.store.item.TemporalItemLookupRequest; +import io.micrometer.observation.ObservationRegistry; import java.time.Duration; import java.util.Comparator; import java.util.Optional; @@ -36,9 +38,17 @@ public final class DefaultTemporalItemChannel implements TemporalItemChannel { private static final Logger log = LoggerFactory.getLogger(DefaultTemporalItemChannel.class); private final MemoryStore memoryStore; + private final ObservationRegistry observationRegistry; public DefaultTemporalItemChannel(MemoryStore memoryStore) { + this(memoryStore, ObservationRegistry.NOOP); + } + + public DefaultTemporalItemChannel( + MemoryStore memoryStore, ObservationRegistry observationRegistry) { this.memoryStore = memoryStore; + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } @Override @@ -47,6 +57,19 @@ public Mono retrieve( RetrievalConfig config, Optional temporalConstraint, TemporalItemChannelSettings settings) { + return DefaultTemporalItemChannelObservation.observe( + observationRegistry, + context, + temporalConstraint, + settings, + () -> retrieveInternal(context, config, temporalConstraint, settings)); + } + + private Mono retrieveInternal( + QueryContext context, + RetrievalConfig config, + Optional temporalConstraint, + TemporalItemChannelSettings settings) { var effectiveSettings = settings != null ? settings : TemporalItemChannelSettings.defaults(); boolean constraintPresent = temporalConstraint != null && temporalConstraint.isPresent(); diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/temporal/observation/DefaultTemporalItemChannelObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/temporal/observation/DefaultTemporalItemChannelObservation.java new file mode 100644 index 00000000..2b66567d --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/temporal/observation/DefaultTemporalItemChannelObservation.java @@ -0,0 +1,311 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.temporal.observation; + +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import com.openmemind.ai.memory.core.retrieval.query.QueryContext; +import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; +import com.openmemind.ai.memory.core.retrieval.temporal.TemporalConstraint; +import com.openmemind.ai.memory.core.retrieval.temporal.TemporalItemChannelResult; +import com.openmemind.ai.memory.core.retrieval.temporal.TemporalItemChannelSettings; +import com.openmemind.ai.memory.core.retrieval.trace.ObservationTiming; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEvent; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEventSource; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** Observation contracts for DefaultTemporalItemChannel. */ +public final class DefaultTemporalItemChannelObservation { + + private DefaultTemporalItemChannelObservation() {} + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + Optional temporalConstraint, + TemporalItemChannelSettings settings, + Supplier> operation) { + return observe( + observationRegistry, + queryContext, + temporalConstraint, + settings, + ignored -> operation.get()); + } + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + Optional temporalConstraint, + TemporalItemChannelSettings settings, + Function> + operation) { + return MemoryObservation.mono( + observationRegistry, + TemporalItemChannelDocument.RETRIEVE, + TemporalItemChannelConvention.INSTANCE, + reactorContext -> + new TemporalItemChannelObservationContext( + queryContext, temporalConstraint, settings, reactorContext), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum TemporalItemChannelDocument implements ObservationDocumentation { + RETRIEVE; + + @Override + public String getName() { + return "memind.retrieval.channel.temporal"; + } + + @Override + public Class> + getDefaultConvention() { + return TemporalItemChannelConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }, + CHANNEL { + @Override + public String asString() { + return "memind.retrieval.channel"; + } + }, + RESULT_COUNT { + @Override + public String asString() { + return "memind.retrieval.result_count"; + } + }, + CANDIDATE_COUNT { + @Override + public String asString() { + return "memind.retrieval.candidate_count"; + } + }, + TEMPORAL_ENABLED { + @Override + public String asString() { + return "memind.retrieval.temporal.enabled"; + } + }, + TEMPORAL_CONSTRAINT_PRESENT { + @Override + public String asString() { + return "memind.retrieval.temporal.constraint_present"; + } + }, + TEMPORAL_DEGRADED { + @Override + public String asString() { + return "memind.retrieval.temporal.degraded"; + } + }; + } + + public static final class TemporalItemChannelObservationContext extends MemoryObservationContext + implements RetrievalTraceEventSource { + + private final QueryContext queryContext; + private final Optional temporalConstraint; + private final TemporalItemChannelSettings settings; + private TemporalItemChannelResult result; + + public TemporalItemChannelObservationContext( + QueryContext queryContext, + Optional temporalConstraint, + TemporalItemChannelSettings settings) { + this(queryContext, temporalConstraint, settings, null); + } + + public TemporalItemChannelObservationContext( + QueryContext queryContext, + Optional temporalConstraint, + TemporalItemChannelSettings settings, + ContextView reactorContext) { + super(reactorContext); + this.queryContext = queryContext; + this.temporalConstraint = temporalConstraint; + this.settings = settings; + } + + public void recordResult(TemporalItemChannelResult result) { + this.result = result; + add(HighCardinalityKeyNames.RESULT_COUNT, resultCount(result)); + add( + HighCardinalityKeyNames.CANDIDATE_COUNT, + result == null ? 0 : result.candidateCount()); + add(HighCardinalityKeyNames.TEMPORAL_ENABLED, result != null && result.enabled()); + add( + HighCardinalityKeyNames.TEMPORAL_CONSTRAINT_PRESENT, + result != null && result.constraintPresent()); + add(HighCardinalityKeyNames.TEMPORAL_DEGRADED, result != null && result.degraded()); + } + + private int resultCount(TemporalItemChannelResult result) { + return result == null || result.items() == null ? 0 : result.items().size(); + } + + public int resultCount() { + return resultCount(result); + } + + public int candidateCount() { + return result == null ? 0 : result.candidateCount(); + } + + public boolean degraded() { + return result != null && result.degraded(); + } + + public boolean skipped() { + return result == null || !result.enabled(); + } + + public String strategyName() { + return null; + } + + public String stage() { + return "channel"; + } + + public String tier() { + return "item"; + } + + public String method() { + return "temporal"; + } + + public String source() { + return "core"; + } + + @Override + public String status() { + String terminalStatus = errorOrCancellationStatus(); + if (terminalStatus != null) { + return terminalStatus; + } + return degraded() ? "degraded" : "success"; + } + + @Override + public Optional toRetrievalTraceEvent( + ObservationTiming timing, RetrievalTraceOptions options) { + List results = result == null ? List.of() : result.items(); + return Optional.of( + new RetrievalTraceEvent( + TemporalItemChannelDocument.RETRIEVE.getName(), + TemporalItemChannelDocument.RETRIEVE.getName(), + status(), + timing.startedAt(), + timing.completedAt(), + timing.durationMillis(), + Map.of("operation", "retrieval", "stage", "channel"), + Map.of( + HighCardinalityKeyNames.RESULT_COUNT.asString(), + String.valueOf(resultCount()), + HighCardinalityKeyNames.CANDIDATE_COUNT.asString(), + String.valueOf(candidateCount())), + new RetrievalTraceEvent.StagePayload( + "channel", + "item", + "temporal", + null, + candidateCount(), + resultCount(), + degraded(), + skipped(), + result == null + ? Map.of() + : Map.of( + "constraintPresent", + result.constraintPresent()), + RetrievalTraceEvent.candidates( + results, + options.maxCandidatesPerStage(), + options.maxTextLength())))); + } + + private void add(HighCardinalityKeyNames key, Object value) { + if (value != null) { + addHighCardinalityKeyValue(key.withValue(String.valueOf(value))); + } + } + } + + public static final class TemporalItemChannelConvention + implements ObservationConvention { + + public static final TemporalItemChannelConvention INSTANCE = + new TemporalItemChannelConvention(); + + @Override + public String getName() { + return TemporalItemChannelDocument.RETRIEVE.getName(); + } + + @Override + public String getContextualName(TemporalItemChannelObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues( + TemporalItemChannelObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue( + context.queryContext.memoryId().toIdentifier()), + HighCardinalityKeyNames.CHANNEL.withValue("temporal"), + HighCardinalityKeyNames.TEMPORAL_ENABLED.withValue( + String.valueOf(context.settings != null && context.settings.enabled())), + HighCardinalityKeyNames.TEMPORAL_CONSTRAINT_PRESENT.withValue( + String.valueOf( + context.temporalConstraint != null + && context.temporalConstraint.isPresent()))); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof TemporalItemChannelObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/thread/DefaultMemoryThreadAssistant.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/thread/DefaultMemoryThreadAssistant.java index d1401f3a..5f8eb995 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/thread/DefaultMemoryThreadAssistant.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/thread/DefaultMemoryThreadAssistant.java @@ -17,7 +17,9 @@ import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; import com.openmemind.ai.memory.core.retrieval.query.QueryContext; import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; +import com.openmemind.ai.memory.core.retrieval.thread.observation.DefaultMemoryThreadAssistantObservation; import com.openmemind.ai.memory.core.store.MemoryStore; +import io.micrometer.observation.ObservationRegistry; import java.time.Clock; import java.time.Duration; import java.util.LinkedHashSet; @@ -35,19 +37,26 @@ public final class DefaultMemoryThreadAssistant implements MemoryThreadAssistant private final ThreadAssistSeedResolver seedResolver; private final ThreadAssistThreadRanker threadRanker; private final ThreadAssistMemberRanker memberRanker; + private final ObservationRegistry observationRegistry; public DefaultMemoryThreadAssistant(MemoryStore store) { this(store, MemoryThreadLifecycleOptions.defaults().dormantAfter()); } public DefaultMemoryThreadAssistant(MemoryStore store, Duration dormantAfter) { + this(store, dormantAfter, ObservationRegistry.NOOP); + } + + public DefaultMemoryThreadAssistant( + MemoryStore store, Duration dormantAfter, ObservationRegistry observationRegistry) { this( store, dormantAfter, Clock.systemUTC(), new ThreadAssistSeedResolver(), new ThreadAssistThreadRanker(store, dormantAfter, Clock.systemUTC()), - new ThreadAssistMemberRanker(store)); + new ThreadAssistMemberRanker(store), + observationRegistry); } DefaultMemoryThreadAssistant(MemoryStore store, Duration dormantAfter, Clock clock) { @@ -57,7 +66,8 @@ public DefaultMemoryThreadAssistant(MemoryStore store, Duration dormantAfter) { clock, new ThreadAssistSeedResolver(), new ThreadAssistThreadRanker(store, dormantAfter, clock), - new ThreadAssistMemberRanker(store)); + new ThreadAssistMemberRanker(store), + ObservationRegistry.NOOP); } DefaultMemoryThreadAssistant( @@ -67,12 +77,32 @@ public DefaultMemoryThreadAssistant(MemoryStore store, Duration dormantAfter) { ThreadAssistSeedResolver seedResolver, ThreadAssistThreadRanker threadRanker, ThreadAssistMemberRanker memberRanker) { + this( + store, + dormantAfter, + clock, + seedResolver, + threadRanker, + memberRanker, + ObservationRegistry.NOOP); + } + + DefaultMemoryThreadAssistant( + MemoryStore store, + Duration dormantAfter, + Clock clock, + ThreadAssistSeedResolver seedResolver, + ThreadAssistThreadRanker threadRanker, + ThreadAssistMemberRanker memberRanker, + ObservationRegistry observationRegistry) { Objects.requireNonNull(store, "store"); Objects.requireNonNull(dormantAfter, "dormantAfter"); Objects.requireNonNull(clock, "clock"); this.seedResolver = Objects.requireNonNull(seedResolver, "seedResolver"); this.threadRanker = Objects.requireNonNull(threadRanker, "threadRanker"); this.memberRanker = Objects.requireNonNull(memberRanker, "memberRanker"); + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } @Override @@ -81,6 +111,18 @@ public Mono assist( RetrievalConfig config, RetrievalMemoryThreadSettings settings, List directWindow) { + return DefaultMemoryThreadAssistantObservation.observe( + observationRegistry, + context, + settings, + () -> assistInternal(context, config, settings, directWindow)); + } + + private Mono assistInternal( + QueryContext context, + RetrievalConfig config, + RetrievalMemoryThreadSettings settings, + List directWindow) { boolean enabled = settings != null && settings.enabled(); if (!enabled || directWindow == null || directWindow.isEmpty()) { return Mono.just(MemoryThreadAssistResult.directOnly(directWindow, enabled)); diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/thread/observation/DefaultMemoryThreadAssistantObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/thread/observation/DefaultMemoryThreadAssistantObservation.java new file mode 100644 index 00000000..29debdb6 --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/thread/observation/DefaultMemoryThreadAssistantObservation.java @@ -0,0 +1,187 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.thread.observation; + +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import com.openmemind.ai.memory.core.retrieval.query.QueryContext; +import com.openmemind.ai.memory.core.retrieval.thread.MemoryThreadAssistResult; +import com.openmemind.ai.memory.core.retrieval.thread.RetrievalMemoryThreadSettings; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; + +/** Observation contracts for DefaultMemoryThreadAssistant. */ +public final class DefaultMemoryThreadAssistantObservation { + + private DefaultMemoryThreadAssistantObservation() {} + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + RetrievalMemoryThreadSettings settings, + Supplier> operation) { + return observe(observationRegistry, queryContext, settings, ignored -> operation.get()); + } + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + RetrievalMemoryThreadSettings settings, + Function> + operation) { + return MemoryObservation.mono( + observationRegistry, + MemoryThreadAssistDocument.ASSIST, + MemoryThreadAssistConvention.INSTANCE, + () -> new MemoryThreadAssistObservationContext(queryContext, settings), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum MemoryThreadAssistDocument implements ObservationDocumentation { + ASSIST; + + @Override + public String getName() { + return "memind.retrieval.memory_thread.assist"; + } + + @Override + public Class> + getDefaultConvention() { + return MemoryThreadAssistConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }, + ENABLED { + @Override + public String asString() { + return "memind.retrieval.memory_thread.enabled"; + } + }, + SEED_THREAD_COUNT { + @Override + public String asString() { + return "memind.retrieval.memory_thread.seed_thread_count"; + } + }, + CANDIDATE_COUNT { + @Override + public String asString() { + return "memind.retrieval.memory_thread.candidate_count"; + } + }, + ADMITTED_COUNT { + @Override + public String asString() { + return "memind.retrieval.memory_thread.admitted_count"; + } + }, + CLAMPED { + @Override + public String asString() { + return "memind.retrieval.memory_thread.clamped"; + } + }, + DEGRADED { + @Override + public String asString() { + return "memind.retrieval.memory_thread.degraded"; + } + }, + TIMEOUT { + @Override + public String asString() { + return "memind.retrieval.memory_thread.timeout"; + } + }; + } + + public static final class MemoryThreadAssistObservationContext extends Observation.Context { + + private final QueryContext queryContext; + private final RetrievalMemoryThreadSettings settings; + + public MemoryThreadAssistObservationContext( + QueryContext queryContext, RetrievalMemoryThreadSettings settings) { + this.queryContext = queryContext; + this.settings = settings; + } + + public void recordResult(MemoryThreadAssistResult result) { + var stats = result.stats(); + add(HighCardinalityKeyNames.SEED_THREAD_COUNT, stats.seedThreadCount()); + add(HighCardinalityKeyNames.CANDIDATE_COUNT, stats.candidateCount()); + add(HighCardinalityKeyNames.ADMITTED_COUNT, stats.admittedMemberCount()); + add(HighCardinalityKeyNames.CLAMPED, stats.clamped()); + add(HighCardinalityKeyNames.DEGRADED, stats.degraded()); + add(HighCardinalityKeyNames.TIMEOUT, stats.timedOut()); + } + + private void add(HighCardinalityKeyNames key, Object value) { + if (value != null) { + addHighCardinalityKeyValue(key.withValue(String.valueOf(value))); + } + } + } + + public static final class MemoryThreadAssistConvention + implements ObservationConvention { + + public static final MemoryThreadAssistConvention INSTANCE = + new MemoryThreadAssistConvention(); + + @Override + public String getName() { + return MemoryThreadAssistDocument.ASSIST.getName(); + } + + @Override + public String getContextualName(MemoryThreadAssistObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(MemoryThreadAssistObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue( + context.queryContext.memoryId().toIdentifier()), + HighCardinalityKeyNames.ENABLED.withValue( + String.valueOf( + context.settings != null && context.settings.enabled()))); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof MemoryThreadAssistObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/InsightTierRetriever.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/InsightTierRetriever.java index 26cd5bc3..b260528b 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/InsightTierRetriever.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/InsightTierRetriever.java @@ -22,8 +22,10 @@ import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; import com.openmemind.ai.memory.core.retrieval.query.QueryContext; import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; +import com.openmemind.ai.memory.core.retrieval.tier.observation.InsightTierRetrieverObservation; import com.openmemind.ai.memory.core.store.MemoryStore; import com.openmemind.ai.memory.core.vector.MemoryVector; +import io.micrometer.observation.ObservationRegistry; import java.util.ArrayList; import java.util.Comparator; import java.util.EnumMap; @@ -58,21 +60,46 @@ public class InsightTierRetriever implements InsightTierSearch { private final MemoryVector memoryVector; private final InsightTypeRouter router; private final int maxExpandedLeafsPerBranch; + private final ObservationRegistry observationRegistry; public InsightTierRetriever( MemoryStore memoryStore, MemoryVector memoryVector, InsightTypeRouter router) { this(memoryStore, memoryVector, router, 0); } + public InsightTierRetriever( + MemoryStore memoryStore, + MemoryVector memoryVector, + InsightTypeRouter router, + ObservationRegistry observationRegistry) { + this(memoryStore, memoryVector, router, 0, observationRegistry); + } + public InsightTierRetriever( MemoryStore memoryStore, MemoryVector memoryVector, InsightTypeRouter router, int maxExpandedLeafsPerBranch) { + this( + memoryStore, + memoryVector, + router, + maxExpandedLeafsPerBranch, + ObservationRegistry.NOOP); + } + + public InsightTierRetriever( + MemoryStore memoryStore, + MemoryVector memoryVector, + InsightTypeRouter router, + int maxExpandedLeafsPerBranch, + ObservationRegistry observationRegistry) { this.memoryStore = Objects.requireNonNull(memoryStore, "memoryStore must not be null"); this.memoryVector = Objects.requireNonNull(memoryVector, "memoryVector must not be null"); this.router = Objects.requireNonNull(router, "router must not be null"); this.maxExpandedLeafsPerBranch = maxExpandedLeafsPerBranch; + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } /** @@ -83,6 +110,11 @@ public InsightTierRetriever( * @return Results containing ROOT + routed matching BRANCH + expanded LEAF */ public Mono retrieve(QueryContext context, RetrievalConfig config) { + return InsightTierRetrieverObservation.observe( + observationRegistry, context, config, () -> retrieveInternal(context, config)); + } + + private Mono retrieveInternal(QueryContext context, RetrievalConfig config) { if (!config.tier1().enabled()) { return Mono.just(TierResult.empty()); } diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/ItemTierRetriever.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/ItemTierRetriever.java index cf8d9265..a566ec7c 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/ItemTierRetriever.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/ItemTierRetriever.java @@ -23,11 +23,14 @@ import com.openmemind.ai.memory.core.retrieval.scoring.ScoringConfig; import com.openmemind.ai.memory.core.retrieval.scoring.TimeDecay; import com.openmemind.ai.memory.core.retrieval.strategy.SimpleStrategyConfig; +import com.openmemind.ai.memory.core.retrieval.tier.observation.ItemTierRetrieverObservation; +import com.openmemind.ai.memory.core.retrieval.tier.observation.ItemTierRetrieverObservation.ItemTierDocument; import com.openmemind.ai.memory.core.retrieval.truncation.AdaptiveTruncator; import com.openmemind.ai.memory.core.store.MemoryStore; import com.openmemind.ai.memory.core.textsearch.MemoryTextSearch; import com.openmemind.ai.memory.core.vector.MemoryVector; import com.openmemind.ai.memory.core.vector.VectorSearchResult; +import io.micrometer.observation.ObservationRegistry; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -54,6 +57,7 @@ public class ItemTierRetriever implements ItemTierSearch { private final MemoryStore memoryStore; private final MemoryVector memoryVector; private final MemoryTextSearch textSearch; + private final ObservationRegistry observationRegistry; public ItemTierRetriever(MemoryStore memoryStore, MemoryVector memoryVector) { this(memoryStore, memoryVector, null); @@ -61,9 +65,19 @@ public ItemTierRetriever(MemoryStore memoryStore, MemoryVector memoryVector) { public ItemTierRetriever( MemoryStore memoryStore, MemoryVector memoryVector, MemoryTextSearch textSearch) { + this(memoryStore, memoryVector, textSearch, ObservationRegistry.NOOP); + } + + public ItemTierRetriever( + MemoryStore memoryStore, + MemoryVector memoryVector, + MemoryTextSearch textSearch, + ObservationRegistry observationRegistry) { this.memoryStore = Objects.requireNonNull(memoryStore, "memoryStore must not be null"); this.memoryVector = Objects.requireNonNull(memoryVector, "memoryVector must not be null"); this.textSearch = textSearch; + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } /** @@ -83,6 +97,16 @@ public MemoryTextSearch textSearch() { * @return Scoring results + scopeHints (rawDataIds) */ public Mono searchByVector(QueryContext context, RetrievalConfig config) { + return ItemTierRetrieverObservation.observe( + observationRegistry, + context, + "vector", + ItemTierDocument.VECTOR_SEARCH, + config.tier2().topK(), + () -> searchByVectorInternal(context, config)); + } + + private Mono searchByVectorInternal(QueryContext context, RetrievalConfig config) { if (!config.tier2().enabled()) { return Mono.just(TierResult.empty()); } @@ -371,7 +395,8 @@ static double timeDecayFactor(MemoryItem item, QueryContext context, ScoringConf public Mono> searchByVector( QueryContext context, RetrievalConfig.TierConfig tier, ScoringConfig scoring) { RetrievalConfig tempConfig = buildTempConfig(tier, scoring); - return searchByVector(context, tempConfig).map(TierResult::results); + var operation = searchByVectorInternal(context, tempConfig).map(TierResult::results); + return observeListSearch(operation, context, tier.topK(), ItemTierDocument.VECTOR_SEARCH); } /** @@ -382,6 +407,12 @@ public Mono> searchByVector( @Override public Mono> searchByKeyword( QueryContext context, RetrievalConfig.TierConfig tier, ScoringConfig scoring) { + var operation = searchByKeywordInternal(context, tier, scoring); + return observeListSearch(operation, context, tier.topK(), ItemTierDocument.KEYWORD_SEARCH); + } + + private Mono> searchByKeywordInternal( + QueryContext context, RetrievalConfig.TierConfig tier, ScoringConfig scoring) { if (textSearch == null || !tier.enabled()) { return Mono.just(List.of()); } @@ -434,15 +465,23 @@ public Mono> searchByKeyword( @Override public Mono> searchHybrid( QueryContext context, RetrievalConfig.TierConfig tier, ScoringConfig scoring) { + var operation = searchHybridInternal(context, tier, scoring); + return observeListSearch(operation, context, tier.topK(), ItemTierDocument.HYBRID_SEARCH); + } + + private Mono> searchHybridInternal( + QueryContext context, RetrievalConfig.TierConfig tier, ScoringConfig scoring) { + RetrievalConfig tempConfig = buildTempConfig(tier, scoring); Mono> vectorMono = - searchByVector(context, tier, scoring) + searchByVectorInternal(context, tempConfig) + .map(TierResult::results) .onErrorResume( e -> { log.warn("searchHybrid vector channel failed", e); return Mono.just(List.of()); }); Mono> keywordMono = - searchByKeyword(context, tier, scoring) + searchByKeywordInternal(context, tier, scoring) .onErrorResume( e -> { log.warn("searchHybrid keyword channel failed", e); @@ -490,4 +529,13 @@ private RetrievalConfig buildTempConfig( private static String formatItemText(MemoryItem item) { return item.content(); } + + private Mono> observeListSearch( + Mono> operation, + QueryContext queryContext, + int topK, + ItemTierDocument document) { + return ItemTierRetrieverObservation.observeResults( + observationRegistry, queryContext, "item", document, topK, () -> operation); + } } diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/LlmInsightTypeRouter.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/LlmInsightTypeRouter.java index 20bb5efe..7fcfe197 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/LlmInsightTypeRouter.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/LlmInsightTypeRouter.java @@ -17,6 +17,8 @@ import com.openmemind.ai.memory.core.llm.StructuredChatClient; import com.openmemind.ai.memory.core.prompt.PromptRegistry; import com.openmemind.ai.memory.core.prompt.retrieval.InsightTypeRoutingPrompts; +import com.openmemind.ai.memory.core.retrieval.tier.observation.LlmInsightTypeRouterObservation; +import io.micrometer.observation.ObservationRegistry; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -38,6 +40,7 @@ public class LlmInsightTypeRouter implements InsightTypeRouter { private final StructuredChatClient structuredChatClient; private final PromptRegistry promptRegistry; + private final ObservationRegistry observationRegistry; public LlmInsightTypeRouter(StructuredChatClient structuredChatClient) { this(structuredChatClient, PromptRegistry.EMPTY); @@ -45,42 +48,57 @@ public LlmInsightTypeRouter(StructuredChatClient structuredChatClient) { public LlmInsightTypeRouter( StructuredChatClient structuredChatClient, PromptRegistry promptRegistry) { + this(structuredChatClient, promptRegistry, ObservationRegistry.NOOP); + } + + public LlmInsightTypeRouter( + StructuredChatClient structuredChatClient, + PromptRegistry promptRegistry, + ObservationRegistry observationRegistry) { this.structuredChatClient = Objects.requireNonNull( structuredChatClient, "structuredChatClient must not be null"); this.promptRegistry = Objects.requireNonNull(promptRegistry, "promptRegistry must not be null"); + this.observationRegistry = + observationRegistry == null ? ObservationRegistry.NOOP : observationRegistry; } @Override public Mono> route( String query, List conversationHistory, Map availableTypes) { var typeNames = new ArrayList<>(availableTypes.keySet()); - return Mono.defer( - () -> { - var promptResult = - InsightTypeRoutingPrompts.build( - promptRegistry, - query, - typeNames, - availableTypes, - conversationHistory) - .render("English"); - var messages = - ChatMessages.systemUser( - promptResult.systemPrompt(), promptResult.userPrompt()); - return structuredChatClient - .call(messages, RoutingResponse.class) - .map(response -> sanitize(response, typeNames)) - .switchIfEmpty(Mono.just(List.of())); - }) - .subscribeOn(Schedulers.boundedElastic()) - .retry(1) - .onErrorResume( - e -> { - log.warn("Insight type routing failed, fallback returns all types", e); - return Mono.just(typeNames); - }); + Mono> operation = + Mono.defer( + () -> { + var promptResult = + InsightTypeRoutingPrompts.build( + promptRegistry, + query, + typeNames, + availableTypes, + conversationHistory) + .render("English"); + var messages = + ChatMessages.systemUser( + promptResult.systemPrompt(), + promptResult.userPrompt()); + return structuredChatClient + .call(messages, RoutingResponse.class) + .map(response -> sanitize(response, typeNames)) + .switchIfEmpty(Mono.just(List.of())); + }) + .subscribeOn(Schedulers.boundedElastic()) + .retry(1) + .onErrorResume( + e -> { + log.warn( + "Insight type routing failed, fallback returns all" + + " types", + e); + return Mono.just(typeNames); + }); + return LlmInsightTypeRouterObservation.observe(observationRegistry, () -> operation); } private List sanitize(RoutingResponse response, List available) { diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/observation/InsightTierRetrieverObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/observation/InsightTierRetrieverObservation.java new file mode 100644 index 00000000..c942a4ce --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/observation/InsightTierRetrieverObservation.java @@ -0,0 +1,226 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.tier.observation; + +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; +import com.openmemind.ai.memory.core.retrieval.query.QueryContext; +import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; +import com.openmemind.ai.memory.core.retrieval.tier.TierResult; +import com.openmemind.ai.memory.core.retrieval.trace.ObservationTiming; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEvent; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEventSource; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** Observation contracts for InsightTierRetriever. */ +public final class InsightTierRetrieverObservation { + + private InsightTierRetrieverObservation() {} + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + RetrievalConfig config, + Supplier> operation) { + return observe(observationRegistry, queryContext, config, ignored -> operation.get()); + } + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + RetrievalConfig config, + Function> operation) { + return MemoryObservation.mono( + observationRegistry, + InsightTierDocument.RETRIEVE, + InsightTierConvention.INSTANCE, + reactorContext -> + new InsightTierObservationContext(queryContext, config, reactorContext), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public enum InsightTierDocument implements ObservationDocumentation { + RETRIEVE; + + @Override + public String getName() { + return "memind.retrieval.tier.insight"; + } + + @Override + public Class> + getDefaultConvention() { + return InsightTierConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }, + RESULT_COUNT { + @Override + public String asString() { + return "memind.retrieval.result_count"; + } + }, + TIER_NAME { + @Override + public String asString() { + return "memind.retrieval.tier"; + } + }, + TOP_K { + @Override + public String asString() { + return "memind.retrieval.top_k"; + } + }; + } + + public static final class InsightTierObservationContext extends MemoryObservationContext + implements RetrievalTraceEventSource { + + private final QueryContext queryContext; + private final RetrievalConfig config; + private List results = List.of(); + private int resultCount; + + public InsightTierObservationContext(QueryContext queryContext, RetrievalConfig config) { + this(queryContext, config, null); + } + + public InsightTierObservationContext( + QueryContext queryContext, RetrievalConfig config, ContextView reactorContext) { + super(reactorContext); + this.queryContext = queryContext; + this.config = config; + } + + public void recordResult(TierResult result) { + results = result == null || result.results() == null ? List.of() : result.results(); + resultCount = results.size(); + addHighCardinalityKeyValue( + HighCardinalityKeyNames.RESULT_COUNT.withValue(String.valueOf(resultCount))); + } + + public int resultCount() { + return resultCount; + } + + public String strategyName() { + return null; + } + + public String stage() { + return "tier"; + } + + public String tier() { + return "insight"; + } + + public String method() { + return "vector"; + } + + public String source() { + return "core"; + } + + @Override + public Optional toRetrievalTraceEvent( + ObservationTiming timing, RetrievalTraceOptions options) { + return Optional.of( + new RetrievalTraceEvent( + InsightTierDocument.RETRIEVE.getName(), + InsightTierDocument.RETRIEVE.getName(), + status(), + timing.startedAt(), + timing.completedAt(), + timing.durationMillis(), + Map.of("operation", "retrieval", "stage", "tier"), + Map.of( + HighCardinalityKeyNames.RESULT_COUNT.asString(), + String.valueOf(resultCount)), + new RetrievalTraceEvent.StagePayload( + "tier", + "insight", + "vector", + null, + null, + resultCount, + false, + false, + Map.of(), + RetrievalTraceEvent.candidates( + results, + options.maxCandidatesPerStage(), + options.maxTextLength())))); + } + } + + public static final class InsightTierConvention + implements ObservationConvention { + + public static final InsightTierConvention INSTANCE = new InsightTierConvention(); + + @Override + public String getName() { + return InsightTierDocument.RETRIEVE.getName(); + } + + @Override + public String getContextualName(InsightTierObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(InsightTierObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue( + context.queryContext.memoryId().toIdentifier()), + HighCardinalityKeyNames.TIER_NAME.withValue("insight"), + HighCardinalityKeyNames.TOP_K.withValue( + String.valueOf(context.config.tier1().topK()))); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof InsightTierObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/observation/ItemTierRetrieverObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/observation/ItemTierRetrieverObservation.java new file mode 100644 index 00000000..89f43ad4 --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/observation/ItemTierRetrieverObservation.java @@ -0,0 +1,316 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.tier.observation; + +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import com.openmemind.ai.memory.core.retrieval.query.QueryContext; +import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; +import com.openmemind.ai.memory.core.retrieval.tier.TierResult; +import com.openmemind.ai.memory.core.retrieval.trace.ObservationTiming; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEvent; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEventSource; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +/** Observation contracts for ItemTierRetriever. */ +public final class ItemTierRetrieverObservation { + + private ItemTierRetrieverObservation() {} + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + String tierName, + ItemTierDocument document, + int topK, + Supplier> operation) { + return observe( + observationRegistry, + queryContext, + tierName, + document, + topK, + ignored -> operation.get()); + } + + public static Mono observe( + ObservationRegistry observationRegistry, + QueryContext queryContext, + String tierName, + ItemTierDocument document, + int topK, + Function> operation) { + return MemoryObservation.mono( + observationRegistry, + document, + ItemTierConvention.of(document), + reactorContext -> + new ItemTierObservationContext( + queryContext, tierName, document, topK, reactorContext), + context -> operation.apply(context).doOnNext(context::recordResult)); + } + + public static Mono> observeResults( + ObservationRegistry observationRegistry, + QueryContext queryContext, + String tierName, + ItemTierDocument document, + int topK, + Supplier>> operation) { + return observeResults( + observationRegistry, + queryContext, + tierName, + document, + topK, + ignored -> operation.get()); + } + + public static Mono> observeResults( + ObservationRegistry observationRegistry, + QueryContext queryContext, + String tierName, + ItemTierDocument document, + int topK, + Function>> operation) { + return MemoryObservation.mono( + observationRegistry, + document, + ItemTierConvention.of(document), + reactorContext -> + new ItemTierObservationContext( + queryContext, tierName, document, topK, reactorContext), + context -> operation.apply(context).doOnNext(context::recordResults)); + } + + public enum ItemTierDocument implements ObservationDocumentation { + VECTOR_SEARCH("memind.retrieval.vector_search"), + KEYWORD_SEARCH("memind.retrieval.keyword_search"), + HYBRID_SEARCH("memind.retrieval.tier.item"); + + private final String name; + + ItemTierDocument(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public Class> + getDefaultConvention() { + return ItemTierConvention.class; + } + + @Override + public KeyName[] getHighCardinalityKeyNames() { + return HighCardinalityKeyNames.values(); + } + } + + public enum HighCardinalityKeyNames implements KeyName { + MEMORY_ID { + @Override + public String asString() { + return "memind.memory_id"; + } + }, + RESULT_COUNT { + @Override + public String asString() { + return "memind.retrieval.result_count"; + } + }, + TIER_NAME { + @Override + public String asString() { + return "memind.retrieval.tier"; + } + }, + TOP_K { + @Override + public String asString() { + return "memind.retrieval.top_k"; + } + }; + } + + public static final class ItemTierObservationContext extends MemoryObservationContext + implements RetrievalTraceEventSource { + + private final QueryContext queryContext; + private final String tierName; + private final ItemTierDocument document; + private final int topK; + private List results = List.of(); + private int resultCount; + + public ItemTierObservationContext( + QueryContext queryContext, String tierName, ItemTierDocument document, int topK) { + this(queryContext, tierName, document, topK, null); + } + + public ItemTierObservationContext( + QueryContext queryContext, + String tierName, + ItemTierDocument document, + int topK, + ContextView reactorContext) { + super(reactorContext); + this.queryContext = queryContext; + this.tierName = tierName; + this.document = document; + this.topK = topK; + } + + public void recordResult(int resultCount) { + this.resultCount = resultCount; + addHighCardinalityKeyValue( + HighCardinalityKeyNames.RESULT_COUNT.withValue(String.valueOf(resultCount))); + } + + public void recordResult(TierResult result) { + recordResults(result == null ? List.of() : result.results()); + } + + public void recordResults(List results) { + this.results = results == null ? List.of() : List.copyOf(results); + recordResult(this.results.size()); + } + + public int resultCount() { + return resultCount; + } + + public String method() { + return switch (document) { + case VECTOR_SEARCH -> "vector"; + case KEYWORD_SEARCH -> "keyword"; + case HYBRID_SEARCH -> "hybrid"; + }; + } + + public String strategyName() { + return null; + } + + public String stage() { + return "tier"; + } + + public String tier() { + return "item"; + } + + public String source() { + return "core"; + } + + @Override + public Optional toRetrievalTraceEvent( + ObservationTiming timing, RetrievalTraceOptions options) { + return Optional.of( + new RetrievalTraceEvent( + document.getName(), + document.getName(), + status(), + timing.startedAt(), + timing.completedAt(), + timing.durationMillis(), + Map.of("operation", "retrieval", "stage", "tier"), + Map.of( + HighCardinalityKeyNames.RESULT_COUNT.asString(), + String.valueOf(resultCount)), + new RetrievalTraceEvent.StagePayload( + "tier", + "item", + method(), + null, + null, + resultCount, + false, + false, + Map.of(), + RetrievalTraceEvent.candidates( + results, + options.maxCandidatesPerStage(), + options.maxTextLength())))); + } + } + + public static final class ItemTierConvention + implements ObservationConvention { + + public static final ItemTierConvention VECTOR_SEARCH = + new ItemTierConvention(ItemTierDocument.VECTOR_SEARCH); + public static final ItemTierConvention KEYWORD_SEARCH = + new ItemTierConvention(ItemTierDocument.KEYWORD_SEARCH); + public static final ItemTierConvention HYBRID_SEARCH = + new ItemTierConvention(ItemTierDocument.HYBRID_SEARCH); + + private final ItemTierDocument document; + + public ItemTierConvention(ItemTierDocument document) { + this.document = document; + } + + public static ItemTierConvention of(ItemTierDocument document) { + return switch (document) { + case VECTOR_SEARCH -> VECTOR_SEARCH; + case KEYWORD_SEARCH -> KEYWORD_SEARCH; + case HYBRID_SEARCH -> HYBRID_SEARCH; + }; + } + + @Override + public String getName() { + return document.getName(); + } + + @Override + public String getContextualName(ItemTierObservationContext context) { + return context.document.getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(ItemTierObservationContext context) { + return KeyValues.of( + HighCardinalityKeyNames.MEMORY_ID.withValue( + context.queryContext.memoryId().toIdentifier()), + HighCardinalityKeyNames.TIER_NAME.withValue(context.tierName), + HighCardinalityKeyNames.TOP_K.withValue(String.valueOf(context.topK))); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof ItemTierObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/observation/LlmInsightTypeRouterObservation.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/observation/LlmInsightTypeRouterObservation.java new file mode 100644 index 00000000..c74ff8c0 --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/tier/observation/LlmInsightTypeRouterObservation.java @@ -0,0 +1,91 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.tier.observation; + +import com.openmemind.ai.memory.core.observation.MemoryObservation; +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; + +/** Observation contracts for LlmInsightTypeRouter. */ +public final class LlmInsightTypeRouterObservation { + + private LlmInsightTypeRouterObservation() {} + + public static Mono> observe( + ObservationRegistry observationRegistry, Supplier>> operation) { + return observe(observationRegistry, ignored -> operation.get()); + } + + public static Mono> observe( + ObservationRegistry observationRegistry, + Function>> operation) { + return MemoryObservation.mono( + observationRegistry, + InsightTypeRoutingDocument.ROUTING, + InsightTypeRoutingConvention.INSTANCE, + InsightTypeRoutingObservationContext::new, + operation); + } + + public enum InsightTypeRoutingDocument implements ObservationDocumentation { + ROUTING; + + @Override + public String getName() { + return "memind.retrieval.insight_type_routing"; + } + + @Override + public Class> + getDefaultConvention() { + return InsightTypeRoutingConvention.class; + } + } + + public static final class InsightTypeRoutingObservationContext extends Observation.Context {} + + public static final class InsightTypeRoutingConvention + implements ObservationConvention { + + public static final InsightTypeRoutingConvention INSTANCE = + new InsightTypeRoutingConvention(); + + @Override + public String getName() { + return InsightTypeRoutingDocument.ROUTING.getName(); + } + + @Override + public String getContextualName(InsightTypeRoutingObservationContext context) { + return getName(); + } + + @Override + public KeyValues getHighCardinalityKeyValues(InsightTypeRoutingObservationContext context) { + return KeyValues.empty(); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof InsightTypeRoutingObservationContext; + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/BoundedRetrievalTraceCollector.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/BoundedRetrievalTraceCollector.java deleted file mode 100644 index 2bf69a14..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/BoundedRetrievalTraceCollector.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.retrieval.trace; - -import java.time.Instant; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Queue; -import java.util.UUID; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -public final class BoundedRetrievalTraceCollector implements RetrievalTraceCollector { - - private final String traceId; - private final Instant startedAt; - private final RetrievalTraceOptions options; - private final Queue stages = new ConcurrentLinkedQueue<>(); - private final AtomicBoolean truncated = new AtomicBoolean(); - private final AtomicReference merge = new AtomicReference<>(); - private final AtomicReference finalResults = new AtomicReference<>(); - - public BoundedRetrievalTraceCollector(RetrievalTraceOptions options) { - this(UUID.randomUUID().toString(), Instant.now(), options); - } - - public BoundedRetrievalTraceCollector( - String traceId, Instant startedAt, RetrievalTraceOptions options) { - this.traceId = Objects.requireNonNull(traceId, "traceId"); - this.startedAt = Objects.requireNonNull(startedAt, "startedAt"); - this.options = options == null ? RetrievalTraceOptions.defaults() : options; - } - - @Override - public void stageCompleted(RetrievalStageTrace event) { - if (event == null) { - return; - } - if (stages.size() >= options.maxStages()) { - truncated.set(true); - return; - } - stages.add(event); - } - - @Override - public void mergeCompleted(RetrievalMergeTrace event) { - if (event != null) { - merge.set(event); - } - } - - @Override - public void finalResults(RetrievalFinalTrace event) { - if (event != null) { - finalResults.set(event); - } - } - - @Override - public Optional snapshot() { - List orderedStages = - stages.stream() - .sorted(Comparator.comparing(RetrievalStageTrace::startedAt)) - .toList(); - return Optional.of( - new RetrievalDebugTrace( - traceId, - startedAt, - Instant.now(), - truncated.get(), - orderedStages, - merge.get(), - finalResults.get())); - } - - public RetrievalTraceOptions options() { - return options; - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/BoundedRetrievalTraceRecorder.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/BoundedRetrievalTraceRecorder.java new file mode 100644 index 00000000..0464db7f --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/BoundedRetrievalTraceRecorder.java @@ -0,0 +1,174 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.trace; + +import java.time.Instant; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Queue; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/** + * In-memory recorder for one retrieval response trace. + * + *

The recorder can be called from different Reactor worker threads because retrieval stages may + * run concurrently. Stage events are stored in a concurrent queue and ordered by start time only + * when the server builds the response snapshot. Merge and final result summaries are single-slot + * values because each retrieval should expose only the latest aggregate summary for those sections. + */ +public final class BoundedRetrievalTraceRecorder implements RetrievalTraceRecorder { + + private final String traceId; + private final Instant startedAt; + private final RetrievalTraceOptions options; + private final Queue stages = new ConcurrentLinkedQueue<>(); + private final AtomicInteger stageCount = new AtomicInteger(); + private final AtomicBoolean truncated = new AtomicBoolean(); + private final AtomicReference merge = new AtomicReference<>(); + private final AtomicReference finalResults = new AtomicReference<>(); + + public BoundedRetrievalTraceRecorder(RetrievalTraceOptions options) { + this(UUID.randomUUID().toString(), Instant.now(), options); + } + + public BoundedRetrievalTraceRecorder( + String traceId, Instant startedAt, RetrievalTraceOptions options) { + this.traceId = Objects.requireNonNull(traceId, "traceId"); + this.startedAt = Objects.requireNonNull(startedAt, "startedAt"); + this.options = options == null ? RetrievalTraceOptions.defaults() : options; + } + + @Override + public void record(RetrievalTraceEvent event) { + if (event == null || event.payload() == null) { + return; + } + // The recorder owns the public debug model; observations only emit normalized events. + switch (event.payload()) { + case RetrievalTraceEvent.StagePayload stage -> recordStage(toStageTrace(event, stage)); + case RetrievalTraceEvent.MergePayload merge -> recordMerge(toMergeTrace(event, merge)); + case RetrievalTraceEvent.FinalPayload finalPayload -> + recordFinal(toFinalTrace(event, finalPayload)); + } + } + + private void recordStage(RetrievalStageTrace event) { + if (event == null) { + return; + } + // The response trace is bounded so a pathological retrieval cannot create a huge payload. + if (stageCount.incrementAndGet() > options.maxStages()) { + truncated.set(true); + return; + } + stages.add(event); + } + + private void recordMerge(RetrievalMergeTrace event) { + if (event != null) { + merge.set(event); + } + } + + private void recordFinal(RetrievalFinalTrace event) { + if (event != null) { + finalResults.set(event); + } + } + + @Override + public Optional snapshot() { + // Stage observations may finish on different threads; sort once for deterministic output. + List orderedStages = + stages.stream() + .sorted(Comparator.comparing(RetrievalStageTrace::startedAt)) + .toList(); + return Optional.of( + new RetrievalDebugTrace( + traceId, + startedAt, + Instant.now(), + truncated.get(), + orderedStages, + merge.get(), + finalResults.get())); + } + + public RetrievalTraceOptions options() { + return options; + } + + private RetrievalStageTrace toStageTrace( + RetrievalTraceEvent event, RetrievalTraceEvent.StagePayload payload) { + return new RetrievalStageTrace( + payload.stage(), + payload.tier(), + payload.method(), + event.status(), + payload.inputCount(), + payload.candidateCount(), + payload.resultCount(), + payload.degraded(), + payload.skipped(), + event.startedAt(), + event.durationMillis(), + payload.attributes() == null ? Map.of() : payload.attributes(), + toCandidateTraces(payload.candidates())); + } + + private RetrievalMergeTrace toMergeTrace( + RetrievalTraceEvent event, RetrievalTraceEvent.MergePayload payload) { + return new RetrievalMergeTrace( + payload.inputCount(), + payload.outputCount(), + payload.deduplicatedCount(), + payload.sourceCount(), + event.status()); + } + + private RetrievalFinalTrace toFinalTrace( + RetrievalTraceEvent event, RetrievalTraceEvent.FinalPayload payload) { + return new RetrievalFinalTrace( + payload.strategy(), + event.status(), + payload.itemCount(), + payload.insightCount(), + payload.rawDataCount(), + payload.evidenceCount()); + } + + private List toCandidateTraces( + List candidates) { + if (candidates == null || candidates.isEmpty()) { + return List.of(); + } + return candidates.stream() + .map( + candidate -> + new RetrievalCandidateTrace( + candidate.sourceType(), + candidate.rank(), + candidate.finalScore(), + candidate.vectorScore(), + candidate.preview())) + .toList(); + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/NoopRetrievalTraceCollector.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/NoopRetrievalTraceRecorder.java similarity index 66% rename from memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/NoopRetrievalTraceCollector.java rename to memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/NoopRetrievalTraceRecorder.java index 3b9cf8b0..804e5f61 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/NoopRetrievalTraceCollector.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/NoopRetrievalTraceRecorder.java @@ -15,17 +15,18 @@ import java.util.Optional; -public enum NoopRetrievalTraceCollector implements RetrievalTraceCollector { +/** + * Disabled trace recorder. + * + *

Callers can use this singleton to avoid null checks in lower-level retrieval code. It is not + * propagated into Reactor Context, so observations do not spend time building trace events when + * tracing is disabled. + */ +public enum NoopRetrievalTraceRecorder implements RetrievalTraceRecorder { INSTANCE; @Override - public void stageCompleted(RetrievalStageTrace event) {} - - @Override - public void mergeCompleted(RetrievalMergeTrace event) {} - - @Override - public void finalResults(RetrievalFinalTrace event) {} + public void record(RetrievalTraceEvent event) {} @Override public Optional snapshot() { diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/RetrievalResultType.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/ObservationTiming.java similarity index 75% rename from memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/RetrievalResultType.java rename to memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/ObservationTiming.java index 9bfe00db..a852fd58 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/RetrievalResultType.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/ObservationTiming.java @@ -11,11 +11,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.openmemind.ai.memory.core.metrics; +package com.openmemind.ai.memory.core.retrieval.trace; -public enum RetrievalResultType { - ITEM, - INSIGHT, - RAW_DATA, - EVIDENCE -} +import java.time.Instant; + +public record ObservationTiming(Instant startedAt, Instant completedAt, long durationMillis) {} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalCandidateTrace.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalCandidateTrace.java index 5dac2bf9..5608dbe7 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalCandidateTrace.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalCandidateTrace.java @@ -13,5 +13,11 @@ */ package com.openmemind.ai.memory.core.retrieval.trace; +/** + * Bounded preview of a ranked candidate shown inside a stage trace. + * + *

The trace intentionally stores a text preview rather than the full scored result. Full result + * data remains in the normal retrieval response and source stores. + */ public record RetrievalCandidateTrace( String sourceType, int rank, Double finalScore, Float vectorScore, String textPreview) {} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalDebugTrace.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalDebugTrace.java index e141cb6a..48182981 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalDebugTrace.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalDebugTrace.java @@ -16,6 +16,12 @@ import java.time.Instant; import java.util.List; +/** + * Immutable snapshot of a retrieval trace returned to the server layer. + * + *

This is the core debug model. The server maps it to its API DTO so the core module does not + * depend on server response types. + */ public record RetrievalDebugTrace( String traceId, Instant startedAt, diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalFinalTrace.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalFinalTrace.java index dd704765..b94ac2d4 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalFinalTrace.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalFinalTrace.java @@ -13,6 +13,12 @@ */ package com.openmemind.ai.memory.core.retrieval.trace; +/** + * Final result summary for the retrieval strategy. + * + *

This mirrors the high-level response counts so trace consumers can read outcome information + * without recomputing it from the response payload. + */ public record RetrievalFinalTrace( String strategy, String status, diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalMergeTrace.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalMergeTrace.java index b1e486a2..dd1b81a0 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalMergeTrace.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalMergeTrace.java @@ -13,5 +13,11 @@ */ package com.openmemind.ai.memory.core.retrieval.trace; +/** + * Summary of the scoring merge step after retrieval stages have produced candidates. + * + *

The trace records aggregate counts only here; per-candidate details stay with the stage that + * produced or reranked them. + */ public record RetrievalMergeTrace( int inputCount, int outputCount, int deduplicatedCount, int sourceCount, String status) {} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalStageTrace.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalStageTrace.java index f6b5dd2b..94f19b67 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalStageTrace.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalStageTrace.java @@ -17,6 +17,12 @@ import java.util.List; import java.util.Map; +/** + * Debug data for one retrieval stage. + * + *

A stage can represent an item/insight/raw-data tier, graph expansion, query expansion, rerank, + * or sufficiency gate. Counts are optional because not every stage naturally has every count. + */ public record RetrievalStageTrace( String stage, String tier, diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceContext.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceContext.java index 2a943981..1fd0ea6e 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceContext.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceContext.java @@ -16,22 +16,35 @@ import reactor.util.context.Context; import reactor.util.context.ContextView; +/** + * Reactor Context access point for the request-scoped retrieval trace recorder. + * + *

The recorder is deliberately not stored in ObservationRegistry because it is mutable and + * belongs to one retrieve call. Observation contexts copy it from Reactor Context when each + * instrumented operation starts, so the ObservationHandler can find it later on stop. + */ public final class RetrievalTraceContext { - private static final Class KEY = RetrievalTraceCollector.class; + private static final Class KEY = RetrievalTraceRecorder.class; private RetrievalTraceContext() {} - public static Context withCollector(Context context, RetrievalTraceCollector collector) { - if (collector == null || collector instanceof NoopRetrievalTraceCollector) { + /** + * Attaches a real recorder to the reactive chain; no-op recorders are not propagated. + */ + public static Context withRecorder(Context context, RetrievalTraceRecorder recorder) { + if (recorder == null) { return context; } - return context.put(KEY, collector); + return context.put(KEY, recorder); } - public static RetrievalTraceCollector collector(ContextView context) { + /** + * Returns the current request recorder, or the no-op singleton when tracing is disabled. + */ + public static RetrievalTraceRecorder recorder(ContextView context) { if (context == null || !context.hasKey(KEY)) { - return NoopRetrievalTraceCollector.INSTANCE; + return NoopRetrievalTraceRecorder.INSTANCE; } return context.get(KEY); } diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceEvent.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceEvent.java new file mode 100644 index 00000000..c1820d18 --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceEvent.java @@ -0,0 +1,107 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.trace; + +import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +/** + * Normalized event emitted by a retrieval-related Observation when it stops. + * + *

Component observation contexts own the domain-specific data they collected while the + * operation was running. When Micrometer calls {@link RetrievalTraceObservationHandler#onStop}, + * those contexts convert that data into one of the payload shapes below. Keeping this event small + * avoids coupling the recorder to every retrieval component's internal result type. + */ +public record RetrievalTraceEvent( + String observationName, + String contextualName, + String status, + Instant startedAt, + Instant completedAt, + long durationMillis, + Map lowCardinality, + Map highCardinality, + Payload payload) { + + /** Marker for the three trace sections exposed by the debug response. */ + public sealed interface Payload permits StagePayload, MergePayload, FinalPayload {} + + /** + * A retrieval stage, such as an item tier, insight tier, graph expansion, rerank, or gate. + * + *

Counts describe the shape of the stage; candidates contain a bounded preview of concrete + * returned results when that stage naturally produces ranked items. + */ + public record StagePayload( + String stage, + String tier, + String method, + Integer inputCount, + Integer candidateCount, + Integer resultCount, + boolean degraded, + boolean skipped, + Map attributes, + List candidates) + implements Payload {} + + /** Summary of the merge/dedup step that combines stage outputs. */ + public record MergePayload( + int inputCount, int outputCount, int deduplicatedCount, int sourceCount) + implements Payload {} + + /** Final retrieval result summary after strategy execution completes. */ + public record FinalPayload( + String strategy, int itemCount, int insightCount, int rawDataCount, int evidenceCount) + implements Payload {} + + /** Bounded candidate preview for debug display; full item/raw data stays in the main result. */ + public record CandidatePayload( + String sourceType, int rank, Double finalScore, Float vectorScore, String preview) {} + + /** Converts ranked results into bounded candidate previews according to request trace limits. */ + public static List candidates( + List results, int maxCandidates, int maxTextLength) { + if (results == null || maxCandidates <= 0) { + return List.of(); + } + return IntStream.range(0, Math.min(maxCandidates, results.size())) + .mapToObj(index -> candidate(results.get(index), index + 1, maxTextLength)) + .toList(); + } + + private static CandidatePayload candidate(ScoredResult result, int rank, int maxTextLength) { + return new CandidatePayload( + result.sourceType() == null ? "unknown" : result.sourceType().name().toLowerCase(), + rank, + result.finalScore(), + result.vectorScore(), + preview(result.text(), maxTextLength)); + } + + private static String preview(String text, int maxTextLength) { + if (text == null || maxTextLength <= 0) { + return null; + } + String compact = text.replaceAll("\\s+", " ").trim(); + if (compact.length() <= maxTextLength) { + return compact; + } + return compact.substring(0, maxTextLength); + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceCollector.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceEventSource.java similarity index 55% rename from memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceCollector.java rename to memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceEventSource.java index 516885fc..d3620058 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceCollector.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceEventSource.java @@ -15,13 +15,15 @@ import java.util.Optional; -public interface RetrievalTraceCollector { - - void stageCompleted(RetrievalStageTrace event); - - void mergeCompleted(RetrievalMergeTrace event); - - void finalResults(RetrievalFinalTrace event); +/** + * Implemented by observation contexts that can expose retrieval debug details. + * + *

The ObservationHandler does not inspect component-specific fields directly. Instead, each + * context decides whether it has enough data to emit a trace event and how much detail to include + * based on the active {@link RetrievalTraceOptions}. + */ +public interface RetrievalTraceEventSource { - Optional snapshot(); + Optional toRetrievalTraceEvent( + ObservationTiming timing, RetrievalTraceOptions options); } diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceObservationHandler.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceObservationHandler.java new file mode 100644 index 00000000..3eb35d7d --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceObservationHandler.java @@ -0,0 +1,71 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.trace; + +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import java.time.Duration; +import java.time.Instant; + +/** + * Bridges Micrometer Observation lifecycle callbacks into retrieval debug trace events. + * + *

The handler is registered globally with the ObservationRegistry, but it only records when the + * current {@link MemoryObservationContext} carries a request-scoped {@link RetrievalTraceRecorder}. + * This keeps tracing opt-in per retrieval call while still letting every retrieval Observation use + * the normal Micrometer tap/handler path. + */ +public final class RetrievalTraceObservationHandler + implements ObservationHandler { + + private static final Object STARTED_AT = new Object(); + private static final Object START_NANOS = new Object(); + + @Override + public void onStart(MemoryObservationContext context) { + context.put(STARTED_AT, Instant.now()); + context.put(START_NANOS, System.nanoTime()); + } + + @Override + public void onStop(MemoryObservationContext context) { + RetrievalTraceRecorder recorder = context.get(RetrievalTraceRecorder.class); + if (recorder == null || !(context instanceof RetrievalTraceEventSource source)) { + return; + } + // Only contexts that explicitly know how to project themselves into trace data are + // recorded. + source.toRetrievalTraceEvent(timing(context), recorder.options()) + .ifPresent(recorder::record); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof MemoryObservationContext; + } + + private ObservationTiming timing(MemoryObservationContext context) { + Instant completedAt = Instant.now(); + Instant startedAt = context.get(STARTED_AT); + Long startNanos = context.get(START_NANOS); + if (startedAt == null || startNanos == null) { + return new ObservationTiming(completedAt, completedAt, 0L); + } + return new ObservationTiming( + startedAt, + completedAt, + Duration.ofNanos(System.nanoTime() - startNanos).toMillis()); + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceOptions.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceOptions.java index 0ba4c211..6c28477a 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceOptions.java +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceOptions.java @@ -13,6 +13,13 @@ */ package com.openmemind.ai.memory.core.retrieval.trace; +/** + * Size limits applied while producing a retrieval debug trace. + * + *

These limits protect the API response from becoming a telemetry dump. Stage count bounds the + * number of recorded observations, candidate count bounds per-stage previews, and text length + * bounds each preview string. + */ public record RetrievalTraceOptions(int maxStages, int maxCandidatesPerStage, int maxTextLength) { public static RetrievalTraceOptions defaults() { @@ -20,6 +27,7 @@ public static RetrievalTraceOptions defaults() { } public RetrievalTraceOptions { + // Keep invalid configuration safe and deterministic instead of failing the whole request. maxStages = Math.max(1, maxStages); maxCandidatesPerStage = Math.max(0, maxCandidatesPerStage); maxTextLength = Math.max(0, maxTextLength); diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceRecorder.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceRecorder.java new file mode 100644 index 00000000..1788f58e --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceRecorder.java @@ -0,0 +1,36 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.trace; + +import java.util.Optional; + +/** + * Collects normalized retrieval trace events for one retrieval execution. + * + *

Implementations should be cheap when disabled and bounded when enabled. Production code + * usually reads the recorder from {@link RetrievalTraceContext}; tests can inject their own + * recorder through Reactor Context to assert what observations emitted. + */ +public interface RetrievalTraceRecorder { + + void record(RetrievalTraceEvent event); + + /** Returns the current response snapshot, or empty when the recorder intentionally captures nothing. */ + Optional snapshot(); + + /** Limits applied by event sources before they attach candidate previews or long text. */ + default RetrievalTraceOptions options() { + return RetrievalTraceOptions.defaults(); + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceSupport.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceSupport.java deleted file mode 100644 index 56740bbc..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceSupport.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.retrieval.trace; - -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import java.time.Duration; -import java.time.Instant; -import java.util.List; -import java.util.Map; -import reactor.core.publisher.Mono; - -public final class RetrievalTraceSupport { - - private RetrievalTraceSupport() {} - - public static Mono traceStage( - Mono operation, - String stage, - String tier, - String method, - Integer inputCount, - StageResultMapper mapper) { - return Mono.deferContextual( - context -> { - RetrievalTraceCollector collector = RetrievalTraceContext.collector(context); - if (collector instanceof NoopRetrievalTraceCollector) { - return operation; - } - RetrievalTraceOptions options = - collector instanceof BoundedRetrievalTraceCollector bounded - ? bounded.options() - : RetrievalTraceOptions.defaults(); - Instant startedAt = Instant.now(); - return operation - .doOnNext( - result -> - collector.stageCompleted( - mapper.toStageTrace( - result, - stage, - tier, - method, - inputCount, - startedAt, - elapsedMillis(startedAt), - options))) - .doOnError( - ignored -> - collector.stageCompleted( - new RetrievalStageTrace( - stage, - tier, - method, - "error", - inputCount, - null, - 0, - false, - false, - startedAt, - elapsedMillis(startedAt), - Map.of(), - List.of()))); - }); - } - - public static List candidates( - List results, int maxCandidates, int maxTextLength) { - if (results == null || maxCandidates <= 0) { - return List.of(); - } - return java.util.stream.IntStream.range(0, Math.min(maxCandidates, results.size())) - .mapToObj(index -> candidate(results.get(index), index + 1, maxTextLength)) - .toList(); - } - - private static RetrievalCandidateTrace candidate( - ScoredResult result, int rank, int maxTextLength) { - return new RetrievalCandidateTrace( - result.sourceType() == null ? "unknown" : result.sourceType().name().toLowerCase(), - rank, - result.finalScore(), - result.vectorScore(), - preview(result.text(), maxTextLength)); - } - - public static long elapsedMillis(Instant startedAt) { - return Duration.between(startedAt, Instant.now()).toMillis(); - } - - public static String preview(String text, int maxTextLength) { - if (text == null || maxTextLength <= 0) { - return null; - } - String compact = text.replaceAll("\\s+", " ").trim(); - if (compact.length() <= maxTextLength) { - return compact; - } - return compact.substring(0, maxTextLength); - } - - @FunctionalInterface - public interface StageResultMapper { - RetrievalStageTrace toStageTrace( - T result, - String stage, - String tier, - String method, - Integer inputCount, - Instant startedAt, - long durationMillis, - RetrievalTraceOptions options); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/package-info.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/package-info.java new file mode 100644 index 00000000..4caa2928 --- /dev/null +++ b/memind-core/src/main/java/com/openmemind/ai/memory/core/retrieval/trace/package-info.java @@ -0,0 +1,38 @@ +/* + * 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. + */ +/** + * Request-scoped retrieval debug trace support. + * + *

The retrieval trace is a user-facing debug projection built from Micrometer Observation + * callbacks. It is intentionally separate from the normal meter/span exporters: every retrieval + * request that asks for tracing owns a {@link RetrievalTraceRecorder}, and that recorder is passed + * through Reactor Context instead of being stored in the shared ObservationRegistry. + * + *

The flow is: + * + *

    + *
  1. The server creates a bounded recorder when the request sets {@code trace=true}. + *
  2. {@link RetrievalTraceContext} puts that recorder into the reactive chain. + *
  3. Memory observation contexts copy the recorder from Reactor Context. + *
  4. {@link RetrievalTraceObservationHandler} runs when each Observation stops. + *
  5. Observation contexts that implement {@link RetrievalTraceEventSource} convert their + * component-specific result into a normalized {@link RetrievalTraceEvent}. + *
  6. The recorder aggregates those events into a {@link RetrievalDebugTrace} snapshot for the + * response body. + *
+ * + *

The objects in this package should stay bounded and serializable. They are for debugging a + * single retrieval call, not for durable telemetry storage. + */ +package com.openmemind.ai.memory.core.retrieval.trace; diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/MemoryAttributes.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/MemoryAttributes.java deleted file mode 100644 index 6a30ab71..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/MemoryAttributes.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing; - -/** - * memind span attribute key constants - * - *

Follows the OpenTelemetry semantic convention naming style: {namespace}.{name} - */ -public final class MemoryAttributes { - - private MemoryAttributes() {} - - // ===== Common ===== - public static final String MEMORY_ID = "memind.memory_id"; - public static final String OPERATION = "memind.operation"; - - // ===== Extraction ===== - public static final String EXTRACTION_LAYER = "memind.extraction.layer"; - public static final String EXTRACTION_CONTENT_TYPE = "memind.extraction.content_type"; - public static final String EXTRACTION_SEGMENT_COUNT = "memind.extraction.segment_count"; - public static final String EXTRACTION_ITEM_COUNT = "memind.extraction.item_count"; - public static final String EXTRACTION_NEW_ITEM_COUNT = "memind.extraction.new_item_count"; - public static final String EXTRACTION_REINFORCED_ITEM_COUNT = - "memind.extraction.reinforced_item_count"; - public static final String EXTRACTION_INSIGHT_COUNT = "memind.extraction.insight_count"; - public static final String EXTRACTION_MODE = "memind.extraction.mode"; - public static final String EXTRACTION_CATEGORY_COUNT = "memind.extraction.category_count"; - public static final String EXTRACTION_EXISTED = "memind.extraction.existed"; - public static final String EXTRACTION_STATUS = "memind.extraction.status"; - public static final String EXTRACTION_DURATION_MS = "memind.extraction.duration_ms"; - public static final String EXTRACTION_GRAPH_ENTITY_COUNT = - "memind.extraction.graph.entity_count"; - public static final String EXTRACTION_GRAPH_MENTION_COUNT = - "memind.extraction.graph.mention_count"; - public static final String EXTRACTION_GRAPH_STRUCTURED_LINK_COUNT = - "memind.extraction.graph.structured_link_count"; - public static final String EXTRACTION_GRAPH_TEMPORAL_SOURCE_COUNT = - "memind.extraction.graph.temporal_source_count"; - public static final String EXTRACTION_GRAPH_TEMPORAL_HISTORY_QUERY_BATCH_COUNT = - "memind.extraction.graph.temporal_history_query_batch_count"; - public static final String EXTRACTION_GRAPH_TEMPORAL_HISTORY_CANDIDATE_COUNT = - "memind.extraction.graph.temporal_history_candidate_count"; - public static final String EXTRACTION_GRAPH_TEMPORAL_INTRA_BATCH_CANDIDATE_COUNT = - "memind.extraction.graph.temporal_intra_batch_candidate_count"; - public static final String EXTRACTION_GRAPH_TEMPORAL_SELECTED_PAIR_COUNT = - "memind.extraction.graph.temporal_selected_pair_count"; - public static final String EXTRACTION_GRAPH_TEMPORAL_CREATED_LINK_COUNT = - "memind.extraction.graph.temporal_created_link_count"; - public static final String EXTRACTION_GRAPH_TEMPORAL_QUERY_DURATION_MS = - "memind.extraction.graph.temporal_query_duration_ms"; - public static final String EXTRACTION_GRAPH_TEMPORAL_BUILD_DURATION_MS = - "memind.extraction.graph.temporal_build_duration_ms"; - public static final String EXTRACTION_GRAPH_TEMPORAL_UPSERT_DURATION_MS = - "memind.extraction.graph.temporal_upsert_duration_ms"; - public static final String EXTRACTION_GRAPH_TEMPORAL_BELOW_RETRIEVAL_FLOOR_COUNT = - "memind.extraction.graph.temporal_below_retrieval_floor_count"; - public static final String EXTRACTION_GRAPH_TEMPORAL_MIN_STRENGTH = - "memind.extraction.graph.temporal_min_strength"; - public static final String EXTRACTION_GRAPH_TEMPORAL_MAX_STRENGTH = - "memind.extraction.graph.temporal_max_strength"; - public static final String EXTRACTION_GRAPH_TEMPORAL_STRENGTH_BUCKET_SUMMARY = - "memind.extraction.graph.temporal_strength_bucket_summary"; - public static final String EXTRACTION_GRAPH_TEMPORAL_DEGRADED = - "memind.extraction.graph.temporal_degraded"; - public static final String EXTRACTION_GRAPH_RESOLUTION_CANDIDATE_COUNT = - "memind.extraction.graph.resolution_candidate_count"; - public static final String EXTRACTION_GRAPH_RESOLUTION_SOURCE_DISTRIBUTION = - "memind.extraction.graph.resolution_source_distribution"; - public static final String EXTRACTION_GRAPH_RESOLUTION_SCORE_HISTOGRAM = - "memind.extraction.graph.resolution_score_histogram"; - public static final String EXTRACTION_GRAPH_RESOLUTION_CANDIDATE_REJECTED_COUNT = - "memind.extraction.graph.resolution_candidate_rejected_count"; - public static final String EXTRACTION_GRAPH_RESOLUTION_MERGE_ACCEPTED_COUNT = - "memind.extraction.graph.resolution_merge_accepted_count"; - public static final String EXTRACTION_GRAPH_RESOLUTION_MERGE_REJECTED_COUNT = - "memind.extraction.graph.resolution_merge_rejected_count"; - public static final String EXTRACTION_GRAPH_RESOLUTION_CREATE_NEW_COUNT = - "memind.extraction.graph.resolution_create_new_count"; - public static final String EXTRACTION_GRAPH_RESOLUTION_EXACT_FALLBACK_COUNT = - "memind.extraction.graph.resolution_exact_fallback_count"; - public static final String EXTRACTION_GRAPH_RESOLUTION_CANDIDATE_CAP_HIT_COUNT = - "memind.extraction.graph.resolution_candidate_cap_hit_count"; - public static final String EXTRACTION_GRAPH_ALIAS_EVIDENCE_OBSERVED_COUNT = - "memind.extraction.graph.alias_evidence_observed_count"; - public static final String EXTRACTION_GRAPH_ALIAS_EVIDENCE_MERGED_COUNT = - "memind.extraction.graph.alias_evidence_merged_count"; - public static final String EXTRACTION_GRAPH_RESOLUTION_SPECIAL_BYPASS_COUNT = - "memind.extraction.graph.resolution_special_bypass_count"; - public static final String EXTRACTION_GRAPH_SEMANTIC_SEARCH_REQUEST_COUNT = - "memind.extraction.graph.semantic_search_request_count"; - public static final String EXTRACTION_GRAPH_SEMANTIC_SEARCH_INVOCATION_COUNT = - "memind.extraction.graph.semantic_search_invocation_count"; - public static final String EXTRACTION_GRAPH_SEMANTIC_SEARCH_HIT_COUNT = - "memind.extraction.graph.semantic_search_hit_count"; - public static final String EXTRACTION_GRAPH_SEMANTIC_RESOLVED_CANDIDATE_COUNT = - "memind.extraction.graph.semantic_resolved_candidate_count"; - public static final String EXTRACTION_GRAPH_SEMANTIC_LINK_COUNT = - "memind.extraction.graph.semantic_link_count"; - public static final String EXTRACTION_GRAPH_SEMANTIC_UPSERT_BATCH_COUNT = - "memind.extraction.graph.semantic_upsert_batch_count"; - public static final String EXTRACTION_GRAPH_SEMANTIC_SOURCE_WINDOW_COUNT = - "memind.extraction.graph.semantic_source_window_count"; - public static final String EXTRACTION_GRAPH_SEMANTIC_FAILED_RESOLVE_CHUNK_COUNT = - "memind.extraction.graph.semantic_failed_resolve_chunk_count"; - public static final String EXTRACTION_GRAPH_SEMANTIC_FAILED_WINDOW_COUNT = - "memind.extraction.graph.semantic_failed_window_count"; - public static final String EXTRACTION_GRAPH_SEMANTIC_FAILED_UPSERT_BATCH_COUNT = - "memind.extraction.graph.semantic_failed_upsert_batch_count"; - public static final String EXTRACTION_GRAPH_SEMANTIC_SAME_BATCH_HIT_COUNT = - "memind.extraction.graph.semantic_same_batch_hit_count"; - public static final String EXTRACTION_GRAPH_SEMANTIC_SEARCH_FALLBACK_COUNT = - "memind.extraction.graph.semantic_search_fallback_count"; - public static final String EXTRACTION_GRAPH_SEMANTIC_INTRA_BATCH_CANDIDATE_COUNT = - "memind.extraction.graph.semantic_intra_batch_candidate_count"; - public static final String EXTRACTION_GRAPH_SEMANTIC_SEARCH_PHASE_DURATION_MS = - "memind.extraction.graph.semantic_search_phase_duration_ms"; - public static final String EXTRACTION_GRAPH_SEMANTIC_RESOLVE_PHASE_DURATION_MS = - "memind.extraction.graph.semantic_resolve_phase_duration_ms"; - public static final String EXTRACTION_GRAPH_SEMANTIC_UPSERT_PHASE_DURATION_MS = - "memind.extraction.graph.semantic_upsert_phase_duration_ms"; - public static final String EXTRACTION_GRAPH_SEMANTIC_INTRA_BATCH_PHASE_DURATION_MS = - "memind.extraction.graph.semantic_intra_batch_phase_duration_ms"; - public static final String EXTRACTION_GRAPH_SEMANTIC_DEGRADED = - "memind.extraction.graph.semantic_degraded"; - public static final String EXTRACTION_GRAPH_TYPE_FALLBACK_TO_OTHER_COUNT = - "memind.extraction.graph.type_fallback_to_other_count"; - public static final String EXTRACTION_GRAPH_TOP_UNRESOLVED_TYPE_LABELS = - "memind.extraction.graph.top_unresolved_type_labels"; - public static final String EXTRACTION_GRAPH_DROPPED_BLANK_COUNT = - "memind.extraction.graph.dropped_blank_count"; - public static final String EXTRACTION_GRAPH_DROPPED_PUNCTUATION_ONLY_COUNT = - "memind.extraction.graph.dropped_punctuation_only_count"; - public static final String EXTRACTION_GRAPH_DROPPED_PRONOUN_LIKE_COUNT = - "memind.extraction.graph.dropped_pronoun_like_count"; - public static final String EXTRACTION_GRAPH_DROPPED_TEMPORAL_COUNT = - "memind.extraction.graph.dropped_temporal_count"; - public static final String EXTRACTION_GRAPH_DROPPED_DATE_LIKE_COUNT = - "memind.extraction.graph.dropped_date_like_count"; - public static final String EXTRACTION_GRAPH_DROPPED_RESERVED_SPECIAL_COLLISION_COUNT = - "memind.extraction.graph.dropped_reserved_special_collision_count"; - public static final String EXTRACTION_GRAPH_STRUCTURED_BATCH_DEGRADED = - "memind.extraction.graph.structured_batch_degraded"; - - // ===== Extraction (Insight Tree) ===== - public static final String EXTRACTION_INSIGHT_TYPE = "memind.extraction.insight_type"; - public static final String EXTRACTION_INSIGHT_TIER = "memind.extraction.insight_tier"; - public static final String EXTRACTION_INSIGHT_GROUP_NAME = - "memind.extraction.insight_group_name"; - public static final String EXTRACTION_INSIGHT_GROUP_COUNT = - "memind.extraction.insight_group_count"; - public static final String EXTRACTION_INSIGHT_POINT_COUNT = - "memind.extraction.insight_point_count"; - public static final String EXTRACTION_INSIGHT_LEAF_COUNT = - "memind.extraction.insight_leaf_count"; - public static final String EXTRACTION_INSIGHT_ADD_COUNT = "memind.extraction.insight_add_count"; - public static final String EXTRACTION_INSIGHT_UPDATE_COUNT = - "memind.extraction.insight_update_count"; - public static final String EXTRACTION_INSIGHT_DELETE_COUNT = - "memind.extraction.insight_delete_count"; - - // ===== Retrieval ===== - public static final String RETRIEVAL_STRATEGY = "memind.retrieval.strategy"; - public static final String RETRIEVAL_QUERY = "memind.retrieval.query"; - public static final String RETRIEVAL_REWRITTEN_QUERY = "memind.retrieval.rewritten_query"; - public static final String RETRIEVAL_INTENT = "memind.retrieval.intent"; - public static final String RETRIEVAL_TIER = "memind.retrieval.sufficiency"; - public static final String RETRIEVAL_RESULT_COUNT = "memind.retrieval.result_count"; - public static final String RETRIEVAL_CANDIDATE_COUNT = "memind.retrieval.candidate_count"; - public static final String RETRIEVAL_SOURCE_LIST_COUNT = "memind.retrieval.source_list_count"; - public static final String RETRIEVAL_DEDUPED_COUNT = "memind.retrieval.deduped_count"; - public static final String RETRIEVAL_WEIGHT_COUNT = "memind.retrieval.weight_count"; - public static final String RETRIEVAL_TIER_NAME = "memind.retrieval.tier"; - public static final String RETRIEVAL_CHANNEL = "memind.retrieval.channel"; - public static final String RETRIEVAL_SUFFICIENT = "memind.retrieval.sufficient"; - public static final String RETRIEVAL_TOP_K = "memind.retrieval.top_k"; - public static final String RETRIEVAL_MEMORY_THREAD_ENABLED = - "memind.retrieval.memory_thread.enabled"; - public static final String RETRIEVAL_MEMORY_THREAD_SEED_THREAD_COUNT = - "memind.retrieval.memory_thread.seed_thread_count"; - public static final String RETRIEVAL_MEMORY_THREAD_CANDIDATE_COUNT = - "memind.retrieval.memory_thread.candidate_count"; - public static final String RETRIEVAL_MEMORY_THREAD_ADMITTED_COUNT = - "memind.retrieval.memory_thread.admitted_count"; - public static final String RETRIEVAL_MEMORY_THREAD_CLAMPED = - "memind.retrieval.memory_thread.clamped"; - public static final String RETRIEVAL_MEMORY_THREAD_DEGRADED = - "memind.retrieval.memory_thread.degraded"; - public static final String RETRIEVAL_MEMORY_THREAD_TIMEOUT = - "memind.retrieval.memory_thread.timeout"; - public static final String RETRIEVAL_GRAPH_ENABLED = "memind.retrieval.graph.enabled"; - public static final String RETRIEVAL_GRAPH_SEED_COUNT = "memind.retrieval.graph.seed_count"; - public static final String RETRIEVAL_GRAPH_LINK_EXPANSION_COUNT = - "memind.retrieval.graph.link_expansion_count"; - public static final String RETRIEVAL_GRAPH_ENTITY_EXPANSION_COUNT = - "memind.retrieval.graph.entity_expansion_count"; - public static final String RETRIEVAL_GRAPH_DEDUPED_CANDIDATE_COUNT = - "memind.retrieval.graph.deduped_candidate_count"; - public static final String RETRIEVAL_GRAPH_ADMITTED_CANDIDATE_COUNT = - "memind.retrieval.graph.admitted_candidate_count"; - public static final String RETRIEVAL_GRAPH_DISPLACED_DIRECT_COUNT = - "memind.retrieval.graph.displaced_direct_count"; - public static final String RETRIEVAL_GRAPH_OVERLAP_COUNT = - "memind.retrieval.graph.overlap_count"; - public static final String RETRIEVAL_GRAPH_SKIPPED_OVERFANOUT_ENTITY_COUNT = - "memind.retrieval.graph.skipped_overfanout_entity_count"; - public static final String RETRIEVAL_GRAPH_TIMEOUT = "memind.retrieval.graph.timeout"; - public static final String RETRIEVAL_GRAPH_DEGRADED = "memind.retrieval.graph.degraded"; - public static final String RETRIEVAL_TEMPORAL_ENABLED = "memind.retrieval.temporal.enabled"; - public static final String RETRIEVAL_TEMPORAL_CONSTRAINT_PRESENT = - "memind.retrieval.temporal.constraint_present"; - public static final String RETRIEVAL_TEMPORAL_DEGRADED = "memind.retrieval.temporal.degraded"; -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/MemoryObserver.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/MemoryObserver.java deleted file mode 100644 index 3fde0731..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/MemoryObserver.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing; - -import java.util.function.Supplier; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -/** - * Observability provider abstraction - * - *

Defines the observation wrapping capability for Mono/Flux operations. - * The default implementation is {@link NoopMemoryObserver} (zero overhead), - * specific implementations are provided by provider modules (such as OpenTelemetry). - * - *

Constraints: operation supplier must return cold Mono/Flux (lazy), cannot return already - * subscribed hot streams. - */ -public interface MemoryObserver { - - Mono observeMono(ObservationContext ctx, Supplier> operation); - - Flux observeFlux(ObservationContext ctx, Supplier> operation); -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/MemorySpanNames.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/MemorySpanNames.java deleted file mode 100644 index c268d762..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/MemorySpanNames.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing; - -/** - * memind span name constants - */ -public final class MemorySpanNames { - - private MemorySpanNames() {} - - // ===== Extraction ===== - public static final String EXTRACTION = "memind.extraction"; - public static final String EXTRACTION_RAWDATA = "memind.extraction.rawdata"; - public static final String EXTRACTION_RAWDATA_NORMALIZE = "memind.extraction.rawdata.normalize"; - public static final String EXTRACTION_RAWDATA_CHUNK = "memind.extraction.rawdata.chunk"; - public static final String EXTRACTION_RAWDATA_CAPTION = "memind.extraction.rawdata.caption"; - public static final String EXTRACTION_RAWDATA_VECTORIZE = "memind.extraction.rawdata.vectorize"; - public static final String EXTRACTION_ITEM = "memind.extraction.item"; - public static final String EXTRACTION_ITEM_EXTRACT = "memind.extraction.item.extract"; - public static final String EXTRACTION_ITEM_DEDUP = "memind.extraction.item.dedup"; - public static final String EXTRACTION_ITEM_SEMANTIC_DEDUP = - "memind.extraction.item.semantic_dedup"; - public static final String EXTRACTION_ITEM_VECTORIZE = "memind.extraction.item.vectorize"; - public static final String EXTRACTION_INSIGHT = "memind.extraction.insight"; - public static final String EXTRACTION_INSIGHT_GENERATE = "memind.extraction.insight.generate"; - - // ===== Insight Tree ===== - public static final String EXTRACTION_INSIGHT_PIPELINE = "memind.extraction.insight.pipeline"; - public static final String EXTRACTION_INSIGHT_GROUP_CLASSIFY = - "memind.extraction.insight.group.classify"; - public static final String EXTRACTION_INSIGHT_GENERATE_LEAF = - "memind.extraction.insight.generate.leaf"; - public static final String EXTRACTION_INSIGHT_GENERATE_BRANCH = - "memind.extraction.insight.generate.branch"; - public static final String EXTRACTION_INSIGHT_GENERATE_ROOT = - "memind.extraction.insight.generate.root"; - public static final String EXTRACTION_INSIGHT_TREE_REORGANIZE = - "memind.extraction.insight.tree.reorganize"; - - // ===== Retrieval ===== - public static final String RETRIEVAL = "memind.retrieval"; - public static final String RETRIEVAL_INTENT = "memind.retrieval.intent"; - public static final String RETRIEVAL_REWRITE = "memind.retrieval.rewrite"; - public static final String RETRIEVAL_STRATEGY = "memind.retrieval.strategy"; - public static final String RETRIEVAL_TIER_INSIGHT = "memind.retrieval.tier.insight"; - public static final String RETRIEVAL_TIER_ITEM = "memind.retrieval.tier.item"; - public static final String RETRIEVAL_TIER_RAWDATA = "memind.retrieval.tier.rawdata"; - public static final String RETRIEVAL_SUFFICIENCY = "memind.retrieval.sufficiency"; - public static final String RETRIEVAL_VECTOR_SEARCH = "memind.retrieval.vector_search"; - public static final String RETRIEVAL_RESULT_MERGE = "memind.retrieval.result_merge"; - public static final String RETRIEVAL_RERANK = "memind.retrieval.rerank"; - public static final String RETRIEVAL_KEYWORD_SEARCH = "memind.retrieval.keyword_search"; - public static final String RETRIEVAL_MEMORY_THREAD_ASSIST = - "memind.retrieval.memory_thread.assist"; - public static final String RETRIEVAL_GRAPH_ASSIST = "memind.retrieval.graph.assist"; - public static final String RETRIEVAL_GRAPH_CHANNEL = "memind.retrieval.channel.graph"; - public static final String RETRIEVAL_TEMPORAL_CHANNEL = "memind.retrieval.channel.temporal"; - public static final String RETRIEVAL_INSIGHT_TYPE_ROUTING = - "memind.retrieval.insight_type_routing"; - public static final String RETRIEVAL_MULTI_QUERY_EXPAND = "memind.retrieval.multi_query_expand"; - // ===== Graph ===== - public static final String GRAPH_EXTRACT_ENTITIES = "memind.graph.extract_entities"; - public static final String GRAPH_EXTRACT_EDGES = "memind.graph.extract_edges"; - public static final String GRAPH_RESOLVE_ENTITIES = "memind.graph.resolve_entities"; - public static final String GRAPH_RESOLVE_EDGES = "memind.graph.resolve_edges"; - public static final String GRAPH_MATERIALIZE = "memind.graph.materialize"; - public static final String GRAPH_SEMANTIC_LINK = "memind.graph.semantic_link"; -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/NoopMemoryObserver.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/NoopMemoryObserver.java deleted file mode 100644 index faa508ee..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/NoopMemoryObserver.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing; - -import java.util.function.Supplier; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -/** - * No-op MemoryObserver implementation, zero overhead direct delegation. - * - *

Directly call {@code op.get()} to return the original Mono/Flux, without additional wrapping defer, - * avoiding unnecessary MonoDefer/FluxDefer operator overhead on high-frequency paths. - * Lazy semantics are guaranteed by the caller (the Supplier passed in by the decorator). - */ -public final class NoopMemoryObserver implements MemoryObserver { - - @Override - public Mono observeMono(ObservationContext ctx, Supplier> op) { - return op.get(); - } - - @Override - public Flux observeFlux(ObservationContext ctx, Supplier> op) { - return op.get(); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/ObservationContext.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/ObservationContext.java deleted file mode 100644 index 1674b1fd..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/ObservationContext.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing; - -import java.util.Map; - -/** - * Observation context (immutable) - * - * @param spanName span name (must come from {@link MemorySpanNames} finite enumeration) - * @param requestAttributes request phase attributes - * @param resultExtractor result attribute extractor (filled back after span completion) - * @param operation return value type - */ -public record ObservationContext( - String spanName, - Map requestAttributes, - ResultAttributeExtractor resultExtractor) { - - public static ObservationContext of(String spanName) { - return new ObservationContext<>(spanName, Map.of(), ResultAttributeExtractor.none()); - } - - public static ObservationContext of(String spanName, Map attrs) { - return new ObservationContext<>(spanName, attrs, ResultAttributeExtractor.none()); - } - - public ObservationContext withResultExtractor(ResultAttributeExtractor extractor) { - return new ObservationContext<>(spanName, requestAttributes, extractor); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/ResultAttributeExtractor.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/ResultAttributeExtractor.java deleted file mode 100644 index a1651ce7..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/ResultAttributeExtractor.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing; - -import java.util.Map; - -/** - * Result Attribute Extractor (Generic) - * - *

Extract attributes from the return value after the operation is completed and fill them back to the span. - * The generic parameter ensures type safety in the decorator, eliminating the need for instanceof checks. - * - * @param The type of the operation return value - */ -@FunctionalInterface -public interface ResultAttributeExtractor { - - Map extract(T result); - - static ResultAttributeExtractor none() { - return r -> Map.of(); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/TracingSupport.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/TracingSupport.java deleted file mode 100644 index 1e1710ab..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/TracingSupport.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing; - -import java.util.Map; -import java.util.function.Supplier; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -/** - * Decorator base class, providing trace shortcut methods. - * - *

All Tracing decorators inherit this class, delegating business operations - * to {@link MemoryObserver} through {@link #trace} / {@link #traceFlux}. - */ -public abstract class TracingSupport { - - protected final MemoryObserver observer; - - protected TracingSupport(MemoryObserver observer) { - this.observer = observer; - } - - protected Mono trace( - String spanName, Map attrs, Supplier> operation) { - return observer.observeMono(ObservationContext.of(spanName, attrs), operation); - } - - protected Mono trace( - String spanName, - Map attrs, - ResultAttributeExtractor resultExtractor, - Supplier> operation) { - return observer.observeMono( - ObservationContext.of(spanName, attrs).withResultExtractor(resultExtractor), - operation); - } - - protected Flux traceFlux( - String spanName, Map attrs, Supplier> operation) { - return observer.observeFlux(ObservationContext.of(spanName, attrs), operation); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingGraphItemChannel.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingGraphItemChannel.java deleted file mode 100644 index ab9e2ae8..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingGraphItemChannel.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import com.openmemind.ai.memory.core.metrics.MemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.NoopMemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.RetrievalMetricsSupport; -import com.openmemind.ai.memory.core.metrics.RetrievalStageMetrics; -import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; -import com.openmemind.ai.memory.core.retrieval.graph.GraphExpansionResult; -import com.openmemind.ai.memory.core.retrieval.graph.GraphItemChannel; -import com.openmemind.ai.memory.core.retrieval.graph.RetrievalGraphSettings; -import com.openmemind.ai.memory.core.retrieval.query.QueryContext; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalStageTrace; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceSupport; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import reactor.core.publisher.Mono; - -/** Tracing decorator for graph item channel retrieval. */ -public final class TracingGraphItemChannel extends TracingSupport implements GraphItemChannel { - - private final GraphItemChannel delegate; - private final MemoryMetricsRecorder metricsRecorder; - - public TracingGraphItemChannel(GraphItemChannel delegate, MemoryObserver observer) { - this(delegate, observer, NoopMemoryMetricsRecorder.INSTANCE); - } - - public TracingGraphItemChannel( - GraphItemChannel delegate, - MemoryObserver observer, - MemoryMetricsRecorder metricsRecorder) { - super(Objects.requireNonNull(observer, "observer")); - this.delegate = Objects.requireNonNull(delegate, "delegate"); - this.metricsRecorder = - metricsRecorder == null ? NoopMemoryMetricsRecorder.INSTANCE : metricsRecorder; - } - - @Override - public Mono retrieve( - QueryContext context, - RetrievalConfig config, - RetrievalGraphSettings settings, - List seeds) { - return trace( - MemorySpanNames.RETRIEVAL_GRAPH_CHANNEL, - Map.of( - MemoryAttributes.MEMORY_ID, - context.memoryId().toIdentifier(), - MemoryAttributes.RETRIEVAL_CHANNEL, - "graph", - MemoryAttributes.RETRIEVAL_GRAPH_ENABLED, - settings != null && settings.enabled(), - MemoryAttributes.RETRIEVAL_GRAPH_SEED_COUNT, - seeds == null ? 0 : seeds.size()), - result -> - Map.of( - MemoryAttributes.RETRIEVAL_RESULT_COUNT, - result.graphItems().size(), - MemoryAttributes.RETRIEVAL_GRAPH_LINK_EXPANSION_COUNT, - result.linkExpansionCount(), - MemoryAttributes.RETRIEVAL_GRAPH_ENTITY_EXPANSION_COUNT, - result.entityExpansionCount(), - MemoryAttributes.RETRIEVAL_GRAPH_DEDUPED_CANDIDATE_COUNT, - result.dedupedCandidateCount(), - MemoryAttributes.RETRIEVAL_GRAPH_OVERLAP_COUNT, - result.overlapCount(), - MemoryAttributes.RETRIEVAL_GRAPH_SKIPPED_OVERFANOUT_ENTITY_COUNT, - result.skippedOverFanoutEntityCount(), - MemoryAttributes.RETRIEVAL_GRAPH_TIMEOUT, - result.timedOut(), - MemoryAttributes.RETRIEVAL_GRAPH_DEGRADED, - result.degraded()), - () -> - RetrievalTraceSupport.traceStage( - delegate.retrieve(context, config, settings, seeds), - "channel", - "item", - "graph", - seeds == null ? 0 : seeds.size(), - this::graphStageTrace) - .doOnNext( - result -> - recordStage( - seeds, - result == null - ? 0 - : result.dedupedCandidateCount(), - result == null - || result.graphItems() - == null - ? 0 - : result.graphItems().size(), - result != null && result.degraded(), - result == null || !result.enabled(), - result != null && result.degraded() - ? "degraded" - : "success")) - .doOnError( - ignored -> - recordStage(seeds, 0, 0, false, false, "error"))); - } - - private void recordStage( - List seeds, - int candidateCount, - int resultCount, - boolean degraded, - boolean skipped, - String status) { - RetrievalMetricsSupport.safeRecord( - () -> - metricsRecorder.recordRetrievalStage( - new RetrievalStageMetrics( - null, - "channel", - "item", - "graph", - status, - seeds == null ? 0 : seeds.size(), - candidateCount, - resultCount, - degraded, - skipped, - "core"))); - } - - private RetrievalStageTrace graphStageTrace( - GraphExpansionResult result, - String stage, - String tier, - String method, - Integer inputCount, - java.time.Instant startedAt, - long durationMillis, - RetrievalTraceOptions options) { - List results = result == null ? List.of() : result.graphItems(); - return new RetrievalStageTrace( - stage, - tier, - method, - result != null && result.degraded() ? "degraded" : "success", - inputCount, - result == null ? 0 : result.dedupedCandidateCount(), - results == null ? 0 : results.size(), - result != null && result.degraded(), - result == null || !result.enabled(), - startedAt, - durationMillis, - result == null - ? Map.of() - : Map.of( - "linkExpansionCount", - result.linkExpansionCount(), - "entityExpansionCount", - result.entityExpansionCount(), - "overlapCount", - result.overlapCount(), - "timedOut", - result.timedOut()), - RetrievalTraceSupport.candidates( - results, options.maxCandidatesPerStage(), options.maxTextLength())); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightExtractStep.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightExtractStep.java deleted file mode 100644 index 7afbc150..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightExtractStep.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import com.openmemind.ai.memory.core.data.MemoryId; -import com.openmemind.ai.memory.core.extraction.result.InsightResult; -import com.openmemind.ai.memory.core.extraction.result.MemoryItemResult; -import com.openmemind.ai.memory.core.extraction.step.InsightExtractStep; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.Map; -import reactor.core.publisher.Mono; - -/** - * A decorator that adds observability to {@link InsightExtractStep}. - */ -public class TracingInsightExtractStep extends TracingSupport implements InsightExtractStep { - - private final InsightExtractStep delegate; - - public TracingInsightExtractStep(InsightExtractStep delegate, MemoryObserver observer) { - super(observer); - this.delegate = delegate; - } - - @Override - public Mono extract(MemoryId memoryId, MemoryItemResult memoryItemResult) { - return trace( - MemorySpanNames.EXTRACTION_INSIGHT, - Map.of(MemoryAttributes.MEMORY_ID, memoryId.toIdentifier()), - () -> delegate.extract(memoryId, memoryItemResult)); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightGenerator.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightGenerator.java deleted file mode 100644 index 2156578a..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightGenerator.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.EXTRACTION_INSIGHT_ADD_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.EXTRACTION_INSIGHT_DELETE_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.EXTRACTION_INSIGHT_GROUP_NAME; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.EXTRACTION_INSIGHT_LEAF_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.EXTRACTION_INSIGHT_POINT_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.EXTRACTION_INSIGHT_TYPE; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.EXTRACTION_INSIGHT_UPDATE_COUNT; - -import com.openmemind.ai.memory.core.data.InsightPoint; -import com.openmemind.ai.memory.core.data.MemoryInsight; -import com.openmemind.ai.memory.core.data.MemoryInsightType; -import com.openmemind.ai.memory.core.data.MemoryItem; -import com.openmemind.ai.memory.core.data.PointOperation; -import com.openmemind.ai.memory.core.extraction.insight.generator.InsightGenerator; -import com.openmemind.ai.memory.core.extraction.insight.generator.InsightPointGenerateResponse; -import com.openmemind.ai.memory.core.extraction.insight.generator.InsightPointOpsResponse; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.List; -import java.util.Map; -import reactor.core.publisher.Mono; - -/** - * A decorator that adds observability to {@link InsightGenerator}. - * - *

{@link #generatePoints} wraps with a leaf span, {@link #generateBranchSummary} with a branch - * span, and {@link #generateRootSynthesis} with a root span, each recording span attributes. - */ -public class TracingInsightGenerator extends TracingSupport implements InsightGenerator { - - private final InsightGenerator delegate; - - public TracingInsightGenerator(InsightGenerator delegate, MemoryObserver observer) { - super(observer); - this.delegate = delegate; - } - - @Override - public Mono generatePoints( - MemoryInsightType insightType, - String groupName, - List existingPoints, - List newItems, - int targetTokens, - String additionalContext, - String language) { - return trace( - MemorySpanNames.EXTRACTION_INSIGHT_GENERATE_LEAF, - Map.of( - EXTRACTION_INSIGHT_TYPE, - insightType.name(), - EXTRACTION_INSIGHT_GROUP_NAME, - groupName != null ? groupName : ""), - r -> Map.of(EXTRACTION_INSIGHT_POINT_COUNT, r.points().size()), - () -> - delegate.generatePoints( - insightType, - groupName, - existingPoints, - newItems, - targetTokens, - additionalContext, - language)); - } - - @Override - public Mono generateLeafPointOps( - MemoryInsightType insightType, - String groupName, - List existingPoints, - List newItems, - int targetTokens, - String additionalContext, - String language) { - return trace( - MemorySpanNames.EXTRACTION_INSIGHT_GENERATE_LEAF, - Map.of( - EXTRACTION_INSIGHT_TYPE, - insightType.name(), - EXTRACTION_INSIGHT_GROUP_NAME, - groupName != null ? groupName : ""), - this::operationCountAttributes, - () -> - delegate.generateLeafPointOps( - insightType, - groupName, - existingPoints, - newItems, - targetTokens, - additionalContext, - language)); - } - - @Override - public Mono generateBranchSummary( - MemoryInsightType insightType, - List existingPoints, - List leafInsights, - int targetTokens, - String language) { - return trace( - MemorySpanNames.EXTRACTION_INSIGHT_GENERATE_BRANCH, - Map.of( - EXTRACTION_INSIGHT_TYPE, - insightType.name(), - EXTRACTION_INSIGHT_LEAF_COUNT, - leafInsights.size()), - r -> Map.of(EXTRACTION_INSIGHT_POINT_COUNT, r.points().size()), - () -> - delegate.generateBranchSummary( - insightType, existingPoints, leafInsights, targetTokens, language)); - } - - @Override - public Mono generateBranchSummary( - MemoryInsightType insightType, - List existingPoints, - List leafInsights, - int targetTokens, - String additionalContext, - String language) { - return trace( - MemorySpanNames.EXTRACTION_INSIGHT_GENERATE_BRANCH, - Map.of( - EXTRACTION_INSIGHT_TYPE, - insightType.name(), - EXTRACTION_INSIGHT_LEAF_COUNT, - leafInsights.size()), - r -> Map.of(EXTRACTION_INSIGHT_POINT_COUNT, r.points().size()), - () -> - delegate.generateBranchSummary( - insightType, - existingPoints, - leafInsights, - targetTokens, - additionalContext, - language)); - } - - @Override - public Mono generateBranchPointOps( - MemoryInsightType insightType, - List existingPoints, - List leafInsights, - int targetTokens, - String language) { - return trace( - MemorySpanNames.EXTRACTION_INSIGHT_GENERATE_BRANCH, - Map.of( - EXTRACTION_INSIGHT_TYPE, - insightType.name(), - EXTRACTION_INSIGHT_LEAF_COUNT, - leafInsights.size()), - this::operationCountAttributes, - () -> - delegate.generateBranchPointOps( - insightType, existingPoints, leafInsights, targetTokens, language)); - } - - @Override - public Mono generateBranchPointOps( - MemoryInsightType insightType, - List existingPoints, - List leafInsights, - int targetTokens, - String additionalContext, - String language) { - return trace( - MemorySpanNames.EXTRACTION_INSIGHT_GENERATE_BRANCH, - Map.of( - EXTRACTION_INSIGHT_TYPE, - insightType.name(), - EXTRACTION_INSIGHT_LEAF_COUNT, - leafInsights.size()), - this::operationCountAttributes, - () -> - delegate.generateBranchPointOps( - insightType, - existingPoints, - leafInsights, - targetTokens, - additionalContext, - language)); - } - - @Override - public Mono generateRootSynthesis( - MemoryInsightType rootInsightType, - List existingPoints, - List branchInsights, - int targetTokens, - String language) { - return trace( - MemorySpanNames.EXTRACTION_INSIGHT_GENERATE_ROOT, - Map.of( - EXTRACTION_INSIGHT_TYPE, - rootInsightType.name(), - EXTRACTION_INSIGHT_LEAF_COUNT, - branchInsights.size()), - r -> Map.of(EXTRACTION_INSIGHT_POINT_COUNT, r.points().size()), - () -> - delegate.generateRootSynthesis( - rootInsightType, - existingPoints, - branchInsights, - targetTokens, - language)); - } - - @Override - public Mono generateRootSynthesis( - MemoryInsightType rootInsightType, - List existingPoints, - List branchInsights, - int targetTokens, - String additionalContext, - String language) { - return trace( - MemorySpanNames.EXTRACTION_INSIGHT_GENERATE_ROOT, - Map.of( - EXTRACTION_INSIGHT_TYPE, - rootInsightType.name(), - EXTRACTION_INSIGHT_LEAF_COUNT, - branchInsights.size()), - r -> Map.of(EXTRACTION_INSIGHT_POINT_COUNT, r.points().size()), - () -> - delegate.generateRootSynthesis( - rootInsightType, - existingPoints, - branchInsights, - targetTokens, - additionalContext, - language)); - } - - private Map operationCountAttributes(InsightPointOpsResponse response) { - var operations = response != null ? response.operations() : List.of(); - return Map.of( - EXTRACTION_INSIGHT_ADD_COUNT, - countOperations(operations, PointOperation.OpType.ADD), - EXTRACTION_INSIGHT_UPDATE_COUNT, - countOperations(operations, PointOperation.OpType.UPDATE), - EXTRACTION_INSIGHT_DELETE_COUNT, - countOperations(operations, PointOperation.OpType.DELETE)); - } - - private int countOperations(List operations, PointOperation.OpType opType) { - return (int) operations.stream().filter(operation -> operation.op() == opType).count(); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightGroupClassifier.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightGroupClassifier.java deleted file mode 100644 index 249ffaee..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightGroupClassifier.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import com.openmemind.ai.memory.core.data.MemoryInsightType; -import com.openmemind.ai.memory.core.data.MemoryItem; -import com.openmemind.ai.memory.core.extraction.insight.group.InsightGroupClassifier; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.List; -import java.util.Map; -import reactor.core.publisher.Mono; - -/** - * A decorator that adds observability to {@link InsightGroupClassifier}. - * - *

Records the insight type and item count as request attributes, and the group count as a result - * attribute. - */ -public class TracingInsightGroupClassifier extends TracingSupport - implements InsightGroupClassifier { - - private final InsightGroupClassifier delegate; - - public TracingInsightGroupClassifier(InsightGroupClassifier delegate, MemoryObserver observer) { - super(observer); - this.delegate = delegate; - } - - @Override - public Mono>> classify( - MemoryInsightType insightType, - List items, - List existingGroupNames) { - return trace( - MemorySpanNames.EXTRACTION_INSIGHT_GROUP_CLASSIFY, - Map.of( - MemoryAttributes.EXTRACTION_INSIGHT_TYPE, - insightType.name(), - MemoryAttributes.EXTRACTION_ITEM_COUNT, - items.size()), - r -> Map.of(MemoryAttributes.EXTRACTION_INSIGHT_GROUP_COUNT, r.size()), - () -> delegate.classify(insightType, items, existingGroupNames)); - } - - @Override - public Mono>> classify( - MemoryInsightType insightType, - List items, - List existingGroupNames, - String additionalContext, - String language) { - return trace( - MemorySpanNames.EXTRACTION_INSIGHT_GROUP_CLASSIFY, - Map.of( - MemoryAttributes.EXTRACTION_INSIGHT_TYPE, - insightType.name(), - MemoryAttributes.EXTRACTION_ITEM_COUNT, - items.size()), - r -> Map.of(MemoryAttributes.EXTRACTION_INSIGHT_GROUP_COUNT, r.size()), - () -> - delegate.classify( - insightType, - items, - existingGroupNames, - additionalContext, - language)); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightTierRetriever.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightTierRetriever.java deleted file mode 100644 index e94c9812..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightTierRetriever.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.MEMORY_ID; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_RESULT_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_TIER_NAME; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_TOP_K; - -import com.openmemind.ai.memory.core.metrics.MemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.NoopMemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.RetrievalMetricsSupport; -import com.openmemind.ai.memory.core.metrics.RetrievalStageMetrics; -import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; -import com.openmemind.ai.memory.core.retrieval.query.QueryContext; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.tier.InsightTierSearch; -import com.openmemind.ai.memory.core.retrieval.tier.TierResult; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalStageTrace; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceSupport; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.List; -import java.util.Map; -import reactor.core.publisher.Mono; - -/** Tracing decorator for insight tier retrieval. */ -public class TracingInsightTierRetriever extends TracingSupport implements InsightTierSearch { - - private final InsightTierSearch delegate; - private final MemoryMetricsRecorder metricsRecorder; - - public TracingInsightTierRetriever(InsightTierSearch delegate, MemoryObserver observer) { - this(delegate, observer, NoopMemoryMetricsRecorder.INSTANCE); - } - - public TracingInsightTierRetriever( - InsightTierSearch delegate, - MemoryObserver observer, - MemoryMetricsRecorder metricsRecorder) { - super(observer); - this.delegate = delegate; - this.metricsRecorder = - metricsRecorder == null ? NoopMemoryMetricsRecorder.INSTANCE : metricsRecorder; - } - - @Override - public Mono retrieve(QueryContext context, RetrievalConfig config) { - return trace( - MemorySpanNames.RETRIEVAL_TIER_INSIGHT, - Map.of( - MEMORY_ID, - context.memoryId().toIdentifier(), - RETRIEVAL_TIER_NAME, - "insight", - RETRIEVAL_TOP_K, - config.tier1().topK()), - result -> Map.of(RETRIEVAL_RESULT_COUNT, result.results().size()), - () -> - RetrievalTraceSupport.traceStage( - delegate.retrieve(context, config), - "tier", - "insight", - "vector", - null, - this::tierStageTrace) - .doOnNext( - result -> - recordStage( - result == null || result.results() == null - ? 0 - : result.results().size(), - "success")) - .doOnError(ignored -> recordStage(0, "error"))); - } - - private void recordStage(int resultCount, String status) { - RetrievalMetricsSupport.safeRecord( - () -> - metricsRecorder.recordRetrievalStage( - new RetrievalStageMetrics( - null, - "tier", - "insight", - "vector", - status, - null, - null, - resultCount, - false, - false, - "core"))); - } - - private RetrievalStageTrace tierStageTrace( - TierResult result, - String stage, - String tier, - String method, - Integer inputCount, - java.time.Instant startedAt, - long durationMillis, - RetrievalTraceOptions options) { - List results = result == null ? List.of() : result.results(); - return new RetrievalStageTrace( - stage, - tier, - method, - "success", - inputCount, - null, - results == null ? 0 : results.size(), - false, - false, - startedAt, - durationMillis, - Map.of(), - RetrievalTraceSupport.candidates( - results, options.maxCandidatesPerStage(), options.maxTextLength())); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightTypeRouter.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightTypeRouter.java deleted file mode 100644 index 187912c0..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightTypeRouter.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import com.openmemind.ai.memory.core.retrieval.tier.InsightTypeRouter; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.List; -import java.util.Map; -import reactor.core.publisher.Mono; - -/** - * A decorator that adds observability to {@link InsightTypeRouter}. - */ -public class TracingInsightTypeRouter extends TracingSupport implements InsightTypeRouter { - - private final InsightTypeRouter delegate; - - public TracingInsightTypeRouter(InsightTypeRouter delegate, MemoryObserver observer) { - super(observer); - this.delegate = delegate; - } - - @Override - public Mono> route( - String query, List conversationHistory, Map availableTypes) { - return trace( - MemorySpanNames.RETRIEVAL_INSIGHT_TYPE_ROUTING, - Map.of(), - () -> delegate.route(query, conversationHistory, availableTypes)); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingItemGraphMaterializer.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingItemGraphMaterializer.java deleted file mode 100644 index 89c81186..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingItemGraphMaterializer.java +++ /dev/null @@ -1,267 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import com.openmemind.ai.memory.core.data.MemoryId; -import com.openmemind.ai.memory.core.data.MemoryItem; -import com.openmemind.ai.memory.core.extraction.item.graph.ItemGraphMaterializationResult; -import com.openmemind.ai.memory.core.extraction.item.graph.ItemGraphMaterializer; -import com.openmemind.ai.memory.core.extraction.item.support.ExtractedMemoryEntry; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import reactor.core.publisher.Mono; - -/** - * Tracing decorator for post-commit graph materialization. - */ -public final class TracingItemGraphMaterializer extends TracingSupport - implements ItemGraphMaterializer { - - private final ItemGraphMaterializer delegate; - - public TracingItemGraphMaterializer(ItemGraphMaterializer delegate, MemoryObserver observer) { - super(observer); - this.delegate = Objects.requireNonNull(delegate, "delegate"); - } - - @Override - public Mono materialize( - MemoryId memoryId, List items, List sourceEntries) { - return trace( - MemorySpanNames.GRAPH_MATERIALIZE, - Map.of( - MemoryAttributes.MEMORY_ID, - memoryId.toIdentifier(), - MemoryAttributes.EXTRACTION_ITEM_COUNT, - items != null ? items.size() : 0), - result -> - Map.ofEntries( - Map.entry( - MemoryAttributes.EXTRACTION_GRAPH_ENTITY_COUNT, - result.stats().entityCount()), - Map.entry( - MemoryAttributes.EXTRACTION_GRAPH_MENTION_COUNT, - result.stats().mentionCount()), - Map.entry( - MemoryAttributes.EXTRACTION_GRAPH_STRUCTURED_LINK_COUNT, - result.stats().structuredItemLinkCount()), - Map.entry( - MemoryAttributes.EXTRACTION_GRAPH_TEMPORAL_SOURCE_COUNT, - result.stats().temporalSourceCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_TEMPORAL_HISTORY_QUERY_BATCH_COUNT, - result.stats().temporalHistoryQueryBatchCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_TEMPORAL_HISTORY_CANDIDATE_COUNT, - result.stats().temporalHistoryCandidateCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_TEMPORAL_INTRA_BATCH_CANDIDATE_COUNT, - result.stats().temporalIntraBatchCandidateCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_TEMPORAL_SELECTED_PAIR_COUNT, - result.stats().temporalSelectedPairCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_TEMPORAL_CREATED_LINK_COUNT, - result.stats().temporalCreatedLinkCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_TEMPORAL_QUERY_DURATION_MS, - result.stats().temporalQueryDurationMs()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_TEMPORAL_BUILD_DURATION_MS, - result.stats().temporalBuildDurationMs()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_TEMPORAL_UPSERT_DURATION_MS, - result.stats().temporalUpsertDurationMs()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_TEMPORAL_BELOW_RETRIEVAL_FLOOR_COUNT, - result.stats().temporalBelowRetrievalFloorCount()), - Map.entry( - MemoryAttributes.EXTRACTION_GRAPH_TEMPORAL_MIN_STRENGTH, - result.stats().temporalMinStrength()), - Map.entry( - MemoryAttributes.EXTRACTION_GRAPH_TEMPORAL_MAX_STRENGTH, - result.stats().temporalMaxStrength()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_TEMPORAL_STRENGTH_BUCKET_SUMMARY, - result.stats().temporalStrengthBucketSummary()), - Map.entry( - MemoryAttributes.EXTRACTION_GRAPH_TEMPORAL_DEGRADED, - result.stats().temporalDegraded()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_RESOLUTION_CANDIDATE_COUNT, - result.stats().resolutionCandidateCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_RESOLUTION_SOURCE_DISTRIBUTION, - result.stats().resolutionCandidateSourceSummary()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_RESOLUTION_SCORE_HISTOGRAM, - result.stats().resolutionMergeScoreHistogramSummary()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_RESOLUTION_CANDIDATE_REJECTED_COUNT, - result.stats().resolutionCandidateRejectedCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_RESOLUTION_MERGE_ACCEPTED_COUNT, - result.stats().resolutionMergeAcceptedCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_RESOLUTION_MERGE_REJECTED_COUNT, - result.stats().resolutionMergeRejectedCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_RESOLUTION_CREATE_NEW_COUNT, - result.stats().resolutionCreateNewCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_RESOLUTION_EXACT_FALLBACK_COUNT, - result.stats().resolutionExactFallbackCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_RESOLUTION_CANDIDATE_CAP_HIT_COUNT, - result.stats().resolutionCandidateCapHitCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_ALIAS_EVIDENCE_OBSERVED_COUNT, - result.stats().aliasEvidenceObservedCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_ALIAS_EVIDENCE_MERGED_COUNT, - result.stats().aliasEvidenceMergedCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_RESOLUTION_SPECIAL_BYPASS_COUNT, - result.stats().resolutionSpecialBypassCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_SEMANTIC_SEARCH_REQUEST_COUNT, - result.stats().semanticSearchRequestCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_SEMANTIC_SEARCH_INVOCATION_COUNT, - result.stats().semanticSearchInvocationCount()), - Map.entry( - MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_SEARCH_HIT_COUNT, - result.stats().semanticSearchHitCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_SEMANTIC_RESOLVED_CANDIDATE_COUNT, - result.stats().semanticResolvedCandidateCount()), - Map.entry( - MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_LINK_COUNT, - result.stats().semanticLinkCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_SEMANTIC_UPSERT_BATCH_COUNT, - result.stats().semanticUpsertBatchCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_SEMANTIC_SOURCE_WINDOW_COUNT, - result.stats().semanticSourceWindowCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_SEMANTIC_FAILED_RESOLVE_CHUNK_COUNT, - result.stats().semanticFailedResolveChunkCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_SEMANTIC_FAILED_WINDOW_COUNT, - result.stats().semanticFailedWindowCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_SEMANTIC_FAILED_UPSERT_BATCH_COUNT, - result.stats().semanticFailedUpsertBatchCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_SEMANTIC_SAME_BATCH_HIT_COUNT, - result.stats().semanticSameBatchHitCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_SEMANTIC_SEARCH_FALLBACK_COUNT, - result.stats().semanticSearchFallbackCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_SEMANTIC_INTRA_BATCH_CANDIDATE_COUNT, - result.stats().semanticIntraBatchCandidateCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_SEMANTIC_SEARCH_PHASE_DURATION_MS, - result.stats().semanticSearchPhaseDurationMs()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_SEMANTIC_RESOLVE_PHASE_DURATION_MS, - result.stats().semanticResolvePhaseDurationMs()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_SEMANTIC_UPSERT_PHASE_DURATION_MS, - result.stats().semanticUpsertPhaseDurationMs()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_SEMANTIC_INTRA_BATCH_PHASE_DURATION_MS, - result.stats().semanticIntraBatchPhaseDurationMs()), - Map.entry( - MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_DEGRADED, - result.stats().semanticDegraded()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_TYPE_FALLBACK_TO_OTHER_COUNT, - result.stats().typeFallbackToOtherCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_TOP_UNRESOLVED_TYPE_LABELS, - result.stats().topUnresolvedTypeLabelsSummary()), - Map.entry( - MemoryAttributes.EXTRACTION_GRAPH_DROPPED_BLANK_COUNT, - result.stats().droppedBlankCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_DROPPED_PUNCTUATION_ONLY_COUNT, - result.stats().droppedPunctuationOnlyCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_DROPPED_PRONOUN_LIKE_COUNT, - result.stats().droppedPronounLikeCount()), - Map.entry( - MemoryAttributes.EXTRACTION_GRAPH_DROPPED_TEMPORAL_COUNT, - result.stats().droppedTemporalCount()), - Map.entry( - MemoryAttributes.EXTRACTION_GRAPH_DROPPED_DATE_LIKE_COUNT, - result.stats().droppedDateLikeCount()), - Map.entry( - MemoryAttributes - .EXTRACTION_GRAPH_DROPPED_RESERVED_SPECIAL_COLLISION_COUNT, - result.stats().droppedReservedSpecialCollisionCount()), - Map.entry( - MemoryAttributes.EXTRACTION_GRAPH_STRUCTURED_BATCH_DEGRADED, - result.stats().structuredBatchDegraded())), - () -> delegate.materialize(memoryId, items, sourceEntries)); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingItemTierRetriever.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingItemTierRetriever.java deleted file mode 100644 index f32bcd15..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingItemTierRetriever.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.MEMORY_ID; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_RESULT_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_TIER_NAME; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_TOP_K; - -import com.openmemind.ai.memory.core.metrics.MemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.NoopMemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.RetrievalMetricsSupport; -import com.openmemind.ai.memory.core.metrics.RetrievalStageMetrics; -import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; -import com.openmemind.ai.memory.core.retrieval.query.QueryContext; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoringConfig; -import com.openmemind.ai.memory.core.retrieval.tier.ItemTierSearch; -import com.openmemind.ai.memory.core.retrieval.tier.TierResult; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalStageTrace; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceSupport; -import com.openmemind.ai.memory.core.textsearch.MemoryTextSearch; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.List; -import java.util.Map; -import reactor.core.publisher.Mono; - -/** Tracing decorator for item tier retrieval operations. */ -public class TracingItemTierRetriever extends TracingSupport implements ItemTierSearch { - - private final ItemTierSearch delegate; - private final MemoryMetricsRecorder metricsRecorder; - - public TracingItemTierRetriever(ItemTierSearch delegate, MemoryObserver observer) { - this(delegate, observer, NoopMemoryMetricsRecorder.INSTANCE); - } - - public TracingItemTierRetriever( - ItemTierSearch delegate, - MemoryObserver observer, - MemoryMetricsRecorder metricsRecorder) { - super(observer); - this.delegate = delegate; - this.metricsRecorder = - metricsRecorder == null ? NoopMemoryMetricsRecorder.INSTANCE : metricsRecorder; - } - - @Override - public Mono searchByVector(QueryContext context, RetrievalConfig config) { - return trace( - MemorySpanNames.RETRIEVAL_VECTOR_SEARCH, - Map.of( - MEMORY_ID, - context.memoryId().toIdentifier(), - RETRIEVAL_TIER_NAME, - "item", - RETRIEVAL_TOP_K, - config.tier2().topK()), - result -> Map.of(RETRIEVAL_RESULT_COUNT, result.results().size()), - () -> - RetrievalTraceSupport.traceStage( - delegate.searchByVector(context, config), - "tier", - "item", - "vector", - null, - this::tierStageTrace) - .doOnNext( - result -> - recordStage( - "tier", - "vector", - null, - result.results().size(), - false, - false, - "success")) - .doOnError( - ignored -> - recordStage( - "tier", "vector", null, 0, false, false, - "error"))); - } - - @Override - public MemoryTextSearch textSearch() { - return delegate.textSearch(); - } - - @Override - public Mono> searchByVector( - QueryContext context, RetrievalConfig.TierConfig tier, ScoringConfig scoring) { - return trace( - MemorySpanNames.RETRIEVAL_VECTOR_SEARCH, - Map.of( - MEMORY_ID, - context.memoryId().toIdentifier(), - RETRIEVAL_TIER_NAME, - "item", - RETRIEVAL_TOP_K, - tier.topK()), - result -> Map.of(RETRIEVAL_RESULT_COUNT, result.size()), - () -> - recordListStage( - delegate.searchByVector(context, tier, scoring), - context, - "vector")); - } - - @Override - public Mono> searchByKeyword( - QueryContext context, RetrievalConfig.TierConfig tier, ScoringConfig scoring) { - return trace( - MemorySpanNames.RETRIEVAL_KEYWORD_SEARCH, - Map.of( - MEMORY_ID, - context.memoryId().toIdentifier(), - RETRIEVAL_TIER_NAME, - "item", - RETRIEVAL_TOP_K, - tier.topK()), - result -> Map.of(RETRIEVAL_RESULT_COUNT, result.size()), - () -> - recordListStage( - delegate.searchByKeyword(context, tier, scoring), - context, - "keyword")); - } - - @Override - public Mono> searchHybrid( - QueryContext context, RetrievalConfig.TierConfig tier, ScoringConfig scoring) { - return trace( - MemorySpanNames.RETRIEVAL_TIER_ITEM, - Map.of( - MEMORY_ID, - context.memoryId().toIdentifier(), - RETRIEVAL_TIER_NAME, - "item", - RETRIEVAL_TOP_K, - tier.topK()), - result -> Map.of(RETRIEVAL_RESULT_COUNT, result.size()), - () -> - recordListStage( - delegate.searchHybrid(context, tier, scoring), context, "hybrid")); - } - - private Mono> recordListStage( - Mono> operation, QueryContext context, String method) { - return RetrievalTraceSupport.traceStage( - operation, "tier", "item", method, null, this::listStageTrace) - .doOnNext( - result -> - recordStage( - "tier", - method, - null, - result == null ? 0 : result.size(), - false, - false, - "success")) - .doOnError(ignored -> recordStage("tier", method, null, 0, false, false, "error")); - } - - private void recordStage( - String stage, - String method, - Integer candidateCount, - Integer resultCount, - boolean degraded, - boolean skipped, - String status) { - RetrievalMetricsSupport.safeRecord( - () -> - metricsRecorder.recordRetrievalStage( - new RetrievalStageMetrics( - null, - stage, - "item", - method, - status, - null, - candidateCount, - resultCount, - degraded, - skipped, - "core"))); - } - - private RetrievalStageTrace tierStageTrace( - TierResult result, - String stage, - String tier, - String method, - Integer inputCount, - java.time.Instant startedAt, - long durationMillis, - RetrievalTraceOptions options) { - List results = result == null ? List.of() : result.results(); - return new RetrievalStageTrace( - stage, - tier, - method, - "success", - inputCount, - null, - results == null ? 0 : results.size(), - false, - false, - startedAt, - durationMillis, - Map.of(), - RetrievalTraceSupport.candidates( - results, options.maxCandidatesPerStage(), options.maxTextLength())); - } - - private RetrievalStageTrace listStageTrace( - List result, - String stage, - String tier, - String method, - Integer inputCount, - java.time.Instant startedAt, - long durationMillis, - RetrievalTraceOptions options) { - return new RetrievalStageTrace( - stage, - tier, - method, - "success", - inputCount, - null, - result == null ? 0 : result.size(), - false, - false, - startedAt, - durationMillis, - Map.of(), - RetrievalTraceSupport.candidates( - result, options.maxCandidatesPerStage(), options.maxTextLength())); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryExtractor.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryExtractor.java deleted file mode 100644 index 0506d067..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryExtractor.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import com.openmemind.ai.memory.core.data.MemoryId; -import com.openmemind.ai.memory.core.extraction.ExtractionConfig; -import com.openmemind.ai.memory.core.extraction.ExtractionRequest; -import com.openmemind.ai.memory.core.extraction.ExtractionResult; -import com.openmemind.ai.memory.core.extraction.MemoryExtractor; -import com.openmemind.ai.memory.core.extraction.rawdata.content.conversation.message.Message; -import com.openmemind.ai.memory.core.metrics.ExtractionMetrics; -import com.openmemind.ai.memory.core.metrics.ExtractionMetricsExtractor; -import com.openmemind.ai.memory.core.metrics.MemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.NoopMemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.RetrievalMetricsSupport; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.Map; -import reactor.core.publisher.Mono; - -/** - * A decorator that adds observability to {@link MemoryExtractor}. - * - *

All extraction methods are wrapped by the observer to record spans and attributes. - */ -public class TracingMemoryExtractor extends TracingSupport implements MemoryExtractor { - - private final MemoryExtractor delegate; - private final MemoryMetricsRecorder metricsRecorder; - - public TracingMemoryExtractor(MemoryExtractor delegate, MemoryObserver observer) { - this(delegate, observer, NoopMemoryMetricsRecorder.INSTANCE); - } - - public TracingMemoryExtractor( - MemoryExtractor delegate, - MemoryObserver observer, - MemoryMetricsRecorder metricsRecorder) { - super(observer); - this.delegate = delegate; - this.metricsRecorder = - metricsRecorder == null ? NoopMemoryMetricsRecorder.INSTANCE : metricsRecorder; - } - - @Override - public Mono extract(ExtractionRequest request) { - return trace( - MemorySpanNames.EXTRACTION, - Map.of(MemoryAttributes.MEMORY_ID, request.memoryId().toIdentifier()), - () -> recordExtraction(delegate.extract(request))); - } - - @Override - public Mono addMessage( - MemoryId memoryId, Message message, ExtractionConfig config) { - return trace( - MemorySpanNames.EXTRACTION, - Map.of(MemoryAttributes.MEMORY_ID, memoryId.toIdentifier()), - () -> recordExtraction(delegate.addMessage(memoryId, message, config))); - } - - private Mono recordExtraction(Mono operation) { - return operation - .doOnNext( - result -> - RetrievalMetricsSupport.safeRecord( - () -> - metricsRecorder.recordExtractionSummary( - ExtractionMetricsExtractor.extract( - result, "core")))) - .doOnError( - ignored -> - RetrievalMetricsSupport.safeRecord( - () -> - metricsRecorder.recordExtractionSummary( - new ExtractionMetrics( - "error", 0, null, 0, null, 0, null, - null, null, "core")))); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryItemDeduplicator.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryItemDeduplicator.java deleted file mode 100644 index 77619fba..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryItemDeduplicator.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import com.openmemind.ai.memory.core.data.MemoryId; -import com.openmemind.ai.memory.core.extraction.item.dedup.DeduplicationResult; -import com.openmemind.ai.memory.core.extraction.item.dedup.MemoryItemDeduplicator; -import com.openmemind.ai.memory.core.extraction.item.support.ExtractedMemoryEntry; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.List; -import java.util.Map; -import reactor.core.publisher.Mono; - -/** - * A decorator that adds observability to {@link MemoryItemDeduplicator}. - * - *

Uses the delegate's spanName as the span name. - */ -public class TracingMemoryItemDeduplicator extends TracingSupport - implements MemoryItemDeduplicator { - - private final MemoryItemDeduplicator delegate; - - public TracingMemoryItemDeduplicator(MemoryItemDeduplicator delegate, MemoryObserver observer) { - super(observer); - this.delegate = delegate; - } - - @Override - public Mono deduplicate( - MemoryId memoryId, List entries) { - return trace( - delegate.spanName(), - Map.of(MemoryAttributes.MEMORY_ID, memoryId.toIdentifier()), - () -> delegate.deduplicate(memoryId, entries)); - } - - @Override - public String spanName() { - return delegate.spanName(); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryItemExtractStep.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryItemExtractStep.java deleted file mode 100644 index 4ee9b56e..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryItemExtractStep.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import com.openmemind.ai.memory.core.data.MemoryId; -import com.openmemind.ai.memory.core.extraction.item.ItemExtractionConfig; -import com.openmemind.ai.memory.core.extraction.result.MemoryItemResult; -import com.openmemind.ai.memory.core.extraction.result.RawDataResult; -import com.openmemind.ai.memory.core.extraction.step.MemoryItemExtractStep; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.Map; -import reactor.core.publisher.Mono; - -/** - * A decorator that adds observability to {@link MemoryItemExtractStep}. - * - *

Records the number of items and the number of new items as result attributes. - */ -public class TracingMemoryItemExtractStep extends TracingSupport implements MemoryItemExtractStep { - - private final MemoryItemExtractStep delegate; - - public TracingMemoryItemExtractStep(MemoryItemExtractStep delegate, MemoryObserver observer) { - super(observer); - this.delegate = delegate; - } - - @Override - public Mono extract( - MemoryId memoryId, RawDataResult rawDataResult, ItemExtractionConfig config) { - return trace( - MemorySpanNames.EXTRACTION_ITEM, - Map.of(MemoryAttributes.MEMORY_ID, memoryId.toIdentifier()), - r -> - Map.of( - MemoryAttributes.EXTRACTION_ITEM_COUNT, - r.newCount(), - MemoryAttributes.EXTRACTION_NEW_ITEM_COUNT, - r.newItems().size()), - () -> delegate.extract(memoryId, rawDataResult, config)); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryRetriever.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryRetriever.java deleted file mode 100644 index 9ad5f26f..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryRetriever.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.MEMORY_ID; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_RESULT_COUNT; - -import com.openmemind.ai.memory.core.data.MemoryId; -import com.openmemind.ai.memory.core.retrieval.MemoryRetriever; -import com.openmemind.ai.memory.core.retrieval.RetrievalRequest; -import com.openmemind.ai.memory.core.retrieval.RetrievalResult; -import com.openmemind.ai.memory.core.retrieval.strategy.RetrievalStrategy; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.Map; -import reactor.core.publisher.Mono; - -/** - * A decorator that adds observability to {@link MemoryRetriever}. - * - *

{@link #retrieve} wraps with observer, recording span and attributes; - * {@link #registerStrategy} and {@link #onDataChanged} delegate directly, no tracking needed. - */ -public class TracingMemoryRetriever extends TracingSupport implements MemoryRetriever { - - private final MemoryRetriever delegate; - - public TracingMemoryRetriever(MemoryRetriever delegate, MemoryObserver observer) { - super(observer); - this.delegate = delegate; - } - - @Override - public Mono retrieve(RetrievalRequest request) { - return trace( - MemorySpanNames.RETRIEVAL, - Map.of(MEMORY_ID, request.memoryId().toIdentifier()), - r -> Map.of(RETRIEVAL_RESULT_COUNT, r.items().size()), - () -> delegate.retrieve(request)); - } - - @Override - public void registerStrategy(RetrievalStrategy strategy) { - delegate.registerStrategy(strategy); - } - - @Override - public void onDataChanged(MemoryId memoryId) { - delegate.onDataChanged(memoryId); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryThreadAssistant.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryThreadAssistant.java deleted file mode 100644 index 583910cd..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryThreadAssistant.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; -import com.openmemind.ai.memory.core.retrieval.query.QueryContext; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.thread.MemoryThreadAssistResult; -import com.openmemind.ai.memory.core.retrieval.thread.MemoryThreadAssistant; -import com.openmemind.ai.memory.core.retrieval.thread.RetrievalMemoryThreadSettings; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import reactor.core.publisher.Mono; - -/** - * Tracing decorator for retrieval memory-thread assist. - */ -public final class TracingMemoryThreadAssistant extends TracingSupport - implements MemoryThreadAssistant { - - private final MemoryThreadAssistant delegate; - - public TracingMemoryThreadAssistant(MemoryThreadAssistant delegate, MemoryObserver observer) { - super(Objects.requireNonNull(observer, "observer")); - this.delegate = Objects.requireNonNull(delegate, "delegate"); - } - - @Override - public Mono assist( - QueryContext context, - RetrievalConfig config, - RetrievalMemoryThreadSettings settings, - List directWindow) { - return trace( - MemorySpanNames.RETRIEVAL_MEMORY_THREAD_ASSIST, - Map.of( - MemoryAttributes.MEMORY_ID, - context.memoryId().toIdentifier(), - MemoryAttributes.RETRIEVAL_MEMORY_THREAD_ENABLED, - settings != null && settings.enabled()), - result -> - Map.of( - MemoryAttributes.RETRIEVAL_MEMORY_THREAD_SEED_THREAD_COUNT, - result.stats().seedThreadCount(), - MemoryAttributes.RETRIEVAL_MEMORY_THREAD_CANDIDATE_COUNT, - result.stats().candidateCount(), - MemoryAttributes.RETRIEVAL_MEMORY_THREAD_ADMITTED_COUNT, - result.stats().admittedMemberCount(), - MemoryAttributes.RETRIEVAL_MEMORY_THREAD_CLAMPED, - result.stats().clamped(), - MemoryAttributes.RETRIEVAL_MEMORY_THREAD_DEGRADED, - result.stats().degraded(), - MemoryAttributes.RETRIEVAL_MEMORY_THREAD_TIMEOUT, - result.stats().timedOut()), - () -> delegate.assist(context, config, settings, directWindow)); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRawDataExtractStep.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRawDataExtractStep.java deleted file mode 100644 index 717e97bb..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRawDataExtractStep.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import com.openmemind.ai.memory.core.data.MemoryId; -import com.openmemind.ai.memory.core.extraction.rawdata.content.RawContent; -import com.openmemind.ai.memory.core.extraction.result.RawDataResult; -import com.openmemind.ai.memory.core.extraction.step.RawDataExtractStep; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.Map; -import reactor.core.publisher.Mono; - -/** - * A decorator that adds observability to {@link RawDataExtractStep}. - * - *

Records the number of segments as a result attribute. - */ -public class TracingRawDataExtractStep extends TracingSupport implements RawDataExtractStep { - - private final RawDataExtractStep delegate; - - public TracingRawDataExtractStep(RawDataExtractStep delegate, MemoryObserver observer) { - super(observer); - this.delegate = delegate; - } - - @Override - public Mono extract( - MemoryId memoryId, - RawContent content, - String contentType, - Map metadata) { - return trace( - MemorySpanNames.EXTRACTION_RAWDATA, - Map.of(MemoryAttributes.MEMORY_ID, memoryId.toIdentifier()), - r -> Map.of(MemoryAttributes.EXTRACTION_SEGMENT_COUNT, r.segments().size()), - () -> delegate.extract(memoryId, content, contentType, metadata)); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingReranker.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingReranker.java deleted file mode 100644 index dc6f87b9..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingReranker.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_QUERY; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_TOP_K; - -import com.openmemind.ai.memory.core.llm.rerank.Reranker; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.List; -import java.util.Map; -import reactor.core.publisher.Mono; - -/** - * A decorator that adds observability to {@link Reranker}. - */ -public class TracingReranker extends TracingSupport implements Reranker { - - private final Reranker delegate; - - public TracingReranker(Reranker delegate, MemoryObserver observer) { - super(observer); - this.delegate = delegate; - } - - @Override - public Mono> rerank(String query, List results, int topK) { - return trace( - MemorySpanNames.RETRIEVAL_RERANK, - Map.of( - RETRIEVAL_QUERY, - query, - "memind.retrieval.rerank.candidates", - results.size(), - RETRIEVAL_TOP_K, - topK), - () -> delegate.rerank(query, results, topK)); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalGraphAssistant.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalGraphAssistant.java deleted file mode 100644 index b2e7a041..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalGraphAssistant.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; -import com.openmemind.ai.memory.core.retrieval.graph.RetrievalGraphAssistResult; -import com.openmemind.ai.memory.core.retrieval.graph.RetrievalGraphAssistant; -import com.openmemind.ai.memory.core.retrieval.graph.RetrievalGraphSettings; -import com.openmemind.ai.memory.core.retrieval.query.QueryContext; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import reactor.core.publisher.Mono; - -/** - * Tracing decorator for retrieval graph assist. - */ -public final class TracingRetrievalGraphAssistant extends TracingSupport - implements RetrievalGraphAssistant { - - private final RetrievalGraphAssistant delegate; - - public TracingRetrievalGraphAssistant( - RetrievalGraphAssistant delegate, MemoryObserver observer) { - super(Objects.requireNonNull(observer, "observer")); - this.delegate = Objects.requireNonNull(delegate, "delegate"); - } - - @Override - public Mono assist( - QueryContext context, - RetrievalConfig config, - RetrievalGraphSettings graphSettings, - List directItems) { - return trace( - MemorySpanNames.RETRIEVAL_GRAPH_ASSIST, - Map.of( - MemoryAttributes.MEMORY_ID, - context.memoryId().toIdentifier(), - MemoryAttributes.RETRIEVAL_GRAPH_ENABLED, - graphSettings != null && graphSettings.enabled()), - result -> - Map.of( - MemoryAttributes.RETRIEVAL_GRAPH_SEED_COUNT, - result.stats().seedCount(), - MemoryAttributes.RETRIEVAL_GRAPH_LINK_EXPANSION_COUNT, - result.stats().linkExpansionCount(), - MemoryAttributes.RETRIEVAL_GRAPH_ENTITY_EXPANSION_COUNT, - result.stats().entityExpansionCount(), - MemoryAttributes.RETRIEVAL_GRAPH_DEDUPED_CANDIDATE_COUNT, - result.stats().dedupedCandidateCount(), - MemoryAttributes.RETRIEVAL_GRAPH_ADMITTED_CANDIDATE_COUNT, - result.stats().admittedGraphCandidateCount(), - MemoryAttributes.RETRIEVAL_GRAPH_DISPLACED_DIRECT_COUNT, - result.stats().displacedDirectCount(), - MemoryAttributes.RETRIEVAL_GRAPH_OVERLAP_COUNT, - result.stats().overlapCount(), - MemoryAttributes.RETRIEVAL_GRAPH_SKIPPED_OVERFANOUT_ENTITY_COUNT, - result.stats().skippedOverFanoutEntityCount(), - MemoryAttributes.RETRIEVAL_GRAPH_TIMEOUT, - result.stats().timedOut(), - MemoryAttributes.RETRIEVAL_GRAPH_DEGRADED, - result.stats().degraded()), - () -> delegate.assist(context, config, graphSettings, directItems)); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalResultMerger.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalResultMerger.java deleted file mode 100644 index 00812be0..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalResultMerger.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import com.openmemind.ai.memory.core.metrics.MemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.NoopMemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.RetrievalMergeMetrics; -import com.openmemind.ai.memory.core.metrics.RetrievalMetricsSupport; -import com.openmemind.ai.memory.core.retrieval.scoring.RetrievalResultMerger; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoringConfig; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalMergeTrace; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceCollector; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceContext; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import reactor.core.publisher.Mono; - -/** Tracing decorator for strategy-level retrieval result fusion. */ -public final class TracingRetrievalResultMerger extends TracingSupport - implements RetrievalResultMerger { - - private final RetrievalResultMerger delegate; - private final MemoryMetricsRecorder metricsRecorder; - - public TracingRetrievalResultMerger(RetrievalResultMerger delegate, MemoryObserver observer) { - this(delegate, observer, NoopMemoryMetricsRecorder.INSTANCE); - } - - public TracingRetrievalResultMerger( - RetrievalResultMerger delegate, - MemoryObserver observer, - MemoryMetricsRecorder metricsRecorder) { - super(Objects.requireNonNull(observer, "observer")); - this.delegate = Objects.requireNonNull(delegate, "delegate"); - this.metricsRecorder = - metricsRecorder == null ? NoopMemoryMetricsRecorder.INSTANCE : metricsRecorder; - } - - @Override - public Mono> merge( - ScoringConfig scoring, List> rankedLists, double... weights) { - return trace( - MemorySpanNames.RETRIEVAL_RESULT_MERGE, - requestAttributes(rankedLists, weights), - result -> resultAttributes(rankedLists, result), - () -> - Mono.deferContextual( - context -> { - RetrievalTraceCollector collector = - RetrievalTraceContext.collector(context); - return delegate.merge(scoring, rankedLists, weights) - .doOnNext( - result -> - recordMerge( - collector, - rankedLists, - result, - "success")) - .doOnError( - ignored -> - recordMerge( - collector, - rankedLists, - null, - "error")); - })); - } - - private void recordMerge( - RetrievalTraceCollector collector, - List> rankedLists, - List result, - String status) { - int inputCount = candidateCount(rankedLists); - int outputCount = result == null ? 0 : result.size(); - RetrievalMetricsSupport.safeRecord( - () -> - metricsRecorder.recordRetrievalMerge( - new RetrievalMergeMetrics( - null, - inputCount, - outputCount, - RetrievalMetricsSupport.deduplicatedCount( - inputCount, outputCount), - rankedLists == null ? 0 : rankedLists.size(), - status, - "core"))); - RetrievalMetricsSupport.safeRecord( - () -> - collector.mergeCompleted( - new RetrievalMergeTrace( - inputCount, - outputCount, - RetrievalMetricsSupport.deduplicatedCount( - inputCount, outputCount), - rankedLists == null ? 0 : rankedLists.size(), - status))); - } - - private Map requestAttributes( - List> rankedLists, double[] weights) { - return Map.of( - MemoryAttributes.RETRIEVAL_SOURCE_LIST_COUNT, - rankedLists == null ? 0 : rankedLists.size(), - MemoryAttributes.RETRIEVAL_CANDIDATE_COUNT, - candidateCount(rankedLists), - MemoryAttributes.RETRIEVAL_DEDUPED_COUNT, - dedupedCount(rankedLists), - MemoryAttributes.RETRIEVAL_WEIGHT_COUNT, - weights == null ? 0 : weights.length); - } - - private Map resultAttributes( - List> rankedLists, List result) { - int before = candidateCount(rankedLists); - int after = result == null ? 0 : result.size(); - return Map.of( - MemoryAttributes.RETRIEVAL_RESULT_COUNT, - after, - MemoryAttributes.RETRIEVAL_DEDUPED_COUNT, - Math.max(0, before - after)); - } - - private int candidateCount(List> rankedLists) { - if (rankedLists == null) { - return 0; - } - return rankedLists.stream().filter(Objects::nonNull).mapToInt(List::size).sum(); - } - - private int dedupedCount(List> rankedLists) { - if (rankedLists == null) { - return 0; - } - return (int) - rankedLists.stream() - .filter(Objects::nonNull) - .flatMap(List::stream) - .map(ScoredResult::dedupKey) - .distinct() - .count(); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalStrategy.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalStrategy.java deleted file mode 100644 index ce152a0f..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalStrategy.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.MEMORY_ID; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_RESULT_COUNT; - -import com.openmemind.ai.memory.core.data.MemoryId; -import com.openmemind.ai.memory.core.metrics.MemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.NoopMemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.RetrievalMetricsSupport; -import com.openmemind.ai.memory.core.metrics.RetrievalSummaryMetrics; -import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; -import com.openmemind.ai.memory.core.retrieval.RetrievalResult; -import com.openmemind.ai.memory.core.retrieval.query.QueryContext; -import com.openmemind.ai.memory.core.retrieval.strategy.RetrievalStrategy; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalFinalTrace; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceCollector; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceContext; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.Map; -import reactor.core.publisher.Mono; - -/** - * A decorator that adds observability to {@link RetrievalStrategy}. - * - *

{@link #retrieve} wraps with observer, recording span and attributes; - * {@link #name()} and {@link #onDataChanged} delegate directly, no tracking needed. - */ -public class TracingRetrievalStrategy extends TracingSupport implements RetrievalStrategy { - - private final RetrievalStrategy delegate; - private final MemoryMetricsRecorder metricsRecorder; - - public TracingRetrievalStrategy(RetrievalStrategy delegate, MemoryObserver observer) { - this(delegate, observer, NoopMemoryMetricsRecorder.INSTANCE); - } - - public TracingRetrievalStrategy( - RetrievalStrategy delegate, - MemoryObserver observer, - MemoryMetricsRecorder metricsRecorder) { - super(observer); - this.delegate = delegate; - this.metricsRecorder = - metricsRecorder == null ? NoopMemoryMetricsRecorder.INSTANCE : metricsRecorder; - } - - @Override - public String name() { - return delegate.name(); - } - - @Override - public Mono retrieve(QueryContext context, RetrievalConfig config) { - return trace( - MemorySpanNames.RETRIEVAL_STRATEGY, - Map.of( - MEMORY_ID, - context.memoryId().toIdentifier(), - MemoryAttributes.RETRIEVAL_STRATEGY, - delegate.name()), - r -> Map.of(RETRIEVAL_RESULT_COUNT, r.items().size()), - () -> - Mono.deferContextual( - reactorContext -> { - RetrievalTraceCollector collector = - RetrievalTraceContext.collector(reactorContext); - return delegate.retrieve(context, config) - .doOnNext(result -> recordSummary(collector, result)) - .doOnError(ignored -> recordSummaryError(collector)); - })); - } - - @Override - public void onDataChanged(MemoryId memoryId) { - delegate.onDataChanged(memoryId); - } - - private void recordSummary(RetrievalTraceCollector collector, RetrievalResult result) { - RetrievalSummaryMetrics metrics = - RetrievalMetricsSupport.summary(delegate.name(), result, "core"); - RetrievalMetricsSupport.safeRecord(() -> metricsRecorder.recordRetrievalSummary(metrics)); - RetrievalMetricsSupport.safeRecord( - () -> - collector.finalResults( - new RetrievalFinalTrace( - metrics.strategy(), - metrics.status(), - metrics.itemCount(), - metrics.insightCount(), - metrics.rawDataCount(), - metrics.evidenceCount()))); - } - - private void recordSummaryError(RetrievalTraceCollector collector) { - RetrievalSummaryMetrics metrics = - new RetrievalSummaryMetrics(delegate.name(), "error", 0, 0, 0, 0, "core"); - RetrievalMetricsSupport.safeRecord(() -> metricsRecorder.recordRetrievalSummary(metrics)); - RetrievalMetricsSupport.safeRecord( - () -> - collector.finalResults( - new RetrievalFinalTrace(metrics.strategy(), "error", 0, 0, 0, 0))); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingSufficiencyGate.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingSufficiencyGate.java deleted file mode 100644 index 52611b17..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingSufficiencyGate.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_SUFFICIENT; - -import com.openmemind.ai.memory.core.retrieval.query.QueryContext; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.sufficiency.SufficiencyGate; -import com.openmemind.ai.memory.core.retrieval.sufficiency.SufficiencyResult; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.List; -import java.util.Map; -import reactor.core.publisher.Mono; - -/** - * A decorator that adds observability to {@link SufficiencyGate}. - */ -public class TracingSufficiencyGate extends TracingSupport implements SufficiencyGate { - - private final SufficiencyGate delegate; - - public TracingSufficiencyGate(SufficiencyGate delegate, MemoryObserver observer) { - super(observer); - this.delegate = delegate; - } - - @Override - public Mono check(QueryContext context, List results) { - return trace( - MemorySpanNames.RETRIEVAL_SUFFICIENCY, - Map.of(), - r -> Map.of(RETRIEVAL_SUFFICIENT, r.sufficient()), - () -> delegate.check(context, results)); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingTemporalItemChannel.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingTemporalItemChannel.java deleted file mode 100644 index 96640ea2..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingTemporalItemChannel.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import com.openmemind.ai.memory.core.metrics.MemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.NoopMemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.RetrievalMetricsSupport; -import com.openmemind.ai.memory.core.metrics.RetrievalStageMetrics; -import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; -import com.openmemind.ai.memory.core.retrieval.query.QueryContext; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.temporal.TemporalConstraint; -import com.openmemind.ai.memory.core.retrieval.temporal.TemporalItemChannel; -import com.openmemind.ai.memory.core.retrieval.temporal.TemporalItemChannelResult; -import com.openmemind.ai.memory.core.retrieval.temporal.TemporalItemChannelSettings; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalStageTrace; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceSupport; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import reactor.core.publisher.Mono; - -/** Tracing decorator for temporal item channel retrieval. */ -public final class TracingTemporalItemChannel extends TracingSupport - implements TemporalItemChannel { - - private final TemporalItemChannel delegate; - private final MemoryMetricsRecorder metricsRecorder; - - public TracingTemporalItemChannel(TemporalItemChannel delegate, MemoryObserver observer) { - this(delegate, observer, NoopMemoryMetricsRecorder.INSTANCE); - } - - public TracingTemporalItemChannel( - TemporalItemChannel delegate, - MemoryObserver observer, - MemoryMetricsRecorder metricsRecorder) { - super(Objects.requireNonNull(observer, "observer")); - this.delegate = Objects.requireNonNull(delegate, "delegate"); - this.metricsRecorder = - metricsRecorder == null ? NoopMemoryMetricsRecorder.INSTANCE : metricsRecorder; - } - - @Override - public Mono retrieve( - QueryContext context, - RetrievalConfig config, - Optional temporalConstraint, - TemporalItemChannelSettings settings) { - return trace( - MemorySpanNames.RETRIEVAL_TEMPORAL_CHANNEL, - Map.of( - MemoryAttributes.MEMORY_ID, - context.memoryId().toIdentifier(), - MemoryAttributes.RETRIEVAL_CHANNEL, - "temporal", - MemoryAttributes.RETRIEVAL_TEMPORAL_ENABLED, - settings != null && settings.enabled(), - MemoryAttributes.RETRIEVAL_TEMPORAL_CONSTRAINT_PRESENT, - temporalConstraint != null && temporalConstraint.isPresent()), - result -> - Map.of( - MemoryAttributes.RETRIEVAL_RESULT_COUNT, - result.items().size(), - MemoryAttributes.RETRIEVAL_CANDIDATE_COUNT, - result.candidateCount(), - MemoryAttributes.RETRIEVAL_TEMPORAL_ENABLED, - result.enabled(), - MemoryAttributes.RETRIEVAL_TEMPORAL_CONSTRAINT_PRESENT, - result.constraintPresent(), - MemoryAttributes.RETRIEVAL_TEMPORAL_DEGRADED, - result.degraded()), - () -> - RetrievalTraceSupport.traceStage( - delegate.retrieve( - context, config, temporalConstraint, settings), - "channel", - "item", - "temporal", - null, - this::temporalStageTrace) - .doOnNext( - result -> - recordStage( - result == null - ? 0 - : result.candidateCount(), - result == null || result.items() == null - ? 0 - : result.items().size(), - result != null && result.degraded(), - result == null || !result.enabled(), - result != null && result.degraded() - ? "degraded" - : "success")) - .doOnError(ignored -> recordStage(0, 0, false, false, "error"))); - } - - private void recordStage( - int candidateCount, int resultCount, boolean degraded, boolean skipped, String status) { - RetrievalMetricsSupport.safeRecord( - () -> - metricsRecorder.recordRetrievalStage( - new RetrievalStageMetrics( - null, - "channel", - "item", - "temporal", - status, - null, - candidateCount, - resultCount, - degraded, - skipped, - "core"))); - } - - private RetrievalStageTrace temporalStageTrace( - TemporalItemChannelResult result, - String stage, - String tier, - String method, - Integer inputCount, - java.time.Instant startedAt, - long durationMillis, - RetrievalTraceOptions options) { - List results = result == null ? List.of() : result.items(); - return new RetrievalStageTrace( - stage, - tier, - method, - result != null && result.degraded() ? "degraded" : "success", - inputCount, - result == null ? 0 : result.candidateCount(), - results == null ? 0 : results.size(), - result != null && result.degraded(), - result == null || !result.enabled(), - startedAt, - durationMillis, - result == null ? Map.of() : Map.of("constraintPresent", result.constraintPresent()), - RetrievalTraceSupport.candidates( - results, options.maxCandidatesPerStage(), options.maxTextLength())); - } -} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingTypedQueryExpander.java b/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingTypedQueryExpander.java deleted file mode 100644 index 0668034b..00000000 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/tracing/decorator/TracingTypedQueryExpander.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import com.openmemind.ai.memory.core.retrieval.deep.ExpandedQuery; -import com.openmemind.ai.memory.core.retrieval.deep.TypedQueryExpander; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.TracingSupport; -import java.util.List; -import java.util.Map; -import reactor.core.publisher.Mono; - -/** - * A decorator that adds observability to {@link TypedQueryExpander}. - */ -public class TracingTypedQueryExpander extends TracingSupport implements TypedQueryExpander { - - private final TypedQueryExpander delegate; - - public TracingTypedQueryExpander(TypedQueryExpander delegate, MemoryObserver observer) { - super(observer); - this.delegate = delegate; - } - - @Override - public Mono> expand( - String query, - List gaps, - List keyInformation, - List conversationHistory, - int maxExpansions) { - return trace( - MemorySpanNames.RETRIEVAL_MULTI_QUERY_EXPAND, - Map.of(), - () -> - delegate.expand( - query, gaps, keyInformation, conversationHistory, maxExpansions)); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/builder/MemoryAssemblersTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/builder/MemoryAssemblersTest.java index 349c584b..b6cabd2e 100644 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/builder/MemoryAssemblersTest.java +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/builder/MemoryAssemblersTest.java @@ -51,7 +51,9 @@ import com.openmemind.ai.memory.core.llm.ChatClientRegistry; import com.openmemind.ai.memory.core.llm.ChatClientSlot; import com.openmemind.ai.memory.core.llm.StructuredChatClient; +import com.openmemind.ai.memory.core.llm.rerank.LlmReranker; import com.openmemind.ai.memory.core.llm.rerank.NoopReranker; +import com.openmemind.ai.memory.core.llm.rerank.Reranker; import com.openmemind.ai.memory.core.plugin.RawDataPlugin; import com.openmemind.ai.memory.core.prompt.PromptRegistry; import com.openmemind.ai.memory.core.resource.ContentParserRegistry; @@ -61,6 +63,7 @@ import com.openmemind.ai.memory.core.retrieval.graph.NoOpRetrievalGraphAssistant; import com.openmemind.ai.memory.core.retrieval.query.LlmLongQueryCondenser; import com.openmemind.ai.memory.core.retrieval.query.LongQueryCondenser; +import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; import com.openmemind.ai.memory.core.retrieval.strategy.DeepStrategyConfig; import com.openmemind.ai.memory.core.retrieval.strategy.RetrievalStrategies; import com.openmemind.ai.memory.core.retrieval.strategy.SimpleStrategyConfig; @@ -77,9 +80,8 @@ import com.openmemind.ai.memory.core.store.rawdata.InMemoryRawDataOperations; import com.openmemind.ai.memory.core.store.rawdata.RawDataOperations; import com.openmemind.ai.memory.core.store.resource.ResourceOperations; +import com.openmemind.ai.memory.core.support.RecordingObservationRegistry; import com.openmemind.ai.memory.core.textsearch.MemoryTextSearch; -import com.openmemind.ai.memory.core.tracing.decorator.TracingItemGraphMaterializer; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryThreadAssistant; import com.openmemind.ai.memory.core.vector.MemoryVector; import java.lang.reflect.Proxy; import java.time.Duration; @@ -89,6 +91,7 @@ import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; class MemoryAssemblersTest { @@ -232,6 +235,33 @@ void retrievalAssemblerWiresLlmLongQueryCondenserWhenChatClientExists() { .isInstanceOf(LlmLongQueryCondenser.class); } + @Test + void retrievalAssemblerObservesCustomReranker() { + var registry = new RecordingObservationRegistry(); + Reranker customReranker = (query, results, topK) -> Mono.just(results); + var reranker = MemoryRetrievalAssembler.observeCustomReranker(customReranker, registry); + var candidates = + List.of(new ScoredResult(ScoredResult.SourceType.ITEM, "1", "item-1", 0.8f, 0.9)); + + StepVerifier.create(reranker.rerank("query", candidates, 1)) + .expectNext(candidates) + .verifyComplete(); + + assertThat(registry.observations()) + .extracting(observation -> observation.observationName()) + .containsExactly("memind.retrieval.rerank"); + } + + @Test + void retrievalAssemblerDoesNotWrapLlmRerankerTwice() { + var reranker = new LlmReranker("http://localhost", "test-key"); + + assertThat( + MemoryRetrievalAssembler.observeCustomReranker( + reranker, new RecordingObservationRegistry())) + .isSameAs(reranker); + } + @Test void retrievalAssemblerAlwaysWiresRuntimeGraphAssistantIntoBothStrategies() { var retriever = @@ -292,20 +322,8 @@ void retrievalAssemblerWiresRuntimeThreadAssistantAndClampedCapsWhenEnabled() { var deep = strategies.get(RetrievalStrategies.DEEP_RETRIEVAL); assertThat(readField(simple, "memoryThreadAssistant", Object.class)) - .isInstanceOf(TracingMemoryThreadAssistant.class); - assertThat(readField(deep, "memoryThreadAssistant", Object.class)) - .isInstanceOf(TracingMemoryThreadAssistant.class); - assertThat( - readField( - readField(simple, "memoryThreadAssistant", Object.class), - "delegate", - Object.class)) .isInstanceOf(DefaultMemoryThreadAssistant.class); - assertThat( - readField( - readField(deep, "memoryThreadAssistant", Object.class), - "delegate", - Object.class)) + assertThat(readField(deep, "memoryThreadAssistant", Object.class)) .isInstanceOf(DefaultMemoryThreadAssistant.class); assertThat( readField(simple, "defaultStrategyConfig", SimpleStrategyConfig.class) @@ -457,8 +475,8 @@ void extractionAssemblerWiresExactResolutionByDefault() { new InMemoryMemoryStore())); var extractor = (DefaultMemoryExtractor) assembly.pipeline(); var itemLayer = readField(extractor, "memoryItemStep", MemoryItemLayer.class); - var tracing = readField(itemLayer, "graphMaterializer", TracingItemGraphMaterializer.class); - var delegate = readField(tracing, "delegate", DefaultItemGraphMaterializer.class); + var delegate = + readField(itemLayer, "graphMaterializer", DefaultItemGraphMaterializer.class); assertThat(readResolutionStrategy(delegate)) .isInstanceOf(ExactCanonicalEntityResolutionStrategy.class); @@ -532,8 +550,8 @@ void extractionAssemblerWiresConservativeResolutionWhenRequested() { context(options, null, null, List.of(), new InMemoryMemoryStore())); var extractor = (DefaultMemoryExtractor) assembly.pipeline(); var itemLayer = readField(extractor, "memoryItemStep", MemoryItemLayer.class); - var tracing = readField(itemLayer, "graphMaterializer", TracingItemGraphMaterializer.class); - var delegate = readField(tracing, "delegate", DefaultItemGraphMaterializer.class); + var delegate = + readField(itemLayer, "graphMaterializer", DefaultItemGraphMaterializer.class); assertThat(readResolutionStrategy(delegate)) .isInstanceOf(ConservativeHeuristicEntityResolutionStrategy.class); @@ -563,8 +581,8 @@ void extractionAssemblerWiresHistoricalAliasAwareRetrieverWhenCapabilityExists() .assemble(context(options, null, null, List.of(), store)); var extractor = (DefaultMemoryExtractor) assembly.pipeline(); var itemLayer = readField(extractor, "memoryItemStep", MemoryItemLayer.class); - var tracing = readField(itemLayer, "graphMaterializer", TracingItemGraphMaterializer.class); - var delegate = readField(tracing, "delegate", DefaultItemGraphMaterializer.class); + var delegate = + readField(itemLayer, "graphMaterializer", DefaultItemGraphMaterializer.class); var strategy = readResolutionStrategy(delegate); assertThat(strategy).isInstanceOf(ConservativeHeuristicEntityResolutionStrategy.class); @@ -619,8 +637,8 @@ public boolean supportsHistoricalAliasLookup() { .assemble(context(options, null, null, List.of(), stageTwoOnlyStore)); var extractor = (DefaultMemoryExtractor) assembly.pipeline(); var itemLayer = readField(extractor, "memoryItemStep", MemoryItemLayer.class); - var tracing = readField(itemLayer, "graphMaterializer", TracingItemGraphMaterializer.class); - var delegate = readField(tracing, "delegate", DefaultItemGraphMaterializer.class); + var delegate = + readField(itemLayer, "graphMaterializer", DefaultItemGraphMaterializer.class); var strategy = readResolutionStrategy(delegate); assertThat(strategy).isInstanceOf(ConservativeHeuristicEntityResolutionStrategy.class); diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/builder/internal/DefaultMemoryBuilderTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/builder/internal/DefaultMemoryBuilderTest.java index b8f9aa62..4c558708 100644 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/builder/internal/DefaultMemoryBuilderTest.java +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/builder/internal/DefaultMemoryBuilderTest.java @@ -79,14 +79,9 @@ import com.openmemind.ai.memory.core.store.insight.InsightOperations; import com.openmemind.ai.memory.core.store.item.ItemOperations; import com.openmemind.ai.memory.core.store.rawdata.RawDataOperations; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; import com.openmemind.ai.memory.core.textsearch.MemoryTextSearch; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.NoopMemoryObserver; -import com.openmemind.ai.memory.core.tracing.decorator.TracingItemGraphMaterializer; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryExtractor; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryRetriever; import com.openmemind.ai.memory.core.vector.MemoryVector; +import io.micrometer.observation.ObservationRegistry; import java.lang.reflect.Proxy; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; @@ -229,34 +224,40 @@ void builderStoresSanitizedBuildOptionsOnDefaultMemory() { } @Test - void builderWrapsExtractorWithTracingDecoratorWhenObserverConfigured() { - var observer = new RecordingMemoryObserver(); + void builderInjectsObservationRegistryIntoDefaultExtractorWhenConfigured() { + var observationRegistry = ObservationRegistry.create(); - var memory = buildMinimalMemory(observer); + var memory = buildMinimalMemory(observationRegistry); var extractor = readField(memory, "extractor", MemoryExtractor.class); - assertThat(extractor).isInstanceOf(TracingMemoryExtractor.class); + assertThat(extractor).isInstanceOf(DefaultMemoryExtractor.class); + assertThat(readField(extractor, "observationRegistry", ObservationRegistry.class)) + .isSameAs(observationRegistry); } @Test - void builderWrapsRetrieverWithTracingDecoratorWhenObserverConfigured() { - var observer = new RecordingMemoryObserver(); + void builderInjectsObservationRegistryIntoDefaultRetrieverWhenConfigured() { + var observationRegistry = ObservationRegistry.create(); - var memory = buildMinimalMemory(observer); + var memory = buildMinimalMemory(observationRegistry); var retriever = readField(memory, "retriever", MemoryRetriever.class); - assertThat(retriever).isInstanceOf(TracingMemoryRetriever.class); + assertThat(retriever).isInstanceOf(DefaultMemoryRetriever.class); + assertThat(readField(retriever, "observationRegistry", ObservationRegistry.class)) + .isSameAs(observationRegistry); } @Test - void builderKeepsTopLevelDelegatesUnwrappedForNoopObserver() { - var memory = buildMinimalMemory(new NoopMemoryObserver()); + void builderInjectsNoopObservationRegistryIntoTopLevelComponentsByDefault() { + var memory = buildMinimalMemory(ObservationRegistry.NOOP); var extractor = readField(memory, "extractor", MemoryExtractor.class); var retriever = readField(memory, "retriever", MemoryRetriever.class); - assertThat(extractor).isNotInstanceOf(TracingMemoryExtractor.class); - assertThat(retriever).isNotInstanceOf(TracingMemoryRetriever.class); + assertThat(readField(extractor, "observationRegistry", ObservationRegistry.class)) + .isSameAs(ObservationRegistry.NOOP); + assertThat(readField(retriever, "observationRegistry", ObservationRegistry.class)) + .isSameAs(ObservationRegistry.NOOP); } @Test @@ -510,8 +511,8 @@ void buildPropagatesPromptRegistryAcrossExtractionAndRetrievalAssemblies() { } @Test - void builderManagedRuntimeShouldWrapGraphMaterializerWithTracingObserver() { - var observer = new RecordingMemoryObserver(); + void builderManagedRuntimeShouldInjectObservationRegistryIntoGraphMaterializer() { + var observationRegistry = ObservationRegistry.create(); var memory = (DefaultMemory) @@ -520,15 +521,18 @@ void builderManagedRuntimeShouldWrapGraphMaterializerWithTracingObserver() { .store(new InMemoryMemoryStore()) .buffer(MEMORY_BUFFER) .vector(MEMORY_VECTOR) - .memoryObserver(observer) + .observationRegistry(observationRegistry) .options(graphEnabledBuildOptions()) .build(); var extractor = underlyingExtractor(memory); var itemLayer = readField(extractor, "memoryItemStep", MemoryItemLayer.class); + var graphMaterializer = + readField(itemLayer, "graphMaterializer", ItemGraphMaterializer.class); - assertThat(readField(itemLayer, "graphMaterializer", ItemGraphMaterializer.class)) - .isInstanceOf(TracingItemGraphMaterializer.class); + assertThat(graphMaterializer).isInstanceOf(DefaultItemGraphMaterializer.class); + assertThat(readField(graphMaterializer, "observationRegistry", ObservationRegistry.class)) + .isSameAs(observationRegistry); } @Test @@ -545,18 +549,17 @@ void builderManagedRuntimeShouldWireExactResolutionByDefault() { var extractor = underlyingExtractor(memory); var itemLayer = readField(extractor, "memoryItemStep", MemoryItemLayer.class); - var tracing = readField(itemLayer, "graphMaterializer", TracingItemGraphMaterializer.class); - var delegate = readField(tracing, "delegate", DefaultItemGraphMaterializer.class); - - var planner = readField(delegate, "planner", DefaultItemGraphPlanner.class); + var graphMaterializer = + readField(itemLayer, "graphMaterializer", DefaultItemGraphMaterializer.class); + var planner = readField(graphMaterializer, "planner", DefaultItemGraphPlanner.class); assertThat(readField(planner, "resolutionStrategy", EntityResolutionStrategy.class)) .isInstanceOf(ExactCanonicalEntityResolutionStrategy.class); } @Test - void builderManagedRuntimeShouldKeepGraphMaterializerNoOpAndUntracedWhenGraphDisabled() { - var observer = new RecordingMemoryObserver(); + void builderManagedRuntimeShouldKeepGraphMaterializerNoOpAndUnobservedWhenGraphDisabled() { + var observationRegistry = ObservationRegistry.create(); var memory = (DefaultMemory) @@ -565,7 +568,7 @@ void builderManagedRuntimeShouldKeepGraphMaterializerNoOpAndUntracedWhenGraphDis .store(MEMORY_STORE) .buffer(MEMORY_BUFFER) .vector(MEMORY_VECTOR) - .memoryObserver(observer) + .observationRegistry(observationRegistry) .options(MemoryBuildOptions.defaults()) .build(); @@ -936,30 +939,24 @@ private static T proxy(Class type) { }); } - private DefaultMemory buildMinimalMemory(MemoryObserver observer) { + private DefaultMemory buildMinimalMemory(ObservationRegistry observationRegistry) { return (DefaultMemory) Memory.builder() .chatClient(CHAT_CLIENT) .store(new InMemoryMemoryStore()) .buffer(MEMORY_BUFFER) .vector(MEMORY_VECTOR) - .memoryObserver(observer) + .observationRegistry(observationRegistry) .build(); } private static DefaultMemoryExtractor underlyingExtractor(DefaultMemory memory) { MemoryExtractor extractor = readField(memory, "extractor", MemoryExtractor.class); - if (extractor instanceof TracingMemoryExtractor tracing) { - return readField(tracing, "delegate", DefaultMemoryExtractor.class); - } return DefaultMemoryExtractor.class.cast(extractor); } private static DefaultMemoryRetriever underlyingRetriever(DefaultMemory memory) { MemoryRetriever retriever = readField(memory, "retriever", MemoryRetriever.class); - if (retriever instanceof TracingMemoryRetriever tracing) { - return readField(tracing, "delegate", DefaultMemoryRetriever.class); - } return DefaultMemoryRetriever.class.cast(retriever); } diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/DefaultMemoryExtractorConstructorTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/DefaultMemoryExtractorConstructorTest.java index 151f3989..4aa8a3f2 100644 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/DefaultMemoryExtractorConstructorTest.java +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/DefaultMemoryExtractorConstructorTest.java @@ -29,6 +29,7 @@ import com.openmemind.ai.memory.core.resource.ContentParserRegistry; import com.openmemind.ai.memory.core.resource.ResourceFetcher; import com.openmemind.ai.memory.core.resource.ResourceStore; +import io.micrometer.observation.ObservationRegistry; import java.lang.reflect.Constructor; import java.util.Arrays; import java.util.List; @@ -62,7 +63,23 @@ void onlySupportedPublicConstructorsRemain() { ResourceFetcher.class, RawDataIngestionPolicyRegistry.class, RawDataExtractionOptions.class, - ItemExtractionOptions.class)); + ItemExtractionOptions.class), + List.of( + RawDataExtractStep.class, + MemoryItemExtractStep.class, + InsightExtractStep.class, + SegmentProcessor.class, + ContextCommitDetector.class, + PendingConversationBuffer.class, + RecentConversationBuffer.class, + RawContentProcessorRegistry.class, + ContentParserRegistry.class, + ResourceStore.class, + ResourceFetcher.class, + RawDataIngestionPolicyRegistry.class, + RawDataExtractionOptions.class, + ItemExtractionOptions.class, + ObservationRegistry.class)); } private List>> publicConstructorSignatures() { diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/InsightLayerObservationTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/InsightLayerObservationTest.java new file mode 100644 index 00000000..54935126 --- /dev/null +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/InsightLayerObservationTest.java @@ -0,0 +1,94 @@ +/* + * 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 com.openmemind.ai.memory.core.extraction.insight; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.openmemind.ai.memory.core.data.DefaultMemoryId; +import com.openmemind.ai.memory.core.data.MemoryInsightType; +import com.openmemind.ai.memory.core.data.MemoryItem; +import com.openmemind.ai.memory.core.data.enums.InsightAnalysisMode; +import com.openmemind.ai.memory.core.data.enums.MemoryItemType; +import com.openmemind.ai.memory.core.data.enums.MemoryScope; +import com.openmemind.ai.memory.core.extraction.insight.scheduler.InsightBuildScheduler; +import com.openmemind.ai.memory.core.extraction.result.MemoryItemResult; +import com.openmemind.ai.memory.core.store.MemoryStore; +import com.openmemind.ai.memory.core.store.insight.InsightOperations; +import com.openmemind.ai.memory.core.support.RecordingObservationRegistry; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +class InsightLayerObservationTest { + + @Test + void extractPublishesInsightObservation() { + var registry = new RecordingObservationRegistry(); + var store = mock(MemoryStore.class); + var insightOperations = mock(InsightOperations.class); + var scheduler = mock(InsightBuildScheduler.class); + var memoryId = DefaultMemoryId.of("user1", "agent1"); + var insightType = + new MemoryInsightType( + 1L, + "profile", + "Profile", + null, + List.of("profile"), + 400, + null, + null, + null, + InsightAnalysisMode.BRANCH, + null, + MemoryScope.USER); + var item = + new MemoryItem( + 1L, + memoryId.toIdentifier(), + "User likes Java", + MemoryScope.USER, + null, + "conversation", + "vector-1", + "raw-1", + "hash-1", + Instant.parse("2026-03-20T00:00:00Z"), + null, + Map.of("insightTypes", List.of("profile")), + Instant.parse("2026-03-20T00:00:00Z"), + MemoryItemType.FACT); + when(store.insightOperations()).thenReturn(insightOperations); + when(insightOperations.listInsightTypes()).thenReturn(List.of(insightType)); + var layer = new InsightLayer(store, scheduler, registry); + + StepVerifier.create( + layer.extract( + memoryId, + new MemoryItemResult(List.of(item), List.of(insightType)), + "English")) + .assertNext(result -> assertThat(result.isEmpty()).isTrue()) + .verifyComplete(); + + assertThat(registry.observations()).hasSize(1); + var observation = registry.observations().getFirst(); + assertThat(observation.observationName()).isEqualTo("memind.extraction.insight"); + assertThat(observation.requestAttributes()) + .containsEntry("memind.memory_id", memoryId.toIdentifier()); + } +} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/generator/LlmInsightGeneratorTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/generator/LlmInsightGeneratorTest.java index 0332004a..a7c80390 100644 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/generator/LlmInsightGeneratorTest.java +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/generator/LlmInsightGeneratorTest.java @@ -15,14 +15,17 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.openmemind.ai.memory.core.data.InsightPoint; import com.openmemind.ai.memory.core.data.MemoryInsight; import com.openmemind.ai.memory.core.data.MemoryInsightType; +import com.openmemind.ai.memory.core.data.PointOperation; import com.openmemind.ai.memory.core.data.enums.InsightAnalysisMode; import com.openmemind.ai.memory.core.data.enums.MemoryScope; import com.openmemind.ai.memory.core.llm.ChatMessage; import com.openmemind.ai.memory.core.llm.StructuredChatClient; import com.openmemind.ai.memory.core.prompt.InMemoryPromptRegistry; import com.openmemind.ai.memory.core.prompt.PromptType; +import com.openmemind.ai.memory.core.support.RecordingObservationRegistry; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -302,6 +305,100 @@ void generateRootSynthesisShouldAppendAdditionalContextToUserPrompt() { .contains("GraphRootHints: weak bridge between branches"); } + @Test + @DisplayName("generatePoints should publish leaf generation observation") + void generatePointsShouldPublishLeafGenerationObservation() { + var registry = new RecordingObservationRegistry(); + var client = + new FakeStructuredChatClient( + new InsightPointGenerateResponse( + List.of( + new InsightPoint( + InsightPoint.PointType.SUMMARY, + "summary", + List.of("1"))))); + var generator = + new LlmInsightGenerator(client, InMemoryPromptRegistry.builder().build(), registry); + + StepVerifier.create( + generator.generatePoints( + rootInsightType("profile"), + "work", + List.of(), + List.of(), + 300, + null, + "English")) + .assertNext(response -> assertThat(response.points()).hasSize(1)) + .verifyComplete(); + + assertThat(registry.observations()).hasSize(1); + var observation = registry.observations().getFirst(); + assertThat(observation.observationName()) + .isEqualTo("memind.extraction.insight.generate.leaf"); + assertThat(observation.requestAttributes()) + .containsEntry("memind.extraction.insight_type", "profile") + .containsEntry("memind.extraction.insight_group_name", "work"); + assertThat(observation.resultAttributes()) + .containsEntry("memind.extraction.insight_point_count", "1"); + } + + @Test + @DisplayName("generateBranchPointOps should publish operation counts") + void generateBranchPointOpsShouldPublishOperationCounts() { + var registry = new RecordingObservationRegistry(); + var client = + new FakeStructuredChatClient( + new InsightPointOpsResponse( + List.of( + new PointOperation( + PointOperation.OpType.ADD, + null, + new InsightPoint( + InsightPoint.PointType.SUMMARY, + "new", + List.of("1")), + null), + new PointOperation( + PointOperation.OpType.UPDATE, + "point-1", + new InsightPoint( + "point-1", + InsightPoint.PointType.SUMMARY, + "updated", + List.of("2")), + null), + new PointOperation( + PointOperation.OpType.DELETE, + "point-2", + null, + null)))); + var generator = + new LlmInsightGenerator(client, InMemoryPromptRegistry.builder().build(), registry); + + StepVerifier.create( + generator.generateBranchPointOps( + rootInsightType("profile"), + List.of(), + List.of(memoryInsight(), memoryInsight()), + 300, + "English")) + .assertNext(response -> assertThat(response.operations()).hasSize(3)) + .verifyComplete(); + + assertThat(registry.observations()).hasSize(1); + var observation = registry.observations().getFirst(); + assertThat(observation.observationName()) + .isEqualTo("memind.extraction.insight.generate.branch"); + assertThat(observation.requestAttributes()) + .containsEntry("memind.extraction.insight_type", "profile") + .containsEntry("memind.extraction.insight_leaf_count", "2"); + assertThat(observation.resultAttributes()) + .containsEntry("memind.extraction.insight_add_count", "1") + .containsEntry("memind.extraction.insight_update_count", "1") + .containsEntry("memind.extraction.insight_delete_count", "1"); + } + private static MemoryInsightType rootInsightType(String name) { return new MemoryInsightType( 1L, diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/generator/observation/LlmInsightGeneratorObservationTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/generator/observation/LlmInsightGeneratorObservationTest.java new file mode 100644 index 00000000..40676d54 --- /dev/null +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/generator/observation/LlmInsightGeneratorObservationTest.java @@ -0,0 +1,75 @@ +/* + * 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 com.openmemind.ai.memory.core.extraction.insight.generator.observation; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.openmemind.ai.memory.core.data.MemoryInsightType; +import com.openmemind.ai.memory.core.data.enums.InsightAnalysisMode; +import com.openmemind.ai.memory.core.data.enums.MemoryScope; +import com.openmemind.ai.memory.core.extraction.insight.generator.InsightPointGenerateResponse; +import com.openmemind.ai.memory.core.extraction.insight.generator.observation.LlmInsightGeneratorObservation.InsightGenerateObservationContext; +import io.micrometer.observation.ObservationRegistry; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class LlmInsightGeneratorObservationTest { + + @Test + void createsFreshContextForEachSubscription() { + var contexts = new CopyOnWriteArrayList(); + var subscriptions = new AtomicInteger(); + Mono observed = + LlmInsightGeneratorObservation.observeLeafPointGeneration( + ObservationRegistry.create(), + new MemoryInsightType( + 1L, + "profile", + "Profile", + null, + List.of(), + 300, + null, + null, + null, + InsightAnalysisMode.BRANCH, + null, + MemoryScope.AGENT), + "work", + context -> { + contexts.add(context); + return Mono.defer( + () -> + subscriptions.getAndIncrement() == 0 + ? Mono.error(new IllegalStateException("first")) + : Mono.just( + new InsightPointGenerateResponse( + List.of()))); + }); + + StepVerifier.create(observed) + .expectErrorMatches(error -> "first".equals(error.getMessage())) + .verify(); + StepVerifier.create(observed).expectNextCount(1).verifyComplete(); + + assertThat(contexts).hasSize(2); + assertThat(contexts.get(0)).isNotSameAs(contexts.get(1)); + assertThat(contexts.get(0).getError()).isNotNull(); + assertThat(contexts.get(1).getError()).isNull(); + } +} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/group/LlmInsightGroupClassifierTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/group/LlmInsightGroupClassifierTest.java index 4f8c0eda..48af3f15 100644 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/group/LlmInsightGroupClassifierTest.java +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/group/LlmInsightGroupClassifierTest.java @@ -21,6 +21,7 @@ import com.openmemind.ai.memory.core.llm.StructuredChatClient; import com.openmemind.ai.memory.core.prompt.InMemoryPromptRegistry; import com.openmemind.ai.memory.core.prompt.PromptType; +import com.openmemind.ai.memory.core.support.RecordingObservationRegistry; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -146,6 +147,52 @@ void shouldAppendAdditionalGraphContextToUserPrompt() { .contains("GraphGroupingHints: cluster alpha"); } + @Test + @DisplayName("Should publish group classification observation") + void shouldPublishGroupClassificationObservation() { + var item = + new MemoryItem( + 1L, + "m1", + "The user prefers short status updates.", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null); + var response = + new InsightGroupClassifyResponse( + List.of( + new InsightGroupClassifyResponse.GroupAssignment( + "Communication", List.of("1")))); + var registry = new RecordingObservationRegistry(); + var classifier = + new LlmInsightGroupClassifier( + new FakeStructuredChatClient(response), + InMemoryPromptRegistry.builder().build(), + registry); + + StepVerifier.create(classifier.classify(createInsightType(), List.of(item), List.of())) + .assertNext(groups -> assertThat(groups).containsKey("Communication")) + .verifyComplete(); + + assertThat(registry.observations()).hasSize(1); + var observation = registry.observations().getFirst(); + assertThat(observation.observationName()) + .isEqualTo("memind.extraction.insight.group.classify"); + assertThat(observation.requestAttributes()) + .containsEntry("memind.extraction.insight_type", "experiences") + .containsEntry("memind.extraction.item_count", "1"); + assertThat(observation.resultAttributes()) + .containsEntry("memind.extraction.insight_group_count", "1"); + } + private static MemoryInsightType createInsightType() { return new MemoryInsightType( 1L, diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/scheduler/InsightBuildSchedulerTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/scheduler/InsightBuildSchedulerTest.java index 20b54646..5a1f8bfd 100644 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/scheduler/InsightBuildSchedulerTest.java +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/insight/scheduler/InsightBuildSchedulerTest.java @@ -49,6 +49,8 @@ import com.openmemind.ai.memory.core.store.MemoryStore; import com.openmemind.ai.memory.core.store.insight.InsightOperations; import com.openmemind.ai.memory.core.store.item.ItemOperations; +import com.openmemind.ai.memory.core.support.RecordedObservation; +import com.openmemind.ai.memory.core.support.RecordingObservationRegistry; import com.openmemind.ai.memory.core.utils.IdUtils; import java.time.Instant; import java.util.List; @@ -69,6 +71,12 @@ class InsightBuildSchedulerTest { private static final MemoryId MEMORY_ID = () -> "memory-1"; private static final String TYPE_NAME = "identity"; private static final String GROUP_NAME = "group-a"; + private static final String EXTRACTION_INSIGHT_PIPELINE = "memind.extraction.insight.pipeline"; + private static final String EXTRACTION_INSIGHT_TREE_REORGANIZE = + "memind.extraction.insight.tree.reorganize"; + private static final String MEMORY_ID_KEY = "memind.memory_id"; + private static final String INSIGHT_TYPE_KEY = "memind.extraction.insight_type"; + private static final String INSIGHT_LEAF_COUNT_KEY = "memind.extraction.insight_leaf_count"; @Mock private MemoryStore store; @Mock private InsightGenerator generator; @@ -166,6 +174,52 @@ void nullCategorySemanticGroupingShouldRouteThroughSemanticPathWithGraphContext( .classify(any(), anyList(), anyList(), anyString(), anyString()); } + @Test + @DisplayName("flushSync should publish scheduler observations through ObservationRegistry") + void flushSyncShouldPublishSchedulerObservationsThroughObservationRegistry() { + var registry = new RecordingObservationRegistry(); + var observedScheduler = + new InsightBuildScheduler( + buffer, + store, + generator, + groupClassifier, + groupRouter, + treeReorganizer, + null, + IdUtils.snowflake(), + InsightBuildConfig.defaults(), + graphAssistant, + registry); + seedGroupedBuffer(1L); + when(itemOperations.getItemsByIds(eq(MEMORY_ID), anyList())).thenReturn(List.of(item(1L))); + when(generator.generateLeafPointOps( + any(), anyString(), anyList(), anyList(), anyInt(), any(), any())) + .thenReturn( + Mono.just( + new InsightPointOpsResponse( + List.of( + new PointOperation( + PointOperation.OpType.ADD, + null, + new InsightPoint( + InsightPoint.PointType.SUMMARY, + "new", + List.of("1")), + "add"))))); + + observedScheduler.flushSync(MEMORY_ID, TYPE_NAME, "English"); + + assertThat(registry.observations()) + .extracting(RecordedObservation::observationName) + .contains(EXTRACTION_INSIGHT_PIPELINE, EXTRACTION_INSIGHT_TREE_REORGANIZE); + assertThat(observation(registry, EXTRACTION_INSIGHT_PIPELINE).requestAttributes()) + .containsEntry(MEMORY_ID_KEY, MEMORY_ID.toIdentifier()) + .containsEntry(INSIGHT_TYPE_KEY, TYPE_NAME); + assertThat(observation(registry, EXTRACTION_INSIGHT_TREE_REORGANIZE).requestAttributes()) + .containsEntry(INSIGHT_LEAF_COUNT_KEY, "1"); + } + @Test @DisplayName("flushSync should pass graph grouping context to categorized semantic routing") void categorizedSemanticGroupingShouldPassGroupingAssistContextToRouter() { @@ -519,6 +573,14 @@ private static boolean itemIdsMatch(List items, List expectedI return items.stream().map(MemoryItem::id).toList().equals(expectedIds); } + private static RecordedObservation observation( + RecordingObservationRegistry registry, String observationName) { + return registry.observations().stream() + .filter(observation -> observation.observationName().equals(observationName)) + .findFirst() + .orElseThrow(); + } + private static MemoryInsight existingLeaf(String content) { return new MemoryInsight( 10L, diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayerGraphTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayerGraphTest.java index 5950a0c3..f9a18605 100644 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayerGraphTest.java +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayerGraphTest.java @@ -106,7 +106,6 @@ void extractShouldDelegateCommittedItemBatchToGraphMaterializerAfterStoreBatch() .thenReturn(Mono.just(List.of(entry))); when(deduplicator.deduplicate(eq(memoryId), anyList())) .thenReturn(Mono.just(new DeduplicationResult(List.of(entry), List.of()))); - when(deduplicator.spanName()).thenReturn("test"); when(vector.storeBatch(eq(memoryId), anyList(), anyList())) .thenReturn(Mono.just(List.of("vec-1"))); var graphResult = @@ -205,7 +204,6 @@ void extractShouldDeleteStoredVectorsAndFailWhenGraphMaterializerFails() { .thenReturn(Mono.just(List.of(entry))); when(deduplicator.deduplicate(eq(memoryId), anyList())) .thenReturn(Mono.just(new DeduplicationResult(List.of(entry), List.of()))); - when(deduplicator.spanName()).thenReturn("test"); when(vector.storeBatch(eq(memoryId), anyList(), anyList())) .thenReturn(Mono.just(List.of("vec-1"))); when(graphMaterializer.materialize(eq(memoryId), anyList(), eq(List.of(entry)))) @@ -278,7 +276,6 @@ void extractShouldIgnoreGraphDiagnosticsAndStillPersistItemsWhenGraphMaterializa .thenReturn(Mono.just(List.of(entry))); when(deduplicator.deduplicate(eq(memoryId), anyList())) .thenReturn(Mono.just(new DeduplicationResult(List.of(entry), List.of()))); - when(deduplicator.spanName()).thenReturn("test"); when(vector.storeBatch(eq(memoryId), anyList(), anyList())) .thenReturn(Mono.just(List.of("vec-1"))); when(graphMaterializer.materialize(eq(memoryId), anyList(), eq(List.of(entry)))) @@ -355,7 +352,6 @@ void extractShouldPreserveWhenToUseForToolItemsOnly() { .thenReturn(Mono.just(List.of(entry))); when(deduplicator.deduplicate(eq(memoryId), anyList())) .thenReturn(Mono.just(new DeduplicationResult(List.of(entry), List.of()))); - when(deduplicator.spanName()).thenReturn("test"); when(vector.storeBatch(eq(memoryId), anyList(), anyList())) .thenReturn(Mono.just(List.of("vec-1"))); when(graphMaterializer.materialize(eq(memoryId), anyList(), eq(List.of(entry)))) diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayerLanguageTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayerLanguageTest.java index b1b18995..714fc000 100644 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayerLanguageTest.java +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayerLanguageTest.java @@ -100,7 +100,6 @@ void selfVerification_receives_extraction_language() { when(deduplicator.deduplicate( org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any())) .thenReturn(Mono.just(new DeduplicationResult(List.of(), List.of()))); - when(deduplicator.spanName()).thenReturn("test"); StepVerifier.create( layer.extract( @@ -178,7 +177,6 @@ void entriesOutsideAllowedCategoriesAreDroppedBeforeDeduplication() { when(deduplicator.deduplicate( org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any())) .thenReturn(Mono.just(new DeduplicationResult(List.of(), List.of()))); - when(deduplicator.spanName()).thenReturn("test"); StepVerifier.create( layer.extract( @@ -253,7 +251,6 @@ void extractShouldPersistNormalizedTemporalFieldsAndLegacyAnchor() { .thenReturn(Mono.just(List.of(temporalEntry))); when(deduplicator.deduplicate(eq(memoryId), anyList())) .thenReturn(Mono.just(new DeduplicationResult(List.of(temporalEntry), List.of()))); - when(deduplicator.spanName()).thenReturn("test"); when(vector.storeBatch(eq(memoryId), anyList(), anyList())) .thenReturn(Mono.just(List.of("vec-1"))); diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayerObservationTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayerObservationTest.java new file mode 100644 index 00000000..aa4c46aa --- /dev/null +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayerObservationTest.java @@ -0,0 +1,145 @@ +/* + * 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 com.openmemind.ai.memory.core.extraction.item; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.openmemind.ai.memory.core.data.DefaultMemoryId; +import com.openmemind.ai.memory.core.data.MemoryId; +import com.openmemind.ai.memory.core.data.enums.MemoryCategory; +import com.openmemind.ai.memory.core.data.enums.MemoryItemType; +import com.openmemind.ai.memory.core.data.enums.MemoryScope; +import com.openmemind.ai.memory.core.extraction.item.dedup.DeduplicationResult; +import com.openmemind.ai.memory.core.extraction.item.dedup.MemoryItemDeduplicator; +import com.openmemind.ai.memory.core.extraction.item.extractor.MemoryItemExtractor; +import com.openmemind.ai.memory.core.extraction.item.support.ExtractedMemoryEntry; +import com.openmemind.ai.memory.core.extraction.rawdata.ParsedSegment; +import com.openmemind.ai.memory.core.extraction.rawdata.content.ConversationContent; +import com.openmemind.ai.memory.core.extraction.result.RawDataResult; +import com.openmemind.ai.memory.core.store.InMemoryMemoryStore; +import com.openmemind.ai.memory.core.support.RecordingObservationRegistry; +import com.openmemind.ai.memory.core.vector.MemoryVector; +import com.openmemind.ai.memory.core.vector.VectorSearchResult; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class MemoryItemLayerObservationTest { + + @Test + void extractPublishesItemObservationWithItemCounts() { + var registry = new RecordingObservationRegistry(); + var extractor = mock(MemoryItemExtractor.class); + var deduplicator = mock(MemoryItemDeduplicator.class); + var store = new InMemoryMemoryStore(); + var layer = + new MemoryItemLayer( + extractor, deduplicator, store, new StubMemoryVector(), registry); + var memoryId = DefaultMemoryId.of("user1", "agent1"); + var segment = new ParsedSegment("hello", "hello caption", 0, 5, "raw-1", Map.of(), null); + var entry = + new ExtractedMemoryEntry( + "User likes Java", + 0.9f, + Instant.parse("2026-03-20T00:00:00Z"), + Instant.parse("2026-03-20T00:00:00Z"), + "raw-1", + null, + List.of("profile"), + Map.of(), + MemoryItemType.FACT, + MemoryCategory.PROFILE.name()); + var config = + new ItemExtractionConfig( + MemoryScope.USER, ConversationContent.TYPE, false, "English"); + when(extractor.extract(eq(List.of(segment)), anyList(), eq(config))) + .thenReturn(Mono.just(List.of(entry))); + when(deduplicator.deduplicate(any(), any())) + .thenReturn(Mono.just(new DeduplicationResult(List.of(entry), List.of()))); + + StepVerifier.create( + layer.extract( + memoryId, + new RawDataResult(List.of(), List.of(segment), false), + config)) + .assertNext(result -> assertThat(result.newItems()).hasSize(1)) + .verifyComplete(); + + assertThat(registry.observations()).hasSize(1); + var observation = registry.observations().getFirst(); + assertThat(observation.observationName()).isEqualTo("memind.extraction.item"); + assertThat(observation.requestAttributes()) + .containsEntry("memind.memory_id", memoryId.toIdentifier()); + assertThat(observation.resultAttributes()) + .containsEntry("memind.extraction.item_count", "1") + .containsEntry("memind.extraction.new_item_count", "1"); + } + + private static final class StubMemoryVector implements MemoryVector { + + @Override + public Mono store(MemoryId memoryId, String text, Map metadata) { + return Mono.just("vector-1"); + } + + @Override + public Mono> storeBatch( + MemoryId memoryId, List texts, List> metadataList) { + return Mono.just( + java.util.stream.IntStream.range(0, texts.size()) + .mapToObj(i -> "vector-" + i) + .toList()); + } + + @Override + public Mono delete(MemoryId memoryId, String vectorId) { + return Mono.empty(); + } + + @Override + public Mono deleteBatch(MemoryId memoryId, List vectorIds) { + return Mono.empty(); + } + + @Override + public Flux search(MemoryId memoryId, String query, int topK) { + return Flux.empty(); + } + + @Override + public Flux search( + MemoryId memoryId, String query, int topK, Map filter) { + return Flux.empty(); + } + + @Override + public Mono> embed(String text) { + return Mono.just(List.of(0.1f)); + } + + @Override + public Mono>> embedAll(List texts) { + return Mono.just(texts.stream().map(ignored -> List.of(0.1f)).toList()); + } + } +} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayerRuntimeContextTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayerRuntimeContextTest.java index 34763e58..a2bbecae 100644 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayerRuntimeContextTest.java +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/MemoryItemLayerRuntimeContextTest.java @@ -112,7 +112,6 @@ void selfVerificationUsesFirstUserNameAndLastObservedAtFromSegments() { when(deduplicator.deduplicate( org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any())) .thenReturn(Mono.just(new DeduplicationResult(List.of(), List.of()))); - when(deduplicator.spanName()).thenReturn("test"); StepVerifier.create( layer.extract( @@ -192,7 +191,6 @@ void selfVerificationMissedEntriesInheritSourceClientFromSegments() { .thenReturn(Mono.just(List.of(initialEntry))); when(deduplicator.deduplicate(eq(memoryId), entriesCaptor.capture())) .thenReturn(Mono.just(new DeduplicationResult(List.of(), List.of()))); - when(deduplicator.spanName()).thenReturn("test"); StepVerifier.create( layer.extract( diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/dedup/CompositeDeduplicatorObservationTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/dedup/CompositeDeduplicatorObservationTest.java new file mode 100644 index 00000000..aca283c2 --- /dev/null +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/item/dedup/CompositeDeduplicatorObservationTest.java @@ -0,0 +1,73 @@ +/* + * 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 com.openmemind.ai.memory.core.extraction.item.dedup; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.openmemind.ai.memory.core.data.DefaultMemoryId; +import com.openmemind.ai.memory.core.data.MemoryId; +import com.openmemind.ai.memory.core.extraction.item.support.ExtractedMemoryEntry; +import com.openmemind.ai.memory.core.support.RecordingObservationRegistry; +import java.util.List; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class CompositeDeduplicatorObservationTest { + + @Test + void deduplicatePublishesCompositeObservation() { + var registry = new RecordingObservationRegistry(); + var result = new DeduplicationResult(List.of(), List.of()); + var deduplicator = + new CompositeDeduplicator( + List.of(new StubDeduplicator("memind.extraction.item.dedup.hash", result)), + registry); + var memoryId = DefaultMemoryId.of("user1", "agent1"); + + StepVerifier.create(deduplicator.deduplicate(memoryId, List.of(entry()))) + .expectNext(result) + .verifyComplete(); + + assertThat(registry.observations()).hasSize(1); + var observation = registry.observations().getFirst(); + assertThat(observation.observationName()).isEqualTo("memind.extraction.item.dedup"); + assertThat(observation.requestAttributes()) + .containsEntry("memind.memory_id", memoryId.toIdentifier()); + } + + private static ExtractedMemoryEntry entry() { + return new ExtractedMemoryEntry( + "User likes Java", + 0.9f, + null, + null, + "raw-1", + null, + List.of(), + java.util.Map.of(), + null, + null); + } + + private record StubDeduplicator(String observationName, DeduplicationResult result) + implements MemoryItemDeduplicator { + + @Override + public Mono deduplicate( + MemoryId memoryId, List entries) { + return Mono.just(result); + } + } +} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/rawdata/RawDataLayerObservationTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/rawdata/RawDataLayerObservationTest.java new file mode 100644 index 00000000..c887a03c --- /dev/null +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/extraction/rawdata/RawDataLayerObservationTest.java @@ -0,0 +1,127 @@ +/* + * 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 com.openmemind.ai.memory.core.extraction.rawdata; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.openmemind.ai.memory.core.data.DefaultMemoryId; +import com.openmemind.ai.memory.core.data.MemoryId; +import com.openmemind.ai.memory.core.extraction.rawdata.caption.CaptionGenerator; +import com.openmemind.ai.memory.core.extraction.rawdata.content.ConversationContent; +import com.openmemind.ai.memory.core.extraction.rawdata.processor.ConversationContentProcessor; +import com.openmemind.ai.memory.core.extraction.rawdata.segment.Segment; +import com.openmemind.ai.memory.core.store.InMemoryMemoryStore; +import com.openmemind.ai.memory.core.support.RecordingObservationRegistry; +import com.openmemind.ai.memory.core.vector.MemoryVector; +import com.openmemind.ai.memory.core.vector.VectorSearchResult; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class RawDataLayerObservationTest { + + @Test + void extractPublishesRawDataObservationWithSegmentCount() { + var registry = new RecordingObservationRegistry(); + var processor = mock(ConversationContentProcessor.class); + var captionGenerator = mock(CaptionGenerator.class); + var segment = Segment.single("hello"); + when(processor.contentClass()).thenReturn(ConversationContent.class); + when(processor.chunk(any(ConversationContent.class))) + .thenReturn(Mono.just(List.of(segment))); + when(processor.captionGenerator()).thenReturn(captionGenerator); + when(captionGenerator.generateForSegments(any(), any())) + .thenReturn(Mono.just(List.of(segment.withCaption("hello caption")))); + var layer = + new RawDataLayer( + List.of(processor), + captionGenerator, + new InMemoryMemoryStore(), + new StubMemoryVector(), + 64, + registry); + var memoryId = DefaultMemoryId.of("user1", "agent1"); + + StepVerifier.create( + layer.extract( + memoryId, + ConversationContent.builder().addUserMessage("hello").build(), + ConversationContent.TYPE, + Map.of())) + .assertNext(result -> assertThat(result.segments()).hasSize(1)) + .verifyComplete(); + + assertThat(registry.observations()).hasSize(1); + var observation = registry.observations().getFirst(); + assertThat(observation.observationName()).isEqualTo("memind.extraction.rawdata"); + assertThat(observation.requestAttributes()) + .containsEntry("memind.memory_id", memoryId.toIdentifier()); + assertThat(observation.resultAttributes()) + .containsEntry("memind.extraction.segment_count", "1"); + } + + private static final class StubMemoryVector implements MemoryVector { + + @Override + public Mono store(MemoryId memoryId, String text, Map metadata) { + return Mono.just("vector-1"); + } + + @Override + public Mono> storeBatch( + MemoryId memoryId, List texts, List> metadataList) { + return Mono.just( + java.util.stream.IntStream.range(0, texts.size()) + .mapToObj(i -> "vector-" + i) + .toList()); + } + + @Override + public Mono delete(MemoryId memoryId, String vectorId) { + return Mono.empty(); + } + + @Override + public Mono deleteBatch(MemoryId memoryId, List vectorIds) { + return Mono.empty(); + } + + @Override + public Flux search(MemoryId memoryId, String query, int topK) { + return Flux.empty(); + } + + @Override + public Flux search( + MemoryId memoryId, String query, int topK, Map filter) { + return Flux.empty(); + } + + @Override + public Mono> embed(String text) { + return Mono.just(List.of(0.1f)); + } + + @Override + public Mono>> embedAll(List texts) { + return Mono.just(texts.stream().map(ignored -> List.of(0.1f)).toList()); + } + } +} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/metrics/ExtractionMetricsExtractorTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/metrics/ExtractionMetricsExtractorTest.java deleted file mode 100644 index 628b49f4..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/metrics/ExtractionMetricsExtractorTest.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.metrics; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.openmemind.ai.memory.core.data.DefaultMemoryId; -import com.openmemind.ai.memory.core.extraction.ExtractionResult; -import com.openmemind.ai.memory.core.extraction.item.graph.ItemGraphMaterializationResult; -import com.openmemind.ai.memory.core.extraction.rawdata.ParsedSegment; -import com.openmemind.ai.memory.core.extraction.result.InsightResult; -import com.openmemind.ai.memory.core.extraction.result.MemoryItemResult; -import com.openmemind.ai.memory.core.extraction.result.RawDataResult; -import java.time.Duration; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; - -class ExtractionMetricsExtractorTest { - - @Test - void extractsRawDataSegmentItemAndInsightCounts() { - var result = - ExtractionResult.success( - DefaultMemoryId.of("user", "agent"), - new RawDataResult( - List.of(), List.of(parsedSegment(), parsedSegment()), false), - MemoryItemResult.empty(), - InsightResult.empty(), - Duration.ofMillis(10)); - - ExtractionMetrics metrics = ExtractionMetricsExtractor.extract(result, "core"); - - assertThat(metrics.status()).isEqualTo("success"); - assertThat(metrics.rawDataCount()).isZero(); - assertThat(metrics.segmentCount()).isEqualTo(2); - assertThat(metrics.newItemCount()).isZero(); - assertThat(metrics.reinforcedItemCount()).isNull(); - assertThat(metrics.insightCount()).isZero(); - assertThat(metrics.graphRelationCount()).isZero(); - assertThat(metrics.source()).isEqualTo("core"); - } - - @Test - void sumsFinalRelationStatsWithoutMixingLowerLevelCreatedLinkCounters() { - var stats = - ItemGraphMaterializationResult.Stats.empty() - .withFinalRelationStats( - new ItemGraphMaterializationResult.Stats.FinalRelationStats( - 1, 2, 3, 4)); - var graphResult = new ItemGraphMaterializationResult(stats); - var result = - ExtractionResult.success( - DefaultMemoryId.of("user", "agent"), - RawDataResult.empty(), - new MemoryItemResult(List.of(), List.of(), graphResult), - InsightResult.empty(), - Duration.ofMillis(10)); - - ExtractionMetrics metrics = ExtractionMetricsExtractor.extract(result, "core"); - - assertThat(metrics.graphEntityCount()).isEqualTo(0); - assertThat(metrics.graphMentionCount()).isEqualTo(0); - assertThat(metrics.graphRelationCount()).isEqualTo(10); - } - - @Test - void handlesFailedExtractionWithoutGraphOrItemResults() { - var result = - ExtractionResult.failed(DefaultMemoryId.of("user", "agent"), Duration.ZERO, "boom"); - - ExtractionMetrics metrics = ExtractionMetricsExtractor.extract(result, "api"); - - assertThat(metrics.status()).isEqualTo("failed"); - assertThat(metrics.rawDataCount()).isZero(); - assertThat(metrics.segmentCount()).isNull(); - assertThat(metrics.newItemCount()).isZero(); - assertThat(metrics.reinforcedItemCount()).isNull(); - assertThat(metrics.graphRelationCount()).isNull(); - assertThat(metrics.source()).isEqualTo("api"); - } - - private static ParsedSegment parsedSegment() { - return new ParsedSegment("segment", null, 0, 7, null, Map.of()); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/metrics/NoopMemoryMetricsRecorderTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/metrics/NoopMemoryMetricsRecorderTest.java deleted file mode 100644 index 4377dde4..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/metrics/NoopMemoryMetricsRecorderTest.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.metrics; - -import static org.assertj.core.api.Assertions.assertThatCode; - -import org.junit.jupiter.api.Test; - -class NoopMemoryMetricsRecorderTest { - - @Test - void acceptsAllMetricPayloadsWithoutSideEffects() { - MemoryMetricsRecorder recorder = NoopMemoryMetricsRecorder.INSTANCE; - - assertThatCode( - () -> { - recorder.recordExtractionSummary( - new ExtractionMetrics( - "success", 1, null, 2, null, 3, 4, 5, 6, "core")); - recorder.recordRetrievalStage( - new RetrievalStageMetrics( - "simple", "tier", "item", "vector", "success", null, 10, - 5, false, false, "core")); - recorder.recordRetrievalMerge( - new RetrievalMergeMetrics( - "simple", 20, 12, 8, 3, "success", "core")); - recorder.recordRetrievalSummary( - new RetrievalSummaryMetrics( - "simple", "success", 5, 1, 0, 0, "core")); - }) - .doesNotThrowAnyException(); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/metrics/RetrievalMetricsSupportTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/metrics/RetrievalMetricsSupportTest.java deleted file mode 100644 index 05c68fec..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/metrics/RetrievalMetricsSupportTest.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.metrics; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.openmemind.ai.memory.core.retrieval.RetrievalResult; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -@DisplayName("RetrievalMetricsSupport Unit Test") -class RetrievalMetricsSupportTest { - - @Test - @DisplayName("summary should use status from retrieval result") - void summaryShouldUseStatusFromRetrievalResult() { - var summary = - RetrievalMetricsSupport.summary( - "simple", RetrievalResult.degraded("simple", "query"), "server"); - - assertThat(summary.status()).isEqualTo("degraded"); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/observation/MemoryObservationContextTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/observation/MemoryObservationContextTest.java new file mode 100644 index 00000000..7c37bc73 --- /dev/null +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/observation/MemoryObservationContextTest.java @@ -0,0 +1,69 @@ +/* + * 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 com.openmemind.ai.memory.core.observation; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.openmemind.ai.memory.core.retrieval.query.QueryContext; +import com.openmemind.ai.memory.core.retrieval.strategy.observation.SimpleRetrievalStrategyObservation; +import com.openmemind.ai.memory.core.retrieval.strategy.observation.SimpleRetrievalStrategyObservation.StrategyObservationContext; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class MemoryObservationContextTest { + + @Test + void mapsReactorCancellationToCancelledStatus() { + var observationRegistry = ObservationRegistry.create(); + observationRegistry + .observationConfig() + .observationHandler( + new ObservationHandler() { + @Override + public boolean supportsContext(Observation.Context context) { + return true; + } + }); + var observedContext = new AtomicReference(); + Mono observed = + SimpleRetrievalStrategyObservation.observe( + observationRegistry, + new QueryContext( + () -> "memory-1", + "query", + null, + List.of(), + Map.of(), + null, + Set.of()), + "simple", + context -> { + observedContext.set(context); + return Mono.never(); + }); + + StepVerifier.create(observed).thenCancel().verify(); + + assertThat(observedContext.get()).isNotNull(); + assertThat(observedContext.get().status()).isEqualTo("cancelled"); + } +} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/DefaultMemoryRetrieverTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/DefaultMemoryRetrieverTest.java index 14e2b40c..7172c96a 100644 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/DefaultMemoryRetrieverTest.java +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/DefaultMemoryRetrieverTest.java @@ -29,10 +29,16 @@ import com.openmemind.ai.memory.core.retrieval.query.QueryRewriter; import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; import com.openmemind.ai.memory.core.retrieval.strategy.RetrievalStrategy; +import com.openmemind.ai.memory.core.retrieval.trace.BoundedRetrievalTraceRecorder; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceObservationHandler; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceRecorder; import com.openmemind.ai.memory.core.store.MemoryStore; import com.openmemind.ai.memory.core.store.item.ItemOperations; import com.openmemind.ai.memory.core.support.TestMemoryIds; import com.openmemind.ai.memory.core.textsearch.MemoryTextSearch; +import io.micrometer.observation.ObservationRegistry; +import java.time.Duration; import java.util.List; import java.util.Map; import org.junit.jupiter.api.DisplayName; @@ -72,6 +78,55 @@ void strategyErrorShouldReturnDegradedStatus() { assertThat(result.status()).isEqualTo(RetrievalStatus.DEGRADED); } + @Test + @DisplayName("strategy timeout should record the final result as DEGRADED") + void strategyTimeoutShouldRecordFinalResultAsDegraded() { + var store = mock(MemoryStore.class); + var itemOperations = mock(ItemOperations.class); + var strategy = mock(RetrievalStrategy.class); + var observationRegistry = ObservationRegistry.create(); + observationRegistry + .observationConfig() + .observationHandler(new RetrievalTraceObservationHandler()); + var recorder = new BoundedRetrievalTraceRecorder(RetrievalTraceOptions.defaults()); + when(store.itemOperations()).thenReturn(itemOperations); + when(itemOperations.hasItems(memoryId)).thenReturn(true); + when(strategy.name()).thenReturn("simple"); + when(strategy.retrieve(any(), any())).thenReturn(Mono.never()); + + var retriever = + new DefaultMemoryRetriever( + store, + null, + null, + new com.openmemind.ai.memory.core.retrieval.admission + .DefaultRetrievalAdmissionPolicy( + com.openmemind.ai.memory.core.retrieval.admission + .RetrievalAdmissionOptions.defaults()), + com.openmemind.ai.memory.core.retrieval.admission.RetrievalAdmissionOptions + .defaults(), + null, + observationRegistry); + retriever.registerStrategy(strategy); + var config = RetrievalConfig.simple().withTimeout(Duration.ofMillis(10)); + var request = + new RetrievalRequest(memoryId, "hello", List.of(), config, Map.of(), null, null); + + var result = + retriever + .retrieve(request) + .contextWrite( + context -> context.put(RetrievalTraceRecorder.class, recorder)) + .block(); + + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(RetrievalStatus.DEGRADED); + assertThat(recorder.snapshot().orElseThrow().finalResults()) + .isNotNull() + .extracting(trace -> trace.status()) + .isEqualTo("degraded"); + } + @Test @DisplayName("successful retrieval with results should have SUCCESS status") void successfulRetrievalWithResultsShouldHaveSuccessStatus() { diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/deep/LlmTypedQueryExpanderTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/deep/LlmTypedQueryExpanderTest.java index 5b65149b..f436bcac 100644 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/deep/LlmTypedQueryExpanderTest.java +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/deep/LlmTypedQueryExpanderTest.java @@ -23,7 +23,12 @@ import com.openmemind.ai.memory.core.prompt.PromptType; import com.openmemind.ai.memory.core.prompt.retrieval.TypedQueryExpandPrompts; import com.openmemind.ai.memory.core.retrieval.deep.ExpandedQuery.QueryType; +import com.openmemind.ai.memory.core.retrieval.deep.observation.LlmTypedQueryExpanderObservation.MultiQueryExpandObservationContext; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -171,13 +176,37 @@ void shouldMapQueryTypesCaseInsensitively() { @Test @DisplayName("Should return an empty list on error when LLM call fails") void shouldReturnEmptyListOnError() { + var observedContext = new AtomicReference(); + var observationRegistry = ObservationRegistry.create(); + observationRegistry + .observationConfig() + .observationHandler( + new ObservationHandler() { + @Override + public void onStop(MultiQueryExpandObservationContext context) { + observedContext.set(context); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof MultiQueryExpandObservationContext; + } + }); var structuredLlmClient = fakeStructuredLlmClientThrowing(new RuntimeException("LLM unavailable")); - var expander = new LlmTypedQueryExpander(structuredLlmClient); + var expander = + new LlmTypedQueryExpander( + structuredLlmClient, + com.openmemind.ai.memory.core.prompt.PromptRegistry.EMPTY, + observationRegistry); StepVerifier.create(expander.expand("test", List.of("gap"), List.of(), List.of(), 3)) .assertNext(queries -> assertThat(queries).isEmpty()) .verifyComplete(); + + assertThat(observedContext.get()).isNotNull(); + assertThat(observedContext.get().degraded()).isTrue(); + assertThat(observedContext.get().status()).isEqualTo("degraded"); } @Test diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/scoring/DefaultRetrievalResultMergerObservationTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/scoring/DefaultRetrievalResultMergerObservationTest.java new file mode 100644 index 00000000..6939599b --- /dev/null +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/scoring/DefaultRetrievalResultMergerObservationTest.java @@ -0,0 +1,99 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.scoring; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.openmemind.ai.memory.core.retrieval.scoring.observation.DefaultRetrievalResultMergerObservation; +import com.openmemind.ai.memory.core.retrieval.scoring.observation.DefaultRetrievalResultMergerObservation.ResultMergeObservationContext; +import com.openmemind.ai.memory.core.support.RecordingObservationRegistry; +import io.micrometer.common.KeyValues; +import io.micrometer.observation.GlobalObservationConvention; +import io.micrometer.observation.Observation; +import java.util.List; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +class DefaultRetrievalResultMergerObservationTest { + + @Test + void recordsRemovedDuplicateCountAfterMerge() { + var registry = new RecordingObservationRegistry(); + var first = scored("1", 0.9); + var second = scored("2", 0.8); + var duplicate = scored("1", 0.7); + var merger = new DefaultRetrievalResultMerger(registry); + + var result = + merger.merge( + ScoringConfig.defaults(), + List.of(List.of(first, second), List.of(duplicate)), + 1.0, + 1.0) + .block(); + + assertThat(result).hasSize(2); + assertThat(registry.observations()) + .singleElement() + .satisfies( + observation -> + assertThat(observation.resultAttributes()) + .containsEntry("memind.retrieval.candidate_count", "3") + .containsEntry("memind.retrieval.result_count", "2") + .containsEntry("memind.retrieval.deduped_count", "1")); + } + + @Test + void usesMatchingGlobalConventionInsteadOfDefaultConvention() { + var registry = new RecordingObservationRegistry(); + registry.observationConfig().observationConvention(new ResultMergeGlobalConvention()); + + DefaultRetrievalResultMergerObservation.observe( + registry, + List.of(List.of(scored("1", 0.9))), + new double[] {1.0}, + () -> Mono.just(List.of(scored("1", 0.9)))) + .block(); + + assertThat(registry.observations()) + .singleElement() + .satisfies( + observation -> + assertThat(observation.resultAttributes()) + .containsEntry("test.global_convention", "applied")); + } + + private static ScoredResult scored(String id, double score) { + return new ScoredResult(ScoredResult.SourceType.ITEM, id, "item-" + id, 0.8f, score); + } + + private static final class ResultMergeGlobalConvention + implements GlobalObservationConvention { + + @Override + public String getName() { + return "test.result_merge"; + } + + @Override + public KeyValues getHighCardinalityKeyValues(ResultMergeObservationContext context) { + return KeyValues.of("test.global_convention", "applied"); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof ResultMergeObservationContext; + } + } +} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/trace/BoundedRetrievalTraceRecorderTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/trace/BoundedRetrievalTraceRecorderTest.java new file mode 100644 index 00000000..9d26c7b3 --- /dev/null +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/trace/BoundedRetrievalTraceRecorderTest.java @@ -0,0 +1,108 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.trace; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class BoundedRetrievalTraceRecorderTest { + + private static final Instant STARTED_AT = Instant.parse("2026-08-05T00:00:00Z"); + private static final Instant COMPLETED_AT = Instant.parse("2026-08-05T00:00:01Z"); + + @Test + void recordStageEventAppendsBoundedStageTrace() { + var collector = + new BoundedRetrievalTraceRecorder( + "trace-1", STARTED_AT, new RetrievalTraceOptions(8, 4, 16)); + + collector.record( + event( + new RetrievalTraceEvent.StagePayload( + "tier", + "item", + "vector", + 3, + 2, + 1, + false, + false, + Map.of("source", "vector"), + List.of( + new RetrievalTraceEvent.CandidatePayload( + "item", 1, 0.9d, 0.8f, "candidate"))))); + + var trace = collector.snapshot().orElseThrow(); + assertThat(trace.traceId()).isEqualTo("trace-1"); + assertThat(trace.stages()).hasSize(1); + assertThat(trace.stages().getFirst().stage()).isEqualTo("tier"); + assertThat(trace.stages().getFirst().candidateCount()).isEqualTo(2); + assertThat(trace.stages().getFirst().candidates()) + .extracting(RetrievalCandidateTrace::textPreview) + .containsExactly("candidate"); + } + + @Test + void recordMergeAndFinalEventsPopulateSnapshotSections() { + var collector = + new BoundedRetrievalTraceRecorder( + "trace-1", STARTED_AT, RetrievalTraceOptions.defaults()); + + collector.record(event(new RetrievalTraceEvent.MergePayload(6, 4, 2, 3))); + collector.record(event(new RetrievalTraceEvent.FinalPayload("simple", 2, 1, 0, 3))); + + var trace = collector.snapshot().orElseThrow(); + assertThat(trace.merge()).isEqualTo(new RetrievalMergeTrace(6, 4, 2, 3, "success")); + assertThat(trace.finalResults()) + .isEqualTo(new RetrievalFinalTrace("simple", "success", 2, 1, 0, 3)); + } + + @Test + void recordStageEventMarksTraceTruncatedWhenStageLimitExceeded() { + var collector = + new BoundedRetrievalTraceRecorder( + "trace-1", STARTED_AT, new RetrievalTraceOptions(1, 4, 16)); + + collector.record(stageEvent("first")); + collector.record(stageEvent("second")); + + var trace = collector.snapshot().orElseThrow(); + assertThat(trace.stages()).hasSize(1); + assertThat(trace.stages().getFirst().method()).isEqualTo("first"); + assertThat(trace.truncated()).isTrue(); + } + + private static RetrievalTraceEvent stageEvent(String method) { + return event( + new RetrievalTraceEvent.StagePayload( + "tier", "item", method, null, null, 0, false, false, Map.of(), List.of())); + } + + private static RetrievalTraceEvent event(RetrievalTraceEvent.Payload payload) { + return new RetrievalTraceEvent( + "memind.test", + "memind.test", + "success", + STARTED_AT, + COMPLETED_AT, + 1000L, + Map.of("operation", "retrieval"), + Map.of(), + payload); + } +} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalStageObservationCoverageTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalStageObservationCoverageTest.java new file mode 100644 index 00000000..d2c8ea14 --- /dev/null +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalStageObservationCoverageTest.java @@ -0,0 +1,169 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.trace; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.openmemind.ai.memory.core.data.DefaultMemoryId; +import com.openmemind.ai.memory.core.llm.rerank.observation.LlmRerankerObservation; +import com.openmemind.ai.memory.core.llm.rerank.observation.LlmRerankerObservation.RerankObservationContext; +import com.openmemind.ai.memory.core.retrieval.deep.ExpandedQuery; +import com.openmemind.ai.memory.core.retrieval.deep.observation.LlmTypedQueryExpanderObservation; +import com.openmemind.ai.memory.core.retrieval.graph.RetrievalGraphAssistResult; +import com.openmemind.ai.memory.core.retrieval.graph.observation.DefaultRetrievalGraphAssistantObservation; +import com.openmemind.ai.memory.core.retrieval.query.QueryContext; +import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; +import com.openmemind.ai.memory.core.retrieval.strategy.SimpleStrategyConfig; +import com.openmemind.ai.memory.core.retrieval.sufficiency.SufficiencyResult; +import com.openmemind.ai.memory.core.retrieval.sufficiency.observation.LlmSufficiencyGateObservation; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +class RetrievalStageObservationCoverageTest { + + private static final Instant STARTED_AT = Instant.parse("2026-08-05T00:00:00Z"); + + @Test + void recordsDocumentedRetrievalStageObservationsFromReactorRecorder() { + var registry = ObservationRegistry.create(); + registry.observationConfig().observationHandler(new RetrievalTraceObservationHandler()); + var recorder = + new BoundedRetrievalTraceRecorder( + "trace-1", STARTED_AT, new RetrievalTraceOptions(16, 2, 16)); + var queryContext = + new QueryContext( + DefaultMemoryId.of("u1", "a1"), + "coffee", + null, + List.of(), + Map.of(), + null, + null); + var direct = scored("1", "direct coffee memory"); + var reranked = scored("2", "reranked coffee memory"); + + LlmTypedQueryExpanderObservation.observe( + registry, + "coffee", + List.of("preference detail"), + List.of("likes coffee"), + List.of("history"), + 3, + ignored -> + Mono.just( + List.of( + new ExpandedQuery( + ExpandedQuery.QueryType.VEC, + "coffee preference")))) + .contextWrite(context -> context.put(RetrievalTraceRecorder.class, recorder)) + .block(); + LlmSufficiencyGateObservation.observe( + registry, + queryContext, + List.of(direct), + ignored -> + Mono.just( + new SufficiencyResult( + false, + "needs more", + List.of(), + List.of("time range"), + List.of("coffee")))) + .contextWrite(context -> context.put(RetrievalTraceRecorder.class, recorder)) + .block(); + LlmRerankerObservation.observe( + registry, + "coffee", + List.of(direct), + 1, + ignored -> Mono.just(List.of(reranked))) + .contextWrite(context -> context.put(RetrievalTraceRecorder.class, recorder)) + .block(); + DefaultRetrievalGraphAssistantObservation.observe( + registry, + queryContext, + SimpleStrategyConfig.GraphAssistConfig.defaults(), + List.of(direct), + ignored -> + Mono.just( + new RetrievalGraphAssistResult( + List.of(direct, reranked), + new RetrievalGraphAssistResult.GraphAssistStats( + true, false, false, 1, 2, 0, 2, 1, 0, 0, + 0)))) + .contextWrite(context -> context.put(RetrievalTraceRecorder.class, recorder)) + .block(); + + var trace = recorder.snapshot().orElseThrow(); + assertThat(trace.stages()) + .extracting(RetrievalStageTrace::stage) + .contains("query_expand", "sufficiency", "rerank", "graph_assist"); + assertThat(trace.stages()) + .filteredOn(stage -> "graph_assist".equals(stage.stage())) + .singleElement() + .satisfies( + stage -> { + assertThat(stage.resultCount()).isEqualTo(2); + assertThat(stage.candidates()).hasSize(2); + }); + } + + @Test + void recordsRerankFallbackAsDegraded() { + var registry = ObservationRegistry.create(); + var observedContext = new AtomicReference(); + registry.observationConfig() + .observationHandler( + new ObservationHandler() { + @Override + public void onStop(Observation.Context context) { + if (context instanceof RerankObservationContext rerank) { + observedContext.set(rerank); + } + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof RerankObservationContext; + } + }); + var candidate = scored("1", "candidate"); + + LlmRerankerObservation.observe( + registry, + "coffee", + List.of(candidate), + 1, + context -> { + context.markDegraded(); + return Mono.just(List.of(candidate)); + }) + .block(); + + assertThat(observedContext.get()).isNotNull(); + assertThat(observedContext.get().degraded()).isTrue(); + assertThat(observedContext.get().status()).isEqualTo("degraded"); + } + + private static ScoredResult scored(String sourceId, String text) { + return new ScoredResult(ScoredResult.SourceType.ITEM, sourceId, text, 0.8F, 0.9D); + } +} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceObservationHandlerTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceObservationHandlerTest.java new file mode 100644 index 00000000..e41b93d2 --- /dev/null +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/retrieval/trace/RetrievalTraceObservationHandlerTest.java @@ -0,0 +1,81 @@ +/* + * 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 com.openmemind.ai.memory.core.retrieval.trace; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import reactor.util.context.Context; +import reactor.util.context.ContextView; + +class RetrievalTraceObservationHandlerTest { + + @Test + void recordsEventFromObservationContextRecorderOnStop() { + var registry = ObservationRegistry.create(); + registry.observationConfig().observationHandler(new RetrievalTraceObservationHandler()); + var recorder = new BoundedRetrievalTraceRecorder(new RetrievalTraceOptions(8, 4, 16)); + var context = new TestTraceContext(Context.of(RetrievalTraceRecorder.class, recorder)); + + Observation.createNotStarted("memind.test", () -> context, registry).observe(() -> {}); + + var trace = recorder.snapshot().orElseThrow(); + assertThat(trace.stages()).hasSize(1); + assertThat(trace.stages().getFirst().stage()).isEqualTo("tier"); + assertThat(trace.stages().getFirst().durationMillis()).isNotNegative(); + } + + @Test + void ignoresObservationContextsWithoutTraceRecorder() { + var registry = ObservationRegistry.create(); + registry.observationConfig().observationHandler(new RetrievalTraceObservationHandler()); + var context = new TestTraceContext(Context.empty()); + + Observation.createNotStarted("memind.test", () -> context, registry).observe(() -> {}); + + assertThat(context.traceRecorder()).isEmpty(); + } + + private static final class TestTraceContext extends MemoryObservationContext + implements RetrievalTraceEventSource { + + private TestTraceContext(ContextView reactorContext) { + super(reactorContext); + } + + @Override + public Optional toRetrievalTraceEvent( + ObservationTiming timing, RetrievalTraceOptions options) { + return Optional.of( + new RetrievalTraceEvent( + "memind.test", + "memind.test", + status(), + timing.startedAt(), + timing.completedAt(), + timing.durationMillis(), + Map.of("operation", "retrieval", "stage", "tier"), + Map.of("memind.retrieval.result_count", "1"), + new RetrievalTraceEvent.StagePayload( + "tier", "item", "vector", null, null, 1, false, false, Map.of(), + List.of()))); + } + } +} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/support/ObservationDocumentationSchemaTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/support/ObservationDocumentationSchemaTest.java new file mode 100644 index 00000000..e56f9968 --- /dev/null +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/support/ObservationDocumentationSchemaTest.java @@ -0,0 +1,151 @@ +/* + * 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 com.openmemind.ai.memory.core.support; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.openmemind.ai.memory.core.extraction.DefaultMemoryExtractor; +import com.openmemind.ai.memory.core.extraction.insight.InsightLayer; +import com.openmemind.ai.memory.core.extraction.insight.generator.LlmInsightGenerator; +import com.openmemind.ai.memory.core.extraction.insight.group.LlmInsightGroupClassifier; +import com.openmemind.ai.memory.core.extraction.insight.scheduler.InsightBuildScheduler; +import com.openmemind.ai.memory.core.extraction.item.MemoryItemLayer; +import com.openmemind.ai.memory.core.extraction.item.dedup.CompositeDeduplicator; +import com.openmemind.ai.memory.core.extraction.item.graph.pipeline.DefaultItemGraphMaterializer; +import com.openmemind.ai.memory.core.extraction.rawdata.RawDataLayer; +import com.openmemind.ai.memory.core.llm.rerank.LlmReranker; +import com.openmemind.ai.memory.core.retrieval.DefaultMemoryRetriever; +import com.openmemind.ai.memory.core.retrieval.deep.LlmTypedQueryExpander; +import com.openmemind.ai.memory.core.retrieval.graph.DefaultGraphItemChannel; +import com.openmemind.ai.memory.core.retrieval.graph.DefaultRetrievalGraphAssistant; +import com.openmemind.ai.memory.core.retrieval.scoring.DefaultRetrievalResultMerger; +import com.openmemind.ai.memory.core.retrieval.strategy.DeepRetrievalStrategy; +import com.openmemind.ai.memory.core.retrieval.strategy.SimpleRetrievalStrategy; +import com.openmemind.ai.memory.core.retrieval.sufficiency.LlmSufficiencyGate; +import com.openmemind.ai.memory.core.retrieval.temporal.DefaultTemporalItemChannel; +import com.openmemind.ai.memory.core.retrieval.thread.DefaultMemoryThreadAssistant; +import com.openmemind.ai.memory.core.retrieval.tier.InsightTierRetriever; +import com.openmemind.ai.memory.core.retrieval.tier.ItemTierRetriever; +import com.openmemind.ai.memory.core.retrieval.tier.LlmInsightTypeRouter; +import io.micrometer.observation.Observation; +import io.micrometer.observation.docs.ObservationDocumentation; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ObservationDocumentationSchemaTest { + + private static final List> OBSERVED_COMPONENTS = + List.of( + DefaultMemoryExtractor.class, + RawDataLayer.class, + MemoryItemLayer.class, + CompositeDeduplicator.class, + DefaultItemGraphMaterializer.class, + InsightLayer.class, + LlmInsightGenerator.class, + LlmInsightGroupClassifier.class, + InsightBuildScheduler.class, + LlmReranker.class, + DefaultMemoryRetriever.class, + LlmTypedQueryExpander.class, + DefaultGraphItemChannel.class, + DefaultRetrievalGraphAssistant.class, + DefaultRetrievalResultMerger.class, + SimpleRetrievalStrategy.class, + DeepRetrievalStrategy.class, + LlmSufficiencyGate.class, + DefaultTemporalItemChannel.class, + DefaultMemoryThreadAssistant.class, + InsightTierRetriever.class, + ItemTierRetriever.class, + LlmInsightTypeRouter.class); + + @Test + void observedDocumentsDeclareHighCardinalityKeySchema() { + var missingDocuments = new ArrayList(); + + for (var component : OBSERVED_COMPONENTS) { + if (!declaresHighCardinalityKeyNames(component)) { + continue; + } + for (var document : documents(component)) { + if (document.getHighCardinalityKeyNames().length == 0) { + missingDocuments.add( + observationHolder(component).getName() + "." + document.getName()); + } + } + } + + assertThat(missingDocuments).isEmpty(); + } + + @Test + void observedComponentsKeepObservationContractsInObservationSubpackage() { + var misplacedContracts = new ArrayList(); + + for (var component : OBSERVED_COMPONENTS) { + for (var nestedClass : component.getDeclaredClasses()) { + if (ObservationDocumentation.class.isAssignableFrom(nestedClass) + || Observation.Context.class.isAssignableFrom(nestedClass)) { + misplacedContracts.add(component.getName() + "." + nestedClass.getSimpleName()); + } + } + observationHolder(component); + } + + assertThat(misplacedContracts).isEmpty(); + } + + private static List documents(Class component) { + var documents = new ArrayList(); + for (var nestedClass : observationHolder(component).getDeclaredClasses()) { + if (!ObservationDocumentation.class.isAssignableFrom(nestedClass) + || !nestedClass.isEnum()) { + continue; + } + for (var constant : nestedClass.getEnumConstants()) { + documents.add((ObservationDocumentation) constant); + } + } + assertThat(documents) + .as( + observationHolder(component).getName() + + " should declare local observation documents") + .isNotEmpty(); + return documents; + } + + private static boolean declaresHighCardinalityKeyNames(Class component) { + for (var nestedClass : observationHolder(component).getDeclaredClasses()) { + if (nestedClass.getSimpleName().equals("HighCardinalityKeyNames")) { + return true; + } + } + return false; + } + + private static Class observationHolder(Class component) { + var holderName = + component.getPackageName() + + ".observation." + + component.getSimpleName() + + "Observation"; + try { + return Class.forName(holderName); + } catch (ClassNotFoundException ex) { + throw new AssertionError("Missing observation holder " + holderName, ex); + } + } +} diff --git a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/RetrievalMergeMetrics.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/support/RecordedObservation.java similarity index 68% rename from memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/RetrievalMergeMetrics.java rename to memind-core/src/test/java/com/openmemind/ai/memory/core/support/RecordedObservation.java index 9d76463e..3df81a6f 100644 --- a/memind-core/src/main/java/com/openmemind/ai/memory/core/metrics/RetrievalMergeMetrics.java +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/support/RecordedObservation.java @@ -11,13 +11,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.openmemind.ai.memory.core.metrics; +package com.openmemind.ai.memory.core.support; -public record RetrievalMergeMetrics( - String strategy, - int inputCount, - int outputCount, - int deduplicatedCount, - int sourceCount, - String status, - String source) {} +import java.util.Map; + +public record RecordedObservation( + String observationName, + Map requestAttributes, + Map resultAttributes) {} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/support/RecordingMemoryObserver.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/support/RecordingMemoryObserver.java deleted file mode 100644 index 0552fbed..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/support/RecordingMemoryObserver.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.support; - -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.ObservationContext; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Supplier; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -public final class RecordingMemoryObserver implements MemoryObserver { - - private final List> monoContexts = new ArrayList<>(); - private final List> fluxContexts = new ArrayList<>(); - - @Override - public Mono observeMono(ObservationContext ctx, Supplier> operation) { - monoContexts.add(ctx); - return operation.get(); - } - - @Override - public Flux observeFlux(ObservationContext ctx, Supplier> operation) { - fluxContexts.add(ctx); - return operation.get(); - } - - public List> monoContexts() { - return List.copyOf(monoContexts); - } - - public List> fluxContexts() { - return List.copyOf(fluxContexts); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/support/RecordingObservationRegistry.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/support/RecordingObservationRegistry.java new file mode 100644 index 00000000..966da566 --- /dev/null +++ b/memind-core/src/test/java/com/openmemind/ai/memory/core/support/RecordingObservationRegistry.java @@ -0,0 +1,99 @@ +/* + * 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 com.openmemind.ai.memory.core.support; + +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; + +public final class RecordingObservationRegistry implements ObservationRegistry { + + private final ObservationRegistry delegate = ObservationRegistry.create(); + private final Map> started = new IdentityHashMap<>(); + private final List observations = new CopyOnWriteArrayList<>(); + + public RecordingObservationRegistry() { + delegate.observationConfig().observationHandler(new RecordingObservationHandler()); + } + + @Override + public Observation getCurrentObservation() { + return delegate.getCurrentObservation(); + } + + @Override + public Observation.Scope getCurrentObservationScope() { + return delegate.getCurrentObservationScope(); + } + + @Override + public void setCurrentObservationScope(Observation.Scope current) { + delegate.setCurrentObservationScope(current); + } + + @Override + public ObservationRegistry.ObservationConfig observationConfig() { + return delegate.observationConfig(); + } + + public List observations() { + return List.copyOf(observations); + } + + public List monoContexts() { + return observations(); + } + + private static Map toMap(KeyValues keyValues) { + Map values = new LinkedHashMap<>(); + keyValues.forEach(keyValue -> values.put(keyValue.getKey(), keyValue.getValue())); + return Map.copyOf(values); + } + + private final class RecordingObservationHandler + implements ObservationHandler { + + @Override + public void onStart(Observation.Context context) { + synchronized (started) { + started.put(context, toMap(context.getAllKeyValues())); + } + } + + @Override + public void onStop(Observation.Context context) { + Map requestAttributes; + synchronized (started) { + requestAttributes = started.remove(context); + } + Map finalAttributes = toMap(context.getAllKeyValues()); + observations.add( + new RecordedObservation( + context.getName(), + requestAttributes == null ? Map.of() : requestAttributes, + finalAttributes)); + } + + @Override + public boolean supportsContext(Observation.Context context) { + return true; + } + } +} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/NoopMemoryObserverTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/NoopMemoryObserverTest.java deleted file mode 100644 index e24aa13c..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/NoopMemoryObserverTest.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import reactor.test.StepVerifier; - -class NoopMemoryObserverTest { - - private final NoopMemoryObserver observer = new NoopMemoryObserver(); - - @Nested - @DisplayName("observeMono()") - class ObserveMonoTests { - - @Test - @DisplayName("Directly return supplier result, no additional wrapping") - void directDelegation() { - var ctx = ObservationContext.of("test.span"); - StepVerifier.create( - observer.observeMono( - ctx, () -> reactor.core.publisher.Mono.just("hello"))) - .expectNext("hello") - .verifyComplete(); - } - } - - @Nested - @DisplayName("observeFlux()") - class ObserveFluxTests { - - @Test - @DisplayName("Directly return supplier result, no additional wrapping") - void directDelegation() { - var ctx = ObservationContext.of("test.span"); - StepVerifier.create( - observer.observeFlux( - ctx, () -> reactor.core.publisher.Flux.just("a", "b"))) - .expectNext("a", "b") - .verifyComplete(); - } - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/ObservationContextTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/ObservationContextTest.java deleted file mode 100644 index a543e8aa..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/ObservationContextTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.Map; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -class ObservationContextTest { - - @Nested - @DisplayName("of() factory method") - class OfTests { - - @Test - @DisplayName("only spanName — attributes are empty, extractor is none") - void spanNameOnly() { - ObservationContext ctx = ObservationContext.of("test.span"); - assertThat(ctx.spanName()).isEqualTo("test.span"); - assertThat(ctx.requestAttributes()).isEmpty(); - assertThat(ctx.resultExtractor().extract("x")).isEmpty(); - } - - @Test - @DisplayName("spanName + attrs") - void withAttrs() { - var attrs = Map.of("key", "value"); - ObservationContext ctx = ObservationContext.of("test.span", attrs); - assertThat(ctx.requestAttributes()).containsEntry("key", "value"); - } - } - - @Nested - @DisplayName("withResultExtractor()") - class WithExtractorTests { - - @Test - @DisplayName("after replacing extractor, can extract result attributes") - void replacesExtractor() { - ObservationContext ctx = - ObservationContext.of("test.span") - .withResultExtractor(s -> Map.of("len", s.length())); - assertThat(ctx.resultExtractor().extract("hi")).containsEntry("len", 2); - } - - @Test - @DisplayName("original context is immutable") - void immutable() { - ObservationContext original = ObservationContext.of("test.span"); - ObservationContext derived = - original.withResultExtractor(s -> Map.of("len", s.length())); - assertThat(original.resultExtractor().extract("hi")).isEmpty(); - assertThat(derived.resultExtractor().extract("hi")).containsEntry("len", 2); - } - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/ResultAttributeExtractorTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/ResultAttributeExtractorTest.java deleted file mode 100644 index 9830898e..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/ResultAttributeExtractorTest.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.Map; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -class ResultAttributeExtractorTest { - - @Nested - @DisplayName("none()") - class NoneTests { - - @Test - @DisplayName("returns empty map") - void returnsEmptyMap() { - ResultAttributeExtractor extractor = ResultAttributeExtractor.none(); - assertThat(extractor.extract("anything")).isEmpty(); - } - - @Test - @DisplayName("passing null returns empty map") - void handlesNull() { - ResultAttributeExtractor extractor = ResultAttributeExtractor.none(); - assertThat(extractor.extract(null)).isEmpty(); - } - } - - @Nested - @DisplayName("custom extractor") - class CustomTests { - - @Test - @DisplayName("extract result attributes") - void extractsAttributes() { - ResultAttributeExtractor extractor = s -> Map.of("length", s.length()); - assertThat(extractor.extract("hello")).containsEntry("length", 5); - } - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/TracingSupportTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/TracingSupportTest.java deleted file mode 100644 index a80f31d6..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/TracingSupportTest.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Map; -import java.util.function.Supplier; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingSupportTest { - - // Specific subclass for testing - static class TestTracingSupport extends TracingSupport { - TestTracingSupport(MemoryObserver observer) { - super(observer); - } - - Mono doTrace(String spanName) { - return trace(spanName, Map.of(), () -> Mono.just("result")); - } - - Mono doTraceWithExtractor(String spanName) { - return trace( - spanName, Map.of(), s -> Map.of("len", s.length()), () -> Mono.just("hello")); - } - - Flux doTraceFlux(String spanName) { - return traceFlux(spanName, Map.of(), () -> Flux.just("a", "b")); - } - } - - @Test - @DisplayName("trace() delegates to observer.observeMono()") - void traceDelegatesToObserveMono() { - var observer = new NoopMemoryObserver(); - var support = new TestTracingSupport(observer); - - StepVerifier.create(support.doTrace("test.span")).expectNext("result").verifyComplete(); - } - - @SuppressWarnings("unchecked") - @Test - @DisplayName("trace() with ResultExtractor passes extractor") - void traceWithExtractorPassesExtractor() { - var observer = mock(MemoryObserver.class); - when(observer.observeMono(any(ObservationContext.class), any(Supplier.class))) - .thenAnswer(inv -> ((Supplier>) inv.getArgument(1)).get()); - var support = new TestTracingSupport(observer); - - StepVerifier.create(support.doTraceWithExtractor("test.span")) - .expectNext("hello") - .verifyComplete(); - - verify(observer) - .observeMono( - argThat(ctx -> ctx.resultExtractor().extract("hi").containsKey("len")), - any()); - } - - @Test - @DisplayName("traceFlux() delegates to observer.observeFlux()") - void traceFluxDelegatesToObserveFlux() { - var observer = new NoopMemoryObserver(); - var support = new TestTracingSupport(observer); - - StepVerifier.create(support.doTraceFlux("test.span")).expectNext("a", "b").verifyComplete(); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingGraphItemChannelTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingGraphItemChannelTest.java deleted file mode 100644 index c0b55c48..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingGraphItemChannelTest.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.MEMORY_ID; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_CHANNEL; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_GRAPH_DEGRADED; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_GRAPH_ENABLED; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_GRAPH_SEED_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_GRAPH_TIMEOUT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_RESULT_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.RETRIEVAL_GRAPH_CHANNEL; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.openmemind.ai.memory.core.data.DefaultMemoryId; -import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; -import com.openmemind.ai.memory.core.retrieval.graph.GraphExpansionResult; -import com.openmemind.ai.memory.core.retrieval.graph.GraphItemChannel; -import com.openmemind.ai.memory.core.retrieval.query.QueryContext; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.strategy.SimpleStrategyConfig; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import com.openmemind.ai.memory.core.tracing.ObservationContext; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingGraphItemChannelTest { - - @Test - void retrievePublishesGraphChannelSpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - GraphItemChannel delegate = mock(GraphItemChannel.class); - var context = queryContext(); - var config = RetrievalConfig.simple(); - var settings = SimpleStrategyConfig.GraphAssistConfig.defaults(); - var seed = scoredResult("seed-1"); - var graphResult = scoredResult("graph-1"); - var result = - new GraphExpansionResult( - List.of(graphResult), true, false, false, 1, 2, 3, 4, 5, 6); - when(delegate.retrieve(context, config, settings, List.of(seed))) - .thenReturn(Mono.just(result)); - - var traced = new TracingGraphItemChannel(delegate, observer); - - StepVerifier.create(traced.retrieve(context, config, settings, List.of(seed))) - .expectNext(result) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - var observation = observer.monoContexts().getFirst(); - assertThat(observation.spanName()).isEqualTo(RETRIEVAL_GRAPH_CHANNEL); - assertThat(observation.requestAttributes()) - .containsEntry(MEMORY_ID, "memory") - .containsEntry(RETRIEVAL_CHANNEL, "graph") - .containsEntry(RETRIEVAL_GRAPH_ENABLED, true) - .containsEntry(RETRIEVAL_GRAPH_SEED_COUNT, 1); - assertThat(resultAttributes(observation, result)) - .containsEntry(RETRIEVAL_RESULT_COUNT, 1) - .containsEntry(RETRIEVAL_GRAPH_TIMEOUT, false) - .containsEntry(RETRIEVAL_GRAPH_DEGRADED, false); - } - - private QueryContext queryContext() { - return new QueryContext( - DefaultMemoryId.of("memory", null), "query", null, List.of(), Map.of(), null, null); - } - - private ScoredResult scoredResult(String id) { - return new ScoredResult(ScoredResult.SourceType.ITEM, id, "text", 0.9f, 0.8d); - } - - @SuppressWarnings("unchecked") - private Map resultAttributes( - ObservationContext context, GraphExpansionResult result) { - return ((ObservationContext) context) - .resultExtractor() - .extract(result); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightExtractStepTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightExtractStepTest.java deleted file mode 100644 index 4779f950..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightExtractStepTest.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.MEMORY_ID; -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.EXTRACTION_INSIGHT; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.openmemind.ai.memory.core.extraction.result.InsightResult; -import com.openmemind.ai.memory.core.extraction.result.MemoryItemResult; -import com.openmemind.ai.memory.core.extraction.step.InsightExtractStep; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import com.openmemind.ai.memory.core.support.TestMemoryIds; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingInsightExtractStepTest { - - @Test - void extractPublishesInsightSpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(InsightExtractStep.class); - var result = InsightResult.empty(); - when(delegate.extract(any(), any())).thenReturn(Mono.just(result)); - - var traced = new TracingInsightExtractStep(delegate, observer); - var memoryId = TestMemoryIds.userAgent(); - - StepVerifier.create(traced.extract(memoryId, MemoryItemResult.empty())) - .expectNext(result) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().spanName()).isEqualTo(EXTRACTION_INSIGHT); - assertThat(observer.monoContexts().getFirst().requestAttributes()) - .containsEntry(MEMORY_ID, memoryId.toIdentifier()); - } - - @Test - void extractPropagatesDelegateErrorsThroughObserver() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(InsightExtractStep.class); - when(delegate.extract(any(), any())) - .thenReturn(Mono.error(new IllegalStateException("boom"))); - - var traced = new TracingInsightExtractStep(delegate, observer); - - StepVerifier.create(traced.extract(TestMemoryIds.userAgent(), MemoryItemResult.empty())) - .expectErrorMessage("boom") - .verify(); - - assertThat(observer.monoContexts()).hasSize(1); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightGeneratorTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightGeneratorTest.java deleted file mode 100644 index 3ef946d1..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightGeneratorTest.java +++ /dev/null @@ -1,342 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.EXTRACTION_INSIGHT_ADD_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.EXTRACTION_INSIGHT_DELETE_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.EXTRACTION_INSIGHT_GROUP_NAME; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.EXTRACTION_INSIGHT_LEAF_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.EXTRACTION_INSIGHT_TYPE; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.EXTRACTION_INSIGHT_UPDATE_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.EXTRACTION_INSIGHT_GENERATE_BRANCH; -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.EXTRACTION_INSIGHT_GENERATE_LEAF; -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.EXTRACTION_INSIGHT_GENERATE_ROOT; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.openmemind.ai.memory.core.data.InsightPoint; -import com.openmemind.ai.memory.core.data.MemoryInsight; -import com.openmemind.ai.memory.core.data.MemoryInsightType; -import com.openmemind.ai.memory.core.data.PointOperation; -import com.openmemind.ai.memory.core.extraction.insight.generator.InsightGenerator; -import com.openmemind.ai.memory.core.extraction.insight.generator.InsightPointGenerateResponse; -import com.openmemind.ai.memory.core.extraction.insight.generator.InsightPointOpsResponse; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import com.openmemind.ai.memory.core.tracing.ObservationContext; -import java.util.List; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingInsightGeneratorTest { - - @Test - void generatePointsPublishesLeafSpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(InsightGenerator.class); - var response = new InsightPointGenerateResponse(List.of()); - when(delegate.generatePoints(any(), any(), any(), any(), anyInt(), any(), any())) - .thenReturn(Mono.just(response)); - var insightType = insightType(); - - var traced = new TracingInsightGenerator(delegate, observer); - - StepVerifier.create( - traced.generatePoints( - insightType, "group-a", List.of(), List.of(), 100, null, "zh-CN")) - .expectNext(response) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().spanName()) - .isEqualTo(EXTRACTION_INSIGHT_GENERATE_LEAF); - assertThat(observer.monoContexts().getFirst().requestAttributes()) - .containsEntry(EXTRACTION_INSIGHT_TYPE, "PROFILE") - .containsEntry(EXTRACTION_INSIGHT_GROUP_NAME, "group-a"); - } - - @Test - void generateBranchSummaryPublishesBranchSpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(InsightGenerator.class); - var response = new InsightPointGenerateResponse(List.of()); - when(delegate.generateBranchSummary(any(), any(), any(), anyInt(), any())) - .thenReturn(Mono.just(response)); - var insightType = insightType(); - - var traced = new TracingInsightGenerator(delegate, observer); - - StepVerifier.create( - traced.generateBranchSummary( - insightType, List.of(), List.of(memoryInsight()), 100, "zh-CN")) - .expectNext(response) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().spanName()) - .isEqualTo(EXTRACTION_INSIGHT_GENERATE_BRANCH); - assertThat(observer.monoContexts().getFirst().requestAttributes()) - .containsEntry(EXTRACTION_INSIGHT_TYPE, "PROFILE") - .containsEntry(EXTRACTION_INSIGHT_LEAF_COUNT, 1); - } - - @Test - void generateRootSynthesisPublishesRootSpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(InsightGenerator.class); - var response = new InsightPointGenerateResponse(List.of()); - when(delegate.generateRootSynthesis(any(), any(), any(), anyInt(), any())) - .thenReturn(Mono.just(response)); - var insightType = insightType(); - - var traced = new TracingInsightGenerator(delegate, observer); - - StepVerifier.create( - traced.generateRootSynthesis( - insightType, List.of(), List.of(memoryInsight()), 100, "zh-CN")) - .expectNext(response) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().spanName()) - .isEqualTo(EXTRACTION_INSIGHT_GENERATE_ROOT); - assertThat(observer.monoContexts().getFirst().requestAttributes()) - .containsEntry(EXTRACTION_INSIGHT_TYPE, "PROFILE") - .containsEntry(EXTRACTION_INSIGHT_LEAF_COUNT, 1); - } - - @Test - void generatePointsPropagatesDelegateErrorsThroughObserver() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(InsightGenerator.class); - when(delegate.generatePoints(any(), any(), any(), any(), anyInt(), any(), any())) - .thenReturn(Mono.error(new IllegalStateException("boom"))); - var insightType = insightType(); - - var traced = new TracingInsightGenerator(delegate, observer); - - StepVerifier.create( - traced.generatePoints( - insightType, "group-a", List.of(), List.of(), 100, null, "zh-CN")) - .expectErrorMessage("boom") - .verify(); - - assertThat(observer.monoContexts()).hasSize(1); - } - - @Test - void generateLeafPointOpsPublishesOperationCounts() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(InsightGenerator.class); - var response = - new InsightPointOpsResponse( - List.of( - new PointOperation( - PointOperation.OpType.ADD, - null, - new InsightPoint( - "pt_add", - InsightPoint.PointType.SUMMARY, - "new", - List.of("1", "2")), - null), - new PointOperation( - PointOperation.OpType.DELETE, "pt_delete", null, "drop"))); - when(delegate.generateLeafPointOps(any(), any(), any(), any(), anyInt(), any(), any())) - .thenReturn(Mono.just(response)); - var insightType = insightType(); - - var traced = new TracingInsightGenerator(delegate, observer); - - StepVerifier.create( - traced.generateLeafPointOps( - insightType, "group-a", List.of(), List.of(), 100, null, "zh-CN")) - .expectNext(response) - .verifyComplete(); - - assertThat(extractResultAttributes(observer, response)) - .containsEntry(EXTRACTION_INSIGHT_ADD_COUNT, 1) - .containsEntry(EXTRACTION_INSIGHT_UPDATE_COUNT, 0) - .containsEntry(EXTRACTION_INSIGHT_DELETE_COUNT, 1); - } - - @Test - void generateBranchPointOpsPublishesOperationCounts() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(InsightGenerator.class); - var response = - new InsightPointOpsResponse( - List.of( - new PointOperation( - PointOperation.OpType.UPDATE, - "pt_update", - new InsightPoint( - "pt_update", - InsightPoint.PointType.SUMMARY, - "updated", - List.of("1", "2")), - null))); - when(delegate.generateBranchPointOps(any(), any(), any(), anyInt(), any())) - .thenReturn(Mono.just(response)); - var insightType = insightType(); - - var traced = new TracingInsightGenerator(delegate, observer); - - StepVerifier.create( - traced.generateBranchPointOps( - insightType, List.of(), List.of(memoryInsight()), 100, "zh-CN")) - .expectNext(response) - .verifyComplete(); - - assertThat(extractResultAttributes(observer, response)) - .containsEntry(EXTRACTION_INSIGHT_ADD_COUNT, 0) - .containsEntry(EXTRACTION_INSIGHT_UPDATE_COUNT, 1) - .containsEntry(EXTRACTION_INSIGHT_DELETE_COUNT, 0); - } - - @Test - void generateBranchPointOpsForwardsAdditionalContextOverload() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(InsightGenerator.class); - var response = new InsightPointOpsResponse(List.of()); - when(delegate.generateBranchPointOps(any(), anyList(), anyList(), anyInt(), any(), any())) - .thenReturn(Mono.just(response)); - var insightType = insightType(); - - var traced = new TracingInsightGenerator(delegate, observer); - - StepVerifier.create( - traced.generateBranchPointOps( - insightType, - List.of(), - List.of(memoryInsight()), - 100, - "GraphBranchHints: shared entity project-x", - "zh-CN")) - .expectNext(response) - .verifyComplete(); - - verify(delegate) - .generateBranchPointOps( - eq(insightType), - anyList(), - anyList(), - eq(100), - eq("GraphBranchHints: shared entity project-x"), - eq("zh-CN")); - } - - @Test - void generateBranchSummaryForwardsAdditionalContextOverload() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(InsightGenerator.class); - var response = new InsightPointGenerateResponse(List.of()); - when(delegate.generateBranchSummary(any(), anyList(), anyList(), anyInt(), any(), any())) - .thenReturn(Mono.just(response)); - var insightType = insightType(); - - var traced = new TracingInsightGenerator(delegate, observer); - - StepVerifier.create( - traced.generateBranchSummary( - insightType, - List.of(), - List.of(memoryInsight()), - 100, - "GraphBranchHints: shared entity project-x", - "zh-CN")) - .expectNext(response) - .verifyComplete(); - - verify(delegate) - .generateBranchSummary( - eq(insightType), - anyList(), - anyList(), - eq(100), - eq("GraphBranchHints: shared entity project-x"), - eq("zh-CN")); - } - - @Test - void generateRootSynthesisForwardsAdditionalContextOverload() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(InsightGenerator.class); - var response = new InsightPointGenerateResponse(List.of()); - when(delegate.generateRootSynthesis(any(), anyList(), anyList(), anyInt(), any(), any())) - .thenReturn(Mono.just(response)); - var insightType = insightType(); - - var traced = new TracingInsightGenerator(delegate, observer); - - StepVerifier.create( - traced.generateRootSynthesis( - insightType, - List.of(), - List.of(memoryInsight()), - 100, - "GraphRootHints: weak bridge between branches", - "zh-CN")) - .expectNext(response) - .verifyComplete(); - - verify(delegate) - .generateRootSynthesis( - eq(insightType), - anyList(), - anyList(), - eq(100), - eq("GraphRootHints: weak bridge between branches"), - eq("zh-CN")); - } - - private static MemoryInsightType insightType() { - return new MemoryInsightType( - 1L, "PROFILE", "desc", null, List.of(), 400, null, null, null, null, null, null); - } - - private static MemoryInsight memoryInsight() { - return new MemoryInsight( - 1L, - "user1:agent1", - "PROFILE", - null, - "group", - List.of(), - List.of(), - null, - null, - null, - null, - null, - null, - null, - List.of(), - 0); - } - - @SuppressWarnings("unchecked") - private static java.util.Map extractResultAttributes( - RecordingMemoryObserver observer, InsightPointOpsResponse response) { - var context = - (ObservationContext) observer.monoContexts().getFirst(); - return context.resultExtractor().extract(response); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightGroupClassifierTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightGroupClassifierTest.java deleted file mode 100644 index 36cf68da..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightGroupClassifierTest.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.EXTRACTION_INSIGHT_TYPE; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.EXTRACTION_ITEM_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.EXTRACTION_INSIGHT_GROUP_CLASSIFY; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.openmemind.ai.memory.core.data.MemoryInsightType; -import com.openmemind.ai.memory.core.data.MemoryItem; -import com.openmemind.ai.memory.core.extraction.insight.group.InsightGroupClassifier; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingInsightGroupClassifierTest { - - @Test - void classifyPublishesGroupClassifySpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(InsightGroupClassifier.class); - var item = - new MemoryItem( - 1L, - "user1:agent1", - "content", - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null); - var result = Map.of("group-1", List.of(item)); - when(delegate.classify(any(), any(), any())).thenReturn(Mono.just(result)); - var insightType = - new MemoryInsightType( - 1L, "PROFILE", "desc", null, List.of(), 400, null, null, null, null, null, - null); - - var traced = new TracingInsightGroupClassifier(delegate, observer); - - StepVerifier.create(traced.classify(insightType, List.of(item), List.of("group-1"))) - .expectNext(result) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().spanName()) - .isEqualTo(EXTRACTION_INSIGHT_GROUP_CLASSIFY); - assertThat(observer.monoContexts().getFirst().requestAttributes()) - .containsEntry(EXTRACTION_INSIGHT_TYPE, "PROFILE") - .containsEntry(EXTRACTION_ITEM_COUNT, 1); - } - - @Test - void classifyPropagatesDelegateErrorsThroughObserver() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(InsightGroupClassifier.class); - when(delegate.classify(any(), any(), any())) - .thenReturn(Mono.error(new IllegalStateException("boom"))); - var insightType = - new MemoryInsightType( - 1L, "PROFILE", "desc", null, List.of(), 400, null, null, null, null, null, - null); - - var traced = new TracingInsightGroupClassifier(delegate, observer); - - StepVerifier.create( - traced.classify( - insightType, - List.of( - new MemoryItem( - 1L, - "user1:agent1", - "content", - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null)), - List.of())) - .expectErrorMessage("boom") - .verify(); - - assertThat(observer.monoContexts()).hasSize(1); - } - - @Test - void classifyWithAdditionalContextShouldDelegateToOverload() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(InsightGroupClassifier.class); - var item = - new MemoryItem( - 1L, - "user1:agent1", - "content", - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null); - var result = Map.of("group-1", List.of(item)); - when(delegate.classify( - any(), - anyList(), - anyList(), - eq("GraphGroupingHints: cluster alpha"), - eq("English"))) - .thenReturn(Mono.just(result)); - var insightType = - new MemoryInsightType( - 1L, "PROFILE", "desc", null, List.of(), 400, null, null, null, null, null, - null); - - var traced = new TracingInsightGroupClassifier(delegate, observer); - - StepVerifier.create( - traced.classify( - insightType, - List.of(item), - List.of("group-1"), - "GraphGroupingHints: cluster alpha", - "English")) - .expectNext(result) - .verifyComplete(); - - verify(delegate) - .classify( - eq(insightType), - anyList(), - anyList(), - eq("GraphGroupingHints: cluster alpha"), - eq("English")); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightTierRetrieverTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightTierRetrieverTest.java deleted file mode 100644 index d7cde885..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightTierRetrieverTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.MEMORY_ID; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_RESULT_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_TIER_NAME; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_TOP_K; -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.RETRIEVAL_TIER_INSIGHT; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.openmemind.ai.memory.core.data.DefaultMemoryId; -import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; -import com.openmemind.ai.memory.core.retrieval.query.QueryContext; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.tier.InsightTierSearch; -import com.openmemind.ai.memory.core.retrieval.tier.TierResult; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import com.openmemind.ai.memory.core.tracing.ObservationContext; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingInsightTierRetrieverTest { - - @Test - void retrievePublishesInsightTierSpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - InsightTierSearch delegate = mock(InsightTierSearch.class); - var context = queryContext(); - var config = RetrievalConfig.simple(); - var result = new TierResult(List.of(scoredResult()), List.of()); - when(delegate.retrieve(context, config)).thenReturn(Mono.just(result)); - - var traced = new TracingInsightTierRetriever(delegate, observer); - - StepVerifier.create(traced.retrieve(context, config)).expectNext(result).verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().spanName()).isEqualTo(RETRIEVAL_TIER_INSIGHT); - assertThat(observer.monoContexts().getFirst().requestAttributes()) - .containsEntry(MEMORY_ID, "memory") - .containsEntry(RETRIEVAL_TIER_NAME, "insight") - .containsEntry(RETRIEVAL_TOP_K, config.tier1().topK()); - assertThat(resultAttributes(observer.monoContexts().getFirst(), result)) - .containsEntry(RETRIEVAL_RESULT_COUNT, 1); - } - - private QueryContext queryContext() { - return new QueryContext( - DefaultMemoryId.of("memory", null), "query", null, List.of(), Map.of(), null, null); - } - - private ScoredResult scoredResult() { - return new ScoredResult(ScoredResult.SourceType.INSIGHT, "insight-1", "text", 0.9f, 0.8d); - } - - @SuppressWarnings("unchecked") - private Map resultAttributes(ObservationContext context, TierResult result) { - return ((ObservationContext) context).resultExtractor().extract(result); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightTypeRouterTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightTypeRouterTest.java deleted file mode 100644 index fc6ed19f..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingInsightTypeRouterTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.RETRIEVAL_INSIGHT_TYPE_ROUTING; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.openmemind.ai.memory.core.retrieval.tier.InsightTypeRouter; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingInsightTypeRouterTest { - - @Test - void routePublishesRoutingSpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(InsightTypeRouter.class); - var routedTypes = List.of("preference", "behavior"); - when(delegate.route(any(), any(), any())).thenReturn(Mono.just(routedTypes)); - - var traced = new TracingInsightTypeRouter(delegate, observer); - - StepVerifier.create( - traced.route( - "query", - List.of("history"), - Map.of("preference", "User preferences"))) - .expectNext(routedTypes) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().spanName()) - .isEqualTo(RETRIEVAL_INSIGHT_TYPE_ROUTING); - assertThat(observer.monoContexts().getFirst().requestAttributes()).isEmpty(); - } - - @Test - void routePropagatesDelegateErrorsThroughObserver() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(InsightTypeRouter.class); - when(delegate.route(any(), any(), any())) - .thenReturn(Mono.error(new IllegalStateException("boom"))); - - var traced = new TracingInsightTypeRouter(delegate, observer); - - StepVerifier.create(traced.route("query", List.of(), Map.of())) - .expectErrorMessage("boom") - .verify(); - - assertThat(observer.monoContexts()).hasSize(1); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingItemGraphMaterializerTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingItemGraphMaterializerTest.java deleted file mode 100644 index 02902c96..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingItemGraphMaterializerTest.java +++ /dev/null @@ -1,452 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.openmemind.ai.memory.core.data.DefaultMemoryId; -import com.openmemind.ai.memory.core.data.MemoryItem; -import com.openmemind.ai.memory.core.data.enums.MemoryCategory; -import com.openmemind.ai.memory.core.data.enums.MemoryItemType; -import com.openmemind.ai.memory.core.data.enums.MemoryScope; -import com.openmemind.ai.memory.core.extraction.item.graph.ItemGraphMaterializationResult; -import com.openmemind.ai.memory.core.extraction.item.graph.ItemGraphMaterializer; -import com.openmemind.ai.memory.core.extraction.item.graph.entity.resolve.EntityResolutionDiagnostics; -import com.openmemind.ai.memory.core.extraction.item.graph.link.semantic.SemanticItemLinker; -import com.openmemind.ai.memory.core.extraction.item.graph.link.temporal.TemporalItemLinker; -import com.openmemind.ai.memory.core.extraction.item.support.ExtractedMemoryEntry; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.ObservationContext; -import java.time.Instant; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingItemGraphMaterializerTest { - - @Test - void tracingDecoratorEmitsExpandedSemanticThroughputAndDegradationAttributes() { - var observer = new RecordingMemoryObserver(); - var stats = semanticThroughputStats(); - ItemGraphMaterializer materializer = - new TracingItemGraphMaterializer( - (memoryId, items, entries) -> - Mono.just(new ItemGraphMaterializationResult(stats)), - observer); - - StepVerifier.create( - materializer.materialize( - DefaultMemoryId.of("user-1", "agent-1"), - List.of(newItem(101L)), - List.of(newEntry()))) - .assertNext(result -> assertThat(result.stats().semanticDegraded()).isTrue()) - .verifyComplete(); - - assertThat(observer.monoContexts()) - .extracting(ObservationContext::spanName) - .contains(MemorySpanNames.GRAPH_MATERIALIZE); - - @SuppressWarnings("unchecked") - var resultAttributes = - ((ObservationContext) - observer.monoContexts().getFirst()) - .resultExtractor() - .extract(new ItemGraphMaterializationResult(stats)); - assertThat(resultAttributes) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_ENTITY_COUNT, 2) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_MENTION_COUNT, 3) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_STRUCTURED_LINK_COUNT, 1) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_SEARCH_REQUEST_COUNT, 4) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_SEARCH_INVOCATION_COUNT, 4) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_SEARCH_HIT_COUNT, 8) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_RESOLVED_CANDIDATE_COUNT, 6) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_LINK_COUNT, 5) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_UPSERT_BATCH_COUNT, 2) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_SOURCE_WINDOW_COUNT, 2) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_FAILED_RESOLVE_CHUNK_COUNT, 1) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_FAILED_WINDOW_COUNT, 1) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_FAILED_UPSERT_BATCH_COUNT, 1) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_SAME_BATCH_HIT_COUNT, 3) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_SEARCH_FALLBACK_COUNT, 0) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_INTRA_BATCH_CANDIDATE_COUNT, 4) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_SEARCH_PHASE_DURATION_MS, 15L) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_RESOLVE_PHASE_DURATION_MS, 9L) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_UPSERT_PHASE_DURATION_MS, 6L) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_INTRA_BATCH_PHASE_DURATION_MS, - 12L) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_DEGRADED, true); - } - - @Test - void tracingDecoratorEmitsStage3IntraBatchAttributes() { - var observer = new RecordingMemoryObserver(); - var stats = semanticThroughputStats(); - ItemGraphMaterializer materializer = - new TracingItemGraphMaterializer( - (memoryId, items, entries) -> - Mono.just(new ItemGraphMaterializationResult(stats)), - observer); - - StepVerifier.create( - materializer.materialize( - DefaultMemoryId.of("user-1", "agent-1"), - List.of(newItem(101L)), - List.of(newEntry()))) - .expectNextCount(1) - .verifyComplete(); - - @SuppressWarnings("unchecked") - var resultAttributes = - ((ObservationContext) - observer.monoContexts().getFirst()) - .resultExtractor() - .extract(new ItemGraphMaterializationResult(stats)); - assertThat(resultAttributes) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_INTRA_BATCH_CANDIDATE_COUNT, 4) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_SEMANTIC_INTRA_BATCH_PHASE_DURATION_MS, - 12L); - } - - @Test - void tracingDecoratorShouldEmitDedicatedTemporalAndRolloutSummaryAttributes() { - var observer = new RecordingMemoryObserver(); - var stats = - ItemGraphMaterializationResult.Stats.withTemporalAndSemantic( - 2, - 2, - 1, - new TemporalItemLinker.TemporalLinkingStats( - 2, 1, 3, 1, 1, 1, 4L, 3L, 2L, 0, 0.75d, 0.75d, "0.75-0.89=1", true), - EntityResolutionDiagnostics.empty(), - new SemanticItemLinker.SemanticLinkingStats( - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0L, 0L, 0L, 0L, false), - 0, - "", - 0, - 0, - 0, - 0, - 0, - 0); - ItemGraphMaterializer materializer = - new TracingItemGraphMaterializer( - (memoryId, items, entries) -> - Mono.just(new ItemGraphMaterializationResult(stats)), - observer); - - StepVerifier.create( - materializer.materialize( - DefaultMemoryId.of("user-1", "agent-1"), - List.of(newItem(101L)), - List.of(newEntry()))) - .expectNextCount(1) - .verifyComplete(); - - @SuppressWarnings("unchecked") - var resultAttributes = - ((ObservationContext) - observer.monoContexts().getFirst()) - .resultExtractor() - .extract(new ItemGraphMaterializationResult(stats)); - assertThat(resultAttributes) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_TEMPORAL_SOURCE_COUNT, 2) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_TEMPORAL_CREATED_LINK_COUNT, 1) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_TEMPORAL_BELOW_RETRIEVAL_FLOOR_COUNT, 0) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_TEMPORAL_MIN_STRENGTH, 0.75d) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_TEMPORAL_MAX_STRENGTH, 0.75d) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_TEMPORAL_STRENGTH_BUCKET_SUMMARY, - "0.75-0.89=1") - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_TEMPORAL_DEGRADED, true); - } - - @Test - void tracingDecoratorEmitsStructuredBatchDegradedAttribute() { - var observer = new RecordingMemoryObserver(); - var stats = semanticThroughputStats().withStructuredBatchDegraded(true); - ItemGraphMaterializer materializer = - new TracingItemGraphMaterializer( - (memoryId, items, entries) -> - Mono.just(new ItemGraphMaterializationResult(stats)), - observer); - - StepVerifier.create( - materializer.materialize( - DefaultMemoryId.of("user-1", "agent-1"), - List.of(newItem(101L)), - List.of(newEntry()))) - .expectNextCount(1) - .verifyComplete(); - - @SuppressWarnings("unchecked") - var resultAttributes = - ((ObservationContext) - observer.monoContexts().getFirst()) - .resultExtractor() - .extract(new ItemGraphMaterializationResult(stats)); - assertThat(resultAttributes) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_STRUCTURED_BATCH_DEGRADED, true); - } - - @Test - void tracingDecoratorShouldEmitStage1EntityHardeningCounters() { - var observer = new RecordingMemoryObserver(); - var stats = stageOneStats(2, 2, 1, "未分类标签=1", 0, 1, 0, 1, 0, 1); - ItemGraphMaterializer materializer = - new TracingItemGraphMaterializer( - (memoryId, items, entries) -> - Mono.just(new ItemGraphMaterializationResult(stats)), - observer); - - StepVerifier.create( - materializer.materialize( - DefaultMemoryId.of("user-1", "agent-1"), - List.of(newItem(101L)), - List.of(newEntry()))) - .expectNextCount(1) - .verifyComplete(); - - @SuppressWarnings("unchecked") - var resultAttributes = - ((ObservationContext) - observer.monoContexts().getFirst()) - .resultExtractor() - .extract(new ItemGraphMaterializationResult(stats)); - assertThat(resultAttributes) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_TYPE_FALLBACK_TO_OTHER_COUNT, 1) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_TOP_UNRESOLVED_TYPE_LABELS, "未分类标签=1") - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_DROPPED_PUNCTUATION_ONLY_COUNT, 1) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_DROPPED_RESERVED_SPECIAL_COLLISION_COUNT, - 1) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_DROPPED_TEMPORAL_COUNT, 1); - } - - @Test - void tracingDecoratorShouldEmitStage2ResolutionAttributes() { - var observer = new RecordingMemoryObserver(); - var stats = - stageTwoStats( - 2, - 2, - 0, - 3, - "exact_canonical_hit=1,safe_variant_hit=1", - "0.90-1.00=2", - 1, - 2, - 1, - 1, - 4, - 2, - 3, - 1, - 1); - ItemGraphMaterializer materializer = - new TracingItemGraphMaterializer( - (memoryId, items, entries) -> - Mono.just(new ItemGraphMaterializationResult(stats)), - observer); - - StepVerifier.create( - materializer.materialize( - DefaultMemoryId.of("user-1", "agent-1"), - List.of(newItem(101L)), - List.of(newEntry()))) - .expectNextCount(1) - .verifyComplete(); - - @SuppressWarnings("unchecked") - var resultAttributes = - ((ObservationContext) - observer.monoContexts().getFirst()) - .resultExtractor() - .extract(new ItemGraphMaterializationResult(stats)); - assertThat(resultAttributes) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_RESOLUTION_CANDIDATE_COUNT, 3) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_RESOLUTION_SOURCE_DISTRIBUTION, - "exact_canonical_hit=1,safe_variant_hit=1") - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_RESOLUTION_SCORE_HISTOGRAM, "0.90-1.00=2") - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_RESOLUTION_CANDIDATE_REJECTED_COUNT, 1) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_RESOLUTION_MERGE_ACCEPTED_COUNT, 2) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_RESOLUTION_MERGE_REJECTED_COUNT, 1) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_RESOLUTION_CREATE_NEW_COUNT, 1) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_RESOLUTION_EXACT_FALLBACK_COUNT, 4) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_RESOLUTION_CANDIDATE_CAP_HIT_COUNT, 2) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_ALIAS_EVIDENCE_OBSERVED_COUNT, 3) - .containsEntry(MemoryAttributes.EXTRACTION_GRAPH_ALIAS_EVIDENCE_MERGED_COUNT, 1) - .containsEntry( - MemoryAttributes.EXTRACTION_GRAPH_RESOLUTION_SPECIAL_BYPASS_COUNT, 1); - } - - private static MemoryItem newItem(Long id) { - return new MemoryItem( - id, - "user-1:agent-1", - "User discussed OpenAI deployment", - MemoryScope.USER, - MemoryCategory.EVENT, - "conversation", - "vector-" + id, - "raw-" + id, - "hash-" + id, - Instant.parse("2026-04-16T10:00:00Z"), - Instant.parse("2026-04-16T10:00:00Z"), - Map.of(), - Instant.parse("2026-04-16T10:00:00Z"), - MemoryItemType.FACT); - } - - private static ExtractedMemoryEntry newEntry() { - return new ExtractedMemoryEntry( - "User discussed OpenAI deployment", - 1.0f, - Instant.parse("2026-04-16T10:00:00Z"), - Instant.parse("2026-04-16T10:00:00Z"), - "raw-1", - "hash-1", - List.of(), - Map.of(), - MemoryItemType.FACT, - "event"); - } - - private static ItemGraphMaterializationResult.Stats semanticThroughputStats() { - return new ItemGraphMaterializationResult.Stats( - 2, 3, 1, 0, "", "", 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 8, 6, 5, 2, 2, 1, 1, 1, 3, 0, - 4, 15L, 9L, 6L, 12L, true, 0, "", 0, 0, 0, 0, 0, 0); - } - - private static ItemGraphMaterializationResult.Stats stageTwoStats( - int entityCount, - int mentionCount, - int structuredItemLinkCount, - int resolutionCandidateCount, - String resolutionCandidateSourceSummary, - String resolutionMergeScoreHistogramSummary, - int resolutionCandidateRejectedCount, - int resolutionMergeAcceptedCount, - int resolutionMergeRejectedCount, - int resolutionCreateNewCount, - int resolutionExactFallbackCount, - int resolutionCandidateCapHitCount, - int aliasEvidenceObservedCount, - int aliasEvidenceMergedCount, - int resolutionSpecialBypassCount) { - return new ItemGraphMaterializationResult.Stats( - entityCount, - mentionCount, - structuredItemLinkCount, - resolutionCandidateCount, - resolutionCandidateSourceSummary, - resolutionMergeScoreHistogramSummary, - resolutionCandidateRejectedCount, - resolutionMergeAcceptedCount, - resolutionMergeRejectedCount, - resolutionCreateNewCount, - resolutionExactFallbackCount, - resolutionCandidateCapHitCount, - aliasEvidenceObservedCount, - aliasEvidenceMergedCount, - resolutionSpecialBypassCount, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0L, - 0L, - 0L, - 0L, - false, - 0, - "", - 0, - 0, - 0, - 0, - 0, - 0); - } - - private static ItemGraphMaterializationResult.Stats stageOneStats( - int entityCount, - int mentionCount, - int typeFallbackToOtherCount, - String topUnresolvedTypeLabelsSummary, - int droppedBlankCount, - int droppedPunctuationOnlyCount, - int droppedPronounLikeCount, - int droppedTemporalCount, - int droppedDateLikeCount, - int droppedReservedSpecialCollisionCount) { - return new ItemGraphMaterializationResult.Stats( - entityCount, - mentionCount, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0L, - 0L, - 0L, - false, - typeFallbackToOtherCount, - topUnresolvedTypeLabelsSummary, - droppedBlankCount, - droppedPunctuationOnlyCount, - droppedPronounLikeCount, - droppedTemporalCount, - droppedDateLikeCount, - droppedReservedSpecialCollisionCount); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingItemTierRetrieverTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingItemTierRetrieverTest.java deleted file mode 100644 index 008cee9b..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingItemTierRetrieverTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.MEMORY_ID; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_RESULT_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_TIER_NAME; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_TOP_K; -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.RETRIEVAL_KEYWORD_SEARCH; -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.RETRIEVAL_VECTOR_SEARCH; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.openmemind.ai.memory.core.data.DefaultMemoryId; -import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; -import com.openmemind.ai.memory.core.retrieval.query.QueryContext; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoringConfig; -import com.openmemind.ai.memory.core.retrieval.tier.ItemTierSearch; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import com.openmemind.ai.memory.core.tracing.ObservationContext; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingItemTierRetrieverTest { - - @Test - void searchByVectorPublishesVectorSpanAndPropagatesResults() { - var observer = new RecordingMemoryObserver(); - ItemTierSearch delegate = mock(ItemTierSearch.class); - var context = queryContext(); - var tier = RetrievalConfig.TierConfig.enabled(5); - var scoring = ScoringConfig.defaults(); - var result = scoredResult(); - when(delegate.searchByVector(context, tier, scoring)) - .thenReturn(Mono.just(List.of(result))); - - var traced = new TracingItemTierRetriever(delegate, observer); - - StepVerifier.create(traced.searchByVector(context, tier, scoring)) - .expectNext(List.of(result)) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().spanName()) - .isEqualTo(RETRIEVAL_VECTOR_SEARCH); - assertThat(observer.monoContexts().getFirst().requestAttributes()) - .containsEntry(MEMORY_ID, "memory") - .containsEntry(RETRIEVAL_TIER_NAME, "item") - .containsEntry(RETRIEVAL_TOP_K, 5); - assertThat(resultAttributes(observer.monoContexts().getFirst(), List.of(result))) - .containsEntry(RETRIEVAL_RESULT_COUNT, 1); - } - - @Test - void searchByKeywordPublishesKeywordSpanAndPropagatesResults() { - var observer = new RecordingMemoryObserver(); - ItemTierSearch delegate = mock(ItemTierSearch.class); - var context = queryContext(); - var tier = RetrievalConfig.TierConfig.enabled(3); - var scoring = ScoringConfig.defaults(); - var result = scoredResult(); - when(delegate.searchByKeyword(context, tier, scoring)) - .thenReturn(Mono.just(List.of(result))); - - var traced = new TracingItemTierRetriever(delegate, observer); - - StepVerifier.create(traced.searchByKeyword(context, tier, scoring)) - .expectNext(List.of(result)) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().spanName()) - .isEqualTo(RETRIEVAL_KEYWORD_SEARCH); - assertThat(resultAttributes(observer.monoContexts().getFirst(), List.of(result))) - .containsEntry(RETRIEVAL_RESULT_COUNT, 1); - } - - private QueryContext queryContext() { - return new QueryContext( - DefaultMemoryId.of("memory", null), "query", null, List.of(), Map.of(), null, null); - } - - private ScoredResult scoredResult() { - return new ScoredResult(ScoredResult.SourceType.ITEM, "item-1", "text", 0.9f, 0.8d); - } - - @SuppressWarnings("unchecked") - private Map resultAttributes( - ObservationContext context, List result) { - return ((ObservationContext>) context).resultExtractor().extract(result); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryItemDeduplicatorTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryItemDeduplicatorTest.java deleted file mode 100644 index 97578cb8..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryItemDeduplicatorTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.MEMORY_ID; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.openmemind.ai.memory.core.extraction.item.dedup.DeduplicationResult; -import com.openmemind.ai.memory.core.extraction.item.dedup.MemoryItemDeduplicator; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import com.openmemind.ai.memory.core.support.TestMemoryIds; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingMemoryItemDeduplicatorTest { - - @Test - void deduplicatePublishesDelegateSpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(MemoryItemDeduplicator.class); - var result = new DeduplicationResult(java.util.List.of(), java.util.List.of()); - when(delegate.spanName()).thenReturn("memind.extraction.item.dedup"); - when(delegate.deduplicate(any(), any())).thenReturn(Mono.just(result)); - - var traced = new TracingMemoryItemDeduplicator(delegate, observer); - var memoryId = TestMemoryIds.userAgent(); - - StepVerifier.create(traced.deduplicate(memoryId, java.util.List.of())) - .expectNext(result) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().spanName()) - .isEqualTo("memind.extraction.item.dedup"); - assertThat(observer.monoContexts().getFirst().requestAttributes()) - .containsEntry(MEMORY_ID, memoryId.toIdentifier()); - } - - @Test - void deduplicatePropagatesDelegateErrorsThroughObserver() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(MemoryItemDeduplicator.class); - when(delegate.spanName()).thenReturn("memind.extraction.item.dedup"); - when(delegate.deduplicate(any(), any())) - .thenReturn(Mono.error(new IllegalStateException("boom"))); - - var traced = new TracingMemoryItemDeduplicator(delegate, observer); - - StepVerifier.create(traced.deduplicate(TestMemoryIds.userAgent(), java.util.List.of())) - .expectErrorMessage("boom") - .verify(); - - assertThat(observer.monoContexts()).hasSize(1); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryItemExtractStepTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryItemExtractStepTest.java deleted file mode 100644 index 38309711..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryItemExtractStepTest.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.MEMORY_ID; -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.EXTRACTION_ITEM; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.openmemind.ai.memory.core.extraction.item.ItemExtractionConfig; -import com.openmemind.ai.memory.core.extraction.result.MemoryItemResult; -import com.openmemind.ai.memory.core.extraction.result.RawDataResult; -import com.openmemind.ai.memory.core.extraction.step.MemoryItemExtractStep; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import com.openmemind.ai.memory.core.support.TestMemoryIds; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingMemoryItemExtractStepTest { - - @Test - void extractPublishesItemSpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(MemoryItemExtractStep.class); - var result = MemoryItemResult.empty(); - when(delegate.extract(any(), any(), any())).thenReturn(Mono.just(result)); - - var traced = new TracingMemoryItemExtractStep(delegate, observer); - var memoryId = TestMemoryIds.userAgent(); - - StepVerifier.create( - traced.extract( - memoryId, RawDataResult.empty(), ItemExtractionConfig.defaults())) - .expectNext(result) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().spanName()).isEqualTo(EXTRACTION_ITEM); - assertThat(observer.monoContexts().getFirst().requestAttributes()) - .containsEntry(MEMORY_ID, memoryId.toIdentifier()); - } - - @Test - void extractPropagatesDelegateErrorsThroughObserver() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(MemoryItemExtractStep.class); - when(delegate.extract(any(), any(), any())) - .thenReturn(Mono.error(new IllegalStateException("boom"))); - - var traced = new TracingMemoryItemExtractStep(delegate, observer); - - StepVerifier.create( - traced.extract( - TestMemoryIds.userAgent(), - RawDataResult.empty(), - ItemExtractionConfig.defaults())) - .expectErrorMessage("boom") - .verify(); - - assertThat(observer.monoContexts()).hasSize(1); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryRetrieverTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryRetrieverTest.java deleted file mode 100644 index e7b674cf..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryRetrieverTest.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.MEMORY_ID; -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.RETRIEVAL; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.openmemind.ai.memory.core.retrieval.MemoryRetriever; -import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; -import com.openmemind.ai.memory.core.retrieval.RetrievalRequest; -import com.openmemind.ai.memory.core.retrieval.RetrievalResult; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import com.openmemind.ai.memory.core.support.TestMemoryIds; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingMemoryRetrieverTest { - - @Test - void retrievePublishesRetrievalSpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(MemoryRetriever.class); - var result = RetrievalResult.empty("simple", "query"); - when(delegate.retrieve(any())).thenReturn(Mono.just(result)); - - var traced = new TracingMemoryRetriever(delegate, observer); - var memoryId = TestMemoryIds.userAgent(); - var request = RetrievalRequest.of(memoryId, "query", RetrievalConfig.Strategy.SIMPLE); - - StepVerifier.create(traced.retrieve(request)).expectNext(result).verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().spanName()).isEqualTo(RETRIEVAL); - assertThat(observer.monoContexts().getFirst().requestAttributes()) - .containsEntry(MEMORY_ID, memoryId.toIdentifier()); - } - - @Test - void retrievePropagatesDelegateErrorsThroughObserver() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(MemoryRetriever.class); - when(delegate.retrieve(any())).thenReturn(Mono.error(new IllegalStateException("boom"))); - - var traced = new TracingMemoryRetriever(delegate, observer); - - StepVerifier.create( - traced.retrieve( - RetrievalRequest.of( - TestMemoryIds.userAgent(), - "query", - RetrievalConfig.Strategy.SIMPLE))) - .expectErrorMessage("boom") - .verify(); - - assertThat(observer.monoContexts()).hasSize(1); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryThreadAssistantTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryThreadAssistantTest.java deleted file mode 100644 index 18bfbb14..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingMemoryThreadAssistantTest.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; -import com.openmemind.ai.memory.core.retrieval.query.QueryContext; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.thread.MemoryThreadAssistResult; -import com.openmemind.ai.memory.core.retrieval.thread.MemoryThreadAssistant; -import com.openmemind.ai.memory.core.retrieval.thread.RetrievalMemoryThreadSettings; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import com.openmemind.ai.memory.core.support.TestMemoryIds; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import java.time.Duration; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingMemoryThreadAssistantTest { - - @Test - void tracingDecoratorShouldEmitFullAssistObservationSurface() { - var observer = new RecordingMemoryObserver(); - MemoryThreadAssistant delegate = - (context, config, settings, directWindow) -> - Mono.just( - new MemoryThreadAssistResult( - directWindow, - MemoryThreadAssistResult.Stats.success(1, 2, 2, false))); - var traced = new TracingMemoryThreadAssistant(delegate, observer); - var direct = - List.of( - new ScoredResult( - ScoredResult.SourceType.ITEM, "101", "item-101", 0.8f, 1.0d)); - - StepVerifier.create( - traced.assist( - new QueryContext( - TestMemoryIds.userAgent(), - "what changed", - null, - List.of(), - Map.of(), - null, - null), - RetrievalConfig.simple(), - new TestThreadSettings(true, 1, 2, 1, Duration.ofMillis(150)), - direct)) - .assertNext(result -> assertThat(result.items()).containsExactlyElementsOf(direct)) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - var observation = observer.monoContexts().getFirst(); - assertThat(observation.spanName()) - .isEqualTo(MemorySpanNames.RETRIEVAL_MEMORY_THREAD_ASSIST); - assertThat(observation.requestAttributes()) - .containsEntry(MemoryAttributes.MEMORY_ID, TestMemoryIds.userAgent().toIdentifier()) - .containsEntry(MemoryAttributes.RETRIEVAL_MEMORY_THREAD_ENABLED, true); - - @SuppressWarnings("unchecked") - var resultAttributes = - ((com.openmemind.ai.memory.core.tracing.ObservationContext< - MemoryThreadAssistResult>) - observation) - .resultExtractor() - .extract( - new MemoryThreadAssistResult( - direct, - MemoryThreadAssistResult.Stats.success(1, 2, 2, false))); - assertThat(resultAttributes) - .containsEntry(MemoryAttributes.RETRIEVAL_MEMORY_THREAD_SEED_THREAD_COUNT, 1) - .containsEntry(MemoryAttributes.RETRIEVAL_MEMORY_THREAD_CANDIDATE_COUNT, 2) - .containsEntry(MemoryAttributes.RETRIEVAL_MEMORY_THREAD_ADMITTED_COUNT, 2) - .containsEntry(MemoryAttributes.RETRIEVAL_MEMORY_THREAD_CLAMPED, false) - .containsEntry(MemoryAttributes.RETRIEVAL_MEMORY_THREAD_DEGRADED, false) - .containsEntry(MemoryAttributes.RETRIEVAL_MEMORY_THREAD_TIMEOUT, false); - } - - private record TestThreadSettings( - boolean enabled, - int maxThreads, - int maxMembersPerThread, - int protectDirectTopK, - Duration timeout) - implements RetrievalMemoryThreadSettings {} -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRerankerTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRerankerTest.java deleted file mode 100644 index 4335d9c2..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRerankerTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_QUERY; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_TOP_K; -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.RETRIEVAL_RERANK; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.openmemind.ai.memory.core.llm.rerank.Reranker; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import java.util.List; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingRerankerTest { - - @Test - void rerankPublishesRerankSpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(Reranker.class); - var results = - List.of(new ScoredResult(ScoredResult.SourceType.ITEM, "id-1", "text", 0.9f, 0.85)); - when(delegate.rerank(any(), any(), any(int.class))).thenReturn(Mono.just(results)); - - var traced = new TracingReranker(delegate, observer); - - StepVerifier.create(traced.rerank("query", results, 5)) - .expectNext(results) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().spanName()).isEqualTo(RETRIEVAL_RERANK); - assertThat(observer.monoContexts().getFirst().requestAttributes()) - .containsEntry(RETRIEVAL_QUERY, "query") - .containsEntry(RETRIEVAL_TOP_K, 5); - } - - @Test - void rerankPropagatesDelegateErrorsThroughObserver() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(Reranker.class); - when(delegate.rerank(any(), any(), any(int.class))) - .thenReturn(Mono.error(new IllegalStateException("boom"))); - - var traced = new TracingReranker(delegate, observer); - - StepVerifier.create(traced.rerank("query", List.of(), 5)) - .expectErrorMessage("boom") - .verify(); - - assertThat(observer.monoContexts()).hasSize(1); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalGraphAssistantTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalGraphAssistantTest.java deleted file mode 100644 index a141c586..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalGraphAssistantTest.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; -import com.openmemind.ai.memory.core.retrieval.graph.RetrievalGraphAssistResult; -import com.openmemind.ai.memory.core.retrieval.graph.RetrievalGraphAssistant; -import com.openmemind.ai.memory.core.retrieval.query.QueryContext; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.strategy.DeepStrategyConfig; -import com.openmemind.ai.memory.core.retrieval.strategy.SimpleStrategyConfig; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import com.openmemind.ai.memory.core.support.TestMemoryIds; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingRetrievalGraphAssistantTest { - - @Test - void tracingDecoratorShouldEmitFullGraphAssistObservationSurface() { - var observer = new RecordingMemoryObserver(); - RetrievalGraphAssistant delegate = - (context, config, strategyConfig, directItems) -> - Mono.just( - new RetrievalGraphAssistResult( - directItems, - new RetrievalGraphAssistResult.GraphAssistStats( - true, false, false, 2, 3, 4, 5, 6, 1, 1, 1))); - var traced = new TracingRetrievalGraphAssistant(delegate, observer); - var context = - new QueryContext( - TestMemoryIds.userAgent(), - "what changed", - null, - List.of(), - Map.of(), - null, - null); - var direct = - List.of( - new ScoredResult( - ScoredResult.SourceType.ITEM, "101", "item-101", 0.8f, 1.0d)); - - StepVerifier.create( - traced.assist( - context, - RetrievalConfig.simple(), - SimpleStrategyConfig.defaults() - .withGraphAssist( - SimpleStrategyConfig.GraphAssistConfig.defaults() - .withEnabled(true)) - .graphAssist(), - direct)) - .assertNext(result -> assertThat(result.items()).containsExactlyElementsOf(direct)) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - var observation = observer.monoContexts().getFirst(); - assertThat(observation.spanName()).isEqualTo(MemorySpanNames.RETRIEVAL_GRAPH_ASSIST); - assertThat(observation.requestAttributes()) - .containsEntry(MemoryAttributes.MEMORY_ID, context.memoryId().toIdentifier()) - .containsEntry(MemoryAttributes.RETRIEVAL_GRAPH_ENABLED, true); - - @SuppressWarnings("unchecked") - var resultAttributes = - ((com.openmemind.ai.memory.core.tracing.ObservationContext< - RetrievalGraphAssistResult>) - observation) - .resultExtractor() - .extract( - new RetrievalGraphAssistResult( - direct, - new RetrievalGraphAssistResult.GraphAssistStats( - true, false, false, 2, 3, 4, 5, 6, 1, 1, 1))); - assertThat(resultAttributes) - .containsEntry(MemoryAttributes.RETRIEVAL_GRAPH_SEED_COUNT, 2) - .containsEntry(MemoryAttributes.RETRIEVAL_GRAPH_LINK_EXPANSION_COUNT, 3) - .containsEntry(MemoryAttributes.RETRIEVAL_GRAPH_ENTITY_EXPANSION_COUNT, 4) - .containsEntry(MemoryAttributes.RETRIEVAL_GRAPH_DEDUPED_CANDIDATE_COUNT, 5) - .containsEntry(MemoryAttributes.RETRIEVAL_GRAPH_ADMITTED_CANDIDATE_COUNT, 6) - .containsEntry(MemoryAttributes.RETRIEVAL_GRAPH_DISPLACED_DIRECT_COUNT, 1) - .containsEntry(MemoryAttributes.RETRIEVAL_GRAPH_OVERLAP_COUNT, 1) - .containsEntry(MemoryAttributes.RETRIEVAL_GRAPH_SKIPPED_OVERFANOUT_ENTITY_COUNT, 1) - .containsEntry(MemoryAttributes.RETRIEVAL_GRAPH_TIMEOUT, false) - .containsEntry(MemoryAttributes.RETRIEVAL_GRAPH_DEGRADED, false); - } - - @Test - void tracingDecoratorShouldAcceptDeepGraphSettings() { - var observer = new RecordingMemoryObserver(); - RetrievalGraphAssistant delegate = - (context, config, graphSettings, directItems) -> - Mono.just( - new RetrievalGraphAssistResult( - directItems, - new RetrievalGraphAssistResult.GraphAssistStats( - true, false, false, 1, 2, 3, 4, 1, 0, 0, 0))); - var traced = new TracingRetrievalGraphAssistant(delegate, observer); - var context = - new QueryContext( - TestMemoryIds.userAgent(), - "what changed", - null, - List.of(), - Map.of(), - null, - null); - var direct = - List.of( - new ScoredResult( - ScoredResult.SourceType.ITEM, "101", "item-101", 0.8f, 1.0d)); - var deepConfig = - DeepStrategyConfig.defaults() - .withGraphAssist( - DeepStrategyConfig.GraphAssistConfig.defaults().withEnabled(true)); - - StepVerifier.create( - traced.assist( - context, - RetrievalConfig.deep(deepConfig), - deepConfig.graphAssist(), - direct)) - .assertNext(result -> assertThat(result.items()).containsExactlyElementsOf(direct)) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().requestAttributes()) - .containsEntry(MemoryAttributes.RETRIEVAL_GRAPH_ENABLED, true); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalResultMergerTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalResultMergerTest.java deleted file mode 100644 index e306d404..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalResultMergerTest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_CANDIDATE_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_DEDUPED_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_RESULT_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_SOURCE_LIST_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_WEIGHT_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.RETRIEVAL_RESULT_MERGE; -import static org.assertj.core.api.Assertions.assertThat; - -import com.openmemind.ai.memory.core.retrieval.scoring.RetrievalResultMerger; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoringConfig; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import com.openmemind.ai.memory.core.tracing.ObservationContext; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingRetrievalResultMergerTest { - - @Test - void mergePublishesResultMergeSpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - var merged = List.of(scoredResult("item-1")); - RetrievalResultMerger delegate = (scoring, rankedLists, weights) -> Mono.just(merged); - var rankedLists = List.of(List.of(scoredResult("item-1")), List.of(scoredResult("item-1"))); - - var traced = new TracingRetrievalResultMerger(delegate, observer); - - StepVerifier.create(traced.merge(ScoringConfig.defaults(), rankedLists, 1.0d, 0.8d)) - .expectNext(merged) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - var observation = observer.monoContexts().getFirst(); - assertThat(observation.spanName()).isEqualTo(RETRIEVAL_RESULT_MERGE); - assertThat(observation.requestAttributes()) - .containsEntry(RETRIEVAL_SOURCE_LIST_COUNT, 2) - .containsEntry(RETRIEVAL_CANDIDATE_COUNT, 2) - .containsEntry(RETRIEVAL_DEDUPED_COUNT, 1) - .containsEntry(RETRIEVAL_WEIGHT_COUNT, 2); - assertThat(resultAttributes(observation, merged)) - .containsEntry(RETRIEVAL_RESULT_COUNT, 1) - .containsEntry(RETRIEVAL_DEDUPED_COUNT, 1); - } - - private ScoredResult scoredResult(String id) { - return new ScoredResult(ScoredResult.SourceType.ITEM, id, "text", 0.9f, 0.8d); - } - - @SuppressWarnings("unchecked") - private Map resultAttributes( - ObservationContext context, List result) { - return ((ObservationContext>) context).resultExtractor().extract(result); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalStrategyTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalStrategyTest.java deleted file mode 100644 index e71d8b5f..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingRetrievalStrategyTest.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.MEMORY_ID; -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.RETRIEVAL_STRATEGY; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; -import com.openmemind.ai.memory.core.retrieval.RetrievalResult; -import com.openmemind.ai.memory.core.retrieval.query.QueryContext; -import com.openmemind.ai.memory.core.retrieval.strategy.RetrievalStrategy; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import com.openmemind.ai.memory.core.support.TestMemoryIds; -import com.openmemind.ai.memory.core.tracing.MemoryAttributes; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingRetrievalStrategyTest { - - @Test - void retrievePublishesRetrievalStrategySpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(RetrievalStrategy.class); - var result = RetrievalResult.empty("simple", "query"); - when(delegate.name()).thenReturn("simple"); - when(delegate.retrieve(any(), any())).thenReturn(Mono.just(result)); - - var traced = new TracingRetrievalStrategy(delegate, observer); - var memoryId = TestMemoryIds.userAgent(); - var context = - new QueryContext(memoryId, "query", null, List.of("history"), Map.of(), null, null); - - StepVerifier.create(traced.retrieve(context, RetrievalConfig.simple())) - .expectNext(result) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().spanName()).isEqualTo(RETRIEVAL_STRATEGY); - assertThat(observer.monoContexts().getFirst().requestAttributes()) - .containsEntry(MEMORY_ID, memoryId.toIdentifier()) - .containsEntry(MemoryAttributes.RETRIEVAL_STRATEGY, "simple"); - } - - @Test - void retrievePropagatesDelegateErrorsThroughObserver() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(RetrievalStrategy.class); - when(delegate.name()).thenReturn("simple"); - when(delegate.retrieve(any(), any())) - .thenReturn(Mono.error(new IllegalStateException("boom"))); - - var traced = new TracingRetrievalStrategy(delegate, observer); - - StepVerifier.create( - traced.retrieve( - new QueryContext( - TestMemoryIds.userAgent(), - "query", - null, - List.of(), - Map.of(), - null, - null), - RetrievalConfig.simple())) - .expectErrorMessage("boom") - .verify(); - - assertThat(observer.monoContexts()).hasSize(1); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingSufficiencyGateTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingSufficiencyGateTest.java deleted file mode 100644 index 923b4f99..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingSufficiencyGateTest.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.RETRIEVAL_SUFFICIENCY; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.openmemind.ai.memory.core.retrieval.query.QueryContext; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.sufficiency.SufficiencyGate; -import com.openmemind.ai.memory.core.retrieval.sufficiency.SufficiencyResult; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import com.openmemind.ai.memory.core.support.TestMemoryIds; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingSufficiencyGateTest { - - @Test - void checkPublishesSufficiencySpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(SufficiencyGate.class); - var result = SufficiencyResult.fallbackInsufficient(); - when(delegate.check(any(), any())).thenReturn(Mono.just(result)); - - var traced = new TracingSufficiencyGate(delegate, observer); - var context = - new QueryContext( - TestMemoryIds.userAgent(), "query", null, List.of(), Map.of(), null, null); - var scored = - List.of(new ScoredResult(ScoredResult.SourceType.ITEM, "id-1", "text", 0.9f, 0.85)); - - StepVerifier.create(traced.check(context, scored)).expectNext(result).verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().spanName()).isEqualTo(RETRIEVAL_SUFFICIENCY); - assertThat(observer.monoContexts().getFirst().requestAttributes()).isEmpty(); - } - - @Test - void checkPropagatesDelegateErrorsThroughObserver() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(SufficiencyGate.class); - when(delegate.check(any(), any())) - .thenReturn(Mono.error(new IllegalStateException("boom"))); - - var traced = new TracingSufficiencyGate(delegate, observer); - - StepVerifier.create( - traced.check( - new QueryContext( - TestMemoryIds.userAgent(), - "query", - null, - List.of(), - Map.of(), - null, - null), - List.of())) - .expectErrorMessage("boom") - .verify(); - - assertThat(observer.monoContexts()).hasSize(1); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingTemporalItemChannelTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingTemporalItemChannelTest.java deleted file mode 100644 index 8776e8cb..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingTemporalItemChannelTest.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.MEMORY_ID; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_CANDIDATE_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_CHANNEL; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_RESULT_COUNT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_TEMPORAL_CONSTRAINT_PRESENT; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_TEMPORAL_DEGRADED; -import static com.openmemind.ai.memory.core.tracing.MemoryAttributes.RETRIEVAL_TEMPORAL_ENABLED; -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.RETRIEVAL_TEMPORAL_CHANNEL; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.openmemind.ai.memory.core.data.DefaultMemoryId; -import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; -import com.openmemind.ai.memory.core.retrieval.query.QueryContext; -import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.temporal.TemporalConstraint; -import com.openmemind.ai.memory.core.retrieval.temporal.TemporalItemChannel; -import com.openmemind.ai.memory.core.retrieval.temporal.TemporalItemChannelResult; -import com.openmemind.ai.memory.core.retrieval.temporal.TemporalItemChannelSettings; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import com.openmemind.ai.memory.core.tracing.ObservationContext; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingTemporalItemChannelTest { - - @Test - void retrievePublishesTemporalChannelSpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - TemporalItemChannel delegate = mock(TemporalItemChannel.class); - var context = queryContext(); - var config = RetrievalConfig.simple(); - Optional constraint = Optional.empty(); - var settings = TemporalItemChannelSettings.defaults(); - var item = scoredResult(); - var result = new TemporalItemChannelResult(List.of(item), true, false, false, 7); - when(delegate.retrieve(context, config, constraint, settings)) - .thenReturn(Mono.just(result)); - - var traced = new TracingTemporalItemChannel(delegate, observer); - - StepVerifier.create(traced.retrieve(context, config, constraint, settings)) - .expectNext(result) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - var observation = observer.monoContexts().getFirst(); - assertThat(observation.spanName()).isEqualTo(RETRIEVAL_TEMPORAL_CHANNEL); - assertThat(observation.requestAttributes()) - .containsEntry(MEMORY_ID, "memory") - .containsEntry(RETRIEVAL_CHANNEL, "temporal") - .containsEntry(RETRIEVAL_TEMPORAL_ENABLED, true) - .containsEntry(RETRIEVAL_TEMPORAL_CONSTRAINT_PRESENT, false); - assertThat(resultAttributes(observation, result)) - .containsEntry(RETRIEVAL_RESULT_COUNT, 1) - .containsEntry(RETRIEVAL_CANDIDATE_COUNT, 7) - .containsEntry(RETRIEVAL_TEMPORAL_DEGRADED, false); - } - - private QueryContext queryContext() { - return new QueryContext( - DefaultMemoryId.of("memory", null), "query", null, List.of(), Map.of(), null, null); - } - - private ScoredResult scoredResult() { - return new ScoredResult(ScoredResult.SourceType.ITEM, "item-1", "text", 0.9f, 0.8d); - } - - @SuppressWarnings("unchecked") - private Map resultAttributes( - ObservationContext context, TemporalItemChannelResult result) { - return ((ObservationContext) context) - .resultExtractor() - .extract(result); - } -} diff --git a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingTypedQueryExpanderTest.java b/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingTypedQueryExpanderTest.java deleted file mode 100644 index 87eb878b..00000000 --- a/memind-core/src/test/java/com/openmemind/ai/memory/core/tracing/decorator/TracingTypedQueryExpanderTest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.core.tracing.decorator; - -import static com.openmemind.ai.memory.core.tracing.MemorySpanNames.RETRIEVAL_MULTI_QUERY_EXPAND; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.openmemind.ai.memory.core.retrieval.deep.ExpandedQuery; -import com.openmemind.ai.memory.core.retrieval.deep.TypedQueryExpander; -import com.openmemind.ai.memory.core.support.RecordingMemoryObserver; -import java.util.List; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -class TracingTypedQueryExpanderTest { - - @Test - void expandPublishesExpansionSpanAndPropagatesResult() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(TypedQueryExpander.class); - var expanded = - List.of( - new ExpandedQuery(ExpandedQuery.QueryType.VEC, "semantic query"), - new ExpandedQuery(ExpandedQuery.QueryType.LEX, "keyword query")); - when(delegate.expand(any(), any(), any(), any(), anyInt())).thenReturn(Mono.just(expanded)); - - var traced = new TracingTypedQueryExpander(delegate, observer); - - StepVerifier.create( - traced.expand( - "query", List.of("gap"), List.of("info"), List.of("history"), 3)) - .expectNext(expanded) - .verifyComplete(); - - assertThat(observer.monoContexts()).hasSize(1); - assertThat(observer.monoContexts().getFirst().spanName()) - .isEqualTo(RETRIEVAL_MULTI_QUERY_EXPAND); - assertThat(observer.monoContexts().getFirst().requestAttributes()).isEmpty(); - } - - @Test - void expandPropagatesDelegateErrorsThroughObserver() { - var observer = new RecordingMemoryObserver(); - var delegate = mock(TypedQueryExpander.class); - when(delegate.expand(any(), any(), any(), any(), anyInt())) - .thenReturn(Mono.error(new IllegalStateException("boom"))); - - var traced = new TracingTypedQueryExpander(delegate, observer); - - StepVerifier.create(traced.expand("query", List.of(), List.of(), List.of(), 3)) - .expectErrorMessage("boom") - .verify(); - - assertThat(observer.monoContexts()).hasSize(1); - } -} diff --git a/memind-dependencies/pom.xml b/memind-dependencies/pom.xml index ef05c82a..26c238c2 100644 --- a/memind-dependencies/pom.xml +++ b/memind-dependencies/pom.xml @@ -87,14 +87,6 @@ - - io.opentelemetry.instrumentation - opentelemetry-instrumentation-bom - 2.29.0 - pom - import - - org.springframework.boot @@ -190,7 +182,7 @@ com.openmemind.ai - memind-plugin-tracing-opentelemetry + memind-plugin-observability-micrometer ${revision} diff --git a/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-mysql/src/test/java/com/openmemind/ai/memory/plugin/jdbc/mysql/MysqlConversationBufferConcurrencyTest.java b/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-mysql/src/test/java/com/openmemind/ai/memory/plugin/jdbc/mysql/MysqlConversationBufferConcurrencyTest.java index 68d1ea47..a9c3d90e 100644 --- a/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-mysql/src/test/java/com/openmemind/ai/memory/plugin/jdbc/mysql/MysqlConversationBufferConcurrencyTest.java +++ b/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-mysql/src/test/java/com/openmemind/ai/memory/plugin/jdbc/mysql/MysqlConversationBufferConcurrencyTest.java @@ -34,7 +34,7 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -@Testcontainers +@Testcontainers(disabledWithoutDocker = true) class MysqlConversationBufferConcurrencyTest { @Container private static final MySQLContainer MYSQL = new MySQLContainer<>("mysql:8.4"); diff --git a/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-mysql/src/test/java/com/openmemind/ai/memory/plugin/jdbc/mysql/MysqlStoreSchemaBootstrapIntegrationTest.java b/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-mysql/src/test/java/com/openmemind/ai/memory/plugin/jdbc/mysql/MysqlStoreSchemaBootstrapIntegrationTest.java index a438e30f..45bb73c6 100644 --- a/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-mysql/src/test/java/com/openmemind/ai/memory/plugin/jdbc/mysql/MysqlStoreSchemaBootstrapIntegrationTest.java +++ b/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-mysql/src/test/java/com/openmemind/ai/memory/plugin/jdbc/mysql/MysqlStoreSchemaBootstrapIntegrationTest.java @@ -29,7 +29,7 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -@Testcontainers +@Testcontainers(disabledWithoutDocker = true) class MysqlStoreSchemaBootstrapIntegrationTest { @Container private static final MySQLContainer MYSQL = new MySQLContainer<>("mysql:8.4"); diff --git a/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-postgresql/src/test/java/com/openmemind/ai/memory/plugin/jdbc/postgresql/PostgresqlConversationBufferConcurrencyTest.java b/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-postgresql/src/test/java/com/openmemind/ai/memory/plugin/jdbc/postgresql/PostgresqlConversationBufferConcurrencyTest.java index 25cd712d..5120d88b 100644 --- a/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-postgresql/src/test/java/com/openmemind/ai/memory/plugin/jdbc/postgresql/PostgresqlConversationBufferConcurrencyTest.java +++ b/memind-plugins/memind-plugin-jdbc/memind-plugin-jdbc-postgresql/src/test/java/com/openmemind/ai/memory/plugin/jdbc/postgresql/PostgresqlConversationBufferConcurrencyTest.java @@ -34,7 +34,7 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -@Testcontainers +@Testcontainers(disabledWithoutDocker = true) class PostgresqlConversationBufferConcurrencyTest { @Container diff --git a/memind-plugins/memind-plugin-tracing-opentelemetry/pom.xml b/memind-plugins/memind-plugin-observability-micrometer/pom.xml similarity index 59% rename from memind-plugins/memind-plugin-tracing-opentelemetry/pom.xml rename to memind-plugins/memind-plugin-observability-micrometer/pom.xml index 72ea38f4..083467b4 100644 --- a/memind-plugins/memind-plugin-tracing-opentelemetry/pom.xml +++ b/memind-plugins/memind-plugin-observability-micrometer/pom.xml @@ -24,8 +24,8 @@ ../pom.xml - memind-plugin-tracing-opentelemetry - Memind - OpenTelemetry Tracing Plugin + memind-plugin-observability-micrometer + Memind - Micrometer Observability Plugin @@ -36,34 +36,12 @@ - io.projectreactor - reactor-core-micrometer + io.micrometer + micrometer-core io.micrometer micrometer-observation - - - io.opentelemetry - opentelemetry-api - - - - io.opentelemetry.instrumentation - opentelemetry-reactor-3.1 - 2.29.0-alpha - - - - io.opentelemetry - opentelemetry-sdk-testing - test - - - io.opentelemetry - opentelemetry-sdk-metrics - test - diff --git a/memind-plugins/memind-plugin-observability-micrometer/src/main/java/com/openmemind/ai/memory/plugin/observability/micrometer/MemoryMeterObservationHandler.java b/memind-plugins/memind-plugin-observability-micrometer/src/main/java/com/openmemind/ai/memory/plugin/observability/micrometer/MemoryMeterObservationHandler.java new file mode 100644 index 00000000..2ee93eef --- /dev/null +++ b/memind-plugins/memind-plugin-observability-micrometer/src/main/java/com/openmemind/ai/memory/plugin/observability/micrometer/MemoryMeterObservationHandler.java @@ -0,0 +1,363 @@ +/* + * 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 com.openmemind.ai.memory.plugin.observability.micrometer; + +import com.openmemind.ai.memory.core.extraction.observation.DefaultMemoryExtractorObservation.ExtractionObservationContext; +import com.openmemind.ai.memory.core.llm.rerank.observation.LlmRerankerObservation.RerankObservationContext; +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import com.openmemind.ai.memory.core.retrieval.deep.observation.LlmTypedQueryExpanderObservation.MultiQueryExpandObservationContext; +import com.openmemind.ai.memory.core.retrieval.graph.observation.DefaultGraphItemChannelObservation.GraphItemChannelObservationContext; +import com.openmemind.ai.memory.core.retrieval.graph.observation.DefaultRetrievalGraphAssistantObservation.GraphAssistObservationContext; +import com.openmemind.ai.memory.core.retrieval.observation.DefaultMemoryRetrieverObservation.RetrievalObservationContext; +import com.openmemind.ai.memory.core.retrieval.scoring.observation.DefaultRetrievalResultMergerObservation.ResultMergeObservationContext; +import com.openmemind.ai.memory.core.retrieval.sufficiency.observation.LlmSufficiencyGateObservation.SufficiencyObservationContext; +import com.openmemind.ai.memory.core.retrieval.temporal.observation.DefaultTemporalItemChannelObservation.TemporalItemChannelObservationContext; +import com.openmemind.ai.memory.core.retrieval.tier.observation.InsightTierRetrieverObservation.InsightTierObservationContext; +import com.openmemind.ai.memory.core.retrieval.tier.observation.ItemTierRetrieverObservation.ItemTierObservationContext; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationHandler; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; + +public final class MemoryMeterObservationHandler + implements ObservationHandler { + + private static final Set STRATEGIES = + Set.of("simple", "deep", "deep_retrieval", "unknown"); + private static final Set STAGES = + Set.of( + "tier", + "channel", + "merge", + "rerank", + "final", + "sufficiency", + "query_expand", + "graph_assist"); + private static final Set TIERS = Set.of("item", "insight", "raw_data", "none"); + private static final Set METHODS = + Set.of( + "vector", + "keyword", + "hybrid", + "graph", + "temporal", + "rrf", + "rerank", + "llm", + "final", + "none"); + private static final Set STATUSES = + Set.of( + "success", + "error", + "cancelled", + "degraded", + "skipped", + "empty", + "failed", + "partial_success", + "unknown"); + private static final Set SOURCES = Set.of("api", "core", "internal"); + + private final MeterRegistry meterRegistry; + + public MemoryMeterObservationHandler(MeterRegistry meterRegistry) { + this.meterRegistry = Objects.requireNonNull(meterRegistry, "meterRegistry"); + } + + @Override + public void onStop(MemoryObservationContext context) { + try { + write(context); + } catch (RuntimeException ignored) { + // Metrics collection must never affect memory operations. + } + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof MemoryObservationContext; + } + + private void write(MemoryObservationContext context) { + if (context instanceof ExtractionObservationContext extraction) { + writeExtractionSummary(extraction); + } else if (context instanceof RetrievalObservationContext retrieval) { + writeRetrievalSummary( + retrieval.strategyName(), + retrieval.status(), + retrieval.itemCount(), + retrieval.insightCount(), + retrieval.rawDataCount(), + retrieval.evidenceCount(), + retrieval.source()); + } else if (context instanceof ItemTierObservationContext itemTier) { + writeRetrievalStage( + itemTier.strategyName(), + itemTier.stage(), + itemTier.tier(), + itemTier.method(), + itemTier.status(), + null, + itemTier.resultCount(), + false, + false, + itemTier.source()); + } else if (context instanceof InsightTierObservationContext insightTier) { + writeRetrievalStage( + insightTier.strategyName(), + insightTier.stage(), + insightTier.tier(), + insightTier.method(), + insightTier.status(), + null, + insightTier.resultCount(), + false, + false, + insightTier.source()); + } else if (context instanceof GraphItemChannelObservationContext graphChannel) { + writeRetrievalStage( + graphChannel.strategyName(), + graphChannel.stage(), + graphChannel.tier(), + graphChannel.method(), + graphChannel.status(), + graphChannel.candidateCount(), + graphChannel.resultCount(), + graphChannel.degraded(), + graphChannel.skipped(), + graphChannel.source()); + } else if (context instanceof TemporalItemChannelObservationContext temporalChannel) { + writeRetrievalStage( + temporalChannel.strategyName(), + temporalChannel.stage(), + temporalChannel.tier(), + temporalChannel.method(), + temporalChannel.status(), + temporalChannel.candidateCount(), + temporalChannel.resultCount(), + temporalChannel.degraded(), + temporalChannel.skipped(), + temporalChannel.source()); + } else if (context instanceof MultiQueryExpandObservationContext queryExpand) { + writeRetrievalStage( + null, + queryExpand.stage(), + queryExpand.tier(), + queryExpand.method(), + queryExpand.status(), + queryExpand.candidateCount(), + queryExpand.resultCount(), + queryExpand.degraded(), + queryExpand.skipped(), + queryExpand.source()); + } else if (context instanceof SufficiencyObservationContext sufficiency) { + writeRetrievalStage( + null, + sufficiency.stage(), + sufficiency.tier(), + sufficiency.method(), + sufficiency.status(), + sufficiency.candidateCount(), + sufficiency.resultCount(), + sufficiency.degraded(), + sufficiency.skipped(), + sufficiency.source()); + } else if (context instanceof RerankObservationContext rerank) { + writeRetrievalStage( + null, + rerank.stage(), + rerank.tier(), + rerank.method(), + rerank.status(), + rerank.candidateCount(), + rerank.resultCount(), + rerank.degraded(), + rerank.skipped(), + rerank.source()); + } else if (context instanceof GraphAssistObservationContext graphAssist) { + writeRetrievalStage( + null, + graphAssist.stage(), + graphAssist.tier(), + graphAssist.method(), + graphAssist.status(), + graphAssist.candidateCount(), + graphAssist.resultCount(), + graphAssist.degraded(), + graphAssist.skipped(), + graphAssist.source()); + } else if (context instanceof ResultMergeObservationContext merge) { + writeRetrievalMerge(merge); + } + } + + private void writeExtractionSummary(ExtractionObservationContext metrics) { + if (!metrics.hasResult() && metrics.getError() == null) { + return; + } + Tags tags = extractionTags(metrics.status(), metrics.source()); + writeDistribution("memind.extraction.raw_data", "raw_data", metrics.rawDataCount(), tags); + writeNullableDistribution( + "memind.extraction.segments", "segment", metrics.segmentCount(), tags); + writeDistribution("memind.extraction.items", "item", metrics.newItemCount(), tags); + writeDistribution("memind.extraction.items.new", "item", metrics.newItemCount(), tags); + writeNullableDistribution( + "memind.extraction.items.reinforced", "item", metrics.reinforcedItemCount(), tags); + writeDistribution("memind.extraction.insights", "insight", metrics.insightCount(), tags); + writeNullableDistribution( + "memind.extraction.graph.entities", "entity", metrics.graphEntityCount(), tags); + writeNullableDistribution( + "memind.extraction.graph.mentions", "mention", metrics.graphMentionCount(), tags); + writeNullableDistribution( + "memind.extraction.graph.relations", + "relation", + metrics.graphRelationCount(), + tags); + } + + private void writeRetrievalStage( + String strategy, + String stage, + String tier, + String method, + String status, + Integer candidateCount, + Integer resultCount, + boolean degraded, + boolean skipped, + String source) { + Tags tags = retrievalTags(strategy, stage, tier, method, status, source); + writeNullableDistribution("memind.retrieval.candidates", "candidate", candidateCount, tags); + writeNullableDistribution( + "memind.retrieval.results", "result", resultCount, tags.and("result_type", "none")); + if (degraded) { + incrementCounter("memind.retrieval.stage.degraded", tags); + } + if (skipped) { + incrementCounter("memind.retrieval.stage.skipped", tags); + } + } + + private void writeRetrievalMerge(ResultMergeObservationContext metrics) { + Tags tags = + retrievalTags( + metrics.strategyName(), + "merge", + "none", + "rrf", + metrics.status(), + metrics.source()); + writeDistribution("memind.retrieval.merge.inputs", "candidate", metrics.inputCount(), tags); + writeDistribution( + "memind.retrieval.merge.outputs", "candidate", metrics.outputCount(), tags); + writeDistribution( + "memind.retrieval.merge.deduplicated", + "candidate", + metrics.deduplicatedCount(), + tags); + } + + private void writeRetrievalSummary( + String strategy, + String status, + int itemCount, + int insightCount, + int rawDataCount, + int evidenceCount, + String source) { + Tags tags = retrievalTags(strategy, "final", "none", "final", status, source); + writeFinalResult(itemCount, "item", tags); + writeFinalResult(insightCount, "insight", tags); + writeFinalResult(rawDataCount, "raw_data", tags); + writeFinalResult(evidenceCount, "evidence", tags); + if ("empty".equals(status)) { + incrementCounter("memind.retrieval.empty_results", tags); + } + } + + private void writeFinalResult(int count, String resultType, Tags baseTags) { + writeDistribution( + "memind.retrieval.results", + "result", + count, + baseTags.and("result_type", resultType)); + } + + private void writeDistribution(String name, String baseUnit, double value, Tags tags) { + DistributionSummary.builder(name) + .baseUnit(baseUnit) + .tags(tags) + .register(meterRegistry) + .record(value); + } + + private void incrementCounter(String name, Tags tags) { + Counter.builder(name).baseUnit("event").tags(tags).register(meterRegistry).increment(); + } + + private void writeNullableDistribution(String name, String baseUnit, Integer value, Tags tags) { + if (value != null) { + writeDistribution(name, baseUnit, value, tags); + } + } + + private static Tags extractionTags(String status, String source) { + return Tags.of( + "operation", + "extraction", + "status", + normalize(status, STATUSES, "unknown"), + "source", + normalize(source, SOURCES, "core")); + } + + private static Tags retrievalTags( + String strategy, + String stage, + String tier, + String method, + String status, + String source) { + return Tags.of( + "operation", + "retrieval", + "strategy", + normalize(strategy, STRATEGIES, "unknown"), + "stage", + normalize(stage, STAGES, "final"), + "tier", + normalize(tier, TIERS, "none"), + "method", + normalize(method, METHODS, "none"), + "status", + normalize(status, STATUSES, "unknown"), + "source", + normalize(source, SOURCES, "core")); + } + + private static String normalize(String value, Set allowed, String fallback) { + if (value == null || value.isBlank()) { + return fallback; + } + String normalized = value.toLowerCase(Locale.ROOT); + return allowed.contains(normalized) ? normalized : fallback; + } +} diff --git a/memind-plugins/memind-plugin-observability-micrometer/src/test/java/com/openmemind/ai/memory/plugin/observability/micrometer/MemoryMeterObservationHandlerTest.java b/memind-plugins/memind-plugin-observability-micrometer/src/test/java/com/openmemind/ai/memory/plugin/observability/micrometer/MemoryMeterObservationHandlerTest.java new file mode 100644 index 00000000..326fbf30 --- /dev/null +++ b/memind-plugins/memind-plugin-observability-micrometer/src/test/java/com/openmemind/ai/memory/plugin/observability/micrometer/MemoryMeterObservationHandlerTest.java @@ -0,0 +1,358 @@ +/* + * 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 com.openmemind.ai.memory.plugin.observability.micrometer; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.openmemind.ai.memory.core.data.MemoryId; +import com.openmemind.ai.memory.core.data.MemoryInsight; +import com.openmemind.ai.memory.core.data.MemoryItem; +import com.openmemind.ai.memory.core.data.MemoryRawData; +import com.openmemind.ai.memory.core.extraction.ExtractionResult; +import com.openmemind.ai.memory.core.extraction.observation.DefaultMemoryExtractorObservation.ExtractionObservationContext; +import com.openmemind.ai.memory.core.extraction.result.InsightResult; +import com.openmemind.ai.memory.core.extraction.result.MemoryItemResult; +import com.openmemind.ai.memory.core.extraction.result.RawDataResult; +import com.openmemind.ai.memory.core.retrieval.RetrievalResult; +import com.openmemind.ai.memory.core.retrieval.observation.DefaultMemoryRetrieverObservation.RetrievalObservationContext; +import com.openmemind.ai.memory.core.retrieval.query.QueryContext; +import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; +import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult.SourceType; +import com.openmemind.ai.memory.core.retrieval.scoring.observation.DefaultRetrievalResultMergerObservation.ResultMergeObservationContext; +import com.openmemind.ai.memory.core.retrieval.temporal.TemporalItemChannelResult; +import com.openmemind.ai.memory.core.retrieval.temporal.TemporalItemChannelSettings; +import com.openmemind.ai.memory.core.retrieval.temporal.observation.DefaultTemporalItemChannelObservation.TemporalItemChannelObservationContext; +import com.openmemind.ai.memory.core.retrieval.tier.observation.ItemTierRetrieverObservation.ItemTierDocument; +import com.openmemind.ai.memory.core.retrieval.tier.observation.ItemTierRetrieverObservation.ItemTierObservationContext; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; + +class MemoryMeterObservationHandlerTest { + + @Test + void recordsBusinessMetricPayloadsFromObservationContexts() { + SimpleMeterRegistry meterRegistry = new SimpleMeterRegistry(); + ObservationRegistry observationRegistry = ObservationRegistry.create(); + observationRegistry + .observationConfig() + .observationHandler(new MemoryMeterObservationHandler(meterRegistry)); + + ExtractionObservationContext extraction = new ExtractionObservationContext(memoryId()); + observe( + observationRegistry, + extraction, + () -> extraction.recordResult(extractionResult(1, 2, 3))); + + ItemTierObservationContext tier = + new ItemTierObservationContext( + queryContext(), "item", ItemTierDocument.VECTOR_SEARCH, 5); + observe(observationRegistry, tier, () -> tier.recordResults(scoredResults(5))); + + TemporalItemChannelObservationContext temporal = + new TemporalItemChannelObservationContext( + queryContext(), Optional.empty(), TemporalItemChannelSettings.defaults()); + observe( + observationRegistry, + temporal, + () -> + temporal.recordResult( + new TemporalItemChannelResult( + scoredResults(2), true, true, true, 3))); + + ResultMergeObservationContext merge = + new ResultMergeObservationContext( + List.of(scoredResults(10), scoredResults(10, 10)), new double[] {1.0, 1.0}); + observe(observationRegistry, merge, () -> merge.recordResult(scoredResults(12))); + + RetrievalObservationContext summary = new RetrievalObservationContext(memoryId()); + observe( + observationRegistry, + summary, + () -> summary.recordResult(RetrievalResult.empty("simple", "query"))); + + RetrievalObservationContext deepSummary = new RetrievalObservationContext(memoryId()); + observe( + observationRegistry, + deepSummary, + () -> deepSummary.recordResult(RetrievalResult.empty("deep_retrieval", "query"))); + + RetrievalObservationContext degradedSummary = new RetrievalObservationContext(memoryId()); + observe( + observationRegistry, + degradedSummary, + () -> degradedSummary.recordResult(RetrievalResult.degraded("simple", "query"))); + + Tags extractionTags = + Tags.of("operation", "extraction", "status", "success", "source", "core"); + assertThat(summaryTotal(meterRegistry, "memind.extraction.raw_data", extractionTags)) + .isEqualTo(1.0); + assertThat(meterRegistry.find("memind.extraction.segments").tags(extractionTags).summary()) + .isNull(); + assertThat(summaryTotal(meterRegistry, "memind.extraction.items", extractionTags)) + .isEqualTo(2.0); + assertThat(summaryTotal(meterRegistry, "memind.extraction.insights", extractionTags)) + .isEqualTo(3.0); + + Tags tierTags = + Tags.of( + "operation", "retrieval", + "strategy", "unknown", + "stage", "tier", + "tier", "item", + "method", "vector", + "status", "success", + "source", "core"); + assertThat( + summaryTotal( + meterRegistry, + "memind.retrieval.results", + tierTags.and("result_type", "none"))) + .isEqualTo(5.0); + + Tags degradedTags = + Tags.of( + "operation", "retrieval", + "strategy", "unknown", + "stage", "channel", + "tier", "item", + "method", "temporal", + "status", "degraded", + "source", "core"); + assertThat(summaryTotal(meterRegistry, "memind.retrieval.candidates", degradedTags)) + .isEqualTo(3.0); + assertThat( + meterRegistry + .find("memind.retrieval.stage.degraded") + .tags(degradedTags) + .counter() + .count()) + .isEqualTo(1.0); + + Tags mergeTags = + Tags.of( + "operation", "retrieval", + "strategy", "unknown", + "stage", "merge", + "tier", "none", + "method", "rrf", + "status", "success", + "source", "core"); + assertThat(summaryTotal(meterRegistry, "memind.retrieval.merge.inputs", mergeTags)) + .isEqualTo(20.0); + assertThat(summaryTotal(meterRegistry, "memind.retrieval.merge.outputs", mergeTags)) + .isEqualTo(12.0); + assertThat(summaryTotal(meterRegistry, "memind.retrieval.merge.deduplicated", mergeTags)) + .isEqualTo(8.0); + + Tags finalTags = + Tags.of( + "operation", "retrieval", + "strategy", "simple", + "stage", "final", + "tier", "none", + "method", "final", + "status", "empty", + "source", "core"); + assertThat( + meterRegistry + .find("memind.retrieval.empty_results") + .tags(finalTags) + .counter() + .count()) + .isEqualTo(1.0); + assertThat( + meterRegistry + .find("memind.retrieval.empty_results") + .tags( + Tags.of( + "operation", "retrieval", + "strategy", "simple", + "stage", "final", + "tier", "none", + "method", "final", + "status", "degraded", + "source", "core")) + .counter()) + .isNull(); + assertThat( + summaryTotal( + meterRegistry, + "memind.retrieval.results", + finalTags.and("result_type", "item"))) + .isEqualTo(0.0); + + Tags deepFinalTags = + Tags.of( + "operation", "retrieval", + "strategy", "deep_retrieval", + "stage", "final", + "tier", "none", + "method", "final", + "status", "empty", + "source", "core"); + assertThat( + meterRegistry + .find("memind.retrieval.empty_results") + .tags(deepFinalTags) + .counter() + .count()) + .isEqualTo(1.0); + assertThat(resultTagKeySets(meterRegistry)) + .containsExactly( + Set.of( + "operation", + "strategy", + "stage", + "tier", + "method", + "status", + "source", + "result_type")); + } + + private static void observe( + ObservationRegistry registry, Observation.Context context, Runnable operation) { + Observation.createNotStarted("memind.test", () -> context, registry).observe(operation); + } + + private static ExtractionResult extractionResult( + int rawDataCount, int itemCount, int insightCount) { + return ExtractionResult.success( + memoryId(), + new RawDataResult(rawData(rawDataCount), null, false), + new MemoryItemResult(memoryItems(itemCount), List.of()), + new InsightResult(insights(insightCount)), + Duration.ZERO); + } + + private static MemoryId memoryId() { + return () -> "memory-1"; + } + + private static QueryContext queryContext() { + return new QueryContext(memoryId(), "query", null, List.of(), Map.of(), null, Set.of()); + } + + private static List scoredResults(int count) { + return scoredResults(count, 0); + } + + private static List scoredResults(int count, int offset) { + return IntStream.range(0, count) + .mapToObj( + index -> + new ScoredResult( + SourceType.ITEM, + String.valueOf(offset + index), + "text", + 1.0f, + 1.0d)) + .toList(); + } + + private static List rawData(int count) { + return IntStream.range(0, count) + .mapToObj( + index -> + new MemoryRawData( + String.valueOf(index), + "memory-1", + "conversation", + null, + null, + null, + null, + Map.of(), + null, + null, + null, + null, + null)) + .toList(); + } + + private static List memoryItems(int count) { + return IntStream.range(0, count) + .mapToObj( + index -> + new MemoryItem( + (long) index, + "memory-1", + "content", + null, + null, + "conversation", + null, + null, + null, + null, + null, + null, + Map.of(), + null, + null)) + .toList(); + } + + private static List insights(int count) { + return IntStream.range(0, count) + .mapToObj( + index -> + new MemoryInsight( + (long) index, + "memory-1", + "type", + null, + "name", + List.of(), + List.of(), + null, + null, + List.of(), + null, + null, + null, + null, + List.of(), + 1)) + .toList(); + } + + private static double summaryTotal( + SimpleMeterRegistry registry, + String name, + Iterable tags) { + return registry.find(name).tags(tags).summary().totalAmount(); + } + + private static Set> resultTagKeySets(SimpleMeterRegistry registry) { + return registry.find("memind.retrieval.results").meters().stream() + .map( + meter -> + meter.getId().getTags().stream() + .map(io.micrometer.core.instrument.Tag::getKey) + .collect(Collectors.toSet())) + .collect(Collectors.toSet()); + } +} diff --git a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-mybatis-plus-starter/src/test/java/com/openmemind/ai/memory/plugin/store/mybatis/buffer/DialectConversationBufferAtomicOperationsTest.java b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-mybatis-plus-starter/src/test/java/com/openmemind/ai/memory/plugin/store/mybatis/buffer/DialectConversationBufferAtomicOperationsTest.java index d13e70d4..27060b54 100644 --- a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-mybatis-plus-starter/src/test/java/com/openmemind/ai/memory/plugin/store/mybatis/buffer/DialectConversationBufferAtomicOperationsTest.java +++ b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-mybatis-plus-starter/src/test/java/com/openmemind/ai/memory/plugin/store/mybatis/buffer/DialectConversationBufferAtomicOperationsTest.java @@ -40,7 +40,7 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -@Testcontainers +@Testcontainers(disabledWithoutDocker = true) class DialectConversationBufferAtomicOperationsTest { @Container private static final MySQLContainer MYSQL = new MySQLContainer<>("mysql:8.4"); diff --git a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-mybatis-plus-starter/src/test/java/com/openmemind/ai/memory/plugin/store/mybatis/schema/MemoryStoreDdlMysqlIntegrationTest.java b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-mybatis-plus-starter/src/test/java/com/openmemind/ai/memory/plugin/store/mybatis/schema/MemoryStoreDdlMysqlIntegrationTest.java index 880789b7..ed780375 100644 --- a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-mybatis-plus-starter/src/test/java/com/openmemind/ai/memory/plugin/store/mybatis/schema/MemoryStoreDdlMysqlIntegrationTest.java +++ b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-mybatis-plus-starter/src/test/java/com/openmemind/ai/memory/plugin/store/mybatis/schema/MemoryStoreDdlMysqlIntegrationTest.java @@ -28,7 +28,7 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -@Testcontainers +@Testcontainers(disabledWithoutDocker = true) class MemoryStoreDdlMysqlIntegrationTest { @Container private static final MySQLContainer MYSQL = new MySQLContainer<>("mysql:8.4"); diff --git a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/pom.xml b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-observability-micrometer-starter/pom.xml similarity index 85% rename from memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/pom.xml rename to memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-observability-micrometer-starter/pom.xml index 7f84e085..502181d9 100644 --- a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/pom.xml +++ b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-observability-micrometer-starter/pom.xml @@ -25,13 +25,13 @@ ../pom.xml - memind-plugin-tracing-opentelemetry-starter - Memind - OpenTelemetry Tracing Plugin Starter + memind-plugin-observability-micrometer-starter + Memind - Micrometer Observability Plugin Starter com.openmemind.ai - memind-plugin-tracing-opentelemetry + memind-plugin-observability-micrometer ${revision} @@ -41,13 +41,12 @@ org.springframework.boot - spring-boot-starter-opentelemetry + spring-boot-starter-test + test - org.springframework.boot - spring-boot-starter-test - test + spring-boot-micrometer-observation diff --git a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-observability-micrometer-starter/src/main/java/com/openmemind/ai/memory/plugin/observability/micrometer/autoconfigure/MemoryObservabilityAutoConfiguration.java b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-observability-micrometer-starter/src/main/java/com/openmemind/ai/memory/plugin/observability/micrometer/autoconfigure/MemoryObservabilityAutoConfiguration.java new file mode 100644 index 00000000..28a8e3e9 --- /dev/null +++ b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-observability-micrometer-starter/src/main/java/com/openmemind/ai/memory/plugin/observability/micrometer/autoconfigure/MemoryObservabilityAutoConfiguration.java @@ -0,0 +1,55 @@ +/* + * 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 com.openmemind.ai.memory.plugin.observability.micrometer.autoconfigure; + +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceObservationHandler; +import com.openmemind.ai.memory.plugin.observability.micrometer.MemoryMeterObservationHandler; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.observation.ObservationRegistry; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +@ConditionalOnClass(ObservationRegistry.class) +@ConditionalOnProperty( + prefix = "memind.observability", + name = "enabled", + havingValue = "true", + matchIfMissing = true) +public class MemoryObservabilityAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(ObservationRegistry.class) + public ObservationRegistry observationRegistry() { + return ObservationRegistry.create(); + } + + @Bean + @ConditionalOnMissingBean(RetrievalTraceObservationHandler.class) + public RetrievalTraceObservationHandler retrievalTraceObservationHandler() { + return new RetrievalTraceObservationHandler(); + } + + @Bean + @ConditionalOnBean(MeterRegistry.class) + @ConditionalOnMissingBean(MemoryMeterObservationHandler.class) + public MemoryMeterObservationHandler memoryMeterObservationHandler( + MeterRegistry meterRegistry) { + return new MemoryMeterObservationHandler(meterRegistry); + } +} diff --git a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-observability-micrometer-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-observability-micrometer-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..d0faa31a --- /dev/null +++ b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-observability-micrometer-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.openmemind.ai.memory.plugin.observability.micrometer.autoconfigure.MemoryObservabilityAutoConfiguration diff --git a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-observability-micrometer-starter/src/test/java/com/openmemind/ai/memory/plugin/observability/micrometer/autoconfigure/MemoryObservabilityAutoConfigurationTest.java b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-observability-micrometer-starter/src/test/java/com/openmemind/ai/memory/plugin/observability/micrometer/autoconfigure/MemoryObservabilityAutoConfigurationTest.java new file mode 100644 index 00000000..d662187f --- /dev/null +++ b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-observability-micrometer-starter/src/test/java/com/openmemind/ai/memory/plugin/observability/micrometer/autoconfigure/MemoryObservabilityAutoConfigurationTest.java @@ -0,0 +1,333 @@ +/* + * 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 com.openmemind.ai.memory.plugin.observability.micrometer.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.openmemind.ai.memory.core.observation.MemoryObservationContext; +import com.openmemind.ai.memory.core.retrieval.DefaultMemoryRetriever; +import com.openmemind.ai.memory.core.retrieval.RetrievalConfig; +import com.openmemind.ai.memory.core.retrieval.RetrievalRequest; +import com.openmemind.ai.memory.core.retrieval.RetrievalResult; +import com.openmemind.ai.memory.core.retrieval.RetrievalStatus; +import com.openmemind.ai.memory.core.retrieval.admission.DefaultRetrievalAdmissionPolicy; +import com.openmemind.ai.memory.core.retrieval.admission.RetrievalAdmissionOptions; +import com.openmemind.ai.memory.core.retrieval.observation.DefaultMemoryRetrieverObservation.RetrievalObservationContext; +import com.openmemind.ai.memory.core.retrieval.query.QueryContext; +import com.openmemind.ai.memory.core.retrieval.strategy.RetrievalStrategy; +import com.openmemind.ai.memory.core.retrieval.trace.BoundedRetrievalTraceRecorder; +import com.openmemind.ai.memory.core.retrieval.trace.ObservationTiming; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEvent; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEventSource; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceObservationHandler; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceRecorder; +import com.openmemind.ai.memory.core.store.MemoryStore; +import com.openmemind.ai.memory.core.store.item.ItemOperations; +import com.openmemind.ai.memory.plugin.observability.micrometer.MemoryMeterObservationHandler; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import reactor.core.publisher.Mono; + +@DisplayName("MemoryObservabilityAutoConfiguration Test") +class MemoryObservabilityAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of( + ObservationAutoConfiguration.class, + MemoryObservabilityAutoConfiguration.class)); + + @Nested + @DisplayName("Default Configuration") + class Defaults { + + @Test + @DisplayName("Register Micrometer meter observation handler when MeterRegistry exists") + void registersMeterObservationHandlerWhenMeterRegistryExists() { + contextRunner + .withUserConfiguration(MicrometerProviderConfig.class) + .run( + context -> { + assertThat(context) + .hasSingleBean(MemoryMeterObservationHandler.class); + + var observationContext = + new RetrievalObservationContext(() -> "memory-1"); + Observation.createNotStarted( + "memind.test", + () -> observationContext, + context.getBean(ObservationRegistry.class)) + .observe( + () -> + observationContext.recordResult( + RetrievalResult.empty( + "simple", "query"))); + + assertThat( + context.getBean(MeterRegistry.class) + .find("memind.retrieval.empty_results") + .counter() + .count()) + .isEqualTo(1.0); + }); + } + + @Test + @DisplayName( + "Register ObservationRegistry without meter handler when MeterRegistry missing") + void registersObservationRegistryWithoutMeterHandlerWhenMeterRegistryMissing() { + contextRunner.run( + context -> { + assertThat(context).hasSingleBean(ObservationRegistry.class); + assertThat(context).doesNotHaveBean(MemoryMeterObservationHandler.class); + }); + } + + @Test + @DisplayName("Register retrieval trace observation handler in ObservationRegistry") + void registersRetrievalTraceObservationHandler() { + contextRunner.run( + context -> { + assertThat(context).hasSingleBean(RetrievalTraceObservationHandler.class); + var recorder = + new BoundedRetrievalTraceRecorder( + new RetrievalTraceOptions(8, 4, 16)); + var observationContext = new TestTraceContext(recorder); + + Observation.createNotStarted( + "memind.test", + () -> observationContext, + context.getBean(ObservationRegistry.class)) + .observe(() -> {}); + + assertThat(recorder.snapshot().orElseThrow().stages()).hasSize(1); + }); + } + + @Test + @DisplayName("Record timeout fallback consistently across API, trace, and metrics") + void recordsTimeoutFallbackConsistently() { + contextRunner + .withUserConfiguration(MicrometerProviderConfig.class) + .run( + context -> { + var memoryId = + (com.openmemind.ai.memory.core.data.MemoryId) + () -> "memory-1"; + var store = mock(MemoryStore.class); + var itemOperations = mock(ItemOperations.class); + var strategy = mock(RetrievalStrategy.class); + when(store.itemOperations()).thenReturn(itemOperations); + when(itemOperations.hasItems(memoryId)).thenReturn(true); + when(strategy.name()).thenReturn("simple"); + when(strategy.retrieve(any(), any())).thenReturn(Mono.never()); + + var options = RetrievalAdmissionOptions.defaults(); + var retriever = + new DefaultMemoryRetriever( + store, + null, + null, + new DefaultRetrievalAdmissionPolicy(options), + options, + null, + context.getBean(ObservationRegistry.class)); + retriever.registerStrategy(strategy); + var config = + RetrievalConfig.simple().withTimeout(Duration.ofMillis(10)); + var request = + new RetrievalRequest( + memoryId, "query", List.of(), config, Map.of(), + null, null); + var recorder = + new BoundedRetrievalTraceRecorder( + RetrievalTraceOptions.defaults()); + + var result = + retriever + .retrieve(request) + .contextWrite( + reactorContext -> + reactorContext.put( + RetrievalTraceRecorder + .class, + recorder)) + .block(Duration.ofSeconds(1)); + + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(RetrievalStatus.DEGRADED); + assertThat(recorder.snapshot().orElseThrow().finalResults()) + .isNotNull() + .extracting(finalTrace -> finalTrace.status()) + .isEqualTo("degraded"); + + var meterRegistry = context.getBean(MeterRegistry.class); + assertThat( + meterRegistry + .find("memind.retrieval.results") + .tags( + "status", + "degraded", + "result_type", + "item") + .summary()) + .isNotNull(); + assertThat( + meterRegistry + .find("memind.retrieval.empty_results") + .counter()) + .isNull(); + }); + } + + @Test + @DisplayName("Back off cleanly when observability is disabled") + void backsOffWhenObservabilityDisabled() { + contextRunner + .withUserConfiguration(MicrometerProviderConfig.class) + .withPropertyValues("memind.observability.enabled=false") + .run( + context -> { + assertThat(context) + .doesNotHaveBean(MemoryMeterObservationHandler.class); + }); + } + } + + @Nested + @DisplayName("Custom Bean Override") + class CustomBeans { + + @Test + @DisplayName("Do not register default meter handler when user defines one") + void userMemoryMeterObservationHandlerTakesPrecedence() { + contextRunner + .withUserConfiguration( + MicrometerProviderConfig.class, + CustomMeterObservationHandlerConfig.class) + .run( + context -> { + assertThat(context) + .hasSingleBean(MemoryMeterObservationHandler.class); + assertThat(context.getBean(MemoryMeterObservationHandler.class)) + .isSameAs( + context.getBean( + CustomMeterObservationHandlerConfig + .class) + .handler); + }); + } + + @Test + @DisplayName("Do not register default observation registry when user defines one") + void userObservationRegistryTakesPrecedence() { + contextRunner + .withUserConfiguration(ObservationOnlyConfig.class) + .run( + context -> + assertThat( + context.getBean(ObservationOnlyConfig.class) + .registry) + .isSameAs(context.getBean(ObservationRegistry.class))); + } + } + + @Configuration + static class MicrometerProviderConfig { + + @Bean + ObservationRegistry observationRegistry() { + return ObservationRegistry.create(); + } + + @Bean + MeterRegistry meterRegistry() { + return new SimpleMeterRegistry(); + } + } + + @Configuration + static class ObservationOnlyConfig { + + private final ObservationRegistry registry = ObservationRegistry.create(); + + @Bean + ObservationRegistry observationRegistry() { + return registry; + } + } + + @Configuration + static class CustomMeterObservationHandlerConfig { + + private final MemoryMeterObservationHandler handler = + new MemoryMeterObservationHandler(new SimpleMeterRegistry()); + + @Bean + MemoryMeterObservationHandler memoryMeterObservationHandler() { + return handler; + } + } + + private static QueryContext queryContext() { + return new QueryContext( + () -> "memory-1", "query", null, List.of(), Map.of(), null, Set.of()); + } + + private static final class TestTraceContext extends MemoryObservationContext + implements RetrievalTraceEventSource { + + private TestTraceContext(RetrievalTraceRecorder recorder) { + put(RetrievalTraceRecorder.class, recorder); + } + + @Override + public Optional toRetrievalTraceEvent( + ObservationTiming timing, RetrievalTraceOptions options) { + return Optional.of( + new RetrievalTraceEvent( + "memind.test", + "memind.test", + status(), + timing.startedAt(), + timing.completedAt(), + timing.durationMillis(), + Map.of("operation", "retrieval", "stage", "tier"), + Map.of(), + new RetrievalTraceEvent.StagePayload( + "tier", "item", "vector", null, null, 1, false, false, Map.of(), + List.of()))); + } + } +} diff --git a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/src/main/java/com/openmemind/ai/memory/plugin/tracing/otel/autoconfigure/MemoryObserverAutoConfiguration.java b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/src/main/java/com/openmemind/ai/memory/plugin/tracing/otel/autoconfigure/MemoryObserverAutoConfiguration.java deleted file mode 100644 index fdbe47b5..00000000 --- a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/src/main/java/com/openmemind/ai/memory/plugin/tracing/otel/autoconfigure/MemoryObserverAutoConfiguration.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.plugin.tracing.otel.autoconfigure; - -import com.openmemind.ai.memory.core.metrics.MemoryMetricsRecorder; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.plugin.tracing.otel.OpenTelemetryMemoryMetricsRecorder; -import com.openmemind.ai.memory.plugin.tracing.otel.OpenTelemetryMemoryObserver; -import io.opentelemetry.api.metrics.Meter; -import io.opentelemetry.api.trace.Tracer; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; - -@AutoConfiguration -@ConditionalOnClass(Tracer.class) -@ConditionalOnProperty( - prefix = "memind.tracing", - name = "enabled", - havingValue = "true", - matchIfMissing = true) -public class MemoryObserverAutoConfiguration { - - @Bean - @ConditionalOnMissingBean(MemoryObserver.class) - public MemoryObserver openTelemetryMemoryObserver(Tracer tracer, Meter meter) { - return new OpenTelemetryMemoryObserver(tracer, meter); - } - - @Bean - @ConditionalOnMissingBean(MemoryMetricsRecorder.class) - public MemoryMetricsRecorder openTelemetryMemoryMetricsRecorder(Meter meter) { - return new OpenTelemetryMemoryMetricsRecorder(meter); - } - - @Bean - public static TracingBeanPostProcessor tracingBeanPostProcessor( - ObjectProvider observerProvider, - ObjectProvider metricsRecorderProvider) { - return new TracingBeanPostProcessor(observerProvider, metricsRecorderProvider); - } -} diff --git a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/src/main/java/com/openmemind/ai/memory/plugin/tracing/otel/autoconfigure/TracingBeanPostProcessor.java b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/src/main/java/com/openmemind/ai/memory/plugin/tracing/otel/autoconfigure/TracingBeanPostProcessor.java deleted file mode 100644 index fd4b510f..00000000 --- a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/src/main/java/com/openmemind/ai/memory/plugin/tracing/otel/autoconfigure/TracingBeanPostProcessor.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.plugin.tracing.otel.autoconfigure; - -import com.openmemind.ai.memory.core.extraction.MemoryExtractor; -import com.openmemind.ai.memory.core.extraction.insight.generator.InsightGenerator; -import com.openmemind.ai.memory.core.extraction.insight.group.InsightGroupClassifier; -import com.openmemind.ai.memory.core.extraction.item.dedup.MemoryItemDeduplicator; -import com.openmemind.ai.memory.core.extraction.item.graph.ItemGraphMaterializer; -import com.openmemind.ai.memory.core.extraction.step.InsightExtractStep; -import com.openmemind.ai.memory.core.extraction.step.MemoryItemExtractStep; -import com.openmemind.ai.memory.core.extraction.step.RawDataExtractStep; -import com.openmemind.ai.memory.core.llm.rerank.Reranker; -import com.openmemind.ai.memory.core.metrics.MemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.NoopMemoryMetricsRecorder; -import com.openmemind.ai.memory.core.retrieval.MemoryRetriever; -import com.openmemind.ai.memory.core.retrieval.deep.TypedQueryExpander; -import com.openmemind.ai.memory.core.retrieval.graph.RetrievalGraphAssistant; -import com.openmemind.ai.memory.core.retrieval.strategy.RetrievalStrategy; -import com.openmemind.ai.memory.core.retrieval.sufficiency.SufficiencyGate; -import com.openmemind.ai.memory.core.retrieval.thread.MemoryThreadAssistant; -import com.openmemind.ai.memory.core.retrieval.tier.InsightTypeRouter; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.NoopMemoryObserver; -import com.openmemind.ai.memory.core.tracing.decorator.TracingInsightExtractStep; -import com.openmemind.ai.memory.core.tracing.decorator.TracingInsightGenerator; -import com.openmemind.ai.memory.core.tracing.decorator.TracingInsightGroupClassifier; -import com.openmemind.ai.memory.core.tracing.decorator.TracingInsightTypeRouter; -import com.openmemind.ai.memory.core.tracing.decorator.TracingItemGraphMaterializer; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryExtractor; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryItemDeduplicator; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryItemExtractStep; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryRetriever; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryThreadAssistant; -import com.openmemind.ai.memory.core.tracing.decorator.TracingRawDataExtractStep; -import com.openmemind.ai.memory.core.tracing.decorator.TracingReranker; -import com.openmemind.ai.memory.core.tracing.decorator.TracingRetrievalGraphAssistant; -import com.openmemind.ai.memory.core.tracing.decorator.TracingRetrievalStrategy; -import com.openmemind.ai.memory.core.tracing.decorator.TracingSufficiencyGate; -import com.openmemind.ai.memory.core.tracing.decorator.TracingTypedQueryExpander; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.core.Ordered; - -/** - * Spring {@link BeanPostProcessor} that automatically wraps memind beans - * with tracing decorators when a non-noop {@link MemoryObserver} is available. - * - *

Uses pattern matching to detect each supported interface and wraps it - * exactly once (already-wrapped beans are returned as-is). - */ -public class TracingBeanPostProcessor implements BeanPostProcessor, Ordered { - - private final ObjectProvider observerProvider; - private final ObjectProvider metricsRecorderProvider; - - public TracingBeanPostProcessor(ObjectProvider observerProvider) { - this(observerProvider, null); - } - - public TracingBeanPostProcessor( - ObjectProvider observerProvider, - ObjectProvider metricsRecorderProvider) { - this.observerProvider = observerProvider; - this.metricsRecorderProvider = metricsRecorderProvider; - } - - @Override - public int getOrder() { - return Ordered.LOWEST_PRECEDENCE; - } - - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) { - if (!isTraceableBean(bean) || isAlreadyWrapped(bean)) { - return bean; - } - - MemoryObserver observer = observerProvider.getIfAvailable(); - if (observer == null) { - observer = new NoopMemoryObserver(); - } - MemoryMetricsRecorder metricsRecorder = - metricsRecorderProvider == null - ? NoopMemoryMetricsRecorder.INSTANCE - : metricsRecorderProvider.getIfAvailable( - () -> NoopMemoryMetricsRecorder.INSTANCE); - if ((observer == null || observer instanceof NoopMemoryObserver) - && metricsRecorder instanceof NoopMemoryMetricsRecorder) { - return bean; - } - - if (bean instanceof MemoryExtractor d && !(bean instanceof TracingMemoryExtractor)) { - return new TracingMemoryExtractor(d, observer, metricsRecorder); - } - if (bean instanceof RawDataExtractStep d && !(bean instanceof TracingRawDataExtractStep)) { - return new TracingRawDataExtractStep(d, observer); - } - if (bean instanceof MemoryItemExtractStep d - && !(bean instanceof TracingMemoryItemExtractStep)) { - return new TracingMemoryItemExtractStep(d, observer); - } - if (bean instanceof InsightExtractStep d && !(bean instanceof TracingInsightExtractStep)) { - return new TracingInsightExtractStep(d, observer); - } - if (bean instanceof InsightGenerator d && !(bean instanceof TracingInsightGenerator)) { - return new TracingInsightGenerator(d, observer); - } - if (bean instanceof InsightGroupClassifier d - && !(bean instanceof TracingInsightGroupClassifier)) { - return new TracingInsightGroupClassifier(d, observer); - } - if (bean instanceof MemoryItemDeduplicator d - && !(bean instanceof TracingMemoryItemDeduplicator)) { - return new TracingMemoryItemDeduplicator(d, observer); - } - if (bean instanceof ItemGraphMaterializer d - && !(bean instanceof TracingItemGraphMaterializer)) { - return new TracingItemGraphMaterializer(d, observer); - } - - if (bean instanceof MemoryRetriever d && !(bean instanceof TracingMemoryRetriever)) { - return new TracingMemoryRetriever(d, observer); - } - if (bean instanceof RetrievalGraphAssistant d - && !(bean instanceof TracingRetrievalGraphAssistant)) { - return new TracingRetrievalGraphAssistant(d, observer); - } - if (bean instanceof MemoryThreadAssistant d - && !(bean instanceof TracingMemoryThreadAssistant)) { - return new TracingMemoryThreadAssistant(d, observer); - } - if (bean instanceof RetrievalStrategy d && !(bean instanceof TracingRetrievalStrategy)) { - return new TracingRetrievalStrategy(d, observer, metricsRecorder); - } - if (bean instanceof Reranker d && !(bean instanceof TracingReranker)) { - return new TracingReranker(d, observer); - } - if (bean instanceof InsightTypeRouter d && !(bean instanceof TracingInsightTypeRouter)) { - return new TracingInsightTypeRouter(d, observer); - } - if (bean instanceof TypedQueryExpander d && !(bean instanceof TracingTypedQueryExpander)) { - return new TracingTypedQueryExpander(d, observer); - } - if (bean instanceof SufficiencyGate d && !(bean instanceof TracingSufficiencyGate)) { - return new TracingSufficiencyGate(d, observer); - } - - return bean; - } - - private boolean isTraceableBean(Object bean) { - return bean instanceof MemoryExtractor - || bean instanceof RawDataExtractStep - || bean instanceof MemoryItemExtractStep - || bean instanceof InsightExtractStep - || bean instanceof InsightGenerator - || bean instanceof InsightGroupClassifier - || bean instanceof MemoryItemDeduplicator - || bean instanceof ItemGraphMaterializer - || bean instanceof MemoryRetriever - || bean instanceof RetrievalGraphAssistant - || bean instanceof MemoryThreadAssistant - || bean instanceof RetrievalStrategy - || bean instanceof Reranker - || bean instanceof InsightTypeRouter - || bean instanceof TypedQueryExpander - || bean instanceof SufficiencyGate; - } - - private boolean isAlreadyWrapped(Object bean) { - return bean instanceof TracingMemoryExtractor - || bean instanceof TracingRawDataExtractStep - || bean instanceof TracingMemoryItemExtractStep - || bean instanceof TracingInsightExtractStep - || bean instanceof TracingInsightGenerator - || bean instanceof TracingInsightGroupClassifier - || bean instanceof TracingMemoryItemDeduplicator - || bean instanceof TracingItemGraphMaterializer - || bean instanceof TracingMemoryRetriever - || bean instanceof TracingRetrievalGraphAssistant - || bean instanceof TracingMemoryThreadAssistant - || bean instanceof TracingRetrievalStrategy - || bean instanceof TracingReranker - || bean instanceof TracingInsightTypeRouter - || bean instanceof TracingTypedQueryExpander - || bean instanceof TracingSufficiencyGate; - } -} diff --git a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 8015a4a1..00000000 --- a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1 +0,0 @@ -com.openmemind.ai.memory.plugin.tracing.otel.autoconfigure.MemoryObserverAutoConfiguration diff --git a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/src/test/java/com/openmemind/ai/memory/plugin/tracing/otel/autoconfigure/MemoryObserverAutoConfigurationTest.java b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/src/test/java/com/openmemind/ai/memory/plugin/tracing/otel/autoconfigure/MemoryObserverAutoConfigurationTest.java deleted file mode 100644 index 670e9436..00000000 --- a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/src/test/java/com/openmemind/ai/memory/plugin/tracing/otel/autoconfigure/MemoryObserverAutoConfigurationTest.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.plugin.tracing.otel.autoconfigure; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.plugin.tracing.otel.OpenTelemetryMemoryObserver; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.metrics.Meter; -import io.opentelemetry.api.trace.Tracer; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@DisplayName("MemoryObserverAutoConfiguration Test") -class MemoryObserverAutoConfigurationTest { - - private final ApplicationContextRunner contextRunner = - new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of(MemoryObserverAutoConfiguration.class)); - - @Nested - @DisplayName("Default Configuration") - class Defaults { - - @Test - @DisplayName( - "Register OpenTelemetryMemoryObserver and TracingBeanPostProcessor when OTel beans" - + " exist") - void registersObserverAndBeanPostProcessorWhenOtelBeansExist() { - contextRunner - .withUserConfiguration(OtelProviderConfig.class) - .run( - context -> { - assertThat(context).hasSingleBean(MemoryObserver.class); - assertThat(context.getBean(MemoryObserver.class)) - .isInstanceOf(OpenTelemetryMemoryObserver.class); - assertThat(context).hasSingleBean(TracingBeanPostProcessor.class); - }); - } - - @Test - @DisplayName("Back off cleanly when tracing is disabled") - void backsOffWhenTracingDisabled() { - contextRunner - .withUserConfiguration(OtelProviderConfig.class) - .withPropertyValues("memind.tracing.enabled=false") - .run( - context -> { - assertThat(context).doesNotHaveBean(MemoryObserver.class); - assertThat(context).doesNotHaveBean(TracingBeanPostProcessor.class); - }); - } - } - - @Nested - @DisplayName("Custom Bean Override") - class CustomBeans { - - @Test - @DisplayName("Do not register default observer when user defines MemoryObserver") - void userMemoryObserverTakesPrecedence() { - contextRunner - .withUserConfiguration(OtelProviderConfig.class, CustomObserverConfig.class) - .run( - context -> { - assertThat(context).hasSingleBean(MemoryObserver.class); - assertThat(context.getBean(MemoryObserver.class)) - .isSameAs( - context.getBean(CustomObserverConfig.class) - .memoryObserver); - assertThat(context).hasSingleBean(TracingBeanPostProcessor.class); - }); - } - } - - @Configuration - static class OtelProviderConfig { - - private static final OpenTelemetry OPEN_TELEMETRY = OpenTelemetry.noop(); - - @Bean - Tracer tracer() { - return OPEN_TELEMETRY.getTracer("test"); - } - - @Bean - Meter meter() { - return OPEN_TELEMETRY.getMeter("test"); - } - } - - @Configuration - static class CustomObserverConfig { - - private final MemoryObserver memoryObserver = new TestMemoryObserver(); - - @Bean - MemoryObserver memoryObserver() { - return memoryObserver; - } - } - - private static final class TestMemoryObserver implements MemoryObserver { - - @Override - public reactor.core.publisher.Mono observeMono( - com.openmemind.ai.memory.core.tracing.ObservationContext ctx, - java.util.function.Supplier> operation) { - return operation.get(); - } - - @Override - public reactor.core.publisher.Flux observeFlux( - com.openmemind.ai.memory.core.tracing.ObservationContext ctx, - java.util.function.Supplier> operation) { - return operation.get(); - } - } -} diff --git a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/src/test/java/com/openmemind/ai/memory/plugin/tracing/otel/autoconfigure/TracingBeanPostProcessorTest.java b/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/src/test/java/com/openmemind/ai/memory/plugin/tracing/otel/autoconfigure/TracingBeanPostProcessorTest.java deleted file mode 100644 index 13a1ab76..00000000 --- a/memind-plugins/memind-plugin-spring-boot-starters/memind-plugin-tracing-opentelemetry-starter/src/test/java/com/openmemind/ai/memory/plugin/tracing/otel/autoconfigure/TracingBeanPostProcessorTest.java +++ /dev/null @@ -1,294 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.plugin.tracing.otel.autoconfigure; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.openmemind.ai.memory.core.extraction.MemoryExtractor; -import com.openmemind.ai.memory.core.extraction.insight.generator.InsightGenerator; -import com.openmemind.ai.memory.core.extraction.insight.group.InsightGroupClassifier; -import com.openmemind.ai.memory.core.extraction.item.dedup.MemoryItemDeduplicator; -import com.openmemind.ai.memory.core.extraction.item.graph.ItemGraphMaterializer; -import com.openmemind.ai.memory.core.extraction.step.InsightExtractStep; -import com.openmemind.ai.memory.core.extraction.step.MemoryItemExtractStep; -import com.openmemind.ai.memory.core.extraction.step.RawDataExtractStep; -import com.openmemind.ai.memory.core.llm.rerank.Reranker; -import com.openmemind.ai.memory.core.retrieval.MemoryRetriever; -import com.openmemind.ai.memory.core.retrieval.deep.TypedQueryExpander; -import com.openmemind.ai.memory.core.retrieval.graph.RetrievalGraphAssistant; -import com.openmemind.ai.memory.core.retrieval.strategy.RetrievalStrategy; -import com.openmemind.ai.memory.core.retrieval.sufficiency.SufficiencyGate; -import com.openmemind.ai.memory.core.retrieval.thread.MemoryThreadAssistant; -import com.openmemind.ai.memory.core.retrieval.tier.InsightTypeRouter; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.NoopMemoryObserver; -import com.openmemind.ai.memory.core.tracing.decorator.TracingInsightExtractStep; -import com.openmemind.ai.memory.core.tracing.decorator.TracingInsightGenerator; -import com.openmemind.ai.memory.core.tracing.decorator.TracingInsightGroupClassifier; -import com.openmemind.ai.memory.core.tracing.decorator.TracingInsightTypeRouter; -import com.openmemind.ai.memory.core.tracing.decorator.TracingItemGraphMaterializer; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryExtractor; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryItemDeduplicator; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryItemExtractStep; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryRetriever; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryThreadAssistant; -import com.openmemind.ai.memory.core.tracing.decorator.TracingRawDataExtractStep; -import com.openmemind.ai.memory.core.tracing.decorator.TracingReranker; -import com.openmemind.ai.memory.core.tracing.decorator.TracingRetrievalGraphAssistant; -import com.openmemind.ai.memory.core.tracing.decorator.TracingRetrievalStrategy; -import com.openmemind.ai.memory.core.tracing.decorator.TracingSufficiencyGate; -import com.openmemind.ai.memory.core.tracing.decorator.TracingTypedQueryExpander; -import java.lang.reflect.Proxy; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; - -@DisplayName("TracingBeanPostProcessor") -class TracingBeanPostProcessorTest { - - private TracingBeanPostProcessor createProcessor(MemoryObserver observer) { - DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); - factory.registerSingleton("memoryObserver", observer); - ObjectProvider provider = factory.getBeanProvider(MemoryObserver.class); - return new TracingBeanPostProcessor(provider); - } - - private TracingBeanPostProcessor createProcessor( - ObjectProvider observerProvider) { - return new TracingBeanPostProcessor(observerProvider); - } - - @SuppressWarnings("unchecked") - private T proxy(Class type) { - return (T) - Proxy.newProxyInstance( - type.getClassLoader(), - new Class[] {type}, - (proxy, method, args) -> - switch (method.getName()) { - case "toString" -> type.getSimpleName() + "Proxy"; - case "hashCode" -> System.identityHashCode(proxy); - case "equals" -> proxy == args[0]; - default -> null; - }); - } - - @Nested - @DisplayName("With real MemoryObserver") - class WithRealObserver { - - private final TracingBeanPostProcessor processor = - createProcessor(new TestMemoryObserver()); - - @Test - @DisplayName("wraps MemoryExtractor") - void wrapsExtractionPipeline() { - var bean = proxy(MemoryExtractor.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isInstanceOf(TracingMemoryExtractor.class); - } - - @Test - @DisplayName("wraps RawDataExtractStep") - void wrapsRawDataExtractStep() { - var bean = proxy(RawDataExtractStep.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isInstanceOf(TracingRawDataExtractStep.class); - } - - @Test - @DisplayName("wraps MemoryItemExtractStep") - void wrapsMemoryItemExtractStep() { - var bean = proxy(MemoryItemExtractStep.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isInstanceOf(TracingMemoryItemExtractStep.class); - } - - @Test - @DisplayName("wraps InsightExtractStep") - void wrapsInsightExtractStep() { - var bean = proxy(InsightExtractStep.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isInstanceOf(TracingInsightExtractStep.class); - } - - @Test - @DisplayName("wraps InsightGenerator") - void wrapsInsightGenerator() { - var bean = proxy(InsightGenerator.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isInstanceOf(TracingInsightGenerator.class); - } - - @Test - @DisplayName("wraps InsightGroupClassifier") - void wrapsInsightGroupClassifier() { - var bean = proxy(InsightGroupClassifier.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isInstanceOf(TracingInsightGroupClassifier.class); - } - - @Test - @DisplayName("wraps MemoryItemDeduplicator") - void wrapsMemoryItemDeduplicator() { - var bean = proxy(MemoryItemDeduplicator.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isInstanceOf(TracingMemoryItemDeduplicator.class); - } - - @Test - @DisplayName("wraps ItemGraphMaterializer") - void wrapsItemGraphMaterializer() { - var bean = proxy(ItemGraphMaterializer.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isInstanceOf(TracingItemGraphMaterializer.class); - } - - @Test - @DisplayName("wraps MemoryRetriever") - void wrapsMemoryRetriever() { - var bean = proxy(MemoryRetriever.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isInstanceOf(TracingMemoryRetriever.class); - } - - @Test - @DisplayName("wraps RetrievalGraphAssistant") - void wrapsRetrievalGraphAssistant() { - var bean = proxy(RetrievalGraphAssistant.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isInstanceOf(TracingRetrievalGraphAssistant.class); - } - - @Test - @DisplayName("wraps MemoryThreadAssistant") - void wrapsMemoryThreadAssistant() { - var bean = proxy(MemoryThreadAssistant.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isInstanceOf(TracingMemoryThreadAssistant.class); - } - - @Test - @DisplayName("wraps RetrievalStrategy") - void wrapsRetrievalStrategy() { - var bean = proxy(RetrievalStrategy.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isInstanceOf(TracingRetrievalStrategy.class); - } - - @Test - @DisplayName("wraps Reranker") - void wrapsReranker() { - var bean = proxy(Reranker.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isInstanceOf(TracingReranker.class); - } - - @Test - @DisplayName("wraps InsightTypeRouter") - void wrapsInsightTypeRouter() { - var bean = proxy(InsightTypeRouter.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isInstanceOf(TracingInsightTypeRouter.class); - } - - @Test - @DisplayName("wraps TypedQueryExpander") - void wrapsTypedQueryExpander() { - var bean = proxy(TypedQueryExpander.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isInstanceOf(TracingTypedQueryExpander.class); - } - - @Test - @DisplayName("wraps SufficiencyGate") - void wrapsSufficiencyGate() { - var bean = proxy(SufficiencyGate.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isInstanceOf(TracingSufficiencyGate.class); - } - - @Test - @DisplayName("does not double-wrap already wrapped bean") - void doesNotDoubleWrap() { - var delegate = proxy(MemoryExtractor.class); - var alreadyWrapped = processor.postProcessAfterInitialization(delegate, "first"); - var result = processor.postProcessAfterInitialization(alreadyWrapped, "second"); - assertThat(result).isSameAs(alreadyWrapped); - } - - @Test - @DisplayName("returns unrelated beans unchanged") - void returnsUnrelatedBeansUnchanged() { - var bean = new Object(); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isSameAs(bean); - } - - @Test - @DisplayName("does not resolve observer for unrelated beans") - void doesNotResolveObserverForUnrelatedBeans() { - ObjectProvider provider = - new ObjectProvider<>() { - @Override - public MemoryObserver getIfAvailable() { - throw new AssertionError( - "observerProvider should not be accessed for unrelated beans"); - } - }; - - var unrelatedBean = new Object(); - var result = - createProcessor(provider) - .postProcessAfterInitialization(unrelatedBean, "unrelatedBean"); - - assertThat(result).isSameAs(unrelatedBean); - } - } - - @Nested - @DisplayName("With NoopMemoryObserver") - class WithNoopObserver { - - private final TracingBeanPostProcessor processor = - createProcessor(new NoopMemoryObserver()); - - @Test - @DisplayName("does not wrap beans when observer is noop") - void doesNotWrapWithNoop() { - var bean = proxy(InsightGenerator.class); - var result = processor.postProcessAfterInitialization(bean, "test"); - assertThat(result).isSameAs(bean); - } - } - - private static final class TestMemoryObserver implements MemoryObserver { - - @Override - public reactor.core.publisher.Mono observeMono( - com.openmemind.ai.memory.core.tracing.ObservationContext ctx, - java.util.function.Supplier> operation) { - return operation.get(); - } - - @Override - public reactor.core.publisher.Flux observeFlux( - com.openmemind.ai.memory.core.tracing.ObservationContext ctx, - java.util.function.Supplier> operation) { - return operation.get(); - } - } -} diff --git a/memind-plugins/memind-plugin-spring-boot-starters/pom.xml b/memind-plugins/memind-plugin-spring-boot-starters/pom.xml index 0d6721e7..1d56bd3e 100644 --- a/memind-plugins/memind-plugin-spring-boot-starters/pom.xml +++ b/memind-plugins/memind-plugin-spring-boot-starters/pom.xml @@ -40,6 +40,6 @@ memind-plugin-rawdata-image-starter memind-plugin-rawdata-jackson-starter memind-plugin-rawdata-toolcall-starter - memind-plugin-tracing-opentelemetry-starter + memind-plugin-observability-micrometer-starter diff --git a/memind-plugins/memind-plugin-tracing-opentelemetry/src/main/java/com/openmemind/ai/memory/plugin/tracing/otel/OpenTelemetryMemoryMetricsRecorder.java b/memind-plugins/memind-plugin-tracing-opentelemetry/src/main/java/com/openmemind/ai/memory/plugin/tracing/otel/OpenTelemetryMemoryMetricsRecorder.java deleted file mode 100644 index dd800ee5..00000000 --- a/memind-plugins/memind-plugin-tracing-opentelemetry/src/main/java/com/openmemind/ai/memory/plugin/tracing/otel/OpenTelemetryMemoryMetricsRecorder.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.plugin.tracing.otel; - -import com.openmemind.ai.memory.core.metrics.ExtractionMetrics; -import com.openmemind.ai.memory.core.metrics.MemoryMetricsRecorder; -import com.openmemind.ai.memory.core.metrics.RetrievalMergeMetrics; -import com.openmemind.ai.memory.core.metrics.RetrievalResultType; -import com.openmemind.ai.memory.core.metrics.RetrievalStageMetrics; -import com.openmemind.ai.memory.core.metrics.RetrievalSummaryMetrics; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.metrics.DoubleHistogram; -import io.opentelemetry.api.metrics.LongCounter; -import io.opentelemetry.api.metrics.Meter; -import java.util.Set; - -public final class OpenTelemetryMemoryMetricsRecorder implements MemoryMetricsRecorder { - - private static final Set STRATEGIES = Set.of("simple", "deep", "unknown"); - private static final Set STAGES = Set.of("tier", "channel", "merge", "rerank", "final"); - private static final Set TIERS = Set.of("item", "insight", "raw_data", "none"); - private static final Set METHODS = - Set.of( - "vector", - "keyword", - "hybrid", - "graph", - "temporal", - "rrf", - "rerank", - "final", - "none"); - private static final Set STATUSES = - Set.of( - "success", - "error", - "degraded", - "skipped", - "empty", - "failed", - "partial_success", - "unknown"); - private static final Set SOURCES = Set.of("api", "core", "internal"); - - private final DoubleHistogram extractionRawData; - private final DoubleHistogram extractionSegments; - private final DoubleHistogram extractionItems; - private final DoubleHistogram extractionItemsNew; - private final DoubleHistogram extractionItemsReinforced; - private final DoubleHistogram extractionInsights; - private final DoubleHistogram extractionGraphEntities; - private final DoubleHistogram extractionGraphMentions; - private final DoubleHistogram extractionGraphRelations; - private final DoubleHistogram retrievalCandidates; - private final DoubleHistogram retrievalResults; - private final DoubleHistogram retrievalMergeInputs; - private final DoubleHistogram retrievalMergeOutputs; - private final DoubleHistogram retrievalMergeDeduplicated; - private final LongCounter retrievalStageDegraded; - private final LongCounter retrievalStageSkipped; - private final LongCounter retrievalEmptyResults; - - public OpenTelemetryMemoryMetricsRecorder(Meter meter) { - this.extractionRawData = histogram(meter, "memind.extraction.raw_data", "{raw_data}"); - this.extractionSegments = histogram(meter, "memind.extraction.segments", "{segment}"); - this.extractionItems = histogram(meter, "memind.extraction.items", "{item}"); - this.extractionItemsNew = histogram(meter, "memind.extraction.items.new", "{item}"); - this.extractionItemsReinforced = - histogram(meter, "memind.extraction.items.reinforced", "{item}"); - this.extractionInsights = histogram(meter, "memind.extraction.insights", "{insight}"); - this.extractionGraphEntities = - histogram(meter, "memind.extraction.graph.entities", "{entity}"); - this.extractionGraphMentions = - histogram(meter, "memind.extraction.graph.mentions", "{mention}"); - this.extractionGraphRelations = - histogram(meter, "memind.extraction.graph.relations", "{relation}"); - this.retrievalCandidates = histogram(meter, "memind.retrieval.candidates", "{candidate}"); - this.retrievalResults = histogram(meter, "memind.retrieval.results", "{result}"); - this.retrievalMergeInputs = - histogram(meter, "memind.retrieval.merge.inputs", "{candidate}"); - this.retrievalMergeOutputs = - histogram(meter, "memind.retrieval.merge.outputs", "{candidate}"); - this.retrievalMergeDeduplicated = - histogram(meter, "memind.retrieval.merge.deduplicated", "{candidate}"); - this.retrievalStageDegraded = counter(meter, "memind.retrieval.stage.degraded"); - this.retrievalStageSkipped = counter(meter, "memind.retrieval.stage.skipped"); - this.retrievalEmptyResults = counter(meter, "memind.retrieval.empty_results"); - } - - @Override - public void recordExtractionSummary(ExtractionMetrics metrics) { - if (metrics == null) { - return; - } - Attributes attrs = extractionAttrs(metrics.status(), metrics.source()); - extractionRawData.record(metrics.rawDataCount(), attrs); - recordNullable(extractionSegments, metrics.segmentCount(), attrs); - extractionItems.record(metrics.newItemCount(), attrs); - extractionItemsNew.record(metrics.newItemCount(), attrs); - recordNullable(extractionItemsReinforced, metrics.reinforcedItemCount(), attrs); - extractionInsights.record(metrics.insightCount(), attrs); - recordNullable(extractionGraphEntities, metrics.graphEntityCount(), attrs); - recordNullable(extractionGraphMentions, metrics.graphMentionCount(), attrs); - recordNullable(extractionGraphRelations, metrics.graphRelationCount(), attrs); - } - - @Override - public void recordRetrievalStage(RetrievalStageMetrics metrics) { - if (metrics == null) { - return; - } - Attributes attrs = - retrievalAttrs( - metrics.strategy(), - metrics.stage(), - metrics.tier(), - metrics.method(), - metrics.status(), - metrics.source()); - recordNullable(retrievalCandidates, metrics.candidateCount(), attrs); - recordNullable(retrievalResults, metrics.resultCount(), attrs); - if (metrics.degraded()) { - retrievalStageDegraded.add(1, attrs); - } - if (metrics.skipped()) { - retrievalStageSkipped.add(1, attrs); - } - } - - @Override - public void recordRetrievalMerge(RetrievalMergeMetrics metrics) { - if (metrics == null) { - return; - } - Attributes attrs = - retrievalAttrs( - metrics.strategy(), - "merge", - "none", - "rrf", - metrics.status(), - metrics.source()); - retrievalMergeInputs.record(metrics.inputCount(), attrs); - retrievalMergeOutputs.record(metrics.outputCount(), attrs); - retrievalMergeDeduplicated.record(metrics.deduplicatedCount(), attrs); - } - - @Override - public void recordRetrievalSummary(RetrievalSummaryMetrics metrics) { - if (metrics == null) { - return; - } - Attributes attrs = - retrievalAttrs( - metrics.strategy(), - "final", - "none", - "final", - metrics.status(), - metrics.source()); - recordFinalResult(metrics, RetrievalResultType.ITEM, attrs); - recordFinalResult(metrics, RetrievalResultType.INSIGHT, attrs); - recordFinalResult(metrics, RetrievalResultType.RAW_DATA, attrs); - recordFinalResult(metrics, RetrievalResultType.EVIDENCE, attrs); - if (metrics.itemCount() == 0 - && metrics.insightCount() == 0 - && metrics.rawDataCount() == 0) { - retrievalEmptyResults.add(1, attrs); - } - } - - private void recordFinalResult( - RetrievalSummaryMetrics metrics, RetrievalResultType type, Attributes baseAttrs) { - retrievalResults.record( - metrics.countFor(type), - baseAttrs.toBuilder() - .put(AttributeKey.stringKey("result_type"), resultType(type)) - .build()); - } - - private static DoubleHistogram histogram(Meter meter, String name, String unit) { - return meter.histogramBuilder(name).setUnit(unit).build(); - } - - private static LongCounter counter(Meter meter, String name) { - return meter.counterBuilder(name).setUnit("{event}").build(); - } - - private static void recordNullable(DoubleHistogram histogram, Integer value, Attributes attrs) { - if (value != null) { - histogram.record(value, attrs); - } - } - - private static Attributes extractionAttrs(String status, String source) { - return Attributes.of( - AttributeKey.stringKey("operation"), - "extraction", - AttributeKey.stringKey("status"), - normalize(status, STATUSES, "unknown"), - AttributeKey.stringKey("source"), - normalize(source, SOURCES, "core")); - } - - private static Attributes retrievalAttrs( - String strategy, - String stage, - String tier, - String method, - String status, - String source) { - return Attributes.builder() - .put(AttributeKey.stringKey("operation"), "retrieval") - .put(AttributeKey.stringKey("strategy"), normalize(strategy, STRATEGIES, "unknown")) - .put(AttributeKey.stringKey("stage"), normalize(stage, STAGES, "final")) - .put(AttributeKey.stringKey("tier"), normalize(tier, TIERS, "none")) - .put(AttributeKey.stringKey("method"), normalize(method, METHODS, "none")) - .put(AttributeKey.stringKey("status"), normalize(status, STATUSES, "unknown")) - .put(AttributeKey.stringKey("source"), normalize(source, SOURCES, "core")) - .build(); - } - - private static String resultType(RetrievalResultType type) { - return switch (type) { - case ITEM -> "item"; - case INSIGHT -> "insight"; - case RAW_DATA -> "raw_data"; - case EVIDENCE -> "evidence"; - }; - } - - private static String normalize(String value, Set allowed, String fallback) { - if (value == null || value.isBlank()) { - return fallback; - } - String normalized = value.toLowerCase(); - return allowed.contains(normalized) ? normalized : fallback; - } -} diff --git a/memind-plugins/memind-plugin-tracing-opentelemetry/src/main/java/com/openmemind/ai/memory/plugin/tracing/otel/OpenTelemetryMemoryObserver.java b/memind-plugins/memind-plugin-tracing-opentelemetry/src/main/java/com/openmemind/ai/memory/plugin/tracing/otel/OpenTelemetryMemoryObserver.java deleted file mode 100644 index 481bff7f..00000000 --- a/memind-plugins/memind-plugin-tracing-opentelemetry/src/main/java/com/openmemind/ai/memory/plugin/tracing/otel/OpenTelemetryMemoryObserver.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.plugin.tracing.otel; - -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.ObservationContext; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.metrics.DoubleHistogram; -import io.opentelemetry.api.metrics.LongCounter; -import io.opentelemetry.api.metrics.Meter; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.api.trace.StatusCode; -import io.opentelemetry.api.trace.Tracer; -import io.opentelemetry.context.Context; -import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator; -import java.util.Map; -import java.util.function.Supplier; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -/** - * OpenTelemetry implementation of {@link MemoryObserver}, providing dual observation of Span + Metrics. - * - *

Each operation generates an INTERNAL span, containing request attributes and (in the case of Mono) result attributes backfill. - * At the same time, it produces two metrics: {@code memind.operation.errors} (Counter) and - * {@code memind.operation.duration} (Histogram), distinguished by the {@code operation} label. - * - *

Context propagation is completed through {@link ContextPropagationOperator} in the Reactor Context, - * nested observeMono/observeFlux calls automatically generate the correct parent-child relationship. - * - *

Note: The Flux scenario does not support result attribute backfill ({@code resultExtractor} is ignored), - * because Flux emits elements one by one, making it impossible to obtain the complete result set in a single callback. - * For Flux result statistics, it is recommended to wrap with {@code observeMono} after {@code collectList()} at the business layer. - */ -public class OpenTelemetryMemoryObserver implements MemoryObserver { - - private final Tracer tracer; - private final LongCounter errorCounter; - private final DoubleHistogram durationHistogram; - - public OpenTelemetryMemoryObserver(Tracer tracer, Meter meter) { - this.tracer = tracer; - this.errorCounter = - meter.counterBuilder("memind.operation.errors") - .setDescription("Error count per operation") - .build(); - this.durationHistogram = - meter.histogramBuilder("memind.operation.duration") - .setDescription("Operation duration") - .setUnit("ms") - .build(); - } - - @Override - public Mono observeMono(ObservationContext ctx, Supplier> operation) { - return Mono.deferContextual( - ctxView -> { - long startNano = System.nanoTime(); - - Context parentOtelCtx = - ContextPropagationOperator.getOpenTelemetryContextFromContextView( - ctxView, Context.current()); - - Span span = - tracer.spanBuilder(ctx.spanName()) - .setParent(parentOtelCtx) - .setSpanKind(SpanKind.INTERNAL) - .setAllAttributes(toOtelAttributes(ctx.requestAttributes())) - .startSpan(); - Context newOtelCtx = span.storeInContext(parentOtelCtx); - - Attributes metricAttrs = - Attributes.of(AttributeKey.stringKey("operation"), ctx.spanName()); - - return operation - .get() - .doOnSuccess( - result -> { - if (result != null) { - Map resultAttrs = - ctx.resultExtractor().extract(result); - span.setAllAttributes(toOtelAttributes(resultAttrs)); - } - }) - .doOnError( - error -> { - span.setStatus(StatusCode.ERROR, error.getMessage()); - span.recordException(error); - errorCounter.add(1, metricAttrs); - }) - .doFinally( - signal -> { - span.end(); - double durationMs = - (System.nanoTime() - startNano) / 1_000_000.0; - durationHistogram.record(durationMs, metricAttrs); - }) - .contextWrite( - reactorCtx -> - ContextPropagationOperator.storeOpenTelemetryContext( - reactorCtx, newOtelCtx)); - }); - } - - @Override - public Flux observeFlux(ObservationContext ctx, Supplier> operation) { - return Flux.deferContextual( - ctxView -> { - long startNano = System.nanoTime(); - - Context parentOtelCtx = - ContextPropagationOperator.getOpenTelemetryContextFromContextView( - ctxView, Context.current()); - - Span span = - tracer.spanBuilder(ctx.spanName()) - .setParent(parentOtelCtx) - .setSpanKind(SpanKind.INTERNAL) - .setAllAttributes(toOtelAttributes(ctx.requestAttributes())) - .startSpan(); - Context newOtelCtx = span.storeInContext(parentOtelCtx); - - Attributes metricAttrs = - Attributes.of(AttributeKey.stringKey("operation"), ctx.spanName()); - - return operation - .get() - .doOnError( - error -> { - span.setStatus(StatusCode.ERROR, error.getMessage()); - span.recordException(error); - errorCounter.add(1, metricAttrs); - }) - .doFinally( - signal -> { - span.end(); - double durationMs = - (System.nanoTime() - startNano) / 1_000_000.0; - durationHistogram.record(durationMs, metricAttrs); - }) - .contextWrite( - reactorCtx -> - ContextPropagationOperator.storeOpenTelemetryContext( - reactorCtx, newOtelCtx)); - }); - } - - private static Attributes toOtelAttributes(Map attrs) { - var builder = Attributes.builder(); - attrs.forEach( - (key, value) -> { - switch (value) { - case String s -> builder.put(AttributeKey.stringKey(key), s); - case Long l -> builder.put(AttributeKey.longKey(key), l); - case Integer i -> builder.put(AttributeKey.longKey(key), i.longValue()); - case Double d -> builder.put(AttributeKey.doubleKey(key), d); - case Boolean b -> builder.put(AttributeKey.booleanKey(key), b); - default -> builder.put(AttributeKey.stringKey(key), value.toString()); - } - }); - return builder.build(); - } -} diff --git a/memind-plugins/memind-plugin-tracing-opentelemetry/src/test/java/com/openmemind/ai/memory/plugin/tracing/otel/InsightPipelineTracingTest.java b/memind-plugins/memind-plugin-tracing-opentelemetry/src/test/java/com/openmemind/ai/memory/plugin/tracing/otel/InsightPipelineTracingTest.java deleted file mode 100644 index af0edea3..00000000 --- a/memind-plugins/memind-plugin-tracing-opentelemetry/src/test/java/com/openmemind/ai/memory/plugin/tracing/otel/InsightPipelineTracingTest.java +++ /dev/null @@ -1,445 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.plugin.tracing.otel; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.openmemind.ai.memory.core.buffer.BufferEntry; -import com.openmemind.ai.memory.core.buffer.InsightBuffer; -import com.openmemind.ai.memory.core.buffer.InsightBuffer.UngroupedContext; -import com.openmemind.ai.memory.core.data.InsightPoint; -import com.openmemind.ai.memory.core.data.MemoryId; -import com.openmemind.ai.memory.core.data.MemoryInsight; -import com.openmemind.ai.memory.core.data.MemoryInsightType; -import com.openmemind.ai.memory.core.data.MemoryItem; -import com.openmemind.ai.memory.core.data.enums.InsightAnalysisMode; -import com.openmemind.ai.memory.core.data.enums.MemoryScope; -import com.openmemind.ai.memory.core.extraction.insight.generator.InsightGenerator; -import com.openmemind.ai.memory.core.extraction.insight.generator.InsightPointGenerateResponse; -import com.openmemind.ai.memory.core.extraction.insight.generator.InsightPointOpsResponse; -import com.openmemind.ai.memory.core.extraction.insight.group.InsightGroupClassifier; -import com.openmemind.ai.memory.core.extraction.insight.group.InsightGroupRouter; -import com.openmemind.ai.memory.core.extraction.insight.scheduler.InsightBuildConfig; -import com.openmemind.ai.memory.core.extraction.insight.scheduler.InsightBuildScheduler; -import com.openmemind.ai.memory.core.extraction.insight.tree.BubbleTrackerStore; -import com.openmemind.ai.memory.core.extraction.insight.tree.InsightTreeConfig; -import com.openmemind.ai.memory.core.extraction.insight.tree.InsightTreeReorganizer; -import com.openmemind.ai.memory.core.store.MemoryStore; -import com.openmemind.ai.memory.core.tracing.MemorySpanNames; -import com.openmemind.ai.memory.core.tracing.decorator.TracingInsightGenerator; -import com.openmemind.ai.memory.core.tracing.decorator.TracingInsightGroupClassifier; -import com.openmemind.ai.memory.core.utils.IdUtils; -import com.openmemind.ai.memory.core.vector.MemoryVector; -import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator; -import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; -import java.lang.reflect.Proxy; -import java.time.Instant; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; - -@DisplayName("InsightPipelineTracingTest") -class InsightPipelineTracingTest { - - private static InMemorySpanExporter spanExporter; - private static OpenTelemetryMemoryObserver observer; - - @BeforeAll - static void setUpOtel() { - ContextPropagationOperator.builder().build().registerOnEachOperator(); - spanExporter = InMemorySpanExporter.create(); - var tracerProvider = - SdkTracerProvider.builder() - .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) - .build(); - var tracer = tracerProvider.get("memind-test"); - var meter = SdkMeterProvider.builder().build().get("memind-test"); - observer = new OpenTelemetryMemoryObserver(tracer, meter); - } - - @BeforeEach - void resetExporter() { - spanExporter.reset(); - } - - @FunctionalInterface - private interface MethodHandler { - Object handle(String methodName, Object[] args); - } - - @SuppressWarnings("unchecked") - private static T proxy(Class type, MethodHandler handler) { - return (T) - Proxy.newProxyInstance( - type.getClassLoader(), - new Class[] {type}, - (proxy, method, args) -> { - var actualArgs = args != null ? args : new Object[0]; - return switch (method.getName()) { - case "toString" -> type.getSimpleName() + "Proxy"; - case "hashCode" -> System.identityHashCode(proxy); - case "equals" -> proxy == actualArgs[0]; - default -> handler.handle(method.getName(), actualArgs); - }; - }); - } - - private static Object unsupported(String typeName, String methodName) { - throw new UnsupportedOperationException( - "Unexpected " + typeName + " method: " + methodName); - } - - private Optional findSpan(String name) { - return spanExporter.getFinishedSpanItems().stream() - .filter(s -> s.getName().equals(name)) - .findFirst(); - } - - private static MemoryId testMemoryId() { - return () -> "test-memory-1"; - } - - private static MemoryInsightType testInsightType(String name) { - return new MemoryInsightType( - 1L, - name, - "Test insight type", - null, - List.of(), - 500, - Instant.now(), - Instant.now(), - Instant.now(), - InsightAnalysisMode.BRANCH, - null, - MemoryScope.USER); - } - - private static MemoryItem testItem(long id) { - return new MemoryItem( - id, - "test-memory-1", - "test content " + id, - MemoryScope.USER, - null, - "conversation", - null, - null, - null, - Instant.now(), - Instant.now(), - null, - Instant.now(), - null); - } - - private InsightBuildScheduler buildScheduler( - InsightBuffer bufferStore, - MemoryStore store, - InsightGenerator generator, - InsightGroupClassifier classifier, - InsightTreeReorganizer treeReorganizer) { - var tracingClassifier = new TracingInsightGroupClassifier(classifier, observer); - var tracingGenerator = new TracingInsightGenerator(generator, observer); - var tracingRouter = new InsightGroupRouter(tracingClassifier); - var config = new InsightBuildConfig(1, 1, 4, 2); - var idGen = IdUtils.snowflake(); - return new InsightBuildScheduler( - bufferStore, - store, - tracingGenerator, - tracingClassifier, - tracingRouter, - treeReorganizer, - null, - idGen, - config, - observer); - } - - private InsightBuffer createBufferStore(BufferEntry ungroupedEntry, BufferEntry groupedEntry) { - var ungroupedContext = new UngroupedContext(List.of(ungroupedEntry), Set.of()); - var unbuiltByGroup = Map.of("general", List.of(groupedEntry)); - return proxy( - InsightBuffer.class, - (methodName, args) -> - switch (methodName) { - case "hasWork" -> true; - case "getUngroupedContext" -> ungroupedContext; - case "getUnbuiltByGroup" -> unbuiltByGroup; - case "assignGroup", "markBuilt", "append" -> null; - case "getUnGrouped" -> ungroupedContext.ungroupedEntries(); - case "countUnGrouped" -> ungroupedContext.ungroupedEntries().size(); - case "listGroups" -> Set.of("general"); - case "countGroupUnbuilt" -> groupedEntry == null ? 0 : 1; - case "getGroupUnbuilt" -> List.of(groupedEntry); - default -> unsupported("InsightBuffer", methodName); - }); - } - - private MemoryStore createStore(MemoryInsightType insightType, MemoryItem item) { - Map leafsByGroup = new HashMap<>(); - - MethodHandler insightHandler = - (methodName, args) -> - switch (methodName) { - case "getInsightType" -> Optional.of(insightType); - case "getLeafByGroup" -> - Optional.ofNullable(leafsByGroup.get((String) args[2])); - case "upsertInsights" -> { - @SuppressWarnings("unchecked") - List insights = (List) args[1]; - insights.forEach( - insight -> leafsByGroup.put(insight.group(), insight)); - yield null; - } - default -> unsupported("InsightOperations", methodName); - }; - - MethodHandler itemHandler = - (methodName, args) -> - switch (methodName) { - case "getItemsByIds" -> List.of(item); - default -> unsupported("ItemOperations", methodName); - }; - - var insightOps = - proxy( - com.openmemind.ai.memory.core.store.insight.InsightOperations.class, - insightHandler); - var itemOps = - proxy(com.openmemind.ai.memory.core.store.item.ItemOperations.class, itemHandler); - - return proxy( - MemoryStore.class, - (methodName, args) -> - switch (methodName) { - case "insightOperations" -> insightOps; - case "itemOperations" -> itemOps; - default -> unsupported("MemoryStore", methodName); - }); - } - - private InsightGroupClassifier createClassifier(MemoryItem item) { - return (insightType, items, existingGroupNames) -> - Mono.just(Map.of("general", List.of(item))); - } - - private InsightGenerator createGenerator(long itemId) { - var point = - new InsightPoint( - InsightPoint.PointType.SUMMARY, - "summary text", - List.of(String.valueOf(itemId))); - var response = new InsightPointGenerateResponse(List.of(point)); - return new InsightGenerator() { - @Override - public Mono generateLeafPointOps( - MemoryInsightType insightType, - String groupName, - List existingPoints, - List newItems, - int targetTokens, - String additionalContext, - String language) { - return Mono.empty(); - } - - @Override - public Mono generatePoints( - MemoryInsightType insightType, - String groupName, - List existingPoints, - List newItems, - int targetTokens, - String additionalContext, - String language) { - return Mono.just(response); - } - - @Override - public Mono generateBranchSummary( - MemoryInsightType insightType, - List existingPoints, - List leafInsights, - int targetTokens, - String language) { - return Mono.empty(); - } - - @Override - public Mono generateRootSynthesis( - MemoryInsightType rootInsightType, - List existingPoints, - List branchInsights, - int targetTokens, - String language) { - return Mono.empty(); - } - }; - } - - @Nested - @DisplayName("PipelineWithoutTree") - class PipelineWithoutTree { - - @Test - @DisplayName("flushSync produces pipeline, classify, and generate.leaf spans") - void pipelineSpanHierarchy() { - var memoryId = testMemoryId(); - var insightTypeName = "profile"; - var insightType = testInsightType(insightTypeName); - var item = testItem(101L); - - var bufferStore = - createBufferStore( - BufferEntry.ungrouped(101L), new BufferEntry(101L, "general", false)); - var store = createStore(insightType, item); - var classifier = createClassifier(item); - var generator = createGenerator(101L); - - var scheduler = buildScheduler(bufferStore, store, generator, classifier, null); - - scheduler.flushSync(memoryId, insightTypeName); - - var pipelineSpan = findSpan(MemorySpanNames.EXTRACTION_INSIGHT_PIPELINE); - assertThat(pipelineSpan).isPresent(); - - var classifySpan = findSpan(MemorySpanNames.EXTRACTION_INSIGHT_GROUP_CLASSIFY); - assertThat(classifySpan).isPresent(); - - var leafSpan = findSpan(MemorySpanNames.EXTRACTION_INSIGHT_GENERATE_LEAF); - assertThat(leafSpan).isPresent(); - } - } - - @Nested - @DisplayName("PipelineWithTree") - class PipelineWithTree { - - @Test - @DisplayName("flushSync produces tree.reorganize span when tree reorganizer is enabled") - void treeReorganizeSpanIsChildOfPipeline() { - var memoryId = testMemoryId(); - var insightTypeName = "profile"; - var insightType = testInsightType(insightTypeName); - var item = testItem(202L); - - var bufferStore = - createBufferStore( - BufferEntry.ungrouped(202L), new BufferEntry(202L, "general", false)); - var store = createStore(insightType, item); - var classifier = createClassifier(item); - var generator = createGenerator(202L); - var treeReorganizer = new RecordingTreeReorganizer(); - - var scheduler = - buildScheduler(bufferStore, store, generator, classifier, treeReorganizer); - - scheduler.flushSync(memoryId, insightTypeName); - - var treeSpan = findSpan(MemorySpanNames.EXTRACTION_INSIGHT_TREE_REORGANIZE); - assertThat(treeSpan).isPresent(); - assertThat(treeReorganizer.wasCalled()).isTrue(); - } - } - - private static final class RecordingTreeReorganizer extends InsightTreeReorganizer { - - private boolean called; - - RecordingTreeReorganizer() { - super( - new NoopInsightGenerator(), - proxy( - MemoryVector.class, - (methodName, args) -> unsupported("MemoryVector", methodName)), - proxy( - MemoryStore.class, - (methodName, args) -> unsupported("MemoryStore", methodName)), - proxy( - BubbleTrackerStore.class, - (methodName, args) -> - switch (methodName) { - case "markDirty", "reset" -> null; - case "shouldResummarize" -> false; - case "getDirtyCount" -> 0; - default -> unsupported("BubbleTrackerStore", methodName); - }), - IdUtils.snowflake()); - } - - @Override - public void onLeafsUpdated( - MemoryId memoryId, - String insightTypeName, - MemoryInsightType insightType, - List builtLeafs, - InsightTreeConfig config, - String language) { - this.called = true; - } - - boolean wasCalled() { - return called; - } - } - - private static final class NoopInsightGenerator implements InsightGenerator { - - @Override - public Mono generatePoints( - MemoryInsightType insightType, - String groupName, - List existingPoints, - List newItems, - int targetTokens, - String additionalContext, - String language) { - return Mono.empty(); - } - - @Override - public Mono generateBranchSummary( - MemoryInsightType insightType, - List existingPoints, - List leafInsights, - int targetTokens, - String language) { - return Mono.empty(); - } - - @Override - public Mono generateRootSynthesis( - MemoryInsightType rootInsightType, - List existingPoints, - List branchInsights, - int targetTokens, - String language) { - return Mono.empty(); - } - } -} diff --git a/memind-plugins/memind-plugin-tracing-opentelemetry/src/test/java/com/openmemind/ai/memory/plugin/tracing/otel/OpenTelemetryMemoryMetricsRecorderTest.java b/memind-plugins/memind-plugin-tracing-opentelemetry/src/test/java/com/openmemind/ai/memory/plugin/tracing/otel/OpenTelemetryMemoryMetricsRecorderTest.java deleted file mode 100644 index fc809ec7..00000000 --- a/memind-plugins/memind-plugin-tracing-opentelemetry/src/test/java/com/openmemind/ai/memory/plugin/tracing/otel/OpenTelemetryMemoryMetricsRecorderTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.plugin.tracing.otel; - -import static org.assertj.core.api.Assertions.assertThatCode; - -import com.openmemind.ai.memory.core.metrics.ExtractionMetrics; -import com.openmemind.ai.memory.core.metrics.RetrievalMergeMetrics; -import com.openmemind.ai.memory.core.metrics.RetrievalStageMetrics; -import com.openmemind.ai.memory.core.metrics.RetrievalSummaryMetrics; -import io.opentelemetry.api.metrics.Meter; -import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import org.junit.jupiter.api.Test; - -class OpenTelemetryMemoryMetricsRecorderTest { - - @Test - void recordsAllBusinessMetricPayloads() { - try (SdkMeterProvider provider = SdkMeterProvider.builder().build()) { - Meter meter = provider.get("memind-test"); - var recorder = new OpenTelemetryMemoryMetricsRecorder(meter); - - assertThatCode( - () -> { - recorder.recordExtractionSummary( - new ExtractionMetrics( - "success", 1, null, 2, null, 3, 4, 5, 6, "core")); - recorder.recordRetrievalStage( - new RetrievalStageMetrics( - "simple", "tier", "item", "vector", "success", null, - 10, 5, false, false, "core")); - recorder.recordRetrievalStage( - new RetrievalStageMetrics( - "simple", - "channel", - "item", - "temporal", - "degraded", - 10, - 3, - 2, - true, - false, - "core")); - recorder.recordRetrievalMerge( - new RetrievalMergeMetrics( - "simple", 20, 12, 8, 3, "success", "core")); - recorder.recordRetrievalSummary( - new RetrievalSummaryMetrics( - "simple", "empty", 0, 0, 0, 0, "core")); - }) - .doesNotThrowAnyException(); - } - } -} diff --git a/memind-plugins/memind-plugin-tracing-opentelemetry/src/test/java/com/openmemind/ai/memory/plugin/tracing/otel/OpenTelemetryMemoryObserverTest.java b/memind-plugins/memind-plugin-tracing-opentelemetry/src/test/java/com/openmemind/ai/memory/plugin/tracing/otel/OpenTelemetryMemoryObserverTest.java deleted file mode 100644 index 601acde4..00000000 --- a/memind-plugins/memind-plugin-tracing-opentelemetry/src/test/java/com/openmemind/ai/memory/plugin/tracing/otel/OpenTelemetryMemoryObserverTest.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * 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 com.openmemind.ai.memory.plugin.tracing.otel; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.openmemind.ai.memory.core.tracing.ObservationContext; -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.api.trace.StatusCode; -import io.opentelemetry.api.trace.Tracer; -import io.opentelemetry.instrumentation.reactor.v3_1.ContextPropagationOperator; -import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -@DisplayName("OpenTelemetryMemoryObserver") -class OpenTelemetryMemoryObserverTest { - - private static InMemorySpanExporter spanExporter; - private static Tracer tracer; - private OpenTelemetryMemoryObserver observer; - - @BeforeAll - static void setUpOtel() { - ContextPropagationOperator.builder().build().registerOnEachOperator(); - spanExporter = InMemorySpanExporter.create(); - SdkTracerProvider tracerProvider = - SdkTracerProvider.builder() - .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) - .build(); - tracer = tracerProvider.get("memind-test"); - } - - @BeforeEach - void setUp() { - spanExporter.reset(); - var meter = SdkMeterProvider.builder().build().get("memind-test"); - observer = new OpenTelemetryMemoryObserver(tracer, meter); - } - - @Nested - @DisplayName("observeMono") - class ObserveMonoTests { - - @Test - @DisplayName("produces span with correct name and kind") - void producesSpanWithCorrectNameAndKind() { - ObservationContext ctx = ObservationContext.of("test.operation"); - - StepVerifier.create(observer.observeMono(ctx, () -> Mono.just("result"))) - .expectNext("result") - .verifyComplete(); - - List spans = spanExporter.getFinishedSpanItems(); - assertThat(spans).hasSize(1); - SpanData span = spans.get(0); - assertThat(span.getName()).isEqualTo("test.operation"); - assertThat(span.getKind()).isEqualTo(SpanKind.INTERNAL); - } - - @Test - @DisplayName("sets request attributes on span") - void setsRequestAttributesOnSpan() { - Map attrs = - Map.of( - "str.attr", - "hello", - "int.attr", - 42, - "long.attr", - 100L, - "double.attr", - 3.14, - "bool.attr", - true); - ObservationContext ctx = ObservationContext.of("test.attrs", attrs); - - StepVerifier.create(observer.observeMono(ctx, () -> Mono.just("ok"))) - .expectNext("ok") - .verifyComplete(); - - SpanData span = spanExporter.getFinishedSpanItems().get(0); - assertThat( - span.getAttributes() - .get( - io.opentelemetry.api.common.AttributeKey.stringKey( - "str.attr"))) - .isEqualTo("hello"); - assertThat( - span.getAttributes() - .get( - io.opentelemetry.api.common.AttributeKey.longKey( - "int.attr"))) - .isEqualTo(42L); - assertThat( - span.getAttributes() - .get( - io.opentelemetry.api.common.AttributeKey.longKey( - "long.attr"))) - .isEqualTo(100L); - assertThat( - span.getAttributes() - .get( - io.opentelemetry.api.common.AttributeKey.doubleKey( - "double.attr"))) - .isEqualTo(3.14); - assertThat( - span.getAttributes() - .get( - io.opentelemetry.api.common.AttributeKey.booleanKey( - "bool.attr"))) - .isEqualTo(true); - } - - @Test - @DisplayName("backfills result attributes via resultExtractor") - void backfillsResultAttributesViaExtractor() { - ObservationContext ctx = - ObservationContext.of("test.result") - .withResultExtractor( - result -> Map.of("result.length", result.length())); - - StepVerifier.create(observer.observeMono(ctx, () -> Mono.just("hello"))) - .expectNext("hello") - .verifyComplete(); - - SpanData span = spanExporter.getFinishedSpanItems().get(0); - assertThat( - span.getAttributes() - .get( - io.opentelemetry.api.common.AttributeKey.longKey( - "result.length"))) - .isEqualTo(5L); - } - - @Test - @DisplayName("records error status and exception on failure") - void recordsErrorStatusAndExceptionEvent() { - ObservationContext ctx = ObservationContext.of("test.error"); - RuntimeException ex = new RuntimeException("something went wrong"); - - StepVerifier.create(observer.observeMono(ctx, () -> Mono.error(ex))) - .expectError(RuntimeException.class) - .verify(); - - SpanData span = spanExporter.getFinishedSpanItems().get(0); - assertThat(span.getStatus().getStatusCode()).isEqualTo(StatusCode.ERROR); - assertThat(span.getStatus().getDescription()).isEqualTo("something went wrong"); - assertThat(span.getEvents()).anyMatch(e -> e.getName().equals("exception")); - } - - @Test - @DisplayName("nested observeMono produces parent-child spans") - void nestedCallsProduceParentChildSpans() { - ObservationContext outerCtx = ObservationContext.of("outer.span"); - ObservationContext innerCtx = ObservationContext.of("inner.span"); - - Mono nested = - observer.observeMono( - outerCtx, - () -> observer.observeMono(innerCtx, () -> Mono.just("nested"))); - - StepVerifier.create(nested).expectNext("nested").verifyComplete(); - - List spans = spanExporter.getFinishedSpanItems(); - assertThat(spans).hasSize(2); - - SpanData inner = - spans.stream() - .filter(s -> s.getName().equals("inner.span")) - .findFirst() - .orElseThrow(); - SpanData outer = - spans.stream() - .filter(s -> s.getName().equals("outer.span")) - .findFirst() - .orElseThrow(); - - assertThat(inner.getParentSpanId()).isEqualTo(outer.getSpanId()); - } - - @Test - @DisplayName("handles null result without error (empty Mono)") - void handlesEmptyMonoWithoutError() { - ObservationContext ctx = ObservationContext.of("test.empty"); - - StepVerifier.create(observer.observeMono(ctx, Mono::empty)).verifyComplete(); - - List spans = spanExporter.getFinishedSpanItems(); - assertThat(spans).hasSize(1); - assertThat(spans.get(0).getStatus().getStatusCode()).isNotEqualTo(StatusCode.ERROR); - } - } - - @Nested - @DisplayName("observeFlux") - class ObserveFluxTests { - - @Test - @DisplayName("produces span wrapping flux emission") - void producesSpanWrappingFluxEmission() { - ObservationContext ctx = ObservationContext.of("test.flux"); - - StepVerifier.create(observer.observeFlux(ctx, () -> Flux.just("a", "b", "c"))) - .expectNext("a", "b", "c") - .verifyComplete(); - - List spans = spanExporter.getFinishedSpanItems(); - assertThat(spans).hasSize(1); - SpanData span = spans.get(0); - assertThat(span.getName()).isEqualTo("test.flux"); - assertThat(span.getKind()).isEqualTo(SpanKind.INTERNAL); - } - - @Test - @DisplayName("records error on flux failure") - void recordsErrorOnFluxFailure() { - ObservationContext ctx = ObservationContext.of("test.flux.error"); - - StepVerifier.create( - observer.observeFlux( - ctx, - () -> - Flux.concat( - Flux.just("a"), - Flux.error(new RuntimeException("flux-boom"))))) - .expectNext("a") - .expectError(RuntimeException.class) - .verify(); - - SpanData span = spanExporter.getFinishedSpanItems().get(0); - assertThat(span.getStatus().getStatusCode()).isEqualTo(StatusCode.ERROR); - assertThat(span.getEvents()).anyMatch(e -> e.getName().equals("exception")); - } - } -} diff --git a/memind-plugins/pom.xml b/memind-plugins/pom.xml index 6ff3ee1c..0d8ffe97 100644 --- a/memind-plugins/pom.xml +++ b/memind-plugins/pom.xml @@ -32,6 +32,6 @@ memind-plugin-jdbc memind-plugin-rawdatas memind-plugin-spring-boot-starters - memind-plugin-tracing-opentelemetry + memind-plugin-observability-micrometer diff --git a/memind-server/pom.xml b/memind-server/pom.xml index 66b93a82..8d93e5bd 100644 --- a/memind-server/pom.xml +++ b/memind-server/pom.xml @@ -35,6 +35,16 @@ + + + org.springframework.boot + spring-boot-starter-opentelemetry + + + com.openmemind.ai + memind-plugin-observability-micrometer-starter + ${revision} + com.openmemind.ai memind-core diff --git a/memind-server/src/main/java/com/openmemind/ai/memory/server/configuration/MemindServerObservabilityProperties.java b/memind-server/src/main/java/com/openmemind/ai/memory/server/configuration/MemindServerObservabilityProperties.java index 7292bb80..b503719c 100644 --- a/memind-server/src/main/java/com/openmemind/ai/memory/server/configuration/MemindServerObservabilityProperties.java +++ b/memind-server/src/main/java/com/openmemind/ai/memory/server/configuration/MemindServerObservabilityProperties.java @@ -18,8 +18,17 @@ @ConfigurationProperties(prefix = "memind.observability") public class MemindServerObservabilityProperties { + private boolean enabled = true; private final RetrievalTrace retrievalTrace = new RetrievalTrace(); + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public RetrievalTrace getRetrievalTrace() { return retrievalTrace; } diff --git a/memind-server/src/main/java/com/openmemind/ai/memory/server/configuration/MemindServerRerankConfiguration.java b/memind-server/src/main/java/com/openmemind/ai/memory/server/configuration/MemindServerRerankConfiguration.java index dd2c36c7..b5ae07c5 100644 --- a/memind-server/src/main/java/com/openmemind/ai/memory/server/configuration/MemindServerRerankConfiguration.java +++ b/memind-server/src/main/java/com/openmemind/ai/memory/server/configuration/MemindServerRerankConfiguration.java @@ -15,6 +15,8 @@ import com.openmemind.ai.memory.core.llm.rerank.LlmReranker; import com.openmemind.ai.memory.core.llm.rerank.Reranker; +import io.micrometer.observation.ObservationRegistry; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -26,18 +28,32 @@ import org.springframework.util.StringUtils; @Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(MemindServerRerankProperties.class) +@EnableConfigurationProperties({ + MemindServerRerankProperties.class, + MemindServerObservabilityProperties.class +}) public class MemindServerRerankConfiguration { @Bean @ConditionalOnMissingBean(Reranker.class) @Conditional(RerankPropertiesConfiguredCondition.class) - Reranker reranker(MemindServerRerankProperties properties) { + Reranker reranker( + MemindServerRerankProperties properties, + ObjectProvider observationRegistryProvider, + MemindServerObservabilityProperties observabilityProperties) { + ObservationRegistry observationRegistry = + observabilityProperties.isEnabled() + ? observationRegistryProvider.getIfAvailable(() -> ObservationRegistry.NOOP) + : ObservationRegistry.NOOP; if (StringUtils.hasText(properties.getModel())) { return new LlmReranker( - properties.getBaseUrl(), properties.getApiKey(), properties.getModel()); + properties.getBaseUrl(), + properties.getApiKey(), + properties.getModel(), + observationRegistry); } - return new LlmReranker(properties.getBaseUrl(), properties.getApiKey()); + return new LlmReranker( + properties.getBaseUrl(), properties.getApiKey(), observationRegistry); } static final class RerankPropertiesConfiguredCondition implements Condition { diff --git a/memind-server/src/main/java/com/openmemind/ai/memory/server/configuration/MemindServerRuntimeConfiguration.java b/memind-server/src/main/java/com/openmemind/ai/memory/server/configuration/MemindServerRuntimeConfiguration.java index 94b29973..23c0a4eb 100644 --- a/memind-server/src/main/java/com/openmemind/ai/memory/server/configuration/MemindServerRuntimeConfiguration.java +++ b/memind-server/src/main/java/com/openmemind/ai/memory/server/configuration/MemindServerRuntimeConfiguration.java @@ -20,7 +20,6 @@ import com.openmemind.ai.memory.core.extraction.insight.tree.BubbleTrackerStore; import com.openmemind.ai.memory.core.llm.StructuredChatClient; import com.openmemind.ai.memory.core.llm.rerank.Reranker; -import com.openmemind.ai.memory.core.metrics.MemoryMetricsRecorder; import com.openmemind.ai.memory.core.plugin.RawDataPlugin; import com.openmemind.ai.memory.core.resource.ContentParser; import com.openmemind.ai.memory.core.resource.ContentParserRegistry; @@ -28,7 +27,6 @@ import com.openmemind.ai.memory.core.resource.ResourceFetcher; import com.openmemind.ai.memory.core.store.MemoryStore; import com.openmemind.ai.memory.core.textsearch.MemoryTextSearch; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; import com.openmemind.ai.memory.core.utils.JsonUtils; import com.openmemind.ai.memory.core.vector.MemoryVector; import com.openmemind.ai.memory.plugin.ai.spring.autoconfigure.MemindChatClients; @@ -40,6 +38,7 @@ import com.openmemind.ai.memory.server.service.config.MemoryOptionsCodec; import com.openmemind.ai.memory.server.service.config.MemoryOptionsProjectionMapper; import com.openmemind.ai.memory.server.service.config.ServerRuntimeConfigRepository; +import io.micrometer.observation.ObservationRegistry; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -82,8 +81,8 @@ MemoryRuntimeFactory memoryRuntimeFactory( ObjectProvider rawDataPluginProvider, ObjectProvider resourceFetcherProvider, ObjectProvider bubbleTrackerStoreProvider, - ObjectProvider memoryObserverProvider, - ObjectProvider memoryMetricsRecorderProvider) { + ObjectProvider observationRegistryProvider, + MemindServerObservabilityProperties observabilityProperties) { return options -> { MemindChatClients memindChatClients = memindChatClientsProvider.getIfAvailable(); StructuredChatClient structuredChatClient = @@ -100,11 +99,11 @@ MemoryRuntimeFactory memoryRuntimeFactory( parsers.isEmpty() ? null : new DefaultContentParserRegistry(parsers); ResourceFetcher resourceFetcher = resourceFetcherProvider.getIfAvailable(); BubbleTrackerStore bubbleTrackerStore = bubbleTrackerStoreProvider.getIfAvailable(); - MemoryObserver memoryObserver = memoryObserverProvider.getIfAvailable(); - MemoryMetricsRecorder memoryMetricsRecorder = - memoryMetricsRecorderProvider == null - ? null - : memoryMetricsRecorderProvider.getIfAvailable(); + ObservationRegistry observationRegistry = + observabilityProperties.isEnabled() && observationRegistryProvider != null + ? observationRegistryProvider.getIfAvailable( + () -> ObservationRegistry.NOOP) + : ObservationRegistry.NOOP; var builder = Memory.builder() .chatClient(structuredChatClient) @@ -119,12 +118,7 @@ MemoryRuntimeFactory memoryRuntimeFactory( if (bubbleTrackerStore != null) { builder.bubbleTrackerStore(bubbleTrackerStore); } - if (memoryObserver != null) { - builder.memoryObserver(memoryObserver); - } - if (memoryMetricsRecorder != null) { - builder.memoryMetricsRecorder(memoryMetricsRecorder); - } + builder.observationRegistry(observationRegistry); if (contentParserRegistry != null) { builder.contentParserRegistry(contentParserRegistry); } @@ -144,6 +138,62 @@ MemoryRuntimeFactory memoryRuntimeFactory( }; } + MemoryRuntimeFactory memoryRuntimeFactory( + ObjectProvider memindChatClientsProvider, + ObjectProvider structuredChatClientProvider, + ObjectProvider memoryStoreProvider, + ObjectProvider memoryBufferProvider, + ObjectProvider memoryVectorProvider, + ObjectProvider memoryTextSearch, + ObjectProvider reranker, + ObjectProvider contentParserProvider, + ObjectProvider rawDataPluginProvider, + ObjectProvider resourceFetcherProvider, + ObjectProvider bubbleTrackerStoreProvider, + ObjectProvider observationRegistryProvider) { + return memoryRuntimeFactory( + memindChatClientsProvider, + structuredChatClientProvider, + memoryStoreProvider, + memoryBufferProvider, + memoryVectorProvider, + memoryTextSearch, + reranker, + contentParserProvider, + rawDataPluginProvider, + resourceFetcherProvider, + bubbleTrackerStoreProvider, + observationRegistryProvider, + new MemindServerObservabilityProperties()); + } + + MemoryRuntimeFactory memoryRuntimeFactory( + ObjectProvider structuredChatClientProvider, + ObjectProvider memoryStoreProvider, + ObjectProvider memoryBufferProvider, + ObjectProvider memoryVectorProvider, + ObjectProvider memoryTextSearch, + ObjectProvider reranker, + ObjectProvider contentParserProvider, + ObjectProvider rawDataPluginProvider, + ObjectProvider resourceFetcherProvider, + ObjectProvider bubbleTrackerStoreProvider, + ObjectProvider observationRegistryProvider) { + return memoryRuntimeFactory( + structuredChatClientProvider, + memoryStoreProvider, + memoryBufferProvider, + memoryVectorProvider, + memoryTextSearch, + reranker, + contentParserProvider, + rawDataPluginProvider, + resourceFetcherProvider, + bubbleTrackerStoreProvider, + observationRegistryProvider, + new MemindServerObservabilityProperties()); + } + MemoryRuntimeFactory memoryRuntimeFactory( ObjectProvider structuredChatClientProvider, ObjectProvider memoryStoreProvider, @@ -155,7 +205,8 @@ MemoryRuntimeFactory memoryRuntimeFactory( ObjectProvider rawDataPluginProvider, ObjectProvider resourceFetcherProvider, ObjectProvider bubbleTrackerStoreProvider, - ObjectProvider memoryObserverProvider) { + ObjectProvider observationRegistryProvider, + MemindServerObservabilityProperties observabilityProperties) { return memoryRuntimeFactory( emptyProvider(MemindChatClients.class), structuredChatClientProvider, @@ -168,8 +219,8 @@ MemoryRuntimeFactory memoryRuntimeFactory( rawDataPluginProvider, resourceFetcherProvider, bubbleTrackerStoreProvider, - memoryObserverProvider, - null); + observationRegistryProvider, + observabilityProperties); } private static ObjectProvider emptyProvider(Class type) { diff --git a/memind-server/src/main/java/com/openmemind/ai/memory/server/domain/memory/response/RetrievalTraceView.java b/memind-server/src/main/java/com/openmemind/ai/memory/server/domain/memory/response/RetrievalTraceView.java index 3b8fa412..1eea1347 100644 --- a/memind-server/src/main/java/com/openmemind/ai/memory/server/domain/memory/response/RetrievalTraceView.java +++ b/memind-server/src/main/java/com/openmemind/ai/memory/server/domain/memory/response/RetrievalTraceView.java @@ -20,6 +20,12 @@ import java.util.List; import java.util.Map; +/** + * API view of the core retrieval debug trace. + * + *

This DTO intentionally mirrors {@code RetrievalDebugTrace} closely while keeping server + * serialization concerns out of the core module. + */ public record RetrievalTraceView( String traceId, Instant startedAt, diff --git a/memind-server/src/main/java/com/openmemind/ai/memory/server/service/memory/OpenMemoryApplicationService.java b/memind-server/src/main/java/com/openmemind/ai/memory/server/service/memory/OpenMemoryApplicationService.java index 590747e5..eb92f490 100644 --- a/memind-server/src/main/java/com/openmemind/ai/memory/server/service/memory/OpenMemoryApplicationService.java +++ b/memind-server/src/main/java/com/openmemind/ai/memory/server/service/memory/OpenMemoryApplicationService.java @@ -28,7 +28,7 @@ import com.openmemind.ai.memory.core.retrieval.RetrievalResult; import com.openmemind.ai.memory.core.retrieval.query.QueryContext; import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.trace.BoundedRetrievalTraceCollector; +import com.openmemind.ai.memory.core.retrieval.trace.BoundedRetrievalTraceRecorder; import com.openmemind.ai.memory.core.retrieval.trace.RetrievalDebugTrace; import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceContext; import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceOptions; @@ -165,20 +165,20 @@ public ExtractMemoryResponse commit(CommitMemoryRequest request) { public RetrieveMemoryResponse retrieve(RetrieveMemoryRequest request) { try (var lease = runtimeManager.acquire()) { - BoundedRetrievalTraceCollector traceCollector = traceCollector(request); + BoundedRetrievalTraceRecorder traceRecorder = traceRecorder(request); RetrievalResult result = Objects.requireNonNull( - retrievalMono(lease.handle().memory(), request, traceCollector) + retrievalMono(lease.handle().memory(), request, traceRecorder) .block(REQUEST_TIMEOUT), "Memory retrieve returned no result"); - return toRetrieveResponse(result, traceCollector); + return toRetrieveResponse(result, traceRecorder); } } private Mono retrievalMono( Memory memory, RetrieveMemoryRequest request, - BoundedRetrievalTraceCollector traceCollector) { + BoundedRetrievalTraceRecorder traceRecorder) { Mono operation = shouldUseStructuredRetrieval(request) ? memory.retrieve(toRetrievalRequest(request)) @@ -186,11 +186,12 @@ private Mono retrievalMono( DefaultMemoryId.of(request.userId(), request.agentId()), request.query(), request.strategy()); - if (traceCollector == null) { + if (traceRecorder == null) { return operation; } + // Retrieval trace is request-scoped: attach the recorder to this reactive chain only. return operation.contextWrite( - context -> RetrievalTraceContext.withCollector(context, traceCollector)); + context -> RetrievalTraceContext.withRecorder(context, traceRecorder)); } private static boolean shouldUseStructuredRetrieval(RetrieveMemoryRequest request) { @@ -278,13 +279,14 @@ private static String normalizeSourceClient(String sourceClient) { return sourceClient.trim(); } - private BoundedRetrievalTraceCollector traceCollector(RetrieveMemoryRequest request) { + private BoundedRetrievalTraceRecorder traceRecorder(RetrieveMemoryRequest request) { if (!Boolean.TRUE.equals(request.trace()) || !observabilityProperties.getRetrievalTrace().isEnabled()) { return null; } + // The recorder owns the response debug trace; Micrometer handlers feed it through context. var properties = observabilityProperties.getRetrievalTrace(); - return new BoundedRetrievalTraceCollector( + return new BoundedRetrievalTraceRecorder( new RetrievalTraceOptions( properties.getMaxStages(), properties.getMaxCandidatesPerStage(), @@ -344,7 +346,7 @@ private static List insightIds(ExtractionResult result) { } private static RetrieveMemoryResponse toRetrieveResponse( - RetrievalResult result, BoundedRetrievalTraceCollector traceCollector) { + RetrievalResult result, BoundedRetrievalTraceRecorder traceRecorder) { return new RetrieveMemoryResponse( result.status().name().toLowerCase(), result.items() == null @@ -384,15 +386,16 @@ private static RetrieveMemoryResponse toRetrieveResponse( result.evidences() == null ? List.of() : result.evidences(), result.strategy(), result.query(), - traceCollector == null + traceRecorder == null ? null - : traceCollector + : traceRecorder .snapshot() .map(OpenMemoryApplicationService::toTraceView) .orElse(null)); } private static RetrievalTraceView toTraceView(RetrievalDebugTrace trace) { + // Keep the core trace model free of server API DTO types. return new RetrievalTraceView( trace.traceId(), trace.startedAt(), diff --git a/memind-server/src/main/resources/application.yml b/memind-server/src/main/resources/application.yml index 3fd078fe..6aa82aa6 100644 --- a/memind-server/src/main/resources/application.yml +++ b/memind-server/src/main/resources/application.yml @@ -19,7 +19,6 @@ # - Optional Memind AI client slot routing can be configured under memind.ai.chat.slots. # - Optional rerank provider settings are read from MEMIND_RERANK_* env vars. # - To switch to MySQL or Qdrant, replace/merge the commented examples at the bottom of this file. - spring: application: name: memind-server @@ -41,7 +40,7 @@ spring: default: type: openai openai: - base-url: ${OPENAI_BASE_URL:https://openrouter.ai/api} + base-url: ${OPENAI_BASE_URL:https://openrouter.ai/api/v1} api-key: ${OPENAI_API_KEY:your-api-key} model: ${OPENAI_CHAT_MODEL:openai/gpt-4o-mini} temperature: ${OPENAI_CHAT_TEMPERATURE:0.2} @@ -49,7 +48,7 @@ spring: default: type: openai openai: - base-url: ${EMBEDDING_BASE_URL:${OPENAI_BASE_URL:https://openrouter.ai/api}} + base-url: ${EMBEDDING_BASE_URL:${OPENAI_BASE_URL:https://openrouter.ai/api/v1}} api-key: ${EMBEDDING_API_KEY:${OPENAI_API_KEY:your-api-key}} model: ${OPENAI_EMBEDDING_MODEL:openai/text-embedding-3-small} vectorstore: @@ -97,6 +96,10 @@ memind: model: ${MEMIND_RERANK_MODEL:jina-reranker-v3} store: init-schema: ${MEMIND_STORE_INIT_SCHEMA:true} + observability: + retrieval-trace: + enabled: true + logging: level: @@ -140,3 +143,18 @@ logging: # driver-class-name: org.postgresql.Driver # username: memind # password: changeit +#management: +# opentelemetry: +# tracing: +# export: +# otlp: +# endpoint: http://localhost:4318/v1/traces +#--- +#management: +# tracing: +# sampling: +# probability: 1.0 +#--- +#spring: +# reactor: +# context-propagation: auto diff --git a/memind-server/src/test/java/com/openmemind/ai/memory/server/configuration/MemindServerRerankConfigurationTest.java b/memind-server/src/test/java/com/openmemind/ai/memory/server/configuration/MemindServerRerankConfigurationTest.java index a9fe2df5..de6a00b8 100644 --- a/memind-server/src/test/java/com/openmemind/ai/memory/server/configuration/MemindServerRerankConfigurationTest.java +++ b/memind-server/src/test/java/com/openmemind/ai/memory/server/configuration/MemindServerRerankConfigurationTest.java @@ -18,6 +18,7 @@ import com.openmemind.ai.memory.core.llm.rerank.LlmReranker; import com.openmemind.ai.memory.core.llm.rerank.NoopReranker; import com.openmemind.ai.memory.core.llm.rerank.Reranker; +import io.micrometer.observation.ObservationRegistry; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -90,6 +91,24 @@ void backsOffWhenUserProvidesRerankerBean() { }); } + @Test + @DisplayName("Uses the noop observation registry when observability is disabled") + void usesNoopObservationRegistryWhenObservabilityDisabled() { + contextRunner + .withPropertyValues( + "memind.rerank.base-url=https://rerank.example", + "memind.rerank.api-key=test-key", + "memind.observability.enabled=false") + .withUserConfiguration(ObservationRegistryConfig.class) + .run( + context -> + assertThat( + ReflectionTestUtils.getField( + context.getBean(Reranker.class), + "observationRegistry")) + .isSameAs(ObservationRegistry.NOOP)); + } + @Configuration(proxyBeanMethods = false) static class UserProvidedRerankerConfig { @@ -98,4 +117,13 @@ Reranker customReranker() { return new NoopReranker(); } } + + @Configuration(proxyBeanMethods = false) + static class ObservationRegistryConfig { + + @Bean + ObservationRegistry observationRegistry() { + return ObservationRegistry.create(); + } + } } diff --git a/memind-server/src/test/java/com/openmemind/ai/memory/server/configuration/MemindServerRuntimeConfigurationTest.java b/memind-server/src/test/java/com/openmemind/ai/memory/server/configuration/MemindServerRuntimeConfigurationTest.java index 99d8ef2f..f5f225ca 100644 --- a/memind-server/src/test/java/com/openmemind/ai/memory/server/configuration/MemindServerRuntimeConfigurationTest.java +++ b/memind-server/src/test/java/com/openmemind/ai/memory/server/configuration/MemindServerRuntimeConfigurationTest.java @@ -40,6 +40,7 @@ import com.openmemind.ai.memory.core.extraction.insight.tree.InsightTreeReorganizer; import com.openmemind.ai.memory.core.extraction.item.MemoryItemLayer; import com.openmemind.ai.memory.core.extraction.item.graph.ItemGraphMaterializer; +import com.openmemind.ai.memory.core.extraction.item.graph.pipeline.DefaultItemGraphMaterializer; import com.openmemind.ai.memory.core.extraction.rawdata.RawContentProcessor; import com.openmemind.ai.memory.core.extraction.rawdata.RawContentProcessorRegistry; import com.openmemind.ai.memory.core.extraction.rawdata.content.RawContent; @@ -60,10 +61,6 @@ import com.openmemind.ai.memory.core.store.item.ItemOperations; import com.openmemind.ai.memory.core.store.rawdata.RawDataOperations; import com.openmemind.ai.memory.core.textsearch.MemoryTextSearch; -import com.openmemind.ai.memory.core.tracing.MemoryObserver; -import com.openmemind.ai.memory.core.tracing.ObservationContext; -import com.openmemind.ai.memory.core.tracing.decorator.TracingItemGraphMaterializer; -import com.openmemind.ai.memory.core.tracing.decorator.TracingMemoryExtractor; import com.openmemind.ai.memory.core.vector.MemoryVector; import com.openmemind.ai.memory.plugin.ai.spring.autoconfigure.MemindChatClients; import com.openmemind.ai.memory.plugin.rawdata.audio.content.AudioContent; @@ -79,6 +76,7 @@ import com.openmemind.ai.memory.server.service.config.MemoryOptionsCodec; import com.openmemind.ai.memory.server.service.config.ServerRuntimeConfigRepository; import com.openmemind.ai.memory.server.support.TestMemory; +import io.micrometer.observation.ObservationRegistry; import java.lang.reflect.Proxy; import java.util.Map; import java.util.Optional; @@ -145,7 +143,7 @@ void runtimeFactoryForwardsBubbleTrackerStoreBeanIntoBuiltMemory() { emptyProvider(RawDataPlugin.class), emptyProvider(ResourceFetcher.class), provider(BubbleTrackerStore.class, customBubbleTracker), - emptyProvider(MemoryObserver.class)); + emptyProvider(ObservationRegistry.class)); Memory memory = factory.create(MemoryBuildOptions.defaults()).memory(); var extractor = underlyingExtractor((DefaultMemory) memory); @@ -183,9 +181,7 @@ void runtimeFactoryAppliesConfiguredChatClientsToBuilderSlots() { emptyProvider(RawDataPlugin.class), emptyProvider(ResourceFetcher.class), emptyProvider(BubbleTrackerStore.class), - emptyProvider(MemoryObserver.class), - emptyProvider( - com.openmemind.ai.memory.core.metrics.MemoryMetricsRecorder.class)); + emptyProvider(ObservationRegistry.class)); Memory memory = factory.create(MemoryBuildOptions.defaults()).memory(); var extractor = underlyingExtractor((DefaultMemory) memory); @@ -199,8 +195,8 @@ void runtimeFactoryAppliesConfiguredChatClientsToBuilderSlots() { } @Test - void serverRuntimeFactoryShouldForwardObserverBeanIntoBuilderManagedRuntime() { - var observer = new TestMemoryObserver(); + void serverRuntimeFactoryShouldForwardObservationRegistryBeanIntoBuilderManagedRuntime() { + var observationRegistry = ObservationRegistry.create(); var configuration = new MemindServerRuntimeConfiguration(); MemoryRuntimeFactory factory = @@ -215,14 +211,49 @@ void serverRuntimeFactoryShouldForwardObserverBeanIntoBuilderManagedRuntime() { emptyProvider(RawDataPlugin.class), emptyProvider(ResourceFetcher.class), emptyProvider(BubbleTrackerStore.class), - provider(MemoryObserver.class, observer)); + provider(ObservationRegistry.class, observationRegistry)); var memory = (DefaultMemory) factory.create(graphEnabledBuildOptions()).memory(); var extractor = underlyingExtractor(memory); var itemLayer = readField(extractor, "memoryItemStep", MemoryItemLayer.class); + var graphMaterializer = + readField(itemLayer, "graphMaterializer", ItemGraphMaterializer.class); - assertThat(readField(itemLayer, "graphMaterializer", ItemGraphMaterializer.class)) - .isInstanceOf(TracingItemGraphMaterializer.class); + assertThat(graphMaterializer).isInstanceOf(DefaultItemGraphMaterializer.class); + assertThat(readField(graphMaterializer, "observationRegistry", ObservationRegistry.class)) + .isSameAs(observationRegistry); + } + + @Test + void serverRuntimeFactoryShouldUseNoopRegistryWhenObservabilityIsDisabled() { + var observationRegistry = ObservationRegistry.create(); + var observabilityProperties = new MemindServerObservabilityProperties(); + observabilityProperties.setEnabled(false); + var configuration = new MemindServerRuntimeConfiguration(); + + MemoryRuntimeFactory factory = + configuration.memoryRuntimeFactory( + provider(StructuredChatClient.class, proxy(StructuredChatClient.class)), + provider(MemoryStore.class, new InMemoryMemoryStore()), + provider(MemoryBuffer.class, memoryBuffer()), + provider(MemoryVector.class, proxy(MemoryVector.class)), + emptyProvider(MemoryTextSearch.class), + provider(Reranker.class, new NoopReranker()), + emptyProvider(ContentParser.class), + emptyProvider(RawDataPlugin.class), + emptyProvider(ResourceFetcher.class), + emptyProvider(BubbleTrackerStore.class), + provider(ObservationRegistry.class, observationRegistry), + observabilityProperties); + + var memory = (DefaultMemory) factory.create(graphEnabledBuildOptions()).memory(); + var extractor = underlyingExtractor(memory); + var itemLayer = readField(extractor, "memoryItemStep", MemoryItemLayer.class); + var graphMaterializer = + readField(itemLayer, "graphMaterializer", ItemGraphMaterializer.class); + + assertThat(readField(graphMaterializer, "observationRegistry", ObservationRegistry.class)) + .isSameAs(ObservationRegistry.NOOP); } @Test @@ -241,7 +272,7 @@ void runtimeFactoryPreservesSemanticSourceWindowSizeInEffectiveOptions() { emptyProvider(RawDataPlugin.class), emptyProvider(ResourceFetcher.class), emptyProvider(BubbleTrackerStore.class), - emptyProvider(MemoryObserver.class)); + emptyProvider(ObservationRegistry.class)); var created = factory.create( @@ -338,7 +369,7 @@ public Mono parse(byte[] data, SourceDescriptor source) { emptyProvider(RawDataPlugin.class), provider(ResourceFetcher.class, fetcher), emptyProvider(BubbleTrackerStore.class), - emptyProvider(MemoryObserver.class)); + emptyProvider(ObservationRegistry.class)); Memory memory = factory.create(MemoryBuildOptions.defaults()).memory(); var extractor = underlyingExtractor((DefaultMemory) memory); @@ -402,7 +433,7 @@ public Mono parse(byte[] data, SourceDescriptor source) { provider(RawDataPlugin.class, plugin), emptyProvider(ResourceFetcher.class), emptyProvider(BubbleTrackerStore.class), - emptyProvider(MemoryObserver.class)); + emptyProvider(ObservationRegistry.class)); Memory memory = factory.create(MemoryBuildOptions.defaults()).memory(); var extractor = underlyingExtractor((DefaultMemory) memory); @@ -443,7 +474,7 @@ void runtimeFactoryCollectsImageAndAudioParserBeansFromSpringContext() { emptyProvider(RawDataPlugin.class), emptyProvider(ResourceFetcher.class), emptyProvider(BubbleTrackerStore.class), - emptyProvider(MemoryObserver.class)); + emptyProvider(ObservationRegistry.class)); Memory memory = factory.create(MemoryBuildOptions.defaults()).memory(); var extractor = underlyingExtractor((DefaultMemory) memory); @@ -498,7 +529,7 @@ void runtimeFactoryReturnsSanitizedEffectiveOptionsForInvalidMemoryThreadDerivat emptyProvider(RawDataPlugin.class), emptyProvider(ResourceFetcher.class), emptyProvider(BubbleTrackerStore.class), - emptyProvider(MemoryObserver.class)); + emptyProvider(ObservationRegistry.class)); var created = factory.create(requested); @@ -702,21 +733,6 @@ public String getContentId() { } } - private static final class TestMemoryObserver implements MemoryObserver { - - @Override - public Mono observeMono( - ObservationContext ctx, java.util.function.Supplier> operation) { - return operation.get(); - } - - @Override - public Flux observeFlux( - ObservationContext ctx, java.util.function.Supplier> operation) { - return operation.get(); - } - } - private static final class RacingInitialInsertRepository implements ServerRuntimeConfigRepository { @@ -786,9 +802,6 @@ private static T readField(Object target, String fieldName, Class fieldTy private static DefaultMemoryExtractor underlyingExtractor(DefaultMemory memory) { MemoryExtractor extractor = readField(memory, "extractor", MemoryExtractor.class); - if (extractor instanceof TracingMemoryExtractor tracing) { - return readField(tracing, "delegate", DefaultMemoryExtractor.class); - } return DefaultMemoryExtractor.class.cast(extractor); } } diff --git a/memind-server/src/test/java/com/openmemind/ai/memory/server/service/memory/OpenMemoryApplicationServiceTest.java b/memind-server/src/test/java/com/openmemind/ai/memory/server/service/memory/OpenMemoryApplicationServiceTest.java index 8231b526..44ca008d 100644 --- a/memind-server/src/test/java/com/openmemind/ai/memory/server/service/memory/OpenMemoryApplicationServiceTest.java +++ b/memind-server/src/test/java/com/openmemind/ai/memory/server/service/memory/OpenMemoryApplicationServiceTest.java @@ -43,8 +43,8 @@ import com.openmemind.ai.memory.core.retrieval.RetrievalResult; import com.openmemind.ai.memory.core.retrieval.query.QueryContext; import com.openmemind.ai.memory.core.retrieval.scoring.ScoredResult; -import com.openmemind.ai.memory.core.retrieval.trace.RetrievalFinalTrace; import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceContext; +import com.openmemind.ai.memory.core.retrieval.trace.RetrievalTraceEvent; import com.openmemind.ai.memory.server.configuration.MemindServerObservabilityProperties; import com.openmemind.ai.memory.server.domain.memory.request.AddMessageRequest; import com.openmemind.ai.memory.server.domain.memory.request.CommitMemoryRequest; @@ -483,9 +483,7 @@ public Mono retrieve( return Mono.deferContextual( context -> { if (recordTrace) { - RetrievalTraceContext.collector(context) - .finalResults( - new RetrievalFinalTrace("SIMPLE", "empty", 0, 0, 0, 0)); + RetrievalTraceContext.recorder(context).record(finalTraceEvent()); } return Mono.just(retrieveResult); }); @@ -510,5 +508,19 @@ public Mono deleteInsights(MemoryId memoryId, Collection insightIds) @Override public void close() {} + + private RetrievalTraceEvent finalTraceEvent() { + Instant now = Instant.parse("2026-03-30T10:00:00Z"); + return new RetrievalTraceEvent( + "memind.retrieval.strategy", + "memind.retrieval.strategy", + "empty", + now, + now, + 0L, + Map.of("operation", "retrieval", "strategy", "SIMPLE"), + Map.of(), + new RetrievalTraceEvent.FinalPayload("SIMPLE", 0, 0, 0, 0)); + } } }