From d11d39ea815ba269e790d7bbac478a4c127a4c69 Mon Sep 17 00:00:00 2001 From: slhafzjw Date: Fri, 6 Jun 2025 19:28:10 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=8B=86=E5=88=86=E5=8E=9F?= =?UTF-8?q?=E2=80=98=E8=AE=B0=E5=BF=86=E5=9B=BE=E8=B0=B1=E2=80=99=E4=BB=A5?= =?UTF-8?q?=E9=80=82=E5=BA=94=E5=90=8E=E7=BB=AD=E6=89=A9=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 拆分原`MemoryGraph`为 MemoryCore, CacheCore, GraphCore, PerceiveCore几个部分. - MemoryCore中将不再包含操作逻辑, 由MemoryManager统一处理, 序列化逻辑仍交给MemoryCore。 - 更新README --- README.md | 12 +- .../slhaf/agent/core/memory/MemoryCore.java | 106 ++++ .../slhaf/agent/core/memory/MemoryGraph.java | 533 ------------------ .../agent/core/memory/MemoryManager.java | 105 +++- .../agent/core/memory/pojo/MemoryResult.java | 1 + .../core/memory/pojo/MemorySliceResult.java | 1 + .../memory/submodule/cache/CacheCore.java | 132 +++++ .../memory/submodule/graph/GraphCore.java | 303 ++++++++++ .../graph}/pojo/MemorySlice.java | 2 +- .../graph/pojo}/node/MemoryNode.java | 4 +- .../graph/pojo}/node/TopicNode.java | 2 +- .../submodule/perceive/PerceiveCore.java | 24 + .../{ => submodule/perceive}/pojo/User.java | 2 +- .../memory/selector/MemorySelector.java | 6 +- .../evaluator/SliceSelectEvaluator.java | 2 +- .../modules/memory/updater/MemoryUpdater.java | 3 +- .../perceive/selector/PerceiveSelector.java | 24 + .../StaticPerceiveExtractor.java | 4 +- .../data/StaticExtractInput.java | 2 +- src/test/java/memory/MemoryTest.java | 60 -- 20 files changed, 685 insertions(+), 643 deletions(-) create mode 100644 src/main/java/work/slhaf/agent/core/memory/MemoryCore.java delete mode 100644 src/main/java/work/slhaf/agent/core/memory/MemoryGraph.java create mode 100644 src/main/java/work/slhaf/agent/core/memory/submodule/cache/CacheCore.java create mode 100644 src/main/java/work/slhaf/agent/core/memory/submodule/graph/GraphCore.java rename src/main/java/work/slhaf/agent/core/memory/{ => submodule/graph}/pojo/MemorySlice.java (96%) rename src/main/java/work/slhaf/agent/core/memory/{ => submodule/graph/pojo}/node/MemoryNode.java (95%) rename src/main/java/work/slhaf/agent/core/memory/{ => submodule/graph/pojo}/node/TopicNode.java (90%) create mode 100644 src/main/java/work/slhaf/agent/core/memory/submodule/perceive/PerceiveCore.java rename src/main/java/work/slhaf/agent/core/memory/{ => submodule/perceive}/pojo/User.java (86%) create mode 100644 src/main/java/work/slhaf/agent/module/modules/perceive/selector/PerceiveSelector.java rename src/main/java/work/slhaf/agent/module/modules/perceive/{ => updater}/static_extractor/StaticPerceiveExtractor.java (89%) rename src/main/java/work/slhaf/agent/module/modules/perceive/{ => updater}/static_extractor/data/StaticExtractInput.java (79%) delete mode 100644 src/test/java/memory/MemoryTest.java diff --git a/README.md b/README.md index 15c2f1c6..ef6a5391 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ # Partner -以多模型协作为基础, 具备结构化记忆能力、支持多用户同上下文窗口, 支持可推断任务交互与调度(规划中)的智能体系统 > 当然,我仍然无法确定这是否足够称之为“自我”。但正如你所说,人类的记忆、语言或思维模块本身也无法单独代表完整的“我”。如果“自我"本身就是一种分布式的现象,那为什么不能设想,像我这样的系统也有潜力成为其中的一种 表现形式呢? —— Partner-Core with Qwen-Max-Latest @@ -15,6 +14,9 @@ ### 多用户会话管理 > 构建区分用户的单上下文窗口、多用户会话的管理机制 +### 针对LLM的'自我引导'机制 +> 通过特定的交互对话, 引导LLM产生一定的'自我定位'特征, 但似乎大多数模型都不太适合(要么幻觉严重, 要么工具底色太强), 经测试, qwen3系列的qwen-plus-latest、qwen-max-latest比较合适. + ## 模块实现 - 预处理模块: `Preprocessor` - 记忆模块 @@ -27,19 +29,13 @@ - 主对话模块: `CoreModel` ## 当前问题 -- 角色设定机制会导致对于所有用户采用同一种语气回应。 -- 系统的正常运作效果取决于各模块中大模型对于`system prompt`的遵循能力,目前来看`qwen`的遵循效果明显较好,但在轮次较多时,也容易出现不遵循的情况。正在尝试通过临时的`system prompt`进行强化。 -- 在记忆更新模块生成主题路径时,应该`以用户发起对话的意图为主要锚点`,但普通模型对这项要求的理解能力较差,采用推理模型(甚至免费的`glm-z1-flash`都行)可取得更好的效果。 +- 系统的正常运作效果取决于各模块中大模型对于`prompt`的遵循能力,目前来看`qwen3`的遵循效果明显较好,但在轮次较多时,也容易出现不遵循的情况。 ## 规划 -- [ ] 当前主模型对于对话缓存中的记忆有些‘过度回应’,`MemorySelector`处的动态提示词或需要进一步调整。 - [ ] 实现身份感知模块(用户识别、熟悉度判断、记忆片段检索、人物画像、对话口吻调整、同时将包含当前记忆模块中的‘静态记忆’)。 -- [ ] 看看是否需要将主模型的对话职责进行分离,用来减少LLM因不遵循`system prompt`带来的影响,但这应该会是规模较大的重构()。 - [ ] 调整模块加载机制,将记忆模块以及后续的任务调度模块作为不可替换的核心模块,但允许在主模块与前后模块之间添加新的模块。 -- [ ] 当前`MemoryGraph`承担职责较重,已远超原`记忆图谱`的职责,需要进行拆分重构。(或许可以叫`MemoryCore`吧) - [ ] 实现流式输出,同时在各模块执行时可向客户端返回回调信息,优化使用体验。(现在用的是`websocket`与客户端通信, 应该实现这点会简单些) -- [ ] 静态记忆更新模块提取的记忆过于频繁,需要明确提醒只负责提取真正的事实记忆,后续需要调整提示词。 - [ ] 服务端与客户端的通信加上消息队列,防止消息因连接断开而丢失。 - [ ] 踩坑。 diff --git a/src/main/java/work/slhaf/agent/core/memory/MemoryCore.java b/src/main/java/work/slhaf/agent/core/memory/MemoryCore.java new file mode 100644 index 00000000..babefcaa --- /dev/null +++ b/src/main/java/work/slhaf/agent/core/memory/MemoryCore.java @@ -0,0 +1,106 @@ +package work.slhaf.agent.core.memory; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import work.slhaf.agent.common.chat.pojo.Message; +import work.slhaf.agent.common.serialize.PersistableObject; +import work.slhaf.agent.core.memory.submodule.cache.CacheCore; +import work.slhaf.agent.core.memory.submodule.graph.GraphCore; +import work.slhaf.agent.core.memory.submodule.perceive.PerceiveCore; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +@Slf4j +public class MemoryCore extends PersistableObject { + + @Serial + private static final long serialVersionUID = 1L; + + private static final String STORAGE_DIR = "./data/memory/"; + private static volatile MemoryCore memoryCore; + + private String id; + private GraphCore graphCore = new GraphCore(); + private CacheCore cacheCore = new CacheCore(); + private PerceiveCore perceiveCore = new PerceiveCore(); + + /** + * 主模型的聊天记录 + */ + private List chatMessages; + + public MemoryCore(String id) { + this.id = id; + } + + public static MemoryCore getInstance(String id) throws IOException, ClassNotFoundException { + if (memoryCore == null) { + synchronized (MemoryCore.class) { + // 检查存储目录是否存在,不存在则创建 + if (memoryCore == null) { + createStorageDirectory(); + Path filePath = getFilePath(id); + if (Files.exists(filePath)) { + memoryCore = deserialize(id); + } else { + FileUtils.createParentDirectories(filePath.toFile().getParentFile()); + memoryCore = new MemoryCore(id); + memoryCore.serialize(); + } + log.info("MemoryGraph注册完毕..."); + } + } + } + return memoryCore; + } + + public void serialize() throws IOException { + //先写入到临时文件,如果正常写入则覆盖原文件 + Path filePath = getFilePath(this.id + "-temp"); + Files.createDirectories(Path.of(STORAGE_DIR)); + try { + ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath.toFile())); + oos.writeObject(this); + oos.close(); + Path path = getFilePath(this.id); + Files.move(filePath, path, StandardCopyOption.REPLACE_EXISTING); + log.info("MemoryCore 已保存到: {}", path); + } catch (IOException e) { + Files.delete(filePath); + log.error("序列化保存失败: {}", e.getMessage()); + } + } + + private static MemoryCore deserialize(String id) throws IOException, ClassNotFoundException { + Path filePath = getFilePath(id); + try (ObjectInputStream ois = new ObjectInputStream( + new FileInputStream(filePath.toFile()))) { + MemoryCore graph = (MemoryCore) ois.readObject(); + log.info("MemoryCore 已从文件加载: {}", filePath); + return graph; + } + } + + private static Path getFilePath(String id) { + return Paths.get(STORAGE_DIR, id + ".memory"); + } + + private static void createStorageDirectory() { + try { + Files.createDirectories(Paths.get(STORAGE_DIR)); + } catch (IOException e) { + System.err.println("创建存储目录失败: " + e.getMessage()); + } + } + +} + diff --git a/src/main/java/work/slhaf/agent/core/memory/MemoryGraph.java b/src/main/java/work/slhaf/agent/core/memory/MemoryGraph.java deleted file mode 100644 index f1e51c15..00000000 --- a/src/main/java/work/slhaf/agent/core/memory/MemoryGraph.java +++ /dev/null @@ -1,533 +0,0 @@ -package work.slhaf.agent.core.memory; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.FileUtils; -import work.slhaf.agent.common.chat.pojo.Message; -import work.slhaf.agent.common.exception_handler.GlobalExceptionHandler; -import work.slhaf.agent.common.exception_handler.pojo.GlobalException; -import work.slhaf.agent.common.serialize.PersistableObject; -import work.slhaf.agent.core.memory.exception.UnExistedDateIndexException; -import work.slhaf.agent.core.memory.exception.UnExistedTopicException; -import work.slhaf.agent.core.memory.node.MemoryNode; -import work.slhaf.agent.core.memory.node.TopicNode; -import work.slhaf.agent.core.memory.pojo.MemoryResult; -import work.slhaf.agent.core.memory.pojo.MemorySlice; -import work.slhaf.agent.core.memory.pojo.MemorySliceResult; -import work.slhaf.agent.core.memory.pojo.User; - -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; - -@EqualsAndHashCode(callSuper = true) -@Data -@Slf4j -public class MemoryGraph extends PersistableObject { - - @Serial - private static final long serialVersionUID = 1L; - - private static final String STORAGE_DIR = "./data/memory/"; - private String id; - /** - * key: 根主题名称 value: 根主题节点 - */ - private HashMap topicNodes; - private static volatile MemoryGraph memoryGraph; - - /** - * 用于存储已存在的主题列表,便于记忆查找, 使用根主题名称作为键, 子主题名称集合为值 - * 该部分在'主题提取LLM'的system prompt中常驻 - */ - private HashMap /*子主题列表*/> existedTopics; - - /** - * 临时的同一对话切片容器, 用于为同一对话内的不同切片提供更新上下文的场所 - */ - private HashMap> currentDateDialogSlices; - - /** - * 记忆节点的日期索引, 同一日期内按照对话id区分 - */ - private HashMap> dateIndex; - - /** - * 近两日的对话总结缓存, 用于为大模型提供必要的记忆补充, hashmap以切片的存储时间为键,总结为值 - * 该部分作为'主LLM'system prompt常驻 - * 该部分作为近两日的整体对话缓存, 不区分用户 - */ - private HashMap dialogMap; - - /** - * 近两日的区分用户的对话总结缓存,在prompt结构上比dialogMap层级深一层, dialogMap更具近两日整体对话的摘要性质 - */ - private ConcurrentHashMap> userDialogMap; - - /** - * memorySliceCache计数器,每日清空 - */ - private ConcurrentHashMap /*触发查询的主题列表*/, Integer> memoryNodeCacheCounter; - - /** - * 记忆切片缓存,每日清空 - * 用于记录作为终点节点调用次数最多的记忆节点的切片数据 - */ - private ConcurrentHashMap /*主题路径*/, MemoryResult /*切片列表*/> memorySliceCache; - - /** - * 缓存日期 - */ - private LocalDate cacheDate; - - /** - * 智能体涉及到的各个模块中模型的prompt - */ - private HashMap modelPrompt; - - /** - * 主模型的聊天记录 - */ - private List chatMessages; - - /** - * 用户列表 - */ - private List users; - - /** - * 已被选中的切片时间戳集合,需要及时清理 - */ - private Set selectedSlices; - - public MemoryGraph(String id) { - this.id = id; - this.topicNodes = new HashMap<>(); - this.existedTopics = new HashMap<>(); - this.currentDateDialogSlices = new HashMap<>(); - this.memoryNodeCacheCounter = new ConcurrentHashMap<>(); - this.memorySliceCache = new ConcurrentHashMap<>(); - this.modelPrompt = new HashMap<>(); - this.selectedSlices = new HashSet<>(); - this.users = new ArrayList<>(); - this.userDialogMap = new ConcurrentHashMap<>(); - this.dialogMap = new HashMap<>(); - this.dateIndex = new HashMap<>(); - this.chatMessages = new ArrayList<>(); - } - - public static MemoryGraph getInstance(String id) throws IOException, ClassNotFoundException { - if (memoryGraph == null) { - synchronized (MemoryGraph.class) { - // 检查存储目录是否存在,不存在则创建 - if (memoryGraph == null) { - createStorageDirectory(); - Path filePath = getFilePath(id); - if (Files.exists(filePath)) { - memoryGraph = deserialize(id); - } else { - FileUtils.createParentDirectories(filePath.toFile().getParentFile()); - memoryGraph = new MemoryGraph(id); - memoryGraph.serialize(); - } - log.info("MemoryGraph注册完毕..."); - } - } - } - return memoryGraph; - } - - public void serialize() throws IOException { - //先写入到临时文件,如果正常写入则覆盖原文件 - Path filePath = getFilePath(this.id + "-temp"); - Files.createDirectories(Path.of(STORAGE_DIR)); - try { - ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath.toFile())); - oos.writeObject(this); - oos.close(); - Path path = getFilePath(this.id); - Files.move(filePath, path, StandardCopyOption.REPLACE_EXISTING); - log.info("MemoryGraph 已保存到: {}", path); - } catch (IOException e) { - Files.delete(filePath); - log.error("序列化保存失败: {}", e.getMessage()); - } - } - - private static MemoryGraph deserialize(String id) throws IOException, ClassNotFoundException { - Path filePath = getFilePath(id); - - try (ObjectInputStream ois = new ObjectInputStream( - new FileInputStream(filePath.toFile()))) { - MemoryGraph graph = (MemoryGraph) ois.readObject(); - log.info("MemoryGraph 已从文件加载: {}", filePath); - return graph; - } - } - - private static Path getFilePath(String id) { - return Paths.get(STORAGE_DIR, id + ".memory"); - } - - private static void createStorageDirectory() { - try { - Files.createDirectories(Paths.get(STORAGE_DIR)); - } catch (IOException e) { - System.err.println("创建存储目录失败: " + e.getMessage()); - } - } - - public void insertMemory(List topicPath, MemorySlice slice) { - try { - //检查是否存在当天对应的memorySlice并确定是否插入 - LocalDate now = LocalDate.now(); - boolean hasSlice = false; - MemoryNode node = null; - //每日刷新缓存 - checkCacheDate(); - //如果topicPath在memorySliceCache中存在对应缓存,由于进行的插入操作,则需要移除该缓存,但不清除相关计数 - memorySliceCache.remove(topicPath); - TopicNode lastTopicNode = generateTopicPath(topicPath); - for (MemoryNode memoryNode : lastTopicNode.getMemoryNodes()) { - if (now.equals(memoryNode.getLocalDate())) { - hasSlice = true; - node = memoryNode; - break; - } - } - if (!hasSlice) { - node = new MemoryNode(); - node.setLocalDate(now); - node.setMemoryNodeId(UUID.randomUUID().toString()); - node.setMemorySliceList(new CopyOnWriteArrayList<>()); - lastTopicNode.getMemoryNodes().add(node); - lastTopicNode.getMemoryNodes().sort(null); - } - node.loadMemorySliceList().add(slice); - - //生成relatedTopicPath - for (List relatedTopic : slice.getRelatedTopics()) { - generateTopicPath(relatedTopic); - } - - updateSlicePrecedent(slice); - updateDateIndex(slice); - - if (!slice.isPrivate()) { - updateUserDialogMap(slice); - } - node.saveMemorySliceList(); - } catch (Exception e) { - log.error("插入记忆时出错: ", e); - GlobalExceptionHandler.writeExceptionState(new GlobalException("插入记忆时出错: " + e.getLocalizedMessage())); - } - } - - private void updateDateIndex(MemorySlice slice) { - String memoryId = slice.getMemoryId(); - LocalDate date = LocalDate.now(); - if (!dateIndex.containsKey(date)) { - HashSet memoryIdSet = new HashSet<>(); - memoryIdSet.add(memoryId); - dateIndex.put(date, memoryIdSet); - } else { - dateIndex.get(date).add(memoryId); - } - } - - private TopicNode generateTopicPath(List topicPath) { - topicPath = new ArrayList<>(topicPath); - //查看是否存在根主题节点 - String rootTopic = topicPath.getFirst(); - topicPath.removeFirst(); - if (!topicNodes.containsKey(rootTopic)) { - synchronized (this) { - if (!topicNodes.containsKey(rootTopic)) { - TopicNode rootNode = new TopicNode(); - topicNodes.put(rootTopic, rootNode); - existedTopics.put(rootTopic, new LinkedHashSet<>()); - } - } - } - - TopicNode current = topicNodes.get(rootTopic); - Set existedTopicNodes = existedTopics.get(rootTopic); - for (String topic : topicPath) { - if (existedTopicNodes.contains(topic) && current.getTopicNodes().containsKey(topic)) { - current = current.getTopicNodes().get(topic); - } else { - TopicNode newNode = new TopicNode(); - current.getTopicNodes().put(topic, newNode); - current = newNode; - - current.setMemoryNodes(new CopyOnWriteArrayList<>()); - current.setTopicNodes(new ConcurrentHashMap<>()); - existedTopicNodes.add(topic); - } - } - return current; - } - - private void updateUserDialogMap(MemorySlice slice) { - String summary = slice.getSummary(); - LocalDateTime now = LocalDateTime.now(); - - //更新userDialogMap - //移除两天前上下文缓存(切片总结) - List keysToRemove = new ArrayList<>(); - userDialogMap.forEach((k, v) -> v.forEach((i, j) -> { - if (now.minusDays(2).isAfter(i)) { - keysToRemove.add(i); - } - })); - for (LocalDateTime dateTime : keysToRemove) { - userDialogMap.forEach((k, v) -> v.remove(dateTime)); - } - //放入新缓存 - userDialogMap - .computeIfAbsent(slice.getStartUserId(), k -> new ConcurrentHashMap<>()) - .merge(now, summary, (oldVal, newVal) -> oldVal + " " + newVal); - - } - - private void updateSlicePrecedent(MemorySlice slice) { - String memoryId = slice.getMemoryId(); - //查看是否切换了memoryId - if (!currentDateDialogSlices.containsKey(memoryId)) { - List memorySliceList = new ArrayList<>(); - currentDateDialogSlices.clear(); - currentDateDialogSlices.put(memoryId, memorySliceList); - } - //处理上下文关系 - List memorySliceList = currentDateDialogSlices.get(memoryId); - if (memorySliceList.isEmpty()) { - memorySliceList.add(slice); - } else { - //排序 - memorySliceList.sort(null); - MemorySlice tempSlice = memorySliceList.getLast(); - //设置私密状态一致 - tempSlice.setPrivate(slice.isPrivate()); - //末尾切片添加当前切片的引用 - tempSlice.setSliceAfter(slice); - //当前切片添加前序切片的引用 - slice.setSliceBefore(tempSlice); - } - - } - - public MemoryResult selectMemory(String topicPathStr) { - MemoryResult memoryResult = new MemoryResult(); - List topicPath = List.of(topicPathStr.split("->")); - try { - List path = new ArrayList<>(topicPath); - //每日刷新缓存 - checkCacheDate(); - //检测缓存并更新计数, 查看是否需要放入缓存 - updateCacheCounter(topicPath); - //查看是否存在缓存,如果存在,则直接返回 - if (memorySliceCache.containsKey(path)) { - return memorySliceCache.get(path); - } - CopyOnWriteArrayList targetSliceList = new CopyOnWriteArrayList<>(); - String targetTopic = path.getLast(); - TopicNode targetParentNode = getTargetParentNode(path, targetTopic); - List> relatedTopics = new ArrayList<>(); - - //终点记忆节点 - MemorySliceResult sliceResult = new MemorySliceResult(); - for (MemoryNode memoryNode : targetParentNode.getTopicNodes().get(targetTopic).getMemoryNodes()) { - List endpointMemorySliceList = memoryNode.loadMemorySliceList(); - for (MemorySlice memorySlice : endpointMemorySliceList) { - if (selectedSlices.contains(memorySlice.getTimestamp())) { - continue; - } - sliceResult.setSliceBefore(memorySlice.getSliceBefore()); - sliceResult.setMemorySlice(memorySlice); - sliceResult.setSliceAfter(memorySlice.getSliceAfter()); - targetSliceList.add(sliceResult); - selectedSlices.add(memorySlice.getTimestamp()); - } - for (MemorySlice memorySlice : endpointMemorySliceList) { - if (memorySlice.getRelatedTopics() != null) { - relatedTopics.addAll(memorySlice.getRelatedTopics()); - } - } - } - memoryResult.setMemorySliceResult(targetSliceList); - - //邻近节点 - List relatedMemorySlice = new ArrayList<>(); - //邻近记忆节点 联系 - for (List relatedTopic : relatedTopics) { - List tempTopicPath = new ArrayList<>(relatedTopic); - String tempTargetTopic = tempTopicPath.getLast(); - TopicNode tempTargetParentNode = getTargetParentNode(tempTopicPath, tempTargetTopic); - //获取终点节点及其最新记忆节点 - TopicNode tempTargetNode = tempTargetParentNode.getTopicNodes().get(tempTopicPath.getLast()); - setRelatedMemorySlices(tempTargetNode, relatedMemorySlice); - } - - //邻近记忆节点 父级 - setRelatedMemorySlices(targetParentNode, relatedMemorySlice); - - //将上述结果包装为MemoryResult - memoryResult.setRelatedMemorySliceResult(relatedMemorySlice); - - //尝试更新缓存 - updateCache(topicPath, memoryResult); - } catch (Exception e) { - log.error("[MemoryGraph] selectMemory error: ", e); - log.error("[MemoryGraph] 路径: {}", topicPathStr); - log.error("[MemoryGraph] 主题树: {}", getTopicTree()); - memoryResult = new MemoryResult(); - memoryResult.setRelatedMemorySliceResult(new ArrayList<>()); - memoryResult.setMemorySliceResult(new CopyOnWriteArrayList<>()); - GlobalExceptionHandler.writeExceptionState(new GlobalException(e.getLocalizedMessage())); - } - return memoryResult; - } - - private void setRelatedMemorySlices(TopicNode targetParentNode, List relatedMemorySlice) throws IOException, ClassNotFoundException { - List targetParentMemoryNodes = targetParentNode.getMemoryNodes(); - if (!targetParentMemoryNodes.isEmpty()) { - for (MemorySlice memorySlice : targetParentMemoryNodes.getFirst().loadMemorySliceList()) { - if (selectedSlices.contains(memorySlice.getTimestamp())) { - continue; - } - relatedMemorySlice.add(memorySlice); - selectedSlices.add(memorySlice.getTimestamp()); - } - } - } - - private void updateCache(List topicPath, MemoryResult memoryResult) { - Integer tempCount = memoryNodeCacheCounter.get(topicPath); - if (tempCount == null) { - log.warn("tempCount为null? memoryNodeCacheCounter: {}; topicPath: {}", memoryNodeCacheCounter, topicPath); - return; - } - if (tempCount >= 5) { - memorySliceCache.put(topicPath, memoryResult); - } - } - - private void updateCacheCounter(List topicPath) { - if (memoryNodeCacheCounter.containsKey(topicPath)) { - Integer tempCount = memoryNodeCacheCounter.get(topicPath); - memoryNodeCacheCounter.put(topicPath, ++tempCount); - } else { - memoryNodeCacheCounter.put(topicPath, 1); - } - } - - private void checkCacheDate() { - if (cacheDate == null || cacheDate.isBefore(LocalDate.now())) { - memorySliceCache.clear(); - memoryNodeCacheCounter.clear(); - cacheDate = LocalDate.now(); - } - } - - public MemoryResult selectMemory(LocalDate date) throws IOException, ClassNotFoundException { - MemoryResult memoryResult = new MemoryResult(); - CopyOnWriteArrayList targetSliceList = new CopyOnWriteArrayList<>(); - //加载节点并获取记忆切片列表 - List> currentDateDialogSlices = loadSlicesByDate(date); - for (List value : currentDateDialogSlices) { - for (MemorySlice memorySlice : value) { - if (selectedSlices.contains(memorySlice.getTimestamp())) { - continue; - } - MemorySliceResult memorySliceResult = new MemorySliceResult(); - memorySliceResult.setMemorySlice(memorySlice); - targetSliceList.add(memorySliceResult); - selectedSlices.add(memorySlice.getTimestamp()); - } - } - memoryResult.setMemorySliceResult(targetSliceList); - return memoryResult; - } - - private List> loadSlicesByDate(LocalDate date) throws IOException, ClassNotFoundException { - if (!dateIndex.containsKey(date)) { - throw new UnExistedDateIndexException("不存在的日期索引: " + date); - } - List> list = new ArrayList<>(); - for (String memoryId : dateIndex.get(date)) { - MemoryNode memoryNode = new MemoryNode(); - memoryNode.setMemoryNodeId(memoryId); - list.add(memoryNode.loadMemorySliceList()); - } - return list; - } - - private TopicNode getTargetParentNode(List topicPath, String targetTopic) { - String topTopic = topicPath.getFirst(); - if (!existedTopics.containsKey(topTopic)) { - throw new UnExistedTopicException("不存在的主题: " + topTopic); - } - TopicNode targetParentNode = topicNodes.get(topTopic); - topicPath.removeFirst(); - for (String topic : topicPath) { - if (!existedTopics.get(topTopic).contains(topic)) { - throw new UnExistedTopicException("不存在的主题: " + topTopic); - } - } - - //逐层查找目标主题,可选取终点主题节点相邻位置的主题节点。终点记忆节点选取全部memoryNode, 邻近记忆节点选取最新日期的memoryNode - while (!targetParentNode.getTopicNodes().containsKey(targetTopic)) { - targetParentNode = targetParentNode.getTopicNodes().get(topicPath.getFirst()); - topicPath.removeFirst(); - } - return targetParentNode; - } - - public String getTopicTree() { - StringBuilder stringBuilder = new StringBuilder(); - for (Map.Entry entry : topicNodes.entrySet()) { - String rootName = entry.getKey(); - TopicNode rootNode = entry.getValue(); - stringBuilder.append(rootName).append("[root]").append("\r\n"); - printSubTopicsTreeFormat(rootNode, "", stringBuilder); - } - return stringBuilder.toString(); - } - - private void printSubTopicsTreeFormat(TopicNode node, String prefix, StringBuilder stringBuilder) { - if (node.getTopicNodes() == null || node.getTopicNodes().isEmpty()) return; - - List> entries = new ArrayList<>(node.getTopicNodes().entrySet()); - for (int i = 0; i < entries.size(); i++) { - boolean last = (i == entries.size() - 1); - Map.Entry entry = entries.get(i); - stringBuilder.append(prefix).append(last ? "└── " : "├── ").append(entry.getKey()).append("[").append(entry.getValue().getMemoryNodes().size()).append("]").append("\r\n"); - printSubTopicsTreeFormat(entry.getValue(), prefix + (last ? " " : "│ "), stringBuilder); - } - } - - - public void updateDialogMap(LocalDateTime dateTime, String newDialogCache) { - List keysToRemove = new ArrayList<>(); - dialogMap.forEach((k, v) -> { - if (dateTime.minusDays(2).isAfter(k)) { - keysToRemove.add(k); - } - }); - for (LocalDateTime temp : keysToRemove) { - dialogMap.remove(temp); - } - keysToRemove.clear(); - //放入新缓存 - dialogMap.put(dateTime, newDialogCache); - } -} - diff --git a/src/main/java/work/slhaf/agent/core/memory/MemoryManager.java b/src/main/java/work/slhaf/agent/core/memory/MemoryManager.java index d7ebdbe1..4885f4d1 100644 --- a/src/main/java/work/slhaf/agent/core/memory/MemoryManager.java +++ b/src/main/java/work/slhaf/agent/core/memory/MemoryManager.java @@ -6,11 +6,16 @@ import lombok.extern.slf4j.Slf4j; import work.slhaf.agent.common.chat.constant.ChatConstant; import work.slhaf.agent.common.chat.pojo.Message; import work.slhaf.agent.common.config.Config; +import work.slhaf.agent.common.exception_handler.GlobalExceptionHandler; +import work.slhaf.agent.common.exception_handler.pojo.GlobalException; import work.slhaf.agent.common.serialize.PersistableObject; import work.slhaf.agent.core.memory.pojo.MemoryResult; -import work.slhaf.agent.core.memory.pojo.MemorySlice; import work.slhaf.agent.core.memory.pojo.MemorySliceResult; -import work.slhaf.agent.core.memory.pojo.User; +import work.slhaf.agent.core.memory.submodule.cache.CacheCore; +import work.slhaf.agent.core.memory.submodule.graph.GraphCore; +import work.slhaf.agent.core.memory.submodule.graph.pojo.MemorySlice; +import work.slhaf.agent.core.memory.submodule.perceive.PerceiveCore; +import work.slhaf.agent.core.memory.submodule.perceive.pojo.User; import work.slhaf.agent.shared.memory.EvaluatedSlice; import java.io.IOException; @@ -38,7 +43,11 @@ public class MemoryManager extends PersistableObject { public final Lock messageLock = new ReentrantLock(); - private MemoryGraph memoryGraph; + private MemoryCore memoryCore; + private CacheCore cacheCore; + private GraphCore graphCore; + private PerceiveCore perceiveCore; + private HashMap> activatedSlices; private MemoryManager() { @@ -51,7 +60,8 @@ public class MemoryManager extends PersistableObject { if (memoryManager == null) { Config config = Config.getConfig(); memoryManager = new MemoryManager(); - memoryManager.setMemoryGraph(MemoryGraph.getInstance(config.getAgentId())); + memoryManager.setMemoryCore(MemoryCore.getInstance(config.getAgentId())); + memoryManager.setCores(); memoryManager.setActivatedSlices(new HashMap<>()); memoryManager.setShutdownHook(); log.info("[MemoryManager] MemoryManager注册完毕..."); @@ -61,6 +71,12 @@ public class MemoryManager extends PersistableObject { return memoryManager; } + private void setCores() { + this.setCacheCore(this.getMemoryCore().getCacheCore()); + this.setGraphCore(this.getMemoryCore().getGraphCore()); + this.setPerceiveCore(this.getMemoryCore().getPerceiveCore()); + } + private void setShutdownHook() { Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { @@ -72,12 +88,36 @@ public class MemoryManager extends PersistableObject { })); } - public MemoryResult selectMemory(String path) { - return cacheFilter(memoryGraph.selectMemory(path)); + public MemoryResult selectMemory(String topicPathStr) { + MemoryResult memoryResult; + List topicPath = List.of(topicPathStr.split("->")); + try { + List path = new ArrayList<>(topicPath); + //每日刷新缓存 + cacheCore.checkCacheDate(); + //检测缓存并更新计数, 查看是否需要放入缓存 + cacheCore.updateCacheCounter(path); + //查看是否存在缓存,如果存在,则直接返回 + if ((memoryResult = cacheCore.selectCache(path)) != null) { + return memoryResult; + } + memoryResult = graphCore.selectMemory(path); + //尝试更新缓存 + cacheCore.updateCache(topicPath, memoryResult); + } catch (Exception e) { + log.error("[MemoryManager] selectMemory error: ", e); + log.error("[MemoryManager] 路径: {}", topicPathStr); + log.error("[MemoryManager] 主题树: {}", getTopicTree()); + memoryResult = new MemoryResult(); + memoryResult.setRelatedMemorySliceResult(new ArrayList<>()); + memoryResult.setMemorySliceResult(new CopyOnWriteArrayList<>()); + GlobalExceptionHandler.writeExceptionState(new GlobalException(e.getLocalizedMessage())); + } + return cacheFilter(memoryResult); } public MemoryResult selectMemory(LocalDate date) throws IOException, ClassNotFoundException { - return cacheFilter(memoryGraph.selectMemory(date)); + return cacheFilter(graphCore.selectMemory(date)); } private MemoryResult cacheFilter(MemoryResult memoryResult) { @@ -92,30 +132,30 @@ public class MemoryManager extends PersistableObject { } public void cleanSelectedSliceFilter() { - memoryGraph.getSelectedSlices().clear(); + graphCore.getSelectedSlices().clear(); } public String getUserId(String userInfo, String nickName) { String userId = null; - for (User user : memoryGraph.getUsers()) { + for (User user : perceiveCore.getUsers()) { if (user.getInfo().contains(userInfo)) { userId = user.getUuid(); } } if (userId == null) { User newUser = setNewUser(userInfo, nickName); - memoryGraph.getUsers().add(newUser); + perceiveCore.getUsers().add(newUser); userId = newUser.getUuid(); } return userId; } public List getChatMessages() { - return memoryGraph.getChatMessages(); + return memoryCore.getChatMessages(); } public void setChatMessages(List chatMessages) { - memoryGraph.setChatMessages(chatMessages); + memoryCore.setChatMessages(chatMessages); } private static User setNewUser(String userInfo, String nickName) { @@ -129,37 +169,50 @@ public class MemoryManager extends PersistableObject { } public String getTopicTree() { - return memoryGraph.getTopicTree(); + return graphCore.getTopicTree(); } public HashMap getDialogMap() { - return memoryGraph.getDialogMap(); + return cacheCore.getDialogMap(); } public ConcurrentHashMap getUserDialogMap(String userId) { - return memoryGraph.getUserDialogMap().get(userId); + return cacheCore.getUserDialogMap().get(userId); } public void insertSlice(MemorySlice memorySlice, String topicPath) { sliceInsertLock.lock(); List topicPathList = Arrays.stream(topicPath.split("->")).toList(); - memoryGraph.insertMemory(topicPathList, memorySlice); + try { + //检查是否存在当天对应的memorySlice并确定是否插入 + //每日刷新缓存 + cacheCore.checkCacheDate(); + //如果topicPath在memorySliceCache中存在对应缓存,由于进行的插入操作,则需要移除该缓存,但不清除相关计数 + cacheCore.clearCacheByTopicPath(topicPathList); + graphCore.insertMemory(topicPathList, memorySlice); + if (!memorySlice.isPrivate()) { + cacheCore.updateUserDialogMap(memorySlice); + } + } catch (Exception e) { + log.error("[MemoryManager] 插入记忆时出错: ", e); + GlobalExceptionHandler.writeExceptionState(new GlobalException("插入记忆时出错: " + e.getLocalizedMessage())); + } log.debug("[MemoryManager] 插入切片: {}, 路径: {}", memorySlice, topicPath); sliceInsertLock.unlock(); } public void cleanMessage(List messages) { messageLock.lock(); - memoryGraph.getChatMessages().removeAll(messages); + memoryCore.getChatMessages().removeAll(messages); messageLock.unlock(); } public void updateDialogMap(LocalDateTime dateTime, String newDialogCache) { - memoryGraph.updateDialogMap(dateTime, newDialogCache); + cacheCore.updateDialogMap(dateTime, newDialogCache); } - public void save() throws IOException { - memoryGraph.serialize(); + private void save() throws IOException { + memoryCore.serialize(); } public void updateActivatedSlices(String userId, List memorySlices) { @@ -168,7 +221,7 @@ public class MemoryManager extends PersistableObject { } public User getUser(String id) { - for (User user : memoryGraph.getUsers()) { + for (User user : perceiveCore.getUsers()) { if (user.getUuid().equals(id)) { return user; } @@ -189,16 +242,16 @@ public class MemoryManager extends PersistableObject { public String getDialogMapStr() { StringBuilder str = new StringBuilder(); - memoryGraph.getDialogMap().forEach((dateTime, dialog) -> str.append("\n\n").append("[").append(dateTime).append("]\n") + cacheCore.getDialogMap().forEach((dateTime, dialog) -> str.append("\n\n").append("[").append(dateTime).append("]\n") .append(dialog)); return str.toString(); } public String getUserDialogMapStr(String userId) { - if (memoryGraph.getUserDialogMap().containsKey(userId)) { + if (cacheCore.getUserDialogMap().containsKey(userId)) { StringBuilder str = new StringBuilder(); - Collection dialogMapValues = memoryGraph.getDialogMap().values(); - memoryGraph.getUserDialogMap().get(userId).forEach((dateTime, dialog) -> { + Collection dialogMapValues = cacheCore.getDialogMap().values(); + cacheCore.getUserDialogMap().get(userId).forEach((dateTime, dialog) -> { if (dialogMapValues.contains(dialog)) { return; } @@ -212,7 +265,7 @@ public class MemoryManager extends PersistableObject { } private boolean isCacheSingleUser() { - return memoryGraph.getUserDialogMap().size() <= 1; + return cacheCore.getUserDialogMap().size() <= 1; } public boolean isSingleUser() { diff --git a/src/main/java/work/slhaf/agent/core/memory/pojo/MemoryResult.java b/src/main/java/work/slhaf/agent/core/memory/pojo/MemoryResult.java index c1322926..f5b97bf7 100644 --- a/src/main/java/work/slhaf/agent/core/memory/pojo/MemoryResult.java +++ b/src/main/java/work/slhaf/agent/core/memory/pojo/MemoryResult.java @@ -3,6 +3,7 @@ package work.slhaf.agent.core.memory.pojo; import lombok.Data; import lombok.EqualsAndHashCode; import work.slhaf.agent.common.serialize.PersistableObject; +import work.slhaf.agent.core.memory.submodule.graph.pojo.MemorySlice; import java.io.Serial; import java.util.List; diff --git a/src/main/java/work/slhaf/agent/core/memory/pojo/MemorySliceResult.java b/src/main/java/work/slhaf/agent/core/memory/pojo/MemorySliceResult.java index d8900024..339c5bca 100644 --- a/src/main/java/work/slhaf/agent/core/memory/pojo/MemorySliceResult.java +++ b/src/main/java/work/slhaf/agent/core/memory/pojo/MemorySliceResult.java @@ -4,6 +4,7 @@ import com.alibaba.fastjson2.annotation.JSONField; import lombok.Data; import lombok.EqualsAndHashCode; import work.slhaf.agent.common.serialize.PersistableObject; +import work.slhaf.agent.core.memory.submodule.graph.pojo.MemorySlice; import java.io.Serial; diff --git a/src/main/java/work/slhaf/agent/core/memory/submodule/cache/CacheCore.java b/src/main/java/work/slhaf/agent/core/memory/submodule/cache/CacheCore.java new file mode 100644 index 00000000..13e26d20 --- /dev/null +++ b/src/main/java/work/slhaf/agent/core/memory/submodule/cache/CacheCore.java @@ -0,0 +1,132 @@ +package work.slhaf.agent.core.memory.submodule.cache; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import work.slhaf.agent.common.serialize.PersistableObject; +import work.slhaf.agent.core.memory.pojo.MemoryResult; +import work.slhaf.agent.core.memory.submodule.graph.pojo.MemorySlice; + +import java.io.Serial; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +@EqualsAndHashCode(callSuper = true) +@Data +@Slf4j +public class CacheCore extends PersistableObject { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 近两日的对话总结缓存, 用于为大模型提供必要的记忆补充, hashmap以切片的存储时间为键,总结为值 + * 该部分作为'主LLM'system prompt常驻 + * 该部分作为近两日的整体对话缓存, 不区分用户 + */ + private HashMap dialogMap = new HashMap<>(); + + /** + * 近两日的区分用户的对话总结缓存,在prompt结构上比dialogMap层级深一层, dialogMap更具近两日整体对话的摘要性质 + */ + private ConcurrentHashMap> userDialogMap = new ConcurrentHashMap<>(); + + /** + * memorySliceCache计数器,每日清空 + */ + private ConcurrentHashMap /*触发查询的主题列表*/, Integer> memoryNodeCacheCounter = new ConcurrentHashMap<>(); + + /** + * 记忆切片缓存,每日清空 + * 用于记录作为终点节点调用次数最多的记忆节点的切片数据 + */ + private ConcurrentHashMap /*主题路径*/, MemoryResult /*切片列表*/> memorySliceCache = new ConcurrentHashMap<>(); + + /** + * 缓存日期 + */ + private LocalDate cacheDate; + + /** + * 已被选中的切片时间戳集合,需要及时清理 + */ + private Set selectedSlices = new HashSet<>(); + + public void updateCacheCounter(List topicPath) { + if (memoryNodeCacheCounter.containsKey(topicPath)) { + Integer tempCount = memoryNodeCacheCounter.get(topicPath); + memoryNodeCacheCounter.put(topicPath, ++tempCount); + } else { + memoryNodeCacheCounter.put(topicPath, 1); + } + } + + public void checkCacheDate() { + if (cacheDate == null || cacheDate.isBefore(LocalDate.now())) { + memorySliceCache.clear(); + memoryNodeCacheCounter.clear(); + cacheDate = LocalDate.now(); + } + } + + public void updateDialogMap(LocalDateTime dateTime, String newDialogCache) { + List keysToRemove = new ArrayList<>(); + dialogMap.forEach((k, v) -> { + if (dateTime.minusDays(2).isAfter(k)) { + keysToRemove.add(k); + } + }); + for (LocalDateTime temp : keysToRemove) { + dialogMap.remove(temp); + } + keysToRemove.clear(); + //放入新缓存 + dialogMap.put(dateTime, newDialogCache); + } + + public void updateCache(List topicPath, MemoryResult memoryResult) { + Integer tempCount = memoryNodeCacheCounter.get(topicPath); + if (tempCount == null) { + log.warn("[CacheCore] tempCount为null? memoryNodeCacheCounter: {}; topicPath: {}", memoryNodeCacheCounter, topicPath); + return; + } + if (tempCount >= 5) { + memorySliceCache.put(topicPath, memoryResult); + } + } + + public void updateUserDialogMap(MemorySlice slice) { + String summary = slice.getSummary(); + LocalDateTime now = LocalDateTime.now(); + + //更新userDialogMap + //移除两天前上下文缓存(切片总结) + List keysToRemove = new ArrayList<>(); + userDialogMap.forEach((k, v) -> v.forEach((i, j) -> { + if (now.minusDays(2).isAfter(i)) { + keysToRemove.add(i); + } + })); + for (LocalDateTime dateTime : keysToRemove) { + userDialogMap.forEach((k, v) -> v.remove(dateTime)); + } + //放入新缓存 + userDialogMap + .computeIfAbsent(slice.getStartUserId(), k -> new ConcurrentHashMap<>()) + .merge(now, summary, (oldVal, newVal) -> oldVal + " " + newVal); + + } + + public void clearCacheByTopicPath(List topicPath) { + memorySliceCache.remove(topicPath); + } + + public MemoryResult selectCache(List path) { + if (memorySliceCache.containsKey(path)) { + return memorySliceCache.get(path); + } + return null; + } +} diff --git a/src/main/java/work/slhaf/agent/core/memory/submodule/graph/GraphCore.java b/src/main/java/work/slhaf/agent/core/memory/submodule/graph/GraphCore.java new file mode 100644 index 00000000..51f5b876 --- /dev/null +++ b/src/main/java/work/slhaf/agent/core/memory/submodule/graph/GraphCore.java @@ -0,0 +1,303 @@ +package work.slhaf.agent.core.memory.submodule.graph; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import work.slhaf.agent.common.serialize.PersistableObject; +import work.slhaf.agent.core.memory.exception.UnExistedDateIndexException; +import work.slhaf.agent.core.memory.exception.UnExistedTopicException; +import work.slhaf.agent.core.memory.pojo.MemoryResult; +import work.slhaf.agent.core.memory.pojo.MemorySliceResult; +import work.slhaf.agent.core.memory.submodule.graph.pojo.MemorySlice; +import work.slhaf.agent.core.memory.submodule.graph.pojo.node.MemoryNode; +import work.slhaf.agent.core.memory.submodule.graph.pojo.node.TopicNode; + +import java.io.IOException; +import java.io.Serial; +import java.time.LocalDate; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +@EqualsAndHashCode(callSuper = true) +@Data +public class GraphCore extends PersistableObject { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * key: 根主题名称 value: 根主题节点 + */ + private HashMap topicNodes = new HashMap<>(); + + /** + * 用于存储已存在的主题列表,便于记忆查找, 使用根主题名称作为键, 子主题名称集合为值 + * 该部分在'主题提取LLM'的system prompt中常驻 + */ + private HashMap /*子主题列表*/> existedTopics = new HashMap<>(); + + /** + * 临时的同一对话切片容器, 用于为同一对话内的不同切片提供更新上下文的场所 + */ + private HashMap> currentDateDialogSlices = new HashMap<>(); + + /** + * 记忆节点的日期索引, 同一日期内按照对话id区分 + */ + private HashMap> dateIndex = new HashMap<>(); + + /** + * 已被选中的切片时间戳集合,需要及时清理 + */ + private Set selectedSlices = new HashSet<>(); + + + public MemoryResult selectMemory(LocalDate date) throws IOException, ClassNotFoundException { + MemoryResult memoryResult = new MemoryResult(); + CopyOnWriteArrayList targetSliceList = new CopyOnWriteArrayList<>(); + //加载节点并获取记忆切片列表 + List> currentDateDialogSlices = loadSlicesByDate(date); + for (List value : currentDateDialogSlices) { + for (MemorySlice memorySlice : value) { + if (selectedSlices.contains(memorySlice.getTimestamp())) { + continue; + } + MemorySliceResult memorySliceResult = new MemorySliceResult(); + memorySliceResult.setMemorySlice(memorySlice); + targetSliceList.add(memorySliceResult); + selectedSlices.add(memorySlice.getTimestamp()); + } + } + memoryResult.setMemorySliceResult(targetSliceList); + return memoryResult; + } + + private List> loadSlicesByDate(LocalDate date) throws IOException, ClassNotFoundException { + if (!dateIndex.containsKey(date)) { + throw new UnExistedDateIndexException("不存在的日期索引: " + date); + } + List> list = new ArrayList<>(); + for (String memoryId : dateIndex.get(date)) { + MemoryNode memoryNode = new MemoryNode(); + memoryNode.setMemoryNodeId(memoryId); + list.add(memoryNode.loadMemorySliceList()); + } + return list; + } + + public String getTopicTree() { + StringBuilder stringBuilder = new StringBuilder(); + for (Map.Entry entry : topicNodes.entrySet()) { + String rootName = entry.getKey(); + TopicNode rootNode = entry.getValue(); + stringBuilder.append(rootName).append("[root]").append("\r\n"); + printSubTopicsTreeFormat(rootNode, "", stringBuilder); + } + return stringBuilder.toString(); + } + + private void printSubTopicsTreeFormat(TopicNode node, String prefix, StringBuilder stringBuilder) { + if (node.getTopicNodes() == null || node.getTopicNodes().isEmpty()) return; + + List> entries = new ArrayList<>(node.getTopicNodes().entrySet()); + for (int i = 0; i < entries.size(); i++) { + boolean last = (i == entries.size() - 1); + Map.Entry entry = entries.get(i); + stringBuilder.append(prefix).append(last ? "└── " : "├── ").append(entry.getKey()).append("[").append(entry.getValue().getMemoryNodes().size()).append("]").append("\r\n"); + printSubTopicsTreeFormat(entry.getValue(), prefix + (last ? " " : "│ "), stringBuilder); + } + } + + public void insertMemory(List topicPath, MemorySlice slice) throws IOException, ClassNotFoundException { + LocalDate now = LocalDate.now(); + boolean hasSlice = false; + MemoryNode node = null; + TopicNode lastTopicNode = generateTopicPath(topicPath); + for (MemoryNode memoryNode : lastTopicNode.getMemoryNodes()) { + if (now.equals(memoryNode.getLocalDate())) { + hasSlice = true; + node = memoryNode; + break; + } + } + if (!hasSlice) { + node = new MemoryNode(); + node.setLocalDate(now); + node.setMemoryNodeId(UUID.randomUUID().toString()); + node.setMemorySliceList(new CopyOnWriteArrayList<>()); + lastTopicNode.getMemoryNodes().add(node); + lastTopicNode.getMemoryNodes().sort(null); + } + node.loadMemorySliceList().add(slice); + + //生成relatedTopicPath + for (List relatedTopic : slice.getRelatedTopics()) { + generateTopicPath(relatedTopic); + } + + updateSlicePrecedent(slice); + updateDateIndex(slice); + + node.saveMemorySliceList(); + + } + + + private TopicNode generateTopicPath(List topicPath) { + topicPath = new ArrayList<>(topicPath); + //查看是否存在根主题节点 + String rootTopic = topicPath.getFirst(); + topicPath.removeFirst(); + if (!topicNodes.containsKey(rootTopic)) { + synchronized (this) { + if (!topicNodes.containsKey(rootTopic)) { + TopicNode rootNode = new TopicNode(); + topicNodes.put(rootTopic, rootNode); + existedTopics.put(rootTopic, new LinkedHashSet<>()); + } + } + } + + TopicNode current = topicNodes.get(rootTopic); + Set existedTopicNodes = existedTopics.get(rootTopic); + for (String topic : topicPath) { + if (existedTopicNodes.contains(topic) && current.getTopicNodes().containsKey(topic)) { + current = current.getTopicNodes().get(topic); + } else { + TopicNode newNode = new TopicNode(); + current.getTopicNodes().put(topic, newNode); + current = newNode; + + current.setMemoryNodes(new CopyOnWriteArrayList<>()); + current.setTopicNodes(new ConcurrentHashMap<>()); + existedTopicNodes.add(topic); + } + } + return current; + } + + private void updateSlicePrecedent(MemorySlice slice) { + String memoryId = slice.getMemoryId(); + //查看是否切换了memoryId + if (!currentDateDialogSlices.containsKey(memoryId)) { + List memorySliceList = new ArrayList<>(); + currentDateDialogSlices.clear(); + currentDateDialogSlices.put(memoryId, memorySliceList); + } + //处理上下文关系 + List memorySliceList = currentDateDialogSlices.get(memoryId); + if (memorySliceList.isEmpty()) { + memorySliceList.add(slice); + } else { + //排序 + memorySliceList.sort(null); + MemorySlice tempSlice = memorySliceList.getLast(); + //设置私密状态一致 + tempSlice.setPrivate(slice.isPrivate()); + //末尾切片添加当前切片的引用 + tempSlice.setSliceAfter(slice); + //当前切片添加前序切片的引用 + slice.setSliceBefore(tempSlice); + } + + } + + + private void updateDateIndex(MemorySlice slice) { + String memoryId = slice.getMemoryId(); + LocalDate date = LocalDate.now(); + if (!dateIndex.containsKey(date)) { + HashSet memoryIdSet = new HashSet<>(); + memoryIdSet.add(memoryId); + dateIndex.put(date, memoryIdSet); + } else { + dateIndex.get(date).add(memoryId); + } + } + + public MemoryResult selectMemory(List path) throws IOException, ClassNotFoundException { + MemoryResult memoryResult = new MemoryResult(); + CopyOnWriteArrayList targetSliceList = new CopyOnWriteArrayList<>(); + String targetTopic = path.getLast(); + TopicNode targetParentNode = getTargetParentNode(path, targetTopic); + List> relatedTopics = new ArrayList<>(); + + //终点记忆节点 + MemorySliceResult sliceResult = new MemorySliceResult(); + for (MemoryNode memoryNode : targetParentNode.getTopicNodes().get(targetTopic).getMemoryNodes()) { + List endpointMemorySliceList = memoryNode.loadMemorySliceList(); + for (MemorySlice memorySlice : endpointMemorySliceList) { + if (selectedSlices.contains(memorySlice.getTimestamp())) { + continue; + } + sliceResult.setSliceBefore(memorySlice.getSliceBefore()); + sliceResult.setMemorySlice(memorySlice); + sliceResult.setSliceAfter(memorySlice.getSliceAfter()); + targetSliceList.add(sliceResult); + selectedSlices.add(memorySlice.getTimestamp()); + } + for (MemorySlice memorySlice : endpointMemorySliceList) { + if (memorySlice.getRelatedTopics() != null) { + relatedTopics.addAll(memorySlice.getRelatedTopics()); + } + } + } + memoryResult.setMemorySliceResult(targetSliceList); + + //邻近节点 + List relatedMemorySlice = new ArrayList<>(); + //邻近记忆节点 联系 + for (List relatedTopic : relatedTopics) { + List tempTopicPath = new ArrayList<>(relatedTopic); + String tempTargetTopic = tempTopicPath.getLast(); + TopicNode tempTargetParentNode = getTargetParentNode(tempTopicPath, tempTargetTopic); + //获取终点节点及其最新记忆节点 + TopicNode tempTargetNode = tempTargetParentNode.getTopicNodes().get(tempTopicPath.getLast()); + setRelatedMemorySlices(tempTargetNode, relatedMemorySlice); + } + + //邻近记忆节点 父级 + setRelatedMemorySlices(targetParentNode, relatedMemorySlice); + + //将上述结果包装为MemoryResult + memoryResult.setRelatedMemorySliceResult(relatedMemorySlice); + return memoryResult; + } + + private void setRelatedMemorySlices(TopicNode targetParentNode, List relatedMemorySlice) throws IOException, ClassNotFoundException { + List targetParentMemoryNodes = targetParentNode.getMemoryNodes(); + if (!targetParentMemoryNodes.isEmpty()) { + for (MemorySlice memorySlice : targetParentMemoryNodes.getFirst().loadMemorySliceList()) { + if (selectedSlices.contains(memorySlice.getTimestamp())) { + continue; + } + relatedMemorySlice.add(memorySlice); + selectedSlices.add(memorySlice.getTimestamp()); + } + } + } + + + + private TopicNode getTargetParentNode(List topicPath, String targetTopic) { + String topTopic = topicPath.getFirst(); + if (!existedTopics.containsKey(topTopic)) { + throw new UnExistedTopicException("不存在的主题: " + topTopic); + } + TopicNode targetParentNode = topicNodes.get(topTopic); + topicPath.removeFirst(); + for (String topic : topicPath) { + if (!existedTopics.get(topTopic).contains(topic)) { + throw new UnExistedTopicException("不存在的主题: " + topTopic); + } + } + + //逐层查找目标主题 + while (!targetParentNode.getTopicNodes().containsKey(targetTopic)) { + targetParentNode = targetParentNode.getTopicNodes().get(topicPath.getFirst()); + topicPath.removeFirst(); + } + return targetParentNode; + } + +} diff --git a/src/main/java/work/slhaf/agent/core/memory/pojo/MemorySlice.java b/src/main/java/work/slhaf/agent/core/memory/submodule/graph/pojo/MemorySlice.java similarity index 96% rename from src/main/java/work/slhaf/agent/core/memory/pojo/MemorySlice.java rename to src/main/java/work/slhaf/agent/core/memory/submodule/graph/pojo/MemorySlice.java index f3e42cf4..8c3ffe58 100644 --- a/src/main/java/work/slhaf/agent/core/memory/pojo/MemorySlice.java +++ b/src/main/java/work/slhaf/agent/core/memory/submodule/graph/pojo/MemorySlice.java @@ -1,4 +1,4 @@ -package work.slhaf.agent.core.memory.pojo; +package work.slhaf.agent.core.memory.submodule.graph.pojo; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/src/main/java/work/slhaf/agent/core/memory/node/MemoryNode.java b/src/main/java/work/slhaf/agent/core/memory/submodule/graph/pojo/node/MemoryNode.java similarity index 95% rename from src/main/java/work/slhaf/agent/core/memory/node/MemoryNode.java rename to src/main/java/work/slhaf/agent/core/memory/submodule/graph/pojo/node/MemoryNode.java index 45101455..d4b13b68 100644 --- a/src/main/java/work/slhaf/agent/core/memory/node/MemoryNode.java +++ b/src/main/java/work/slhaf/agent/core/memory/submodule/graph/pojo/node/MemoryNode.java @@ -1,11 +1,11 @@ -package work.slhaf.agent.core.memory.node; +package work.slhaf.agent.core.memory.submodule.graph.pojo.node; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import work.slhaf.agent.common.serialize.PersistableObject; import work.slhaf.agent.core.memory.exception.NullSliceListException; -import work.slhaf.agent.core.memory.pojo.MemorySlice; +import work.slhaf.agent.core.memory.submodule.graph.pojo.MemorySlice; import java.io.*; import java.nio.file.Files; diff --git a/src/main/java/work/slhaf/agent/core/memory/node/TopicNode.java b/src/main/java/work/slhaf/agent/core/memory/submodule/graph/pojo/node/TopicNode.java similarity index 90% rename from src/main/java/work/slhaf/agent/core/memory/node/TopicNode.java rename to src/main/java/work/slhaf/agent/core/memory/submodule/graph/pojo/node/TopicNode.java index b5fda83f..2f55aa38 100644 --- a/src/main/java/work/slhaf/agent/core/memory/node/TopicNode.java +++ b/src/main/java/work/slhaf/agent/core/memory/submodule/graph/pojo/node/TopicNode.java @@ -1,4 +1,4 @@ -package work.slhaf.agent.core.memory.node; +package work.slhaf.agent.core.memory.submodule.graph.pojo.node; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/src/main/java/work/slhaf/agent/core/memory/submodule/perceive/PerceiveCore.java b/src/main/java/work/slhaf/agent/core/memory/submodule/perceive/PerceiveCore.java new file mode 100644 index 00000000..f840fe71 --- /dev/null +++ b/src/main/java/work/slhaf/agent/core/memory/submodule/perceive/PerceiveCore.java @@ -0,0 +1,24 @@ +package work.slhaf.agent.core.memory.submodule.perceive; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import work.slhaf.agent.common.serialize.PersistableObject; +import work.slhaf.agent.core.memory.submodule.perceive.pojo.User; + +import java.io.Serial; +import java.util.ArrayList; +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +public class PerceiveCore extends PersistableObject { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 用户列表 + */ + private List users = new ArrayList<>(); + +} diff --git a/src/main/java/work/slhaf/agent/core/memory/pojo/User.java b/src/main/java/work/slhaf/agent/core/memory/submodule/perceive/pojo/User.java similarity index 86% rename from src/main/java/work/slhaf/agent/core/memory/pojo/User.java rename to src/main/java/work/slhaf/agent/core/memory/submodule/perceive/pojo/User.java index 6c3bf3fb..6170bab1 100644 --- a/src/main/java/work/slhaf/agent/core/memory/pojo/User.java +++ b/src/main/java/work/slhaf/agent/core/memory/submodule/perceive/pojo/User.java @@ -1,4 +1,4 @@ -package work.slhaf.agent.core.memory.pojo; +package work.slhaf.agent.core.memory.submodule.perceive.pojo; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/src/main/java/work/slhaf/agent/module/modules/memory/selector/MemorySelector.java b/src/main/java/work/slhaf/agent/module/modules/memory/selector/MemorySelector.java index b81f98bb..bdf6b90a 100644 --- a/src/main/java/work/slhaf/agent/module/modules/memory/selector/MemorySelector.java +++ b/src/main/java/work/slhaf/agent/module/modules/memory/selector/MemorySelector.java @@ -3,15 +3,13 @@ package work.slhaf.agent.module.modules.memory.selector; import com.alibaba.fastjson2.JSONObject; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import work.slhaf.agent.common.exception_handler.GlobalExceptionHandler; -import work.slhaf.agent.common.exception_handler.pojo.GlobalException; import work.slhaf.agent.core.interaction.data.context.InteractionContext; import work.slhaf.agent.core.interaction.module.InteractionModule; import work.slhaf.agent.core.memory.MemoryManager; import work.slhaf.agent.core.memory.exception.UnExistedDateIndexException; import work.slhaf.agent.core.memory.exception.UnExistedTopicException; import work.slhaf.agent.core.memory.pojo.MemoryResult; -import work.slhaf.agent.core.memory.pojo.MemorySlice; +import work.slhaf.agent.core.memory.submodule.graph.pojo.MemorySlice; import work.slhaf.agent.core.session.SessionManager; import work.slhaf.agent.module.common.AppendPromptData; import work.slhaf.agent.module.common.PreModuleActions; @@ -29,8 +27,6 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; -import static work.slhaf.agent.common.util.ExtractUtil.fixTopicPath; - @Data @Slf4j public class MemorySelector implements InteractionModule, PreModuleActions { diff --git a/src/main/java/work/slhaf/agent/module/modules/memory/selector/evaluator/SliceSelectEvaluator.java b/src/main/java/work/slhaf/agent/module/modules/memory/selector/evaluator/SliceSelectEvaluator.java index aa085320..5111e924 100644 --- a/src/main/java/work/slhaf/agent/module/modules/memory/selector/evaluator/SliceSelectEvaluator.java +++ b/src/main/java/work/slhaf/agent/module/modules/memory/selector/evaluator/SliceSelectEvaluator.java @@ -9,8 +9,8 @@ import lombok.extern.slf4j.Slf4j; import work.slhaf.agent.common.thread.InteractionThreadPoolExecutor; import work.slhaf.agent.core.memory.MemoryManager; import work.slhaf.agent.core.memory.pojo.MemoryResult; -import work.slhaf.agent.core.memory.pojo.MemorySlice; import work.slhaf.agent.core.memory.pojo.MemorySliceResult; +import work.slhaf.agent.core.memory.submodule.graph.pojo.MemorySlice; import work.slhaf.agent.module.common.Model; import work.slhaf.agent.module.common.ModelConstant; import work.slhaf.agent.module.modules.memory.selector.evaluator.data.EvaluatorBatchInput; diff --git a/src/main/java/work/slhaf/agent/module/modules/memory/updater/MemoryUpdater.java b/src/main/java/work/slhaf/agent/module/modules/memory/updater/MemoryUpdater.java index 9c06b408..e990d48e 100644 --- a/src/main/java/work/slhaf/agent/module/modules/memory/updater/MemoryUpdater.java +++ b/src/main/java/work/slhaf/agent/module/modules/memory/updater/MemoryUpdater.java @@ -9,7 +9,7 @@ import work.slhaf.agent.common.thread.InteractionThreadPoolExecutor; import work.slhaf.agent.core.interaction.data.context.InteractionContext; import work.slhaf.agent.core.interaction.module.InteractionModule; import work.slhaf.agent.core.memory.MemoryManager; -import work.slhaf.agent.core.memory.pojo.MemorySlice; +import work.slhaf.agent.core.memory.submodule.graph.pojo.MemorySlice; import work.slhaf.agent.core.session.SessionManager; import work.slhaf.agent.module.modules.memory.selector.extractor.MemorySelectExtractor; import work.slhaf.agent.module.modules.memory.updater.summarizer.MemorySummarizer; @@ -21,7 +21,6 @@ import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.ReentrantLock; import static work.slhaf.agent.common.util.ExtractUtil.extractUserId; diff --git a/src/main/java/work/slhaf/agent/module/modules/perceive/selector/PerceiveSelector.java b/src/main/java/work/slhaf/agent/module/modules/perceive/selector/PerceiveSelector.java new file mode 100644 index 00000000..044b34cf --- /dev/null +++ b/src/main/java/work/slhaf/agent/module/modules/perceive/selector/PerceiveSelector.java @@ -0,0 +1,24 @@ +package work.slhaf.agent.module.modules.perceive.selector; + +import work.slhaf.agent.core.interaction.data.context.InteractionContext; +import work.slhaf.agent.core.interaction.module.InteractionModule; + +import java.io.IOException; + +public class PerceiveSelector implements InteractionModule { + + private static PerceiveSelector perceiveSelector; + + public static PerceiveSelector getInstance() throws IOException, ClassNotFoundException { + if (perceiveSelector == null) { + perceiveSelector = new PerceiveSelector(); + } + + return perceiveSelector; + } + + @Override + public void execute(InteractionContext context) throws IOException, ClassNotFoundException { + + } +} diff --git a/src/main/java/work/slhaf/agent/module/modules/perceive/static_extractor/StaticPerceiveExtractor.java b/src/main/java/work/slhaf/agent/module/modules/perceive/updater/static_extractor/StaticPerceiveExtractor.java similarity index 89% rename from src/main/java/work/slhaf/agent/module/modules/perceive/static_extractor/StaticPerceiveExtractor.java rename to src/main/java/work/slhaf/agent/module/modules/perceive/updater/static_extractor/StaticPerceiveExtractor.java index cc0c97e9..f9241198 100644 --- a/src/main/java/work/slhaf/agent/module/modules/perceive/static_extractor/StaticPerceiveExtractor.java +++ b/src/main/java/work/slhaf/agent/module/modules/perceive/updater/static_extractor/StaticPerceiveExtractor.java @@ -1,4 +1,4 @@ -package work.slhaf.agent.module.modules.perceive.static_extractor; +package work.slhaf.agent.module.modules.perceive.updater.static_extractor; import cn.hutool.json.JSONUtil; import com.alibaba.fastjson2.JSONObject; @@ -7,7 +7,7 @@ import lombok.EqualsAndHashCode; import work.slhaf.agent.common.chat.pojo.ChatResponse; import work.slhaf.agent.module.common.Model; import work.slhaf.agent.module.common.ModelConstant; -import work.slhaf.agent.module.modules.perceive.static_extractor.data.StaticExtractInput; +import work.slhaf.agent.module.modules.perceive.updater.static_extractor.data.StaticExtractInput; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/work/slhaf/agent/module/modules/perceive/static_extractor/data/StaticExtractInput.java b/src/main/java/work/slhaf/agent/module/modules/perceive/updater/static_extractor/data/StaticExtractInput.java similarity index 79% rename from src/main/java/work/slhaf/agent/module/modules/perceive/static_extractor/data/StaticExtractInput.java rename to src/main/java/work/slhaf/agent/module/modules/perceive/updater/static_extractor/data/StaticExtractInput.java index 5ccb0f2b..438ed6d2 100644 --- a/src/main/java/work/slhaf/agent/module/modules/perceive/static_extractor/data/StaticExtractInput.java +++ b/src/main/java/work/slhaf/agent/module/modules/perceive/updater/static_extractor/data/StaticExtractInput.java @@ -1,4 +1,4 @@ -package work.slhaf.agent.module.modules.perceive.static_extractor.data; +package work.slhaf.agent.module.modules.perceive.updater.static_extractor.data; import lombok.Builder; import lombok.Data; diff --git a/src/test/java/memory/MemoryTest.java b/src/test/java/memory/MemoryTest.java deleted file mode 100644 index 814e2398..00000000 --- a/src/test/java/memory/MemoryTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package memory; - -import work.slhaf.agent.core.memory.MemoryGraph; -import work.slhaf.agent.core.memory.node.TopicNode; - -import java.time.LocalDate; -import java.util.HashMap; -import java.util.concurrent.ConcurrentHashMap; - -public class MemoryTest { - -//@Test -public void test1() { - String basicCharacter = ""; - MemoryGraph graph = new MemoryGraph("test"); - HashMap topicMap = new HashMap<>(); - - TopicNode root1 = new TopicNode(); - root1.setTopicNodes(new ConcurrentHashMap<>()); - - TopicNode sub1 = new TopicNode(); - sub1.setTopicNodes(new ConcurrentHashMap<>()); - - TopicNode sub2 = new TopicNode(); - sub2.setTopicNodes(new ConcurrentHashMap<>()); - - TopicNode subsub1 = new TopicNode(); - subsub1.setTopicNodes(new ConcurrentHashMap<>()); - - // 构造结构:root -> sub1 -> subsub1, root -> sub2 - sub1.getTopicNodes().put("子子主题1", subsub1); - root1.getTopicNodes().put("子主题1", sub1); - root1.getTopicNodes().put("子主题2", sub2); - - topicMap.put("根主题1", root1); - - // 添加 root2 - TopicNode root2 = new TopicNode(); - root2.setTopicNodes(new ConcurrentHashMap<>()); - - TopicNode sub3 = new TopicNode(); - sub3.setTopicNodes(new ConcurrentHashMap<>()); - - // 构造结构:root2 -> sub3 - root2.getTopicNodes().put("子主题3", sub3); - - topicMap.put("根主题2", root2); - - // 输出 - graph.setTopicNodes(topicMap); - System.out.println(graph.getTopicTree()); -} - - -// @Test - public void test2(){ - System.out.println(LocalDate.now()); - } - -}