mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-12 16:53:04 +08:00
refactor(memory): enhance topic-based memory runtime on recalling and indexing
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
package work.slhaf.partner.module.memory.pojo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ActivationProfile {
|
||||
private Float activationWeight;
|
||||
private Float diffusionWeight;
|
||||
private Float contextIndependenceWeight;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package work.slhaf.partner.module.memory.runtime;
|
||||
|
||||
import work.slhaf.partner.core.memory.pojo.SliceRef;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
final class DateMemoryIndex {
|
||||
|
||||
private final Map<LocalDate, CopyOnWriteArrayList<SliceRef>> dateIndex = new HashMap<>();
|
||||
|
||||
void record(SliceRef sliceRef, LocalDate date) {
|
||||
dateIndex.computeIfAbsent(date, key -> new CopyOnWriteArrayList<>()).addIfAbsent(sliceRef);
|
||||
}
|
||||
|
||||
List<SliceRef> find(LocalDate date) {
|
||||
List<SliceRef> refs = dateIndex.get(date);
|
||||
return refs == null ? null : new ArrayList<>(refs);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
dateIndex.clear();
|
||||
}
|
||||
|
||||
void restore(LocalDate date, CopyOnWriteArrayList<SliceRef> refs) {
|
||||
dateIndex.put(date, refs);
|
||||
}
|
||||
|
||||
Map<LocalDate, CopyOnWriteArrayList<SliceRef>> entries() {
|
||||
return dateIndex;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package work.slhaf.partner.module.memory.runtime;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.memory.MemoryCapability;
|
||||
@@ -16,8 +14,8 @@ import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.framework.agent.model.pojo.Message;
|
||||
import work.slhaf.partner.framework.agent.state.State;
|
||||
import work.slhaf.partner.framework.agent.state.StateSerializable;
|
||||
import work.slhaf.partner.framework.agent.state.StateValue;
|
||||
import work.slhaf.partner.framework.agent.support.Result;
|
||||
import work.slhaf.partner.module.memory.pojo.ActivationProfile;
|
||||
import work.slhaf.partner.module.memory.runtime.exception.MemoryLookupException;
|
||||
import work.slhaf.partner.module.memory.selector.ActivatedMemorySlice;
|
||||
|
||||
@@ -25,11 +23,10 @@ import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@Slf4j
|
||||
public class MemoryRuntime extends AbstractAgentModule.Standalone implements StateSerializable {
|
||||
|
||||
@InjectCapability
|
||||
@@ -38,8 +35,10 @@ public class MemoryRuntime extends AbstractAgentModule.Standalone implements Sta
|
||||
private CognitionCapability cognitionCapability;
|
||||
|
||||
private final ReentrantLock runtimeLock = new ReentrantLock();
|
||||
private Map<String, CopyOnWriteArrayList<SliceRef>> topicSlices = new HashMap<>();
|
||||
private Map<LocalDate, CopyOnWriteArrayList<SliceRef>> dateIndex = new HashMap<>();
|
||||
private final TopicMemoryIndex topicIndex = new TopicMemoryIndex();
|
||||
private final DateMemoryIndex dateIndex = new DateMemoryIndex();
|
||||
private final TopicRecallCollector topicRecallCollector = new TopicRecallCollector(new TopicRecallScorer());
|
||||
private final MemoryRuntimeStateCodec stateCodec = new MemoryRuntimeStateCodec();
|
||||
|
||||
@Init
|
||||
public void init() {
|
||||
@@ -53,80 +52,32 @@ public class MemoryRuntime extends AbstractAgentModule.Standalone implements Sta
|
||||
}
|
||||
}
|
||||
|
||||
private void bindTopic(String topicPath, SliceRef sliceRef) {
|
||||
String normalizedPath = normalizeTopicPath(topicPath);
|
||||
runtimeLock.lock();
|
||||
try {
|
||||
CopyOnWriteArrayList<SliceRef> refs = topicSlices.computeIfAbsent(normalizedPath, key -> new CopyOnWriteArrayList<>());
|
||||
boolean exists = refs.stream().anyMatch(ref -> Objects.equals(ref.getUnitId(), sliceRef.getUnitId())
|
||||
&& Objects.equals(ref.getSliceId(), sliceRef.getSliceId()));
|
||||
if (!exists) {
|
||||
refs.add(sliceRef);
|
||||
}
|
||||
} finally {
|
||||
runtimeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void recordMemory(MemoryUnit memoryUnit, String topicPath, List<String> relatedTopicPaths) {
|
||||
public void recordMemory(MemoryUnit memoryUnit,
|
||||
String topicPath,
|
||||
List<String> relatedTopicPaths,
|
||||
ActivationProfile activationProfile) {
|
||||
MemorySlice memorySlice = memoryUnit.getSlices().getLast();
|
||||
SliceRef sliceRef = new SliceRef(memoryUnit.getId(), memorySlice.getId());
|
||||
indexMemoryUnit(memoryUnit);
|
||||
if (topicPath != null && !topicPath.isBlank()) {
|
||||
bindTopic(topicPath, sliceRef);
|
||||
}
|
||||
for (String relatedTopicPath : relatedTopicPaths) {
|
||||
if (relatedTopicPath != null && !relatedTopicPath.isBlank()) {
|
||||
bindTopic(relatedTopicPath, sliceRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void indexMemoryUnit(MemoryUnit memoryUnit) {
|
||||
LocalDate date = toLocalDate(memorySlice.getTimestamp());
|
||||
runtimeLock.lock();
|
||||
try {
|
||||
for (CopyOnWriteArrayList<SliceRef> refs : dateIndex.values()) {
|
||||
refs.removeIf(ref -> memoryUnit.getId().equals(ref.getUnitId()));
|
||||
}
|
||||
for (MemorySlice slice : memoryUnit.getSlices()) {
|
||||
LocalDate date = Instant.ofEpochMilli(slice.getTimestamp())
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDate();
|
||||
dateIndex.computeIfAbsent(date, key -> new CopyOnWriteArrayList<>())
|
||||
.addIfAbsent(new SliceRef(memoryUnit.getId(), slice.getId()));
|
||||
List<String> normalizedRelatedTopicPaths = topicIndex.normalizeTopicPaths(relatedTopicPaths);
|
||||
dateIndex.record(sliceRef, date);
|
||||
if (topicPath != null && !topicPath.isBlank()) {
|
||||
topicIndex.recordBinding(
|
||||
topicPath,
|
||||
sliceRef,
|
||||
memorySlice.getTimestamp(),
|
||||
normalizedRelatedTopicPaths,
|
||||
activationProfile
|
||||
);
|
||||
}
|
||||
topicIndex.ensureTopicPaths(normalizedRelatedTopicPaths);
|
||||
} finally {
|
||||
runtimeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private List<SliceRef> findByTopicPath(String topicPath) {
|
||||
String normalizedPath = normalizeTopicPath(topicPath);
|
||||
List<SliceRef> refs = topicSlices.get(normalizedPath);
|
||||
if (refs == null) {
|
||||
ExceptionReporterHandler.INSTANCE.report(new MemoryLookupException(
|
||||
"Unexisted topic path: " + normalizedPath,
|
||||
normalizedPath,
|
||||
"TOPIC"
|
||||
));
|
||||
return List.of();
|
||||
}
|
||||
return new ArrayList<>(refs);
|
||||
}
|
||||
|
||||
private List<SliceRef> findByDate(LocalDate date) {
|
||||
List<SliceRef> refs = dateIndex.get(date);
|
||||
if (refs == null) {
|
||||
ExceptionReporterHandler.INSTANCE.report(new MemoryLookupException(
|
||||
"Unexisted date index: " + date,
|
||||
date.toString(),
|
||||
"DATE_INDEX"
|
||||
));
|
||||
return List.of();
|
||||
}
|
||||
return new ArrayList<>(refs);
|
||||
}
|
||||
|
||||
public List<ActivatedMemorySlice> queryActivatedMemoryByTopicPath(String topicPath) {
|
||||
return buildActivatedMemorySlices(findByTopicPath(topicPath));
|
||||
}
|
||||
@@ -136,23 +87,61 @@ public class MemoryRuntime extends AbstractAgentModule.Standalone implements Sta
|
||||
}
|
||||
|
||||
public String getTopicTree() {
|
||||
TopicTreeNode root = new TopicTreeNode();
|
||||
for (Map.Entry<String, CopyOnWriteArrayList<SliceRef>> entry : topicSlices.entrySet()) {
|
||||
String[] parts = entry.getKey().split("->");
|
||||
TopicTreeNode current = root;
|
||||
for (String part : parts) {
|
||||
current = current.children.computeIfAbsent(part, key -> new TopicTreeNode());
|
||||
}
|
||||
current.count += entry.getValue().size();
|
||||
runtimeLock.lock();
|
||||
try {
|
||||
return topicIndex.getTopicTree();
|
||||
} finally {
|
||||
runtimeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
List<Map.Entry<String, TopicTreeNode>> roots = new ArrayList<>(root.children.entrySet());
|
||||
for (Map.Entry<String, TopicTreeNode> entry : roots) {
|
||||
stringBuilder.append(entry.getKey()).append("[root]").append("\r\n");
|
||||
printSubTopicsTreeFormat(entry.getValue(), "", stringBuilder);
|
||||
public String fixTopicPath(String topicPath) {
|
||||
String[] parts = topicPath.split("->");
|
||||
List<String> cleanedParts = new ArrayList<>();
|
||||
for (String part : parts) {
|
||||
String cleaned = part.replaceAll("\\[[^]]*]", "").trim();
|
||||
if (!cleaned.isEmpty()) {
|
||||
cleanedParts.add(cleaned);
|
||||
}
|
||||
}
|
||||
return String.join("->", cleanedParts);
|
||||
}
|
||||
|
||||
private List<SliceRef> findByTopicPath(String topicPath) {
|
||||
runtimeLock.lock();
|
||||
try {
|
||||
TopicMemoryIndex.TopicTreeNode topicNode = topicIndex.findTopicNode(topicPath);
|
||||
if (topicNode == null) {
|
||||
String normalizedPath = topicIndex.normalizeTopicPath(topicPath);
|
||||
ExceptionReporterHandler.INSTANCE.report(new MemoryLookupException(
|
||||
"Unexisted topic path: " + normalizedPath,
|
||||
normalizedPath,
|
||||
"TOPIC"
|
||||
));
|
||||
return List.of();
|
||||
}
|
||||
return topicRecallCollector.collect(topicIndex, topicNode);
|
||||
} finally {
|
||||
runtimeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private List<SliceRef> findByDate(LocalDate date) {
|
||||
runtimeLock.lock();
|
||||
try {
|
||||
List<SliceRef> refs = dateIndex.find(date);
|
||||
if (refs == null) {
|
||||
ExceptionReporterHandler.INSTANCE.report(new MemoryLookupException(
|
||||
"Unexisted date index: " + date,
|
||||
date.toString(),
|
||||
"DATE_INDEX"
|
||||
));
|
||||
return List.of();
|
||||
}
|
||||
return refs;
|
||||
} finally {
|
||||
runtimeLock.unlock();
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
private List<ActivatedMemorySlice> buildActivatedMemorySlices(List<SliceRef> refs) {
|
||||
@@ -169,14 +158,12 @@ public class MemoryRuntime extends AbstractAgentModule.Standalone implements Sta
|
||||
private ActivatedMemorySlice buildActivatedMemorySlice(SliceRef ref) {
|
||||
MemoryUnit memoryUnit = memoryCapability.getMemoryUnit(ref.getUnitId());
|
||||
Result<MemorySlice> memorySliceResult = memoryCapability.getMemorySlice(ref.getUnitId(), ref.getSliceId());
|
||||
if (memorySliceResult.exceptionOrNull() != null) {
|
||||
if (memoryUnit == null || memorySliceResult.exceptionOrNull() != null) {
|
||||
return null;
|
||||
}
|
||||
MemorySlice memorySlice = memorySliceResult.getOrThrow();
|
||||
List<Message> messages = sliceMessages(memoryUnit, memorySlice);
|
||||
LocalDate date = Instant.ofEpochMilli(memorySlice.getTimestamp())
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDate();
|
||||
LocalDate date = toLocalDate(memorySlice.getTimestamp());
|
||||
return ActivatedMemorySlice.builder()
|
||||
.unitId(ref.getUnitId())
|
||||
.sliceId(ref.getSliceId())
|
||||
@@ -201,29 +188,14 @@ public class MemoryRuntime extends AbstractAgentModule.Standalone implements Sta
|
||||
return new ArrayList<>(conversationMessages.subList(start, end));
|
||||
}
|
||||
|
||||
private void printSubTopicsTreeFormat(TopicTreeNode node, String prefix, StringBuilder stringBuilder) {
|
||||
List<Map.Entry<String, TopicTreeNode>> entries = new ArrayList<>(node.children.entrySet());
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
boolean last = i == entries.size() - 1;
|
||||
Map.Entry<String, TopicTreeNode> entry = entries.get(i);
|
||||
stringBuilder.append(prefix)
|
||||
.append(last ? "└── " : "├── ")
|
||||
.append(entry.getKey())
|
||||
.append("[")
|
||||
.append(entry.getValue().count)
|
||||
.append("]")
|
||||
.append("\r\n");
|
||||
printSubTopicsTreeFormat(entry.getValue(), prefix + (last ? " " : "│ "), stringBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeTopicPath(String topicPath) {
|
||||
return topicPath == null ? "" : topicPath.trim();
|
||||
private LocalDate toLocalDate(Long timestamp) {
|
||||
return Instant.ofEpochMilli(timestamp)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDate();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Path statePath() {
|
||||
public @NotNull Path statePath() {
|
||||
return Path.of("module", "memory", "topic_based_memory.json");
|
||||
}
|
||||
|
||||
@@ -231,42 +203,7 @@ public class MemoryRuntime extends AbstractAgentModule.Standalone implements Sta
|
||||
public void load(@NotNull JSONObject state) {
|
||||
runtimeLock.lock();
|
||||
try {
|
||||
topicSlices = new HashMap<>();
|
||||
dateIndex = new HashMap<>();
|
||||
|
||||
JSONArray topicSlicesArray = state.getJSONArray("topic_slices");
|
||||
if (topicSlicesArray != null) {
|
||||
for (int i = 0; i < topicSlicesArray.size(); i++) {
|
||||
JSONObject topicObject = topicSlicesArray.getJSONObject(i);
|
||||
if (topicObject == null) {
|
||||
continue;
|
||||
}
|
||||
String topicPath = topicObject.getString("topic_path");
|
||||
if (topicPath == null) {
|
||||
continue;
|
||||
}
|
||||
topicSlices.put(normalizeTopicPath(topicPath), decodeSliceRefs(topicObject.getJSONArray("refs")));
|
||||
}
|
||||
}
|
||||
|
||||
JSONArray dateIndexArray = state.getJSONArray("date_index");
|
||||
if (dateIndexArray != null) {
|
||||
for (int i = 0; i < dateIndexArray.size(); i++) {
|
||||
JSONObject dateObject = dateIndexArray.getJSONObject(i);
|
||||
if (dateObject == null) {
|
||||
continue;
|
||||
}
|
||||
String date = dateObject.getString("date");
|
||||
if (date == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
dateIndex.put(LocalDate.parse(date), decodeSliceRefs(dateObject.getJSONArray("refs")));
|
||||
} catch (Exception e) {
|
||||
log.warn("skip invalid date index: {}", date, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
stateCodec.load(state, topicIndex, dateIndex);
|
||||
} finally {
|
||||
runtimeLock.unlock();
|
||||
}
|
||||
@@ -276,78 +213,9 @@ public class MemoryRuntime extends AbstractAgentModule.Standalone implements Sta
|
||||
public @NotNull State convert() {
|
||||
runtimeLock.lock();
|
||||
try {
|
||||
State state = new State();
|
||||
|
||||
List<StateValue.Obj> topicSliceStates = topicSlices.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.map(entry -> StateValue.obj(Map.of(
|
||||
"topic_path", StateValue.str(entry.getKey()),
|
||||
"refs", StateValue.arr(encodeSliceRefs(entry.getValue()))
|
||||
)))
|
||||
.toList();
|
||||
state.append("topic_slices", StateValue.arr(topicSliceStates));
|
||||
|
||||
List<StateValue.Obj> dateIndexStates = dateIndex.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.map(entry -> StateValue.obj(Map.of(
|
||||
"date", StateValue.str(entry.getKey().toString()),
|
||||
"refs", StateValue.arr(encodeSliceRefs(entry.getValue()))
|
||||
)))
|
||||
.toList();
|
||||
state.append("date_index", StateValue.arr(dateIndexStates));
|
||||
|
||||
return state;
|
||||
return stateCodec.convert(topicIndex, dateIndex);
|
||||
} finally {
|
||||
runtimeLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private List<StateValue> encodeSliceRefs(List<SliceRef> refs) {
|
||||
return refs.stream()
|
||||
.map(ref -> (StateValue) StateValue.obj(Map.of(
|
||||
"unit_id", StateValue.str(ref.getUnitId()),
|
||||
"slice_id", StateValue.str(ref.getSliceId())
|
||||
)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private CopyOnWriteArrayList<SliceRef> decodeSliceRefs(JSONArray refsArray) {
|
||||
CopyOnWriteArrayList<SliceRef> refs = new CopyOnWriteArrayList<>();
|
||||
if (refsArray == null) {
|
||||
return refs;
|
||||
}
|
||||
for (int i = 0; i < refsArray.size(); i++) {
|
||||
JSONObject refObject = refsArray.getJSONObject(i);
|
||||
if (refObject == null) {
|
||||
continue;
|
||||
}
|
||||
String unitId = refObject.getString("unit_id");
|
||||
String sliceId = refObject.getString("slice_id");
|
||||
if (unitId == null || sliceId == null) {
|
||||
continue;
|
||||
}
|
||||
refs.addIfAbsent(new SliceRef(unitId, sliceId));
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
public String fixTopicPath(String topicPath) {
|
||||
String[] parts = topicPath.split("->");
|
||||
List<String> cleanedParts = new ArrayList<>();
|
||||
|
||||
for (String part : parts) {
|
||||
// 修正正则表达式,正确移除 [xxx] 部分
|
||||
String cleaned = part.replaceAll("\\[[^\\]]*\\]", "").trim();
|
||||
if (!cleaned.isEmpty()) { // 忽略空字符串
|
||||
cleanedParts.add(cleaned);
|
||||
}
|
||||
}
|
||||
|
||||
return String.join("->", cleanedParts);
|
||||
}
|
||||
|
||||
private static final class TopicTreeNode {
|
||||
private final Map<String, TopicTreeNode> children = new LinkedHashMap<>();
|
||||
private int count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package work.slhaf.partner.module.memory.runtime;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import work.slhaf.partner.core.memory.pojo.SliceRef;
|
||||
import work.slhaf.partner.framework.agent.state.State;
|
||||
import work.slhaf.partner.framework.agent.state.StateValue;
|
||||
import work.slhaf.partner.module.memory.pojo.ActivationProfile;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
@Slf4j
|
||||
final class MemoryRuntimeStateCodec {
|
||||
|
||||
void load(JSONObject state, TopicMemoryIndex topicIndex, DateMemoryIndex dateIndex) {
|
||||
topicIndex.reset();
|
||||
dateIndex.reset();
|
||||
|
||||
JSONArray topicSlicesArray = state.getJSONArray("topic_slices");
|
||||
if (topicSlicesArray != null) {
|
||||
for (int i = 0; i < topicSlicesArray.size(); i++) {
|
||||
JSONObject topicObject = topicSlicesArray.getJSONObject(i);
|
||||
if (topicObject == null) {
|
||||
continue;
|
||||
}
|
||||
String topicPath = topicObject.getString("topic_path");
|
||||
if (topicPath == null) {
|
||||
continue;
|
||||
}
|
||||
topicIndex.ensureTopicPath(topicPath);
|
||||
decodeTopicBindings(topicIndex, topicPath, topicObject.getJSONArray("bindings"));
|
||||
}
|
||||
}
|
||||
|
||||
JSONArray dateIndexArray = state.getJSONArray("date_index");
|
||||
if (dateIndexArray != null) {
|
||||
for (int i = 0; i < dateIndexArray.size(); i++) {
|
||||
JSONObject dateObject = dateIndexArray.getJSONObject(i);
|
||||
if (dateObject == null) {
|
||||
continue;
|
||||
}
|
||||
String date = dateObject.getString("date");
|
||||
if (date == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
dateIndex.restore(LocalDate.parse(date), decodeSliceRefs(dateObject.getJSONArray("refs")));
|
||||
} catch (Exception e) {
|
||||
log.warn("skip invalid date index: {}", date, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
State convert(TopicMemoryIndex topicIndex, DateMemoryIndex dateIndex) {
|
||||
State state = new State();
|
||||
|
||||
List<StateValue.Obj> topicSliceStates = new ArrayList<>();
|
||||
for (Map.Entry<String, TopicMemoryIndex.TopicTreeNode> entry : topicIndex.roots().entrySet()) {
|
||||
collectTopicStates(entry.getKey(), entry.getValue(), topicSliceStates);
|
||||
}
|
||||
state.append("topic_slices", StateValue.arr(topicSliceStates));
|
||||
|
||||
List<StateValue.Obj> dateIndexStates = dateIndex.entries().entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.map(entry -> StateValue.obj(Map.of(
|
||||
"date", StateValue.str(entry.getKey().toString()),
|
||||
"refs", StateValue.arr(encodeSliceRefs(entry.getValue()))
|
||||
)))
|
||||
.toList();
|
||||
state.append("date_index", StateValue.arr(dateIndexStates));
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
private void collectTopicStates(String path,
|
||||
TopicMemoryIndex.TopicTreeNode topicNode,
|
||||
List<StateValue.Obj> topicStates) {
|
||||
topicStates.add(StateValue.obj(Map.of(
|
||||
"topic_path", StateValue.str(path),
|
||||
"bindings", StateValue.arr(encodeTopicBindings(topicNode.bindings()))
|
||||
)));
|
||||
for (Map.Entry<String, TopicMemoryIndex.TopicTreeNode> childEntry : topicNode.children().entrySet()) {
|
||||
collectTopicStates(path + "->" + childEntry.getKey(), childEntry.getValue(), topicStates);
|
||||
}
|
||||
}
|
||||
|
||||
private List<StateValue> encodeTopicBindings(List<TopicMemoryIndex.TopicBinding> bindings) {
|
||||
return bindings.stream()
|
||||
.map(binding -> (StateValue) StateValue.obj(Map.of(
|
||||
"unit_id", StateValue.str(binding.sliceRef().getUnitId()),
|
||||
"slice_id", StateValue.str(binding.sliceRef().getSliceId()),
|
||||
"timestamp", StateValue.num(binding.timestamp()),
|
||||
"activation_profile", StateValue.obj(Map.of(
|
||||
"activation_weight", StateValue.num(binding.activationProfile().getActivationWeight()),
|
||||
"diffusion_weight", StateValue.num(binding.activationProfile().getDiffusionWeight()),
|
||||
"context_independence_weight",
|
||||
StateValue.num(binding.activationProfile().getContextIndependenceWeight())
|
||||
)),
|
||||
"related_topic_paths", StateValue.arr(binding.relatedTopicPaths().stream()
|
||||
.map(StateValue::str)
|
||||
.toList())
|
||||
)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void decodeTopicBindings(TopicMemoryIndex topicIndex, String topicPath, JSONArray bindingsArray) {
|
||||
if (bindingsArray == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < bindingsArray.size(); i++) {
|
||||
JSONObject bindingObject = bindingsArray.getJSONObject(i);
|
||||
if (bindingObject == null) {
|
||||
continue;
|
||||
}
|
||||
String unitId = bindingObject.getString("unit_id");
|
||||
String sliceId = bindingObject.getString("slice_id");
|
||||
if (unitId == null || sliceId == null) {
|
||||
continue;
|
||||
}
|
||||
Long timestamp = bindingObject.getLong("timestamp");
|
||||
if (timestamp == null) {
|
||||
log.warn("skip topic binding without timestamp: {}:{}", unitId, sliceId);
|
||||
continue;
|
||||
}
|
||||
List<String> relatedTopicPaths = topicIndex.normalizeTopicPaths(
|
||||
bindingObject.getList("related_topic_paths", String.class)
|
||||
);
|
||||
topicIndex.recordBinding(
|
||||
topicPath,
|
||||
new SliceRef(unitId, sliceId),
|
||||
timestamp,
|
||||
relatedTopicPaths,
|
||||
decodeActivationProfile(bindingObject.getJSONObject("activation_profile"))
|
||||
);
|
||||
topicIndex.ensureTopicPaths(relatedTopicPaths);
|
||||
}
|
||||
}
|
||||
|
||||
private ActivationProfile decodeActivationProfile(JSONObject profileObject) {
|
||||
if (profileObject == null) {
|
||||
return null;
|
||||
}
|
||||
return new ActivationProfile(
|
||||
profileObject.getFloat("activation_weight"),
|
||||
profileObject.getFloat("diffusion_weight"),
|
||||
profileObject.getFloat("context_independence_weight")
|
||||
);
|
||||
}
|
||||
|
||||
private List<StateValue> encodeSliceRefs(List<SliceRef> refs) {
|
||||
return refs.stream()
|
||||
.map(ref -> (StateValue) StateValue.obj(Map.of(
|
||||
"unit_id", StateValue.str(ref.getUnitId()),
|
||||
"slice_id", StateValue.str(ref.getSliceId())
|
||||
)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private CopyOnWriteArrayList<SliceRef> decodeSliceRefs(JSONArray refsArray) {
|
||||
CopyOnWriteArrayList<SliceRef> refs = new CopyOnWriteArrayList<>();
|
||||
if (refsArray == null) {
|
||||
return refs;
|
||||
}
|
||||
for (int i = 0; i < refsArray.size(); i++) {
|
||||
JSONObject refObject = refsArray.getJSONObject(i);
|
||||
if (refObject == null) {
|
||||
continue;
|
||||
}
|
||||
String unitId = refObject.getString("unit_id");
|
||||
String sliceId = refObject.getString("slice_id");
|
||||
if (unitId == null || sliceId == null) {
|
||||
continue;
|
||||
}
|
||||
refs.addIfAbsent(new SliceRef(unitId, sliceId));
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package work.slhaf.partner.module.memory.runtime;
|
||||
|
||||
import work.slhaf.partner.core.memory.pojo.SliceRef;
|
||||
import work.slhaf.partner.module.memory.pojo.ActivationProfile;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
final class TopicMemoryIndex {
|
||||
|
||||
private static final float DEFAULT_ACTIVATION_WEIGHT = 0.55f;
|
||||
private static final float DEFAULT_DIFFUSION_WEIGHT = 0.35f;
|
||||
private static final float DEFAULT_CONTEXT_INDEPENDENCE_WEIGHT = 0.50f;
|
||||
|
||||
private final Map<String, TopicTreeNode> topicSlices = new LinkedHashMap<>();
|
||||
|
||||
void recordBinding(String topicPath,
|
||||
SliceRef sliceRef,
|
||||
long timestamp,
|
||||
Collection<String> relatedTopicPaths,
|
||||
ActivationProfile activationProfile) {
|
||||
String normalizedPath = normalizeTopicPath(topicPath);
|
||||
if (normalizedPath.isBlank()) {
|
||||
return;
|
||||
}
|
||||
ensureTopicNode(normalizedPath).addBinding(
|
||||
sliceRef,
|
||||
timestamp,
|
||||
relatedTopicPaths,
|
||||
normalizeActivationProfile(activationProfile)
|
||||
);
|
||||
}
|
||||
|
||||
void ensureTopicPaths(Collection<String> topicPaths) {
|
||||
if (topicPaths == null || topicPaths.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (String topicPath : topicPaths) {
|
||||
ensureTopicNode(topicPath);
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
topicSlices.clear();
|
||||
}
|
||||
|
||||
void ensureTopicPath(String topicPath) {
|
||||
String normalizedPath = normalizeTopicPath(topicPath);
|
||||
if (normalizedPath.isBlank()) {
|
||||
return;
|
||||
}
|
||||
ensureTopicNode(normalizedPath);
|
||||
}
|
||||
|
||||
TopicTreeNode findTopicNode(String topicPath) {
|
||||
String normalizedPath = normalizeTopicPath(topicPath);
|
||||
if (normalizedPath.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String[] parts = normalizedPath.split("->");
|
||||
TopicTreeNode current = topicSlices.get(parts[0]);
|
||||
for (int i = 1; current != null && i < parts.length; i++) {
|
||||
current = current.children().get(parts[i]);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
String getTopicTree() {
|
||||
List<String> lines = new ArrayList<>();
|
||||
for (Map.Entry<String, TopicTreeNode> entry : topicSlices.entrySet()) {
|
||||
collectTopicTreeLines(entry.getKey(), entry.getValue(), lines);
|
||||
}
|
||||
return String.join("\r\n", lines);
|
||||
}
|
||||
|
||||
List<String> normalizeTopicPaths(Collection<String> topicPaths) {
|
||||
if (topicPaths == null || topicPaths.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
LinkedHashSet<String> normalized = new LinkedHashSet<>();
|
||||
for (String topicPath : topicPaths) {
|
||||
String normalizedPath = normalizeTopicPath(topicPath);
|
||||
if (!normalizedPath.isBlank()) {
|
||||
normalized.add(normalizedPath);
|
||||
}
|
||||
}
|
||||
return List.copyOf(normalized);
|
||||
}
|
||||
|
||||
String normalizeTopicPath(String topicPath) {
|
||||
return topicPath == null ? "" : topicPath.trim();
|
||||
}
|
||||
|
||||
Map<String, TopicTreeNode> roots() {
|
||||
return topicSlices;
|
||||
}
|
||||
|
||||
private TopicTreeNode ensureTopicNode(String topicPath) {
|
||||
String[] parts = topicPath.split("->");
|
||||
TopicTreeNode current = topicSlices.computeIfAbsent(parts[0], ignored -> new TopicTreeNode(null));
|
||||
for (int i = 1; i < parts.length; i++) {
|
||||
TopicTreeNode parent = current;
|
||||
current = current.children.computeIfAbsent(parts[i], ignored -> new TopicTreeNode(parent));
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private void collectTopicTreeLines(String path, TopicTreeNode node, List<String> lines) {
|
||||
if (node.parent() == null) {
|
||||
lines.add(path + " [root]");
|
||||
} else {
|
||||
lines.add(path + " {slices: " + node.bindings().size() + "}");
|
||||
}
|
||||
for (Map.Entry<String, TopicTreeNode> childEntry : node.children().entrySet()) {
|
||||
collectTopicTreeLines(path + "->" + childEntry.getKey(), childEntry.getValue(), lines);
|
||||
}
|
||||
}
|
||||
|
||||
private ActivationProfile normalizeActivationProfile(ActivationProfile activationProfile) {
|
||||
ActivationProfile profile = activationProfile == null ? defaultActivationProfile() : new ActivationProfile(
|
||||
activationProfile.getActivationWeight(),
|
||||
activationProfile.getDiffusionWeight(),
|
||||
activationProfile.getContextIndependenceWeight()
|
||||
);
|
||||
profile.setActivationWeight(clampOrDefault(profile.getActivationWeight(), DEFAULT_ACTIVATION_WEIGHT));
|
||||
profile.setDiffusionWeight(clampOrDefault(profile.getDiffusionWeight(), DEFAULT_DIFFUSION_WEIGHT));
|
||||
profile.setContextIndependenceWeight(clampOrDefault(
|
||||
profile.getContextIndependenceWeight(),
|
||||
DEFAULT_CONTEXT_INDEPENDENCE_WEIGHT
|
||||
));
|
||||
return profile;
|
||||
}
|
||||
|
||||
private ActivationProfile defaultActivationProfile() {
|
||||
return new ActivationProfile(
|
||||
DEFAULT_ACTIVATION_WEIGHT,
|
||||
DEFAULT_DIFFUSION_WEIGHT,
|
||||
DEFAULT_CONTEXT_INDEPENDENCE_WEIGHT
|
||||
);
|
||||
}
|
||||
|
||||
private float clampOrDefault(Float value, float defaultValue) {
|
||||
return value == null ? defaultValue : clamp(value);
|
||||
}
|
||||
|
||||
private float clamp(float value) {
|
||||
return Math.clamp(value, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
static final class TopicTreeNode {
|
||||
private final TopicTreeNode parent;
|
||||
private final Map<String, TopicTreeNode> children = new LinkedHashMap<>();
|
||||
private final CopyOnWriteArrayList<TopicBinding> bindings = new CopyOnWriteArrayList<>();
|
||||
|
||||
private TopicTreeNode(TopicTreeNode parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
TopicTreeNode parent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
Map<String, TopicTreeNode> children() {
|
||||
return children;
|
||||
}
|
||||
|
||||
List<TopicBinding> bindings() {
|
||||
return bindings;
|
||||
}
|
||||
|
||||
private void addBinding(SliceRef sliceRef,
|
||||
long timestamp,
|
||||
Collection<String> relatedTopicPaths,
|
||||
ActivationProfile activationProfile) {
|
||||
for (TopicBinding binding : bindings) {
|
||||
if (Objects.equals(binding.sliceRef().getUnitId(), sliceRef.getUnitId())
|
||||
&& Objects.equals(binding.sliceRef().getSliceId(), sliceRef.getSliceId())) {
|
||||
binding.refresh(timestamp, relatedTopicPaths, activationProfile);
|
||||
return;
|
||||
}
|
||||
}
|
||||
bindings.add(new TopicBinding(sliceRef, timestamp, relatedTopicPaths, activationProfile));
|
||||
}
|
||||
}
|
||||
|
||||
static final class TopicBinding {
|
||||
private final SliceRef sliceRef;
|
||||
private final CopyOnWriteArrayList<String> relatedTopicPaths = new CopyOnWriteArrayList<>();
|
||||
private long timestamp;
|
||||
private ActivationProfile activationProfile;
|
||||
|
||||
private TopicBinding(SliceRef sliceRef,
|
||||
long timestamp,
|
||||
Collection<String> relatedTopicPaths,
|
||||
ActivationProfile activationProfile) {
|
||||
this.sliceRef = sliceRef;
|
||||
this.timestamp = timestamp;
|
||||
this.activationProfile = activationProfile;
|
||||
mergeRelatedTopicPaths(relatedTopicPaths);
|
||||
}
|
||||
|
||||
SliceRef sliceRef() {
|
||||
return sliceRef;
|
||||
}
|
||||
|
||||
long timestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
ActivationProfile activationProfile() {
|
||||
return activationProfile;
|
||||
}
|
||||
|
||||
List<String> relatedTopicPaths() {
|
||||
return relatedTopicPaths;
|
||||
}
|
||||
|
||||
private void refresh(long timestamp,
|
||||
Collection<String> relatedTopicPaths,
|
||||
ActivationProfile activationProfile) {
|
||||
this.timestamp = timestamp;
|
||||
this.activationProfile = activationProfile;
|
||||
mergeRelatedTopicPaths(relatedTopicPaths);
|
||||
}
|
||||
|
||||
private void mergeRelatedTopicPaths(Collection<String> relatedTopicPaths) {
|
||||
if (relatedTopicPaths == null) {
|
||||
return;
|
||||
}
|
||||
for (String relatedTopicPath : relatedTopicPaths) {
|
||||
if (relatedTopicPath != null && !relatedTopicPath.isBlank()) {
|
||||
this.relatedTopicPaths.addIfAbsent(relatedTopicPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package work.slhaf.partner.module.memory.runtime;
|
||||
|
||||
import work.slhaf.partner.core.memory.pojo.SliceRef;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
final class TopicRecallCollector {
|
||||
|
||||
private static final int TOPIC_RESULT_LIMIT = 5;
|
||||
private static final int PARENT_CANDIDATE_LIMIT = 2;
|
||||
private static final int RELATED_CANDIDATE_LIMIT = 2;
|
||||
|
||||
private final TopicRecallScorer scorer;
|
||||
|
||||
TopicRecallCollector(TopicRecallScorer scorer) {
|
||||
this.scorer = scorer;
|
||||
}
|
||||
|
||||
List<SliceRef> collect(TopicMemoryIndex topicIndex, TopicMemoryIndex.TopicTreeNode topicNode) {
|
||||
LinkedHashMap<String, ScoredSliceCandidate> candidates = new LinkedHashMap<>();
|
||||
LinkedHashMap<String, Float> relatedTopicPaths = new LinkedHashMap<>();
|
||||
collectTopicCandidates(
|
||||
topicNode,
|
||||
TopicRecallScorer.CandidateSource.PRIMARY,
|
||||
Integer.MAX_VALUE,
|
||||
candidates,
|
||||
relatedTopicPaths
|
||||
);
|
||||
collectTopicCandidates(
|
||||
topicNode.parent(),
|
||||
TopicRecallScorer.CandidateSource.PARENT,
|
||||
PARENT_CANDIDATE_LIMIT,
|
||||
candidates,
|
||||
null
|
||||
);
|
||||
for (Map.Entry<String, Float> relatedTopicEntry : relatedTopicPaths.entrySet()) {
|
||||
if (relatedTopicEntry.getValue() <= 0.0f) {
|
||||
continue;
|
||||
}
|
||||
collectTopicCandidates(
|
||||
topicIndex.findTopicNode(relatedTopicEntry.getKey()),
|
||||
TopicRecallScorer.CandidateSource.RELATED,
|
||||
RELATED_CANDIDATE_LIMIT,
|
||||
candidates,
|
||||
null
|
||||
);
|
||||
}
|
||||
return candidates.values().stream()
|
||||
.sorted(Comparator.comparingDouble(ScoredSliceCandidate::score)
|
||||
.reversed()
|
||||
.thenComparing(Comparator.comparingLong(ScoredSliceCandidate::timestamp).reversed()))
|
||||
.limit(TOPIC_RESULT_LIMIT)
|
||||
.map(ScoredSliceCandidate::sliceRef)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void collectTopicCandidates(TopicMemoryIndex.TopicTreeNode topicNode,
|
||||
TopicRecallScorer.CandidateSource source,
|
||||
int limit,
|
||||
LinkedHashMap<String, ScoredSliceCandidate> candidates,
|
||||
Map<String, Float> relatedTopicPaths) {
|
||||
if (topicNode == null || topicNode.bindings().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<TopicMemoryIndex.TopicBinding> bindings = new ArrayList<>(topicNode.bindings());
|
||||
bindings.sort(Comparator.comparingLong(TopicMemoryIndex.TopicBinding::timestamp).reversed());
|
||||
int actualLimit = limit == Integer.MAX_VALUE ? bindings.size() : Math.min(limit, bindings.size());
|
||||
for (int i = 0; i < actualLimit; i++) {
|
||||
TopicMemoryIndex.TopicBinding binding = bindings.get(i);
|
||||
if (relatedTopicPaths != null) {
|
||||
for (String relatedTopicPath : binding.relatedTopicPaths()) {
|
||||
relatedTopicPaths.merge(
|
||||
relatedTopicPath,
|
||||
binding.activationProfile().getDiffusionWeight(),
|
||||
Math::max
|
||||
);
|
||||
}
|
||||
}
|
||||
double score = scorer.score(binding, source);
|
||||
String key = binding.sliceRef().getUnitId() + ":" + binding.sliceRef().getSliceId();
|
||||
ScoredSliceCandidate current = candidates.get(key);
|
||||
if (current == null || score > current.score()) {
|
||||
candidates.put(key, new ScoredSliceCandidate(binding.sliceRef(), binding.timestamp(), score));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record ScoredSliceCandidate(SliceRef sliceRef, long timestamp, double score) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package work.slhaf.partner.module.memory.runtime;
|
||||
|
||||
import work.slhaf.partner.module.memory.pojo.ActivationProfile;
|
||||
|
||||
final class TopicRecallScorer {
|
||||
|
||||
double score(TopicMemoryIndex.TopicBinding binding, CandidateSource source) {
|
||||
ActivationProfile profile = binding.activationProfile();
|
||||
return source.sourceScore
|
||||
+ recencyScore(binding.timestamp())
|
||||
+ 0.50d * profile.getActivationWeight()
|
||||
+ 0.30d * profile.getContextIndependenceWeight()
|
||||
+ 0.20d * source.relationFactor * profile.getDiffusionWeight();
|
||||
}
|
||||
|
||||
private double recencyScore(long timestamp) {
|
||||
long ageMillis = Math.max(0L, System.currentTimeMillis() - timestamp);
|
||||
long ageDays = ageMillis / 86_400_000L;
|
||||
if (ageDays <= 1) {
|
||||
return 0.30d;
|
||||
}
|
||||
if (ageDays <= 3) {
|
||||
return 0.22d;
|
||||
}
|
||||
if (ageDays <= 7) {
|
||||
return 0.15d;
|
||||
}
|
||||
if (ageDays <= 30) {
|
||||
return 0.08d;
|
||||
}
|
||||
return 0.00d;
|
||||
}
|
||||
|
||||
enum CandidateSource {
|
||||
PRIMARY(1.00f, 0.30f),
|
||||
RELATED(0.65f, 1.00f),
|
||||
PARENT(0.45f, 0.20f);
|
||||
|
||||
private final float sourceScore;
|
||||
private final float relationFactor;
|
||||
|
||||
CandidateSource(float sourceScore, float relationFactor) {
|
||||
this.sourceScore = sourceScore;
|
||||
this.relationFactor = relationFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,6 +177,6 @@ public class MemoryRecallCueExtractor extends AbstractAgentModule.Sub<ExtractorI
|
||||
@NotNull
|
||||
@Override
|
||||
public String modelKey() {
|
||||
return "topic_extractor";
|
||||
return "memory_recall_cue_extractor";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ package work.slhaf.partner.module.memory.updater;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import work.slhaf.partner.core.cognition.CognitionCapability;
|
||||
import work.slhaf.partner.core.cognition.ContextBlock;
|
||||
import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability;
|
||||
import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.Init;
|
||||
import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule;
|
||||
@@ -16,6 +13,7 @@ import work.slhaf.partner.module.TaskBlock;
|
||||
import work.slhaf.partner.module.communication.AfterRolling;
|
||||
import work.slhaf.partner.module.communication.AfterRollingRegistry;
|
||||
import work.slhaf.partner.module.communication.RollingResult;
|
||||
import work.slhaf.partner.module.memory.pojo.ActivationProfile;
|
||||
import work.slhaf.partner.module.memory.runtime.MemoryRuntime;
|
||||
import work.slhaf.partner.module.memory.updater.summarizer.entity.MemoryTopicResult;
|
||||
|
||||
@@ -23,8 +21,11 @@ import java.util.List;
|
||||
|
||||
public class MemoryUpdater extends AbstractAgentModule.Standalone implements AfterRolling, ActivateModel {
|
||||
|
||||
@InjectCapability
|
||||
private CognitionCapability cognitionCapability;
|
||||
private static final float DEFAULT_ACTIVATION_WEIGHT = 0.55f;
|
||||
private static final float DEFAULT_DIFFUSION_WEIGHT = 0.35f;
|
||||
private static final float DEFAULT_CONTEXT_INDEPENDENCE_WEIGHT = 0.50f;
|
||||
private static final float NO_RELATED_DIFFUSION_CAP = 0.45f;
|
||||
private static final float SINGLE_MESSAGE_ACTIVATION_PENALTY = 0.05f;
|
||||
|
||||
@InjectModule
|
||||
private MemoryRuntime memoryRuntime;
|
||||
@@ -44,10 +45,6 @@ public class MemoryUpdater extends AbstractAgentModule.Standalone implements Aft
|
||||
}
|
||||
Result<MemoryTopicResult> extractResult = formattedChat(
|
||||
List.of(
|
||||
cognitionCapability.contextWorkspace().resolve(List.of(
|
||||
ContextBlock.FocusedDomain.COGNITION,
|
||||
ContextBlock.FocusedDomain.MEMORY
|
||||
)).encodeToMessage(),
|
||||
resolveTopicTaskMessage(result, slicedMessages)
|
||||
),
|
||||
MemoryTopicResult.class
|
||||
@@ -57,8 +54,18 @@ public class MemoryUpdater extends AbstractAgentModule.Standalone implements Aft
|
||||
List<String> relatedTopicPaths = topicResult.getRelatedTopicPaths() == null
|
||||
? List.of()
|
||||
: topicResult.getRelatedTopicPaths().stream().map(memoryRuntime::fixTopicPath).toList();
|
||||
memoryRuntime.recordMemory(result.memoryUnit(), topicPath, relatedTopicPaths);
|
||||
}).onFailure(exp -> memoryRuntime.recordMemory(result.memoryUnit(), null, List.of()));
|
||||
ActivationProfile activationProfile = stabilizeActivationProfile(
|
||||
topicResult.getActivationProfile(),
|
||||
relatedTopicPaths,
|
||||
slicedMessages
|
||||
);
|
||||
memoryRuntime.recordMemory(result.memoryUnit(), topicPath, relatedTopicPaths, activationProfile);
|
||||
}).onFailure(exp -> memoryRuntime.recordMemory(
|
||||
result.memoryUnit(),
|
||||
null,
|
||||
List.of(),
|
||||
defaultActivationProfile()
|
||||
));
|
||||
}
|
||||
|
||||
private List<Message> sliceMessages(RollingResult result) {
|
||||
@@ -91,4 +98,44 @@ public class MemoryUpdater extends AbstractAgentModule.Standalone implements Aft
|
||||
public String modelKey() {
|
||||
return "topic_extractor";
|
||||
}
|
||||
|
||||
private ActivationProfile stabilizeActivationProfile(ActivationProfile activationProfile,
|
||||
List<String> relatedTopicPaths,
|
||||
List<Message> slicedMessages) {
|
||||
ActivationProfile profile = activationProfile == null ? defaultActivationProfile() : new ActivationProfile(
|
||||
activationProfile.getActivationWeight(),
|
||||
activationProfile.getDiffusionWeight(),
|
||||
activationProfile.getContextIndependenceWeight()
|
||||
);
|
||||
profile.setActivationWeight(clampOrDefault(profile.getActivationWeight(), DEFAULT_ACTIVATION_WEIGHT));
|
||||
profile.setDiffusionWeight(clampOrDefault(profile.getDiffusionWeight(), DEFAULT_DIFFUSION_WEIGHT));
|
||||
profile.setContextIndependenceWeight(clampOrDefault(
|
||||
profile.getContextIndependenceWeight(),
|
||||
DEFAULT_CONTEXT_INDEPENDENCE_WEIGHT
|
||||
));
|
||||
|
||||
if (relatedTopicPaths.isEmpty()) {
|
||||
profile.setDiffusionWeight(Math.min(profile.getDiffusionWeight(), NO_RELATED_DIFFUSION_CAP));
|
||||
}
|
||||
if (slicedMessages.size() <= 1) {
|
||||
profile.setActivationWeight(clamp(profile.getActivationWeight() - SINGLE_MESSAGE_ACTIVATION_PENALTY));
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
private ActivationProfile defaultActivationProfile() {
|
||||
return new ActivationProfile(
|
||||
DEFAULT_ACTIVATION_WEIGHT,
|
||||
DEFAULT_DIFFUSION_WEIGHT,
|
||||
DEFAULT_CONTEXT_INDEPENDENCE_WEIGHT
|
||||
);
|
||||
}
|
||||
|
||||
private float clampOrDefault(Float value, float defaultValue) {
|
||||
return value == null ? defaultValue : clamp(value);
|
||||
}
|
||||
|
||||
private float clamp(float value) {
|
||||
return Math.clamp(value, 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package work.slhaf.partner.module.memory.updater.summarizer.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import work.slhaf.partner.module.memory.pojo.ActivationProfile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -8,4 +9,5 @@ import java.util.List;
|
||||
public class MemoryTopicResult {
|
||||
private String topicPath;
|
||||
private List<String> relatedTopicPaths;
|
||||
private ActivationProfile activationProfile;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user