Merge branch 'develop' into fix/workflow
This commit is contained in:
@@ -20,6 +20,8 @@ from . import (
|
||||
knowledgeshare_controller,
|
||||
memory_agent_controller,
|
||||
memory_dashboard_controller,
|
||||
memory_episodic_controller,
|
||||
memory_explicit_controller,
|
||||
memory_forget_controller,
|
||||
memory_reflection_controller,
|
||||
memory_short_term_controller,
|
||||
@@ -67,6 +69,8 @@ manager_router.include_router(memory_agent_controller.router)
|
||||
manager_router.include_router(memory_dashboard_controller.router)
|
||||
manager_router.include_router(memory_storage_controller.router)
|
||||
manager_router.include_router(user_memory_controllers.router)
|
||||
manager_router.include_router(memory_episodic_controller.router)
|
||||
manager_router.include_router(memory_explicit_controller.router)
|
||||
manager_router.include_router(api_key_controller.router)
|
||||
manager_router.include_router(release_share_controller.router)
|
||||
manager_router.include_router(public_share_controller.router) # 公开路由(无需认证)
|
||||
|
||||
@@ -30,7 +30,7 @@ from sqlalchemy.orm import Session
|
||||
api_logger = get_api_logger()
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/memory/emotion",
|
||||
prefix="/memory/emotion-memory",
|
||||
tags=["Emotion Analysis"],
|
||||
dependencies=[Depends(get_current_user)] # 所有路由都需要认证
|
||||
)
|
||||
|
||||
@@ -32,7 +32,6 @@ def get_workspace_list(
|
||||
@router.get("/version", response_model=ApiResponse)
|
||||
def get_system_version():
|
||||
"""获取系统版本号+说明"""
|
||||
return success(data={
|
||||
"version": settings.SYSTEM_VERSION,
|
||||
"introduction": settings.SYSTEM_INTRODUCTION
|
||||
}, msg="系统版本获取成功")
|
||||
current_version = settings.SYSTEM_VERSION
|
||||
version_introduction = HomePageService.load_version_introduction(current_version)
|
||||
return success(data={"version": current_version, "introduction": version_introduction}, msg="系统版本获取成功")
|
||||
@@ -1,18 +1,15 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
from app.repositories.end_user_repository import update_end_user_other_name
|
||||
import uuid
|
||||
from typing import Optional
|
||||
from app.core.response_utils import success
|
||||
from app.db import get_db
|
||||
from app.dependencies import get_current_user
|
||||
from app.models.user_model import User
|
||||
from app.schemas.memory_agent_schema import End_User_Information
|
||||
from app.schemas.response_schema import ApiResponse
|
||||
from app.schemas.app_schema import App as AppSchema
|
||||
|
||||
from app.services import memory_dashboard_service, memory_storage_service, workspace_service
|
||||
from app.services.memory_agent_service import get_end_users_connected_configs_batch
|
||||
from app.core.logging_config import get_api_logger
|
||||
|
||||
# 获取API专用日志器
|
||||
@@ -102,7 +99,8 @@ async def get_workspace_end_users(
|
||||
"""
|
||||
获取工作空间的宿主列表
|
||||
|
||||
返回格式与原 memory_list 接口中的 end_users 字段相同
|
||||
返回格式与原 memory_list 接口中的 end_users 字段相同,
|
||||
并包含每个用户的记忆配置信息(memory_config_id 和 memory_config_name)
|
||||
"""
|
||||
workspace_id = current_user.current_workspace_id
|
||||
# 获取当前空间类型
|
||||
@@ -113,6 +111,17 @@ async def get_workspace_end_users(
|
||||
workspace_id=workspace_id,
|
||||
current_user=current_user
|
||||
)
|
||||
|
||||
# 批量获取所有用户的记忆配置信息(优化:一次查询而非 N 次)
|
||||
end_user_ids = [str(user.id) for user in end_users]
|
||||
memory_configs_map = {}
|
||||
if end_user_ids:
|
||||
try:
|
||||
memory_configs_map = get_end_users_connected_configs_batch(end_user_ids, db)
|
||||
except Exception as e:
|
||||
api_logger.error(f"批量获取记忆配置失败: {str(e)}")
|
||||
# 失败时使用空字典,不影响其他数据返回
|
||||
|
||||
result = []
|
||||
for end_user in end_users:
|
||||
memory_num = {}
|
||||
@@ -123,10 +132,25 @@ async def get_workspace_end_users(
|
||||
memory_num = {
|
||||
"total":memory_dashboard_service.get_current_user_total_chunk(str(end_user.id), db, current_user)
|
||||
}
|
||||
|
||||
# 从批量查询结果中获取配置信息
|
||||
user_id = str(end_user.id)
|
||||
memory_config_info = memory_configs_map.get(user_id, {
|
||||
"memory_config_id": None,
|
||||
"memory_config_name": None
|
||||
})
|
||||
|
||||
# 只保留需要的字段,移除 error 字段(如果有)
|
||||
memory_config = {
|
||||
"memory_config_id": memory_config_info.get("memory_config_id"),
|
||||
"memory_config_name": memory_config_info.get("memory_config_name")
|
||||
}
|
||||
|
||||
result.append(
|
||||
{
|
||||
'end_user':end_user,
|
||||
'memory_num':memory_num
|
||||
'end_user': end_user,
|
||||
'memory_num': memory_num,
|
||||
'memory_config': memory_config
|
||||
}
|
||||
)
|
||||
|
||||
@@ -465,7 +489,6 @@ async def dashboard_data(
|
||||
if storage_type is None:
|
||||
storage_type = 'neo4j'
|
||||
|
||||
user_rag_memory_id = None
|
||||
|
||||
# 根据 storage_type 决定返回哪个数据对象
|
||||
# 如果是 'rag',neo4j_data 为 null;否则 rag_data 为 null
|
||||
|
||||
125
api/app/controllers/memory_episodic_controller.py
Normal file
125
api/app/controllers/memory_episodic_controller.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
情景记忆相关的控制器
|
||||
包含情景记忆总览和详情查询接口
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.core.error_codes import BizCode
|
||||
from app.core.logging_config import get_api_logger
|
||||
from app.core.response_utils import fail, success
|
||||
from app.dependencies import get_current_user
|
||||
from app.models.user_model import User
|
||||
from app.schemas.response_schema import ApiResponse
|
||||
from app.schemas.memory_episodic_schema import (
|
||||
EpisodicMemoryOverviewRequest,
|
||||
EpisodicMemoryDetailsRequest,
|
||||
)
|
||||
from app.services.memory_episodic_service import memory_episodic_service
|
||||
|
||||
# Get API logger
|
||||
api_logger = get_api_logger()
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/memory/episodic-memory",
|
||||
tags=["Episodic Memory"],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/overview", response_model=ApiResponse)
|
||||
async def get_episodic_memory_overview_api(
|
||||
request: EpisodicMemoryOverviewRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""
|
||||
获取情景记忆总览
|
||||
|
||||
返回指定用户的所有情景记忆列表,包括标题和创建时间。
|
||||
支持通过时间范围、情景类型和标题关键词进行筛选。
|
||||
|
||||
"""
|
||||
workspace_id = current_user.current_workspace_id
|
||||
|
||||
# 检查用户是否已选择工作空间
|
||||
if workspace_id is None:
|
||||
api_logger.warning(f"用户 {current_user.username} 尝试查询情景记忆总览但未选择工作空间")
|
||||
return fail(BizCode.INVALID_PARAMETER, "请先切换到一个工作空间", "current_workspace_id is None")
|
||||
|
||||
# 验证参数
|
||||
valid_time_ranges = ["all", "today", "this_week", "this_month"]
|
||||
valid_episodic_types = ["all", "conversation", "project_work", "learning", "decision", "important_event"]
|
||||
|
||||
if request.time_range not in valid_time_ranges:
|
||||
return fail(BizCode.INVALID_PARAMETER, f"无效的时间范围参数,可选值:{', '.join(valid_time_ranges)}")
|
||||
|
||||
if request.episodic_type not in valid_episodic_types:
|
||||
return fail(BizCode.INVALID_PARAMETER, f"无效的情景类型参数,可选值:{', '.join(valid_episodic_types)}")
|
||||
|
||||
# 处理 title_keyword(去除首尾空格)
|
||||
title_keyword = request.title_keyword.strip() if request.title_keyword else None
|
||||
|
||||
api_logger.info(
|
||||
f"情景记忆总览查询请求: end_user_id={request.end_user_id}, user={current_user.username}, "
|
||||
f"workspace={workspace_id}, time_range={request.time_range}, episodic_type={request.episodic_type}, "
|
||||
f"title_keyword={title_keyword}"
|
||||
)
|
||||
|
||||
try:
|
||||
# 调用Service层方法
|
||||
result = await memory_episodic_service.get_episodic_memory_overview(
|
||||
request.end_user_id, request.time_range, request.episodic_type, title_keyword
|
||||
)
|
||||
|
||||
api_logger.info(
|
||||
f"成功获取情景记忆总览: end_user_id={request.end_user_id}, "
|
||||
f"total={result['total']}"
|
||||
)
|
||||
return success(data=result, msg="查询成功")
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(f"情景记忆总览查询失败: end_user_id={request.end_user_id}, error={str(e)}")
|
||||
return fail(BizCode.INTERNAL_ERROR, "情景记忆总览查询失败", str(e))
|
||||
|
||||
|
||||
@router.post("/details", response_model=ApiResponse)
|
||||
async def get_episodic_memory_details_api(
|
||||
request: EpisodicMemoryDetailsRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""
|
||||
获取情景记忆详情
|
||||
|
||||
返回指定情景记忆的详细信息,包括涉及对象、情景类型、内容记录和情绪。
|
||||
|
||||
"""
|
||||
workspace_id = current_user.current_workspace_id
|
||||
|
||||
# 检查用户是否已选择工作空间
|
||||
if workspace_id is None:
|
||||
api_logger.warning(f"用户 {current_user.username} 尝试查询情景记忆详情但未选择工作空间")
|
||||
return fail(BizCode.INVALID_PARAMETER, "请先切换到一个工作空间", "current_workspace_id is None")
|
||||
|
||||
api_logger.info(
|
||||
f"情景记忆详情查询请求: end_user_id={request.end_user_id}, summary_id={request.summary_id}, "
|
||||
f"user={current_user.username}, workspace={workspace_id}"
|
||||
)
|
||||
|
||||
try:
|
||||
# 调用Service层方法
|
||||
result = await memory_episodic_service.get_episodic_memory_details(
|
||||
end_user_id=request.end_user_id,
|
||||
summary_id=request.summary_id
|
||||
)
|
||||
|
||||
api_logger.info(
|
||||
f"成功获取情景记忆详情: end_user_id={request.end_user_id}, summary_id={request.summary_id}"
|
||||
)
|
||||
return success(data=result, msg="查询成功")
|
||||
|
||||
except ValueError as e:
|
||||
# 处理情景记忆不存在的情况
|
||||
api_logger.warning(f"情景记忆不存在: end_user_id={request.end_user_id}, summary_id={request.summary_id}, error={str(e)}")
|
||||
return fail(BizCode.INVALID_PARAMETER, "情景记忆不存在", str(e))
|
||||
except Exception as e:
|
||||
api_logger.error(f"情景记忆详情查询失败: end_user_id={request.end_user_id}, summary_id={request.summary_id}, error={str(e)}")
|
||||
return fail(BizCode.INTERNAL_ERROR, "情景记忆详情查询失败", str(e))
|
||||
115
api/app/controllers/memory_explicit_controller.py
Normal file
115
api/app/controllers/memory_explicit_controller.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
显性记忆控制器
|
||||
|
||||
处理显性记忆相关的API接口,包括情景记忆和语义记忆的查询。
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.core.logging_config import get_api_logger
|
||||
from app.core.response_utils import success, fail
|
||||
from app.core.error_codes import BizCode
|
||||
from app.services.memory_explicit_service import MemoryExplicitService
|
||||
from app.schemas.response_schema import ApiResponse
|
||||
from app.schemas.memory_explicit_schema import (
|
||||
ExplicitMemoryOverviewRequest,
|
||||
ExplicitMemoryDetailsRequest,
|
||||
)
|
||||
from app.dependencies import get_current_user
|
||||
from app.models.user_model import User
|
||||
|
||||
# Get API logger
|
||||
api_logger = get_api_logger()
|
||||
|
||||
# Initialize service
|
||||
memory_explicit_service = MemoryExplicitService()
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/memory/explicit-memory",
|
||||
tags=["Explicit Memory"],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/overview", response_model=ApiResponse)
|
||||
async def get_explicit_memory_overview_api(
|
||||
request: ExplicitMemoryOverviewRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""
|
||||
获取显性记忆总览
|
||||
|
||||
返回指定用户的所有显性记忆列表,包括标题、完整内容、创建时间和情绪信息。
|
||||
"""
|
||||
workspace_id = current_user.current_workspace_id
|
||||
|
||||
# 检查用户是否已选择工作空间
|
||||
if workspace_id is None:
|
||||
api_logger.warning(f"用户 {current_user.username} 尝试查询显性记忆总览但未选择工作空间")
|
||||
return fail(BizCode.INVALID_PARAMETER, "请先切换到一个工作空间", "current_workspace_id is None")
|
||||
|
||||
api_logger.info(
|
||||
f"显性记忆总览查询请求: end_user_id={request.end_user_id}, user={current_user.username}, "
|
||||
f"workspace={workspace_id}"
|
||||
)
|
||||
|
||||
try:
|
||||
# 调用Service层方法
|
||||
result = await memory_explicit_service.get_explicit_memory_overview(
|
||||
request.end_user_id
|
||||
)
|
||||
|
||||
api_logger.info(
|
||||
f"成功获取显性记忆总览: end_user_id={request.end_user_id}, "
|
||||
f"total={result['total']}"
|
||||
)
|
||||
return success(data=result, msg="查询成功")
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(f"显性记忆总览查询失败: end_user_id={request.end_user_id}, error={str(e)}")
|
||||
return fail(BizCode.INTERNAL_ERROR, "显性记忆总览查询失败", str(e))
|
||||
|
||||
|
||||
@router.post("/details", response_model=ApiResponse)
|
||||
async def get_explicit_memory_details_api(
|
||||
request: ExplicitMemoryDetailsRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""
|
||||
获取显性记忆详情
|
||||
|
||||
根据 memory_id 返回情景记忆或语义记忆的详细信息。
|
||||
- 情景记忆:包括标题、内容、情绪、创建时间
|
||||
- 语义记忆:包括名称、核心定义、详细笔记、创建时间
|
||||
"""
|
||||
workspace_id = current_user.current_workspace_id
|
||||
|
||||
# 检查用户是否已选择工作空间
|
||||
if workspace_id is None:
|
||||
api_logger.warning(f"用户 {current_user.username} 尝试查询显性记忆详情但未选择工作空间")
|
||||
return fail(BizCode.INVALID_PARAMETER, "请先切换到一个工作空间", "current_workspace_id is None")
|
||||
|
||||
api_logger.info(
|
||||
f"显性记忆详情查询请求: end_user_id={request.end_user_id}, memory_id={request.memory_id}, "
|
||||
f"user={current_user.username}, workspace={workspace_id}"
|
||||
)
|
||||
|
||||
try:
|
||||
# 调用Service层方法
|
||||
result = await memory_explicit_service.get_explicit_memory_details(
|
||||
end_user_id=request.end_user_id,
|
||||
memory_id=request.memory_id
|
||||
)
|
||||
|
||||
api_logger.info(
|
||||
f"成功获取显性记忆详情: end_user_id={request.end_user_id}, memory_id={request.memory_id}, "
|
||||
f"memory_type={result.get('memory_type')}"
|
||||
)
|
||||
return success(data=result, msg="查询成功")
|
||||
|
||||
except ValueError as e:
|
||||
# 处理记忆不存在的情况
|
||||
api_logger.warning(f"显性记忆不存在: end_user_id={request.end_user_id}, memory_id={request.memory_id}, error={str(e)}")
|
||||
return fail(BizCode.INVALID_PARAMETER, "显性记忆不存在", str(e))
|
||||
except Exception as e:
|
||||
api_logger.error(f"显性记忆详情查询失败: end_user_id={request.end_user_id}, memory_id={request.memory_id}, error={str(e)}")
|
||||
return fail(BizCode.INTERNAL_ERROR, "显性记忆详情查询失败", str(e))
|
||||
@@ -39,7 +39,7 @@ from app.services.memory_forget_service import MemoryForgetService
|
||||
api_logger = get_api_logger()
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/memory/forget",
|
||||
prefix="/memory/forget-memory",
|
||||
tags=["Memory Forgetting Engine"],
|
||||
dependencies=[Depends(get_current_user)] # 所有路由都需要认证
|
||||
)
|
||||
|
||||
@@ -20,12 +20,6 @@ from app.services.user_memory_service import (
|
||||
from app.services.memory_entity_relationship_service import MemoryEntityService,MemoryEmotion,MemoryInteraction
|
||||
from app.schemas.response_schema import ApiResponse
|
||||
from app.schemas.memory_storage_schema import GenerateCacheRequest
|
||||
from app.schemas.user_memory_schema import (
|
||||
EpisodicMemoryOverviewRequest,
|
||||
EpisodicMemoryDetailsRequest,
|
||||
ExplicitMemoryOverviewRequest,
|
||||
ExplicitMemoryDetailsRequest,
|
||||
)
|
||||
|
||||
from app.schemas.end_user_schema import (
|
||||
EndUserProfileResponse,
|
||||
@@ -440,195 +434,3 @@ async def memory_space_relationship_evolution(id: str, label: str,
|
||||
except Exception as e:
|
||||
api_logger.error(f"关系演变查询失败: id={id}, table={label}, error={str(e)}", exc_info=True)
|
||||
return fail(BizCode.INTERNAL_ERROR, "关系演变查询失败", str(e))
|
||||
|
||||
|
||||
@router.post("/classifications/episodic-memory", response_model=ApiResponse)
|
||||
async def get_episodic_memory_overview_api(
|
||||
request: EpisodicMemoryOverviewRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""
|
||||
获取情景记忆总览
|
||||
|
||||
返回指定用户的所有情景记忆列表,包括标题和创建时间。
|
||||
支持通过时间范围、情景类型和标题关键词进行筛选。
|
||||
|
||||
"""
|
||||
workspace_id = current_user.current_workspace_id
|
||||
|
||||
# 检查用户是否已选择工作空间
|
||||
if workspace_id is None:
|
||||
api_logger.warning(f"用户 {current_user.username} 尝试查询情景记忆总览但未选择工作空间")
|
||||
return fail(BizCode.INVALID_PARAMETER, "请先切换到一个工作空间", "current_workspace_id is None")
|
||||
|
||||
# 验证参数
|
||||
valid_time_ranges = ["all", "today", "this_week", "this_month"]
|
||||
valid_episodic_types = ["all", "conversation", "project_work", "learning", "decision", "important_event"]
|
||||
|
||||
if request.time_range not in valid_time_ranges:
|
||||
return fail(BizCode.INVALID_PARAMETER, f"无效的时间范围参数,可选值:{', '.join(valid_time_ranges)}")
|
||||
|
||||
if request.episodic_type not in valid_episodic_types:
|
||||
return fail(BizCode.INVALID_PARAMETER, f"无效的情景类型参数,可选值:{', '.join(valid_episodic_types)}")
|
||||
|
||||
# 处理 title_keyword(去除首尾空格)
|
||||
title_keyword = request.title_keyword.strip() if request.title_keyword else None
|
||||
|
||||
api_logger.info(
|
||||
f"情景记忆总览查询请求: end_user_id={request.end_user_id}, user={current_user.username}, "
|
||||
f"workspace={workspace_id}, time_range={request.time_range}, episodic_type={request.episodic_type}, "
|
||||
f"title_keyword={title_keyword}"
|
||||
)
|
||||
|
||||
try:
|
||||
# 调用Service层方法
|
||||
result = await user_memory_service.get_episodic_memory_overview(
|
||||
db, request.end_user_id, request.time_range, request.episodic_type, title_keyword
|
||||
)
|
||||
|
||||
api_logger.info(
|
||||
f"成功获取情景记忆总览: end_user_id={request.end_user_id}, "
|
||||
f"total={result['total']}"
|
||||
)
|
||||
return success(data=result, msg="查询成功")
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(f"情景记忆总览查询失败: end_user_id={request.end_user_id}, error={str(e)}")
|
||||
return fail(BizCode.INTERNAL_ERROR, "情景记忆总览查询失败", str(e))
|
||||
|
||||
|
||||
@router.post("/classifications/episodic-memory-details", response_model=ApiResponse)
|
||||
async def get_episodic_memory_details_api(
|
||||
request: EpisodicMemoryDetailsRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""
|
||||
获取情景记忆详情
|
||||
|
||||
返回指定情景记忆的详细信息,包括涉及对象、情景类型、内容记录和情绪。
|
||||
|
||||
"""
|
||||
workspace_id = current_user.current_workspace_id
|
||||
|
||||
# 检查用户是否已选择工作空间
|
||||
if workspace_id is None:
|
||||
api_logger.warning(f"用户 {current_user.username} 尝试查询情景记忆详情但未选择工作空间")
|
||||
return fail(BizCode.INVALID_PARAMETER, "请先切换到一个工作空间", "current_workspace_id is None")
|
||||
|
||||
api_logger.info(
|
||||
f"情景记忆详情查询请求: end_user_id={request.end_user_id}, summary_id={request.summary_id}, "
|
||||
f"user={current_user.username}, workspace={workspace_id}"
|
||||
)
|
||||
|
||||
try:
|
||||
# 调用Service层方法
|
||||
result = await user_memory_service.get_episodic_memory_details(
|
||||
db=db,
|
||||
end_user_id=request.end_user_id,
|
||||
summary_id=request.summary_id
|
||||
)
|
||||
|
||||
api_logger.info(
|
||||
f"成功获取情景记忆详情: end_user_id={request.end_user_id}, summary_id={request.summary_id}"
|
||||
)
|
||||
return success(data=result, msg="查询成功")
|
||||
|
||||
except ValueError as e:
|
||||
# 处理情景记忆不存在的情况
|
||||
api_logger.warning(f"情景记忆不存在: end_user_id={request.end_user_id}, summary_id={request.summary_id}, error={str(e)}")
|
||||
return fail(BizCode.INVALID_PARAMETER, "情景记忆不存在", str(e))
|
||||
except Exception as e:
|
||||
api_logger.error(f"情景记忆详情查询失败: end_user_id={request.end_user_id}, summary_id={request.summary_id}, error={str(e)}")
|
||||
return fail(BizCode.INTERNAL_ERROR, "情景记忆详情查询失败", str(e))
|
||||
|
||||
|
||||
@router.post("/classifications/explicit-memory", response_model=ApiResponse)
|
||||
async def get_explicit_memory_overview_api(
|
||||
request: ExplicitMemoryOverviewRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""
|
||||
获取显性记忆总览
|
||||
|
||||
返回指定用户的所有显性记忆列表,包括标题、完整内容、创建时间和情绪信息。
|
||||
"""
|
||||
workspace_id = current_user.current_workspace_id
|
||||
|
||||
# 检查用户是否已选择工作空间
|
||||
if workspace_id is None:
|
||||
api_logger.warning(f"用户 {current_user.username} 尝试查询显性记忆总览但未选择工作空间")
|
||||
return fail(BizCode.INVALID_PARAMETER, "请先切换到一个工作空间", "current_workspace_id is None")
|
||||
|
||||
api_logger.info(
|
||||
f"显性记忆总览查询请求: end_user_id={request.end_user_id}, user={current_user.username}, "
|
||||
f"workspace={workspace_id}"
|
||||
)
|
||||
|
||||
try:
|
||||
# 调用Service层方法
|
||||
result = await user_memory_service.get_explicit_memory_overview(
|
||||
db, request.end_user_id
|
||||
)
|
||||
|
||||
api_logger.info(
|
||||
f"成功获取显性记忆总览: end_user_id={request.end_user_id}, "
|
||||
f"total={result['total']}"
|
||||
)
|
||||
return success(data=result, msg="查询成功")
|
||||
|
||||
except Exception as e:
|
||||
api_logger.error(f"显性记忆总览查询失败: end_user_id={request.end_user_id}, error={str(e)}")
|
||||
return fail(BizCode.INTERNAL_ERROR, "显性记忆总览查询失败", str(e))
|
||||
|
||||
|
||||
@router.post("/classifications/explicit-memory-details", response_model=ApiResponse)
|
||||
async def get_explicit_memory_details_api(
|
||||
request: ExplicitMemoryDetailsRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""
|
||||
获取显性记忆详情
|
||||
|
||||
根据 memory_id 返回情景记忆或语义记忆的详细信息。
|
||||
- 情景记忆:包括标题、内容、情绪、创建时间
|
||||
- 语义记忆:包括名称、核心定义、详细笔记、创建时间
|
||||
"""
|
||||
workspace_id = current_user.current_workspace_id
|
||||
|
||||
# 检查用户是否已选择工作空间
|
||||
if workspace_id is None:
|
||||
api_logger.warning(f"用户 {current_user.username} 尝试查询显性记忆详情但未选择工作空间")
|
||||
return fail(BizCode.INVALID_PARAMETER, "请先切换到一个工作空间", "current_workspace_id is None")
|
||||
|
||||
api_logger.info(
|
||||
f"显性记忆详情查询请求: end_user_id={request.end_user_id}, memory_id={request.memory_id}, "
|
||||
f"user={current_user.username}, workspace={workspace_id}"
|
||||
)
|
||||
|
||||
try:
|
||||
# 调用Service层方法
|
||||
result = await user_memory_service.get_explicit_memory_details(
|
||||
db=db,
|
||||
end_user_id=request.end_user_id,
|
||||
memory_id=request.memory_id
|
||||
)
|
||||
|
||||
api_logger.info(
|
||||
f"成功获取显性记忆详情: end_user_id={request.end_user_id}, memory_id={request.memory_id}, "
|
||||
f"memory_type={result.get('memory_type')}"
|
||||
)
|
||||
return success(data=result, msg="查询成功")
|
||||
|
||||
except ValueError as e:
|
||||
# 处理记忆不存在的情况
|
||||
api_logger.warning(f"显性记忆不存在: end_user_id={request.end_user_id}, memory_id={request.memory_id}, error={str(e)}")
|
||||
return fail(BizCode.INVALID_PARAMETER, "显性记忆不存在", str(e))
|
||||
except Exception as e:
|
||||
api_logger.error(f"显性记忆详情查询失败: end_user_id={request.end_user_id}, memory_id={request.memory_id}, error={str(e)}")
|
||||
return fail(BizCode.INTERNAL_ERROR, "显性记忆详情查询失败", str(e))
|
||||
|
||||
|
||||
|
||||
@@ -167,7 +167,6 @@ class Settings:
|
||||
|
||||
# official environment system version
|
||||
SYSTEM_VERSION: str = os.getenv("SYSTEM_VERSION", "v0.2.0")
|
||||
SYSTEM_INTRODUCTION: str = os.getenv("SYSTEM_INTRODUCTION", "")
|
||||
|
||||
def get_memory_output_path(self, filename: str = "") -> str:
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Tuple
|
||||
from uuid import uuid4
|
||||
|
||||
from app.core.logging_config import get_memory_logger
|
||||
@@ -28,6 +29,118 @@ class MemorySummaryResponse(RobustLLMResponse):
|
||||
)
|
||||
|
||||
|
||||
async def generate_title_and_type_for_summary(
|
||||
content: str,
|
||||
llm_client
|
||||
) -> Tuple[str, str]:
|
||||
"""
|
||||
为MemorySummary生成标题和类型
|
||||
|
||||
此方法应该在创建MemorySummary节点时调用,生成title和type
|
||||
|
||||
Args:
|
||||
content: Summary的内容文本
|
||||
llm_client: LLM客户端实例
|
||||
|
||||
Returns:
|
||||
(标题, 类型)元组
|
||||
"""
|
||||
from app.core.memory.utils.prompt.prompt_utils import render_episodic_title_and_type_prompt
|
||||
|
||||
# 定义有效的类型集合
|
||||
VALID_TYPES = {
|
||||
"conversation", # 对话
|
||||
"project_work", # 项目/工作
|
||||
"learning", # 学习
|
||||
"decision", # 决策
|
||||
"important_event" # 重要事件
|
||||
}
|
||||
DEFAULT_TYPE = "conversation" # 默认类型
|
||||
|
||||
try:
|
||||
if not content:
|
||||
logger.warning("content为空,无法生成标题和类型")
|
||||
return ("空内容", DEFAULT_TYPE)
|
||||
|
||||
# 1. 渲染Jinja2提示词模板
|
||||
prompt = await render_episodic_title_and_type_prompt(content)
|
||||
|
||||
# 2. 调用LLM生成标题和类型
|
||||
messages = [
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
response = await llm_client.chat(messages=messages)
|
||||
|
||||
# 3. 解析LLM响应
|
||||
content_response = response.content
|
||||
if isinstance(content_response, list):
|
||||
if len(content_response) > 0:
|
||||
if isinstance(content_response[0], dict):
|
||||
text = content_response[0].get('text', content_response[0].get('content', str(content_response[0])))
|
||||
full_response = str(text)
|
||||
else:
|
||||
full_response = str(content_response[0])
|
||||
else:
|
||||
full_response = ""
|
||||
elif isinstance(content_response, dict):
|
||||
full_response = str(content_response.get('text', content_response.get('content', str(content_response))))
|
||||
else:
|
||||
full_response = str(content_response) if content_response is not None else ""
|
||||
|
||||
# 4. 解析JSON响应
|
||||
try:
|
||||
# 尝试从响应中提取JSON
|
||||
# 移除可能的markdown代码块标记
|
||||
json_str = full_response.strip()
|
||||
if json_str.startswith("```json"):
|
||||
json_str = json_str[7:]
|
||||
if json_str.startswith("```"):
|
||||
json_str = json_str[3:]
|
||||
if json_str.endswith("```"):
|
||||
json_str = json_str[:-3]
|
||||
json_str = json_str.strip()
|
||||
|
||||
result_data = json.loads(json_str)
|
||||
title = result_data.get("title", "未知标题")
|
||||
episodic_type_raw = result_data.get("type", DEFAULT_TYPE)
|
||||
|
||||
# 5. 校验和归一化类型
|
||||
# 将类型转换为小写并去除空格
|
||||
episodic_type_normalized = str(episodic_type_raw).lower().strip()
|
||||
|
||||
# 检查是否在有效类型集合中
|
||||
if episodic_type_normalized in VALID_TYPES:
|
||||
episodic_type = episodic_type_normalized
|
||||
else:
|
||||
# 尝试映射常见的中文类型到英文
|
||||
type_mapping = {
|
||||
"对话": "conversation",
|
||||
"项目": "project_work",
|
||||
"工作": "project_work",
|
||||
"项目/工作": "project_work",
|
||||
"学习": "learning",
|
||||
"决策": "decision",
|
||||
"重要事件": "important_event",
|
||||
"事件": "important_event"
|
||||
}
|
||||
episodic_type = type_mapping.get(episodic_type_raw, DEFAULT_TYPE)
|
||||
logger.warning(
|
||||
f"LLM返回的类型 '{episodic_type_raw}' 不在有效集合中,"
|
||||
f"已归一化为 '{episodic_type}'"
|
||||
)
|
||||
|
||||
logger.info(f"成功生成标题和类型: title={title}, type={episodic_type}")
|
||||
return (title, episodic_type)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"无法解析LLM响应为JSON: {full_response}")
|
||||
return ("解析失败", DEFAULT_TYPE)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成标题和类型时出错: {str(e)}", exc_info=True)
|
||||
return ("错误", DEFAULT_TYPE)
|
||||
|
||||
async def _process_chunk_summary(
|
||||
dialog: DialogData,
|
||||
chunk,
|
||||
@@ -63,10 +176,9 @@ async def _process_chunk_summary(
|
||||
title = None
|
||||
episodic_type = None
|
||||
try:
|
||||
from app.services.user_memory_service import UserMemoryService
|
||||
title, episodic_type = await UserMemoryService.generate_title_and_type_for_summary(
|
||||
title, episodic_type = await generate_title_and_type_for_summary(
|
||||
content=summary_text,
|
||||
end_user_id=dialog.group_id
|
||||
llm_client=llm_client
|
||||
)
|
||||
logger.info(f"Generated title and type for MemorySummary: title={title}, type={episodic_type}")
|
||||
except Exception as e:
|
||||
|
||||
@@ -260,17 +260,32 @@ class ForgettingStrategy:
|
||||
)
|
||||
|
||||
# 生成标题和类型(使用LLM)
|
||||
from app.services.user_memory_service import UserMemoryService
|
||||
from app.core.memory.storage_services.extraction_engine.knowledge_extraction.memory_summary import generate_title_and_type_for_summary
|
||||
|
||||
# 获取 LLM 客户端
|
||||
llm_client = None
|
||||
if config_id is not None and db is not None:
|
||||
try:
|
||||
llm_client = await self._get_llm_client(db, config_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"获取 LLM 客户端失败: {str(e)}")
|
||||
|
||||
# 生成标题和类型
|
||||
try:
|
||||
title, episodic_type = await UserMemoryService.generate_title_and_type_for_summary(
|
||||
content=summary_text,
|
||||
end_user_id=group_id
|
||||
)
|
||||
logger.info(f"成功为MemorySummary生成标题和类型: title={title}, type={episodic_type}")
|
||||
if llm_client is not None:
|
||||
title, episodic_type = await generate_title_and_type_for_summary(
|
||||
content=summary_text,
|
||||
llm_client=llm_client
|
||||
)
|
||||
logger.info(f"成功为MemorySummary生成标题和类型: title={title}, type={episodic_type}")
|
||||
else:
|
||||
logger.warning("LLM 客户端不可用,使用默认标题和类型")
|
||||
title = "未命名"
|
||||
episodic_type = "conversation"
|
||||
except Exception as e:
|
||||
logger.error(f"生成标题和类型失败,使用默认值: {str(e)}")
|
||||
title = "未命名"
|
||||
episodic_type = "其他"
|
||||
episodic_type = "conversation"
|
||||
|
||||
# 计算继承的激活值和重要性(取较高值)
|
||||
inherited_activation = max(statement_activation, entity_activation)
|
||||
|
||||
@@ -110,7 +110,7 @@ class BaiduSearchTool(BuiltinTool):
|
||||
|
||||
execution_time = time.time() - start_time
|
||||
return ToolResult.success_result(
|
||||
data=result,
|
||||
data=result["results"],
|
||||
execution_time=execution_time
|
||||
)
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ class DateTimeTool(BuiltinTool):
|
||||
|
||||
execution_time = time.time() - start_time
|
||||
return ToolResult.success_result(
|
||||
data=result,
|
||||
data=result["result_data"],
|
||||
execution_time=execution_time
|
||||
)
|
||||
|
||||
@@ -123,12 +123,14 @@ class DateTimeTool(BuiltinTool):
|
||||
utc_now = datetime.now(timezone.utc)
|
||||
|
||||
return {
|
||||
"datetime": now.strftime(output_format),
|
||||
"timestamp": int(now.timestamp()),
|
||||
"timezone": timezone_str,
|
||||
"iso_format": now.isoformat(),
|
||||
"timestamp_ms": int(now.timestamp() * 1000),
|
||||
"utc_datetime": utc_now.strftime(output_format)
|
||||
"result_data": {
|
||||
"datetime": now.strftime(output_format),
|
||||
"timestamp": int(now.timestamp()),
|
||||
"timestamp_ms": int(now.timestamp() * 1000),
|
||||
"utc_datetime": utc_now.strftime(output_format),
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -148,7 +150,8 @@ class DateTimeTool(BuiltinTool):
|
||||
"original": input_value,
|
||||
"formatted": dt.strftime(output_format),
|
||||
"timestamp": int(dt.timestamp()),
|
||||
"iso_format": dt.isoformat()
|
||||
"iso_format": dt.isoformat(),
|
||||
"result_data": dt.strftime(output_format)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -189,7 +192,8 @@ class DateTimeTool(BuiltinTool):
|
||||
"original_timezone": from_timezone,
|
||||
"converted": converted_dt.strftime(output_format),
|
||||
"converted_timezone": to_timezone,
|
||||
"timestamp": int(converted_dt.timestamp())
|
||||
"timestamp": int(converted_dt.timestamp()),
|
||||
"result_data": converted_dt.strftime(output_format)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -219,7 +223,8 @@ class DateTimeTool(BuiltinTool):
|
||||
"timestamp": timestamp,
|
||||
"datetime": dt.strftime(output_format),
|
||||
"timezone": timezone_str,
|
||||
"iso_format": dt.isoformat()
|
||||
"iso_format": dt.isoformat(),
|
||||
"result_data": dt.strftime(output_format)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -249,7 +254,8 @@ class DateTimeTool(BuiltinTool):
|
||||
"datetime": input_value,
|
||||
"timezone": timezone_str,
|
||||
"timestamp": int(dt.timestamp()),
|
||||
"iso_format": dt.isoformat()
|
||||
"iso_format": dt.isoformat(),
|
||||
"result_data": int(dt.timestamp())
|
||||
}
|
||||
|
||||
def _calculate_datetime(self, kwargs) -> dict:
|
||||
@@ -287,7 +293,8 @@ class DateTimeTool(BuiltinTool):
|
||||
"calculation": calculation,
|
||||
"result": calculated_dt.strftime(output_format),
|
||||
"timezone": timezone_str,
|
||||
"timestamp": int(calculated_dt.timestamp())
|
||||
"timestamp": int(calculated_dt.timestamp()),
|
||||
"result_data": calculated_dt.strftime(output_format)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -69,7 +69,7 @@ class JsonTool(BuiltinTool):
|
||||
ToolParameter(
|
||||
name="json_path",
|
||||
type=ParameterType.STRING,
|
||||
description="JSON路径表达式(用于extract、insert、replace、delete、parse操作,如:$.user.name或users[0].name)",
|
||||
description="JSON路径表达式(用于insert、replace、delete、parse操作,如:$.user.name或users[0].name)",
|
||||
required=False
|
||||
),
|
||||
ToolParameter(
|
||||
@@ -136,7 +136,7 @@ class JsonTool(BuiltinTool):
|
||||
|
||||
execution_time = time.time() - start_time
|
||||
return ToolResult.success_result(
|
||||
data=result,
|
||||
data=result["result_data"],
|
||||
execution_time=execution_time
|
||||
)
|
||||
|
||||
@@ -671,7 +671,8 @@ class JsonTool(BuiltinTool):
|
||||
"success": True,
|
||||
"value": current,
|
||||
"value_type": type(current).__name__,
|
||||
"value_json": json.dumps(current, indent=2, ensure_ascii=False) if isinstance(current, (dict, list)) else str(current)
|
||||
"value_json": json.dumps(current, indent=2, ensure_ascii=False) if isinstance(current, (dict, list)) else str(current),
|
||||
"result_data": json.dumps(current, indent=2, ensure_ascii=False) if isinstance(current, (dict, list)) else str(current)
|
||||
}
|
||||
|
||||
except (KeyError, IndexError, TypeError) as e:
|
||||
@@ -680,7 +681,8 @@ class JsonTool(BuiltinTool):
|
||||
"json_path": json_path,
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"value": None
|
||||
"value": None,
|
||||
"result_data": None
|
||||
}
|
||||
|
||||
def _analyze_json_structure(self, data: Any, depth: int = 0) -> Dict[str, Any]:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from app.core.workflow.nodes.base_node import BaseNode, WorkflowState
|
||||
@@ -25,10 +27,10 @@ class ToolNode(BaseNode):
|
||||
# 获取租户ID和用户ID
|
||||
tenant_id = self.get_variable("sys.tenant_id", state)
|
||||
user_id = self.get_variable("sys.user_id", state)
|
||||
workspace_id = self.get_variable("sys.workspace_id", state)
|
||||
|
||||
# 如果没有租户ID,尝试从工作流ID获取
|
||||
if not tenant_id:
|
||||
workspace_id = self.get_variable("sys.workspace_id", state)
|
||||
if workspace_id:
|
||||
from app.repositories.tool_repository import ToolRepository
|
||||
with get_db_read() as db:
|
||||
@@ -63,21 +65,21 @@ class ToolNode(BaseNode):
|
||||
tool_id=self.typed_config.tool_id,
|
||||
parameters=rendered_parameters,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id
|
||||
user_id=uuid.UUID(user_id),
|
||||
workspace_id=uuid.UUID(workspace_id)
|
||||
)
|
||||
|
||||
if result.success:
|
||||
logger.info(f"节点 {self.node_id} 工具执行成功")
|
||||
return {
|
||||
"success": True,
|
||||
"data": result.data,
|
||||
"data": result.data if isinstance(result.data, str) else json.dumps(result.data, ensure_ascii=False),
|
||||
"error_code": "",
|
||||
"execution_time": result.execution_time
|
||||
}
|
||||
else:
|
||||
logger.error(f"节点 {self.node_id} 工具执行失败: {result.error}")
|
||||
return {
|
||||
"success": False,
|
||||
"data": result.error,
|
||||
"data": result.error if isinstance(result.error, str) else json.dumps(result.error, ensure_ascii=False),
|
||||
"error_code": result.error_code,
|
||||
"execution_time": result.execution_time
|
||||
}
|
||||
@@ -211,12 +211,11 @@ class ToolExecution(Base):
|
||||
token_usage = Column(JSON)
|
||||
|
||||
# 用户信息
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), index=True)
|
||||
user_id = Column(UUID(as_uuid=True), index=True, nullable=True)
|
||||
workspace_id = Column(UUID(as_uuid=True), ForeignKey("workspaces.id"), nullable=False, index=True)
|
||||
|
||||
# 关联关系
|
||||
tool_config = relationship("ToolConfig", back_populates="executions")
|
||||
user = relationship("User")
|
||||
workspace = relationship("Workspace")
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
用户记忆相关的请求和响应模型
|
||||
情景记忆的请求和响应模型
|
||||
"""
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
@@ -28,16 +28,3 @@ class EpisodicMemoryDetailsRequest(BaseModel):
|
||||
|
||||
end_user_id: str = Field(..., description="终端用户ID")
|
||||
summary_id: str = Field(..., description="情景记忆摘要ID")
|
||||
|
||||
|
||||
class ExplicitMemoryOverviewRequest(BaseModel):
|
||||
"""显性记忆总览查询请求"""
|
||||
|
||||
end_user_id: str = Field(..., description="终端用户ID")
|
||||
|
||||
|
||||
class ExplicitMemoryDetailsRequest(BaseModel):
|
||||
"""显性记忆详情查询请求"""
|
||||
|
||||
end_user_id: str = Field(..., description="终端用户ID")
|
||||
memory_id: str = Field(..., description="记忆ID(情景记忆或语义记忆的ID)")
|
||||
15
api/app/schemas/memory_explicit_schema.py
Normal file
15
api/app/schemas/memory_explicit_schema.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
显性记忆的请求和响应模型
|
||||
"""
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class ExplicitMemoryOverviewRequest(BaseModel):
|
||||
"""显性记忆总览查询请求"""
|
||||
|
||||
end_user_id: str = Field(..., description="终端用户ID")
|
||||
|
||||
class ExplicitMemoryDetailsRequest(BaseModel):
|
||||
"""显性记忆详情查询请求"""
|
||||
|
||||
end_user_id: str = Field(..., description="终端用户ID")
|
||||
memory_id: str = Field(..., description="记忆ID(情景记忆或语义记忆的ID)")
|
||||
@@ -55,7 +55,7 @@ def create_long_term_memory_tool(memory_config: Dict[str, Any], end_user_id: str
|
||||
长期记忆工具
|
||||
"""
|
||||
# search_switch = memory_config.get("search_switch", "2")
|
||||
config_id= memory_config.get("memory_content",'17')
|
||||
config_id= memory_config.get("memory_content",None)
|
||||
logger.info(f"创建长期记忆工具,配置: end_user_id={end_user_id}, config_id={config_id}, storage_type={storage_type}")
|
||||
@tool(args_schema=LongTermMemoryInput)
|
||||
def long_term_memory(question: str) -> str:
|
||||
@@ -94,7 +94,7 @@ def create_long_term_memory_tool(memory_config: Dict[str, Any], end_user_id: str
|
||||
group_id=end_user_id,
|
||||
message=question,
|
||||
history=[],
|
||||
search_switch="1",
|
||||
search_switch="2",
|
||||
config_id=config_id,
|
||||
db=db,
|
||||
storage_type=storage_type,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from uuid import UUID
|
||||
from typing import Dict, Any
|
||||
|
||||
from app.repositories.home_page_repository import HomePageRepository
|
||||
from app.schemas.home_page_schema import HomeStatistics, WorkspaceInfo
|
||||
@@ -68,4 +73,69 @@ class HomePageService:
|
||||
)
|
||||
workspace_list.append(workspace_info)
|
||||
|
||||
return workspace_list
|
||||
return workspace_list
|
||||
|
||||
@staticmethod
|
||||
def load_version_introduction(version: str) -> Dict[str, Any]:
|
||||
"""
|
||||
从 JSON 文件加载对应版本的介绍
|
||||
:param version: 系统版本号(如 "0.2.0")
|
||||
:return: 对应版本的详细介绍
|
||||
"""
|
||||
# 1. 定义 JSON 文件路径(使用 Path 处理跨平台路径问题)
|
||||
json_file_path = Path(__file__).parent.parent / "version_info.json"
|
||||
# 转换为绝对路径,便于调试
|
||||
json_abs_path = json_file_path.resolve()
|
||||
|
||||
try:
|
||||
# 2. 读取 JSON 文件
|
||||
if not json_abs_path.exists():
|
||||
return {
|
||||
"message": f"版本介绍文件不存在:{json_abs_path}",
|
||||
"codeName": "",
|
||||
"releaseDate": "",
|
||||
"upgradePosition": "",
|
||||
"coreUpgrades": []
|
||||
}
|
||||
|
||||
with open(json_abs_path, "r", encoding="utf-8") as f:
|
||||
changelogs = json.load(f)
|
||||
|
||||
# 3. 匹配对应版本的介绍,若版本不存在返回默认提示
|
||||
if version not in changelogs:
|
||||
return {
|
||||
"message": f"暂未查询到 {version} 版本的详细介绍",
|
||||
"codeName": "",
|
||||
"releaseDate": "",
|
||||
"upgradePosition": "",
|
||||
"coreUpgrades": []
|
||||
}
|
||||
return changelogs[version]
|
||||
|
||||
except FileNotFoundError as e:
|
||||
# 处理文件不存在异常
|
||||
return {
|
||||
"message": f"系统内部错误:{str(e)}",
|
||||
"codeName": "",
|
||||
"releaseDate": "",
|
||||
"upgradePosition": "",
|
||||
"coreUpgrades": []
|
||||
}
|
||||
except json.JSONDecodeError:
|
||||
# 处理 JSON 格式错误
|
||||
return {
|
||||
"message": "版本介绍文件格式错误,无法解析 JSON",
|
||||
"codeName": "",
|
||||
"releaseDate": "",
|
||||
"upgradePosition": "",
|
||||
"coreUpgrades": []
|
||||
}
|
||||
except Exception as e:
|
||||
# 处理其他未知异常
|
||||
return {
|
||||
"message": f"加载版本介绍失败:{str(e)}",
|
||||
"codeName": "",
|
||||
"releaseDate": "",
|
||||
"upgradePosition": "",
|
||||
"coreUpgrades": []
|
||||
}
|
||||
@@ -4,7 +4,6 @@ Memory Agent Service
|
||||
Handles business logic for memory agent operations including read/write services,
|
||||
health checks, and message type classification.
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -27,7 +26,7 @@ from app.db import get_db_context
|
||||
from app.models.knowledge_model import Knowledge, KnowledgeType
|
||||
from app.repositories.memory_short_repository import ShortTermMemoryRepository
|
||||
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
|
||||
from app.schemas.memory_config_schema import ConfigurationError, MemoryConfig
|
||||
from app.schemas.memory_config_schema import ConfigurationError
|
||||
from app.services.memory_config_service import MemoryConfigService
|
||||
from app.services.memory_konwledges_server import (
|
||||
write_rag,
|
||||
@@ -610,7 +609,7 @@ class MemoryAgentService:
|
||||
reranked_results=raw_results.get('reranked_results',[])
|
||||
try:
|
||||
statements=[statement['statement'] for statement in reranked_results.get('statements', [])]
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
statements=[]
|
||||
statements=list(set(statements))
|
||||
retrieved_content.append({query:statements})
|
||||
@@ -832,7 +831,6 @@ class MemoryAgentService:
|
||||
# 获取当前空间下的所有宿主
|
||||
from app.repositories import app_repository, end_user_repository
|
||||
from app.schemas.app_schema import App as AppSchema
|
||||
from app.schemas.end_user_schema import EndUser as EndUserSchema
|
||||
|
||||
# 查询应用并转换为 Pydantic 模型
|
||||
apps_orm = app_repository.get_apps_by_workspace_id(db, current_workspace_id)
|
||||
@@ -1175,19 +1173,21 @@ def get_end_user_connected_config(end_user_id: str, db: Session) -> Dict[str, An
|
||||
1. 根据 end_user_id 获取用户的 app_id
|
||||
2. 获取该应用的最新发布版本
|
||||
3. 从发布版本的 config 字段中提取 memory_config_id
|
||||
4. 根据 memory_config_id 查询配置名称
|
||||
|
||||
Args:
|
||||
end_user_id: 终端用户ID
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
包含 memory_config_id 和相关信息的字典
|
||||
包含 memory_config_id、config_name 和相关信息的字典
|
||||
|
||||
Raises:
|
||||
ValueError: 当终端用户不存在或应用未发布时
|
||||
"""
|
||||
from app.models.app_release_model import AppRelease
|
||||
from app.models.end_user_model import EndUser
|
||||
from app.models.data_config_model import DataConfig
|
||||
from sqlalchemy import select
|
||||
|
||||
logger.info(f"Getting connected config for end_user: {end_user_id}")
|
||||
@@ -1220,13 +1220,158 @@ def get_end_user_connected_config(end_user_id: str, db: Session) -> Dict[str, An
|
||||
memory_obj = config.get('memory', {})
|
||||
memory_config_id = memory_obj.get('memory_content') if isinstance(memory_obj, dict) else None
|
||||
|
||||
# 4. 根据 memory_config_id 查询配置名称
|
||||
config_name = None
|
||||
if memory_config_id:
|
||||
try:
|
||||
# memory_config_id 可能是整数或字符串,需要转换
|
||||
config_id = int(memory_config_id) if isinstance(memory_config_id, str) else memory_config_id
|
||||
data_config = db.query(DataConfig).filter(DataConfig.config_id == config_id).first()
|
||||
if data_config:
|
||||
config_name = data_config.config_name
|
||||
logger.debug(f"Found config_name: {config_name} for config_id: {config_id}")
|
||||
else:
|
||||
logger.warning(f"DataConfig not found for config_id: {config_id}")
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.warning(f"Invalid memory_config_id format: {memory_config_id}, error: {str(e)}")
|
||||
|
||||
result = {
|
||||
"end_user_id": str(end_user_id),
|
||||
"app_id": str(app_id),
|
||||
"release_id": str(latest_release.id),
|
||||
"release_version": latest_release.version,
|
||||
"memory_config_id": memory_config_id
|
||||
"memory_config_id": memory_config_id,
|
||||
"memory_config_name": config_name
|
||||
}
|
||||
|
||||
logger.info(f"Successfully retrieved connected config: memory_config_id={memory_config_id}")
|
||||
logger.info(f"Successfully retrieved connected config: memory_config_id={memory_config_id}, config_name={config_name}")
|
||||
return result
|
||||
|
||||
|
||||
def get_end_users_connected_configs_batch(end_user_ids: List[str], db: Session) -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
批量获取多个终端用户关联的记忆配置
|
||||
|
||||
通过优化的查询减少数据库往返次数:
|
||||
1. 一次性查询所有 end_user 及其 app_id
|
||||
2. 批量查询所有相关的 app_release
|
||||
3. 批量查询所有相关的 data_config
|
||||
|
||||
Args:
|
||||
end_user_ids: 终端用户ID列表
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
字典,key 为 end_user_id,value 为配置信息字典
|
||||
对于查询失败的用户,value 包含 error 字段
|
||||
"""
|
||||
from app.models.app_release_model import AppRelease
|
||||
from app.models.end_user_model import EndUser
|
||||
from app.models.data_config_model import DataConfig
|
||||
from sqlalchemy import select
|
||||
|
||||
logger.info(f"Batch getting connected configs for {len(end_user_ids)} end users")
|
||||
|
||||
result = {}
|
||||
|
||||
# 1. 批量查询所有 end_user 及其 app_id
|
||||
end_users = db.query(EndUser).filter(EndUser.id.in_(end_user_ids)).all()
|
||||
|
||||
# 构建 end_user_id -> end_user 的映射
|
||||
end_user_map = {str(user.id): user for user in end_users}
|
||||
|
||||
# 记录不存在的用户
|
||||
for user_id in end_user_ids:
|
||||
if user_id not in end_user_map:
|
||||
result[user_id] = {
|
||||
"end_user_id": user_id,
|
||||
"memory_config_id": None,
|
||||
"memory_config_name": None,
|
||||
"error": f"终端用户不存在: {user_id}"
|
||||
}
|
||||
|
||||
if not end_users:
|
||||
logger.warning("No valid end users found")
|
||||
return result
|
||||
|
||||
# 2. 批量查询所有相关应用的最新发布版本
|
||||
app_ids = [user.app_id for user in end_users]
|
||||
|
||||
# 使用子查询找到每个 app 的最新版本
|
||||
from sqlalchemy import and_
|
||||
|
||||
# 查询所有相关的活跃发布版本
|
||||
releases = db.query(AppRelease).filter(
|
||||
and_(
|
||||
AppRelease.app_id.in_(app_ids),
|
||||
AppRelease.is_active.is_(True)
|
||||
)
|
||||
).order_by(AppRelease.app_id, AppRelease.version.desc()).all()
|
||||
|
||||
# 构建 app_id -> latest_release 的映射(每个 app 只保留最新版本)
|
||||
app_release_map = {}
|
||||
for release in releases:
|
||||
app_id_str = str(release.app_id)
|
||||
if app_id_str not in app_release_map:
|
||||
app_release_map[app_id_str] = release
|
||||
|
||||
# 3. 收集所有 memory_config_id
|
||||
memory_config_ids = []
|
||||
for release in app_release_map.values():
|
||||
config = release.config or {}
|
||||
memory_obj = config.get('memory', {})
|
||||
memory_config_id = memory_obj.get('memory_content') if isinstance(memory_obj, dict) else None
|
||||
if memory_config_id:
|
||||
try:
|
||||
config_id = int(memory_config_id) if isinstance(memory_config_id, str) else memory_config_id
|
||||
memory_config_ids.append(config_id)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 4. 批量查询所有 data_config
|
||||
config_name_map = {}
|
||||
if memory_config_ids:
|
||||
data_configs = db.query(DataConfig).filter(
|
||||
DataConfig.config_id.in_(memory_config_ids)
|
||||
).all()
|
||||
config_name_map = {config.config_id: config.config_name for config in data_configs}
|
||||
|
||||
# 5. 组装结果
|
||||
for user in end_users:
|
||||
user_id = str(user.id)
|
||||
app_id = str(user.app_id)
|
||||
|
||||
# 检查是否有发布版本
|
||||
if app_id not in app_release_map:
|
||||
result[user_id] = {
|
||||
"end_user_id": user_id,
|
||||
"memory_config_id": None,
|
||||
"memory_config_name": None,
|
||||
"error": f"应用未发布: {app_id}"
|
||||
}
|
||||
continue
|
||||
|
||||
release = app_release_map[app_id]
|
||||
|
||||
# 提取 memory_config_id
|
||||
config = release.config or {}
|
||||
memory_obj = config.get('memory', {})
|
||||
memory_config_id = memory_obj.get('memory_content') if isinstance(memory_obj, dict) else None
|
||||
|
||||
# 获取 config_name
|
||||
config_name = None
|
||||
if memory_config_id:
|
||||
try:
|
||||
config_id = int(memory_config_id) if isinstance(memory_config_id, str) else memory_config_id
|
||||
config_name = config_name_map.get(config_id)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
result[user_id] = {
|
||||
"end_user_id": user_id,
|
||||
"memory_config_id": memory_config_id,
|
||||
"memory_config_name": config_name
|
||||
}
|
||||
|
||||
logger.info(f"Successfully retrieved batch configs: total={len(result)}, with_config={sum(1 for v in result.values() if v.get('memory_config_id'))}")
|
||||
return result
|
||||
297
api/app/services/memory_base_service.py
Normal file
297
api/app/services/memory_base_service.py
Normal file
@@ -0,0 +1,297 @@
|
||||
"""
|
||||
Memory Base Service
|
||||
|
||||
提供记忆服务的基础功能和共享辅助方法。
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from app.core.logging_config import get_logger
|
||||
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
|
||||
from app.services.emotion_analytics_service import EmotionAnalyticsService
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MemoryBaseService:
|
||||
"""记忆服务基类,提供共享的辅助方法"""
|
||||
|
||||
def __init__(self):
|
||||
self.neo4j_connector = Neo4jConnector()
|
||||
|
||||
@staticmethod
|
||||
def parse_timestamp(timestamp_value) -> Optional[int]:
|
||||
"""
|
||||
将时间戳转换为毫秒级时间戳
|
||||
|
||||
支持多种输入格式:
|
||||
- Neo4j DateTime 对象
|
||||
- ISO格式的时间戳字符串
|
||||
- Python datetime 对象
|
||||
|
||||
Args:
|
||||
timestamp_value: 时间戳值(可以是多种类型)
|
||||
|
||||
Returns:
|
||||
毫秒级时间戳,如果解析失败则返回None
|
||||
"""
|
||||
if not timestamp_value:
|
||||
return None
|
||||
|
||||
try:
|
||||
# 处理 Neo4j DateTime 对象
|
||||
if hasattr(timestamp_value, 'to_native'):
|
||||
dt_object = timestamp_value.to_native()
|
||||
return int(dt_object.timestamp() * 1000)
|
||||
|
||||
# 处理 Python datetime 对象
|
||||
if isinstance(timestamp_value, datetime):
|
||||
return int(timestamp_value.timestamp() * 1000)
|
||||
|
||||
# 处理字符串格式
|
||||
if isinstance(timestamp_value, str):
|
||||
dt_object = datetime.fromisoformat(timestamp_value.replace("Z", "+00:00"))
|
||||
return int(dt_object.timestamp() * 1000)
|
||||
|
||||
# 其他情况尝试转换为字符串再解析
|
||||
dt_object = datetime.fromisoformat(str(timestamp_value).replace("Z", "+00:00"))
|
||||
return int(dt_object.timestamp() * 1000)
|
||||
|
||||
except (ValueError, TypeError, AttributeError) as e:
|
||||
logger.warning(f"无法解析时间戳: {timestamp_value}, error={str(e)}")
|
||||
return None
|
||||
|
||||
async def extract_episodic_emotion(
|
||||
self,
|
||||
summary_id: str,
|
||||
end_user_id: str
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
提取情景记忆的主要情绪
|
||||
|
||||
查询MemorySummary节点关联的Statement节点,
|
||||
返回emotion_intensity最大的emotion_type。
|
||||
|
||||
Args:
|
||||
summary_id: Summary节点的ID
|
||||
end_user_id: 终端用户ID (group_id)
|
||||
|
||||
Returns:
|
||||
最大emotion_intensity对应的emotion_type,如果没有则返回None
|
||||
"""
|
||||
try:
|
||||
query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE elementId(s) = $summary_id AND s.group_id = $group_id
|
||||
MATCH (s)-[:DERIVED_FROM_STATEMENT]->(stmt:Statement)
|
||||
WHERE stmt.emotion_type IS NOT NULL
|
||||
AND stmt.emotion_intensity IS NOT NULL
|
||||
RETURN stmt.emotion_type AS emotion_type,
|
||||
stmt.emotion_intensity AS emotion_intensity
|
||||
ORDER BY emotion_intensity DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
|
||||
result = await self.neo4j_connector.execute_query(
|
||||
query,
|
||||
summary_id=summary_id,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
if result and len(result) > 0:
|
||||
emotion_type = result[0].get("emotion_type")
|
||||
logger.info(f"成功提取 summary_id={summary_id} 的情绪: {emotion_type}")
|
||||
return emotion_type
|
||||
else:
|
||||
logger.info(f"summary_id={summary_id} 没有情绪信息")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"提取情景记忆情绪时出错: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
async def get_episodic_memory_count(
|
||||
self,
|
||||
end_user_id: Optional[str] = None
|
||||
) -> int:
|
||||
"""
|
||||
获取情景记忆数量
|
||||
|
||||
查询 MemorySummary 节点的数量。
|
||||
|
||||
Args:
|
||||
end_user_id: 可选的终端用户ID,用于过滤特定用户的节点
|
||||
|
||||
Returns:
|
||||
情景记忆的数量
|
||||
"""
|
||||
try:
|
||||
if end_user_id:
|
||||
query = """
|
||||
MATCH (n:MemorySummary)
|
||||
WHERE n.group_id = $group_id
|
||||
RETURN count(n) as count
|
||||
"""
|
||||
result = await self.neo4j_connector.execute_query(query, group_id=end_user_id)
|
||||
else:
|
||||
query = """
|
||||
MATCH (n:MemorySummary)
|
||||
RETURN count(n) as count
|
||||
"""
|
||||
result = await self.neo4j_connector.execute_query(query)
|
||||
|
||||
count = result[0]["count"] if result and len(result) > 0 else 0
|
||||
logger.debug(f"情景记忆数量: {count} (end_user_id={end_user_id})")
|
||||
return count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取情景记忆数量时出错: {str(e)}", exc_info=True)
|
||||
return 0
|
||||
|
||||
async def get_explicit_memory_count(
|
||||
self,
|
||||
end_user_id: Optional[str] = None
|
||||
) -> int:
|
||||
"""
|
||||
获取显性记忆数量
|
||||
|
||||
显性记忆 = 情景记忆(MemorySummary)+ 语义记忆(ExtractedEntity with is_explicit_memory=true)
|
||||
|
||||
Args:
|
||||
end_user_id: 可选的终端用户ID,用于过滤特定用户的节点
|
||||
|
||||
Returns:
|
||||
显性记忆的数量
|
||||
"""
|
||||
try:
|
||||
# 1. 获取情景记忆数量
|
||||
episodic_count = await self.get_episodic_memory_count(end_user_id)
|
||||
|
||||
# 2. 获取语义记忆数量(ExtractedEntity 且 is_explicit_memory = true)
|
||||
if end_user_id:
|
||||
semantic_query = """
|
||||
MATCH (e:ExtractedEntity)
|
||||
WHERE e.group_id = $group_id AND e.is_explicit_memory = true
|
||||
RETURN count(e) as count
|
||||
"""
|
||||
semantic_result = await self.neo4j_connector.execute_query(
|
||||
semantic_query,
|
||||
group_id=end_user_id
|
||||
)
|
||||
else:
|
||||
semantic_query = """
|
||||
MATCH (e:ExtractedEntity)
|
||||
WHERE e.is_explicit_memory = true
|
||||
RETURN count(e) as count
|
||||
"""
|
||||
semantic_result = await self.neo4j_connector.execute_query(semantic_query)
|
||||
|
||||
semantic_count = semantic_result[0]["count"] if semantic_result and len(semantic_result) > 0 else 0
|
||||
|
||||
# 3. 计算总数
|
||||
explicit_count = episodic_count + semantic_count
|
||||
logger.debug(
|
||||
f"显性记忆数量: {explicit_count} "
|
||||
f"(情景={episodic_count}, 语义={semantic_count}, end_user_id={end_user_id})"
|
||||
)
|
||||
return explicit_count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取显性记忆数量时出错: {str(e)}", exc_info=True)
|
||||
return 0
|
||||
|
||||
async def get_emotional_memory_count(
|
||||
self,
|
||||
end_user_id: Optional[str] = None,
|
||||
statement_count_fallback: int = 0
|
||||
) -> int:
|
||||
"""
|
||||
获取情绪记忆数量
|
||||
|
||||
通过 EmotionAnalyticsService 获取情绪标签统计总数。
|
||||
如果获取失败或没有指定 end_user_id,使用 statement_count_fallback 作为后备。
|
||||
|
||||
Args:
|
||||
end_user_id: 可选的终端用户ID
|
||||
statement_count_fallback: 后备方案的数量(通常是 statement 节点数量)
|
||||
|
||||
Returns:
|
||||
情绪记忆的数量
|
||||
"""
|
||||
try:
|
||||
if end_user_id:
|
||||
emotion_service = EmotionAnalyticsService()
|
||||
|
||||
emotion_data = await emotion_service.get_emotion_tags(
|
||||
end_user_id=end_user_id,
|
||||
emotion_type=None,
|
||||
start_date=None,
|
||||
end_date=None,
|
||||
limit=10
|
||||
)
|
||||
emotion_count = emotion_data.get("total_count", 0)
|
||||
logger.debug(f"情绪记忆数量: {emotion_count} (end_user_id={end_user_id})")
|
||||
return emotion_count
|
||||
else:
|
||||
# 如果没有指定 end_user_id,使用后备方案
|
||||
logger.debug(f"情绪记忆数量: {statement_count_fallback} (使用后备方案)")
|
||||
return statement_count_fallback
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"获取情绪记忆数量失败,使用后备方案: {str(e)}")
|
||||
return statement_count_fallback
|
||||
|
||||
async def get_forget_memory_count(
|
||||
self,
|
||||
end_user_id: Optional[str] = None,
|
||||
forgetting_threshold: float = 0.3
|
||||
) -> int:
|
||||
"""
|
||||
获取遗忘记忆数量
|
||||
|
||||
统计激活值低于遗忘阈值的节点数量(low_activation_nodes)。
|
||||
查询范围包括:Statement、ExtractedEntity、MemorySummary、Chunk 节点。
|
||||
|
||||
Args:
|
||||
end_user_id: 可选的终端用户ID,用于过滤特定用户的节点
|
||||
forgetting_threshold: 遗忘阈值,默认 0.3
|
||||
|
||||
Returns:
|
||||
遗忘记忆的数量(激活值低于阈值的节点数)
|
||||
"""
|
||||
try:
|
||||
# 构建查询语句
|
||||
query = """
|
||||
MATCH (n)
|
||||
WHERE (n:Statement OR n:ExtractedEntity OR n:MemorySummary OR n:Chunk)
|
||||
"""
|
||||
|
||||
if end_user_id:
|
||||
query += " AND n.group_id = $group_id"
|
||||
|
||||
query += """
|
||||
RETURN sum(CASE WHEN n.activation_value IS NOT NULL AND n.activation_value < $threshold THEN 1 ELSE 0 END) as low_activation_nodes
|
||||
"""
|
||||
|
||||
# 设置查询参数
|
||||
params = {'threshold': forgetting_threshold}
|
||||
if end_user_id:
|
||||
params['group_id'] = end_user_id
|
||||
|
||||
# 执行查询
|
||||
result = await self.neo4j_connector.execute_query(query, **params)
|
||||
|
||||
# 提取结果
|
||||
forget_count = result[0]['low_activation_nodes'] if result and len(result) > 0 else 0
|
||||
forget_count = forget_count or 0 # 处理 None 值
|
||||
|
||||
logger.debug(
|
||||
f"遗忘记忆数量: {forget_count} "
|
||||
f"(threshold={forgetting_threshold}, end_user_id={end_user_id})"
|
||||
)
|
||||
return forget_count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取遗忘记忆数量时出错: {str(e)}", exc_info=True)
|
||||
return 0
|
||||
405
api/app/services/memory_episodic_service.py
Normal file
405
api/app/services/memory_episodic_service.py
Normal file
@@ -0,0 +1,405 @@
|
||||
"""
|
||||
Episodic Memory Service
|
||||
|
||||
处理情景记忆相关的业务逻辑,包括情景记忆总览、详情查询等。
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import pytz
|
||||
from app.core.logging_config import get_logger
|
||||
from app.services.memory_base_service import MemoryBaseService
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MemoryEpisodicService(MemoryBaseService):
|
||||
"""情景记忆服务类"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
logger.info("MemoryEpisodicService initialized")
|
||||
|
||||
async def _get_title_and_type(
|
||||
self,
|
||||
summary_id: str,
|
||||
end_user_id: str
|
||||
) -> Tuple[str, str]:
|
||||
"""
|
||||
读取情景记忆的标题(title)和类型(type)
|
||||
|
||||
仅负责读取已存在的title和type,不进行生成
|
||||
title从name属性读取,type从memory_type属性读取
|
||||
|
||||
Args:
|
||||
summary_id: Summary节点的ID
|
||||
end_user_id: 终端用户ID (group_id)
|
||||
|
||||
Returns:
|
||||
(标题, 类型)元组,如果不存在则返回默认值
|
||||
"""
|
||||
try:
|
||||
# 查询Summary节点的name(作为title)和memory_type(作为type)
|
||||
query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE elementId(s) = $summary_id AND s.group_id = $group_id
|
||||
RETURN s.name AS title, s.memory_type AS type
|
||||
"""
|
||||
|
||||
result = await self.neo4j_connector.execute_query(
|
||||
query,
|
||||
summary_id=summary_id,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
if not result or len(result) == 0:
|
||||
logger.warning(f"未找到 summary_id={summary_id} 的节点")
|
||||
return ("未知标题", "其他")
|
||||
|
||||
record = result[0]
|
||||
title = record.get("title") or "未命名"
|
||||
episodic_type = record.get("type") or "其他"
|
||||
|
||||
return (title, episodic_type)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"读取标题和类型时出错: {str(e)}", exc_info=True)
|
||||
return ("错误", "其他")
|
||||
|
||||
async def _extract_involved_objects(
|
||||
self,
|
||||
summary_id: str,
|
||||
end_user_id: str
|
||||
) -> List[str]:
|
||||
"""
|
||||
提取情景记忆涉及的前3个最重要实体
|
||||
|
||||
Args:
|
||||
summary_id: Summary节点的ID
|
||||
end_user_id: 终端用户ID (group_id)
|
||||
|
||||
Returns:
|
||||
前3个实体的name属性列表
|
||||
"""
|
||||
try:
|
||||
# 查询Summary节点指向的Statement节点,再查询Statement指向的ExtractedEntity节点
|
||||
# 按activation_value降序排序,返回前3个
|
||||
query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE elementId(s) = $summary_id AND s.group_id = $group_id
|
||||
MATCH (s)-[:DERIVED_FROM_STATEMENT]->(stmt:Statement)
|
||||
MATCH (stmt)-[:REFERENCES_ENTITY]->(entity:ExtractedEntity)
|
||||
WHERE entity.activation_value IS NOT NULL
|
||||
RETURN DISTINCT entity.name AS name, entity.activation_value AS activation
|
||||
ORDER BY activation DESC
|
||||
LIMIT 3
|
||||
"""
|
||||
|
||||
result = await self.neo4j_connector.execute_query(
|
||||
query,
|
||||
summary_id=summary_id,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
# 提取实体名称
|
||||
involved_objects = [record["name"] for record in result if record.get("name")]
|
||||
|
||||
logger.info(f"成功提取 summary_id={summary_id} 的涉及对象: {involved_objects}")
|
||||
|
||||
return involved_objects
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"提取涉及对象时出错: {str(e)}", exc_info=True)
|
||||
return []
|
||||
|
||||
async def _extract_content_records(
|
||||
self,
|
||||
summary_id: str,
|
||||
end_user_id: str
|
||||
) -> List[str]:
|
||||
"""
|
||||
提取情景记忆的内容记录
|
||||
|
||||
Args:
|
||||
summary_id: Summary节点的ID
|
||||
end_user_id: 终端用户ID (group_id)
|
||||
|
||||
Returns:
|
||||
所有Statement节点的statement属性内容列表
|
||||
"""
|
||||
try:
|
||||
# 查询Summary节点指向的所有Statement节点
|
||||
query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE elementId(s) = $summary_id AND s.group_id = $group_id
|
||||
MATCH (s)-[:DERIVED_FROM_STATEMENT]->(stmt:Statement)
|
||||
WHERE stmt.statement IS NOT NULL AND stmt.statement <> ''
|
||||
RETURN stmt.statement AS statement
|
||||
"""
|
||||
|
||||
result = await self.neo4j_connector.execute_query(
|
||||
query,
|
||||
summary_id=summary_id,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
# 提取statement内容
|
||||
content_records = [record["statement"] for record in result if record.get("statement")]
|
||||
|
||||
logger.info(f"成功提取 summary_id={summary_id} 的内容记录,共 {len(content_records)} 条")
|
||||
|
||||
return content_records
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"提取内容记录时出错: {str(e)}", exc_info=True)
|
||||
return []
|
||||
|
||||
def _calculate_time_filter(self, time_range: str) -> Optional[str]:
|
||||
"""
|
||||
根据时间范围计算过滤的起始时间
|
||||
|
||||
Args:
|
||||
time_range: 时间范围 (all/today/this_week/this_month)
|
||||
|
||||
Returns:
|
||||
ISO格式的时间字符串,如果是"all"则返回None
|
||||
"""
|
||||
if time_range == "all":
|
||||
return None
|
||||
|
||||
# 获取当前时间(UTC)
|
||||
now = datetime.now(pytz.UTC)
|
||||
|
||||
if time_range == "today":
|
||||
# 今天的开始时间(00:00:00)
|
||||
start_time = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
elif time_range == "this_week":
|
||||
# 本周的开始时间(周一00:00:00)
|
||||
days_since_monday = now.weekday()
|
||||
start_time = (now - timedelta(days=days_since_monday)).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
elif time_range == "this_month":
|
||||
# 本月的开始时间(1号00:00:00)
|
||||
start_time = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
else:
|
||||
return None
|
||||
|
||||
# 返回ISO格式字符串
|
||||
return start_time.isoformat()
|
||||
|
||||
async def get_episodic_memory_overview(
|
||||
self,
|
||||
end_user_id: str,
|
||||
time_range: str = "all",
|
||||
episodic_type: str = "all",
|
||||
title_keyword: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取情景记忆总览信息
|
||||
|
||||
Args:
|
||||
end_user_id: 终端用户ID
|
||||
time_range: 时间范围筛选
|
||||
episodic_type: 情景类型筛选
|
||||
title_keyword: 标题关键词(可选,用于模糊搜索)
|
||||
"""
|
||||
try:
|
||||
logger.info(
|
||||
f"开始查询 end_user_id={end_user_id} 的情景记忆总览, "
|
||||
f"time_range={time_range}, episodic_type={episodic_type}, title_keyword={title_keyword}"
|
||||
)
|
||||
|
||||
# 1. 先查询所有情景记忆的总数(不受筛选条件限制)
|
||||
total_all_query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE s.group_id = $group_id
|
||||
RETURN count(s) AS total_all
|
||||
"""
|
||||
total_all_result = await self.neo4j_connector.execute_query(
|
||||
total_all_query,
|
||||
group_id=end_user_id
|
||||
)
|
||||
total_all = total_all_result[0]["total_all"] if total_all_result else 0
|
||||
|
||||
# 2. 计算时间范围的起始时间戳
|
||||
time_filter = self._calculate_time_filter(time_range)
|
||||
|
||||
# 3. 构建Cypher查询
|
||||
query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE s.group_id = $group_id
|
||||
"""
|
||||
|
||||
# 添加时间范围过滤
|
||||
if time_filter:
|
||||
query += " AND s.created_at >= $time_filter"
|
||||
|
||||
# 添加标题关键词过滤(如果提供了title_keyword)
|
||||
if title_keyword:
|
||||
query += " AND toLower(s.name) CONTAINS toLower($title_keyword)"
|
||||
|
||||
query += """
|
||||
RETURN elementId(s) AS id,
|
||||
s.created_at AS created_at,
|
||||
s.memory_type AS type,
|
||||
s.name AS title
|
||||
ORDER BY s.created_at DESC
|
||||
"""
|
||||
|
||||
params = {"group_id": end_user_id}
|
||||
if time_filter:
|
||||
params["time_filter"] = time_filter
|
||||
if title_keyword:
|
||||
params["title_keyword"] = title_keyword
|
||||
|
||||
result = await self.neo4j_connector.execute_query(query, **params)
|
||||
|
||||
# 4. 如果没有数据,返回空列表
|
||||
if not result:
|
||||
logger.info(f"end_user_id={end_user_id} 没有情景记忆数据")
|
||||
return {
|
||||
"total": 0,
|
||||
"total_all": total_all,
|
||||
"episodic_memories": []
|
||||
}
|
||||
|
||||
# 5. 对每个节点读取标题和类型,并应用类型筛选
|
||||
episodic_memories = []
|
||||
for record in result:
|
||||
summary_id = record["id"]
|
||||
created_at_str = record.get("created_at")
|
||||
memory_type = record.get("type", "其他")
|
||||
title = record.get("title") or "未命名" # 直接从查询结果获取标题
|
||||
|
||||
# 应用情景类型筛选
|
||||
if episodic_type != "all":
|
||||
# 检查类型是否匹配
|
||||
# 注意:Neo4j 中存储的 memory_type 现在应该是英文格式(如 "conversation", "project_work")
|
||||
# 但为了兼容旧数据,我们也支持中文格式的匹配
|
||||
type_mapping = {
|
||||
"conversation": "对话",
|
||||
"project_work": "项目/工作",
|
||||
"learning": "学习",
|
||||
"decision": "决策",
|
||||
"important_event": "重要事件"
|
||||
}
|
||||
|
||||
# 获取对应的中文类型(用于兼容旧数据)
|
||||
chinese_type = type_mapping.get(episodic_type)
|
||||
|
||||
# 检查类型是否匹配(支持新的英文格式和旧的中文格式)
|
||||
if memory_type != episodic_type and memory_type != chinese_type:
|
||||
continue
|
||||
|
||||
# 使用基类方法转换时间戳
|
||||
created_at_timestamp = self.parse_timestamp(created_at_str)
|
||||
|
||||
episodic_memories.append({
|
||||
"id": summary_id,
|
||||
"title": title,
|
||||
"type": memory_type,
|
||||
"created_at": created_at_timestamp
|
||||
})
|
||||
|
||||
logger.info(
|
||||
f"成功获取 end_user_id={end_user_id} 的情景记忆总览,"
|
||||
f"筛选后 {len(episodic_memories)} 条,总共 {total_all} 条"
|
||||
)
|
||||
|
||||
return {
|
||||
"total": len(episodic_memories),
|
||||
"total_all": total_all,
|
||||
"episodic_memories": episodic_memories
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取情景记忆总览时出错: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
async def get_episodic_memory_details(
|
||||
self,
|
||||
end_user_id: str,
|
||||
summary_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取单个情景记忆的详细信息
|
||||
|
||||
"""
|
||||
try:
|
||||
logger.info(f"开始查询 end_user_id={end_user_id}, summary_id={summary_id} 的情景记忆详情")
|
||||
|
||||
# 1. 查询指定的MemorySummary节点
|
||||
query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE elementId(s) = $summary_id AND s.group_id = $group_id
|
||||
RETURN elementId(s) AS id, s.created_at AS created_at
|
||||
"""
|
||||
|
||||
result = await self.neo4j_connector.execute_query(
|
||||
query,
|
||||
summary_id=summary_id,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
# 2. 如果节点不存在,返回错误
|
||||
if not result or len(result) == 0:
|
||||
logger.warning(f"未找到 summary_id={summary_id} 的情景记忆")
|
||||
raise ValueError(f"情景记忆不存在: summary_id={summary_id}")
|
||||
|
||||
# 3. 获取基本信息
|
||||
record = result[0]
|
||||
created_at_str = record.get("created_at")
|
||||
|
||||
# 使用基类方法转换时间戳
|
||||
created_at_timestamp = self.parse_timestamp(created_at_str)
|
||||
|
||||
# 4. 调用_get_title_and_type读取标题和类型
|
||||
title, episodic_type = await self._get_title_and_type(
|
||||
summary_id=summary_id,
|
||||
end_user_id=end_user_id
|
||||
)
|
||||
|
||||
# 5. 调用_extract_involved_objects提取涉及对象
|
||||
involved_objects = await self._extract_involved_objects(
|
||||
summary_id=summary_id,
|
||||
end_user_id=end_user_id
|
||||
)
|
||||
|
||||
# 6. 调用_extract_content_records提取内容记录
|
||||
content_records = await self._extract_content_records(
|
||||
summary_id=summary_id,
|
||||
end_user_id=end_user_id
|
||||
)
|
||||
|
||||
# 7. 使用基类方法提取情绪
|
||||
emotion = await self.extract_episodic_emotion(
|
||||
summary_id=summary_id,
|
||||
end_user_id=end_user_id
|
||||
)
|
||||
|
||||
# 8. 返回完整的详情信息
|
||||
details = {
|
||||
"id": summary_id,
|
||||
"created_at": created_at_timestamp,
|
||||
"involved_objects": involved_objects,
|
||||
"episodic_type": episodic_type,
|
||||
"content_records": content_records,
|
||||
"emotion": emotion
|
||||
}
|
||||
|
||||
logger.info(f"成功获取 summary_id={summary_id} 的情景记忆详情")
|
||||
|
||||
return details
|
||||
|
||||
except ValueError:
|
||||
# 重新抛出ValueError,让Controller层处理
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取情景记忆详情时出错: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
# 创建全局服务实例(供控制器层使用)
|
||||
memory_episodic_service = MemoryEpisodicService()
|
||||
274
api/app/services/memory_explicit_service.py
Normal file
274
api/app/services/memory_explicit_service.py
Normal file
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
显性记忆服务
|
||||
|
||||
处理显性记忆相关的业务逻辑,包括情景记忆和语义记忆的查询。
|
||||
"""
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
from app.core.logging_config import get_logger
|
||||
from app.services.memory_base_service import MemoryBaseService
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class MemoryExplicitService(MemoryBaseService):
|
||||
"""显性记忆服务类"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
logger.info("MemoryExplicitService initialized")
|
||||
|
||||
async def get_explicit_memory_overview(
|
||||
self,
|
||||
end_user_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取显性记忆总览信息
|
||||
|
||||
返回两部分:
|
||||
1. 情景记忆(episodic_memories)- 来自MemorySummary节点
|
||||
2. 语义记忆(semantic_memories)- 来自ExtractedEntity节点(is_explicit_memory=true)
|
||||
|
||||
Args:
|
||||
end_user_id: 终端用户ID
|
||||
|
||||
Returns:
|
||||
{
|
||||
"total": int,
|
||||
"episodic_memories": [
|
||||
{
|
||||
"id": str,
|
||||
"title": str,
|
||||
"content": str,
|
||||
"created_at": int
|
||||
}
|
||||
],
|
||||
"semantic_memories": [
|
||||
{
|
||||
"id": str,
|
||||
"name": str,
|
||||
"entity_type": str,
|
||||
"core_definition": str
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
logger.info(f"开始查询 end_user_id={end_user_id} 的显性记忆总览(情景记忆+语义记忆)")
|
||||
|
||||
# ========== 1. 查询情景记忆(MemorySummary节点) ==========
|
||||
episodic_query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE s.group_id = $group_id
|
||||
RETURN elementId(s) AS id,
|
||||
s.name AS title,
|
||||
s.content AS content,
|
||||
s.created_at AS created_at
|
||||
ORDER BY s.created_at DESC
|
||||
"""
|
||||
|
||||
episodic_result = await self.neo4j_connector.execute_query(
|
||||
episodic_query,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
# 处理情景记忆数据
|
||||
episodic_memories = []
|
||||
if episodic_result:
|
||||
for record in episodic_result:
|
||||
summary_id = record["id"]
|
||||
title = record.get("title") or "未命名"
|
||||
content = record.get("content") or ""
|
||||
created_at_str = record.get("created_at")
|
||||
|
||||
# 使用基类方法转换时间戳
|
||||
created_at_timestamp = self.parse_timestamp(created_at_str)
|
||||
|
||||
# 注意:总览接口不返回 emotion 字段
|
||||
episodic_memories.append({
|
||||
"id": summary_id,
|
||||
"title": title,
|
||||
"content": content,
|
||||
"created_at": created_at_timestamp
|
||||
})
|
||||
|
||||
# ========== 2. 查询语义记忆(ExtractedEntity节点) ==========
|
||||
semantic_query = """
|
||||
MATCH (e:ExtractedEntity)
|
||||
WHERE e.group_id = $group_id
|
||||
AND e.is_explicit_memory = true
|
||||
RETURN elementId(e) AS id,
|
||||
e.name AS name,
|
||||
e.entity_type AS entity_type,
|
||||
e.description AS core_definition
|
||||
ORDER BY e.name ASC
|
||||
"""
|
||||
|
||||
semantic_result = await self.neo4j_connector.execute_query(
|
||||
semantic_query,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
# 处理语义记忆数据
|
||||
semantic_memories = []
|
||||
if semantic_result:
|
||||
for record in semantic_result:
|
||||
entity_id = record["id"]
|
||||
name = record.get("name") or "未命名"
|
||||
entity_type = record.get("entity_type") or "未分类"
|
||||
core_definition = record.get("core_definition") or ""
|
||||
|
||||
# 注意:总览接口不返回 detailed_notes 和 created_at 字段
|
||||
semantic_memories.append({
|
||||
"id": entity_id,
|
||||
"name": name,
|
||||
"entity_type": entity_type,
|
||||
"core_definition": core_definition
|
||||
})
|
||||
|
||||
# ========== 3. 返回结果 ==========
|
||||
total_count = len(episodic_memories) + len(semantic_memories)
|
||||
|
||||
logger.info(
|
||||
f"成功获取 end_user_id={end_user_id} 的显性记忆总览,"
|
||||
f"情景记忆={len(episodic_memories)} 条,语义记忆={len(semantic_memories)} 条,"
|
||||
f"总计 {total_count} 条"
|
||||
)
|
||||
|
||||
return {
|
||||
"total": total_count,
|
||||
"episodic_memories": episodic_memories,
|
||||
"semantic_memories": semantic_memories
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取显性记忆总览时出错: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
async def get_explicit_memory_details(
|
||||
self,
|
||||
end_user_id: str,
|
||||
memory_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取显性记忆详情
|
||||
|
||||
根据 memory_id 查询情景记忆或语义记忆的详细信息。
|
||||
先尝试查询情景记忆,如果找不到再查询语义记忆。
|
||||
|
||||
Args:
|
||||
end_user_id: 终端用户ID
|
||||
memory_id: 记忆ID(可以是情景记忆或语义记忆的ID)
|
||||
|
||||
Returns:
|
||||
情景记忆返回:
|
||||
{
|
||||
"memory_type": "episodic",
|
||||
"title": str,
|
||||
"content": str,
|
||||
"emotion": Dict,
|
||||
"created_at": int
|
||||
}
|
||||
|
||||
语义记忆返回:
|
||||
{
|
||||
"memory_type": "semantic",
|
||||
"name": str,
|
||||
"core_definition": str,
|
||||
"detailed_notes": str,
|
||||
"created_at": int
|
||||
}
|
||||
|
||||
Raises:
|
||||
ValueError: 当记忆不存在时
|
||||
"""
|
||||
try:
|
||||
logger.info(f"开始查询显性记忆详情: end_user_id={end_user_id}, memory_id={memory_id}")
|
||||
|
||||
# ========== 1. 先尝试查询情景记忆 ==========
|
||||
episodic_query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE elementId(s) = $memory_id AND s.group_id = $group_id
|
||||
RETURN s.name AS title,
|
||||
s.content AS content,
|
||||
s.created_at AS created_at
|
||||
"""
|
||||
|
||||
episodic_result = await self.neo4j_connector.execute_query(
|
||||
episodic_query,
|
||||
memory_id=memory_id,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
if episodic_result and len(episodic_result) > 0:
|
||||
record = episodic_result[0]
|
||||
title = record.get("title") or "未命名"
|
||||
content = record.get("content") or ""
|
||||
created_at_str = record.get("created_at")
|
||||
|
||||
# 使用基类方法转换时间戳
|
||||
created_at_timestamp = self.parse_timestamp(created_at_str)
|
||||
|
||||
# 使用基类方法获取情绪信息
|
||||
emotion = await self.extract_episodic_emotion(
|
||||
summary_id=memory_id,
|
||||
end_user_id=end_user_id
|
||||
)
|
||||
|
||||
logger.info(f"成功获取情景记忆详情: memory_id={memory_id}")
|
||||
return {
|
||||
"memory_type": "episodic",
|
||||
"title": title,
|
||||
"content": content,
|
||||
"emotion": emotion,
|
||||
"created_at": created_at_timestamp
|
||||
}
|
||||
|
||||
# ========== 2. 如果不是情景记忆,尝试查询语义记忆 ==========
|
||||
semantic_query = """
|
||||
MATCH (e:ExtractedEntity)
|
||||
WHERE elementId(e) = $memory_id
|
||||
AND e.group_id = $group_id
|
||||
AND e.is_explicit_memory = true
|
||||
RETURN e.name AS name,
|
||||
e.description AS core_definition,
|
||||
e.example AS detailed_notes,
|
||||
e.created_at AS created_at
|
||||
"""
|
||||
|
||||
semantic_result = await self.neo4j_connector.execute_query(
|
||||
semantic_query,
|
||||
memory_id=memory_id,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
if semantic_result and len(semantic_result) > 0:
|
||||
record = semantic_result[0]
|
||||
name = record.get("name") or "未命名"
|
||||
core_definition = record.get("core_definition") or ""
|
||||
detailed_notes = record.get("detailed_notes") or ""
|
||||
created_at_str = record.get("created_at")
|
||||
|
||||
# 使用基类方法转换时间戳
|
||||
created_at_timestamp = self.parse_timestamp(created_at_str)
|
||||
|
||||
logger.info(f"成功获取语义记忆详情: memory_id={memory_id}")
|
||||
return {
|
||||
"memory_type": "semantic",
|
||||
"name": name,
|
||||
"core_definition": core_definition,
|
||||
"detailed_notes": detailed_notes,
|
||||
"created_at": created_at_timestamp
|
||||
}
|
||||
|
||||
# ========== 3. 两种记忆都找不到 ==========
|
||||
logger.warning(f"记忆不存在: memory_id={memory_id}, end_user_id={end_user_id}")
|
||||
raise ValueError(f"记忆不存在: memory_id={memory_id}")
|
||||
|
||||
except ValueError:
|
||||
# 重新抛出 ValueError(记忆不存在)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取显性记忆详情时出错: {str(e)}", exc_info=True)
|
||||
raise
|
||||
@@ -15,6 +15,7 @@ from app.core.memory.utils.llm.llm_utils import MemoryClientFactory
|
||||
from app.db import get_db_context
|
||||
from app.repositories.end_user_repository import EndUserRepository
|
||||
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
|
||||
from app.services.memory_base_service import MemoryBaseService
|
||||
from app.services.memory_config_service import MemoryConfigService
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -883,866 +884,6 @@ class UserMemoryService:
|
||||
"failed": failed,
|
||||
"errors": errors + [{"error": f"批量处理失败: {str(e)}"}]
|
||||
}
|
||||
|
||||
async def _get_title_and_type(
|
||||
self,
|
||||
summary_id: str,
|
||||
end_user_id: str
|
||||
) -> Tuple[str, str]:
|
||||
"""
|
||||
读取情景记忆的标题(title)和类型(type)
|
||||
|
||||
仅负责读取已存在的title和type,不进行生成
|
||||
title从name属性读取,type从memory_type属性读取
|
||||
|
||||
Args:
|
||||
summary_id: Summary节点的ID
|
||||
end_user_id: 终端用户ID (group_id)
|
||||
|
||||
Returns:
|
||||
(标题, 类型)元组,如果不存在则返回默认值
|
||||
"""
|
||||
try:
|
||||
# 查询Summary节点的name(作为title)和memory_type(作为type)
|
||||
query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE elementId(s) = $summary_id AND s.group_id = $group_id
|
||||
RETURN s.name AS title, s.memory_type AS type
|
||||
"""
|
||||
|
||||
result = await self.neo4j_connector.execute_query(
|
||||
query,
|
||||
summary_id=summary_id,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
if not result or len(result) == 0:
|
||||
logger.warning(f"未找到 summary_id={summary_id} 的节点")
|
||||
return ("未知标题", "其他")
|
||||
|
||||
record = result[0]
|
||||
title = record.get("title") or "未命名"
|
||||
episodic_type = record.get("type") or "其他"
|
||||
|
||||
return (title, episodic_type)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"读取标题和类型时出错: {str(e)}", exc_info=True)
|
||||
return ("错误", "其他")
|
||||
|
||||
@staticmethod
|
||||
async def generate_title_and_type_for_summary(
|
||||
content: str,
|
||||
end_user_id: str
|
||||
) -> Tuple[str, str]:
|
||||
"""
|
||||
为MemorySummary生成标题和类型(静态方法,用于创建节点时调用)
|
||||
|
||||
此方法应该在创建MemorySummary节点时调用,生成title和type
|
||||
|
||||
Args:
|
||||
content: Summary的内容文本
|
||||
end_user_id: 终端用户ID (group_id)
|
||||
|
||||
Returns:
|
||||
(标题, 类型)元组
|
||||
"""
|
||||
from app.core.memory.utils.prompt.prompt_utils import render_episodic_title_and_type_prompt
|
||||
import json
|
||||
|
||||
# 定义有效的类型集合
|
||||
VALID_TYPES = {
|
||||
"conversation", # 对话
|
||||
"project_work", # 项目/工作
|
||||
"learning", # 学习
|
||||
"decision", # 决策
|
||||
"important_event" # 重要事件
|
||||
}
|
||||
DEFAULT_TYPE = "conversation" # 默认类型
|
||||
|
||||
try:
|
||||
if not content:
|
||||
logger.warning("content为空,无法生成标题和类型")
|
||||
return ("空内容", DEFAULT_TYPE)
|
||||
|
||||
# 1. 渲染Jinja2提示词模板
|
||||
prompt = await render_episodic_title_and_type_prompt(content)
|
||||
|
||||
# 2. 调用LLM生成标题和类型
|
||||
llm_client = _get_llm_client_for_user(end_user_id)
|
||||
messages = [
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
response = await llm_client.chat(messages=messages)
|
||||
|
||||
# 3. 解析LLM响应
|
||||
content_response = response.content
|
||||
if isinstance(content_response, list):
|
||||
if len(content_response) > 0:
|
||||
if isinstance(content_response[0], dict):
|
||||
text = content_response[0].get('text', content_response[0].get('content', str(content_response[0])))
|
||||
full_response = str(text)
|
||||
else:
|
||||
full_response = str(content_response[0])
|
||||
else:
|
||||
full_response = ""
|
||||
elif isinstance(content_response, dict):
|
||||
full_response = str(content_response.get('text', content_response.get('content', str(content_response))))
|
||||
else:
|
||||
full_response = str(content_response) if content_response is not None else ""
|
||||
|
||||
# 4. 解析JSON响应
|
||||
try:
|
||||
# 尝试从响应中提取JSON
|
||||
# 移除可能的markdown代码块标记
|
||||
json_str = full_response.strip()
|
||||
if json_str.startswith("```json"):
|
||||
json_str = json_str[7:]
|
||||
if json_str.startswith("```"):
|
||||
json_str = json_str[3:]
|
||||
if json_str.endswith("```"):
|
||||
json_str = json_str[:-3]
|
||||
json_str = json_str.strip()
|
||||
|
||||
result_data = json.loads(json_str)
|
||||
title = result_data.get("title", "未知标题")
|
||||
episodic_type_raw = result_data.get("type", DEFAULT_TYPE)
|
||||
|
||||
# 5. 校验和归一化类型
|
||||
# 将类型转换为小写并去除空格
|
||||
episodic_type_normalized = str(episodic_type_raw).lower().strip()
|
||||
|
||||
# 检查是否在有效类型集合中
|
||||
if episodic_type_normalized in VALID_TYPES:
|
||||
episodic_type = episodic_type_normalized
|
||||
else:
|
||||
# 尝试映射常见的中文类型到英文
|
||||
type_mapping = {
|
||||
"对话": "conversation",
|
||||
"项目": "project_work",
|
||||
"工作": "project_work",
|
||||
"项目/工作": "project_work",
|
||||
"学习": "learning",
|
||||
"决策": "decision",
|
||||
"重要事件": "important_event",
|
||||
"事件": "important_event"
|
||||
}
|
||||
episodic_type = type_mapping.get(episodic_type_raw, DEFAULT_TYPE)
|
||||
logger.warning(
|
||||
f"LLM返回的类型 '{episodic_type_raw}' 不在有效集合中,"
|
||||
f"已归一化为 '{episodic_type}'"
|
||||
)
|
||||
|
||||
logger.info(f"成功生成标题和类型: title={title}, type={episodic_type}")
|
||||
return (title, episodic_type)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"无法解析LLM响应为JSON: {full_response}")
|
||||
return ("解析失败", DEFAULT_TYPE)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成标题和类型时出错: {str(e)}", exc_info=True)
|
||||
return ("错误", DEFAULT_TYPE)
|
||||
|
||||
async def _extract_involved_objects(
|
||||
self,
|
||||
summary_id: str,
|
||||
end_user_id: str
|
||||
) -> List[str]:
|
||||
"""
|
||||
提取情景记忆涉及的前3个最重要实体
|
||||
|
||||
Args:
|
||||
summary_id: Summary节点的ID
|
||||
end_user_id: 终端用户ID (group_id)
|
||||
|
||||
Returns:
|
||||
前3个实体的name属性列表
|
||||
"""
|
||||
try:
|
||||
# 查询Summary节点指向的Statement节点,再查询Statement指向的ExtractedEntity节点
|
||||
# 按activation_value降序排序,返回前3个
|
||||
query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE elementId(s) = $summary_id AND s.group_id = $group_id
|
||||
MATCH (s)-[:DERIVED_FROM_STATEMENT]->(stmt:Statement)
|
||||
MATCH (stmt)-[:REFERENCES_ENTITY]->(entity:ExtractedEntity)
|
||||
WHERE entity.activation_value IS NOT NULL
|
||||
RETURN DISTINCT entity.name AS name, entity.activation_value AS activation
|
||||
ORDER BY activation DESC
|
||||
LIMIT 3
|
||||
"""
|
||||
|
||||
result = await self.neo4j_connector.execute_query(
|
||||
query,
|
||||
summary_id=summary_id,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
# 提取实体名称
|
||||
involved_objects = [record["name"] for record in result if record.get("name")]
|
||||
|
||||
logger.info(f"成功提取 summary_id={summary_id} 的涉及对象: {involved_objects}")
|
||||
|
||||
return involved_objects
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"提取涉及对象时出错: {str(e)}", exc_info=True)
|
||||
return []
|
||||
|
||||
async def _extract_content_records(
|
||||
self,
|
||||
summary_id: str,
|
||||
end_user_id: str
|
||||
) -> List[str]:
|
||||
"""
|
||||
提取情景记忆的内容记录
|
||||
|
||||
Args:
|
||||
summary_id: Summary节点的ID
|
||||
end_user_id: 终端用户ID (group_id)
|
||||
|
||||
Returns:
|
||||
所有Statement节点的statement属性内容列表
|
||||
"""
|
||||
try:
|
||||
# 查询Summary节点指向的所有Statement节点
|
||||
query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE elementId(s) = $summary_id AND s.group_id = $group_id
|
||||
MATCH (s)-[:DERIVED_FROM_STATEMENT]->(stmt:Statement)
|
||||
WHERE stmt.statement IS NOT NULL AND stmt.statement <> ''
|
||||
RETURN stmt.statement AS statement
|
||||
"""
|
||||
|
||||
result = await self.neo4j_connector.execute_query(
|
||||
query,
|
||||
summary_id=summary_id,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
# 提取statement内容
|
||||
content_records = [record["statement"] for record in result if record.get("statement")]
|
||||
|
||||
logger.info(f"成功提取 summary_id={summary_id} 的内容记录,共 {len(content_records)} 条")
|
||||
|
||||
return content_records
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"提取内容记录时出错: {str(e)}", exc_info=True)
|
||||
return []
|
||||
|
||||
async def _extract_episodic_emotion(
|
||||
self,
|
||||
summary_id: str,
|
||||
end_user_id: str
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
提取情景记忆的主要情绪
|
||||
|
||||
Args:
|
||||
summary_id: Summary节点的ID
|
||||
end_user_id: 终端用户ID (group_id)
|
||||
|
||||
Returns:
|
||||
最大emotion_intensity对应的emotion_type,如果没有则返回None
|
||||
"""
|
||||
try:
|
||||
# 查询Summary节点指向的所有Statement节点
|
||||
# 筛选具有emotion_type属性的节点
|
||||
# 按emotion_intensity降序排序,返回第一个
|
||||
query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE elementId(s) = $summary_id AND s.group_id = $group_id
|
||||
MATCH (s)-[:DERIVED_FROM_STATEMENT]->(stmt:Statement)
|
||||
WHERE stmt.emotion_type IS NOT NULL
|
||||
AND stmt.emotion_intensity IS NOT NULL
|
||||
RETURN stmt.emotion_type AS emotion_type,
|
||||
stmt.emotion_intensity AS emotion_intensity
|
||||
ORDER BY emotion_intensity DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
|
||||
result = await self.neo4j_connector.execute_query(
|
||||
query,
|
||||
summary_id=summary_id,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
# 提取emotion_type
|
||||
if result and len(result) > 0:
|
||||
emotion_type = result[0].get("emotion_type")
|
||||
logger.info(f"成功提取 summary_id={summary_id} 的情绪: {emotion_type}")
|
||||
return emotion_type
|
||||
else:
|
||||
logger.info(f"summary_id={summary_id} 没有情绪信息")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"提取情景记忆情绪时出错: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
async def get_episodic_memory_overview(
|
||||
self,
|
||||
db: Session,
|
||||
end_user_id: str,
|
||||
time_range: str = "all",
|
||||
episodic_type: str = "all",
|
||||
title_keyword: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取情景记忆总览信息
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
end_user_id: 终端用户ID
|
||||
time_range: 时间范围筛选
|
||||
episodic_type: 情景类型筛选
|
||||
title_keyword: 标题关键词(可选,用于模糊搜索)
|
||||
"""
|
||||
try:
|
||||
logger.info(
|
||||
f"开始查询 end_user_id={end_user_id} 的情景记忆总览, "
|
||||
f"time_range={time_range}, episodic_type={episodic_type}, title_keyword={title_keyword}"
|
||||
)
|
||||
|
||||
# 1. 先查询所有情景记忆的总数(不受筛选条件限制)
|
||||
total_all_query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE s.group_id = $group_id
|
||||
RETURN count(s) AS total_all
|
||||
"""
|
||||
total_all_result = await self.neo4j_connector.execute_query(
|
||||
total_all_query,
|
||||
group_id=end_user_id
|
||||
)
|
||||
total_all = total_all_result[0]["total_all"] if total_all_result else 0
|
||||
|
||||
# 2. 计算时间范围的起始时间戳
|
||||
time_filter = self._calculate_time_filter(time_range)
|
||||
|
||||
# 3. 构建Cypher查询
|
||||
query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE s.group_id = $group_id
|
||||
"""
|
||||
|
||||
# 添加时间范围过滤
|
||||
if time_filter:
|
||||
query += " AND s.created_at >= $time_filter"
|
||||
|
||||
# 添加标题关键词过滤(如果提供了title_keyword)
|
||||
if title_keyword:
|
||||
query += " AND toLower(s.name) CONTAINS toLower($title_keyword)"
|
||||
|
||||
query += """
|
||||
RETURN elementId(s) AS id,
|
||||
s.created_at AS created_at,
|
||||
s.memory_type AS type,
|
||||
s.name AS title
|
||||
ORDER BY s.created_at DESC
|
||||
"""
|
||||
|
||||
params = {"group_id": end_user_id}
|
||||
if time_filter:
|
||||
params["time_filter"] = time_filter
|
||||
if title_keyword:
|
||||
params["title_keyword"] = title_keyword
|
||||
|
||||
result = await self.neo4j_connector.execute_query(query, **params)
|
||||
|
||||
# 4. 如果没有数据,返回空列表
|
||||
if not result:
|
||||
logger.info(f"end_user_id={end_user_id} 没有情景记忆数据")
|
||||
return {
|
||||
"total": 0,
|
||||
"total_all": total_all,
|
||||
"episodic_memories": []
|
||||
}
|
||||
|
||||
# 5. 对每个节点读取标题和类型,并应用类型筛选
|
||||
episodic_memories = []
|
||||
for record in result:
|
||||
summary_id = record["id"]
|
||||
created_at_str = record.get("created_at")
|
||||
memory_type = record.get("type", "其他")
|
||||
title = record.get("title") or "未命名" # 直接从查询结果获取标题
|
||||
|
||||
# 应用情景类型筛选
|
||||
if episodic_type != "all":
|
||||
# 检查类型是否匹配
|
||||
# 注意:Neo4j 中存储的 memory_type 现在应该是英文格式(如 "conversation", "project_work")
|
||||
# 但为了兼容旧数据,我们也支持中文格式的匹配
|
||||
type_mapping = {
|
||||
"conversation": "对话",
|
||||
"project_work": "项目/工作",
|
||||
"learning": "学习",
|
||||
"decision": "决策",
|
||||
"important_event": "重要事件"
|
||||
}
|
||||
|
||||
# 获取对应的中文类型(用于兼容旧数据)
|
||||
chinese_type = type_mapping.get(episodic_type)
|
||||
|
||||
# 检查类型是否匹配(支持新的英文格式和旧的中文格式)
|
||||
if memory_type != episodic_type and memory_type != chinese_type:
|
||||
continue
|
||||
|
||||
# 转换时间戳
|
||||
created_at_timestamp = None
|
||||
if created_at_str:
|
||||
try:
|
||||
from datetime import datetime
|
||||
dt_object = datetime.fromisoformat(created_at_str.replace("Z", "+00:00"))
|
||||
created_at_timestamp = int(dt_object.timestamp() * 1000)
|
||||
except (ValueError, TypeError, AttributeError) as e:
|
||||
logger.warning(f"无法解析时间戳: {created_at_str}, error={str(e)}")
|
||||
|
||||
episodic_memories.append({
|
||||
"id": summary_id,
|
||||
"title": title,
|
||||
"type": memory_type,
|
||||
"created_at": created_at_timestamp
|
||||
})
|
||||
|
||||
logger.info(
|
||||
f"成功获取 end_user_id={end_user_id} 的情景记忆总览,"
|
||||
f"筛选后 {len(episodic_memories)} 条,总共 {total_all} 条"
|
||||
)
|
||||
|
||||
return {
|
||||
"total": len(episodic_memories),
|
||||
"total_all": total_all,
|
||||
"episodic_memories": episodic_memories
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取情景记忆总览时出错: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _calculate_time_filter(self, time_range: str) -> Optional[str]:
|
||||
"""
|
||||
根据时间范围计算过滤的起始时间
|
||||
|
||||
Args:
|
||||
time_range: 时间范围 (all/today/this_week/this_month)
|
||||
|
||||
Returns:
|
||||
ISO格式的时间字符串,如果是"all"则返回None
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
|
||||
if time_range == "all":
|
||||
return None
|
||||
|
||||
# 获取当前时间(UTC)
|
||||
now = datetime.now(pytz.UTC)
|
||||
|
||||
if time_range == "today":
|
||||
# 今天的开始时间(00:00:00)
|
||||
start_time = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
elif time_range == "this_week":
|
||||
# 本周的开始时间(周一00:00:00)
|
||||
days_since_monday = now.weekday()
|
||||
start_time = (now - timedelta(days=days_since_monday)).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
elif time_range == "this_month":
|
||||
# 本月的开始时间(1号00:00:00)
|
||||
start_time = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
else:
|
||||
return None
|
||||
|
||||
# 返回ISO格式字符串
|
||||
return start_time.isoformat()
|
||||
|
||||
async def get_episodic_memory_details(
|
||||
self,
|
||||
db: Session,
|
||||
end_user_id: str,
|
||||
summary_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取单个情景记忆的详细信息
|
||||
|
||||
"""
|
||||
try:
|
||||
logger.info(f"开始查询 end_user_id={end_user_id}, summary_id={summary_id} 的情景记忆详情")
|
||||
|
||||
# 1. 查询指定的MemorySummary节点
|
||||
query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE elementId(s) = $summary_id AND s.group_id = $group_id
|
||||
RETURN elementId(s) AS id, s.created_at AS created_at
|
||||
"""
|
||||
|
||||
result = await self.neo4j_connector.execute_query(
|
||||
query,
|
||||
summary_id=summary_id,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
# 2. 如果节点不存在,返回错误
|
||||
if not result or len(result) == 0:
|
||||
logger.warning(f"未找到 summary_id={summary_id} 的情景记忆")
|
||||
raise ValueError(f"情景记忆不存在: summary_id={summary_id}")
|
||||
|
||||
# 3. 获取基本信息
|
||||
record = result[0]
|
||||
created_at_str = record.get("created_at")
|
||||
|
||||
# 转换时间戳
|
||||
created_at_timestamp = None
|
||||
if created_at_str:
|
||||
try:
|
||||
from datetime import datetime
|
||||
dt_object = datetime.fromisoformat(created_at_str.replace("Z", "+00:00"))
|
||||
created_at_timestamp = int(dt_object.timestamp() * 1000)
|
||||
except (ValueError, TypeError, AttributeError) as e:
|
||||
logger.warning(f"无法解析时间戳: {created_at_str}, error={str(e)}")
|
||||
|
||||
# 4. 调用_get_title_and_type读取标题和类型
|
||||
title, episodic_type = await self._get_title_and_type(
|
||||
summary_id=summary_id,
|
||||
end_user_id=end_user_id
|
||||
)
|
||||
|
||||
# 5. 调用_extract_involved_objects提取涉及对象
|
||||
involved_objects = await self._extract_involved_objects(
|
||||
summary_id=summary_id,
|
||||
end_user_id=end_user_id
|
||||
)
|
||||
|
||||
# 6. 调用_extract_content_records提取内容记录
|
||||
content_records = await self._extract_content_records(
|
||||
summary_id=summary_id,
|
||||
end_user_id=end_user_id
|
||||
)
|
||||
|
||||
# 7. 调用_extract_episodic_emotion提取情绪
|
||||
emotion = await self._extract_episodic_emotion(
|
||||
summary_id=summary_id,
|
||||
end_user_id=end_user_id
|
||||
)
|
||||
|
||||
# 8. 返回完整的详情信息
|
||||
details = {
|
||||
"id": summary_id,
|
||||
"created_at": created_at_timestamp,
|
||||
"involved_objects": involved_objects,
|
||||
"episodic_type": episodic_type,
|
||||
"content_records": content_records,
|
||||
"emotion": emotion
|
||||
}
|
||||
|
||||
logger.info(f"成功获取 summary_id={summary_id} 的情景记忆详情")
|
||||
|
||||
return details
|
||||
|
||||
except ValueError:
|
||||
# 重新抛出ValueError,让Controller层处理
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取情景记忆详情时出错: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
async def get_explicit_memory_overview(
|
||||
self,
|
||||
db: Session,
|
||||
end_user_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取显性记忆总览信息
|
||||
|
||||
返回两部分:
|
||||
1. 情景记忆(episodic_memories)- 来自MemorySummary节点
|
||||
2. 语义记忆(semantic_memories)- 来自ExtractedEntity节点(is_explicit_memory=true)
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
end_user_id: 终端用户ID
|
||||
|
||||
Returns:
|
||||
{
|
||||
"total": int,
|
||||
"episodic_memories": [
|
||||
{
|
||||
"id": str,
|
||||
"title": str,
|
||||
"content": str,
|
||||
"created_at": int,
|
||||
"emotion": Dict
|
||||
}
|
||||
],
|
||||
"semantic_memories": [
|
||||
{
|
||||
"id": str,
|
||||
"name": str,
|
||||
"entity_type": str,
|
||||
"core_definition": str,
|
||||
"detailed_notes": str,
|
||||
"created_at": int
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
logger.info(f"开始查询 end_user_id={end_user_id} 的显性记忆总览(情景记忆+语义记忆)")
|
||||
|
||||
# ========== 1. 查询情景记忆(MemorySummary节点) ==========
|
||||
episodic_query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE s.group_id = $group_id
|
||||
RETURN elementId(s) AS id,
|
||||
s.name AS title,
|
||||
s.content AS content,
|
||||
s.created_at AS created_at
|
||||
ORDER BY s.created_at DESC
|
||||
"""
|
||||
|
||||
episodic_result = await self.neo4j_connector.execute_query(
|
||||
episodic_query,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
# 处理情景记忆数据
|
||||
episodic_memories = []
|
||||
if episodic_result:
|
||||
for record in episodic_result:
|
||||
summary_id = record["id"]
|
||||
title = record.get("title") or "未命名"
|
||||
content = record.get("content") or ""
|
||||
created_at_str = record.get("created_at")
|
||||
|
||||
# 转换时间戳
|
||||
created_at_timestamp = None
|
||||
if created_at_str:
|
||||
try:
|
||||
from datetime import datetime
|
||||
dt_object = datetime.fromisoformat(created_at_str.replace("Z", "+00:00"))
|
||||
created_at_timestamp = int(dt_object.timestamp() * 1000)
|
||||
except (ValueError, TypeError, AttributeError) as e:
|
||||
logger.warning(f"无法解析时间戳: {created_at_str}, error={str(e)}")
|
||||
|
||||
# 注意:总览接口不返回 emotion 字段
|
||||
episodic_memories.append({
|
||||
"id": summary_id,
|
||||
"title": title,
|
||||
"content": content,
|
||||
"created_at": created_at_timestamp
|
||||
})
|
||||
|
||||
# ========== 2. 查询语义记忆(ExtractedEntity节点) ==========
|
||||
semantic_query = """
|
||||
MATCH (e:ExtractedEntity)
|
||||
WHERE e.group_id = $group_id
|
||||
AND e.is_explicit_memory = true
|
||||
RETURN elementId(e) AS id,
|
||||
e.name AS name,
|
||||
e.entity_type AS entity_type,
|
||||
e.description AS core_definition,
|
||||
e.example AS detailed_notes,
|
||||
e.created_at AS created_at
|
||||
ORDER BY e.created_at DESC
|
||||
"""
|
||||
|
||||
semantic_result = await self.neo4j_connector.execute_query(
|
||||
semantic_query,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
# 处理语义记忆数据
|
||||
semantic_memories = []
|
||||
if semantic_result:
|
||||
for record in semantic_result:
|
||||
entity_id = record["id"]
|
||||
name = record.get("name") or "未命名"
|
||||
entity_type = record.get("entity_type") or "未分类"
|
||||
core_definition = record.get("core_definition") or ""
|
||||
created_at_str = record.get("created_at")
|
||||
|
||||
# 转换时间戳
|
||||
created_at_timestamp = None
|
||||
if created_at_str:
|
||||
try:
|
||||
from datetime import datetime
|
||||
dt_object = datetime.fromisoformat(created_at_str.replace("Z", "+00:00"))
|
||||
created_at_timestamp = int(dt_object.timestamp() * 1000)
|
||||
except (ValueError, TypeError, AttributeError) as e:
|
||||
logger.warning(f"无法解析时间戳: {created_at_str}, error={str(e)}")
|
||||
|
||||
# 注意:总览接口不返回 detailed_notes 字段
|
||||
semantic_memories.append({
|
||||
"id": entity_id,
|
||||
"name": name,
|
||||
"entity_type": entity_type,
|
||||
"core_definition": core_definition,
|
||||
"created_at": created_at_timestamp
|
||||
})
|
||||
|
||||
# ========== 3. 返回结果 ==========
|
||||
total_count = len(episodic_memories) + len(semantic_memories)
|
||||
|
||||
logger.info(
|
||||
f"成功获取 end_user_id={end_user_id} 的显性记忆总览,"
|
||||
f"情景记忆={len(episodic_memories)} 条,语义记忆={len(semantic_memories)} 条,"
|
||||
f"总计 {total_count} 条"
|
||||
)
|
||||
|
||||
return {
|
||||
"total": total_count,
|
||||
"episodic_memories": episodic_memories,
|
||||
"semantic_memories": semantic_memories
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取显性记忆总览时出错: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
async def get_explicit_memory_details(
|
||||
self,
|
||||
db: Session,
|
||||
end_user_id: str,
|
||||
memory_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取显性记忆详情
|
||||
|
||||
根据 memory_id 查询情景记忆或语义记忆的详细信息。
|
||||
先尝试查询情景记忆,如果找不到再查询语义记忆。
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
end_user_id: 终端用户ID
|
||||
memory_id: 记忆ID(可以是情景记忆或语义记忆的ID)
|
||||
|
||||
Returns:
|
||||
情景记忆返回:
|
||||
{
|
||||
"memory_type": "episodic",
|
||||
"title": str,
|
||||
"content": str,
|
||||
"emotion": Dict,
|
||||
"created_at": int
|
||||
}
|
||||
|
||||
语义记忆返回:
|
||||
{
|
||||
"memory_type": "semantic",
|
||||
"name": str,
|
||||
"core_definition": str,
|
||||
"detailed_notes": str,
|
||||
"created_at": int
|
||||
}
|
||||
|
||||
Raises:
|
||||
ValueError: 当记忆不存在时
|
||||
"""
|
||||
try:
|
||||
logger.info(f"开始查询显性记忆详情: end_user_id={end_user_id}, memory_id={memory_id}")
|
||||
|
||||
# ========== 1. 先尝试查询情景记忆 ==========
|
||||
episodic_query = """
|
||||
MATCH (s:MemorySummary)
|
||||
WHERE elementId(s) = $memory_id AND s.group_id = $group_id
|
||||
RETURN s.name AS title,
|
||||
s.content AS content,
|
||||
s.created_at AS created_at
|
||||
"""
|
||||
|
||||
episodic_result = await self.neo4j_connector.execute_query(
|
||||
episodic_query,
|
||||
memory_id=memory_id,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
if episodic_result and len(episodic_result) > 0:
|
||||
record = episodic_result[0]
|
||||
title = record.get("title") or "未命名"
|
||||
content = record.get("content") or ""
|
||||
created_at_str = record.get("created_at")
|
||||
|
||||
# 转换时间戳
|
||||
created_at_timestamp = None
|
||||
if created_at_str:
|
||||
try:
|
||||
from datetime import datetime
|
||||
dt_object = datetime.fromisoformat(created_at_str.replace("Z", "+00:00"))
|
||||
created_at_timestamp = int(dt_object.timestamp() * 1000)
|
||||
except (ValueError, TypeError, AttributeError) as e:
|
||||
logger.warning(f"无法解析时间戳: {created_at_str}, error={str(e)}")
|
||||
|
||||
# 获取情绪信息
|
||||
emotion = await self._extract_episodic_emotion(
|
||||
summary_id=memory_id,
|
||||
end_user_id=end_user_id
|
||||
)
|
||||
|
||||
logger.info(f"成功获取情景记忆详情: memory_id={memory_id}")
|
||||
return {
|
||||
"memory_type": "episodic",
|
||||
"title": title,
|
||||
"content": content,
|
||||
"emotion": emotion,
|
||||
"created_at": created_at_timestamp
|
||||
}
|
||||
|
||||
# ========== 2. 如果不是情景记忆,尝试查询语义记忆 ==========
|
||||
semantic_query = """
|
||||
MATCH (e:ExtractedEntity)
|
||||
WHERE elementId(e) = $memory_id
|
||||
AND e.group_id = $group_id
|
||||
AND e.is_explicit_memory = true
|
||||
RETURN e.name AS name,
|
||||
e.description AS core_definition,
|
||||
e.example AS detailed_notes,
|
||||
e.created_at AS created_at
|
||||
"""
|
||||
|
||||
semantic_result = await self.neo4j_connector.execute_query(
|
||||
semantic_query,
|
||||
memory_id=memory_id,
|
||||
group_id=end_user_id
|
||||
)
|
||||
|
||||
if semantic_result and len(semantic_result) > 0:
|
||||
record = semantic_result[0]
|
||||
name = record.get("name") or "未命名"
|
||||
core_definition = record.get("core_definition") or ""
|
||||
detailed_notes = record.get("detailed_notes") or ""
|
||||
created_at_str = record.get("created_at")
|
||||
|
||||
# 转换时间戳
|
||||
created_at_timestamp = None
|
||||
if created_at_str:
|
||||
try:
|
||||
from datetime import datetime
|
||||
dt_object = datetime.fromisoformat(created_at_str.replace("Z", "+00:00"))
|
||||
created_at_timestamp = int(dt_object.timestamp() * 1000)
|
||||
except (ValueError, TypeError, AttributeError) as e:
|
||||
logger.warning(f"无法解析时间戳: {created_at_str}, error={str(e)}")
|
||||
|
||||
logger.info(f"成功获取语义记忆详情: memory_id={memory_id}")
|
||||
return {
|
||||
"memory_type": "semantic",
|
||||
"name": name,
|
||||
"core_definition": core_definition,
|
||||
"detailed_notes": detailed_notes,
|
||||
"created_at": created_at_timestamp
|
||||
}
|
||||
|
||||
# ========== 3. 两种记忆都找不到 ==========
|
||||
logger.warning(f"记忆不存在: memory_id={memory_id}, end_user_id={end_user_id}")
|
||||
raise ValueError(f"记忆不存在: memory_id={memory_id}")
|
||||
|
||||
except ValueError:
|
||||
# 重新抛出 ValueError(记忆不存在)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取显性记忆详情时出错: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
# 独立的分析函数
|
||||
@@ -2055,17 +1196,18 @@ async def analytics_memory_types(
|
||||
end_user_id: Optional[str] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
统计8种记忆类型的数量和百分比
|
||||
统计9种记忆类型的数量和百分比
|
||||
|
||||
计算规则:
|
||||
1. 感知记忆 (PERCEPTUAL_MEMORY) = statement + entity
|
||||
2. 工作记忆 (WORKING_MEMORY) = chunk + entity
|
||||
3. 短期记忆 (SHORT_TERM_MEMORY) = chunk
|
||||
4. 长期记忆 (LONG_TERM_MEMORY) = entity
|
||||
5. 显性记忆 (EXPLICIT_MEMORY) = 1/2 * entity
|
||||
5. 显性记忆 (EXPLICIT_MEMORY) = 情景记忆 + 语义记忆(通过 MemoryBaseService.get_explicit_memory_count 获取)
|
||||
6. 隐性记忆 (IMPLICIT_MEMORY) = 1/3 * entity
|
||||
7. 情绪记忆 (EMOTIONAL_MEMORY) = statement
|
||||
8. 情景记忆 (EPISODIC_MEMORY) = memory_summary
|
||||
7. 情绪记忆 (EMOTIONAL_MEMORY) = 情绪标签统计总数(通过 MemoryBaseService.get_emotional_memory_count 获取)
|
||||
8. 情景记忆 (EPISODIC_MEMORY) = memory_summary(通过 MemoryBaseService.get_episodic_memory_count 获取)
|
||||
9. 遗忘记忆 (FORGET_MEMORY) = 激活值低于阈值的节点数(通过 MemoryBaseService.get_forget_memory_count 获取)
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
@@ -2090,13 +1232,16 @@ async def analytics_memory_types(
|
||||
- IMPLICIT_MEMORY: 隐性记忆
|
||||
- EMOTIONAL_MEMORY: 情绪记忆
|
||||
- EPISODIC_MEMORY: 情景记忆
|
||||
- FORGET_MEMORY: 遗忘记忆
|
||||
"""
|
||||
# 定义需要查询的节点类型
|
||||
# 初始化基础服务
|
||||
base_service = MemoryBaseService()
|
||||
|
||||
# 定义需要查询的基础节点类型
|
||||
node_types = {
|
||||
"Statement": "Statement",
|
||||
"Entity": "ExtractedEntity",
|
||||
"Chunk": "Chunk",
|
||||
"MemorySummary": "MemorySummary"
|
||||
"Chunk": "Chunk"
|
||||
}
|
||||
|
||||
# 存储每种节点类型的计数
|
||||
@@ -2126,18 +1271,45 @@ async def analytics_memory_types(
|
||||
statement_count = node_counts.get("Statement", 0)
|
||||
entity_count = node_counts.get("Entity", 0)
|
||||
chunk_count = node_counts.get("Chunk", 0)
|
||||
memory_summary_count = node_counts.get("MemorySummary", 0)
|
||||
|
||||
# 按规则计算8种记忆类型的数量(使用英文枚举作为key)
|
||||
# 获取用户的遗忘阈值配置
|
||||
forgetting_threshold = 0.3 # 默认值
|
||||
if end_user_id:
|
||||
try:
|
||||
from app.services.memory_agent_service import get_end_user_connected_config
|
||||
from app.core.memory.storage_services.forgetting_engine.config_utils import load_actr_config_from_db
|
||||
|
||||
# 获取用户关联的 config_id
|
||||
connected_config = get_end_user_connected_config(end_user_id, db)
|
||||
config_id = connected_config.get('memory_config_id')
|
||||
|
||||
if config_id:
|
||||
# 从数据库加载配置
|
||||
config = load_actr_config_from_db(db, config_id)
|
||||
forgetting_threshold = config.get('forgetting_threshold', 0.3)
|
||||
logger.debug(f"使用用户配置的遗忘阈值: {forgetting_threshold} (end_user_id={end_user_id}, config_id={config_id})")
|
||||
else:
|
||||
logger.debug(f"用户未关联配置,使用默认遗忘阈值: {forgetting_threshold} (end_user_id={end_user_id})")
|
||||
except Exception as e:
|
||||
logger.warning(f"获取用户遗忘阈值配置失败,使用默认值 {forgetting_threshold}: {str(e)}")
|
||||
|
||||
# 使用 MemoryBaseService 的共享方法获取特殊记忆类型的数量
|
||||
episodic_count = await base_service.get_episodic_memory_count(end_user_id)
|
||||
explicit_count = await base_service.get_explicit_memory_count(end_user_id)
|
||||
emotion_count = await base_service.get_emotional_memory_count(end_user_id, statement_count)
|
||||
forget_count = await base_service.get_forget_memory_count(end_user_id, forgetting_threshold)
|
||||
|
||||
# 按规则计算9种记忆类型的数量(使用英文枚举作为key)
|
||||
memory_counts = {
|
||||
"PERCEPTUAL_MEMORY": statement_count + entity_count, # 感知记忆
|
||||
"WORKING_MEMORY": chunk_count + entity_count, # 工作记忆
|
||||
"SHORT_TERM_MEMORY": chunk_count, # 短期记忆
|
||||
"LONG_TERM_MEMORY": entity_count, # 长期记忆
|
||||
"EXPLICIT_MEMORY": entity_count // 2, # 显性记忆 (1/2 entity)
|
||||
"EXPLICIT_MEMORY": explicit_count, # 显性记忆(情景记忆 + 语义记忆)
|
||||
"IMPLICIT_MEMORY": entity_count // 3, # 隐性记忆 (1/3 entity)
|
||||
"EMOTIONAL_MEMORY": statement_count, # 情绪记忆
|
||||
"EPISODIC_MEMORY": memory_summary_count # 情景记忆
|
||||
"EMOTIONAL_MEMORY": emotion_count, # 情绪记忆(使用情绪标签统计)
|
||||
"EPISODIC_MEMORY": episodic_count, # 情景记忆
|
||||
"FORGET_MEMORY": forget_count # 遗忘记忆(激活值低于阈值)
|
||||
}
|
||||
|
||||
# 计算总数
|
||||
|
||||
33
api/app/version_info.json
Normal file
33
api/app/version_info.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"v0.2.0": {
|
||||
"codeName": "启知",
|
||||
"releaseDate": "2026-1-16",
|
||||
"upgradePosition": "本次为架构升级,核心目标是把“被动存储”升级为“主动认知”,让系统具备情绪感知、情景理解与类人记忆机制,为后续多智能体协作与专业场景落地奠定底座。",
|
||||
"coreUpgrades": [
|
||||
"记忆详情:拟人记忆——情绪引擎、情景记忆、短期记忆、工作记忆、感知记忆、显性记忆、隐性记忆,并配套类脑遗忘机制,实现从感知→情绪→情景→长期沉淀的完整人类记忆闭环",
|
||||
"可视化工作流:拖拽式节点编排(LLM、知识库、逻辑、工具),业务落地周期由天缩至小时。",
|
||||
"多模态知识处理:PDF、PPT、MP3、MP4 一键解析,时间感知检索准确率 94.3%,问答对数据即插即用。",
|
||||
"Agent集群内置“记忆-知识-工具-审核”四类角色模板,用户一键生成;主控Agent把复杂任务拆为子任务并行分发,再靠情景记忆统一消解冲突、校验一致性,输出完整报告。"
|
||||
]
|
||||
},
|
||||
"v0.1.0": {
|
||||
"codeName": "初心",
|
||||
"releaseDate": "2025-12-01",
|
||||
"upgradePosition": "这是一款专注于管理和利用AI记忆的工具,支持RAG和知识图谱两种主流存储方式,旨在为AI应用提供持久化、结构化的“记忆”能力。",
|
||||
"coreUpgrades": [
|
||||
"记忆空间:用户可以创建独立的空间来隔离不同记忆,并灵活选择存储方式。",
|
||||
"记忆配置:简化了配置流程,内置自动提取关键信息的“记忆萃取”和管理生命周期的\"遗忘\"引擎。",
|
||||
"知识检索:提供语义、分词和混合三种检索模式,并支持多种参数微调和结果重排序,以提升召回效果。",
|
||||
"全局管理:支持统一设置默认检索参数,并可一键应用到所有知识库。",
|
||||
"测试与调试:内置\"召回测试\"功能,方便用户实时验证检索效果并调整参数,支持通过分享码与他人协作。",
|
||||
"记忆洞察:可查看详细的对话记录、用户画像和分析报告,帮助理解AI的\"记忆\"内容。",
|
||||
"集成与管理:提供API Key用于系统集成,并包含基本的用户管理功能。",
|
||||
"界面与体验:采用现代化的卡片式布局和渐变色设计,注重交互的流畅性和视觉美感。",
|
||||
"起步与使用:文档中提供了清晰的基础使用流程,引导用户从创建空间、配置记忆到测试检索快速上手。",
|
||||
"版本说明与限制: 记忆熊 v0.1.0 版本\"初心\"囊括智能记忆管理的核心思路和基础能力,为后续开发奠定了基础。",
|
||||
"文档资源:用户手册、API文档、FAQ",
|
||||
"问题反馈:GitHub Issues、邮件支持",
|
||||
"致谢:感谢所有参与测试和提供反馈的用户!"
|
||||
]
|
||||
}
|
||||
}
|
||||
30
api/migrations/versions/9ab9b6393f32_20261511.py
Normal file
30
api/migrations/versions/9ab9b6393f32_20261511.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""20261511
|
||||
|
||||
Revision ID: 9ab9b6393f32
|
||||
Revises: 793c31683aa5
|
||||
Create Date: 2026-01-13 15:14:54.708405
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '9ab9b6393f32'
|
||||
down_revision: Union[str, None] = '793c31683aa5'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(op.f('tool_executions_user_id_fkey'), 'tool_executions', type_='foreignkey')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_foreign_key(op.f('tool_executions_user_id_fkey'), 'tool_executions', 'users', ['user_id'], ['id'])
|
||||
# ### end Alembic commands ###
|
||||
@@ -25,7 +25,12 @@ export interface DataResponse {
|
||||
}
|
||||
export interface versionResponse{
|
||||
version: string;
|
||||
introduction: string;
|
||||
introduction: {
|
||||
releaseDate: string;
|
||||
upgradePosition: string;
|
||||
coreUpgrades: string[];
|
||||
codeName: string;
|
||||
};
|
||||
}
|
||||
// 首页数据统计
|
||||
export const getDashboardData = `/home-page/workspaces`
|
||||
|
||||
@@ -117,26 +117,26 @@ export const getRagContent = (end_user_id: string) => {
|
||||
}
|
||||
// 情感分布分析
|
||||
export const getWordCloud = (group_id: string) => {
|
||||
return request.post(`/memory/emotion/wordcloud`, { group_id, limit: 20 })
|
||||
return request.post(`/memory/emotion-memory/wordcloud`, { group_id, limit: 20 })
|
||||
}
|
||||
// 高频情绪关键词
|
||||
export const getEmotionTags = (group_id: string) => {
|
||||
return request.post(`/memory/emotion/tags`, { group_id, limit: 20 })
|
||||
return request.post(`/memory/emotion-memory/tags`, { group_id, limit: 20 })
|
||||
}
|
||||
// 情绪健康指数
|
||||
export const getEmotionHealth = (group_id: string) => {
|
||||
return request.post(`/memory/emotion/health`, { group_id, limit: 20 })
|
||||
return request.post(`/memory/emotion-memory/health`, { group_id, limit: 20 })
|
||||
}
|
||||
// 个性化建议
|
||||
export const getEmotionSuggestions = (group_id: string) => {
|
||||
return request.post(`/memory/emotion/suggestions`, { group_id, limit: 20 })
|
||||
return request.post(`/memory/emotion-memory/suggestions`, { group_id, limit: 20 })
|
||||
}
|
||||
export const analyticsRefresh = (end_user_id: string) => {
|
||||
return request.post('/memory-storage/analytics/generate_cache', { end_user_id })
|
||||
}
|
||||
// 遗忘
|
||||
export const getForgetStats = (group_id: string) => {
|
||||
return request.get(`/memory/forget/stats`, { group_id })
|
||||
return request.get(`/memory/forget-memory/stats`, { group_id })
|
||||
}
|
||||
// 隐性记忆-偏好
|
||||
export const getImplicitPreferences = (end_user_id: string) => {
|
||||
@@ -176,10 +176,10 @@ export const getPerceptualTimeline = (end_user: string) => {
|
||||
}
|
||||
// 情景记忆-总览
|
||||
export const getEpisodicOverview = (data: { end_user_id: string; time_range: string; episodic_type: string; } ) => {
|
||||
return request.post(`/memory-storage/classifications/episodic-memory`, data)
|
||||
return request.post(`/memory/episodic-memory/overview`, data)
|
||||
}
|
||||
export const getEpisodicDetail = (data: { end_user_id: string; summary_id: string; } ) => {
|
||||
return request.post(`/memory-storage/classifications/episodic-memory-details`, data)
|
||||
return request.post(`/memory/episodic-memory/details`, data)
|
||||
}
|
||||
// 关系演化
|
||||
export const getRelationshipEvolution = (data: { id: string; label: string; } ) => {
|
||||
@@ -190,10 +190,10 @@ export const getTimelineMemories = (data: { id: string; label: string; }) => {
|
||||
return request.get(`/memory-storage/memory_space/timeline_memories`, data)
|
||||
}
|
||||
export const getExplicitMemory = (end_user_id: string) => {
|
||||
return request.post(`/memory-storage/classifications/explicit-memory`, { end_user_id })
|
||||
return request.post(`/memory/explicit-memory/overview`, { end_user_id })
|
||||
}
|
||||
export const getExplicitMemoryDetails = (data: { end_user_id: string, memory_id: string; }) => {
|
||||
return request.post(`/memory-storage/classifications/explicit-memory-details`, data)
|
||||
return request.post(`/memory/explicit-memory/details`, data)
|
||||
}
|
||||
export const getConversations = (end_user: string) => {
|
||||
return request.get(`/memory/work/${end_user}/conversations`)
|
||||
@@ -204,8 +204,9 @@ export const getConversationMessages = (end_user: string, conversation_id: strin
|
||||
export const getConversationDetail = (end_user: string, conversation_id: string) => {
|
||||
return request.get(`/memory/work/${end_user}/detail`, { conversation_id })
|
||||
}
|
||||
|
||||
|
||||
export const forgetTrigger = (data: { max_merge_batch_size: number; min_days_since_access: number; end_user_id: string;}) => {
|
||||
return request.post(`/memory/forget-memory/trigger`, data)
|
||||
}
|
||||
/*************** end 用户记忆 相关接口 ******************************/
|
||||
|
||||
/****************** 记忆管理 相关接口 *******************************/
|
||||
@@ -228,11 +229,11 @@ export const deleteMemoryConfig = (config_id: number) => {
|
||||
}
|
||||
// 遗忘引擎-获取配置
|
||||
export const getMemoryForgetConfig = (config_id: number | string) => {
|
||||
return request.get('/memory/forget/read_config', { config_id })
|
||||
return request.get('/memory/forget-memory/read_config', { config_id })
|
||||
}
|
||||
// 遗忘引擎-更新配置
|
||||
export const updateMemoryForgetConfig = (values: ForgetConfigForm) => {
|
||||
return request.post('/memory/forget/update_config', values)
|
||||
return request.post('/memory/forget-memory/update_config', values)
|
||||
}
|
||||
// 记忆萃取引擎-获取配置
|
||||
export const getMemoryExtractionConfig = (config_id: number | string) => {
|
||||
|
||||
17
web/src/assets/images/logout_hover.svg
Normal file
17
web/src/assets/images/logout_hover.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>退出</title>
|
||||
<g id="V1.0版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="应用管理-编排-默认状态" transform="translate(-1262, -24)" stroke="#155EEF">
|
||||
<g id="返回空间" transform="translate(1262, 24)">
|
||||
<g id="退出" transform="translate(8, 8) scale(-1, 1) translate(-8, -8)">
|
||||
<g id="编组-7" transform="translate(2.5, 2)">
|
||||
<path d="M6,12 L1,12 C0.44771525,12 0,11.5522847 0,11 L0,1 C0,0.44771525 0.44771525,1.11022302e-16 1,0 L6,0 L6,0" id="路径"></path>
|
||||
<line x1="11" y1="6" x2="3" y2="6" id="路径-6"></line>
|
||||
<polyline id="路径" points="8 3 11 6 8 9"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
17
web/src/assets/images/menu/spaceConfig.svg
Normal file
17
web/src/assets/images/menu/spaceConfig.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>模型 (1)</title>
|
||||
<g id="v0.2.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="红熊空间-记忆管理" transform="translate(-24, -409)" stroke="#5B6167">
|
||||
<g id="记忆对话备份-2" transform="translate(12, 401)">
|
||||
<g id="模型-(1)" transform="translate(12, 8)">
|
||||
<g id="编组-21" transform="translate(1.5, 1.5)">
|
||||
<path d="M7,0.288675135 L11.6291651,2.96132487 C11.9385662,3.13995766 12.1291651,3.47008468 12.1291651,3.82735027 L12.1291651,9.17264973 C12.1291651,9.52991532 11.9385662,9.86004234 11.6291651,10.0386751 L7,12.7113249 C6.69059892,12.8899577 6.30940108,12.8899577 6,12.7113249 L1.37083488,10.0386751 C1.0614338,9.86004234 0.870834875,9.52991532 0.870834875,9.17264973 L0.870834875,3.82735027 C0.870834875,3.47008468 1.0614338,3.13995766 1.37083488,2.96132487 L6,0.288675135 C6.30940108,0.11004234 6.69059892,0.11004234 7,0.288675135 Z" id="多边形"></path>
|
||||
<polyline id="路径-15" points="0.931223827 3.37218958 6.5 6.5 6.5 12.8581283"></polyline>
|
||||
<line x1="6.5" y1="6.49748419" x2="12.0714286" y2="3.37218958" id="路径-16"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
17
web/src/assets/images/menu/spaceConfig_active.svg
Normal file
17
web/src/assets/images/menu/spaceConfig_active.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>模型 (1)</title>
|
||||
<g id="v0.2.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="红熊空间-记忆管理" transform="translate(-24, -409)" stroke="#212332">
|
||||
<g id="记忆对话备份-2" transform="translate(12, 401)">
|
||||
<g id="模型-(1)" transform="translate(12, 8)">
|
||||
<g id="编组-21" transform="translate(1.5, 1.5)">
|
||||
<path d="M7,0.288675135 L11.6291651,2.96132487 C11.9385662,3.13995766 12.1291651,3.47008468 12.1291651,3.82735027 L12.1291651,9.17264973 C12.1291651,9.52991532 11.9385662,9.86004234 11.6291651,10.0386751 L7,12.7113249 C6.69059892,12.8899577 6.30940108,12.8899577 6,12.7113249 L1.37083488,10.0386751 C1.0614338,9.86004234 0.870834875,9.52991532 0.870834875,9.17264973 L0.870834875,3.82735027 C0.870834875,3.47008468 1.0614338,3.13995766 1.37083488,2.96132487 L6,0.288675135 C6.30940108,0.11004234 6.69059892,0.11004234 7,0.288675135 Z" id="多边形"></path>
|
||||
<polyline id="路径-15" points="0.931223827 3.37218958 6.5 6.5 6.5 12.8581283"></polyline>
|
||||
<line x1="6.5" y1="6.49748419" x2="12.0714286" y2="3.37218958" id="路径-16"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
19
web/src/assets/images/userMemory/goto.svg
Normal file
19
web/src/assets/images/userMemory/goto.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 13备份</title>
|
||||
<g id="V1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="红熊空间-记忆管理" transform="translate(-947, -144)">
|
||||
<g id="1备份-2" transform="translate(651, 128)">
|
||||
<g id="编组-13备份" transform="translate(296, 16)">
|
||||
<rect id="矩形" stroke="#DFE4ED" x="0.5" y="0.5" width="27" height="27" rx="6"></rect>
|
||||
<g id="进入@2x" transform="translate(5.8333, 5.8333)">
|
||||
<g id="编组-11" transform="translate(2.0417, 2.5521)">
|
||||
<path d="M5.42385066,3.34516089 L8.15899029,5.47250014 C8.23746067,5.5335329 8.25159666,5.64662254 8.1905639,5.72509292 C8.1813906,5.73688711 8.17078448,5.74749323 8.15899029,5.75666652 L5.42385066,7.88400578 C5.34538028,7.94503854 5.23229064,7.93090256 5.17125788,7.85243218 C5.14668314,7.82083621 5.13334107,7.78195037 5.13334107,7.74192259 L5.13334107,6.2384308 L5.13334107,6.2384308 L0,6.2384308 L0,4.99073587 L5.13334107,4.99073587 L5.13334107,3.48724407 C5.13334107,3.38783282 5.21392981,3.30724407 5.31334107,3.30724407 C5.35336884,3.30724407 5.39225469,3.32058615 5.42385066,3.34516089 Z" id="路径" fill="#5B6167" fill-rule="nonzero"></path>
|
||||
<path d="M1.60417096,2.83745334 L1.60417096,0.9 C1.60417096,0.402943725 2.00711469,0 2.50417096,-1.11022302e-16 L10.3291667,-1.11022302e-16 C10.8262229,-2.22044605e-16 11.2291667,0.402943725 11.2291667,0.9 L11.2291667,10.3291667 C11.2291667,10.8262229 10.8262229,11.2291667 10.3291667,11.2291667 L2.50417096,11.2291667 C2.00711469,11.2291667 1.60417096,10.8262229 1.60417096,10.3291667 L1.60417096,8.46506778 L1.60417096,8.46506778" id="路径" stroke="#5B6167" stroke-width="1.1" stroke-linejoin="round"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2025-12-10 16:46:17
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2025-12-11 13:40:18
|
||||
* @Last Modified time: 2026-01-12 20:41:27
|
||||
*/
|
||||
import { type FC, useRef, useEffect } from 'react'
|
||||
import clsx from 'clsx'
|
||||
@@ -55,7 +55,7 @@ const ChatContent: FC<ChatContentProps> = ({
|
||||
</div>
|
||||
}
|
||||
{/* 消息气泡框 */}
|
||||
<div className={clsx('rb:border rb:text-left rb:rounded-lg rb:mt-1.5 rb:leading-4.5 rb:p-[10px_12px_2px_12px] rb:inline-block rb:max-w-100', contentClassNames, {
|
||||
<div className={clsx('rb:border rb:text-left rb:rounded-lg rb:mt-1.5 rb:leading-4.5 rb:p-[10px_12px_2px_12px] rb:inline-block rb:max-w-100 rb:wrap-break-word', contentClassNames, {
|
||||
// 错误消息样式(内容为null且非助手消息)
|
||||
'rb:border-[rgba(255,93,52,0.30)] rb:bg-[rgba(255,93,52,0.08)] rb:text-[#FF5D34]': errorDesc && item.role === 'assistant' && item.content === null,
|
||||
// 助手消息样式
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useCallback, useRef, type FC, type Key } from 'react';
|
||||
import { useEffect, useState, type FC, type Key } from 'react';
|
||||
import { Select } from 'antd'
|
||||
import type { SelectProps, DefaultOptionType } from 'antd/es/select'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -26,7 +26,7 @@ interface CustomSelectProps extends Omit<SelectProps, 'filterOption'> {
|
||||
disabled?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
filterOption?: (inputValue: string, option: DefaultOptionType) => boolean;
|
||||
filterOption?: (inputValue: string, option?: DefaultOptionType) => boolean;
|
||||
}
|
||||
interface OptionType {
|
||||
[key: string]: Key | string | number;
|
||||
@@ -48,44 +48,27 @@ const CustomSelect: FC<CustomSelectProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [options, setOptions] = useState<OptionType[]>([]);
|
||||
// 创建防抖定时器引用
|
||||
const debounceRef = useRef<number>();
|
||||
|
||||
// 防抖搜索函数
|
||||
const handleSearch = useCallback((value?: string) => {
|
||||
// 清除之前的定时器
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
|
||||
// 设置新的定时器
|
||||
debounceRef.current = window.setTimeout(() => {
|
||||
request.get<ApiResponse<OptionType>>(url, {...params, [optionFilterProp]: value}).then((res) => {
|
||||
const data = res;
|
||||
setOptions(Array.isArray(data) ? data || [] : Array.isArray(data?.items) ? data.items || [] : []);
|
||||
});
|
||||
}, 300); // 300毫秒防抖延迟
|
||||
}, [url, params, optionFilterProp]);
|
||||
|
||||
// 默认模糊搜索函数
|
||||
const defaultFilterOption = (inputValue: string, option?: DefaultOptionType) => {
|
||||
if (!option || !inputValue) return true;
|
||||
const label = String(option.children || option.label || '');
|
||||
return label.toLowerCase().includes(inputValue.toLowerCase());
|
||||
};
|
||||
// 组件挂载时获取初始数据
|
||||
useEffect(() => {
|
||||
handleSearch();
|
||||
|
||||
// 组件卸载时清除定时器
|
||||
return () => {
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
};
|
||||
}, [url, handleSearch]);
|
||||
request.get<ApiResponse<OptionType>>(url, params).then((res) => {
|
||||
const data = res;
|
||||
setOptions(Array.isArray(data) ? data || [] : Array.isArray(data?.items) ? data.items || [] : []);
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<Select
|
||||
placeholder={placeholder ? placeholder : t('common.select')}
|
||||
onChange={onChange}
|
||||
defaultValue={hasAll ? null : undefined}
|
||||
showSearch={showSearch}
|
||||
onSearch={handleSearch}
|
||||
filterOption={filterOption || false} // 禁用本地过滤,使用服务器端过滤
|
||||
filterOption={filterOption || defaultFilterOption}
|
||||
{...props}
|
||||
>
|
||||
{hasAll && (<Select.Option>{allTitle || t('common.all')}</Select.Option>)}
|
||||
|
||||
@@ -40,6 +40,8 @@ import apiKeyIcon from '@/assets/images/menu/apiKey.png';
|
||||
import apiKeyActiveIcon from '@/assets/images/menu/apiKey_active.png';
|
||||
import pricingIcon from '@/assets/images/menu/pricing.svg'
|
||||
import pricingActiveIcon from '@/assets/images/menu/pricing_active.svg'
|
||||
import spaceConfigIcon from '@/assets/images/menu/spaceConfig.svg'
|
||||
import spaceConfigActiveIcon from '@/assets/images/menu/spaceConfig_active.svg'
|
||||
|
||||
// 图标路径映射表
|
||||
const iconPathMap: Record<string, string> = {
|
||||
@@ -68,7 +70,9 @@ const iconPathMap: Record<string, string> = {
|
||||
'apiKey': apiKeyIcon,
|
||||
'apiKeyActive': apiKeyActiveIcon,
|
||||
'pricing': pricingIcon,
|
||||
'pricingActive': pricingActiveIcon
|
||||
'pricingActive': pricingActiveIcon,
|
||||
'spaceConfig': spaceConfigIcon,
|
||||
'spaceConfigActive': spaceConfigActiveIcon,
|
||||
};
|
||||
|
||||
const { Sider } = Layout;
|
||||
|
||||
@@ -17,6 +17,11 @@ export const en = {
|
||||
spaceTitle:'Memory Bear Intelligent Space Management Platform',
|
||||
spaceSubTitle: 'Making it easier to implement intelligent models - a one-stop platform for model management, knowledge building, workflow orchestration, and spatial operations',
|
||||
},
|
||||
version:{
|
||||
releaseDate: 'Release Date',
|
||||
version: 'Version',
|
||||
name: 'Code Name'
|
||||
},
|
||||
quickActions:{
|
||||
title: 'Quick Actions',
|
||||
spaceManagement: 'Space Management',
|
||||
@@ -82,7 +87,7 @@ export const en = {
|
||||
modelManagement: 'Model Management',
|
||||
memoryStore: 'Memory Store',
|
||||
apiParameters: 'API Parameters',
|
||||
userMemory: 'User Memory',
|
||||
userMemory: 'Memory Store',
|
||||
memberManagement: 'Member Management',
|
||||
memorySummary: 'Memory Summary',
|
||||
memoryConversation: 'Memory Validation',
|
||||
@@ -105,6 +110,7 @@ export const en = {
|
||||
pricing: 'Pricing Management',
|
||||
orderPayment: 'Order Payment',
|
||||
orderHistory: 'Order History',
|
||||
spaceConfig: 'Space Configuration'
|
||||
},
|
||||
dashboard: {
|
||||
total_models: 'Total number of available models',
|
||||
@@ -1227,6 +1233,8 @@ export const en = {
|
||||
hire_date: 'Hire Date',
|
||||
memoryContent: 'Memory Content',
|
||||
created_at: 'Created At',
|
||||
updated_at: 'Updated At',
|
||||
fullScreen: 'Full Screen',
|
||||
|
||||
memoryWindow: "{{name}}'s Window of Memory",
|
||||
memory_insight: 'Overall Overview',
|
||||
@@ -1253,13 +1261,23 @@ export const en = {
|
||||
unix: 'items',
|
||||
completeMemory: 'Complete Memory',
|
||||
relationshipEvolution: 'Relationship Evolution',
|
||||
timelineMemories: 'Shared Memory Timeline',
|
||||
timelineMemories: 'Long-term Memory',
|
||||
emotionLine: 'Emotion Changes Over Time',
|
||||
interaction: 'Interaction Frequency & Relationship Stages',
|
||||
timelines_memory: 'All',
|
||||
MemorySummary: 'Long-term Accumulation',
|
||||
Statement: 'Emotional Memory',
|
||||
ExtractedEntity: 'Episodic Memory',
|
||||
positive: 'Positive Emotion',
|
||||
negative: 'Negative Emotion',
|
||||
neutral: 'Neutral Emotion',
|
||||
interactionCountData: 'Interaction Count',
|
||||
capacity: 'Capacity',
|
||||
type: 'Type',
|
||||
person: 'Personal',
|
||||
memoryNum: 'memories',
|
||||
memory_config_name: 'Memory Engine',
|
||||
searchPlaceholder: 'Search memory store name',
|
||||
},
|
||||
space: {
|
||||
createSpace: 'Create Space',
|
||||
@@ -1275,7 +1293,8 @@ export const en = {
|
||||
neo4jDesc: 'Based on knowledge graph, suitable for relational reasoning and path query',
|
||||
llmModel: 'LLM Model',
|
||||
embeddingModel: 'Embedding Model',
|
||||
rerankModel: 'Rerank Model'
|
||||
rerankModel: 'Rerank Model',
|
||||
configAlert: 'Space model configuration ensures that the space can correctly call the corresponding models to process business data during runtime.',
|
||||
},
|
||||
memoryExtractionEngine: {
|
||||
title: 'Memory Engine Module Configuration Center',
|
||||
@@ -1450,6 +1469,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
quickReply: 'Quick Reply',
|
||||
web_search: 'Online search',
|
||||
memory: 'Memory',
|
||||
memoryConversationAnalysisEmpty: 'There is currently no dialogue analysis content available',
|
||||
memoryConversationAnalysisEmptySubTitle: 'After entering your user ID, click on "Test Memory" to view the conversation memory',
|
||||
},
|
||||
login: {
|
||||
title: 'Red Bear Memory Science',
|
||||
@@ -1604,19 +1625,17 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
|
||||
JsonTool_desc: 'Data Format Conversion',
|
||||
JsonTool_features: 'JSON formatting, compression, validation and conversion functions',
|
||||
jsonFormat: 'JSON Formatting',
|
||||
jsonGzip: 'JSON Compression',
|
||||
jsonCheck: 'JSON Validation',
|
||||
jsonConversion: 'Format Conversion',
|
||||
jsonParse: 'JSON Parse',
|
||||
jsonInsert: 'JSON Insert',
|
||||
jsonReplace: 'JSON Validation',
|
||||
jsonDelete: 'JSON Delete',
|
||||
jsonEg: 'Example JSON',
|
||||
enterJson: 'Enter JSON',
|
||||
jsonPlaceholder: 'Enter JSON data, e.g.: {"name": "test", "value": 123}',
|
||||
clear: 'Clear',
|
||||
parse: 'Paste',
|
||||
format: 'Format',
|
||||
minify: 'Minify',
|
||||
validate: 'Validate',
|
||||
convert: 'Escape',
|
||||
paste: 'Paste',
|
||||
parse: 'Parse',
|
||||
json_path: 'JSON Path Parameters',
|
||||
outputResult: 'Output Result',
|
||||
validJosn: 'JSON format is correct, validation passed!',
|
||||
|
||||
@@ -1935,7 +1954,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
variableConfig: 'Variable Configuration',
|
||||
variableRequired: 'Required',
|
||||
addMessage: 'Add Message',
|
||||
answerDesc: 'Reply'
|
||||
answerDesc: 'Reply',
|
||||
addNode: 'Add Node',
|
||||
},
|
||||
emotionEngine: {
|
||||
emotionEngineConfig: 'Emotion Engine Configuration',
|
||||
@@ -2219,6 +2239,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
node_type: 'Node Type',
|
||||
last_access_time: 'Last Activation Time',
|
||||
activation_value: 'Current Activation Value',
|
||||
refreshSuccess: 'Forgetting Execution Successful',
|
||||
},
|
||||
episodicDetail: {
|
||||
title: 'Record every important scene you have truly experienced',
|
||||
|
||||
@@ -17,6 +17,11 @@ export const zh = {
|
||||
spaceTitle:'记忆熊智能空间管理平台',
|
||||
spaceSubTitle: '使智能模型的实施变得更加容易——一个集模型管理、知识构建、工作流程编排以及空间操作于一体的综合性平台',
|
||||
},
|
||||
version:{
|
||||
releaseDate: '发布日',
|
||||
version: '版本',
|
||||
name: '代号'
|
||||
},
|
||||
quickActions:{
|
||||
title: '快速操作',
|
||||
spaceManagement: '空间管理',
|
||||
@@ -82,7 +87,7 @@ export const zh = {
|
||||
modelManagement: '模型管理',
|
||||
memoryStore: '记忆存储',
|
||||
apiParameters: 'API参数',
|
||||
userMemory: '用户记忆',
|
||||
userMemory: '记忆库',
|
||||
memberManagement: '成员管理',
|
||||
memorySummary: '记忆摘要',
|
||||
memoryConversation: '记忆验证',
|
||||
@@ -97,7 +102,7 @@ export const zh = {
|
||||
knowledgeShare: '详情',
|
||||
knowledgeCreateDataset: '新建数据集',
|
||||
knowledgeDocumentDetails: '详情',
|
||||
userMemoryDetail: '用户记忆详情',
|
||||
userMemoryDetail: '记忆库详情',
|
||||
toolManagement: '工具管理',
|
||||
emotionEngine: '情感引擎',
|
||||
statementDetail: '情绪记忆',
|
||||
@@ -105,6 +110,7 @@ export const zh = {
|
||||
pricing: '收费管理',
|
||||
orderPayment: '订单支付',
|
||||
orderHistory: '订单记录',
|
||||
spaceConfig: '空间配置'
|
||||
},
|
||||
knowledgeBase: {
|
||||
home: '首页',
|
||||
@@ -1308,7 +1314,7 @@ export const zh = {
|
||||
updated_at: '最后更新时间',
|
||||
fullScreen: '全屏',
|
||||
|
||||
memoryWindow: "{{name}}的记忆之窗",
|
||||
memoryWindow: "{{name}} 的记忆之窗",
|
||||
memory_insight: '总体概述',
|
||||
key_findings: '关键发现',
|
||||
behavior_pattern: '行为模式',
|
||||
@@ -1333,13 +1339,23 @@ export const zh = {
|
||||
unix: '个',
|
||||
completeMemory: '完整记忆',
|
||||
relationshipEvolution: '关系演化',
|
||||
timelineMemories: '共同记忆时间线',
|
||||
timelineMemories: '长期记忆',
|
||||
emotionLine: '情绪随时间变化',
|
||||
interaction: '互动频率 & 关系阶段',
|
||||
timelines_memory: '全部',
|
||||
MemorySummary: '长期沉淀',
|
||||
Statement: '情绪记忆',
|
||||
ExtractedEntity: '情景记忆',
|
||||
positive: '正向情绪',
|
||||
negative: '负向情绪',
|
||||
neutral: '中性情绪',
|
||||
interactionCountData: '互动次数',
|
||||
capacity: '容量',
|
||||
type: '类型',
|
||||
person: '个人',
|
||||
memoryNum: '条记忆',
|
||||
memory_config_name: '记忆引擎',
|
||||
searchPlaceholder: '搜索记忆库名称',
|
||||
},
|
||||
space: {
|
||||
createSpace: '创建空间',
|
||||
@@ -1355,7 +1371,8 @@ export const zh = {
|
||||
neo4jDesc: '基于知识图谱,适合关系推理和路径查询',
|
||||
llmModel: 'LLM 模型',
|
||||
embeddingModel: 'Embedding 模型',
|
||||
rerankModel: 'Rerank 模型'
|
||||
rerankModel: 'Rerank 模型',
|
||||
configAlert: '空间模型配置为空间的模型模型,保障空间运行时能正确的调用到相应的模型来处理业务数据。',
|
||||
},
|
||||
memoryExtractionEngine: {
|
||||
title: '记忆引擎模块配置中心',
|
||||
@@ -1528,6 +1545,8 @@ export const zh = {
|
||||
quickReply: '快速回复',
|
||||
web_search: '联网搜索',
|
||||
memory: '记忆',
|
||||
memoryConversationAnalysisEmpty: '目前没有可用的对话分析内容',
|
||||
memoryConversationAnalysisEmptySubTitle: '输入您的用户ID后,点击"测试记忆"查看对话记忆',
|
||||
},
|
||||
login: {
|
||||
title: '红熊记忆科学',
|
||||
@@ -1702,19 +1721,17 @@ export const zh = {
|
||||
|
||||
JsonTool_desc: '数据格式转换',
|
||||
JsonTool_features: 'JSON格式化、压缩、验证和转换功能',
|
||||
jsonFormat: 'JSON格式化',
|
||||
jsonGzip: 'JSON压缩',
|
||||
jsonCheck: 'JSON验证',
|
||||
jsonConversion: '格式转换',
|
||||
jsonParse: 'JSON解析',
|
||||
jsonInsert: 'JSON插入',
|
||||
jsonReplace: 'JSON验证',
|
||||
jsonDelete: 'JSON删除',
|
||||
jsonEg: '示例JSON',
|
||||
enterJson: '输入JSON',
|
||||
jsonPlaceholder: '输入JSON数据,例如:{"name": "测试", "value": 123}',
|
||||
clear: '清空',
|
||||
parse: '粘贴',
|
||||
format: '格式化',
|
||||
minify: '压缩',
|
||||
validate: '验证',
|
||||
convert: '转义',
|
||||
paste: '粘贴',
|
||||
parse: '解析',
|
||||
json_path: 'JSON 路径参数',
|
||||
outputResult: '输出结果',
|
||||
validJosn: 'JSON格式正确,验证通过!',
|
||||
|
||||
@@ -2034,7 +2051,8 @@ export const zh = {
|
||||
variableConfig: '变量配置',
|
||||
variableRequired: '必填',
|
||||
addMessage: '添加消息',
|
||||
answerDesc: '回复'
|
||||
answerDesc: '回复',
|
||||
addNode: '添加节点',
|
||||
},
|
||||
emotionEngine: {
|
||||
emotionEngineConfig: '情感引擎配置',
|
||||
@@ -2318,6 +2336,7 @@ export const zh = {
|
||||
node_type: '节点类型',
|
||||
last_access_time: '最后激活时间',
|
||||
activation_value: '当前激活值',
|
||||
refreshSuccess: '遗忘执行成功',
|
||||
},
|
||||
episodicDetail: {
|
||||
title: '记录你真实经历过的每一个重要场景',
|
||||
|
||||
@@ -66,6 +66,7 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
|
||||
OrderHistory: lazy(() => import('@/views/OrderHistory')),
|
||||
Pricing: lazy(() => import('@/views/Pricing')),
|
||||
ToolManagement: lazy(() => import('@/views/ToolManagement')),
|
||||
SpaceConfig: lazy(() => import('@/views/SpaceConfig')),
|
||||
Login: lazy(() => import('@/views/Login')),
|
||||
InviteRegister: lazy(() => import('@/views/InviteRegister')),
|
||||
NoPermission: lazy(() => import('@/views/NoPermission')),
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
{ "path": "/api-key", "element": "ApiKeyManagement" },
|
||||
{ "path": "/emotion-engine/:id", "element": "EmotionEngine" },
|
||||
{ "path": "/reflection-engine/:id", "element": "SelfReflectionEngine" },
|
||||
{ "path": "/space-config", "element": "SpaceConfig" },
|
||||
{ "path": "/no-permission", "element": "NoPermission" },
|
||||
{ "path": "/*", "element": "NotFound" }
|
||||
]
|
||||
|
||||
@@ -376,6 +376,21 @@
|
||||
"icon": null,
|
||||
"iconActive": null,
|
||||
"subs": null
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"parent": 0,
|
||||
"code": "spaceConfig",
|
||||
"label": "空间配置",
|
||||
"i18nKey": "menu.spaceConfig",
|
||||
"path": "/space-config",
|
||||
"enable": true,
|
||||
"display": true,
|
||||
"level": 1,
|
||||
"sort": 0,
|
||||
"icon": null,
|
||||
"iconActive": null,
|
||||
"subs": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,43 +12,57 @@ export function parseSSEToJSON(sseString: string) {
|
||||
const lines = sseString.trim().split('\n')
|
||||
|
||||
let currentEvent: SSEMessage = {}
|
||||
let dataContent = ''
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event:')) {
|
||||
if (currentEvent.event && dataContent) {
|
||||
currentEvent.data = parseDataContent(dataContent)
|
||||
events.push(currentEvent)
|
||||
}
|
||||
currentEvent = { event: line.substring(6).trim() }
|
||||
dataContent = ''
|
||||
} else if (line.startsWith('data:')) {
|
||||
if (dataContent) dataContent += '\n'
|
||||
dataContent += line.substring(5).trim()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (currentEvent.event && dataContent) {
|
||||
currentEvent.data = parseDataContent(dataContent)
|
||||
console.log('currentEvent', currentEvent)
|
||||
events.push(currentEvent)
|
||||
}
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
function parseDataContent(dataContent: string): string | object {
|
||||
try {
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event:')) {
|
||||
if (Object.keys(currentEvent).length > 0) {
|
||||
events.push(currentEvent)
|
||||
currentEvent = {}
|
||||
}
|
||||
currentEvent.event = line.substring(6).trim()
|
||||
} else if (line.startsWith('data:')) {
|
||||
const dataStr = line.substring(5).trim()
|
||||
if (dataStr) {
|
||||
try {
|
||||
// 尝试解析为 JSON
|
||||
currentEvent.data = JSON.parse(dataStr)
|
||||
} catch {
|
||||
// JSON 解析失败时,检查是否是被转义的 JSON 字符串
|
||||
try {
|
||||
const unescaped = dataStr.replace(/"/g, '"').replace(/&/g, '&')
|
||||
currentEvent.data = JSON.parse(unescaped)
|
||||
} catch {
|
||||
// 如果仍然失败,保存为原始字符串
|
||||
currentEvent.data = dataStr
|
||||
}
|
||||
}
|
||||
}
|
||||
// 第一层解码:HTML实体
|
||||
let unescaped = dataContent
|
||||
.replace(/"/g, '"')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/'/g, "'")
|
||||
|
||||
// 解析第一层JSON
|
||||
const firstParse = JSON.parse(unescaped)
|
||||
|
||||
// 如果data字段是字符串且包含JSON,解析data层但保持chunk为字符串
|
||||
if (firstParse.data && typeof firstParse.data === 'string' && firstParse.data.includes("{")) {
|
||||
try {
|
||||
firstParse.data = JSON.parse(firstParse.data)
|
||||
} catch {
|
||||
// 保持原字符串
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(currentEvent).length > 0) {
|
||||
events.push(currentEvent)
|
||||
}
|
||||
|
||||
return events
|
||||
} catch (error) {
|
||||
console.error('Parse stream error:', error)
|
||||
return []
|
||||
return firstParse
|
||||
} catch {
|
||||
return dataContent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,16 +94,30 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = ''; // 添加缓冲区来处理不完整的消息
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
if (onMessage) {
|
||||
onMessage(parseSSEToJSON(chunk) ?? {});
|
||||
buffer += chunk;
|
||||
|
||||
// 处理完整的事件
|
||||
const events = buffer.split('\n\n');
|
||||
buffer = events.pop() || ''; // 保留最后一个可能不完整的事件
|
||||
|
||||
for (const event of events) {
|
||||
if (event.trim() && onMessage) {
|
||||
onMessage(parseSSEToJSON(event) ?? {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理剩余的缓冲区内容
|
||||
if (buffer.trim() && onMessage) {
|
||||
onMessage(parseSSEToJSON(buffer) ?? {});
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -59,7 +59,7 @@ const GuideCard: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='rb:w-full rb:h-[204px] rb:p-4' style={{ backgroundImage: `url(${guideBgImg})`, backgroundSize: '100% 100%' }}>
|
||||
<div className='rb:w-full rb:p-4' style={{ backgroundImage: `url(${guideBgImg})`, backgroundSize: '100% 100%' }}>
|
||||
<div className='rb:flex rb:justify-start rb:text-white rb:text-base rb:font-semibold' >
|
||||
{ t('index.getStarted')}
|
||||
</div>
|
||||
|
||||
@@ -59,7 +59,6 @@ const list = [
|
||||
]
|
||||
const TopCardList: FC<{data?: DataResponse}> = ({ data }) => {
|
||||
const { t } = useTranslation()
|
||||
debugger
|
||||
return (
|
||||
<div className="rb:grid rb:grid-cols-4 rb:gap-[16px]">
|
||||
{list.map((item) => {
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
/*
|
||||
* @Description:
|
||||
* @Version: 0.0.1
|
||||
* @Author: yujiangping
|
||||
* @Date: 2026-01-12 16:34:59
|
||||
* @LastEditors: yujiangping
|
||||
* @LastEditTime: 2026-01-13 19:14:30
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'antd';
|
||||
import arrowRight from '@/assets/images/index/arrow_right.svg'
|
||||
import { Button, Divider } from 'antd';
|
||||
// import arrowRight from '@/assets/images/index/arrow_right.svg'
|
||||
import { getVersion, type versionResponse } from '@/api/common'
|
||||
|
||||
const GuideCard: React.FC = () => {
|
||||
@@ -27,20 +35,38 @@ const GuideCard: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className='rb:w-full rb:p-4 rb:border-1 rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded-xl'>
|
||||
<div className='rb:flex rb:items-center rb:justify-start rb:text-[#5B6167] rb:text-base rb:font-semibold'>
|
||||
{ t('index.latestUpdate')}
|
||||
{versionInfo && (
|
||||
<span className='rb:ml-2 rb:text-sm rb:text-[#1890FF]'>
|
||||
{versionInfo.version}
|
||||
</span>
|
||||
)}
|
||||
<div className='rb:flex rb:items-center rb:justify-start rb:text-[#5B6167] rb:text-base rb:font-semibold rb:gap-2'>
|
||||
{ t('index.latestUpdate')}
|
||||
<span className='rb:text-xs rb:text-[#1890FF]'>
|
||||
{versionInfo?.version}
|
||||
</span>
|
||||
</div>
|
||||
<div className='rb:flex rb:text-xs rb:text-[#5B6167] rb:leading-[18px] rb:mt-3 rb:pl-2'>
|
||||
{loading ? (
|
||||
<div className='rb:flex rb:flex-col rb:text-[#5B6167]'>
|
||||
{versionInfo && (<>
|
||||
<div className='rb:flex rb:items-center rb:gap-2 rb:text-sm rb:text-[#5B6167] rb:leading-5 '>
|
||||
|
||||
<span className='rb:text-xs rb:text-[#5B6167]'>
|
||||
{t('version.releaseDate')}: {versionInfo.introduction?.releaseDate}
|
||||
</span>
|
||||
<Divider type='vertical' />
|
||||
<span className='rb:text-xs rb:text-[#5B6167]'>
|
||||
{t('version.name')}: {versionInfo.introduction?.codeName}
|
||||
</span>
|
||||
</div>
|
||||
<p className='rb:text-sm rb:text-[#5B6167] rb:leading-5 rb:mt-2 '>
|
||||
{versionInfo.introduction?.upgradePosition}
|
||||
</p>
|
||||
{versionInfo.introduction?.coreUpgrades?.map((item,index) => (
|
||||
<p className='rb:text-sm rb:text-[#5B6167] rb:leading-5'>
|
||||
{index + 1}. {item}
|
||||
</p>
|
||||
))}
|
||||
</>)}
|
||||
{/* {loading ? (
|
||||
t('index.loading')
|
||||
) : (
|
||||
versionInfo?.introduction || t('index.latestUpdateDesc')
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
{/* <div className='rb:flex rb:w-full rb:items-center rb:justify-between rb:gap-3 rb:mt-4'>
|
||||
<Button className='rb:gap-2 rb:flex rb:items-center rb:text-[#212332] '>
|
||||
|
||||
118
web/src/views/SpaceConfig/index.tsx
Normal file
118
web/src/views/SpaceConfig/index.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { type FC, useEffect, useState } from 'react';
|
||||
import { Form, App, Button, Skeleton } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { SpaceConfigData } from './types'
|
||||
import { getWorkspaceModels, updateWorkspaceModels } from '@/api/workspaces'
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import RbAlert from '@/components/RbAlert';
|
||||
|
||||
const SpaceConfig: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [pageLoading, setPageLoding] = useState(false)
|
||||
const [form] = Form.useForm<SpaceConfigData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
useEffect(() => {
|
||||
setPageLoding(true)
|
||||
getWorkspaceModels().then((res) => {
|
||||
const { llm, embedding, rerank } = res as SpaceConfigData
|
||||
form.setFieldsValue({
|
||||
llm,
|
||||
embedding,
|
||||
rerank
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
setPageLoding(false)
|
||||
})
|
||||
}, [])
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
setLoading(true)
|
||||
updateWorkspaceModels(values)
|
||||
.then(() => {
|
||||
setLoading(false)
|
||||
message.success(t('common.updateSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:max-w-140 rb:mx-auto">
|
||||
{pageLoading
|
||||
? <Skeleton active />
|
||||
: <Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
label={t('space.llmModel')}
|
||||
name="llm"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'llm', pagesize: 100 }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('space.embeddingModel')}
|
||||
name="embedding"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'embedding', pagesize: 100 }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('space.rerankModel')}
|
||||
name="rerank"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'rerank', pagesize: 100 }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<RbAlert>{t('space.configAlert')}</RbAlert>
|
||||
|
||||
<Form.Item className="rb:text-right">
|
||||
<Button type="primary" className="rb:mt-6" onClick={handleSave} loading={loading}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpaceConfig;
|
||||
8
web/src/views/SpaceConfig/types.ts
Normal file
8
web/src/views/SpaceConfig/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface SpaceConfigData {
|
||||
llm: string;
|
||||
embedding: string;
|
||||
rerank: string;
|
||||
}
|
||||
export interface SpaceConfigRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, Button, Space, Tree } from 'antd';
|
||||
import { Form, Input, Button, Space } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { TreeDataNode } from 'antd';
|
||||
|
||||
@@ -12,7 +12,7 @@ import { execute } from '@/api/tools';
|
||||
const JsonToolModal = forwardRef<JsonToolModalRef>((_props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<{ json: string; }>();
|
||||
const [form] = Form.useForm<{ json: string; json_path: string; }>();
|
||||
const [data, setData] = useState<ToolItem>({} as ToolItem)
|
||||
const [formatValue, setFormatValue] = useState<string | Record<string, any> | null>(null)
|
||||
|
||||
@@ -60,44 +60,29 @@ const JsonToolModal = forwardRef<JsonToolModalRef>((_props, ref) => {
|
||||
}
|
||||
const handleOperate = (type: string) => {
|
||||
const json = form.getFieldValue('json')
|
||||
const json_path = form.getFieldValue('json_path')
|
||||
if (!json || !data.id) return
|
||||
let params: ExecuteData = {
|
||||
tool_id: data.id,
|
||||
parameters: {
|
||||
operation: type,
|
||||
input_data: json
|
||||
input_data: json,
|
||||
json_path
|
||||
}
|
||||
}
|
||||
if (type === 'format') {
|
||||
if (type === 'parse') {
|
||||
params = {
|
||||
...params,
|
||||
parameters: {
|
||||
...params.parameters,
|
||||
indent: 2,
|
||||
ensure_ascii: false,
|
||||
sort_keys: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
execute(params)
|
||||
.then(res => {
|
||||
const { data } = res as {data: {
|
||||
formatted_json: string;
|
||||
minified_json: string;
|
||||
is_valid: boolean;
|
||||
converted_json: string;
|
||||
error: string;
|
||||
structure: Record<string, string | number>
|
||||
}}
|
||||
switch (type) {
|
||||
case 'format':
|
||||
setFormatValue(data.formatted_json);
|
||||
break
|
||||
case 'minify':
|
||||
setFormatValue(data.minified_json)
|
||||
break
|
||||
}
|
||||
const { data } = res as { data: string; }
|
||||
setFormatValue(data);
|
||||
})
|
||||
}
|
||||
const clear = () => {
|
||||
@@ -126,15 +111,20 @@ const JsonToolModal = forwardRef<JsonToolModalRef>((_props, ref) => {
|
||||
label={<Space size={8}>
|
||||
{t('tool.enterJson')}
|
||||
<Button onClick={clear}>{t('tool.clear')}</Button>
|
||||
<Button onClick={handleParse}>{t('tool.parse')}</Button>
|
||||
<Button onClick={handleParse}>{t('tool.paste')}</Button>
|
||||
</Space>}
|
||||
>
|
||||
<Input.TextArea rows={10} placeholder={t('tool.jsonPlaceholder')} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="json_path"
|
||||
label={t('tool.json_path')}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</FormItem>
|
||||
|
||||
<Space size={8} className="rb:mb-3">
|
||||
<Button onClick={() => handleOperate('format')}>{t('tool.format')}</Button>
|
||||
<Button onClick={() => handleOperate('minify')}>{t('tool.minify')}</Button>
|
||||
<Button onClick={() => handleOperate('parse')}>{t('tool.parse')}</Button>
|
||||
</Space>
|
||||
<FormItem
|
||||
label={t('tool.outputResult')}
|
||||
|
||||
@@ -23,6 +23,7 @@ interface CurrentTimeObj {
|
||||
iso_format: string;
|
||||
timestamp: string;
|
||||
timestamp_ms: string;
|
||||
utc_datetime: string;
|
||||
}
|
||||
const TimeToolModal = forwardRef<TimeToolModalRef>((_props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -88,8 +89,8 @@ const TimeToolModal = forwardRef<TimeToolModalRef>((_props, ref) => {
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
const response = res as { data: CurrentTimeObj }
|
||||
setTimestampFormat(response.data.datetime)
|
||||
const response = res as { data: string }
|
||||
setTimestampFormat(response.data)
|
||||
})
|
||||
}
|
||||
const handleChangeFormatType = () => {
|
||||
@@ -149,7 +150,7 @@ const TimeToolModal = forwardRef<TimeToolModalRef>((_props, ref) => {
|
||||
<Input disabled value={currentTime?.datetime} />
|
||||
</FormItem>
|
||||
<FormItem label={t('tool.utcTime')} >
|
||||
<Input disabled value={currentTime?.iso_format} />
|
||||
<Input disabled value={currentTime?.utc_datetime} />
|
||||
</FormItem>
|
||||
<FormItem label={t('tool.secondsTimestamp')} >
|
||||
<Input disabled value={currentTime?.timestamp} />
|
||||
|
||||
@@ -10,10 +10,10 @@ export const InnerConfigData: Record<string, InnerConfigItem> = {
|
||||
},
|
||||
JsonTool: {
|
||||
features: [
|
||||
'jsonFormat',
|
||||
'jsonGzip',
|
||||
'jsonCheck',
|
||||
'jsonConversion'
|
||||
'jsonParse',
|
||||
'jsonInsert',
|
||||
'jsonReplace',
|
||||
'jsonDelete'
|
||||
],
|
||||
eg: '{"name":"工具","tool_class":"内置"}'
|
||||
},
|
||||
|
||||
@@ -130,6 +130,7 @@ export interface ExecuteData {
|
||||
ensure_ascii?: boolean;
|
||||
sort_keys?: boolean;
|
||||
input_data?: string;
|
||||
json_path?: string;
|
||||
}
|
||||
}
|
||||
export interface CustomToolModalRef {
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ConfigModalData, ConfigModalRef } from '../types'
|
||||
import { getWorkspaceModels, updateWorkspaceModels } from '@/api/workspaces'
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import RbModal from '@/components/RbModal'
|
||||
|
||||
const ConfigModal = forwardRef<ConfigModalRef>((_props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<ConfigModalData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
getWorkspaceModels().then((res) => {
|
||||
const { llm, embedding, rerank } = res as ConfigModalData
|
||||
form.setFieldsValue({
|
||||
llm,
|
||||
embedding,
|
||||
rerank
|
||||
})
|
||||
})
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
setLoading(true)
|
||||
updateWorkspaceModels(values)
|
||||
.then(() => {
|
||||
setLoading(false)
|
||||
handleClose()
|
||||
message.success(t('common.updateSuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
});
|
||||
|
||||
handleClose()
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t(`userMemory.editConfig`)}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
label={t('space.llmModel')}
|
||||
name="llm"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'llm', pagesize: 100 }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('space.embeddingModel')}
|
||||
name="embedding"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'embedding', pagesize: 100 }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('space.rerankModel')}
|
||||
name="rerank"
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'rerank', pagesize: 100 }}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
hasAll={false}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ConfigModal;
|
||||
@@ -1,56 +1,28 @@
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Row, Col, Radio, Button, List, Skeleton, Space } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { RadioChangeEvent } from 'antd';
|
||||
import { AppstoreOutlined, MenuOutlined } from '@ant-design/icons';
|
||||
import { Row, Col, List, Skeleton } from 'antd';
|
||||
import Empty from '@/components/Empty'
|
||||
|
||||
import type { Data, ConfigModalRef } from './types'
|
||||
import totalNum from '@/assets/images/memory/totalNum.svg'
|
||||
import onlineNum from '@/assets/images/memory/onlineNum.svg'
|
||||
import Table from '@/components/Table'
|
||||
import { getTotalEndUsers, userMemoryListUrl, getUserMemoryList } from '@/api/memory';
|
||||
import ConfigModal from './components/ConfigModal';
|
||||
import type { Data } from './types'
|
||||
import { getUserMemoryList } from '@/api/memory';
|
||||
import { useUser } from '@/store/user'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import SearchInput from '@/components/SearchInput';
|
||||
|
||||
const bgList = [
|
||||
'linear-gradient( 180deg, #F1F6FE 0%, #FBFDFF 100%)',
|
||||
'linear-gradient( 180deg, #F1F9FE 0%, #FBFDFF 100%)',
|
||||
'linear-gradient( 180deg, #FEFBF7 0%, #FBFDFF 100%)',
|
||||
'linear-gradient( 180deg, #F1F9FE 0%, #FBFDFF 100%)',
|
||||
]
|
||||
|
||||
const countList = [
|
||||
'total_num', 'online_num',
|
||||
]
|
||||
const IconList: Record<string, string> = {
|
||||
total_num: totalNum,
|
||||
online_num: onlineNum,
|
||||
}
|
||||
export default function UserMemory() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate()
|
||||
const { storageType } = useUser()
|
||||
const configModalRef = useRef<ConfigModalRef>(null)
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [data, setData] = useState<Data[]>([]);
|
||||
const [countData, setCountData] = useState<Record<string, number>>({});
|
||||
const [layout, setLayout] = useState<'card' | 'list'>('card');
|
||||
const [search, setSearch] = useState<string | undefined>(undefined);
|
||||
|
||||
// 获取数据
|
||||
useEffect(() => {
|
||||
getCountData()
|
||||
getData()
|
||||
}, []);
|
||||
|
||||
// 用户记忆统计
|
||||
const getCountData = () => {
|
||||
getTotalEndUsers().then((res) => {
|
||||
setCountData(res as Record<string, number> || {})
|
||||
})
|
||||
}
|
||||
const getData = () => {
|
||||
setLoading(true)
|
||||
getUserMemoryList().then((res) => {
|
||||
@@ -60,7 +32,6 @@ export default function UserMemory() {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
console.log('storageType', storageType)
|
||||
const handleViewDetail = (id: string | number) => {
|
||||
switch (storageType) {
|
||||
case 'neo4j':
|
||||
@@ -70,112 +41,77 @@ export default function UserMemory() {
|
||||
navigate(`/user-memory/${id}`)
|
||||
}
|
||||
}
|
||||
const handleChangeLayout = (e: RadioChangeEvent) => {
|
||||
const type = e.target.value
|
||||
setLayout(type)
|
||||
const handleViewMemoryConfig = () => {
|
||||
navigate(`/memory`)
|
||||
}
|
||||
// 表格列配置
|
||||
const columns: ColumnsType = [
|
||||
{
|
||||
title: t('userMemory.user'),
|
||||
dataIndex: 'end_user',
|
||||
key: 'end_user',
|
||||
render: (value) => value?.other_name && value?.other_name !== '' ? value?.other_name : value?.id || '-'
|
||||
},
|
||||
{
|
||||
title: t('userMemory.knowledgeEntryCount'),
|
||||
dataIndex: 'memory_num',
|
||||
key: 'memory_num',
|
||||
render: (value) => value?.total || 0
|
||||
},
|
||||
{
|
||||
title: t('common.operation'),
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => handleViewDetail(record.end_user?.id)}
|
||||
>
|
||||
{t('common.viewDetail')}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const filterData = useMemo(() => {
|
||||
if (search && search.trim() !== '') {
|
||||
return data.filter((item) => {
|
||||
const { end_user } = item as Data;
|
||||
const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id
|
||||
return name?.includes(search)
|
||||
})
|
||||
}
|
||||
|
||||
return data
|
||||
}, [search, data])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row gutter={16} className="rb:mb-4">
|
||||
{countList.map(key => (
|
||||
<Col key={key} span={6}>
|
||||
<div className="rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-xl rb:p-[18px_20px_20px_20px]">
|
||||
<div className="rb:text-[28px] rb:font-extrabold rb:leading-8.75 rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||
{countData[key] || 0}{key === 'avgInteractionTime' ? 's' : ''}
|
||||
<img className="rb:w-6 rb:h-6" src={IconList[key]} />
|
||||
</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`userMemory.${key}`)}</div>
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
<Col span={12} className="rb:text-right">
|
||||
<Space>
|
||||
<Button type="primary" onClick={() => configModalRef?.current?.handleOpen()}>{t('userMemory.chooseModel')}</Button>
|
||||
<Radio.Group value={layout} onChange={handleChangeLayout}>
|
||||
<Radio.Button value="card" disabled={layout === 'card'}><AppstoreOutlined /></Radio.Button>
|
||||
<Radio.Button value="list" disabled={layout === 'list'}><MenuOutlined /></Radio.Button>
|
||||
</Radio.Group>
|
||||
</Space>
|
||||
<Col span={8}>
|
||||
<SearchInput
|
||||
placeholder={t('userMemory.searchPlaceholder')}
|
||||
onSearch={(value) => setSearch(value)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{layout === 'card' &&
|
||||
<>
|
||||
{loading ?
|
||||
<Skeleton active />
|
||||
: data.length > 0 ? (
|
||||
<List
|
||||
grid={{ gutter: 16, column: 4 }}
|
||||
dataSource={data}
|
||||
renderItem={(item, index) => {
|
||||
const { end_user, memory_num } = item as Data;
|
||||
const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id
|
||||
return (
|
||||
<List.Item key={index}>
|
||||
<div
|
||||
className="rb:p-5 rb:rounded-xl rb:border rb:border-[#DFE4ED] rb:cursor-pointer"
|
||||
style={{
|
||||
background: bgList[index % bgList.length],
|
||||
}}
|
||||
{loading ?
|
||||
<Skeleton active />
|
||||
: filterData.length > 0 ? (
|
||||
<List
|
||||
grid={{ gutter: 16, column: 3 }}
|
||||
dataSource={filterData}
|
||||
renderItem={(item, index) => {
|
||||
const { end_user, memory_num, memory_config } = item as Data;
|
||||
const name = end_user?.other_name && end_user?.other_name !== '' ? end_user?.other_name : end_user?.id
|
||||
return (
|
||||
<List.Item key={index}>
|
||||
<RbCard
|
||||
avatar={<div className="rb:w-12 rb:h-12 rb:text-center rb:font-semibold rb:text-[28px] rb:leading-12 rb:rounded-lg rb:text-[#FBFDFF] rb:bg-[#155EEF] rb:mr-2">{name[0]}</div>}
|
||||
title={name || '-'}
|
||||
extra={<div
|
||||
className="rb:w-7 rb:h-7 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/goto.svg')]"
|
||||
onClick={() => handleViewDetail(end_user.id)}
|
||||
>
|
||||
<div className="rb:flex rb:items-center">
|
||||
<div className="rb:w-12 rb:h-12 rb:text-center rb:font-semibold rb:text-[28px] rb:leading-12 rb:rounded-lg rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name[0]}</div>
|
||||
<div className="rb:max-w-[calc(100%-60px)] rb:text-base rb:font-medium rb:leading-6 rb:ml-3 rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">
|
||||
{name || '-'}<br/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rb:grid rb:grid-cols-1 rb:gap-3 rb:mt-7 rb:mb-7">
|
||||
<div className="rb:text-center">
|
||||
<div className="rb:text-[24px] rb:leading-7.5 rb:font-extrabold">{memory_num.total || 0}</div>
|
||||
<div className="rb:wrap-break-word">{t(`userMemory.knowledgeEntryCount`)}</div>
|
||||
</div>
|
||||
</div>
|
||||
></div>}
|
||||
>
|
||||
<div className="rb:flex rb:justify-between rb:items-center">
|
||||
<div>{t('userMemory.capacity')}</div>
|
||||
<div>{memory_num?.total || 0} {t('userMemory.memoryNum')}</div>
|
||||
</div>
|
||||
<div className="rb:flex rb:justify-between rb:items-center rb:mt-2.5">
|
||||
<div>{t('userMemory.type')}</div>
|
||||
<div>{t(`userMemory.${item.type || 'person'}`)}</div>
|
||||
</div>
|
||||
</List.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
) : <Empty />}
|
||||
</>
|
||||
}
|
||||
|
||||
{layout === 'list' &&
|
||||
<Table
|
||||
apiUrl={userMemoryListUrl}
|
||||
columns={columns}
|
||||
rowKey="end_user.id"
|
||||
pagination={false}
|
||||
/>
|
||||
<div className="rb:mt-3 rb:bg-[#F6F8FC] rb:rounded-lg rb:border rb:border-[#DFE4ED] rb:py-2 rb:px-3" onClick={handleViewMemoryConfig}>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:flex rb:justify-between rb:items-center">
|
||||
{t('userMemory.memory_config_name')}
|
||||
<div
|
||||
className="rb:w-7 rb:h-7 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/arrow_right.svg')]"
|
||||
></div>
|
||||
</div>
|
||||
<div className="rb:font-medium rb:leading-5 rb:mt-1">{memory_config?.memory_config_name || '-'}</div>
|
||||
</div>
|
||||
</RbCard>
|
||||
</List.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
) : <Empty />
|
||||
}
|
||||
<ConfigModal ref={configModalRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -17,13 +17,10 @@ export interface Data {
|
||||
entity: number;
|
||||
}
|
||||
},
|
||||
memory_config: {
|
||||
memory_config_id: string;
|
||||
memory_config_name: string;
|
||||
},
|
||||
type: string;
|
||||
name?: string;
|
||||
}
|
||||
export interface ConfigModalData {
|
||||
llm: string;
|
||||
embedding: string;
|
||||
rerank: string;
|
||||
}
|
||||
export interface ConfigModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import ReactEcharts from 'echarts-for-react';
|
||||
import Empty from '@/components/Empty'
|
||||
import Loading from '@/components/Empty/Loading'
|
||||
import type { Emotion } from './GraphDetail'
|
||||
import type { Emotion } from '../pages/GraphDetail'
|
||||
|
||||
interface EmotionLineProps {
|
||||
chartData: Emotion[];
|
||||
@@ -26,7 +26,7 @@ const EmotionLine: FC<EmotionLineProps> = ({ chartData, loading }) => {
|
||||
const seriesData = timePoints.map(time => dataMap.get(time) || 0)
|
||||
|
||||
return {
|
||||
name: emotionType,
|
||||
name: t(`userMemory.${emotionType}`),
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
@@ -71,7 +71,7 @@ const EmotionLine: FC<EmotionLineProps> = ({ chartData, loading }) => {
|
||||
formatter: function(params: any) {
|
||||
let result = `${params[0].axisValue}<br/>`
|
||||
params.forEach((param: any) => {
|
||||
result += `${param.marker}${param.seriesName}: ${param.value}<br/>`
|
||||
result += `${param.marker}${param.seriesName}: ${param.value}%<br/>`
|
||||
})
|
||||
return result
|
||||
}
|
||||
@@ -92,7 +92,7 @@ const EmotionLine: FC<EmotionLineProps> = ({ chartData, loading }) => {
|
||||
},
|
||||
grid: {
|
||||
top: 16,
|
||||
left: 30,
|
||||
left: 40,
|
||||
right: 36,
|
||||
bottom: 48,
|
||||
// containLabel: false
|
||||
@@ -103,7 +103,7 @@ const EmotionLine: FC<EmotionLineProps> = ({ chartData, loading }) => {
|
||||
boundaryGap: false,
|
||||
axisLabel: {
|
||||
color: '#A8A9AA',
|
||||
fontFamily: 'PingFangSC, PingFang SC'
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
@@ -130,7 +130,8 @@ const EmotionLine: FC<EmotionLineProps> = ({ chartData, loading }) => {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#A8A9AA',
|
||||
fontFamily: 'PingFangSC, PingFang SC'
|
||||
fontFamily: 'PingFangSC, PingFang SC',
|
||||
formatter: '{value}%'
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
@@ -152,7 +153,7 @@ const EmotionLine: FC<EmotionLineProps> = ({ chartData, loading }) => {
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
max: 1,
|
||||
max: 100,
|
||||
min: 0
|
||||
},
|
||||
series: getSeries()
|
||||
|
||||
113
web/src/views/UserMemoryDetail/components/ForgetRefreshModal.tsx
Normal file
113
web/src/views/UserMemoryDetail/components/ForgetRefreshModal.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Form, Slider } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { forgetTrigger } from '@/api/memory'
|
||||
import type { ForgetRefreshModalRef } from '../pages/ForgetDetail'
|
||||
|
||||
interface ForgetRefreshModalProps {
|
||||
refresh: (flag: boolean) => void;
|
||||
}
|
||||
|
||||
const ForgetRefreshModal = forwardRef<ForgetRefreshModalRef, ForgetRefreshModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams()
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<{ max_merge_batch_size: number; min_days_since_access: number; }>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
form.resetFields();
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
if(!id) return
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
setLoading(true)
|
||||
forgetTrigger({
|
||||
...values,
|
||||
end_user_id: id
|
||||
})
|
||||
.then(() => {
|
||||
refresh(true)
|
||||
handleClose()
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('common.refresh')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.refresh')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<div className="rb:pl-3">
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mb-2">
|
||||
{t(`forgettingEngine.max_merge_batch_size`)}
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
name="max_merge_batch_size"
|
||||
>
|
||||
<Slider tooltip={{ open: false }} max={1000} min={1} step={1} style={{ margin: '0' }} />
|
||||
</Form.Item>
|
||||
<div className="rb:flex rb:text-[12px] rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5 rb:-mt-6.5">
|
||||
<span>{t(`forgettingEngine.range`)}: {[1, 1000]?.join('-')}</span>
|
||||
{t('forgettingEngine.CurrentValue')}: {values?.min_days_since_access || 0}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rb:pl-3 rb:mt-4">
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mb-2">
|
||||
{t(`forgettingEngine.min_days_since_access`)}
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
name="min_days_since_access"
|
||||
>
|
||||
<Slider tooltip={{ open: false }} max={365} min={1} step={1} style={{ margin: '0' }} />
|
||||
</Form.Item>
|
||||
<div className="rb:flex rb:text-[12px] rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5 rb:-mt-6.5">
|
||||
<span>{t(`forgettingEngine.range`)}: {[1, 365]?.join('-')}</span>
|
||||
{t('forgettingEngine.CurrentValue')}: {values?.min_days_since_access || 0}
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ForgetRefreshModal;
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type FC } from 'react'
|
||||
import { type FC, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactEcharts from 'echarts-for-react'
|
||||
import Empty from '@/components/Empty'
|
||||
import Loading from '@/components/Empty/Loading'
|
||||
import type { Interaction } from './GraphDetail'
|
||||
import type { Interaction } from '../pages/GraphDetail'
|
||||
|
||||
interface InteractionBarProps {
|
||||
chartData: Interaction[];
|
||||
@@ -14,11 +14,13 @@ const Colors = ['#155EEF', '#369F21', '#FF5D34']
|
||||
const InteractionBar: FC<InteractionBarProps> = ({ chartData, loading }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const series = [{
|
||||
name: 'Interaction Count',
|
||||
type: 'bar',
|
||||
data: chartData.map(item => item.count)
|
||||
}]
|
||||
const series = useMemo(() => {
|
||||
return [{
|
||||
name: t('userMemory.interactionCountData'),
|
||||
type: 'bar',
|
||||
data: chartData.map(item => item.count)
|
||||
}]
|
||||
}, [chartData, t])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -80,6 +82,7 @@ const InteractionBar: FC<InteractionBarProps> = ({ chartData, loading }) => {
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
minInterval: 1,
|
||||
axisLabel: {
|
||||
color: '#A8A9AA',
|
||||
fontFamily: 'PingFangSC, PingFang SC'
|
||||
@@ -104,8 +107,6 @@ const InteractionBar: FC<InteractionBarProps> = ({ chartData, loading }) => {
|
||||
type: 'solid'
|
||||
}
|
||||
},
|
||||
max: 1,
|
||||
min: 0
|
||||
},
|
||||
series
|
||||
}}
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { type FC, type ReactNode } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Layout } from 'antd';
|
||||
import { Layout, Space, Button } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import logoutIcon from '@/assets/images/logout.svg'
|
||||
import logoutIcon from '@/assets/images/logout_hover.svg'
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
interface ConfigHeaderProps {
|
||||
name?: string;
|
||||
operation?: ReactNode;
|
||||
source?: 'detail' | 'node'
|
||||
source?: 'detail' | 'node';
|
||||
extra?: ReactNode;
|
||||
}
|
||||
const PageHeader: FC<ConfigHeaderProps> = ({
|
||||
name,
|
||||
operation,
|
||||
source = 'detail'
|
||||
source = 'detail',
|
||||
extra
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@@ -33,10 +35,13 @@ const PageHeader: FC<ConfigHeaderProps> = ({
|
||||
{operation}
|
||||
</div>
|
||||
|
||||
<div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={goBack}>
|
||||
<img src={logoutIcon} className="rb:mr-2 rb:w-4 rb:h-4" />
|
||||
{t('common.return')}
|
||||
</div>
|
||||
<Space size={12}>
|
||||
<Button type="primary" ghost className="rb:group rb:h-6! rb:px-2!" onClick={goBack}>
|
||||
<img src={logoutIcon} className="rb:w-4 rb:h-4" />
|
||||
{t('common.return')}
|
||||
</Button>
|
||||
{extra}
|
||||
</Space>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import React, { type FC, useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Col, Row, Space, Button } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import ReactEcharts from 'echarts-for-react'
|
||||
import detailEmpty from '@/assets/images/userMemory/detail_empty.png'
|
||||
import type { Node, Edge, GraphData, StatementNodeProperties, ExtractedEntityNodeProperties, GraphDetailRef } from '../types'
|
||||
import type { Node, Edge, GraphData, StatementNodeProperties, ExtractedEntityNodeProperties } from '../types'
|
||||
import {
|
||||
getMemorySearchEdges,
|
||||
} from '@/api/memory'
|
||||
import Empty from '@/components/Empty'
|
||||
import Tag from '@/components/Tag'
|
||||
import GraphDetail from '../components/GraphDetail'
|
||||
|
||||
const colors = ['#155EEF', '#369F21', '#4DA8FF', '#FF5D34', '#9C6FFF', '#FF8A4C', '#8BAEF7', '#FFB048']
|
||||
const RelationshipNetwork:FC = () => {
|
||||
@@ -26,7 +25,7 @@ const RelationshipNetwork:FC = () => {
|
||||
const [categories, setCategories] = useState<{ name: string }[]>([])
|
||||
const [selectedNode, setSelectedNode] = useState<Node | null>(null)
|
||||
// const [fullScreen, setFullScreen] = useState<boolean>(false)
|
||||
const graphDetailRef = useRef<GraphDetailRef>(null)
|
||||
const navigate = useNavigate()
|
||||
|
||||
console.log('categories', categories)
|
||||
// 关系网络
|
||||
@@ -133,15 +132,14 @@ const RelationshipNetwork:FC = () => {
|
||||
}
|
||||
}, [nodes])
|
||||
|
||||
// const handleFullScreen = () => {
|
||||
// setFullScreen(prev => !prev)
|
||||
// }
|
||||
|
||||
console.log('selectedNode', selectedNode)
|
||||
|
||||
const handleViewAll = () => {
|
||||
if (!selectedNode) return
|
||||
graphDetailRef.current?.handleOpen(selectedNode)
|
||||
const params = new URLSearchParams({
|
||||
nodeId: selectedNode.id,
|
||||
nodeLabel: selectedNode.label,
|
||||
nodeName: selectedNode.name || ''
|
||||
})
|
||||
navigate(`/user-memory/detail/${id}/GRAPH?${params.toString()}`)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -336,8 +334,6 @@ const RelationshipNetwork:FC = () => {
|
||||
</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
|
||||
<GraphDetail ref={graphDetailRef} />
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@/api/memory'
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
import Empty from '@/components/Empty'
|
||||
import Tag from '@/components/Tag'
|
||||
|
||||
interface TimelineItem {
|
||||
id: string;
|
||||
@@ -18,6 +19,9 @@ interface TimelineItem {
|
||||
summary: string;
|
||||
storage_type: number;
|
||||
created_time: string | number;
|
||||
domain: string;
|
||||
topic: string;
|
||||
keywords: string[]
|
||||
}
|
||||
|
||||
const KEYS = {
|
||||
@@ -68,9 +72,14 @@ const Timeline: FC = () => {
|
||||
{formatDateTime(vo.created_time)}
|
||||
{index !== data.length - 1 && <Divider type="vertical" className="rb:flex-1 rb:w-px rb:border-[#155EEF]!" />}
|
||||
</div>
|
||||
<div className="rb:flex rb:justify-between rb:flex-1 rb:mb-4">
|
||||
<div className="rb:w-150 rb:leading-5">{vo.summary}</div>
|
||||
<div className="rb:text-[#5B6167] rb:font-medium">{t(`perceptualDetail.${perceptual_type[vo.perceptual_type]}`)}</div>
|
||||
<div className="rb:flex-1 rb:pb-4">
|
||||
<div className="rb:flex rb:justify-between">
|
||||
<div className="rb:w-150 rb:leading-5 rb:font-medium">{vo.summary}</div>
|
||||
<div className="rb:text-[#5B6167] rb:font-medium rb:flex-1 rb:text-right">{t(`perceptualDetail.${perceptual_type[vo.perceptual_type]}`)}</div>
|
||||
</div>
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:mt-2">{[vo.domain, vo.topic].join(' | ')}</div>
|
||||
|
||||
<Space size={8} className="rb:mt-2">{vo.keywords.map(tag => <Tag>{tag}</Tag>)}</Space>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FC, useEffect, useState, useMemo } from 'react'
|
||||
import { useEffect, useState, useMemo, forwardRef, useImperativeHandle, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Progress } from 'antd'
|
||||
import { Row, Col, Progress, App } from 'antd'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import {
|
||||
getForgetStats,
|
||||
@@ -12,6 +12,7 @@ import RecentTrendsLineCard from '../components/RecentTrendsLineCard'
|
||||
import Table from '@/components/Table'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import StatusTag from '@/components/StatusTag'
|
||||
import ForgetRefreshModal from '../components/ForgetRefreshModal'
|
||||
|
||||
const statusTagColors: Record<string, 'success' | 'purple' | 'default' | 'warning' | 'error' | 'lightBlue'> = {
|
||||
statement: 'success',
|
||||
@@ -20,24 +21,33 @@ const statusTagColors: Record<string, 'success' | 'purple' | 'default' | 'warnin
|
||||
chunk: 'warning',
|
||||
}
|
||||
|
||||
const ForgetDetail: FC = () => {
|
||||
export interface ForgetRefreshModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
|
||||
const ForgetDetail = forwardRef((_props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const { message } = App.useApp()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [data, setData] = useState<ForgetData>({} as ForgetData)
|
||||
const forgetRefreshModalRef = useRef<ForgetRefreshModalRef>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
}, [id])
|
||||
|
||||
const getData = () => {
|
||||
const getData = (flag: boolean = false) => {
|
||||
if (!id) return
|
||||
setLoading(true)
|
||||
getForgetStats(id).then((res) => {
|
||||
const response = res as ForgetData
|
||||
setData(response)
|
||||
setLoading(false)
|
||||
if (flag) {
|
||||
message.success(t('forgetDetail.refreshSuccess'))
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
@@ -67,6 +77,14 @@ const ForgetDetail: FC = () => {
|
||||
}
|
||||
}, [data.recent_trends])
|
||||
|
||||
const handleRefresh = () => {
|
||||
forgetRefreshModalRef.current?.handleOpen()
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleRefresh
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:mt-3">{t('forgetDetail.title')}</div>
|
||||
@@ -152,7 +170,12 @@ const ForgetDetail: FC = () => {
|
||||
]}
|
||||
pagination={false}
|
||||
/>
|
||||
|
||||
<ForgetRefreshModal
|
||||
ref={forgetRefreshModalRef}
|
||||
refresh={getData}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
export default ForgetDetail
|
||||
@@ -1,16 +1,17 @@
|
||||
import { useState, forwardRef, useImperativeHandle, useMemo } from 'react'
|
||||
import { useState, forwardRef, useImperativeHandle, useMemo, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { Row, Col, Tabs, Space, Skeleton } from 'antd'
|
||||
|
||||
import { getRelationshipEvolution, getTimelineMemories } from '@/api/memory'
|
||||
import type { Node, GraphDetailRef } from '../types'
|
||||
import RbDrawer from '@/components/RbDrawer'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import EmotionLine from './EmotionLine'
|
||||
import EmotionLine from '../components/EmotionLine'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import Tag from '@/components/Tag'
|
||||
import InteractionBar from './InteractionBar'
|
||||
import InteractionBar from '../components/InteractionBar'
|
||||
import Empty from '@/components/Empty'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
|
||||
export interface Emotion {
|
||||
emotion_intensity: number;
|
||||
@@ -35,7 +36,7 @@ interface Timeline {
|
||||
|
||||
const GraphDetail = forwardRef<GraphDetailRef>((_props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false);
|
||||
const [searchParams] = useSearchParams()
|
||||
const [vo, setVo] = useState<Node | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [emotionData, setEmotionData] = useState<Emotion[]>([])
|
||||
@@ -43,14 +44,23 @@ const GraphDetail = forwardRef<GraphDetailRef>((_props, ref) => {
|
||||
const [activeTab, setActiveTab] = useState('timelines_memory')
|
||||
const [timelineLoading, setTimelineLoading] = useState(false)
|
||||
const [timelineMemories, setTimelineMemories] = useState<Timeline>({ timelines_memory: [], MemorySummary: [], Statement: [], ExtractedEntity: []})
|
||||
useEffect(() => {
|
||||
const nodeId = searchParams.get('nodeId')
|
||||
const nodeLabel = searchParams.get('nodeLabel')
|
||||
const nodeName = searchParams.get('nodeName')
|
||||
|
||||
if (nodeId && nodeLabel) {
|
||||
const nodeFromUrl = {
|
||||
id: nodeId,
|
||||
label: nodeLabel,
|
||||
name: nodeName || nodeLabel
|
||||
}
|
||||
handleOpen(nodeFromUrl as Node)
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
const handleCancel = () => {
|
||||
setVo(null)
|
||||
setOpen(false)
|
||||
}
|
||||
const handleOpen = (vo: Node) => {
|
||||
setActiveTab('timelines_memory')
|
||||
setOpen(true)
|
||||
setVo(vo)
|
||||
getRelationshipEvolutionData(vo)
|
||||
getTimelineMemoriesData(vo)
|
||||
@@ -85,56 +95,57 @@ const GraphDetail = forwardRef<GraphDetailRef>((_props, ref) => {
|
||||
}, [activeTab, timelineMemories])
|
||||
|
||||
return (
|
||||
<RbDrawer
|
||||
title={vo?.name}
|
||||
open={open}
|
||||
onClose={handleCancel}
|
||||
width={1000}
|
||||
>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-3">{t('userMemory.relationshipEvolution')}</div>
|
||||
<RbCard>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<EmotionLine chartData={emotionData} loading={loading} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<InteractionBar chartData={interactionData} loading={loading} />
|
||||
</Col>
|
||||
</Row>
|
||||
</RbCard>
|
||||
<>
|
||||
<PageHeader
|
||||
name={vo?.name}
|
||||
source="node"
|
||||
/>
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-3">{t('userMemory.relationshipEvolution')}</div>
|
||||
<RbCard>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<EmotionLine chartData={emotionData} loading={loading} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<InteractionBar chartData={interactionData} loading={loading} />
|
||||
</Col>
|
||||
</Row>
|
||||
</RbCard>
|
||||
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-3 rb:mt-6">{t('userMemory.timelineMemories')}</div>
|
||||
<RbCard>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
items={['timelines_memory', 'ExtractedEntity', 'Statement', 'MemorySummary'].map(key => ({
|
||||
label: t(`userMemory.${key}`),
|
||||
key
|
||||
}))}
|
||||
onChange={(key: string) => setActiveTab(key)}
|
||||
/>
|
||||
{timelineLoading
|
||||
? <Skeleton active />
|
||||
: !activeContent || activeContent.length === 0
|
||||
? <Empty size={120} className="rb:mt-12 rb:mb-20.25" />
|
||||
: <Space size={16} direction="vertical" className="rb:w-full">
|
||||
{activeContent.map((vo, index) => (
|
||||
<RbCard
|
||||
key={index}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
title={vo.text}
|
||||
>
|
||||
<div className="rb:text-[#A8A9AA] rb:text-[12px] rb:leading-4">{formatDateTime(vo.created_at)}</div>
|
||||
<Tag className="rb:mt-2">{vo.type}</Tag>
|
||||
</RbCard>
|
||||
))}
|
||||
</Space>
|
||||
}
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5 rb:mb-3 rb:mt-6">{t('userMemory.timelineMemories')}</div>
|
||||
<RbCard>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
items={['timelines_memory', 'Statement', 'MemorySummary'].map(key => ({
|
||||
label: t(`userMemory.${key}`),
|
||||
key
|
||||
}))}
|
||||
onChange={(key: string) => setActiveTab(key)}
|
||||
/>
|
||||
{timelineLoading
|
||||
? <Skeleton active />
|
||||
: !activeContent || activeContent.length === 0
|
||||
? <Empty size={120} className="rb:mt-12 rb:mb-20.25" />
|
||||
: <Space size={16} direction="vertical" className="rb:w-full">
|
||||
{activeContent.map((vo, index) => (
|
||||
<RbCard
|
||||
key={index}
|
||||
headerType="borderL"
|
||||
headerClassName="rb:before:bg-[#155EEF]!"
|
||||
title={vo.text}
|
||||
>
|
||||
<div className="rb:text-[#A8A9AA] rb:text-[12px] rb:leading-4">{formatDateTime(vo.created_at)}</div>
|
||||
<Tag className="rb:mt-2">{vo.type}</Tag>
|
||||
</RbCard>
|
||||
))}
|
||||
</Space>
|
||||
}
|
||||
|
||||
|
||||
</RbCard>
|
||||
</RbDrawer>
|
||||
|
||||
</RbCard>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})
|
||||
export default GraphDetail
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FC, useEffect, useState, useMemo } from 'react'
|
||||
import { type FC, useEffect, useState, useMemo, useRef } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Dropdown } from 'antd'
|
||||
import { Dropdown, Button } from 'antd'
|
||||
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import StatementDetail from './StatementDetail'
|
||||
@@ -15,12 +15,16 @@ import WorkingDetail from './WorkingDetail'
|
||||
import {
|
||||
getEndUserProfile,
|
||||
} from '@/api/memory'
|
||||
import refreshIcon from '@/assets/images/refresh_hover.svg'
|
||||
import GraphDetail from './GraphDetail'
|
||||
|
||||
const Detail: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id, type } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const [name, setName] = useState<string>('')
|
||||
const forgetDetailRef = useRef<{ handleRefresh: () => void }>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
getData()
|
||||
@@ -40,6 +44,13 @@ const Detail: FC = () => {
|
||||
const onClick = ({ key }: { key: string }) => {
|
||||
navigate(`/user-memory/detail/${id}/${key}`, { replace: true })
|
||||
}
|
||||
const handleRefresh = () => {
|
||||
forgetDetailRef.current?.handleRefresh()
|
||||
}
|
||||
|
||||
if (type === 'GRAPH') {
|
||||
return <GraphDetail />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:w-full">
|
||||
@@ -49,17 +60,22 @@ const Detail: FC = () => {
|
||||
operation={
|
||||
<Dropdown menu={{ items, onClick, selectedKeys: type ? [type] : [] }}>
|
||||
<div className="rb:cursor-pointer rb:group rb:flex rb:items-center rb:gap-1">
|
||||
- {type ? t(`userMemory.${type}`) : ''}
|
||||
- {type ? t(`userMemory.${type}`) : ''}
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/userMemory/up_border.svg')] rb:transform-[rotate(180deg)] rb:group-hover:transform-[rotate(0deg)]"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
}
|
||||
extra={type === 'FORGETTING_MANAGEMENT' &&
|
||||
<Button type="primary" ghost className="rb:group rb:h-6! rb:px-2!" onClick={handleRefresh}>
|
||||
<img src={refreshIcon} className="rb:w-4 rb:h-4" />
|
||||
{t('common.refresh')}
|
||||
</Button>}
|
||||
/>
|
||||
<div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4">
|
||||
{type === 'EMOTIONAL_MEMORY' && <StatementDetail />}
|
||||
{type === 'FORGETTING_MANAGEMENT' && <ForgetDetail />}
|
||||
{type === 'FORGETTING_MANAGEMENT' && <ForgetDetail ref={forgetDetailRef} />}
|
||||
{type === 'IMPLICIT_MEMORY' && <ImplicitDetail />}
|
||||
{type === 'SHORT_TERM_MEMORY' && <ShortTermDetail />}
|
||||
{type === 'PERCEPTUAL_MEMORY' && <PerceptualDetail />}
|
||||
|
||||
@@ -26,6 +26,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
const [chatList, setChatList] = useState<ChatItem[]>([])
|
||||
const [variables, setVariables] = useState<StartVariableItem[]>([])
|
||||
const [streamLoading, setStreamLoading] = useState(false)
|
||||
const [conversationId, setConversationId] = useState<string | null>(null)
|
||||
|
||||
const handleOpen = () => {
|
||||
setOpen(true)
|
||||
@@ -100,7 +101,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
setStreamLoading(false)
|
||||
|
||||
data.forEach(item => {
|
||||
const { chunk } = item.data as { chunk: string; };
|
||||
const { chunk, conversation_id } = item.data as { chunk: string; conversation_id: string | null; };
|
||||
|
||||
switch(item.event) {
|
||||
case 'message':
|
||||
@@ -131,6 +132,10 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
setStreamLoading(false)
|
||||
break
|
||||
}
|
||||
|
||||
if (conversation_id && conversationId !== conversation_id) {
|
||||
setConversationId(conversation_id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -138,7 +143,8 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
draftRun(appId, {
|
||||
message: message,
|
||||
variables: params,
|
||||
stream: true
|
||||
stream: true,
|
||||
conversation_id: conversationId
|
||||
}, handleStreamMessage)
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
|
||||
@@ -107,7 +107,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
<div style={{ maxHeight: '300px', overflowY: 'auto', minWidth: '240px' }}>
|
||||
{nodeLibrary.map((category, categoryIndex) => {
|
||||
const filteredNodes = category.nodes.filter(nodeType =>
|
||||
nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'loop' && nodeType.type !== 'cycle-start'
|
||||
nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'iteration' && nodeType.type !== 'loop' && nodeType.type !== 'cycle-start'
|
||||
);
|
||||
|
||||
if (filteredNodes.length === 0) return null;
|
||||
|
||||
@@ -33,7 +33,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
y: cycleStartBBox.y,
|
||||
data: {
|
||||
type: 'add-node',
|
||||
label: '添加节点',
|
||||
label: t('workflow.addNode'),
|
||||
icon: '+',
|
||||
parentId: node.id,
|
||||
cycle: data.id,
|
||||
@@ -61,7 +61,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
zIndex: 3
|
||||
zIndex: 10
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
y: centerY,
|
||||
data: {
|
||||
type: 'add-node',
|
||||
label: '添加节点',
|
||||
label: t('workflow.addNode'),
|
||||
icon: '+',
|
||||
parentId: node.id,
|
||||
cycle: data.id,
|
||||
@@ -128,7 +128,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
zIndex: 3
|
||||
zIndex: 10
|
||||
}
|
||||
|
||||
graph.addEdge(edgeConfig)
|
||||
|
||||
@@ -151,11 +151,11 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
|
||||
|
||||
let filteredNodes;
|
||||
if (isChildOfLoop) {
|
||||
// Use same filtering as AddNode for child nodes of loop
|
||||
// Use same filtering as AddNode for child nodes of loop, but allow break
|
||||
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type));
|
||||
} else if (isChildOfIteration) {
|
||||
// Filter out loop and iteration nodes for children of iteration nodes
|
||||
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'break', 'cycle-start', 'iteration'].includes(nodeType.type));
|
||||
// Filter out loop and iteration nodes for children of iteration nodes, but allow break
|
||||
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type));
|
||||
} else {
|
||||
// Original filtering for non-loop child nodes
|
||||
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'break', 'cycle-start'].includes(nodeType.type));
|
||||
|
||||
@@ -60,7 +60,7 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
options={options.filter(vo => vo.nodeData.type === 'loop' || vo.value.includes('conv.'))}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([parentName, name, 'operation'], undefined);
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { type FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input, Button, Form, Space } from 'antd';
|
||||
import { PlusOutlined, CopyOutlined, DeleteOutlined, ExpandOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, Space } from 'antd';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Graph, Node } from '@antv/x6';
|
||||
import type { PortMetadata } from '@antv/x6/lib/model/port';
|
||||
import Editor from '../../Editor';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
|
||||
interface CategoryListProps {
|
||||
parentName: string;
|
||||
options: Suggestion[];
|
||||
selectedNode?: Node | null;
|
||||
graphRef?: React.MutableRefObject<Graph | undefined>;
|
||||
}
|
||||
|
||||
const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRef }) => {
|
||||
const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRef, options }) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
const formValues = Form.useWatch([parentName], form);
|
||||
@@ -167,9 +169,9 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
|
||||
name={[name, 'class_name']}
|
||||
noStyle
|
||||
>
|
||||
<Input.TextArea
|
||||
<Editor
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
rows={2}
|
||||
options={options}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, Row, Col, InputNumber, Radio, type SelectProps } from 'antd'
|
||||
import { Form, Button, Select, Row, Col, InputNumber, Radio, Input, type SelectProps } from 'antd'
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
@@ -114,7 +114,7 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
<Col span={14}>
|
||||
<Form.Item name={[field.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
options={options}
|
||||
options={options.filter(vo => vo.value.includes('sys.') || vo.value.includes('conv.') || vo.nodeData.type === 'loop')}
|
||||
size="small"
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
@@ -186,7 +186,7 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
: <Editor options={options} />
|
||||
: <Input placeholder={t('common.pleaseEnter')} />
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, Row, Col, Input } from 'antd'
|
||||
import { Form, Select, Row, Col, Input } from 'antd'
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
@@ -36,7 +36,6 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
value = [],
|
||||
options,
|
||||
parentName,
|
||||
onChange,
|
||||
selectedNode,
|
||||
graphRef
|
||||
}) => {
|
||||
@@ -139,12 +138,17 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
<Form.Item name={[name, 'value']} noStyle>
|
||||
{currentInputType === 'variable' ? (
|
||||
<VariableSelect
|
||||
placeholder="选择变量"
|
||||
options={availableOptions}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={availableOptions.filter(option => {
|
||||
const currentType = value?.[index]?.type;
|
||||
if (!currentType) return true;
|
||||
|
||||
return option.dataType === currentType
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<Input.TextArea
|
||||
placeholder="输入值"
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
rows={3}
|
||||
className="rb:w-full"
|
||||
/>
|
||||
|
||||
@@ -18,8 +18,22 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
isCanAdd = false
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
const value = form.getFieldValue(name) || [];
|
||||
|
||||
console.log('GroupVariableList', value)
|
||||
|
||||
if (!isCanAdd) {
|
||||
// Filter options based on first variable's dataType if value exists
|
||||
let filteredOptions = options;
|
||||
if (value.length > 0) {
|
||||
const firstVariableValue = value[0];
|
||||
const firstVariable = options.find(opt => `{{${opt.value}}}` === firstVariableValue);
|
||||
if (firstVariable) {
|
||||
filteredOptions = options.filter(opt => opt.dataType === firstVariable.dataType);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:mb-4">
|
||||
<Row gutter={12} className="rb:mb-2!">
|
||||
@@ -38,7 +52,7 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
options={filteredOptions}
|
||||
mode="multiple"
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -77,7 +91,18 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
options={(() => {
|
||||
const currentGroupValue = value[name]?.value || [];
|
||||
if (currentGroupValue.length > 0) {
|
||||
const firstVariableValue = currentGroupValue[0];
|
||||
const firstVariable = options.find(opt => `{{${opt.value}}}` === firstVariableValue);
|
||||
if (firstVariable) {
|
||||
return options.filter(opt => opt.dataType === firstVariable.dataType);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
})()
|
||||
}
|
||||
mode="multiple"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -90,7 +90,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Form.Item name="url">
|
||||
<Editor options={options} variant="outlined" />
|
||||
<Editor options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')} variant="outlined" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -144,7 +144,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
<Form.Item name={['body', 'data']} noStyle>
|
||||
<EditableTable
|
||||
parentName={['body', 'data']}
|
||||
options={options}
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
filterBooleanType={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -154,7 +154,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
<MessageEditor
|
||||
key="json"
|
||||
parentName={['body', 'data']}
|
||||
options={options}
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
isArray={false}
|
||||
title="JSON"
|
||||
/>
|
||||
|
||||
@@ -91,6 +91,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
showSearch
|
||||
allowClear={allowClear}
|
||||
filterOption={(input, option) => {
|
||||
if (input === '/') return true;
|
||||
if (option?.options) {
|
||||
return option.label?.toLowerCase().includes(input.toLowerCase()) ||
|
||||
option.options.some((opt: any) =>
|
||||
|
||||
@@ -22,6 +22,7 @@ import ConditionList from './ConditionList'
|
||||
import CycleVarsList from './CycleVarsList'
|
||||
import AssignmentList from './AssignmentList'
|
||||
import ToolConfig from './ToolConfig'
|
||||
// import { calculateVariableList } from './utils/variableListCalculator'
|
||||
|
||||
interface PropertiesProps {
|
||||
selectedNode?: Node | null;
|
||||
@@ -338,112 +339,35 @@ const Properties: FC<PropertiesProps> = ({
|
||||
const parentLoopNode = getParentLoopNode(selectedNode.id);
|
||||
|
||||
console.log('childNodeIds', selectedNode, childNodeIds)
|
||||
const allRelevantNodeIds = [...allPreviousNodeIds, ...childNodeIds];
|
||||
let allRelevantNodeIds = [...allPreviousNodeIds, ...childNodeIds];
|
||||
|
||||
// Add parent loop/iteration node variables if current node is a child
|
||||
// Add variables from nodes preceding the parent loop/iteration node if current node is a child
|
||||
if (parentLoopNode) {
|
||||
const parentData = parentLoopNode.getData();
|
||||
const parentNodeId = parentLoopNode.getData().id;
|
||||
|
||||
if (parentData.type === 'loop') {
|
||||
const cycleVars = parentData.cycle_vars || [];
|
||||
cycleVars.forEach((cycleVar: any) => {
|
||||
const key = `${parentNodeId}_cycle_${cycleVar.name}`;
|
||||
if (!addedKeys.has(key)) {
|
||||
addedKeys.add(key);
|
||||
variableList.push({
|
||||
key,
|
||||
label: cycleVar.name,
|
||||
type: 'variable',
|
||||
dataType: cycleVar.type || 'String',
|
||||
value: `${parentNodeId}.${cycleVar.name}`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (parentData.type === 'iteration') {
|
||||
// Add item and index variables for iteration parent
|
||||
const itemKey = `${parentNodeId}_item`;
|
||||
const indexKey = `${parentNodeId}_index`;
|
||||
|
||||
if (!addedKeys.has(itemKey)) {
|
||||
addedKeys.add(itemKey);
|
||||
variableList.push({
|
||||
key: itemKey,
|
||||
label: 'item',
|
||||
type: 'variable',
|
||||
dataType: 'Object',
|
||||
value: `${parentNodeId}.item`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
}
|
||||
|
||||
if (!addedKeys.has(indexKey)) {
|
||||
addedKeys.add(indexKey);
|
||||
variableList.push({
|
||||
key: indexKey,
|
||||
label: 'index',
|
||||
type: 'variable',
|
||||
dataType: 'Number',
|
||||
value: `${parentNodeId}.index`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if parent loop/iteration is connected to http-request via ERROR connection
|
||||
if (parentData.type === 'loop' || parentData.type === 'iteration') {
|
||||
const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id);
|
||||
parentPreviousNodeIds.forEach(prevNodeId => {
|
||||
const prevNode = nodes.find(n => n.id === prevNodeId);
|
||||
if (!prevNode) return;
|
||||
|
||||
const prevNodeData = prevNode.getData();
|
||||
if (prevNodeData.type === 'http-request') {
|
||||
// Check if connected via ERROR connection point
|
||||
const errorEdges = edges.filter(edge => {
|
||||
return edge.getTargetCellId() === parentLoopNode.id &&
|
||||
edge.getSourceCellId() === prevNodeId &&
|
||||
edge.getSourcePortId() === 'ERROR'
|
||||
});
|
||||
|
||||
if (errorEdges.length > 0) {
|
||||
const errorMessageKey = `${prevNodeData.id}_error_message`;
|
||||
const errorTypeKey = `${prevNodeData.id}_error_type`;
|
||||
|
||||
if (!addedKeys.has(errorMessageKey)) {
|
||||
addedKeys.add(errorMessageKey);
|
||||
variableList.push({
|
||||
key: errorMessageKey,
|
||||
label: 'error_message',
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
value: `${prevNodeData.id}.error_message`,
|
||||
nodeData: prevNodeData,
|
||||
});
|
||||
}
|
||||
|
||||
if (!addedKeys.has(errorTypeKey)) {
|
||||
addedKeys.add(errorTypeKey);
|
||||
variableList.push({
|
||||
key: errorTypeKey,
|
||||
label: 'error_type',
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
value: `${prevNodeData.id}.error_type`,
|
||||
nodeData: prevNodeData,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add variables from nodes preceding the parent loop/iteration node
|
||||
const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id);
|
||||
allRelevantNodeIds.push(...parentPreviousNodeIds);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Add conversation variables from global config
|
||||
const conversationVariables = workflowConfig?.variables || [];
|
||||
|
||||
conversationVariables.forEach((variable: any) => {
|
||||
const key = `CONVERSATION_${variable.name}`;
|
||||
if (!addedKeys.has(key)) {
|
||||
addedKeys.add(key);
|
||||
variableList.push({
|
||||
key,
|
||||
label: variable.name,
|
||||
type: 'variable',
|
||||
dataType: variable.type,
|
||||
value: `conv.${variable.name}`,
|
||||
nodeData: { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' },
|
||||
group: 'CONVERSATION'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
allRelevantNodeIds.forEach(nodeId => {
|
||||
const node = nodes.find(n => n.id === nodeId);
|
||||
if (!node) return;
|
||||
@@ -496,7 +420,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
key: llmKey,
|
||||
label: 'output',
|
||||
type: 'variable',
|
||||
dataType: 'String',
|
||||
dataType: 'string',
|
||||
value: `${dataNodeId}.output`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
@@ -565,6 +489,17 @@ const Properties: FC<PropertiesProps> = ({
|
||||
const groupVariables = nodeData.config.group_variables.defaultValue || [];
|
||||
groupVariables?.forEach((groupVar: any) => {
|
||||
if (!groupVar || !groupVar.key) return;
|
||||
|
||||
// Determine dataType from first variable in the group
|
||||
let groupDataType = 'string';
|
||||
if (groupVar.value && Array.isArray(groupVar.value) && groupVar.value.length > 0) {
|
||||
const firstVariableValue = groupVar.value[0];
|
||||
const firstVariable = variableList.find(v => `{{${v.value}}}` === firstVariableValue);
|
||||
if (firstVariable) {
|
||||
groupDataType = firstVariable.dataType;
|
||||
}
|
||||
}
|
||||
|
||||
const groupVarKey = `${dataNodeId}_${groupVar.key}`;
|
||||
if (!addedKeys.has(groupVarKey)) {
|
||||
addedKeys.add(groupVarKey);
|
||||
@@ -572,14 +507,26 @@ const Properties: FC<PropertiesProps> = ({
|
||||
key: groupVarKey,
|
||||
label: groupVar.key,
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
dataType: groupDataType,
|
||||
value: `${dataNodeId}.${groupVar.key}`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If group=false, add output variable
|
||||
// If group=false, add output variable with type from first group_variable
|
||||
const groupVariables = nodeData.config.group_variables.defaultValue || [];
|
||||
const firstVariable = groupVariables[0];
|
||||
let outputDataType: string = 'any';
|
||||
if (firstVariable) {
|
||||
const filterVo = [...variableList].find(v => {
|
||||
return `{{${v.value}}}` === firstVariable
|
||||
})
|
||||
if (filterVo) {
|
||||
outputDataType = filterVo?.dataType
|
||||
}
|
||||
}
|
||||
|
||||
const varAggregatorKey = `${dataNodeId}_output`;
|
||||
if (!addedKeys.has(varAggregatorKey)) {
|
||||
addedKeys.add(varAggregatorKey);
|
||||
@@ -587,7 +534,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
key: varAggregatorKey,
|
||||
label: 'output',
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
dataType: outputDataType,
|
||||
value: `${dataNodeId}.output`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
@@ -684,21 +631,20 @@ const Properties: FC<PropertiesProps> = ({
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
if (!addedKeys.has(outputKey)) {
|
||||
addedKeys.add(outputKey);
|
||||
variableList.push({
|
||||
key: outputKey,
|
||||
label: 'output',
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
value: `${dataNodeId}.output`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
// if (!addedKeys.has(outputKey)) {
|
||||
// addedKeys.add(outputKey);
|
||||
// variableList.push({
|
||||
// key: outputKey,
|
||||
// label: 'output',
|
||||
// type: 'variable',
|
||||
// dataType: 'string',
|
||||
// value: `${dataNodeId}.output`,
|
||||
// nodeData: nodeData,
|
||||
// });
|
||||
// }
|
||||
break
|
||||
case 'iteration':
|
||||
const iterationOutputKey = `${dataNodeId}_output`;
|
||||
const iterationItemKey = `${dataNodeId}_item`;
|
||||
if (!addedKeys.has(iterationOutputKey)) {
|
||||
addedKeys.add(iterationOutputKey);
|
||||
// Get the data type from the output configuration, default to string
|
||||
@@ -715,22 +661,11 @@ const Properties: FC<PropertiesProps> = ({
|
||||
key: iterationOutputKey,
|
||||
label: 'output',
|
||||
type: 'variable',
|
||||
dataType: outputDataType,
|
||||
dataType: `array[${outputDataType}]`,
|
||||
value: `${dataNodeId}.output`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
if (!addedKeys.has(iterationItemKey)) {
|
||||
addedKeys.add(iterationItemKey);
|
||||
variableList.push({
|
||||
key: iterationItemKey,
|
||||
label: 'item',
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
value: `${dataNodeId}.item`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
break
|
||||
case 'loop':
|
||||
const cycleVars = nodeData.config.cycle_vars.defaultValue || [];
|
||||
@@ -760,47 +695,337 @@ const Properties: FC<PropertiesProps> = ({
|
||||
key: toolDataKey,
|
||||
label: 'data',
|
||||
type: 'variable',
|
||||
dataType: 'object',
|
||||
dataType: 'string',
|
||||
value: `${dataNodeId}.data`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
break
|
||||
case 'memory-read':
|
||||
const memoryReadAnswerKey = `${dataNodeId}_answer`;
|
||||
const memoryReadIntermediateOutputs = `${dataNodeId}_intermediate_outputs`;
|
||||
if (!addedKeys.has(memoryReadAnswerKey)) {
|
||||
addedKeys.add(memoryReadAnswerKey);
|
||||
variableList.push({
|
||||
key: memoryReadAnswerKey,
|
||||
label: 'answer',
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
value: `${dataNodeId}.answer`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
if (!addedKeys.has(memoryReadIntermediateOutputs)) {
|
||||
addedKeys.add(memoryReadIntermediateOutputs);
|
||||
variableList.push({
|
||||
key: memoryReadIntermediateOutputs,
|
||||
label: 'intermediate_outputs',
|
||||
type: 'variable',
|
||||
dataType: 'array[object]',
|
||||
value: `${dataNodeId}.intermediate_outputs`,
|
||||
nodeData: nodeData,
|
||||
});
|
||||
}
|
||||
break
|
||||
}
|
||||
});
|
||||
|
||||
// Add conversation variables from global config
|
||||
const conversationVariables = workflowConfig?.variables || [];
|
||||
|
||||
conversationVariables.forEach((variable: any) => {
|
||||
const key = `CONVERSATION_${variable.name}`;
|
||||
if (!addedKeys.has(key)) {
|
||||
addedKeys.add(key);
|
||||
variableList.push({
|
||||
key,
|
||||
label: variable.name,
|
||||
type: 'variable',
|
||||
dataType: variable.type,
|
||||
value: `conv.${variable.name}`,
|
||||
nodeData: { type: 'CONVERSATION', name: 'CONVERSATION', icon: '' },
|
||||
group: 'CONVERSATION'
|
||||
|
||||
// Add parent loop/iteration node variables if current node is a child
|
||||
if (parentLoopNode) {
|
||||
const parentData = parentLoopNode.getData();
|
||||
const parentNodeId = parentLoopNode.getData().id;
|
||||
|
||||
if (parentData.type === 'loop') {
|
||||
const cycleVars = parentData.cycle_vars || [];
|
||||
cycleVars.forEach((cycleVar: any) => {
|
||||
const key = `${parentNodeId}_cycle_${cycleVar.name}`;
|
||||
if (!addedKeys.has(key)) {
|
||||
addedKeys.add(key);
|
||||
variableList.push({
|
||||
key,
|
||||
label: cycleVar.name,
|
||||
type: 'variable',
|
||||
dataType: cycleVar.type || 'String',
|
||||
value: `${parentNodeId}.${cycleVar.name}`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (parentData.type === 'iteration') {
|
||||
// Add item and index variables for iteration parent only if input has value
|
||||
if (parentData.config.input.defaultValue) {
|
||||
const itemKey = `${parentNodeId}_item`;
|
||||
const indexKey = `${parentNodeId}_index`;
|
||||
|
||||
// Determine item dataType from input variable
|
||||
let itemDataType = 'object';
|
||||
const inputVariable = variableList.find(v => `{{${v.value}}}` === parentData.config.input.defaultValue);
|
||||
console.log('itemDataType defaultValue', parentData.config.input.defaultValue, variableList, inputVariable)
|
||||
if (inputVariable && inputVariable.dataType.startsWith('array[')) {
|
||||
itemDataType = inputVariable.dataType.replace(/^array\[(.+)\]$/, '$1');
|
||||
console.log('itemDataType', itemDataType)
|
||||
}
|
||||
|
||||
|
||||
if (!addedKeys.has(itemKey)) {
|
||||
addedKeys.add(itemKey);
|
||||
variableList.push({
|
||||
key: itemKey,
|
||||
label: 'item',
|
||||
type: 'variable',
|
||||
dataType: itemDataType,
|
||||
value: `${parentNodeId}.item`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
}
|
||||
|
||||
if (!addedKeys.has(indexKey)) {
|
||||
addedKeys.add(indexKey);
|
||||
variableList.push({
|
||||
key: indexKey,
|
||||
label: 'index',
|
||||
type: 'variable',
|
||||
dataType: 'number',
|
||||
value: `${parentNodeId}.index`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if parent loop/iteration is connected to http-request via ERROR connection
|
||||
if (parentData.type === 'loop' || parentData.type === 'iteration') {
|
||||
const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id);
|
||||
parentPreviousNodeIds.forEach(prevNodeId => {
|
||||
const prevNode = nodes.find(n => n.id === prevNodeId);
|
||||
if (!prevNode) return;
|
||||
|
||||
const prevNodeData = prevNode.getData();
|
||||
if (prevNodeData.type === 'http-request') {
|
||||
// Check if connected via ERROR connection point
|
||||
const errorEdges = edges.filter(edge => {
|
||||
return edge.getTargetCellId() === parentLoopNode.id &&
|
||||
edge.getSourceCellId() === prevNodeId &&
|
||||
edge.getSourcePortId() === 'ERROR'
|
||||
});
|
||||
|
||||
if (errorEdges.length > 0) {
|
||||
const errorMessageKey = `${prevNodeData.id}_error_message`;
|
||||
const errorTypeKey = `${prevNodeData.id}_error_type`;
|
||||
|
||||
if (!addedKeys.has(errorMessageKey)) {
|
||||
addedKeys.add(errorMessageKey);
|
||||
variableList.push({
|
||||
key: errorMessageKey,
|
||||
label: 'error_message',
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
value: `${prevNodeData.id}.error_message`,
|
||||
nodeData: prevNodeData,
|
||||
});
|
||||
}
|
||||
|
||||
if (!addedKeys.has(errorTypeKey)) {
|
||||
addedKeys.add(errorTypeKey);
|
||||
variableList.push({
|
||||
key: errorTypeKey,
|
||||
label: 'error_type',
|
||||
type: 'variable',
|
||||
dataType: 'string',
|
||||
value: `${prevNodeData.id}.error_type`,
|
||||
nodeData: prevNodeData,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return variableList;
|
||||
}, [selectedNode, graphRef, workflowConfig?.variables]);
|
||||
|
||||
// Filter out boolean type variables for loop and llm nodes
|
||||
const getFilteredVariableList = (nodeType?: string) => {
|
||||
if (nodeType === 'loop' || nodeType === 'llm') {
|
||||
return variableList.filter(variable => variable.dataType !== 'boolean');
|
||||
const getFilteredVariableList = (nodeType?: string, key?: string) => {
|
||||
// Check if current node is a child of iteration node
|
||||
const parentIterationNode = selectedNode ? (() => {
|
||||
const nodes = graphRef.current?.getNodes() || [];
|
||||
const nodeData = selectedNode.getData();
|
||||
const cycle = nodeData?.cycle;
|
||||
|
||||
if (cycle) {
|
||||
const parentNode = nodes.find(n => n.getData().id === cycle);
|
||||
if (parentNode) {
|
||||
const parentData = parentNode.getData();
|
||||
if (parentData?.type === 'iteration') {
|
||||
return parentNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})() : null;
|
||||
|
||||
// Helper function to add parent iteration variables
|
||||
const addParentIterationVars = (filteredList: any[]) => {
|
||||
if (parentIterationNode) {
|
||||
const parentData = parentIterationNode.getData();
|
||||
const parentNodeId = parentData.id;
|
||||
|
||||
if (parentData.config?.input?.defaultValue) {
|
||||
const itemKey = `${parentNodeId}_item`;
|
||||
const indexKey = `${parentNodeId}_index`;
|
||||
|
||||
const existingItemVar = filteredList.find(v => v.key === itemKey);
|
||||
const existingIndexVar = filteredList.find(v => v.key === indexKey);
|
||||
|
||||
if (!existingItemVar) {
|
||||
// Determine item dataType from input variable
|
||||
let itemDataType = 'object';
|
||||
const inputVariable = variableList.find(v => `{{${v.value}}}` === parentData.config.input.defaultValue);
|
||||
if (inputVariable && inputVariable.dataType.startsWith('array[')) {
|
||||
itemDataType = inputVariable.dataType.replace(/^array\[(.+)\]$/, '$1');
|
||||
}
|
||||
|
||||
filteredList.push({
|
||||
key: itemKey,
|
||||
label: 'item',
|
||||
type: 'variable',
|
||||
dataType: itemDataType,
|
||||
value: `${parentNodeId}.item`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
}
|
||||
|
||||
if (!existingIndexVar) {
|
||||
filteredList.push({
|
||||
key: indexKey,
|
||||
label: 'index',
|
||||
type: 'variable',
|
||||
dataType: 'number',
|
||||
value: `${parentNodeId}.index`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return filteredList;
|
||||
};
|
||||
|
||||
if (nodeType === 'llm') {
|
||||
// For LLM nodes that are children of iteration or loop nodes, include parent variables
|
||||
const parentLoopNode = selectedNode ? (() => {
|
||||
const nodes = graphRef.current?.getNodes() || [];
|
||||
const nodeData = selectedNode.getData();
|
||||
const cycle = nodeData?.cycle;
|
||||
|
||||
if (cycle) {
|
||||
const parentNode = nodes.find(n => n.getData().id === cycle);
|
||||
if (parentNode) {
|
||||
const parentData = parentNode.getData();
|
||||
if (parentData?.type === 'loop' || parentData?.type === 'iteration') {
|
||||
return parentNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})() : null;
|
||||
|
||||
let filteredList = variableList.filter(variable => variable.dataType !== 'boolean');
|
||||
|
||||
// If this LLM node is a child of iteration/loop, ensure parent variables are included
|
||||
if (parentLoopNode) {
|
||||
const parentData = parentLoopNode.getData();
|
||||
const parentNodeId = parentData.id;
|
||||
|
||||
// Ensure parent loop/iteration variables are included
|
||||
if (parentData.type === 'loop') {
|
||||
const cycleVars = parentData.cycle_vars || [];
|
||||
cycleVars.forEach((cycleVar: any) => {
|
||||
const key = `${parentNodeId}_cycle_${cycleVar.name}`;
|
||||
const existingVar = filteredList.find(v => v.key === key);
|
||||
if (!existingVar && cycleVar.name && cycleVar.type !== 'boolean') {
|
||||
filteredList.push({
|
||||
key,
|
||||
label: cycleVar.name,
|
||||
type: 'variable',
|
||||
dataType: cycleVar.type || 'String',
|
||||
value: `${parentNodeId}.${cycleVar.name}`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (parentData.type === 'iteration') {
|
||||
// Add item and index variables for iteration parent
|
||||
if (parentData.config?.input?.defaultValue) {
|
||||
const itemKey = `${parentNodeId}_item`;
|
||||
const indexKey = `${parentNodeId}_index`;
|
||||
|
||||
const existingItemVar = filteredList.find(v => v.key === itemKey);
|
||||
const existingIndexVar = filteredList.find(v => v.key === indexKey);
|
||||
|
||||
if (!existingItemVar) {
|
||||
// Determine item dataType from input variable
|
||||
let itemDataType = 'object';
|
||||
const inputVariable = variableList.find(v => `{{${v.value}}}` === parentData.config.input.defaultValue);
|
||||
if (inputVariable && inputVariable.dataType.startsWith('array[')) {
|
||||
itemDataType = inputVariable.dataType.replace(/^array\[(.+)\]$/, '$1');
|
||||
}
|
||||
|
||||
filteredList.push({
|
||||
key: itemKey,
|
||||
label: 'item',
|
||||
type: 'variable',
|
||||
dataType: itemDataType,
|
||||
value: `${parentNodeId}.item`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
}
|
||||
|
||||
if (!existingIndexVar) {
|
||||
filteredList.push({
|
||||
key: indexKey,
|
||||
label: 'index',
|
||||
type: 'variable',
|
||||
dataType: 'Number',
|
||||
value: `${parentNodeId}.index`,
|
||||
nodeData: parentData,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredList;
|
||||
}
|
||||
return variableList;
|
||||
if (nodeType === 'knowledge-retrieval' || nodeType === 'parameter-extractor' && key !== 'prompt' || nodeType === 'memory-read' || nodeType === 'memory-write' || nodeType === 'question-classifier') {
|
||||
let filteredList = variableList.filter(variable => variable.dataType === 'string');
|
||||
return addParentIterationVars(filteredList);
|
||||
}
|
||||
if (nodeType === 'parameter-extractor' && key === 'prompt') {
|
||||
let filteredList = variableList.filter(variable => variable.dataType === 'string' || variable.dataType === 'number');
|
||||
return addParentIterationVars(filteredList);
|
||||
}
|
||||
if (nodeType === 'iteration' && key === 'output') {
|
||||
return variableList.filter(variable => variable.value.includes('sys.'));
|
||||
}
|
||||
if (nodeType === 'iteration') {
|
||||
return variableList.filter(variable => variable.dataType.includes('array'));
|
||||
}
|
||||
if (nodeType === 'loop' && key === 'condition') {
|
||||
let filteredList = variableList.filter(variable => variable.nodeData.type !== 'loop');
|
||||
return addParentIterationVars(filteredList);
|
||||
}
|
||||
|
||||
// For all other node types, add parent iteration variables if applicable
|
||||
let baseList = variableList;
|
||||
return addParentIterationVars(baseList);
|
||||
};
|
||||
|
||||
// const defaultVariableList = calculateVariableList(selectedNode as Node, graphRef, workflowConfig )
|
||||
|
||||
console.log('values', values)
|
||||
console.log('variableList', variableList, selectedNode?.data)
|
||||
// console.log('variableList', variableList, defaultVariableList)
|
||||
|
||||
return (
|
||||
<div className="rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3">
|
||||
@@ -901,11 +1126,10 @@ const Properties: FC<PropertiesProps> = ({
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<MessageEditor
|
||||
key={key}
|
||||
key={key}
|
||||
options={contextVariableList.filter(variable => variable.nodeData?.type !== 'knowledge-retrieval')}
|
||||
parentName={key}
|
||||
/>
|
||||
@@ -915,7 +1139,12 @@ const Properties: FC<PropertiesProps> = ({
|
||||
if (selectedNode?.data?.type === 'end' && key === 'output') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<MessageEditor key={key} isArray={false} parentName={key} options={variableList} />
|
||||
<MessageEditor
|
||||
key={key}
|
||||
isArray={false}
|
||||
parentName={key}
|
||||
options={variableList.filter(variable => variable.nodeData?.type !== 'knowledge-retrieval')}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
@@ -943,7 +1172,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
isArray={!!config.isArray}
|
||||
parentName={key}
|
||||
enableJinja2={config.enableJinja2 as boolean}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type)}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
@@ -964,7 +1193,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
<Form.Item key={key} name={key}>
|
||||
<GroupVariableList
|
||||
name={key}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type)}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
isCanAdd={!!(values as any)?.group}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -976,7 +1205,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
<Form.Item key={key} name={key}>
|
||||
<CaseList
|
||||
name={key}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type)}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
selectedNode={selectedNode}
|
||||
graphRef={graphRef}
|
||||
/>
|
||||
@@ -989,7 +1218,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
<Form.Item key={key} name={key}
|
||||
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
>
|
||||
<MappingList name={key} options={getFilteredVariableList(selectedNode?.data?.type)} />
|
||||
<MappingList name={key} options={getFilteredVariableList(selectedNode?.data?.type, key)} />
|
||||
</Form.Item>
|
||||
|
||||
)
|
||||
@@ -999,7 +1228,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
<Form.Item key={key} name={key}>
|
||||
<CycleVarsList
|
||||
parentName={key}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type)}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
@@ -1013,9 +1242,9 @@ const Properties: FC<PropertiesProps> = ({
|
||||
if (config.filterLoopIterationVars) {
|
||||
const loopIterationVars: Suggestion[] = [];
|
||||
|
||||
return [...getFilteredVariableList(selectedNode?.data?.type), ...loopIterationVars];
|
||||
return [...getFilteredVariableList(selectedNode?.data?.type, key), ...loopIterationVars];
|
||||
}
|
||||
return getFilteredVariableList(selectedNode?.data?.type);
|
||||
return getFilteredVariableList(selectedNode?.data?.type, key);
|
||||
})()
|
||||
}
|
||||
/>
|
||||
@@ -1060,7 +1289,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
? <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={(() => {
|
||||
const baseVariableList = getFilteredVariableList(selectedNode?.data?.type);
|
||||
const baseVariableList = getFilteredVariableList(selectedNode?.data?.type, key);
|
||||
// Apply filtering if specified in config
|
||||
if (config.filterNodeTypes || config.filterVariableNames) {
|
||||
return baseVariableList.filter(variable => {
|
||||
@@ -1068,7 +1297,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
(Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type));
|
||||
const variableNameMatch = !config.filterVariableNames ||
|
||||
(Array.isArray(config.filterVariableNames) && config.filterVariableNames.includes(variable.label));
|
||||
return nodeTypeMatch && variableNameMatch;
|
||||
return nodeTypeMatch || variableNameMatch;
|
||||
});
|
||||
}
|
||||
// Filter child nodes for iteration output
|
||||
@@ -1085,7 +1314,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
});
|
||||
|
||||
return baseVariableList.filter(variable =>
|
||||
childNodes.some(node => node.id === variable.nodeData?.id)
|
||||
childNodes.some(node => node.id === variable.nodeData?.id) || selectedNode?.data?.type === 'iteration' && key === 'output' && variable.value.includes('sys.')
|
||||
);
|
||||
}
|
||||
return baseVariableList;
|
||||
@@ -1095,7 +1324,12 @@ const Properties: FC<PropertiesProps> = ({
|
||||
: config.type === 'switch'
|
||||
? <Switch onChange={key === 'group' ? () => { form.setFieldValue('group_variables', []) } : undefined} />
|
||||
: config.type === 'categoryList'
|
||||
? <CategoryList parentName={key} selectedNode={selectedNode} graphRef={graphRef} />
|
||||
? <CategoryList
|
||||
parentName={key}
|
||||
selectedNode={selectedNode}
|
||||
graphRef={graphRef}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
/>
|
||||
: config.type === 'conditionList'
|
||||
? <ConditionList
|
||||
parentName={key}
|
||||
@@ -1109,18 +1343,9 @@ const Properties: FC<PropertiesProps> = ({
|
||||
value: `${selectedNode.getData().id}.${cycleVar.name}`,
|
||||
nodeData: selectedNode.getData(),
|
||||
}));
|
||||
return [...variableList.filter(variable => {
|
||||
// Keep conversation variables
|
||||
if (variable.group === 'CONVERSATION') return true;
|
||||
// Keep sys variables from start nodes
|
||||
if (variable.nodeData?.type === 'start' && variable.value?.startsWith('sys.')) return true;
|
||||
// Keep variables from non-start nodes
|
||||
if (variable.nodeData?.type !== 'start' && variable.nodeData?.type !== 'http-request' && variable.dataType !== 'boolean') return true;
|
||||
// Filter out custom variables from start nodes
|
||||
return false;
|
||||
}), ...cycleVarSuggestions];
|
||||
})()
|
||||
}
|
||||
|
||||
return [...getFilteredVariableList(selectedNode?.data?.type, key), ...cycleVarSuggestions];
|
||||
})()}
|
||||
selectedNode={selectedNode}
|
||||
graphRef={graphRef}
|
||||
addBtnText={t('workflow.config.addCase')}
|
||||
|
||||
@@ -270,7 +270,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
config: {
|
||||
input: {
|
||||
type: 'variableList',
|
||||
filterNodeTypes: ['knowledge-retrieval'],
|
||||
filterNodeTypes: ['knowledge-retrieval', 'iteration', 'loop'],
|
||||
filterVariableNames: ['message']
|
||||
},
|
||||
parallel: {
|
||||
@@ -334,8 +334,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "assigner", icon: assignerIcon,
|
||||
{ type: "assigner", icon: assignerIcon,
|
||||
config: {
|
||||
assignments: {
|
||||
type: 'assignmentList',
|
||||
@@ -628,4 +627,114 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
|
||||
items: [{ group: 'left' }],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export interface OutputVariable {
|
||||
default?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
define?: string[];
|
||||
sys?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
error?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
}
|
||||
export const outputVariable: { [key: string]: OutputVariable } = {
|
||||
start: {
|
||||
sys: [
|
||||
{ name: "message", type: "string" },
|
||||
{ name: "conversation_id", type: "string" },
|
||||
{ name: "execution_id", type: "string", },
|
||||
{ name: "workspace_id", type: "string" },
|
||||
{ name: "user_id", type: "string" },
|
||||
],
|
||||
define: ['variables']
|
||||
},
|
||||
end: {
|
||||
},
|
||||
llm: {
|
||||
default: [
|
||||
{ name: "output", type: "string" },
|
||||
]
|
||||
},
|
||||
'knowledge-retrieval': {
|
||||
default: [
|
||||
{ name: "output", type: "array[object]" },
|
||||
]
|
||||
},
|
||||
'parameter-extractor': {
|
||||
default: [
|
||||
{ name: "__is_success", type: "number" },
|
||||
{ name: "__reason", type: "string" },
|
||||
],
|
||||
define: ['params']
|
||||
},
|
||||
'memory-read': {
|
||||
default: [
|
||||
{ name: "answer", type: "string" },
|
||||
{ name: "intermediate_outputs", type: "array[object]" },
|
||||
],
|
||||
},
|
||||
'memory-write': {
|
||||
|
||||
},
|
||||
'if-else': {
|
||||
|
||||
},
|
||||
'question-classifier': {
|
||||
default: [
|
||||
{ name: "class_name", type: "string" },
|
||||
// { name: "output", type: "string" },
|
||||
],
|
||||
},
|
||||
'iteration': {
|
||||
default: [
|
||||
// { name: "item", type: "string" }, // 仅内部使用
|
||||
{ name: "output", type: "array[string]" },
|
||||
],
|
||||
},
|
||||
'loop': {
|
||||
define: ['cycle_vars']
|
||||
},
|
||||
'cycle-start': {
|
||||
|
||||
},
|
||||
'break': {
|
||||
|
||||
},
|
||||
'var-aggregator': {
|
||||
// default: [
|
||||
// { name: "output", type: "string" },
|
||||
// ],
|
||||
define: ['group_variables']
|
||||
},
|
||||
'assigner': {
|
||||
|
||||
},
|
||||
'http-request': {
|
||||
default: [
|
||||
{ name: "body", type: "string" },
|
||||
{ name: "status_code", type: "number" },
|
||||
],
|
||||
error: [
|
||||
{ name: "error_message", type: "string" },
|
||||
{ name: "error_type", type: "string" },
|
||||
]
|
||||
},
|
||||
'tool': {
|
||||
default: [
|
||||
{ name: "data", type: "string" },
|
||||
],
|
||||
},
|
||||
'jinja-render': {
|
||||
default: [
|
||||
{ name: "output", type: "string" },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -282,9 +282,21 @@ export const useWorkflowGraph = ({
|
||||
}, 100)
|
||||
}
|
||||
if (edges.length) {
|
||||
// 去重处理:相同节点之间的连线仅连一次
|
||||
// 去重处理:对于if-else和question-classifier节点,不同连接桩允许连接到相同节点
|
||||
const uniqueEdges = edges.filter((edge, index, arr) => {
|
||||
return arr.findIndex(e => e.source === edge.source && e.target === edge.target) === index;
|
||||
return arr.findIndex(e => {
|
||||
const sourceCell = graphRef.current?.getCellById(e.source);
|
||||
const sourceType = sourceCell?.getData()?.type;
|
||||
const isMultiPortNode = sourceType === 'question-classifier' || sourceType === 'if-else';
|
||||
|
||||
if (isMultiPortNode) {
|
||||
// 多端口节点需要同时比较source、target和label
|
||||
return e.source === edge.source && e.target === edge.target && e.label === edge.label;
|
||||
} else {
|
||||
// 其他节点只比较source和target
|
||||
return e.source === edge.source && e.target === edge.target;
|
||||
}
|
||||
}) === index;
|
||||
});
|
||||
|
||||
const edgeList = uniqueEdges.map(edge => {
|
||||
@@ -954,7 +966,10 @@ export const useWorkflowGraph = ({
|
||||
itemConfig = {
|
||||
...itemConfig,
|
||||
...data.config[key].defaultValue,
|
||||
knowledge_bases: knowledge_bases?.map((vo: any) => ({ kb_id: vo.id, ...vo.config }))
|
||||
knowledge_bases: knowledge_bases?.map((vo: any) => {
|
||||
const kb_config = vo.config || { similarity_threshold: vo.similarity_threshold, strategy: vo.strategy, top_k: vo.top_k, weight: vo.weight }
|
||||
return { kb_id: vo.kb_id || vo.id, ...kb_config, }
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1025,8 +1040,21 @@ export const useWorkflowGraph = ({
|
||||
})
|
||||
.filter(edge => edge !== null)
|
||||
.filter((edge, index, arr) => {
|
||||
// 去重:相同节点之间的连线仅保留一次
|
||||
return arr.findIndex(e => e && e.source === edge?.source && e.target === edge?.target) === index;
|
||||
// 去重:对于if-else和question-classifier节点,不同连接桩允许连接到相同节点
|
||||
return arr.findIndex(e => {
|
||||
if (!e || !edge) return false;
|
||||
const sourceCell = graphRef.current?.getCellById(e.source);
|
||||
const sourceType = sourceCell?.getData()?.type;
|
||||
const isMultiPortNode = sourceType === 'question-classifier' || sourceType === 'if-else';
|
||||
|
||||
if (isMultiPortNode) {
|
||||
// 多端口节点需要同时比较source、target和label
|
||||
return e.source === edge.source && e.target === edge.target && e.label === edge.label;
|
||||
} else {
|
||||
// 其他节点只比较source和target
|
||||
return e.source === edge.source && e.target === edge.target;
|
||||
}
|
||||
}) === index;
|
||||
}),
|
||||
}
|
||||
saveWorkflowConfig(config.app_id, params as WorkflowConfig)
|
||||
|
||||
Reference in New Issue
Block a user