chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
# app-platform-memory
## 模块概述
app-platform-memory 是一个记忆管理模块,为 AI Agent 提供短期、长期和工作记忆的管理能力。
## 功能特性
- **多类型记忆支持**
- 短期记忆 (short_term): 用于临时存储对话上下文
- 长期记忆 (long_term): 持久化存储重要信息
- 工作记忆 (working): 当前任务相关的活跃记忆
- **记忆管理功能**
- 创建、更新、删除记忆
- 记忆搜索和检索
- 自动归档过期记忆
- 重要性评分机制
- 访问频率统计
- **API 支持**
- RESTful API 接口
- Web 管理界面
- SDK 集成
## 子模块说明
- `app-platform-memory-spec`: 规范定义和常量
- `app-platform-memory-sdk`: SDK 和 DTO 定义
- `app-platform-memory-domain`: 领域模型和仓储接口
- `app-platform-memory-application`: 应用服务层
- `app-platform-memory-infra`: 基础设施实现
- `app-platform-memory-api`: RESTful API 接口
- `app-platform-memory-web`: Web 管理界面
## 使用方式
### 添加依赖
在需要使用记忆功能的模块中添加依赖:
```xml
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-sdk</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
```
### API 调用示例
#### 创建记忆
```bash
POST /api/memory
Content-Type: application/json
{
"memoryType": "short_term",
"content": "用户偏好设置",
"metadata": {
"userId": "user123",
"sessionId": "session456"
}
}
```
#### 查询记忆
```bash
GET /api/memory/user/{userId}/type/{memoryType}
```
#### 搜索记忆
```bash
GET /api/memory/search?userId={userId}&keyword={keyword}
```
## 配置说明
### 默认配置
- 短期记忆最大容量: 100 条
- 工作记忆最大容量: 50 条
- 长期记忆转换阈值: 7 天
### 自定义配置
可以在配置文件中覆盖默认值:
```yaml
memory:
short-term:
max-size: 200
working:
max-size: 100
long-term:
threshold-days: 14
```
## 技术栈
- Java 17
- Spring Boot
- Lombok
- Maven
## 开发指南
### 构建项目
```bash
mvn clean install
```
### 运行测试
```bash
mvn test
```
## 版本历史
- v1.0.0-SNAPSHOT: 初始版本,提供基础记忆管理功能

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>app-platform-memory</artifactId>
<groupId>com.xspaceagi</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-memory-api</artifactId>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-application</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,52 @@
package com.xspaceagi.memory.api.controller;
import com.xspaceagi.memory.api.dto.MemoryUnitQuery;
import com.xspaceagi.memory.sdk.dto.MemoryDeleteDto;
import com.xspaceagi.memory.sdk.dto.MemorySaveDto;
import com.xspaceagi.memory.sdk.dto.MemoryUnitQueryDTO;
import com.xspaceagi.memory.sdk.service.IMemoryRpcService;
import com.xspaceagi.system.sdk.service.dto.UserAccessKeyDto;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ForSandboxAgentController {
@Resource
private IMemoryRpcService iMemoryRpcService;
@PostMapping("/api/v1/4sandbox/memory/query")
public ReqResult<String> query(@RequestBody MemoryUnitQuery memoryUnitQuery) {
UserAccessKeyDto userAccessKey = (UserAccessKeyDto) RequestContext.get().getUserAccessKey();
MemoryUnitQueryDTO memoryUnitQueryDTO = new MemoryUnitQueryDTO();
memoryUnitQueryDTO.setTenantId(RequestContext.get().getTenantId());
memoryUnitQueryDTO.setUserId(RequestContext.get().getUserId());
memoryUnitQueryDTO.setAgentId(Long.parseLong(userAccessKey.getTargetId()));
memoryUnitQueryDTO.setCategories(memoryUnitQuery.getCategories());
memoryUnitQueryDTO.setSubCategories(memoryUnitQuery.getSubCategories());
memoryUnitQueryDTO.setTags(memoryUnitQuery.getTags());
memoryUnitQueryDTO.setQueryType(memoryUnitQuery.getTimeRangeType());
memoryUnitQueryDTO.setTimeRange(memoryUnitQuery.getTimeRange());
memoryUnitQueryDTO.setLimit(memoryUnitQuery.getLimit() == null ? 10 : memoryUnitQuery.getLimit());
String memory = iMemoryRpcService.queryMemoriesMd(memoryUnitQueryDTO, memoryUnitQuery.isFilterSensitive());
return ReqResult.success(memory);
}
@PostMapping("/api/v1/4sandbox/memory/save")
public ReqResult<String> save(@RequestBody MemorySaveDto memorySaveDto) {
UserAccessKeyDto userAccessKey = (UserAccessKeyDto) RequestContext.get().getUserAccessKey();
iMemoryRpcService.saveMemories(RequestContext.get().getTenantId(), RequestContext.get().getUserId(), Long.parseLong(userAccessKey.getTargetId()), memorySaveDto);
return ReqResult.success();
}
@PostMapping("/api/v1/4sandbox/memory/delete")
public ReqResult<String> delete(@RequestBody MemoryDeleteDto memoryDeleteDto) {
iMemoryRpcService.deleteMemories(RequestContext.get().getTenantId(), RequestContext.get().getUserId(), memoryDeleteDto.getMemoryIds());
return ReqResult.success();
}
}

View File

@@ -0,0 +1,31 @@
package com.xspaceagi.memory.api.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class MemoryUnitQuery {
@Schema(description = "必须严格从上述一级分类中选择,不能自创")
private List<String> categories;
@Schema(description = "必须严格从上述二级分类中选择,不能自创")
private List<String> subCategories;
@Schema(description = "关键词标签列表,分词可以细粒度一点,尽量多生成有助于更精确查找")
private List<String> tags;
@Schema(description = "查询范围可选值为today|yesterday|recent|long-term|all")
private String timeRangeType;
@Schema(description = "时间范围非必填格式为2023-01-01 00:00:00-2023-01-01 23:59:59")
private String timeRange;
@Schema(description = "限制返回的记忆数量,默认为 10")
private Integer limit;
@Schema(description = "是否过滤敏感信息,默认为 true")
private boolean filterSensitive = true;
}

View File

@@ -0,0 +1,32 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
<id>repack</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<!--
重新打包需要包含的jar包,这个过程会解压所有的jar包并按照jar的规则重新组合成一个jar包
这里需要包含当前这个工程 ,格式是{groupId}:{artifactId}
-->
<includes>
<!-- 包含项目自己 -->
<include>com.xspaceagi:app-platform-memory-api</include>
<include>com.xspaceagi:app-platform-memory-application</include>
<include>com.xspaceagi:app-platform-memory-domain</include>
<include>com.xspaceagi:app-platform-memory-infra</include>
<include>com.xspaceagi:app-platform-memory-spec</include>
</includes>
<!-- 打包结果目录:/ 表示target/ -->
<outputDirectory>/</outputDirectory>
<!-- 是否解压依赖 -->
<unpack>true</unpack>
<!-- 打包的scope -->
<!-- <scope>system</scope>-->
</dependencySet>
</dependencySets>
</assembly>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>app-platform-memory-api</artifactId>
<groupId>com.xspaceagi</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-memory-api</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>app-platform-memory</artifactId>
<groupId>com.xspaceagi</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-memory-application</artifactId>
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-domain</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-infra</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-agent-core-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.hankcs</groupId>
<artifactId>hanlp</artifactId>
<version>portable-1.8.4</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,31 @@
package com.xspaceagi.memory.app.service;
import com.xspaceagi.memory.app.service.dto.MemoryExtractDto;
import com.xspaceagi.memory.sdk.dto.MemoryMetaData;
import com.xspaceagi.memory.sdk.dto.MemorySaveDto;
import com.xspaceagi.memory.sdk.dto.MemoryUnitDTO;
import java.util.List;
/**
* 记忆应用服务接口
*/
public interface MemoryApplicationService {
/**
* 创建记忆
*/
List<MemoryExtractDto> createMemory(MemoryMetaData memoryData);
/**
* 保存记忆
*/
void saveMemories(Long tenantId, Long userId, Long agentId, MemorySaveDto memorySaveDto);
/**
* 搜索记忆
*/
List<MemoryUnitDTO> searchMemories(Long tenantId, Long userId, Long agentId, String userMessage, String context, boolean justKeywordSearch);
void deleteMemories(Long tenantId, Long userId, List<Long> memoryIds);
}

View File

@@ -0,0 +1,112 @@
package com.xspaceagi.memory.app.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xspaceagi.memory.infra.dao.entity.MemoryUnit;
import com.xspaceagi.memory.sdk.dto.MemoryUnitCreateDTO;
import com.xspaceagi.memory.sdk.dto.MemoryUnitDTO;
import com.xspaceagi.memory.sdk.dto.MemoryUnitQueryDTO;
import com.xspaceagi.memory.sdk.dto.MemoryUnitUpdateDTO;
import java.util.List;
import java.util.Map;
/**
* 记忆单元服务接口
*/
public interface MemoryUnitService extends IService<MemoryUnit> {
/**
* 创建记忆单元
*
* @param createDTO 创建DTO
* @return 记忆单元DTO
*/
MemoryUnitDTO create(MemoryUnitCreateDTO createDTO);
/**
* 更新记忆单元
*
* @param updateDTO 更新DTO
* @return 记忆单元DTO
*/
MemoryUnitDTO update(MemoryUnitUpdateDTO updateDTO);
/**
* 根据ID查询记忆单元
*
* @param id ID
* @return 记忆单元DTO
*/
MemoryUnitDTO getById(Long id);
/**
* 分页查询记忆单元
*
* @param queryDTO 查询条件
* @return 记忆单元列表
*/
List<MemoryUnitDTO> queryList(MemoryUnitQueryDTO queryDTO);
/**
* 根据用户ID和分类查询记忆单元
*
* @param userId 用户ID
* @param category 分类
* @return 记忆单元列表
*/
List<MemoryUnitDTO> findByUserIdAndCategory(Long userId, Long agentId, String category, String subCategory);
/**
* 根据代理ID查询记忆单元
*
* @param agentId 代理ID
* @return 记忆单元列表
*/
List<MemoryUnitDTO> findByAgentId(Long agentId);
/**
* 根据分类和JSON字段值查询记忆单元
*
* @param tenantId 租户ID
* @param userId 用户ID
* @param category 一级分类
* @param subCategory 二级分类(可选)
* @param jsonKey JSON中的键
* @param jsonValue JSON中的值
* @return 记忆单元列表
*/
List<MemoryUnitDTO> findByCategoryAndJsonKeyValue(Long tenantId, Long userId, String category, String subCategory, String jsonKey, String jsonValue);
/**
* 根据分类和JSON字段值查询记忆单元支持模糊匹配
*
* @param tenantId 租户ID
* @param userId 用户ID
* @param category 一级分类
* @param subCategory 二级分类(可选)
* @param jsonKey JSON中的键
* @param jsonValue JSON中的值支持模糊匹配
* @return 记忆单元列表
*/
List<MemoryUnitDTO> findByCategoryAndJsonKeyValueLike(Long tenantId, Long userId, String category, String subCategory, String jsonKey, String jsonValue);
/**
* 根据JSON多个字段值查询记忆单元
*
* @param tenantId 租户ID
* @param userId 用户ID
* @param category 一级分类
* @param subCategory 二级分类(可选)
* @param jsonKeyValues JSON键值对 Map<key, value>
* @return 记忆单元列表
*/
List<MemoryUnitDTO> findByCategoryAndJsonKeyValues(Long tenantId, Long userId, String category, String subCategory, Map<String, String> jsonKeyValues);
/**
* 实体转DTO
*
* @param entity 实体
* @return DTO
*/
MemoryUnitDTO toDTO(MemoryUnit entity);
}

View File

@@ -0,0 +1,33 @@
package com.xspaceagi.memory.app.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.xspaceagi.memory.infra.dao.entity.MemoryUnitTag;
import com.xspaceagi.memory.sdk.dto.MemoryUnitTagCreateDTO;
import com.xspaceagi.memory.sdk.dto.MemoryUnitTagDTO;
import java.util.List;
/**
* 记忆单元标签服务接口
*/
public interface MemoryUnitTagService extends IService<MemoryUnitTag> {
/**
* 创建标签
*
* @param createDTO 创建DTO
* @return 标签DTO
*/
MemoryUnitTagDTO create(MemoryUnitTagCreateDTO createDTO);
/**
* 实体转DTO
*
* @param entity 实体
* @return DTO
*/
MemoryUnitTagDTO toDTO(MemoryUnitTag entity);
List<MemoryUnitTagDTO> searchByTagNames(Long userId, List<String> tags);
}

View File

@@ -0,0 +1,91 @@
package com.xspaceagi.memory.app.service.dto;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* API Key 数据传输对象
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemoryExtractDto {
/**
* 是否需要保存
*/
@JsonPropertyDescription("判断是否包含可提取的记忆,有则需要保存")
private Boolean shouldSave;
/**
* 一级分类
*/
@JsonPropertyDescription("一级分类,严格按照提示词中的约定")
private String category;
/**
* 二级分类
*/
@JsonPropertyDescription("二级分类,严格按照提示词中的约定")
private String subCategory;
/**
* 更新的数据
*/
@JsonPropertyDescription("提取的记忆键值对,严格按照提示词中约定的键名")
private List<KeyValueDto> keyValues;
/**
* 标签列表
*/
@JsonPropertyDescription("关键词标签列表,分词可以细粒度一点,尽量多生成有助于更精确查找,根据上下文以及用户消息提取或扩展标签")
private List<String> tags;
/**
* 是否敏感信息
*/
@JsonPropertyDescription("判断是否包含敏感信息")
private Boolean isSensitive;
//priority 范围 0-10数字越大优先级越高
@JsonPropertyDescription("优先级,范围 0-10数字越大优先级越高")
private Integer priority;
/**
* 更新数据内部类
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class KeyValueDto {
/**
* 键名
*/
@JsonPropertyDescription("键名,尽量使用已定义的键名")
private String key;
/**
* 值
*/
@JsonPropertyDescription("键值(对应的记忆值)")
private String value;
}
public Map<String, String> toMap() {
if (keyValues == null) {
return new HashMap<>();
}
return keyValues.stream().collect(Collectors.toMap(KeyValueDto::getKey, KeyValueDto::getValue, (k1, k2) -> k1));
}
}

View File

@@ -0,0 +1,43 @@
package com.xspaceagi.memory.app.service.dto;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class MemoryMergeResultDto {
@JsonPropertyDescription("更新、新增、追加memoryId对应记忆中的键值对")
private List<MergeUnitWithID> updateMemoryKeyValues;
@JsonPropertyDescription("生成新的记忆")
private List<MergeUnit> insertMemories;
@Data
public static class MergeUnitWithID {
@JsonPropertyDescription("记忆ID")
private Long memoryId;
@JsonPropertyDescription("更新替换原有内容比如手机号变更等、新增、追加在原有内容后面追加新的内容比如之前个人形象身高180现在又获取到了新的数据为皮肤黝黑新内容为“身高180皮肤黝黑”对应记忆中的键值对")
private Map<String, String> keyValues;
@JsonPropertyDescription("关键词标签列表")
private List<String> tags;
@JsonPropertyDescription("是否为敏感信息")
private Boolean isSensitive;
}
@Data
public static class MergeUnit {
@JsonPropertyDescription("一级分类")
private String category;
@JsonPropertyDescription("二级分类")
private String subCategory;
@JsonPropertyDescription("关键词标签列表")
private List<String> tags;
@JsonPropertyDescription("记忆中的键值对")
private Map<String, String> keyValues;
@JsonPropertyDescription("是否为敏感信息")
private Boolean isSensitive;
}
}

View File

@@ -0,0 +1,27 @@
package com.xspaceagi.memory.app.service.dto;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import lombok.Data;
import java.util.List;
@Data
public class MemoryQueryExtractDto {
@JsonPropertyDescription("判断是否需要搜索记忆如果问题不需要查询记忆shouldSearch 设为 false")
private Boolean shouldSearch;
@JsonPropertyDescription("必须严格从上述一级分类中选择,不能自创")
private List<String> categories;
@JsonPropertyDescription("必须严格从上述二级分类中选择,不能自创")
private List<String> subCategories;
@JsonPropertyDescription("关键词标签列表,分词可以细粒度一点,尽量多生成有助于更精确查找")
private List<String> tags;
@JsonPropertyDescription("是否需要记忆的历史变更记录如果问题不需要requiresHistory 设为 false")
private Boolean requiresHistory;
@JsonPropertyDescription("查询范围可选值为today|yesterday|recent|long-term|all")
private String queryType;
@JsonPropertyDescription("时间范围非必填格式为2023-01-01 00:00:00-2023-01-01 23:59:59")
private String timeRange;
@JsonPropertyDescription("限制返回的记忆数量,默认为 10")
private Integer limit;
}

View File

@@ -0,0 +1,22 @@
package com.xspaceagi.memory.app.service.dto;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import lombok.Data;
import java.util.Date;
import java.util.Map;
@Data
public class MemoryUnitMergeInfo {
@JsonPropertyDescription("记忆ID用于更新")
private Long memoryId;
@JsonPropertyDescription("一级分类")
private String category;
@JsonPropertyDescription("二级分类")
private String subCategory;
@JsonPropertyDescription("记忆中的键值对")
private Map<String, String> keyValues;
@JsonPropertyDescription("创建时间")
private Date created;
}

View File

@@ -0,0 +1,530 @@
package com.xspaceagi.memory.app.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.hankcs.hanlp.HanLP;
import com.xspaceagi.agent.core.sdk.IModelRpcService;
import com.xspaceagi.memory.app.service.MemoryApplicationService;
import com.xspaceagi.memory.app.service.MemoryUnitService;
import com.xspaceagi.memory.app.service.MemoryUnitTagService;
import com.xspaceagi.memory.app.service.dto.MemoryExtractDto;
import com.xspaceagi.memory.app.service.dto.MemoryMergeResultDto;
import com.xspaceagi.memory.app.service.dto.MemoryQueryExtractDto;
import com.xspaceagi.memory.app.service.dto.MemoryUnitMergeInfo;
import com.xspaceagi.memory.infra.dao.entity.MemoryUnit;
import com.xspaceagi.memory.infra.dao.entity.MemoryUnitTag;
import com.xspaceagi.memory.sdk.dto.*;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.tenant.thread.TenantFunctions;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
@Slf4j
@Service
public class MemoryApplicationServiceImpl implements MemoryApplicationService {
// JSON key validation pattern: only alphanumeric, underscore, and dot allowed
private static final Pattern JSON_KEY_PATTERN = Pattern.compile("^[a-zA-Z0-9_.]+$");
@Autowired
private MemoryUnitTagService memoryUnitTagService;
/**
* Validate JSON keys to prevent SQL injection
* Only allows alphanumeric characters, underscore, and dot
*/
private void validateJsonKeys(Map<String, String> jsonKeyValues) {
if (jsonKeyValues == null || jsonKeyValues.isEmpty()) {
return;
}
for (String key : jsonKeyValues.keySet()) {
if (key == null || key.isEmpty()) {
throw new IllegalArgumentException("JSON key cannot be null or empty");
}
if (!JSON_KEY_PATTERN.matcher(key).matches()) {
throw new IllegalArgumentException("Invalid JSON key: " + key +
". Only alphanumeric characters, underscore, and dot are allowed");
}
}
}
private static final String MEMORY_EXTRACT_PROMPT = """
你是一个记忆提取专家,分析用户输入,判断用户意图,提取需要记录的记忆。
## 用户输入
{{user_input}}
## 当前对话上下文
{{conversation_context}}
## 完整分类体系
### 一级分类
- profile: 用户画像
- auth: 认证信息
- social: 社交媒体
- project: 项目信息
- preference: 用户偏好
- task: 任务待办
- note: 笔记备忘
- idea: 创意想法
- event: 事件日程
- document: 文档资料
- code: 代码技术
- other: 其他类型
### 二级分类及可用键名
#### profile 子分类及键名
- personal: name, age, gender, birthday, avatar
- professional: company, position, department, title, employee_id
- location: city, address, timezone, country, coordinates
- contact: phone, email, wechat, telegram, slack
- identity: id_card, passport, license, ssn, tax_id
- account: account_id, phone, email, platform
#### auth 子分类及键名
- website: url, username, password, login_method
- service: service_name, api_key, app_id, app_secret, endpoint
- api_key: key, secret, permissions, expiry_date
- ssh_key: private_key, public_key, host, username, port
- oauth_token: access_token, refresh_token, token_type, expires_in
- database: host, port, username, password, database, connection_string
#### social 子分类及键名
- wechat: account_id, nickname, contacts, groups
- twitter: username, handle, followers, following
- linkedin: profile_url, connections, headline
- contacts: name, relation, contact_info, notes
- groups: group_name, members, topic, platform
#### project 子分类及键名
- ongoing: name, description, progress, start_date, owner
- completed: name, completion_date, summary, outcome
- planning: name, timeline, requirements, budget
- milestone: name, deadline, status, dependencies
- task: project_id, title, assignee, deadline, status
#### preference 子分类及键名
- travel: transport, airline, hotel_type, travel_style
- food: cuisine, restrictions, favorites, dislikes
- lifestyle: sleep_time, exercise, hobbies, daily_routine
- entertainment: music, movies, games, sports
- work: working_hours, tools, environment, work_style
- general: general_preference, preference_name, value, notes
#### task 子分类及键名
- daily: title, due_date, priority, description
- weekly: title, week, status, category
- project: project_id, title, assignee, status
- urgent: title, deadline, priority, impact
- recurring: title, frequency, next_due, pattern
#### note 子分类及键名
- meeting: date, participants, summary, action_items
- lecture: topic, key_points, references, instructor
- research: subject, findings, sources, methodology
- personal: topic, content, tags, created_at
#### idea 子分类及键名
- product: name, description, features, target_users
- feature: component, enhancement, benefit, complexity
- improvement: area, problem, solution, priority
- innovation: concept, potential, challenges, feasibility
#### event 子分类及键名
- meeting: title, time, location, attendees, agenda
- appointment: title, time, location, purpose
- reminder: title, time, repeat, notification_method
- deadline: task, due_date, priority, consequences
#### document 子分类及键名
- contract: title, parties, effective_date, expiry_date
- invoice: number, amount, due_date, vendor, status
- report: title, date, summary, author
- reference: title, author, url, tags, category
#### code 子分类及键名
- snippet: language, description, code, filename
- config: service, environment, settings, format
- api_doc: endpoint, method, parameters, response_format
- deployment: environment, version, changes, deploy_date
## 任务
1. 判断用户输入是否包含需要记忆的信息
2. 识别信息的分类(一级)和子分类(二级),必须从上述列表中精确选择
3. 提取关键值和对应的键名,必须使用上述列出的可用键名
4. 判断是否为敏感信息(密码、密钥等)
""";
private static final String MEMORY_QUERY_PROMPT = """
你是一个记忆查询分析专家,分析用户问题,匹配一二级分类、关键词以及时间范围。
## 用户输入
{{user_input}}
## 当前对话上下文
{{conversation_context}}
## 完整分类体系
### 一级分类
- profile: 用户画像
- auth: 认证信息
- social: 社交媒体
- project: 项目信息
- preference: 用户偏好
- task: 任务待办
- note: 笔记备忘
- idea: 创意想法
- event: 事件日程
- document: 文档资料
- code: 代码技术
- other: 其他类型
### 二级分类及可用键名
#### profile 子分类及键名
- personal: name, age, gender, birthday, avatar
- professional: company, position, department, title, employee_id
- location: city, address, timezone, country, coordinates
- contact: phone, email, wechat, telegram, slack
- identity: id_card, passport, license, ssn, tax_id
- account: account_id, phone, email, platform
#### auth 子分类及键名
- website: url, username, password, login_method
- service: service_name, api_key, app_id, app_secret, endpoint
- api_key: key, secret, permissions, expiry_date
- ssh_key: private_key, public_key, host, username, port
- oauth_token: access_token, refresh_token, token_type, expires_in
- database: host, port, username, password, database, connection_string
#### social 子分类及键名
- wechat: account_id, nickname, contacts, groups
- twitter: username, handle, followers, following
- linkedin: profile_url, connections, headline
- contacts: name, relation, contact_info, notes
- groups: group_name, members, topic, platform
#### project 子分类及键名
- ongoing: name, description, progress, start_date, owner
- completed: name, completion_date, summary, outcome
- planning: name, timeline, requirements, budget
- milestone: name, deadline, status, dependencies
- task: project_id, title, assignee, deadline, status
#### preference 子分类及键名
- travel: transport, airline, hotel_type, travel_style
- food: cuisine, restrictions, favorites, dislikes
- lifestyle: sleep_time, exercise, hobbies, daily_routine
- entertainment: music, movies, games, sports
- work: working_hours, tools, environment, work_style
- general: general_preference, preference_name, value, notes
#### task 子分类及键名
- daily: title, due_date, priority, description
- weekly: title, week, status, category
- project: project_id, title, assignee, status
- urgent: title, deadline, priority, impact
- recurring: title, frequency, next_due, pattern
#### note 子分类及键名
- meeting: date, participants, summary, action_items
- lecture: topic, key_points, references, instructor
- research: subject, findings, sources, methodology
- personal: topic, content, tags, created_at
#### idea 子分类及键名
- product: name, description, features, target_users
- feature: component, enhancement, benefit, complexity
- improvement: area, problem, solution, priority
- innovation: concept, potential, challenges, feasibility
#### event 子分类及键名
- meeting: title, time, location, attendees, agenda
- appointment: title, time, location, purpose
- reminder: title, time, repeat, notification_method
- deadline: task, due_date, priority, consequences
#### document 子分类及键名
- contract: title, parties, effective_date, expiry_date
- invoice: number, amount, due_date, vendor, status
- report: title, date, summary, author
- reference: title, author, url, tags, category
#### code 子分类及键名
- snippet: language, description, code, filename
- config: service, environment, settings, format
- api_doc: endpoint, method, parameters, response_format
- deployment: environment, version, changes, deploy_date
## 任务
1. 分析用户问题的意图
2. 识别需要查询的记忆分类(从一级分类中精确选择)
3. 如果问题匹配某个二级分类,也识别出来
4. 提取标签关键词
5. 判断是否需要约定时间范围
""";
private static final String MEMORY_MERGE_PROMPT = """
你是一个记忆合并判断专家,判断新记忆是否应该更新现有记忆。
## 现有记忆
{{existing_memory}}
## 新记忆内容
{{new_content}}
## 任务
1. 识别新旧记忆的关系(重复、更新、相关、独立)
2. 如果是更新判断需要更新的字段keyValues中的字段
3. 计算相似度评分
## 字段说明
- memoryId 记忆ID用于更新现有记忆
- category 一级记忆分类
- subCategory 二级记忆分类
- keyValues 一条记忆中对应的键值对
- created 已有记忆的创建时间
""";
@Resource
private IModelRpcService iModelRpcService;
@Resource
private MemoryUnitService memoryUnitService;
@Override
public List<MemoryExtractDto> createMemory(MemoryMetaData memoryData) {
boolean hasRequestContext = RequestContext.get() != null;
try {
if (!hasRequestContext) {
RequestContext<Object> requestContext = RequestContext.builder()
.tenantId(memoryData.getTenantId())
.userId(memoryData.getUserId())
.build();
RequestContext.set(requestContext);
}
return createMemory0(memoryData);
} finally {
if (!hasRequestContext) {
RequestContext.remove();
}
}
}
public List<MemoryExtractDto> createMemory0(MemoryMetaData memoryData) {
String userPrompt = MEMORY_EXTRACT_PROMPT.replace("{{user_input}}", memoryData.getUserInput()).replace("{{conversation_context}}", memoryData.getContext());
List<MemoryExtractDto> memoryExtractResults = iModelRpcService.call("当前时间: " + (new Date()), userPrompt, new ParameterizedTypeReference<List<MemoryExtractDto>>() {
});
log.debug("memoryExtractResults: {}", memoryExtractResults);
if (memoryExtractResults != null) {
// Validate JSON keys to prevent SQL injection
for (MemoryExtractDto memoryExtractDto : memoryExtractResults) {
Map<String, String> keyValues = memoryExtractDto.toMap();
validateJsonKeys(keyValues);
// 验证重复性
List<MemoryUnitDTO> byCategoryAndJsonKeyValues = TenantFunctions.callWithIgnoreCheck(() -> memoryUnitService.findByUserIdAndCategory(memoryData.getUserId(), memoryData.getAgentId(), memoryExtractDto.getCategory(), memoryExtractDto.getSubCategory()));
if (byCategoryAndJsonKeyValues.isEmpty()) {
MemoryUnitCreateDTO memoryUnitCreateDTO = new MemoryUnitCreateDTO();
memoryUnitCreateDTO.setCategory(memoryExtractDto.getCategory());
memoryUnitCreateDTO.setSubCategory(memoryExtractDto.getSubCategory());
memoryUnitCreateDTO.setKeyValues(memoryExtractDto.toMap());
memoryUnitCreateDTO.setTags(memoryExtractDto.getTags());
memoryUnitCreateDTO.setIsSensitive(memoryExtractDto.getIsSensitive());
memoryUnitCreateDTO.setUserId(memoryData.getUserId());
memoryUnitCreateDTO.setAgentId(memoryData.getAgentId());
memoryUnitCreateDTO.setTenantId(memoryData.getTenantId());
memoryUnitService.create(memoryUnitCreateDTO);
continue;
}
List<MemoryUnitMergeInfo> memoryUnitMergeInfos = byCategoryAndJsonKeyValues.stream().map(memoryUnitDTO -> {
MemoryUnitMergeInfo memoryUnitMergeInfo = new MemoryUnitMergeInfo();
memoryUnitMergeInfo.setMemoryId(memoryUnitDTO.getId());
memoryUnitMergeInfo.setCategory(memoryUnitDTO.getCategory());
memoryUnitMergeInfo.setSubCategory(memoryUnitDTO.getSubCategory());
JSONObject jsonObject = JSON.parseObject(memoryUnitDTO.getContentJson());
if (jsonObject != null) {
memoryUnitMergeInfo.setKeyValues(jsonObject.getObject("keyValues", Map.class));
}
memoryUnitMergeInfo.setCreated(memoryUnitDTO.getCreated());
return memoryUnitMergeInfo;
}).toList();
String mergePrompt = MEMORY_MERGE_PROMPT.replace("{{existing_memory}}", JSON.toJSONString(memoryUnitMergeInfos)).replace("{{new_content}}", JSON.toJSONString(memoryExtractDto));
MemoryMergeResultDto memoryMergeResultDto = iModelRpcService.call("当前时间: " + (new Date()), mergePrompt, new ParameterizedTypeReference<MemoryMergeResultDto>() {
});
log.debug("memoryMergeResultDto: {}", memoryMergeResultDto);
if (memoryMergeResultDto != null) {
List<MemoryMergeResultDto.MergeUnit> insertMemories = memoryMergeResultDto.getInsertMemories();
if (CollectionUtils.isNotEmpty(insertMemories)) {
for (MemoryMergeResultDto.MergeUnit insertMemory : insertMemories) {
insertMemory(memoryData.getTenantId(), memoryData.getUserId(), memoryData.getAgentId(), insertMemory.getCategory(), insertMemory.getSubCategory(), insertMemory.getTags(), insertMemory.getKeyValues(), insertMemory.getIsSensitive());
}
}
List<MemoryMergeResultDto.MergeUnitWithID> updateMemoryKeyValues = memoryMergeResultDto.getUpdateMemoryKeyValues();
if (CollectionUtils.isNotEmpty(updateMemoryKeyValues)) {
for (MemoryMergeResultDto.MergeUnitWithID updateMemoryKeyValue : updateMemoryKeyValues) {
if (updateMemoryKeyValue.getMemoryId() == null || keyValues == null) {
continue;
}
updateMemory(memoryData.getUserId(), updateMemoryKeyValue.getMemoryId(), updateMemoryKeyValue.getTags(), updateMemoryKeyValue.getKeyValues(), memoryExtractDto.getIsSensitive());
}
}
}
}
}
return memoryExtractResults;
}
@Transactional(rollbackFor = Exception.class)
@Override
public void saveMemories(Long tenantId, Long userId, Long agentId, MemorySaveDto memorySaveDto) {
if (memorySaveDto.getInsertMemories() != null) {
for (MemorySaveDto.InsertUnit insertUnit : memorySaveDto.getInsertMemories()) {
insertMemory(tenantId, userId, agentId, insertUnit.getCategory(), insertUnit.getSubCategory(), insertUnit.getTags(), insertUnit.getKeyValues(), insertUnit.getIsSensitive());
}
}
if (memorySaveDto.getUpdateMemoryKeyValues() != null) {
for (MemorySaveDto.UpdateUnitWithID updateUnitWithID : memorySaveDto.getUpdateMemoryKeyValues()) {
if (updateUnitWithID.getMemoryId() == null || updateUnitWithID.getKeyValues() == null) {
continue;
}
updateMemory(userId, updateUnitWithID.getMemoryId(), updateUnitWithID.getTags(), updateUnitWithID.getKeyValues(), updateUnitWithID.getIsSensitive());
}
}
}
private void insertMemory(Long tenantId, Long userId, Long agentId, String category, String subCategory, List<String> tags, Map<String, String> keyValues, Boolean isSensitive) {
if (keyValues == null || keyValues.isEmpty() || category == null || subCategory == null) {
return;
}
MemoryUnitCreateDTO memoryUnitCreateDTO = new MemoryUnitCreateDTO();
memoryUnitCreateDTO.setCategory(category);
memoryUnitCreateDTO.setSubCategory(subCategory);
memoryUnitCreateDTO.setKeyValues(keyValues);
memoryUnitCreateDTO.setTags(tags);
memoryUnitCreateDTO.setIsSensitive(isSensitive);
memoryUnitCreateDTO.setUserId(userId);
memoryUnitCreateDTO.setAgentId(agentId);
memoryUnitCreateDTO.setTenantId(tenantId);
memoryUnitService.create(memoryUnitCreateDTO);
}
private void updateMemory(Long userId, Long memoryId, List<String> updateTags, Map<String, String> keyValues, Boolean isSensitive) {
MemoryUnitDTO memoryUnitDTO = memoryUnitService.getById(memoryId);
if (memoryUnitDTO != null && memoryUnitDTO.getUserId().equals(userId)) {
JSONObject jsonObject = JSON.parseObject(memoryUnitDTO.getContentJson());
JSONObject oldKeyValues = jsonObject.getJSONObject("keyValues");
if (oldKeyValues == null) {
oldKeyValues = new JSONObject();
}
List<String> newTags = new ArrayList<>();
JSONArray tags = jsonObject.getJSONArray("tags");
if (CollectionUtils.isNotEmpty(tags) && CollectionUtils.isNotEmpty(updateTags)) {
updateTags.forEach(tag -> {
if (!tags.contains(tag)) {
newTags.add(tag);
tags.add(tag);
}
});
}
oldKeyValues.putAll(keyValues);
MemoryUnitUpdateDTO updateDTO = new MemoryUnitUpdateDTO();
updateDTO.setId(memoryUnitDTO.getId());
updateDTO.setUserId(userId);
updateDTO.setContentJson(jsonObject.toJSONString());
updateDTO.setTags(newTags);
updateDTO.setIsSensitive(isSensitive);
memoryUnitService.update(updateDTO);
}
}
@Override
public void deleteMemories(Long tenantId, Long userId, List<Long> memoryIds) {
if (memoryIds == null || memoryIds.isEmpty()) {
return;
}
LambdaUpdateWrapper<MemoryUnit> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(MemoryUnit::getUserId, userId);
updateWrapper.eq(MemoryUnit::getTenantId, tenantId);
updateWrapper.in(MemoryUnit::getId, memoryIds);
updateWrapper.set(MemoryUnit::getStatus, "deleted");
memoryUnitService.update(updateWrapper);
LambdaUpdateWrapper<MemoryUnitTag> memoryUnitTagLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
memoryUnitTagLambdaUpdateWrapper.in(MemoryUnitTag::getMemoryId, memoryIds);
memoryUnitTagService.remove(memoryUnitTagLambdaUpdateWrapper);
}
@Override
public List<MemoryUnitDTO> searchMemories(Long tenantId, Long userId, Long agentId, String inputMessage, String context, boolean justKeywordSearch) {
boolean hasRequestContext = RequestContext.get() != null;
try {
if (!hasRequestContext) {
RequestContext<Object> requestContext = RequestContext.builder()
.tenantId(tenantId)
.userId(userId)
.build();
RequestContext.set(requestContext);
}
return searchMemories0(tenantId, userId, agentId, inputMessage, context, justKeywordSearch);
} finally {
if (!hasRequestContext) {
RequestContext.remove();
}
}
}
private List<MemoryUnitDTO> searchMemories0(Long tenantId, Long userId, Long agentId, String inputMessage, String context, boolean justKeywordSearch) {
try {
MemoryQueryExtractDto memoryQueryExtractDto = null;
if (!justKeywordSearch) {
try {
String userPrompt = MEMORY_QUERY_PROMPT.replace("{{user_input}}", inputMessage).replace("{{conversation_context}}", context);
memoryQueryExtractDto = iModelRpcService.call("当前时间:" + (new Date()), userPrompt, new ParameterizedTypeReference<MemoryQueryExtractDto>() {
});
if (memoryQueryExtractDto.getShouldSearch() != null && !memoryQueryExtractDto.getShouldSearch()) {
return List.of();
}
} catch (Exception ignored) {
}
}
if (memoryQueryExtractDto == null) {
// 预先加载个人信息,其他预先加载
// 生成全文检索关键词
memoryQueryExtractDto = new MemoryQueryExtractDto();
memoryQueryExtractDto.setCategories(List.of("profile"));
List<String> tags = new ArrayList<>();
HanLP.segment(inputMessage).forEach(term -> tags.add(term.word));
tags.removeIf(tag -> tag.length() < 2);
memoryQueryExtractDto.setTags(tags);
}
MemoryUnitQueryDTO memoryUnitQueryDTO = new MemoryUnitQueryDTO();
BeanUtils.copyProperties(memoryQueryExtractDto, memoryUnitQueryDTO);
memoryUnitQueryDTO.setUserId(userId);
memoryUnitQueryDTO.setTenantId(tenantId);
memoryUnitQueryDTO.setJustKeywordSearch(justKeywordSearch);
memoryUnitQueryDTO.setAgentId(agentId);
return memoryUnitService.queryList(memoryUnitQueryDTO);
} catch (Exception e) {
log.warn("查询记忆失败, userId {}, inputMessage {}", userId, inputMessage, e);
return List.of();
}
}
}

View File

@@ -0,0 +1,80 @@
package com.xspaceagi.memory.app.service.impl;
import com.alibaba.fastjson2.JSONObject;
import com.xspaceagi.memory.app.service.MemoryApplicationService;
import com.xspaceagi.memory.app.service.MemoryUnitService;
import com.xspaceagi.memory.sdk.dto.MemoryMetaData;
import com.xspaceagi.memory.sdk.dto.MemorySaveDto;
import com.xspaceagi.memory.sdk.dto.MemoryUnitDTO;
import com.xspaceagi.memory.sdk.dto.MemoryUnitQueryDTO;
import com.xspaceagi.memory.sdk.service.IMemoryRpcService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
public class MemoryRpcServiceImpl implements IMemoryRpcService {
@Resource
private MemoryApplicationService memoryApplicationService;
@Resource
private MemoryUnitService memoryUnitService;
@Override
public void createMemory(MemoryMetaData memoryData) {
try {
log.debug("createMemory: {}", memoryData);
memoryApplicationService.createMemory(memoryData);
} catch (Exception e) {
log.error("createMemory error: {}", e.getMessage(), e);
// 由异常处理接管
}
}
@Override
public String searchMemoriesMd(Long tenantId, Long userId, Long agentId, String inputMessage, String context, boolean justKeywordSearch, boolean filterSensitive) {
log.debug("searchMemoriesMd: {}, {}, {}, {}, {}", tenantId, userId, inputMessage, context, justKeywordSearch);
List<MemoryUnitDTO> memoryUnitDTOS = memoryApplicationService.searchMemories(tenantId, userId, agentId, inputMessage, context, justKeywordSearch);
return toMd(memoryUnitDTOS, filterSensitive);
}
private String toMd(List<MemoryUnitDTO> memoryUnitDTOS, boolean filterSensitive) {
StringBuilder sb = new StringBuilder();
memoryUnitDTOS.forEach(memoryUnitDTO -> {
if (filterSensitive && memoryUnitDTO.getIsSensitive()) {
return;
}
JSONObject jsonObject = JSONObject.parseObject(memoryUnitDTO.getContentJson());
JSONObject keyValues = jsonObject.getJSONObject("keyValues");
String category = memoryUnitDTO.getCategory();//md的一级标题
String subCategory = memoryUnitDTO.getSubCategory();// md的二级标题
sb.append("# ").append(category).append("\n");
sb.append("## ").append(subCategory).append("\n");
keyValues.forEach((key, value) -> sb.append("- ").append(key).append(": ").append(value).append("\n"));
sb.append("- memory_id: ").append(memoryUnitDTO.getId()).append("\n");
sb.append("- update_time: ").append(memoryUnitDTO.getModified()).append("\n");
sb.append("\n");
});
return sb.toString();
}
@Override
public String queryMemoriesMd(MemoryUnitQueryDTO memoryUnitQueryDTO, boolean filterSensitive) {
List<MemoryUnitDTO> memoryUnitDTOS = memoryUnitService.queryList(memoryUnitQueryDTO);
return toMd(memoryUnitDTOS, filterSensitive);
}
@Override
public void saveMemories(Long tenantId, Long userId, Long agentId, MemorySaveDto memorySaveDto) {
memoryApplicationService.saveMemories(tenantId, userId, agentId, memorySaveDto);
}
@Override
public void deleteMemories(Long tenantId, Long userId, List<Long> memoryIds) {
memoryApplicationService.deleteMemories(tenantId, userId, memoryIds);
}
}

View File

@@ -0,0 +1,331 @@
package com.xspaceagi.memory.app.service.impl;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xspaceagi.memory.app.service.MemoryUnitService;
import com.xspaceagi.memory.app.service.MemoryUnitTagService;
import com.xspaceagi.memory.infra.dao.entity.MemoryUnit;
import com.xspaceagi.memory.infra.dao.mapper.MemoryUnitMapper;
import com.xspaceagi.memory.sdk.dto.*;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
/**
* 记忆单元服务实现
*/
@Slf4j
@Service
public class MemoryUnitServiceImpl extends ServiceImpl<MemoryUnitMapper, MemoryUnit> implements MemoryUnitService {
@Resource
private MemoryUnitTagService memoryUnitTagService;
@Resource
private MemoryUnitMapper memoryUnitMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public MemoryUnitDTO create(MemoryUnitCreateDTO createDTO) {
log.info("创建记忆单元: {}", createDTO);
JSONObject contentJson = new JSONObject();
contentJson.put("keyValues", createDTO.getKeyValues());
contentJson.put("tags", createDTO.getTags());
MemoryUnit entity = MemoryUnit.builder()
.tenantId(createDTO.getTenantId()) // 这里可以根据实际业务逻辑获取租户ID
.userId(createDTO.getUserId())
.agentId(createDTO.getAgentId())
.category(createDTO.getCategory())
.subCategory(createDTO.getSubCategory())
.contentJson(contentJson.toJSONString())
.isSensitive(createDTO.getIsSensitive() != null ? createDTO.getIsSensitive() : false)
.status("active")
.build();
save(entity);
log.info("记忆单元创建成功: {}", entity.getId());
if (CollectionUtils.isNotEmpty(createDTO.getTags())) {
createDTO.getTags().forEach(tag -> {
MemoryUnitTagCreateDTO memoryUnitTagCreateDTO = new MemoryUnitTagCreateDTO();
memoryUnitTagCreateDTO.setUserId(createDTO.getUserId());
memoryUnitTagCreateDTO.setMemoryId(entity.getId());
memoryUnitTagCreateDTO.setTagName(tag);
memoryUnitTagService.create(memoryUnitTagCreateDTO);
});
}
return toDTO(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public MemoryUnitDTO update(MemoryUnitUpdateDTO updateDTO) {
log.info("更新记忆单元: {}", updateDTO);
MemoryUnit entity = super.getById(updateDTO.getId());
if (entity == null) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.memoryUnitNotFound, updateDTO.getId());
}
if (updateDTO.getCategory() != null) {
entity.setCategory(updateDTO.getCategory());
}
if (updateDTO.getSubCategory() != null) {
entity.setSubCategory(updateDTO.getSubCategory());
}
if (updateDTO.getContentJson() != null) {
entity.setContentJson(updateDTO.getContentJson());
}
if (updateDTO.getIsSensitive() != null) {
entity.setIsSensitive(updateDTO.getIsSensitive());
}
if (updateDTO.getStatus() != null) {
entity.setStatus(updateDTO.getStatus());
}
entity.setModified(new Date());
updateById(entity);
saveTagRelation(updateDTO.getUserId(), entity.getId(), updateDTO.getTags());
log.info("记忆单元更新成功: {}", entity.getId());
return toDTO(entity);
}
private void saveTagRelation(Long userId, Long memoryId, List<String> tags) {
if (CollectionUtils.isNotEmpty(tags)) {
tags.forEach(tag -> {
MemoryUnitTagCreateDTO memoryUnitTagCreateDTO = new MemoryUnitTagCreateDTO();
memoryUnitTagCreateDTO.setUserId(userId);
memoryUnitTagCreateDTO.setMemoryId(memoryId);
memoryUnitTagCreateDTO.setTagName(tag);
try {
memoryUnitTagService.create(memoryUnitTagCreateDTO);
} catch (Exception ignored) {
}
});
}
}
@Override
public MemoryUnitDTO getById(Long id) {
MemoryUnit entity = super.getById(id);
return entity != null ? toDTO(entity) : null;
}
@Override
public List<MemoryUnitDTO> queryList(MemoryUnitQueryDTO queryDTO) {
log.info("查询记忆单元列表: {}", queryDTO);
List<MemoryUnit> entities;
List<Date> timeRange = null;
boolean timeRangeQuery = false;
try {
timeRange = parseTimeRange(queryDTO);
timeRangeQuery = timeRange.size() == 2;
} catch (Exception ignored) {
}
LambdaQueryWrapper<MemoryUnit> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(MemoryUnit::getStatus, "active");
queryWrapper.eq(MemoryUnit::getTenantId, queryDTO.getTenantId());
queryWrapper.eq(MemoryUnit::getUserId, queryDTO.getUserId());
queryWrapper.eq(MemoryUnit::getAgentId, queryDTO.getAgentId());
queryWrapper.in(CollectionUtils.isNotEmpty(queryDTO.getCategories()), MemoryUnit::getCategory, queryDTO.getCategories());
queryWrapper.in(CollectionUtils.isNotEmpty(queryDTO.getSubCategories()), MemoryUnit::getSubCategory, queryDTO.getSubCategories());
queryWrapper.orderByDesc(MemoryUnit::getId);
if (timeRangeQuery) {
queryWrapper.between(MemoryUnit::getModified, timeRange.get(0), timeRange.get(1));
}
queryWrapper.last("LIMIT " + (queryDTO.getLimit() == null ? 10 : queryDTO.getLimit()));
entities = list(queryWrapper);
if (CollectionUtils.isNotEmpty(queryDTO.getTags())) {
List<MemoryUnit> memoryUnits = memoryUnitMapper.selectWithJoinTags(queryDTO.getUserId(), queryDTO.getAgentId(), queryDTO.getTags(), timeRangeQuery ? timeRange.get(0) : null, timeRangeQuery ? timeRange.get(1) : null, 0L, 10L);
// 移除entries中id已包含的
memoryUnits.removeIf(memoryUnit -> entities.stream().anyMatch(entity -> entity.getId().equals(memoryUnit.getId())));
entities.addAll(memoryUnits);
}
return entities.stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
private List<Date> parseTimeRange(MemoryUnitQueryDTO queryDTO) {
//查询范围可选值为today|yesterday|recent|long-term|all
String queryType = queryDTO.getQueryType();
//时间范围非必填格式为2023-01-01 00:00:00-2023-01-01 23:59:59
//有可能为空
String timeRange = queryDTO.getTimeRange();
List<Date> dates = new ArrayList<>();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
// 优先使用自定义时间范围
if (StringUtils.isNotBlank(timeRange)) {
String[] times = timeRange.split("-");
if (times.length == 2) {
Date startTime = sdf.parse(times[0]);
Date endTime = sdf.parse(times[1]);
dates.add(startTime);
dates.add(endTime);
return dates;
}
}
// 根据查询类型计算时间范围
Calendar calendar = Calendar.getInstance();
Date startTime, endTime;
switch (queryType) {
case "today":
// 今天 00:00:00 - 当前时间
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
startTime = calendar.getTime();
endTime = new Date();
break;
case "yesterday":
// 昨天 00:00:00 - 23:59:59
calendar.add(Calendar.DAY_OF_MONTH, -1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
startTime = calendar.getTime();
calendar.set(Calendar.HOUR_OF_DAY, 23);
calendar.set(Calendar.MINUTE, 59);
calendar.set(Calendar.SECOND, 59);
calendar.set(Calendar.MILLISECOND, 999);
endTime = calendar.getTime();
break;
case "recent":
// 最近7天
calendar.add(Calendar.DAY_OF_MONTH, -7);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
startTime = calendar.getTime();
endTime = new Date();
break;
case "long-term":
// 最近30天
calendar.add(Calendar.DAY_OF_MONTH, -30);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
startTime = calendar.getTime();
endTime = new Date();
break;
case "all":
// 全部数据,返回空列表
return new ArrayList<>();
default:
// 默认返回最近7天
calendar.add(Calendar.DAY_OF_MONTH, -7);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
startTime = calendar.getTime();
endTime = new Date();
break;
}
dates.add(startTime);
dates.add(endTime);
} catch (ParseException e) {
throw new RuntimeException("时间格式解析失败正确格式为yyyy-MM-dd HH:mm:ss-yyyy-MM-dd HH:mm:ss", e);
}
return dates;
}
@Override
public List<MemoryUnitDTO> findByUserIdAndCategory(Long userId, Long agentId, String category, String subCategory) {
List<MemoryUnit> entities = baseMapper.findByUserIdAndCategory(userId, agentId, category, subCategory);
return entities.stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
@Override
public List<MemoryUnitDTO> findByAgentId(Long agentId) {
List<MemoryUnit> entities = baseMapper.findByAgentId(agentId);
return entities.stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
@Override
public List<MemoryUnitDTO> findByCategoryAndJsonKeyValue(Long tenantId, Long userId, String category, String subCategory, String jsonKey, String jsonValue) {
log.info("根据分类和JSON字段值查询: tenantId={}, userId={}, category={}, subCategory={}, jsonKey={}, jsonValue={}",
tenantId, userId, category, subCategory, jsonKey, jsonValue);
List<MemoryUnit> entities = baseMapper.findByCategoryAndJsonKeyValue(tenantId, userId, category, subCategory, jsonKey, jsonValue);
return entities.stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
@Override
public List<MemoryUnitDTO> findByCategoryAndJsonKeyValueLike(Long tenantId, Long userId, String category, String subCategory, String jsonKey, String jsonValue) {
log.info("根据分类和JSON字段值查询(模糊匹配): tenantId={}, userId={}, category={}, subCategory={}, jsonKey={}, jsonValue={}",
tenantId, userId, category, subCategory, jsonKey, jsonValue);
List<MemoryUnit> entities = baseMapper.findByCategoryAndJsonKeyValueLike(tenantId, userId, category, subCategory, jsonKey, jsonValue);
return entities.stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
@Override
public List<MemoryUnitDTO> findByCategoryAndJsonKeyValues(Long tenantId, Long userId, String category, String subCategory, Map<String, String> jsonKeyValues) {
log.info("根据分类和JSON多字段值查询: tenantId={}, userId={}, category={}, subCategory={}, jsonKeyValues={}",
tenantId, userId, category, subCategory, jsonKeyValues);
List<MemoryUnit> entities = baseMapper.findByCategoryAndJsonKeyValues(tenantId, userId, category, subCategory, jsonKeyValues);
return entities.stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
@Override
public MemoryUnitDTO toDTO(MemoryUnit entity) {
if (entity == null) {
return null;
}
return MemoryUnitDTO.builder()
.id(entity.getId())
.tenantId(entity.getTenantId())
.userId(entity.getUserId())
.agentId(entity.getAgentId())
.category(entity.getCategory())
.subCategory(entity.getSubCategory())
.contentJson(entity.getContentJson())
.isSensitive(entity.getIsSensitive())
.status(entity.getStatus())
.created(entity.getCreated())
.modified(entity.getModified())
.build();
}
}

View File

@@ -0,0 +1,69 @@
package com.xspaceagi.memory.app.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xspaceagi.memory.app.service.MemoryUnitTagService;
import com.xspaceagi.memory.infra.dao.entity.MemoryUnitTag;
import com.xspaceagi.memory.infra.dao.mapper.MemoryUnitTagMapper;
import com.xspaceagi.memory.sdk.dto.MemoryUnitTagCreateDTO;
import com.xspaceagi.memory.sdk.dto.MemoryUnitTagDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 记忆单元标签服务实现
*/
@Slf4j
@Service
public class MemoryUnitTagServiceImpl extends ServiceImpl<MemoryUnitTagMapper, MemoryUnitTag> implements MemoryUnitTagService {
@Override
@Transactional(rollbackFor = Exception.class)
public MemoryUnitTagDTO create(MemoryUnitTagCreateDTO createDTO) {
log.info("创建记忆标签: {}", createDTO);
MemoryUnitTag entity = MemoryUnitTag.builder()
.tenantId(createDTO.getUserId()) // 这里可以根据实际业务逻辑获取租户ID
.userId(createDTO.getUserId())
.memoryId(createDTO.getMemoryId())
.tagName(createDTO.getTagName())
.created(new Date())
.build();
save(entity);
log.info("记忆标签创建成功: {}", entity.getId());
return toDTO(entity);
}
@Override
public List<MemoryUnitTagDTO> searchByTagNames(Long userId, List<String> tags) {
if (tags == null || tags.isEmpty()) {
return new ArrayList<>();
}
List<MemoryUnitTag> list = list(new QueryWrapper<MemoryUnitTag>().eq("user_id", userId).in("tag_name", tags));
return list.stream().map(this::toDTO).collect(Collectors.toList());
}
@Override
public MemoryUnitTagDTO toDTO(MemoryUnitTag entity) {
if (entity == null) {
return null;
}
return MemoryUnitTagDTO.builder()
.id(entity.getId())
.tenantId(entity.getTenantId())
.userId(entity.getUserId())
.memoryId(entity.getMemoryId())
.tagName(entity.getTagName())
.created(entity.getCreated())
.modified(entity.getModified())
.build();
}
}

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>app-platform-memory</artifactId>
<groupId>com.xspaceagi</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-memory-domain</artifactId>
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-spec</artifactId>
</dependency>
<!-- 其他原有依赖保持不变 -->
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-sdk</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,111 @@
package com.xspaceagi.memory.domain.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 记忆领域模型
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Memory {
/**
* 记忆ID
*/
private String memoryId;
/**
* 用户ID
*/
private String userId;
/**
* 会话ID
*/
private String sessionId;
/**
* 记忆类型 (short_term, long_term, working)
*/
private String memoryType;
/**
* 记忆内容
*/
private String content;
/**
* 扩展元数据
*/
private Map<String, Object> metadata;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 过期时间
*/
private LocalDateTime expireTime;
/**
* 记忆状态 (active, archived, deleted)
*/
private String status;
/**
* 重要性评分 (0.0-1.0)
*/
private Double importanceScore;
/**
* 访问次数
*/
private Integer accessCount;
/**
* 最后访问时间
*/
private LocalDateTime lastAccessTime;
/**
* 标签列表
*/
private String tags;
/**
* 检查记忆是否过期
*/
public boolean isExpired() {
return expireTime != null && LocalDateTime.now().isAfter(expireTime);
}
/**
* 检查记忆是否活跃
*/
public boolean isActive() {
return "active".equals(status) && !isExpired();
}
/**
* 增加访问次数
*/
public void incrementAccessCount() {
this.accessCount = (this.accessCount == null ? 0 : this.accessCount) + 1;
this.lastAccessTime = LocalDateTime.now();
}
}

View File

@@ -0,0 +1,63 @@
package com.xspaceagi.memory.domain.repository;
import com.xspaceagi.memory.domain.model.Memory;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* 记忆仓储接口
*/
public interface MemoryRepository {
/**
* 保存记忆
*/
Memory save(Memory memory);
/**
* 根据ID查找记忆
*/
Optional<Memory> findById(String memoryId);
/**
* 根据用户ID和记忆类型查找记忆列表
*/
List<Memory> findByUserIdAndType(String userId, String memoryType);
/**
* 根据会话ID查找记忆列表
*/
List<Memory> findBySessionId(String sessionId);
/**
* 查找过期的记忆
*/
List<Memory> findExpiredMemories(LocalDateTime currentTime);
/**
* 根据重要性分数查找记忆
*/
List<Memory> findByImportanceScoreGreaterThan(Double minScore);
/**
* 删除记忆
*/
void deleteById(String memoryId);
/**
* 批量删除记忆
*/
void batchDelete(List<String> memoryIds);
/**
* 更新记忆状态
*/
void updateStatus(String memoryId, String status);
/**
* 统计用户记忆数量
*/
long countByUserIdAndType(String userId, String memoryType);
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>app-platform-memory</artifactId>
<groupId>com.xspaceagi</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-infra</artifactId>
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-domain</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>system-sdk</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,84 @@
package com.xspaceagi.memory.infra.dao.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* 记忆单元实体
* @TableName memory_unit
*/
@TableName(value = "memory_unit")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemoryUnit {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 租户ID
*/
@TableField(value = "_tenant_id")
private Long tenantId;
/**
* 用户ID
*/
private Long userId;
/**
* 代理ID
*/
private Long agentId;
/**
* 一级分类
*/
private String category;
/**
* 二级分类
*/
private String subCategory;
/**
* 内容JSON
*/
private String contentJson;
/**
* 是否敏感信息(0:否 1:是)
*/
private Boolean isSensitive;
/**
* 状态
*/
private String status;
/**
* 创建时间
*/
private Date created;
/**
* 修改时间
*/
private Date modified;
}

View File

@@ -0,0 +1,64 @@
package com.xspaceagi.memory.infra.dao.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* 记忆单元标签实体
* @TableName memory_unit_tag
*/
@TableName(value = "memory_unit_tag")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemoryUnitTag {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 租户ID
*/
@TableField(value = "_tenant_id")
private Long tenantId;
/**
* 用户ID
*/
private Long userId;
/**
* 记忆ID
*/
private Long memoryId;
/**
* 标签名称
*/
private String tagName;
/**
* 创建时间
*/
private Date created;
/**
* 修改时间
*/
private Date modified;
}

View File

@@ -0,0 +1,137 @@
package com.xspaceagi.memory.infra.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.xspaceagi.memory.infra.dao.entity.MemoryUnit;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 记忆单元Mapper
*
* @description 针对表【memory_unit(记忆单元表)】的数据库操作Mapper
* @createDate 2026-03-06
* @Entity com.xspaceagi.memory.infra.dao.entity.MemoryUnit
*/
public interface MemoryUnitMapper extends BaseMapper<MemoryUnit> {
/**
* 总条数查询
*
* @param queryMap 筛选条件
* @return 总条数
*/
Long queryTotal(@Param("queryMap") Map<String, Object> queryMap);
/**
* 列表查询
*
* @param queryMap 筛选条件
* @param orderColumns 排序
* @param startIndex 索引开始位置
* @param pageSize 业务大小
* @return 列表
*/
List<MemoryUnit> queryList(@Param("queryMap") Map<String, Object> queryMap,
@Param("orderColumns") List<OrderItem> orderColumns,
@Param("startIndex") Long startIndex, @Param("pageSize") Long pageSize);
/**
* 根据用户ID和分类查询记忆单元
*
* @param userId 用户ID
* @param category 一级分类
* @return 记忆单元列表
*/
List<MemoryUnit> findByUserIdAndCategory(@Param("userId") Long userId, @Param("agentId") Long agentId, @Param("category") String category, @Param("subCategory") String subCategory);
/**
* 根据代理ID查询记忆单元
*
* @param agentId 代理ID
* @return 记忆单元列表
*/
List<MemoryUnit> findByAgentId(@Param("agentId") Long agentId);
/**
* 根据分类和JSON字段值查询记忆单元精确匹配
*
* @param tenantId 租户ID
* @param userId 用户ID
* @param category 一级分类
* @param subCategory 二级分类
* @param jsonKey JSON中的键
* @param jsonValue JSON中的值
* @return 记忆单元列表
*/
List<MemoryUnit> findByCategoryAndJsonKeyValue(
@Param("tenantId") Long tenantId,
@Param("userId") Long userId,
@Param("category") String category,
@Param("subCategory") String subCategory,
@Param("jsonKey") String jsonKey,
@Param("jsonValue") String jsonValue
);
/**
* 根据分类和JSON字段值查询记忆单元模糊匹配
*
* @param tenantId 租户ID
* @param userId 用户ID
* @param category 一级分类
* @param subCategory 二级分类
* @param jsonKey JSON中的键
* @param jsonValue JSON中的值
* @return 记忆单元列表
*/
List<MemoryUnit> findByCategoryAndJsonKeyValueLike(
@Param("tenantId") Long tenantId,
@Param("userId") Long userId,
@Param("category") String category,
@Param("subCategory") String subCategory,
@Param("jsonKey") String jsonKey,
@Param("jsonValue") String jsonValue
);
/**
* 根据分类和JSON多个字段值查询记忆单元
*
* @param tenantId 租户ID
* @param userId 用户ID
* @param category 一级分类
* @param subCategory 二级分类
* @param jsonKeyValues JSON键值对 Map<key, value>
* @return 记忆单元列表
*/
List<MemoryUnit> findByCategoryAndJsonKeyValues(
@Param("tenantId") Long tenantId,
@Param("userId") Long userId,
@Param("category") String category,
@Param("subCategory") String subCategory,
@Param("jsonKeyValues") Map<String, String> jsonKeyValues
);
/**
* 根据用户ID、代理ID和标签查询记忆单元
*
* @param userId 用户ID
* @param agentId 代理ID
* @param tags 标签列表
* @param startTime 开始时间
* @param endTime 结束时间
* @param startIndex 索引开始位置
* @param pageSize 业务大小
* @return 记忆单元列表
*/
List<MemoryUnit> selectWithJoinTags(@Param("userId") Long userId,
@Param("agentId") Long agentId,
@Param("tags") List<String> tags,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime,
@Param("startIndex") Long startIndex,
@Param("pageSize") Long pageSize);
}

View File

@@ -0,0 +1,58 @@
package com.xspaceagi.memory.infra.dao.mapper;
import com.xspaceagi.memory.infra.dao.entity.MemoryUnitTag;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* 记忆单元标签Mapper
* @description 针对表【memory_unit_tag(记忆单元标签表)】的数据库操作Mapper
* @createDate 2026-03-06
* @Entity com.xspaceagi.memory.infra.dao.entity.MemoryUnitTag
*/
public interface MemoryUnitTagMapper extends BaseMapper<MemoryUnitTag> {
/**
* 根据记忆ID查询标签列表
*
* @param memoryId 记忆ID
* @return 标签列表
*/
List<MemoryUnitTag> findByMemoryId(@Param("memoryId") Long memoryId);
/**
* 根据用户ID查询标签列表
*
* @param userId 用户ID
* @return 标签列表
*/
List<MemoryUnitTag> findByUserId(@Param("userId") Long userId);
/**
* 根据标签名称查询标签列表
*
* @param tagName 标签名称
* @return 标签列表
*/
List<MemoryUnitTag> findByTagName(@Param("tagName") String tagName);
/**
* 根据记忆ID删除标签
*
* @param memoryId 记忆ID
* @return 删除数量
*/
int deleteByMemoryId(@Param("memoryId") Long memoryId);
/**
* 批量插入标签
*
* @param tags 标签列表
* @return 插入数量
*/
int batchInsert(@Param("tags") List<MemoryUnitTag> tags);
}

View File

@@ -0,0 +1,192 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xspaceagi.memory.infra.dao.mapper.MemoryUnitMapper">
<resultMap id="BaseResultMap" type="com.xspaceagi.memory.infra.dao.entity.MemoryUnit">
<id property="id" column="id"/>
<result property="tenantId" column="_tenant_id"/>
<result property="userId" column="user_id"/>
<result property="agentId" column="agent_id"/>
<result property="category" column="category"/>
<result property="subCategory" column="sub_category"/>
<result property="contentJson" column="content_json"/>
<result property="isSensitive" column="is_sensitive"/>
<result property="status" column="status"/>
<result property="created" column="created"/>
<result property="modified" column="modified"/>
</resultMap>
<!-- 查询条件 -->
<sql id="base_where">
<if test="queryMap.tenantId != null">
and _tenant_id = #{queryMap.tenantId}
</if>
<if test="queryMap.userId != null">
and user_id = #{queryMap.userId}
</if>
<if test="queryMap.agentId != null">
and agent_id = #{queryMap.agentId}
</if>
<if test="queryMap.category != null and queryMap.category != ''">
and category = #{queryMap.category}
</if>
<if test="queryMap.subCategory != null and queryMap.subCategory != ''">
and sub_category = #{queryMap.subCategory}
</if>
<if test="queryMap.isSensitive != null">
and is_sensitive = #{queryMap.isSensitive}
</if>
<if test="queryMap.status != null and queryMap.status != ''">
and status = #{queryMap.status}
</if>
<if test="queryMap.keyword != null and queryMap.keyword != ''">
and (category like concat('%',#{queryMap.keyword},'%')
or sub_category like concat('%',#{queryMap.keyword},'%')
or content_json like concat('%',#{queryMap.keyword},'%'))
</if>
</sql>
<!-- 总条数查询 -->
<select id="queryTotal" resultType="java.lang.Long">
select count(*)
from memory_unit
<where>
<include refid="base_where"></include>
</where>
</select>
<!-- 列表查询 -->
<select id="queryList" resultMap="BaseResultMap">
select *
from memory_unit
<where>
<include refid="base_where"></include>
</where>
<choose>
<when test="orderColumns != null and orderColumns.size > 0">
<foreach collection="orderColumns" item="entity" index="index" separator="," open="order by " close="">
<if test="entity != null">
${entity.column}
<if test="!entity.asc">
desc
</if>
</if>
</foreach>
</when>
<otherwise>
order by created desc
</otherwise>
</choose>
limit #{startIndex}, #{pageSize}
</select>
<!-- 根据用户ID和分类查询 -->
<select id="findByUserIdAndCategory" resultMap="BaseResultMap">
select *
from memory_unit
where user_id = #{userId}
<if test="agentId != null">
and agent_id = #{agentId}
</if>
and category = #{category}
and sub_category = #{subCategory}
and status = 'active'
order by created desc limit 50
</select>
<!-- 根据代理ID查询 -->
<select id="findByAgentId" resultMap="BaseResultMap">
select *
from memory_unit
where agent_id = #{agentId}
and status = 'active'
order by created desc
</select>
<!-- 根据分类和JSON字段值查询精确匹配 -->
<select id="findByCategoryAndJsonKeyValue" resultMap="BaseResultMap">
select *
from memory_unit
where _tenant_id = #{tenantId}
and user_id = #{userId}
and category = #{category}
<if test="subCategory != null and subCategory != ''">
and sub_category = #{subCategory}
</if>
and JSON_CONTAINS(content_json, JSON_QUOTE(#{jsonValue}), '$.${jsonKey}')
and status = 'active'
order by created desc
</select>
<!-- 根据分类和JSON字段值查询模糊匹配 -->
<select id="findByCategoryAndJsonKeyValueLike" resultMap="BaseResultMap">
select *
from memory_unit
where _tenant_id = #{tenantId}
and user_id = #{userId}
and category = #{category}
<if test="subCategory != null and subCategory != ''">
and sub_category = #{subCategory}
</if>
and JSON_EXTRACT(content_json, '$.${jsonKey}') LIKE CONCAT('%', #{jsonValue}, '%')
and status = 'active'
order by created desc
</select>
<!-- 根据分类和JSON多个字段值查询 -->
<select id="findByCategoryAndJsonKeyValues" resultMap="BaseResultMap">
select *
from memory_unit
where _tenant_id = #{tenantId}
and user_id = #{userId}
and category = #{category}
<if test="subCategory != null and subCategory != ''">
and sub_category = #{subCategory}
</if>
<if test="jsonKeyValues != null and jsonKeyValues.size > 0">
and
<trim prefix="" prefixOverrides="and">
<foreach collection="jsonKeyValues" item="value" index="key" separator=" and ">
JSON_CONTAINS(content_json, JSON_QUOTE(#{value}), '$.keyValues.${key}')
</foreach>
</trim>
</if>
and status = 'active'
order by created desc
</select>
<!-- 根据用户ID、代理ID和标签查询记忆单元 -->
<select id="selectWithJoinTags" resultMap="BaseResultMap">
select distinct mu.*
from memory_unit mu
inner join memory_unit_tag mut on mu.id = mut.memory_id
<where>
<if test="userId != null">
and mu.user_id = #{userId}
</if>
<if test="agentId != null">
and mu.agent_id = #{agentId}
</if>
<if test="tags != null and tags.size > 0">
and mut.tag_name in
<foreach collection="tags" item="tag" open="(" separator="," close=")">
#{tag}
</foreach>
</if>
<if test="startTime != null">
and mu.created &gt;= #{startTime}
</if>
<if test="endTime != null">
and mu.created &lt;= #{endTime}
</if>
and mu.status = 'active'
</where>
order by mu.created desc
<if test="startIndex != null and pageSize != null">
limit #{startIndex}, #{pageSize}
</if>
</select>
</mapper>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xspaceagi.memory.infra.dao.mapper.MemoryUnitTagMapper">
<resultMap id="BaseResultMap" type="com.xspaceagi.memory.infra.dao.entity.MemoryUnitTag">
<id property="id" column="id"/>
<result property="tenantId" column="_tenant_id"/>
<result property="userId" column="user_id"/>
<result property="memoryId" column="memory_id"/>
<result property="tagName" column="tag_name"/>
<result property="created" column="created"/>
<result property="modified" column="modified"/>
</resultMap>
<!-- 根据记忆ID查询标签列表 -->
<select id="findByMemoryId" resultMap="BaseResultMap">
select *
from memory_unit_tag
where memory_id = #{memoryId}
order by created desc
</select>
<!-- 根据用户ID查询标签列表 -->
<select id="findByUserId" resultMap="BaseResultMap">
select *
from memory_unit_tag
where user_id = #{userId}
order by created desc
</select>
<!-- 根据标签名称查询标签列表 -->
<select id="findByTagName" resultMap="BaseResultMap">
select *
from memory_unit_tag
where tag_name = #{tagName}
order by created desc
</select>
<!-- 根据记忆ID删除标签 -->
<delete id="deleteByMemoryId">
delete from memory_unit_tag
where memory_id = #{memoryId}
</delete>
<!-- 批量插入标签 -->
<insert id="batchInsert">
insert into memory_unit_tag (_tenant_id, user_id, memory_id, tag_name, created)
values
<foreach collection="tags" item="tag" separator=",">
(#{tag.tenantId}, #{tag.userId}, #{tag.memoryId}, #{tag.tagName}, #{tag.created})
</foreach>
</insert>
</mapper>

View File

@@ -0,0 +1,153 @@
# 记忆管理模块 - 数据字典
## 表1: memory_unit (记忆单元表)
### 表说明
用于存储AI Agent的记忆单元数据支持多种类型的记忆分类和管理。
### 字段说明
| 字段名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| id | BIGINT(20) | 是 | AUTO_INCREMENT | 主键ID自增 |
| _tenant_id | BIGINT(20) | 是 | - | 租户ID多租户隔离标识 |
| user_id | BIGINT(20) | 是 | - | 用户ID记忆所属用户 |
| agent_id | BIGINT(20) | 否 | NULL | 代理ID关联的AI代理 |
| category | VARCHAR(50) | 是 | - | 一级分类,如:工作、学习、生活等 |
| sub_category | VARCHAR(100) | 否 | NULL | 二级分类,更细粒度的分类 |
| content_json | JSON | 否 | NULL | 记忆内容JSON格式存储结构化数据 |
| is_sensitive | TINYINT(1) | 是 | 0 | 是否敏感信息0-否1-是 |
| status | ENUM | 是 | 'active' | 状态active-活跃archived-已归档deleted-已删除 |
| created | DATETIME | 是 | CURRENT_TIMESTAMP | 创建时间 |
| modified | DATETIME | 是 | CURRENT_TIMESTAMP ON UPDATE | 修改时间 |
### 索引说明
| 索引名 | 字段 | 类型 | 说明 |
|--------|------|------|------|
| PRIMARY | id | PRIMARY | 主键索引 |
| idx_tenant_user | _tenant_id, user_id | INDEX | 租户用户联合索引,支持多租户查询 |
| idx_agent | agent_id | INDEX | 代理ID索引支持按代理查询 |
| idx_category | category, sub_category | INDEX | 分类联合索引,支持分类查询 |
| idx_status | status | INDEX | 状态索引,支持状态筛选 |
| idx_created | created | INDEX | 创建时间索引,支持时间范围查询 |
### 字段约束
- `status` 字段只能为:'active', 'archived', 'deleted'
- `is_sensitive` 只能为 0 或 1
- `_tenant_id``user_id` 必须有值,用于数据隔离
### 使用场景
1. **短期记忆**: 临时存储对话上下文
2. **长期记忆**: 持久化存储重要信息
3. **工作记忆**: 当前任务相关的活跃记忆
4. **敏感信息**: 标记需要特殊保护的数据
---
## 表2: memory_unit_tag (记忆单元标签表)
### 表说明
用于为记忆单元添加标签,支持灵活的分类和检索。
### 字段说明
| 字段名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| id | BIGINT(20) | 是 | AUTO_INCREMENT | 主键ID自增 |
| _tenant_id | BIGINT(20) | 是 | - | 租户ID多租户隔离标识 |
| user_id | BIGINT(20) | 是 | - | 用户ID标签创建者 |
| memory_id | BIGINT(20) | 是 | - | 记忆ID关联到memory_unit表 |
| tag_name | VARCHAR(100) | 是 | - | 标签名称 |
| created | DATETIME | 是 | CURRENT_TIMESTAMP | 创建时间 |
| modified | DATETIME | 是 | CURRENT_TIMESTAMP ON UPDATE | 修改时间 |
### 索引说明
| 索引名 | 字段 | 类型 | 说明 |
|--------|------|------|------|
| PRIMARY | id | PRIMARY | 主键索引 |
| idx_tenant_user | _tenant_id, user_id | INDEX | 租户用户联合索引 |
| idx_memory | memory_id | INDEX | 记忆ID索引支持按记忆查询标签 |
| idx_tag | tag_name | INDEX | 标签名称索引,支持按标签查询 |
### 外键约束
| 约束名 | 字段 | 关联表 | 关联字段 | 级联操作 |
|--------|------|--------|----------|----------|
| fk_memory_tag | memory_id | memory_unit | id | ON DELETE CASCADE |
### 级联删除说明
当删除 memory_unit 表中的记录时,会自动删除 memory_unit_tag 表中对应的标签记录。
---
## 表关系
```
memory_unit (1) ──────< (N) memory_unit_tag
一对多关系
一个记忆可以有多个标签
```
---
## 业务规则
### 1. 数据隔离
- 所有数据按 `_tenant_id``user_id` 进行隔离
- 查询时必须包含租户和用户条件
### 2. 状态管理
- 新创建的记忆默认状态为 'active'
- 删除操作采用软删除,将状态设置为 'deleted'
- 过期记忆应归档为 'archived'
### 3. 标签规则
- 一个记忆可以有多个标签
- 标签名称不区分大小写(建议在应用层处理)
- 重复标签由应用层控制
### 4. 敏感信息处理
- 标记为敏感的信息需要特殊处理
- 敏感信息不应在日志中显示完整内容
---
## 性能优化建议
### 1. 分区策略
对于大数据量场景,建议按时间或租户进行分区:
```sql
-- 按创建时间分区(可选)
ALTER TABLE memory_unit PARTITION BY RANGE (TO_DAYS(created)) (
PARTITION p_old VALUES LESS THAN (TO_DAYS('2026-01-01')),
PARTITION p_current VALUES LESS THAN (MAXVALUE)
);
```
### 2. 查询优化
- 使用覆盖索引减少回表
- 避免SELECT *,只查询需要的字段
- 合理使用LIMIT分页
### 3. 数据清理
- 定期清理已删除超过30天的数据
- 清理孤立的标签数据
- 定期执行ANALYZE TABLE和OPTIMIZE TABLE
---
## 数据迁移注意事项
1. **字符集**: 使用 utf8mb4 以支持emoji等特殊字符
2. **时区**: 确保应用服务器和数据库时区一致
3. **JSON字段**: MySQL 5.7+ 支持JSON类型低版本需要使用TEXT
4. **外键**: 如需禁用外键检查(批量导入时):
```sql
SET FOREIGN_KEY_CHECKS=0;
-- 执行导入操作
SET FOREIGN_KEY_CHECKS=1;
```

View File

@@ -0,0 +1,243 @@
# 记忆管理模块 - MySQL表创建脚本使用说明
## 📁 文件说明
### 1. `memory_schema.sql` - 完整版脚本
**包含内容:**
- 完整的表创建语句
- 示例数据插入
- 常用查询示例
- 数据清理和维护脚本
- 性能优化建议
**适用场景:** 开发环境搭建、功能测试、学习参考
### 2. `memory_tables_simple.sql` - 简化版脚本
**包含内容:**
- 纯净的表创建语句
- 必要的索引和约束
**适用场景:** 生产环境部署、快速部署
### 3. `verify_tables.sql` - 验证脚本
**包含内容:**
- 表结构检查
- 索引验证
- 外键约束检查
- 数据完整性检查
**适用场景:** 部署后验证、故障排查
### 4. `DATA_DICTIONARY.md` - 数据字典
**包含内容:**
- 详细的字段说明
- 索引说明
- 业务规则
- 性能优化建议
**适用场景:** 开发参考、文档维护
---
## 🚀 使用步骤
### 步骤1: 选择合适的脚本
```bash
# 开发环境(推荐)
mysql -u root -p < memory_schema.sql
# 生产环境
mysql -u root -p < memory_tables_simple.sql
```
### 步骤2: 验证表创建
```bash
mysql -u root -p your_database < verify_tables.sql
```
### 步骤3: 检查结果
登录MySQL查看
```sql
USE your_database;
SHOW TABLES LIKE 'memory_unit%';
DESCRIBE memory_unit;
DESCRIBE memory_unit_tag;
```
---
## 📋 表结构概览
### memory_unit (记忆单元表)
存储AI Agent的各种记忆数据支持分类、标签、状态管理。
**核心字段:**
- `id`: 主键
- `_tenant_id`, `user_id`: 多租户隔离
- `category`, `sub_category`: 分类体系
- `content_json`: JSON格式存储记忆内容
- `status`: 状态管理
### memory_unit_tag (记忆标签表)
为记忆单元添加灵活的标签支持。
**核心字段:**
- `id`: 主键
- `memory_id`: 关联记忆单元(外键)
- `tag_name`: 标签名称
**关系:** 一个记忆可以有多个标签(一对多)
---
## 🔧 高级配置
### 字符集设置
```sql
-- 确保使用 utf8mb4 支持emoji
ALTER DATABASE your_database CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
```
### 性能优化
```sql
-- 分析表优化查询
ANALYZE TABLE memory_unit;
ANALYZE TABLE memory_unit_tag;
-- 优化表空间
OPTIMIZE TABLE memory_unit;
OPTIMIZE TABLE memory_unit_tag;
```
### 备份恢复
```bash
# 备份
mysqldump -u root -p your_database memory_unit memory_unit_tag > backup.sql
# 恢复
mysql -u root -p your_database < backup.sql
```
---
## 📊 数据示例
### 记忆单元示例
```json
{
"title": "AI项目开发",
"description": "负责AI代理平台的开发工作",
"priority": "高",
"tags": ["重要", "正在进行"],
"metadata": {
"start_date": "2026-01-01",
"end_date": "2026-06-30"
}
}
```
### 标签示例
```
重要、正在进行、技术、学习、运动、日常
```
---
## ⚠️ 注意事项
### 1. 外键约束
- `memory_unit_tag.memory_id` 有外键约束指向 `memory_unit.id`
- 删除记忆会自动删除相关标签CASCADE
### 2. 级联删除
如需临时禁用外键检查(批量操作时):
```sql
SET FOREIGN_KEY_CHECKS=0;
-- 执行批量操作
SET FOREIGN_KEY_CHECKS=1;
```
### 3. JSON字段
- MySQL 5.7+ 原生支持JSON类型
- 提供JSON函数JSON_EXTRACT、JSON_SEARCH等
- 自动验证JSON格式
### 4. 状态管理
- 新建记录默认状态为 'active'
- 删除采用软删除设置status为'deleted'
- 定期清理归档数据
---
## 🔍 常用查询
### 查询用户的活跃记忆
```sql
SELECT * FROM memory_unit
WHERE user_id = 1001
AND status = 'active'
ORDER BY created DESC;
```
### 查询记忆及其标签
```sql
SELECT m.*, t.tag_name
FROM memory_unit m
LEFT JOIN memory_unit_tag t ON m.id = t.memory_id
WHERE m.user_id = 1001
AND m.status = 'active';
```
### 按标签查询记忆
```sql
SELECT DISTINCT m.*
FROM memory_unit m
INNER JOIN memory_unit_tag t ON m.id = t.memory_id
WHERE t.tag_name = '重要'
AND m.status = 'active';
```
### 统计用户记忆数量(按分类)
```sql
SELECT category, COUNT(*) as count
FROM memory_unit
WHERE user_id = 1001
AND status = 'active'
GROUP BY category;
```
---
## 🛠️ 故障排查
### 问题1: 外键约束错误
```sql
-- 检查外键状态
SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND TABLE_NAME = 'memory_unit_tag';
```
### 问题2: 字符编码问题
```sql
-- 检查字符集设置
SHOW VARIABLES LIKE 'character%';
SHOW VARIABLES LIKE 'collation%';
```
### 问题3: JSON格式错误
```sql
-- 验证JSON数据
SELECT id, JSON_VALID(content_json) as is_valid
FROM memory_unit
WHERE content_json IS NOT NULL;
```
---
## 📞 技术支持
如有问题,请参考:
1. `DATA_DICTIONARY.md` - 详细的数据字典
2. `verify_tables.sql` - 表结构验证脚本
3. 项目文档:`README.md`

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>app-platform-memory-sdk</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Swagger注解 -->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,13 @@
package com.xspaceagi.memory.sdk.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class MemoryDeleteDto {
@Schema(description = "记忆ID列表")
private List<Long> memoryIds;
}

View File

@@ -0,0 +1,42 @@
package com.xspaceagi.memory.sdk.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 记忆数据DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemoryMetaData {
/**
* 租户ID
*/
private Long tenantId;
/**
* 用户ID
*/
private Long userId;
/**
* 代理ID
*/
private Long agentId;
/**
* 上下文
*/
private String context;
/**
* 用户输入
*/
private String userInput;
}

View File

@@ -0,0 +1,62 @@
package com.xspaceagi.memory.sdk.dto;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* API Key 数据传输对象
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemorySaveDto {
@Schema(description = "更新、新增、追加memoryId对应记忆中的键值对")
private List<UpdateUnitWithID> updateMemoryKeyValues;
@Schema(description = "生成新的记忆")
private List<InsertUnit> insertMemories;
@Data
public static class UpdateUnitWithID {
@Schema(description = "记忆ID")
private Long memoryId;
@Schema(description = "更新替换原有内容比如手机号变更等、新增、追加在原有内容后面追加新的内容比如之前个人形象身高180现在又获取到了新的数据为皮肤黝黑新内容为“身高180皮肤黝黑”对应记忆中的键值对")
private Map<String, String> keyValues;
@JsonPropertyDescription("关键词标签列表")
private List<String> tags;
@JsonPropertyDescription("是否为敏感信息")
private Boolean isSensitive;
}
@Data
public static class InsertUnit {
@Schema(description = "一级分类")
private String category;
@Schema(description = "二级分类")
private String subCategory;
@Schema(description = "记忆中的键值对")
private Map<String, String> keyValues;
@JsonPropertyDescription("关键词标签列表")
private List<String> tags;
@JsonPropertyDescription("是否为敏感信息")
private Boolean isSensitive;
}
}

View File

@@ -0,0 +1,24 @@
package com.xspaceagi.memory.sdk.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* 记忆单元创建DTO
*/
@Data
@ApiModel(value = "记忆单元创建DTO", description = "创建记忆单元")
public class MemoryUnitCreateDTO {
private Long tenantId;
private Long userId;
private Long agentId;
private String category;
private String subCategory;
private Map<String, String> keyValues;
private List<String> tags;
private Boolean isSensitive;
}

View File

@@ -0,0 +1,54 @@
package com.xspaceagi.memory.sdk.dto;
import java.util.Date;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 记忆单元DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "记忆单元DTO", description = "记忆单元数据传输对象")
public class MemoryUnitDTO {
@ApiModelProperty(value = "主键ID")
private Long id;
@ApiModelProperty(value = "租户ID")
private Long tenantId;
@ApiModelProperty(value = "用户ID")
private Long userId;
@ApiModelProperty(value = "代理ID")
private Long agentId;
@ApiModelProperty(value = "一级分类")
private String category;
@ApiModelProperty(value = "二级分类")
private String subCategory;
@ApiModelProperty(value = "内容JSON")
private String contentJson;
@ApiModelProperty(value = "是否敏感信息(0:否 1:是)")
private Boolean isSensitive;
@ApiModelProperty(value = "状态active、deleted")
private String status;
@ApiModelProperty(value = "创建时间")
private Date created;
@ApiModelProperty(value = "修改时间")
private Date modified;
}

View File

@@ -0,0 +1,34 @@
package com.xspaceagi.memory.sdk.dto;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
* 记忆单元查询DTO
*/
@Data
@ApiModel(value = "记忆单元查询DTO", description = "记忆单元查询条件")
public class MemoryUnitQueryDTO {
private Long tenantId;
private Long userId;
private Long agentId;
@JsonPropertyDescription("必须严格从上述一级分类中选择,不能自创")
private List<String> categories;
@JsonPropertyDescription("必须严格从上述二级分类中选择,不能自创")
private List<String> subCategories;
@JsonPropertyDescription("关键词标签列表,分词可以细粒度一点,尽量多生成有助于更精确查找")
private List<String> tags;
@JsonPropertyDescription("是否需要记忆的历史变更记录如果问题不需要requiresHistory 设为 false")
private Boolean requiresHistory;
@JsonPropertyDescription("查询范围可选值为today|yesterday|recent|long-term|all")
private String queryType;
@JsonPropertyDescription("时间范围非必填格式为2023-01-01 00:00:00-2023-01-01 23:59:59")
private String timeRange;
@JsonPropertyDescription("限制返回的记忆数量,默认为 10")
private Integer limit;
boolean justKeywordSearch;
}

View File

@@ -0,0 +1,22 @@
package com.xspaceagi.memory.sdk.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 记忆单元标签创建DTO
*/
@Data
@ApiModel(value = "记忆单元标签创建DTO", description = "创建记忆单元标签")
public class MemoryUnitTagCreateDTO {
@ApiModelProperty(value = "用户ID", required = true)
private Long userId;
@ApiModelProperty(value = "记忆ID", required = true)
private Long memoryId;
@ApiModelProperty(value = "标签名称", required = true)
private String tagName;
}

View File

@@ -0,0 +1,42 @@
package com.xspaceagi.memory.sdk.dto;
import java.util.Date;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 记忆单元标签DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "记忆单元标签DTO", description = "记忆单元标签数据传输对象")
public class MemoryUnitTagDTO {
@ApiModelProperty(value = "主键ID")
private Long id;
@ApiModelProperty(value = "租户ID")
private Long tenantId;
@ApiModelProperty(value = "用户ID")
private Long userId;
@ApiModelProperty(value = "记忆ID")
private Long memoryId;
@ApiModelProperty(value = "标签名称")
private String tagName;
@ApiModelProperty(value = "创建时间")
private Date created;
@ApiModelProperty(value = "修改时间")
private Date modified;
}

View File

@@ -0,0 +1,32 @@
package com.xspaceagi.memory.sdk.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
* 记忆单元更新DTO
*/
@Data
public class MemoryUnitUpdateDTO {
private Long id;
private Long userId;
@ApiModelProperty(value = "一级分类")
private String category;
@ApiModelProperty(value = "二级分类")
private String subCategory;
@ApiModelProperty(value = "内容JSON")
private String contentJson;
@ApiModelProperty(value = "是否敏感信息")
private Boolean isSensitive;
@ApiModelProperty(value = "状态")
private String status;
private List<String> tags;
}

View File

@@ -0,0 +1,20 @@
package com.xspaceagi.memory.sdk.service;
import com.xspaceagi.memory.sdk.dto.MemoryMetaData;
import com.xspaceagi.memory.sdk.dto.MemorySaveDto;
import com.xspaceagi.memory.sdk.dto.MemoryUnitQueryDTO;
import java.util.List;
public interface IMemoryRpcService {
void createMemory(MemoryMetaData memoryData);
String searchMemoriesMd(Long tenantId, Long userId, Long agentId, String inputMessage, String context, boolean justKeywordSearch, boolean filterSensitive);
String queryMemoriesMd(MemoryUnitQueryDTO memoryUnitQueryDTO, boolean filterSensitive);
void saveMemories(Long tenantId, Long userId, Long agentId, MemorySaveDto memorySaveDto);
void deleteMemories(Long tenantId, Long userId, List<Long> memoryIds);
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>app-platform-memory</artifactId>
<groupId>com.xspaceagi</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-memory-spec</artifactId>
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>system-sdk</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,38 @@
package com.xspaceagi.memory.spec;
/**
* Memory模块常量定义
*/
public class MemoryConstants {
/**
* 记忆类型
*/
public static final class MemoryType {
public static final String SHORT_TERM = "short_term";
public static final String LONG_TERM = "long_term";
public static final String WORKING = "working";
}
/**
* 记忆状态
*/
public static final class MemoryStatus {
public static final String ACTIVE = "active";
public static final String ARCHIVED = "archived";
public static final String DELETED = "deleted";
}
/**
* 默认配置
*/
public static final class DefaultConfig {
public static final int MAX_SHORT_TERM_MEMORY_SIZE = 100;
public static final int MAX_WORKING_MEMORY_SIZE = 50;
public static final long LONG_TERM_MEMORY_THRESHOLD = 7 * 24 * 60 * 60 * 1000L; // 7天
}
private MemoryConstants() {
// 私有构造函数,防止实例化
}
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>app-platform-memory</artifactId>
<groupId>com.xspaceagi</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-memory-web</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-application</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,32 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
<id>repack</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<!--
重新打包需要包含的jar包,这个过程会解压所有的jar包并按照jar的规则重新组合成一个jar包
这里需要包含当前这个工程 ,格式是{groupId}:{artifactId}
-->
<includes>
<!-- 包含项目自己 -->
<include>com.xspaceagi:app-platform-memory-web</include>
<include>com.xspaceagi:app-platform-memory-application</include>
<include>com.xspaceagi:app-platform-memory-domain</include>
<include>com.xspaceagi:app-platform-memory-infra</include>
<include>com.xspaceagi:app-platform-memory-spec</include>
</includes>
<!-- 打包结果目录:/ 表示target/ -->
<outputDirectory>/</outputDirectory>
<!-- 是否解压依赖 -->
<unpack>true</unpack>
<!-- 打包的scope -->
<!-- <scope>system</scope>-->
</dependencySet>
</dependencySets>
</assembly>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>app-platform-memory-web</artifactId>
<groupId>com.xspaceagi</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-memory-web</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-modules</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app-platform-memory</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties/>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>system-sdk</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-application</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-infra</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-domain</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-spec</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-api</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-sdk</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xspaceagi</groupId>
<artifactId>app-platform-memory-web</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>app-platform-memory-application</module>
<module>app-platform-memory-domain</module>
<module>app-platform-memory-infra</module>
<module>app-platform-memory-spec</module>
<module>app-platform-memory-api</module>
<module>app-platform-memory-web</module>
<module>app-platform-memory-sdk</module>
</modules>
</project>