Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ public record MemoryItem(
/* Semantic memory occurrence time (null for non-temporal items) */
Instant occurredAt,

/* Normalized semantic interval lower bound (null for non-temporal items) */
Instant occurredStart,

/* Normalized semantic interval exclusive upper bound (null for open/point items) */
Instant occurredEnd,

/* Normalized temporal granularity */
String timeGranularity,

/* Source observation time from the original message/segment (null when unavailable) */
Instant observedAt,

Expand All @@ -60,4 +69,40 @@ public record MemoryItem(
Instant createdAt,

/* Item type (FACT / FORESIGHT) */
MemoryItemType type) {}
MemoryItemType type) {

public MemoryItem(
Long id,
String memoryId,
String content,
MemoryScope scope,
MemoryCategory category,
String contentType,
String vectorId,
String rawDataId,
String contentHash,
Instant occurredAt,
Instant observedAt,
Map<String, Object> metadata,
Instant createdAt,
MemoryItemType type) {
this(
id,
memoryId,
content,
scope,
category,
contentType,
vectorId,
rawDataId,
contentHash,
occurredAt,
null,
null,
null,
observedAt,
metadata,
createdAt,
type);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@
import com.openmemind.ai.memory.core.data.enums.MemoryCategory;
import com.openmemind.ai.memory.core.data.enums.MemoryItemType;
import com.openmemind.ai.memory.core.extraction.item.support.ExtractedMemoryEntry;
import com.openmemind.ai.memory.core.extraction.item.support.ExtractedTemporal;
import com.openmemind.ai.memory.core.extraction.item.support.MemoryItemExtractionResponse;
import com.openmemind.ai.memory.core.extraction.item.support.TemporalNormalizer;
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.item.SelfVerificationPrompts;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.slf4j.Logger;
Expand Down Expand Up @@ -170,7 +174,7 @@ public Mono<List<ExtractedMemoryEntry>> verify(

return structuredChatClient
.call(messages, MemoryItemExtractionResponse.class)
.map(response -> toEntries(response, rawDataId, observedAt))
.map(response -> toEntries(response, rawDataId, referenceTime, observedAt))
.switchIfEmpty(Mono.just(List.of()))
.subscribeOn(Schedulers.boundedElastic())
.doOnNext(
Expand All @@ -192,29 +196,54 @@ public Mono<List<ExtractedMemoryEntry>> verify(
}

private List<ExtractedMemoryEntry> toEntries(
MemoryItemExtractionResponse response, String rawDataId, Instant observedAt) {
MemoryItemExtractionResponse response,
String rawDataId,
Instant referenceTime,
Instant observedAt) {
if (response == null || response.items() == null) {
return List.of();
}

return response.items().stream().map(item -> toEntry(item, rawDataId, observedAt)).toList();
return response.items().stream()
.map(item -> toEntry(item, rawDataId, referenceTime, observedAt))
.toList();
}

private static ExtractedMemoryEntry toEntry(
MemoryItemExtractionResponse.ExtractedItem item, String rawDataId, Instant observedAt) {
MemoryItemExtractionResponse.ExtractedItem item,
String rawDataId,
Instant referenceTime,
Instant observedAt) {
ExtractedTemporal temporal =
TemporalNormalizer.normalize(item.time(), item.occurredAt(), referenceTime);
return new ExtractedMemoryEntry(
item.content(),
clamp(item.confidence()),
resolveOccurredAt(item.occurredAt()),
temporal.compatibilityOccurredAt(),
temporal.occurredStart(),
temporal.occurredEnd(),
temporal.granularity(),
observedAt,
rawDataId,
null,
item.insightTypes() != null ? item.insightTypes() : List.of(),
item.metadata(),
mergeMetadata(item, temporal),
MemoryItemType.FACT,
item.category());
}

private static Map<String, Object> mergeMetadata(
MemoryItemExtractionResponse.ExtractedItem item, ExtractedTemporal temporal) {
var merged = new LinkedHashMap<String, Object>();
if (item.metadata() != null) {
merged.putAll(item.metadata());
}
if (temporal.expression() != null && !temporal.expression().isBlank()) {
merged.put("timeExpression", temporal.expression());
}
return merged.isEmpty() ? null : Map.copyOf(merged);
}

private static Instant resolveOccurredAt(String llmValue) {
if (llmValue != null && !llmValue.isBlank()) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ private ExtractedMemoryEntry entryWithNormalizedInsightTypes(
entry.content(),
entry.confidence(),
entry.occurredAt(),
entry.occurredStart(),
entry.occurredEnd(),
entry.timeGranularity(),
entry.observedAt(),
entry.rawDataId(),
entry.contentHash(),
Expand Down Expand Up @@ -346,6 +349,9 @@ private MemoryItem toMemoryItem(
entry.rawDataId(),
entry.contentHash(),
entry.occurredAt(),
entry.occurredStart(),
entry.occurredEnd(),
entry.timeGranularity(),
entry.observedAt(),
buildItemMetadata(entry),
Instant.now(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
import com.openmemind.ai.memory.core.extraction.item.ItemExtractionConfig;
import com.openmemind.ai.memory.core.extraction.item.ItemExtractionStrategy;
import com.openmemind.ai.memory.core.extraction.item.support.ExtractedMemoryEntry;
import com.openmemind.ai.memory.core.extraction.item.support.ExtractedTemporal;
import com.openmemind.ai.memory.core.extraction.item.support.ForesightExtractionResponse;
import com.openmemind.ai.memory.core.extraction.item.support.MemoryItemExtractionResponse;
import com.openmemind.ai.memory.core.extraction.item.support.TemporalNormalizer;
import com.openmemind.ai.memory.core.extraction.rawdata.ParsedSegment;
import com.openmemind.ai.memory.core.llm.ChatMessages;
import com.openmemind.ai.memory.core.llm.StructuredChatClient;
Expand Down Expand Up @@ -141,24 +143,33 @@ private List<ExtractedMemoryEntry> toFactEntries(
var observedAt = resolveObservedAt(segment);

return response.items().stream()
.map(
item ->
new ExtractedMemoryEntry(
item.content(),
clamp(item.confidence()),
resolveOccurredAt(item.occurredAt()),
observedAt,
segment.rawDataId(),
null,
item.insightTypes() != null
? item.insightTypes()
: List.of(),
mergeMetadata(segment, item),
MemoryItemType.FACT,
item.category()))
.map(item -> toFactEntry(item, observedAt, segment, referenceTime))
.toList();
}

private static ExtractedMemoryEntry toFactEntry(
MemoryItemExtractionResponse.ExtractedItem item,
Instant observedAt,
ParsedSegment segment,
Instant referenceTime) {
ExtractedTemporal temporal =
TemporalNormalizer.normalize(item.time(), item.occurredAt(), referenceTime);
return new ExtractedMemoryEntry(
item.content(),
clamp(item.confidence()),
temporal.compatibilityOccurredAt(),
temporal.occurredStart(),
temporal.occurredEnd(),
temporal.granularity(),
observedAt,
segment.rawDataId(),
null,
item.insightTypes() != null ? item.insightTypes() : List.of(),
mergeMetadata(segment, item, temporal),
MemoryItemType.FACT,
item.category());
}

private Mono<List<ExtractedMemoryEntry>> extractForesight(
ParsedSegment segment, String language) {
return Mono.fromCallable(
Expand Down Expand Up @@ -247,6 +258,13 @@ static Instant resolveObservedAt(ParsedSegment segment) {

static Map<String, Object> mergeMetadata(
ParsedSegment segment, MemoryItemExtractionResponse.ExtractedItem item) {
return mergeMetadata(segment, item, null);
}

static Map<String, Object> mergeMetadata(
ParsedSegment segment,
MemoryItemExtractionResponse.ExtractedItem item,
ExtractedTemporal temporal) {
var merged = new LinkedHashMap<String, Object>();
if (segment.metadata() != null) {
merged.putAll(segment.metadata());
Expand All @@ -255,6 +273,9 @@ static Map<String, Object> mergeMetadata(
if (item.metadata() != null) {
merged.putAll(item.metadata());
}
if (temporal != null && temporal.expression() != null && !temporal.expression().isBlank()) {
merged.put("timeExpression", temporal.expression());
}
return merged.isEmpty() ? null : Map.copyOf(merged);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
* @param content Memory content
* @param confidence Confidence level (used only for filtering, not persisted)
* @param occurredAt Semantic time when the memory occurred (null for non-temporal items)
* @param occurredStart Normalized semantic interval lower bound (null for non-temporal items)
* @param occurredEnd Normalized semantic interval exclusive upper bound
* @param timeGranularity Normalized temporal granularity
* @param observedAt Source observation time from the original message/segment
* @param rawDataId Source data ID
* @param contentHash Content hash (filled after deduplication)
Expand All @@ -36,6 +39,9 @@ public record ExtractedMemoryEntry(
String content,
float confidence,
Instant occurredAt,
Instant occurredStart,
Instant occurredEnd,
String timeGranularity,
Instant observedAt,
String rawDataId,
String contentHash,
Expand All @@ -44,6 +50,33 @@ public record ExtractedMemoryEntry(
MemoryItemType type,
String category) {

public ExtractedMemoryEntry(
String content,
float confidence,
Instant occurredAt,
Instant observedAt,
String rawDataId,
String contentHash,
List<String> insightTypes,
Map<String, Object> metadata,
MemoryItemType type,
String category) {
this(
content,
confidence,
occurredAt,
null,
null,
null,
observedAt,
rawDataId,
contentHash,
insightTypes,
metadata,
type,
category);
}

/**
* Set contentHash
*/
Expand All @@ -52,6 +85,9 @@ public ExtractedMemoryEntry withContentHash(String contentHash) {
content,
confidence,
occurredAt,
occurredStart,
occurredEnd,
timeGranularity,
observedAt,
rawDataId,
contentHash,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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.support;

import java.time.Instant;

/**
* Normalized temporal payload used by the extraction pipeline before persistence.
*
* @param occurredStart semantic lower bound in UTC
* @param occurredEnd semantic exclusive upper bound in UTC
* @param granularity normalized temporal granularity
* @param expression original temporal phrase from source text
* @param compatibilityOccurredAt compatibility anchor for legacy occurredAt
*/
public record ExtractedTemporal(
Instant occurredStart,
Instant occurredEnd,
String granularity,
String expression,
Instant compatibilityOccurredAt) {}
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,24 @@
@JsonIgnoreProperties(ignoreUnknown = true)
public record MemoryItemExtractionResponse(List<ExtractedItem> items) {

/**
* Structured temporal extraction result.
*
* @param expression Original temporal phrase from source text
* @param start Normalized lower bound in ISO-8601 UTC
* @param end Normalized exclusive upper bound in ISO-8601 UTC
* @param granularity Temporal granularity
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record ExtractedTime(String expression, String start, String end, String granularity) {}

/**
* Single extraction result
*
* @param content Memory content
* @param confidence Confidence level (0.0-1.0)
* @param occurredAt Memory occurrence time ISO-8601 (LLM parsed, may be null)
* @param time Structured temporal extraction result
* @param insightTypes List of matched InsightType names (may be empty)
* @param metadata Additional metadata (may be null; includes whenToUse, etc.)
*/
Expand All @@ -39,7 +51,19 @@ public record ExtractedItem(
String content,
float confidence,
String occurredAt,
ExtractedTime time,
List<String> insightTypes,
Map<String, Object> metadata,
String category) {}
String category) {

public ExtractedItem(
String content,
float confidence,
String occurredAt,
List<String> insightTypes,
Map<String, Object> metadata,
String category) {
this(content, confidence, occurredAt, null, insightTypes, metadata, category);
}
}
}
Loading
Loading