Merge branch 'refs/heads/release/v0.2.3' into fix/release_memory_bug
# Conflicts: # api/app/core/memory/agent/langgraph_graph/write_graph.py
This commit is contained in:
@@ -3,9 +3,14 @@ import platform
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from app.core.config import settings
|
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
# macOS fork() safety - must be set before any Celery initialization
|
||||||
|
if platform.system() == 'Darwin':
|
||||||
|
os.environ.setdefault('OBJC_DISABLE_INITIALIZE_FORK_SAFETY', 'YES')
|
||||||
|
|
||||||
# 创建 Celery 应用实例
|
# 创建 Celery 应用实例
|
||||||
# broker: 任务队列(使用 Redis DB 0)
|
# broker: 任务队列(使用 Redis DB 0)
|
||||||
# backend: 结果存储(使用 Redis DB 10)
|
# backend: 结果存储(使用 Redis DB 10)
|
||||||
@@ -63,6 +68,11 @@ celery_app.conf.update(
|
|||||||
'app.core.memory.agent.read_message': {'queue': 'memory_tasks'},
|
'app.core.memory.agent.read_message': {'queue': 'memory_tasks'},
|
||||||
'app.core.memory.agent.write_message': {'queue': 'memory_tasks'},
|
'app.core.memory.agent.write_message': {'queue': 'memory_tasks'},
|
||||||
|
|
||||||
|
# Long-term storage tasks → memory_tasks queue (batched write strategies)
|
||||||
|
'app.core.memory.agent.long_term_storage.window': {'queue': 'memory_tasks'},
|
||||||
|
'app.core.memory.agent.long_term_storage.time': {'queue': 'memory_tasks'},
|
||||||
|
'app.core.memory.agent.long_term_storage.aggregate': {'queue': 'memory_tasks'},
|
||||||
|
|
||||||
# Document tasks → document_tasks queue (prefork worker)
|
# Document tasks → document_tasks queue (prefork worker)
|
||||||
'app.core.rag.tasks.parse_document': {'queue': 'document_tasks'},
|
'app.core.rag.tasks.parse_document': {'queue': 'document_tasks'},
|
||||||
'app.core.rag.tasks.build_graphrag_for_kb': {'queue': 'document_tasks'},
|
'app.core.rag.tasks.build_graphrag_for_kb': {'queue': 'document_tasks'},
|
||||||
@@ -79,40 +89,40 @@ celery_app.conf.update(
|
|||||||
celery_app.autodiscover_tasks(['app'])
|
celery_app.autodiscover_tasks(['app'])
|
||||||
|
|
||||||
# Celery Beat schedule for periodic tasks
|
# Celery Beat schedule for periodic tasks
|
||||||
memory_increment_schedule = timedelta(hours=settings.MEMORY_INCREMENT_INTERVAL_HOURS)
|
# memory_increment_schedule = timedelta(hours=settings.MEMORY_INCREMENT_INTERVAL_HOURS)
|
||||||
memory_cache_regeneration_schedule = timedelta(hours=settings.MEMORY_CACHE_REGENERATION_HOURS)
|
# memory_cache_regeneration_schedule = timedelta(hours=settings.MEMORY_CACHE_REGENERATION_HOURS)
|
||||||
workspace_reflection_schedule = timedelta(seconds=30) # 每30秒运行一次settings.REFLECTION_INTERVAL_TIME
|
# workspace_reflection_schedule = timedelta(seconds=30) # 每30秒运行一次settings.REFLECTION_INTERVAL_TIME
|
||||||
forgetting_cycle_schedule = timedelta(hours=24) # 每24小时运行一次遗忘周期
|
# forgetting_cycle_schedule = timedelta(hours=24) # 每24小时运行一次遗忘周期
|
||||||
|
|
||||||
# 构建定时任务配置
|
# 构建定时任务配置
|
||||||
beat_schedule_config = {
|
# beat_schedule_config = {
|
||||||
"run-workspace-reflection": {
|
# "run-workspace-reflection": {
|
||||||
"task": "app.tasks.workspace_reflection_task",
|
# "task": "app.tasks.workspace_reflection_task",
|
||||||
"schedule": workspace_reflection_schedule,
|
# "schedule": workspace_reflection_schedule,
|
||||||
"args": (),
|
# "args": (),
|
||||||
},
|
# },
|
||||||
"regenerate-memory-cache": {
|
# "regenerate-memory-cache": {
|
||||||
"task": "app.tasks.regenerate_memory_cache",
|
# "task": "app.tasks.regenerate_memory_cache",
|
||||||
"schedule": memory_cache_regeneration_schedule,
|
# "schedule": memory_cache_regeneration_schedule,
|
||||||
"args": (),
|
# "args": (),
|
||||||
},
|
# },
|
||||||
"run-forgetting-cycle": {
|
# "run-forgetting-cycle": {
|
||||||
"task": "app.tasks.run_forgetting_cycle_task",
|
# "task": "app.tasks.run_forgetting_cycle_task",
|
||||||
"schedule": forgetting_cycle_schedule,
|
# "schedule": forgetting_cycle_schedule,
|
||||||
"kwargs": {
|
# "kwargs": {
|
||||||
"config_id": None, # 使用默认配置,可以通过环境变量配置
|
# "config_id": None, # 使用默认配置,可以通过环境变量配置
|
||||||
},
|
# },
|
||||||
},
|
# },
|
||||||
}
|
# }
|
||||||
|
|
||||||
# 如果配置了默认工作空间ID,则添加记忆总量统计任务
|
# 如果配置了默认工作空间ID,则添加记忆总量统计任务
|
||||||
if settings.DEFAULT_WORKSPACE_ID:
|
# if settings.DEFAULT_WORKSPACE_ID:
|
||||||
beat_schedule_config["write-total-memory"] = {
|
# beat_schedule_config["write-total-memory"] = {
|
||||||
"task": "app.controllers.memory_storage_controller.search_all",
|
# "task": "app.controllers.memory_storage_controller.search_all",
|
||||||
"schedule": memory_increment_schedule,
|
# "schedule": memory_increment_schedule,
|
||||||
"kwargs": {
|
# "kwargs": {
|
||||||
"workspace_id": settings.DEFAULT_WORKSPACE_ID,
|
# "workspace_id": settings.DEFAULT_WORKSPACE_ID,
|
||||||
},
|
# },
|
||||||
}
|
# }
|
||||||
|
|
||||||
celery_app.conf.beat_schedule = beat_schedule_config
|
# celery_app.conf.beat_schedule = beat_schedule_config
|
||||||
|
|||||||
@@ -182,14 +182,6 @@ def _get_ontology_service(
|
|||||||
detail=f"找不到指定的LLM模型: {llm_id}"
|
detail=f"找不到指定的LLM模型: {llm_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 检查是否为组合模型
|
|
||||||
if hasattr(model_config, 'is_composite') and model_config.is_composite:
|
|
||||||
logger.error(f"Model {llm_id} is a composite model, which is not supported for ontology extraction")
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="本体提取不支持使用组合模型,请选择单个模型"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 验证模型配置了API密钥
|
# 验证模型配置了API密钥
|
||||||
if not model_config.api_keys:
|
if not model_config.api_keys:
|
||||||
logger.error(f"Model {llm_id} has no API key configuration")
|
logger.error(f"Model {llm_id} has no API key configuration")
|
||||||
|
|||||||
@@ -148,8 +148,10 @@ class LangChainAgent:
|
|||||||
messages.append(HumanMessage(content=user_content))
|
messages.append(HumanMessage(content=user_content))
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
# TODO: 移到memory module
|
||||||
async def term_memory_save(self,long_term_messages,actual_config_id,end_user_id,type):
|
async def term_memory_save(self,long_term_messages,actual_config_id,end_user_id,type):
|
||||||
db = next(get_db())
|
db = next(get_db())
|
||||||
|
#TODO: 魔法数字
|
||||||
scope=6
|
scope=6
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -159,6 +161,12 @@ class LangChainAgent:
|
|||||||
|
|
||||||
from app.core.memory.agent.utils.redis_tool import write_store
|
from app.core.memory.agent.utils.redis_tool import write_store
|
||||||
result = write_store.get_session_by_userid(end_user_id)
|
result = write_store.get_session_by_userid(end_user_id)
|
||||||
|
|
||||||
|
# Handle case where no session exists in Redis (returns False)
|
||||||
|
if not result or result is False:
|
||||||
|
logger.debug(f"No existing session in Redis for user {end_user_id}, skipping short-term memory update")
|
||||||
|
return
|
||||||
|
|
||||||
if type=="chunk" or type=="aggregate":
|
if type=="chunk" or type=="aggregate":
|
||||||
data = await format_parsing(result, "dict")
|
data = await format_parsing(result, "dict")
|
||||||
chunk_data = data[:scope]
|
chunk_data = data[:scope]
|
||||||
@@ -166,7 +174,14 @@ class LangChainAgent:
|
|||||||
repo.upsert(end_user_id, chunk_data)
|
repo.upsert(end_user_id, chunk_data)
|
||||||
logger.info(f'写入短长期:')
|
logger.info(f'写入短长期:')
|
||||||
else:
|
else:
|
||||||
|
# TODO: This branch handles type="time" strategy, currently unused.
|
||||||
|
# Will be activated when time-based long-term storage is implemented.
|
||||||
|
# TODO: 魔法数字 - extract 5 to a constant
|
||||||
long_time_data = write_store.find_user_recent_sessions(end_user_id, 5)
|
long_time_data = write_store.find_user_recent_sessions(end_user_id, 5)
|
||||||
|
# Handle case where no session exists in Redis (returns False or empty)
|
||||||
|
if not long_time_data or long_time_data is False:
|
||||||
|
logger.debug(f"No recent sessions in Redis for user {end_user_id}")
|
||||||
|
return
|
||||||
long_messages = await messages_parse(long_time_data)
|
long_messages = await messages_parse(long_time_data)
|
||||||
repo.upsert(end_user_id, long_messages)
|
repo.upsert(end_user_id, long_messages)
|
||||||
logger.info(f'写入短长期:')
|
logger.info(f'写入短长期:')
|
||||||
@@ -307,9 +322,12 @@ class LangChainAgent:
|
|||||||
elapsed_time = time.time() - start_time
|
elapsed_time = time.time() - start_time
|
||||||
if memory_flag:
|
if memory_flag:
|
||||||
long_term_messages=await agent_chat_messages(message_chat,content)
|
long_term_messages=await agent_chat_messages(message_chat,content)
|
||||||
# AI 回复写入(用户消息和 AI 回复配对,一次性写入完整对话)
|
# TODO: DUPLICATE WRITE - Remove this immediate write once batched write (term_memory_save) is verified stable.
|
||||||
|
# This writes to Neo4j immediately via Celery task, but term_memory_save also writes to Neo4j
|
||||||
|
# when the window buffer reaches scope (6 messages). This causes duplicate entities in the graph.
|
||||||
|
# Recommended: Keep only term_memory_save for batched efficiency, or only self.write for real-time.
|
||||||
await self.write(storage_type, actual_end_user_id, message_chat, content, user_rag_memory_id, actual_end_user_id, actual_config_id)
|
await self.write(storage_type, actual_end_user_id, message_chat, content, user_rag_memory_id, actual_end_user_id, actual_config_id)
|
||||||
'''长期'''
|
# Batched long-term memory storage (Redis buffer + Neo4j when window full)
|
||||||
await self.term_memory_save(long_term_messages,actual_config_id,end_user_id,"chunk")
|
await self.term_memory_save(long_term_messages,actual_config_id,end_user_id,"chunk")
|
||||||
response = {
|
response = {
|
||||||
"content": content,
|
"content": content,
|
||||||
@@ -441,9 +459,13 @@ class LangChainAgent:
|
|||||||
yield total_tokens
|
yield total_tokens
|
||||||
break
|
break
|
||||||
if memory_flag:
|
if memory_flag:
|
||||||
# AI 回复写入(用户消息和 AI 回复配对,一次性写入完整对话)
|
# TODO: DUPLICATE WRITE - Remove this immediate write once batched write (term_memory_save) is verified stable.
|
||||||
|
# This writes to Neo4j immediately via Celery task, but term_memory_save also writes to Neo4j
|
||||||
|
# when the window buffer reaches scope (6 messages). This causes duplicate entities in the graph.
|
||||||
|
# Recommended: Keep only term_memory_save for batched efficiency, or only self.write for real-time.
|
||||||
long_term_messages = await agent_chat_messages(message_chat, full_content)
|
long_term_messages = await agent_chat_messages(message_chat, full_content)
|
||||||
await self.write(storage_type, end_user_id, message_chat, full_content, user_rag_memory_id, end_user_id, actual_config_id)
|
await self.write(storage_type, end_user_id, message_chat, full_content, user_rag_memory_id, end_user_id, actual_config_id)
|
||||||
|
# Batched long-term memory storage (Redis buffer + Neo4j when window full)
|
||||||
await self.term_memory_save(long_term_messages, actual_config_id, end_user_id, "chunk")
|
await self.term_memory_save(long_term_messages, actual_config_id, end_user_id, "chunk")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ async def write_messages(end_user_id,langchain_messages,memory_config):
|
|||||||
for node_name, node_data in update_event.items():
|
for node_name, node_data in update_event.items():
|
||||||
if 'save_neo4j' == node_name:
|
if 'save_neo4j' == node_name:
|
||||||
massages = node_data
|
massages = node_data
|
||||||
|
# TODO:删除
|
||||||
massagesstatus = massages.get('write_result')['status']
|
massagesstatus = massages.get('write_result')['status']
|
||||||
contents = massages.get('write_result')
|
contents = massages.get('write_result')
|
||||||
print(contents)
|
print(contents)
|
||||||
@@ -60,6 +61,7 @@ async def window_dialogue(end_user_id,langchain_messages,memory_config,scope):
|
|||||||
scope:窗口大小
|
scope:窗口大小
|
||||||
'''
|
'''
|
||||||
scope=scope
|
scope=scope
|
||||||
|
redis_messages = []
|
||||||
is_end_user_id = count_store.get_sessions_count(end_user_id)
|
is_end_user_id = count_store.get_sessions_count(end_user_id)
|
||||||
if is_end_user_id is not False:
|
if is_end_user_id is not False:
|
||||||
is_end_user_id = count_store.get_sessions_count(end_user_id)[0]
|
is_end_user_id = count_store.get_sessions_count(end_user_id)[0]
|
||||||
@@ -91,6 +93,9 @@ async def memory_long_term_storage(end_user_id,memory_config,time):
|
|||||||
memory_config: 内存配置对象
|
memory_config: 内存配置对象
|
||||||
'''
|
'''
|
||||||
long_time_data = write_store.find_user_recent_sessions(end_user_id, time)
|
long_time_data = write_store.find_user_recent_sessions(end_user_id, time)
|
||||||
|
# Handle case where no session exists in Redis (returns False or empty)
|
||||||
|
if not long_time_data or long_time_data is False:
|
||||||
|
return
|
||||||
format_messages = await chat_data_format(long_time_data)
|
format_messages = await chat_data_format(long_time_data)
|
||||||
if format_messages!=[]:
|
if format_messages!=[]:
|
||||||
await write_messages(end_user_id, format_messages, memory_config)
|
await write_messages(end_user_id, format_messages, memory_config)
|
||||||
@@ -108,8 +113,9 @@ async def aggregate_judgment(end_user_id: str, ori_messages: list, memory_config
|
|||||||
try:
|
try:
|
||||||
# 1. 获取历史会话数据(使用新方法)
|
# 1. 获取历史会话数据(使用新方法)
|
||||||
result = write_store.get_all_sessions_by_end_user_id(end_user_id)
|
result = write_store.get_all_sessions_by_end_user_id(end_user_id)
|
||||||
history = await format_parsing(result)
|
|
||||||
if not result:
|
# Handle case where no session exists in Redis (returns False or empty)
|
||||||
|
if not result or result is False:
|
||||||
history = []
|
history = []
|
||||||
else:
|
else:
|
||||||
history = await format_parsing(result)
|
history = await format_parsing(result)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class CodeNodeConfig(BaseNodeConfig):
|
|||||||
description="code content"
|
description="code content"
|
||||||
)
|
)
|
||||||
|
|
||||||
language: Literal['python3', 'nodejs'] = Field(
|
language: Literal['python3', 'javascript'] = Field(
|
||||||
...,
|
...,
|
||||||
description="language"
|
description="language"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class CodeNode(BaseNode):
|
|||||||
code=code,
|
code=code,
|
||||||
inputs_variable=input_variable_dict,
|
inputs_variable=input_variable_dict,
|
||||||
)
|
)
|
||||||
elif self.typed_config.language == 'nodejs':
|
elif self.typed_config.language == 'javascript':
|
||||||
final_script = NODEJS_SCRIPT_TEMPLATE.substitute(
|
final_script = NODEJS_SCRIPT_TEMPLATE.substitute(
|
||||||
code=code,
|
code=code,
|
||||||
inputs_variable=input_variable_dict,
|
inputs_variable=input_variable_dict,
|
||||||
|
|||||||
@@ -4,16 +4,19 @@
|
|||||||
从文件系统加载预定义的工作流模板
|
从文件系统加载预定义的工作流模板
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')
|
||||||
|
|
||||||
|
|
||||||
class TemplateLoader:
|
class TemplateLoader:
|
||||||
"""工作流模板加载器"""
|
"""工作流模板加载器"""
|
||||||
|
|
||||||
def __init__(self, templates_dir: str = "app/templates/workflows"):
|
def __init__(self, templates_dir: str = TEMPLATE_DIR):
|
||||||
"""初始化模板加载器
|
"""初始化模板加载器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -22,7 +25,7 @@ class TemplateLoader:
|
|||||||
self.templates_dir = Path(templates_dir)
|
self.templates_dir = Path(templates_dir)
|
||||||
if not self.templates_dir.exists():
|
if not self.templates_dir.exists():
|
||||||
raise ValueError(f"模板目录不存在: {templates_dir}")
|
raise ValueError(f"模板目录不存在: {templates_dir}")
|
||||||
|
|
||||||
def list_templates(self) -> list[dict]:
|
def list_templates(self) -> list[dict]:
|
||||||
"""列出所有可用的模板
|
"""列出所有可用的模板
|
||||||
|
|
||||||
@@ -30,22 +33,22 @@ class TemplateLoader:
|
|||||||
模板列表,每个模板包含 id, name, description 等信息
|
模板列表,每个模板包含 id, name, description 等信息
|
||||||
"""
|
"""
|
||||||
templates = []
|
templates = []
|
||||||
|
|
||||||
# 遍历模板目录
|
# 遍历模板目录
|
||||||
for template_dir in self.templates_dir.iterdir():
|
for template_dir in self.templates_dir.iterdir():
|
||||||
if not template_dir.is_dir():
|
if not template_dir.is_dir():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 检查是否有 template.yml 文件
|
# 检查是否有 template.yml 文件
|
||||||
template_file = template_dir / "template.yml"
|
template_file = template_dir / "template.yml"
|
||||||
if not template_file.exists():
|
if not template_file.exists():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 读取模板配置
|
# 读取模板配置
|
||||||
with open(template_file, 'r', encoding='utf-8') as f:
|
with open(template_file, 'r', encoding='utf-8') as f:
|
||||||
template_data = yaml.safe_load(f)
|
template_data = yaml.safe_load(f)
|
||||||
|
|
||||||
# 提取模板信息
|
# 提取模板信息
|
||||||
templates.append({
|
templates.append({
|
||||||
"id": template_dir.name,
|
"id": template_dir.name,
|
||||||
@@ -59,9 +62,9 @@ class TemplateLoader:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"加载模板 {template_dir.name} 失败: {e}")
|
print(f"加载模板 {template_dir.name} 失败: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return templates
|
return templates
|
||||||
|
|
||||||
def load_template(self, template_id: str) -> Optional[dict]:
|
def load_template(self, template_id: str) -> Optional[dict]:
|
||||||
"""加载指定的模板
|
"""加载指定的模板
|
||||||
|
|
||||||
@@ -73,14 +76,14 @@ class TemplateLoader:
|
|||||||
"""
|
"""
|
||||||
template_dir = self.templates_dir / template_id
|
template_dir = self.templates_dir / template_id
|
||||||
template_file = template_dir / "template.yml"
|
template_file = template_dir / "template.yml"
|
||||||
|
|
||||||
if not template_file.exists():
|
if not template_file.exists():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(template_file, 'r', encoding='utf-8') as f:
|
with open(template_file, 'r', encoding='utf-8') as f:
|
||||||
template_data = yaml.safe_load(f)
|
template_data = yaml.safe_load(f)
|
||||||
|
|
||||||
# 返回工作流配置部分
|
# 返回工作流配置部分
|
||||||
return {
|
return {
|
||||||
"name": template_data.get("name", template_id),
|
"name": template_data.get("name", template_id),
|
||||||
@@ -94,7 +97,7 @@ class TemplateLoader:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"加载模板 {template_id} 失败: {e}")
|
print(f"加载模板 {template_id} 失败: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_template_readme(self, template_id: str) -> Optional[str]:
|
def get_template_readme(self, template_id: str) -> Optional[str]:
|
||||||
"""获取模板的 README 文档
|
"""获取模板的 README 文档
|
||||||
|
|
||||||
@@ -106,10 +109,10 @@ class TemplateLoader:
|
|||||||
"""
|
"""
|
||||||
template_dir = self.templates_dir / template_id
|
template_dir = self.templates_dir / template_id
|
||||||
readme_file = template_dir / "README.md"
|
readme_file = template_dir / "README.md"
|
||||||
|
|
||||||
if not readme_file.exists():
|
if not readme_file.exists():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(readme_file, 'r', encoding='utf-8') as f:
|
with open(readme_file, 'r', encoding='utf-8') as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|||||||
@@ -235,6 +235,8 @@ class MemoryConfigRepository:
|
|||||||
llm_id=params.llm_id,
|
llm_id=params.llm_id,
|
||||||
embedding_id=params.embedding_id,
|
embedding_id=params.embedding_id,
|
||||||
rerank_id=params.rerank_id,
|
rerank_id=params.rerank_id,
|
||||||
|
reflection_model_id=params.reflection_model_id,
|
||||||
|
emotion_model_id=params.emotion_model_id,
|
||||||
)
|
)
|
||||||
db.add(db_config)
|
db.add(db_config)
|
||||||
db.flush() # 获取自增ID但不提交事务
|
db.flush() # 获取自增ID但不提交事务
|
||||||
|
|||||||
@@ -877,7 +877,8 @@ RETURN
|
|||||||
CASE
|
CASE
|
||||||
WHEN ms:ExtractedEntity THEN {
|
WHEN ms:ExtractedEntity THEN {
|
||||||
text: ms.name,
|
text: ms.name,
|
||||||
created_at: ms.created_at
|
created_at: ms.created_at,
|
||||||
|
type: "情景记忆"
|
||||||
}
|
}
|
||||||
END
|
END
|
||||||
) AS ExtractedEntity,
|
) AS ExtractedEntity,
|
||||||
@@ -887,7 +888,8 @@ RETURN
|
|||||||
CASE
|
CASE
|
||||||
WHEN n:MemorySummary THEN {
|
WHEN n:MemorySummary THEN {
|
||||||
text: n.content,
|
text: n.content,
|
||||||
created_at: n.created_at
|
created_at: n.created_at,
|
||||||
|
type: "长期沉淀"
|
||||||
}
|
}
|
||||||
END
|
END
|
||||||
) AS MemorySummary,
|
) AS MemorySummary,
|
||||||
@@ -895,7 +897,8 @@ RETURN
|
|||||||
collect(
|
collect(
|
||||||
DISTINCT {
|
DISTINCT {
|
||||||
text: e.statement,
|
text: e.statement,
|
||||||
created_at: e.created_at
|
created_at: e.created_at,
|
||||||
|
type: "情绪记忆"
|
||||||
}
|
}
|
||||||
) AS statement;
|
) AS statement;
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -236,6 +236,8 @@ class ConfigParamsCreate(BaseModel): # 创建配置参数模型(仅 body,
|
|||||||
llm_id: Optional[str] = Field(None, description="LLM模型配置ID")
|
llm_id: Optional[str] = Field(None, description="LLM模型配置ID")
|
||||||
embedding_id: Optional[str] = Field(None, description="嵌入模型配置ID")
|
embedding_id: Optional[str] = Field(None, description="嵌入模型配置ID")
|
||||||
rerank_id: Optional[str] = Field(None, description="重排序模型配置ID")
|
rerank_id: Optional[str] = Field(None, description="重排序模型配置ID")
|
||||||
|
reflection_model_id: Optional[str] = Field(None, description="反思模型ID,默认与llm_id一致")
|
||||||
|
emotion_model_id: Optional[str] = Field(None, description="情绪分析模型ID,默认与llm_id一致")
|
||||||
|
|
||||||
|
|
||||||
class ConfigParamsDelete(BaseModel): # 删除配置参数模型(请求体)
|
class ConfigParamsDelete(BaseModel): # 删除配置参数模型(请求体)
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ class AppStatisticsService:
|
|||||||
daily_tokens[date_str] = 0
|
daily_tokens[date_str] = 0
|
||||||
daily_tokens[date_str] += int(tokens)
|
daily_tokens[date_str] += int(tokens)
|
||||||
|
|
||||||
daily_data = [{"date": date, "tokens": tokens} for date, tokens in sorted(daily_tokens.items()) if tokens != 0]
|
daily_data = [{"date": date, "count": tokens} for date, tokens in sorted(daily_tokens.items()) if tokens != 0]
|
||||||
total = sum(row["tokens"] for row in daily_data)
|
total = sum(row["tokens"] for row in daily_data)
|
||||||
|
|
||||||
return {"daily": daily_data, "total": total}
|
return {"daily": daily_data, "total": total}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""会话服务"""
|
"""会话服务"""
|
||||||
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
@@ -529,12 +530,12 @@ class ConversationService:
|
|||||||
takeaways=[],
|
takeaways=[],
|
||||||
info_score=0,
|
info_score=0,
|
||||||
)
|
)
|
||||||
|
prompt_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'prompt')
|
||||||
with open('app/services/prompt/conversation_summary_system.jinja2', 'r', encoding='utf-8') as f:
|
with open(os.path.join(prompt_path, 'conversation_summary_system.jinja2'), 'r', encoding='utf-8') as f:
|
||||||
system_prompt = f.read()
|
system_prompt = f.read()
|
||||||
rendered_system_message = Template(system_prompt).render()
|
rendered_system_message = Template(system_prompt).render()
|
||||||
|
|
||||||
with open('app/services/prompt/conversation_summary_user.jinja2', 'r', encoding='utf-8') as f:
|
with open(os.path.join(prompt_path, 'conversation_summary_user.jinja2'), 'r', encoding='utf-8') as f:
|
||||||
user_prompt = f.read()
|
user_prompt = f.read()
|
||||||
rendered_user_message = Template(user_prompt).render(
|
rendered_user_message = Template(user_prompt).render(
|
||||||
language=language,
|
language=language,
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ def get_workspace_end_users(
|
|||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
current_user: User
|
current_user: User
|
||||||
) -> List[EndUser]:
|
) -> List[EndUser]:
|
||||||
"""获取工作空间的所有宿主(优化版本:减少数据库查询次数)"""
|
"""获取工作空间的所有宿主(优化版本:减少数据库查询次数)
|
||||||
|
|
||||||
|
返回结果按 updated_at 从新到旧排序(NULL 值排在最后)
|
||||||
|
"""
|
||||||
business_logger.info(f"获取工作空间宿主列表: workspace_id={workspace_id}, 操作者: {current_user.username}")
|
business_logger.info(f"获取工作空间宿主列表: workspace_id={workspace_id}, 操作者: {current_user.username}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -68,9 +71,14 @@ def get_workspace_end_users(
|
|||||||
app_ids = [app.id for app in apps_orm]
|
app_ids = [app.id for app in apps_orm]
|
||||||
|
|
||||||
# 批量查询所有 end_users(一次查询而非循环查询)
|
# 批量查询所有 end_users(一次查询而非循环查询)
|
||||||
|
# 按 updated_at 降序排序,NULL 值排在最后;id 作为次级排序键保证确定性
|
||||||
from app.models.end_user_model import EndUser as EndUserModel
|
from app.models.end_user_model import EndUser as EndUserModel
|
||||||
|
from sqlalchemy import desc, nullslast
|
||||||
end_users_orm = db.query(EndUserModel).filter(
|
end_users_orm = db.query(EndUserModel).filter(
|
||||||
EndUserModel.app_id.in_(app_ids)
|
EndUserModel.app_id.in_(app_ids)
|
||||||
|
).order_by(
|
||||||
|
nullslast(desc(EndUserModel.updated_at)),
|
||||||
|
desc(EndUserModel.id)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
# 转换为 Pydantic 模型(只在需要时转换)
|
# 转换为 Pydantic 模型(只在需要时转换)
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ class MemoryReflectionService:
|
|||||||
# 检查是否需要执行反思
|
# 检查是否需要执行反思
|
||||||
should_execute = False
|
should_execute = False
|
||||||
hours_diff = 0
|
hours_diff = 0
|
||||||
|
|
||||||
if current_reflection_time is None:
|
if current_reflection_time is None:
|
||||||
# 首次执行反思
|
# 首次执行反思
|
||||||
should_execute = True
|
should_execute = True
|
||||||
@@ -298,11 +298,11 @@ class MemoryReflectionService:
|
|||||||
reflection_time = datetime.fromisoformat(current_reflection_time)
|
reflection_time = datetime.fromisoformat(current_reflection_time)
|
||||||
else:
|
else:
|
||||||
reflection_time = current_reflection_time
|
reflection_time = current_reflection_time
|
||||||
|
|
||||||
current_time = datetime.now()
|
current_time = datetime.now()
|
||||||
time_diff = current_time - reflection_time
|
time_diff = current_time - reflection_time
|
||||||
hours_diff = int(time_diff.total_seconds() / 3600)
|
hours_diff = int(time_diff.total_seconds() / 3600)
|
||||||
|
|
||||||
# 检查是否达到反思周期
|
# 检查是否达到反思周期
|
||||||
if hours_diff >= iteration_period:
|
if hours_diff >= iteration_period:
|
||||||
should_execute = True
|
should_execute = True
|
||||||
@@ -312,7 +312,7 @@ class MemoryReflectionService:
|
|||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
api_logger.warning(f"解析反思时间失败: {e},将执行反思")
|
api_logger.warning(f"解析反思时间失败: {e},将执行反思")
|
||||||
should_execute = True
|
should_execute = True
|
||||||
|
|
||||||
if should_execute:
|
if should_execute:
|
||||||
api_logger.info(f"与上次的反思时间间隔为: {hours_diff} 小时")
|
api_logger.info(f"与上次的反思时间间隔为: {hours_diff} 小时")
|
||||||
# 3. 执行反思引擎
|
# 3. 执行反思引擎
|
||||||
@@ -345,7 +345,7 @@ class MemoryReflectionService:
|
|||||||
"next_reflection_in_hours": iteration_period - hours_diff
|
"next_reflection_in_hours": iteration_period - hours_diff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
config_id = config_data.get("config_id", "unknown")
|
config_id = config_data.get("config_id", "unknown")
|
||||||
api_logger.error(f"启动反思失败,config_id: {config_id}, end_user_id: {end_user_id}, 错误: {str(e)}")
|
api_logger.error(f"启动反思失败,config_id: {config_id}, end_user_id: {end_user_id}, 错误: {str(e)}")
|
||||||
@@ -356,7 +356,7 @@ class MemoryReflectionService:
|
|||||||
"end_user_id": end_user_id,
|
"end_user_id": end_user_id,
|
||||||
"config_data": config_data
|
"config_data": config_data
|
||||||
}
|
}
|
||||||
|
|
||||||
def _create_reflection_config_from_data(self, config_data: Dict[str, Any]) -> ReflectionConfig:
|
def _create_reflection_config_from_data(self, config_data: Dict[str, Any]) -> ReflectionConfig:
|
||||||
"""Create reflective configuration objects from configuration data"""
|
"""Create reflective configuration objects from configuration data"""
|
||||||
|
|
||||||
@@ -364,12 +364,12 @@ class MemoryReflectionService:
|
|||||||
if reflexion_range_value is None or reflexion_range_value == "":
|
if reflexion_range_value is None or reflexion_range_value == "":
|
||||||
reflexion_range_value = "partial"
|
reflexion_range_value = "partial"
|
||||||
reflexion_range = ReflectionRange(reflexion_range_value)
|
reflexion_range = ReflectionRange(reflexion_range_value)
|
||||||
|
|
||||||
baseline_value = config_data.get("baseline")
|
baseline_value = config_data.get("baseline")
|
||||||
if baseline_value is None or baseline_value == "":
|
if baseline_value is None or baseline_value == "":
|
||||||
baseline_value = "TIME"
|
baseline_value = "TIME"
|
||||||
baseline = ReflectionBaseline(baseline_value)
|
baseline = ReflectionBaseline(baseline_value)
|
||||||
|
|
||||||
# iteration_period =
|
# iteration_period =
|
||||||
iteration_period = config_data.get("iteration_period", 24)
|
iteration_period = config_data.get("iteration_period", 24)
|
||||||
if isinstance(iteration_period, str):
|
if isinstance(iteration_period, str):
|
||||||
@@ -377,7 +377,6 @@ class MemoryReflectionService:
|
|||||||
iteration_period = int(iteration_period)
|
iteration_period = int(iteration_period)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
iteration_period = 24 # 默认24小时
|
iteration_period = 24 # 默认24小时
|
||||||
|
|
||||||
return ReflectionConfig(
|
return ReflectionConfig(
|
||||||
enabled=config_data.get("enable_self_reflexion", False),
|
enabled=config_data.get("enable_self_reflexion", False),
|
||||||
iteration_period=str(iteration_period), # ReflectionConfig期望字符串
|
iteration_period=str(iteration_period), # ReflectionConfig期望字符串
|
||||||
|
|||||||
@@ -129,6 +129,12 @@ class DataConfigService: # 数据配置服务类(PostgreSQL)
|
|||||||
if not params.rerank_id:
|
if not params.rerank_id:
|
||||||
params.rerank_id = configs.get('rerank')
|
params.rerank_id = configs.get('rerank')
|
||||||
|
|
||||||
|
# reflection_model_id 和 emotion_model_id 默认与 llm_id 一致
|
||||||
|
if not params.reflection_model_id:
|
||||||
|
params.reflection_model_id = params.llm_id
|
||||||
|
if not params.emotion_model_id:
|
||||||
|
params.emotion_model_id = params.llm_id
|
||||||
|
|
||||||
config = MemoryConfigRepository.create(self.db, params)
|
config = MemoryConfigRepository.create(self.db, params)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
return {"affected": 1, "config_id": config.config_id}
|
return {"affected": 1, "config_id": config.config_id}
|
||||||
@@ -203,6 +209,7 @@ class DataConfigService: # 数据配置服务类(PostgreSQL)
|
|||||||
"end_user_id": config.end_user_id,
|
"end_user_id": config.end_user_id,
|
||||||
"config_id_old": config_id_old,
|
"config_id_old": config_id_old,
|
||||||
"apply_id": config.apply_id,
|
"apply_id": config.apply_id,
|
||||||
|
"scene_id": config.scene_id,
|
||||||
"llm_id": config.llm_id,
|
"llm_id": config.llm_id,
|
||||||
"embedding_id": config.embedding_id,
|
"embedding_id": config.embedding_id,
|
||||||
"rerank_id": config.rerank_id,
|
"rerank_id": config.rerank_id,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any, AsyncGenerator
|
from typing import Any, AsyncGenerator
|
||||||
@@ -182,11 +183,12 @@ class PromptOptimizerService:
|
|||||||
base_url=api_config.api_base
|
base_url=api_config.api_base
|
||||||
), type=ModelType(model_config.type))
|
), type=ModelType(model_config.type))
|
||||||
try:
|
try:
|
||||||
with open('app/services/prompt/prompt_optimizer_system.jinja2', 'r', encoding='utf-8') as f:
|
prompt_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'prompt')
|
||||||
|
with open(os.path.join(prompt_path, 'prompt_optimizer_system.jinja2'), 'r', encoding='utf-8') as f:
|
||||||
opt_system_prompt = f.read()
|
opt_system_prompt = f.read()
|
||||||
rendered_system_message = Template(opt_system_prompt).render()
|
rendered_system_message = Template(opt_system_prompt).render()
|
||||||
|
|
||||||
with open('app/services/prompt/prompt_optimizer_user.jinja2', 'r', encoding='utf-8') as f:
|
with open(os.path.join(prompt_path, 'prompt_optimizer_user.jinja2'), 'r', encoding='utf-8') as f:
|
||||||
opt_user_prompt = f.read()
|
opt_user_prompt = f.read()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise BusinessException(message="System prompt template not found", code=BizCode.NOT_FOUND)
|
raise BusinessException(message="System prompt template not found", code=BizCode.NOT_FOUND)
|
||||||
|
|||||||
288
api/app/tasks.py
288
api/app/tasks.py
@@ -1066,6 +1066,7 @@ def workspace_reflection_task(self) -> Dict[str, Any]:
|
|||||||
f"工作空间 {workspace_id} 反思处理完成,处理了 {len(workspace_reflection_results)} 个任务")
|
f"工作空间 {workspace_id} 反思处理完成,处理了 {len(workspace_reflection_results)} 个任务")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
db.rollback() # Rollback failed transaction to allow next query
|
||||||
api_logger.error(f"处理工作空间 {workspace_id} 反思失败: {str(e)}")
|
api_logger.error(f"处理工作空间 {workspace_id} 反思失败: {str(e)}")
|
||||||
all_reflection_results.append({
|
all_reflection_results.append({
|
||||||
"workspace_id": str(workspace_id),
|
"workspace_id": str(workspace_id),
|
||||||
@@ -1204,3 +1205,290 @@ def run_forgetting_cycle_task(self, config_id: Optional[uuid.UUID] = None) -> Di
|
|||||||
return result
|
return result
|
||||||
finally:
|
finally:
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Long-term Memory Storage Tasks (Batched Write Strategies)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@celery_app.task(name="app.core.memory.agent.long_term_storage.window", bind=True)
|
||||||
|
def long_term_storage_window_task(
|
||||||
|
self,
|
||||||
|
end_user_id: str,
|
||||||
|
langchain_messages: List[Dict[str, Any]],
|
||||||
|
config_id: str,
|
||||||
|
scope: int = 6
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Celery task for window-based long-term memory storage.
|
||||||
|
|
||||||
|
Accumulates messages in Redis buffer until window size (scope) is reached,
|
||||||
|
then writes batched messages to Neo4j.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
end_user_id: End user identifier
|
||||||
|
langchain_messages: List of messages [{"role": "user/assistant", "content": "..."}]
|
||||||
|
config_id: Memory configuration ID
|
||||||
|
scope: Window size (number of messages before triggering write)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict containing task status and metadata
|
||||||
|
"""
|
||||||
|
from app.core.logging_config import get_logger
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
logger.info(f"[LONG_TERM_WINDOW] Starting task - end_user_id={end_user_id}, scope={scope}")
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
async def _run() -> Dict[str, Any]:
|
||||||
|
from app.core.memory.agent.langgraph_graph.routing.write_router import window_dialogue
|
||||||
|
from app.core.memory.agent.langgraph_graph.tools.write_tool import chat_data_format
|
||||||
|
from app.core.memory.agent.utils.redis_tool import write_store
|
||||||
|
from app.services.memory_config_service import MemoryConfigService
|
||||||
|
|
||||||
|
db = next(get_db())
|
||||||
|
try:
|
||||||
|
# Save to Redis buffer first
|
||||||
|
write_store.save_session_write(end_user_id, await chat_data_format(langchain_messages))
|
||||||
|
|
||||||
|
# Load memory config
|
||||||
|
config_service = MemoryConfigService(db)
|
||||||
|
memory_config = config_service.load_memory_config(
|
||||||
|
config_id=config_id,
|
||||||
|
service_name="LongTermStorageTask"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Execute window-based dialogue storage
|
||||||
|
await window_dialogue(end_user_id, langchain_messages, memory_config, scope)
|
||||||
|
|
||||||
|
return {"status": "SUCCESS", "strategy": "window", "scope": scope}
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
import nest_asyncio
|
||||||
|
nest_asyncio.apply()
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
if loop.is_closed():
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
except RuntimeError:
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = loop.run_until_complete(_run())
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
|
||||||
|
logger.info(f"[LONG_TERM_WINDOW] Task completed - elapsed_time={elapsed_time:.2f}s")
|
||||||
|
|
||||||
|
return {
|
||||||
|
**result,
|
||||||
|
"end_user_id": end_user_id,
|
||||||
|
"config_id": config_id,
|
||||||
|
"elapsed_time": elapsed_time,
|
||||||
|
"task_id": self.request.id
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
elapsed_time = time.time() - start_time
|
||||||
|
logger.error(f"[LONG_TERM_WINDOW] Task failed - error={str(e)}", exc_info=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "FAILURE",
|
||||||
|
"strategy": "window",
|
||||||
|
"error": str(e),
|
||||||
|
"end_user_id": end_user_id,
|
||||||
|
"config_id": config_id,
|
||||||
|
"elapsed_time": elapsed_time,
|
||||||
|
"task_id": self.request.id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# @celery_app.task(name="app.core.memory.agent.long_term_storage.time", bind=True)
|
||||||
|
# def long_term_storage_time_task(
|
||||||
|
# self,
|
||||||
|
# end_user_id: str,
|
||||||
|
# config_id: str,
|
||||||
|
# time_window: int = 5
|
||||||
|
# ) -> Dict[str, Any]:
|
||||||
|
# """Celery task for time-based long-term memory storage.
|
||||||
|
|
||||||
|
# Retrieves recent sessions from Redis within time window and writes to Neo4j.
|
||||||
|
|
||||||
|
# Args:
|
||||||
|
# end_user_id: End user identifier
|
||||||
|
# config_id: Memory configuration ID
|
||||||
|
# time_window: Time window in minutes for retrieving recent sessions
|
||||||
|
|
||||||
|
# Returns:
|
||||||
|
# Dict containing task status and metadata
|
||||||
|
# """
|
||||||
|
# from app.core.logging_config import get_logger
|
||||||
|
# logger = get_logger(__name__)
|
||||||
|
|
||||||
|
# logger.info(f"[LONG_TERM_TIME] Starting task - end_user_id={end_user_id}, time_window={time_window}")
|
||||||
|
# start_time = time.time()
|
||||||
|
|
||||||
|
# async def _run() -> Dict[str, Any]:
|
||||||
|
# from app.core.memory.agent.langgraph_graph.routing.write_router import memory_long_term_storage
|
||||||
|
# from app.services.memory_config_service import MemoryConfigService
|
||||||
|
|
||||||
|
# db = next(get_db())
|
||||||
|
# try:
|
||||||
|
# # Load memory config
|
||||||
|
# config_service = MemoryConfigService(db)
|
||||||
|
# memory_config = config_service.load_memory_config(
|
||||||
|
# config_id=config_id,
|
||||||
|
# service_name="LongTermStorageTask"
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # Execute time-based storage
|
||||||
|
# await memory_long_term_storage(end_user_id, memory_config, time_window)
|
||||||
|
|
||||||
|
# return {"status": "SUCCESS", "strategy": "time", "time_window": time_window}
|
||||||
|
# finally:
|
||||||
|
# db.close()
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# import nest_asyncio
|
||||||
|
# nest_asyncio.apply()
|
||||||
|
# except ImportError:
|
||||||
|
# pass
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# loop = asyncio.get_event_loop()
|
||||||
|
# if loop.is_closed():
|
||||||
|
# loop = asyncio.new_event_loop()
|
||||||
|
# asyncio.set_event_loop(loop)
|
||||||
|
# except RuntimeError:
|
||||||
|
# loop = asyncio.new_event_loop()
|
||||||
|
# asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# result = loop.run_until_complete(_run())
|
||||||
|
# elapsed_time = time.time() - start_time
|
||||||
|
|
||||||
|
# logger.info(f"[LONG_TERM_TIME] Task completed - elapsed_time={elapsed_time:.2f}s")
|
||||||
|
|
||||||
|
# return {
|
||||||
|
# **result,
|
||||||
|
# "end_user_id": end_user_id,
|
||||||
|
# "config_id": config_id,
|
||||||
|
# "elapsed_time": elapsed_time,
|
||||||
|
# "task_id": self.request.id
|
||||||
|
# }
|
||||||
|
# except Exception as e:
|
||||||
|
# elapsed_time = time.time() - start_time
|
||||||
|
# logger.error(f"[LONG_TERM_TIME] Task failed - error={str(e)}", exc_info=True)
|
||||||
|
|
||||||
|
# return {
|
||||||
|
# "status": "FAILURE",
|
||||||
|
# "strategy": "time",
|
||||||
|
# "error": str(e),
|
||||||
|
# "end_user_id": end_user_id,
|
||||||
|
# "config_id": config_id,
|
||||||
|
# "elapsed_time": elapsed_time,
|
||||||
|
# "task_id": self.request.id
|
||||||
|
# }
|
||||||
|
|
||||||
|
|
||||||
|
# @celery_app.task(name="app.core.memory.agent.long_term_storage.aggregate", bind=True)
|
||||||
|
# def long_term_storage_aggregate_task(
|
||||||
|
# self,
|
||||||
|
# end_user_id: str,
|
||||||
|
# langchain_messages: List[Dict[str, Any]],
|
||||||
|
# config_id: str
|
||||||
|
# ) -> Dict[str, Any]:
|
||||||
|
# """Celery task for aggregate-based long-term memory storage.
|
||||||
|
|
||||||
|
# Uses LLM to determine if new messages describe the same event as history.
|
||||||
|
# Only writes to Neo4j if messages represent new information (not duplicates).
|
||||||
|
|
||||||
|
# Args:
|
||||||
|
# end_user_id: End user identifier
|
||||||
|
# langchain_messages: List of messages [{"role": "user/assistant", "content": "..."}]
|
||||||
|
# config_id: Memory configuration ID
|
||||||
|
|
||||||
|
# Returns:
|
||||||
|
# Dict containing task status, is_same_event flag, and metadata
|
||||||
|
# """
|
||||||
|
# from app.core.logging_config import get_logger
|
||||||
|
# logger = get_logger(__name__)
|
||||||
|
|
||||||
|
# logger.info(f"[LONG_TERM_AGGREGATE] Starting task - end_user_id={end_user_id}")
|
||||||
|
# start_time = time.time()
|
||||||
|
|
||||||
|
# async def _run() -> Dict[str, Any]:
|
||||||
|
# from app.core.memory.agent.langgraph_graph.routing.write_router import aggregate_judgment
|
||||||
|
# from app.core.memory.agent.langgraph_graph.tools.write_tool import chat_data_format
|
||||||
|
# from app.core.memory.agent.utils.redis_tool import write_store
|
||||||
|
# from app.services.memory_config_service import MemoryConfigService
|
||||||
|
|
||||||
|
# db = next(get_db())
|
||||||
|
# try:
|
||||||
|
# # Save to Redis buffer first
|
||||||
|
# write_store.save_session_write(end_user_id, await chat_data_format(langchain_messages))
|
||||||
|
|
||||||
|
# # Load memory config
|
||||||
|
# config_service = MemoryConfigService(db)
|
||||||
|
# memory_config = config_service.load_memory_config(
|
||||||
|
# config_id=config_id,
|
||||||
|
# service_name="LongTermStorageTask"
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # Execute aggregate judgment
|
||||||
|
# result = await aggregate_judgment(end_user_id, langchain_messages, memory_config)
|
||||||
|
|
||||||
|
# return {
|
||||||
|
# "status": "SUCCESS",
|
||||||
|
# "strategy": "aggregate",
|
||||||
|
# "is_same_event": result.get("is_same_event", False),
|
||||||
|
# "wrote_to_neo4j": not result.get("is_same_event", False)
|
||||||
|
# }
|
||||||
|
# finally:
|
||||||
|
# db.close()
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# import nest_asyncio
|
||||||
|
# nest_asyncio.apply()
|
||||||
|
# except ImportError:
|
||||||
|
# pass
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# loop = asyncio.get_event_loop()
|
||||||
|
# if loop.is_closed():
|
||||||
|
# loop = asyncio.new_event_loop()
|
||||||
|
# asyncio.set_event_loop(loop)
|
||||||
|
# except RuntimeError:
|
||||||
|
# loop = asyncio.new_event_loop()
|
||||||
|
# asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# result = loop.run_until_complete(_run())
|
||||||
|
# elapsed_time = time.time() - start_time
|
||||||
|
|
||||||
|
# logger.info(f"[LONG_TERM_AGGREGATE] Task completed - is_same_event={result.get('is_same_event')}, elapsed_time={elapsed_time:.2f}s")
|
||||||
|
|
||||||
|
# return {
|
||||||
|
# **result,
|
||||||
|
# "end_user_id": end_user_id,
|
||||||
|
# "config_id": config_id,
|
||||||
|
# "elapsed_time": elapsed_time,
|
||||||
|
# "task_id": self.request.id
|
||||||
|
# }
|
||||||
|
# except Exception as e:
|
||||||
|
# elapsed_time = time.time() - start_time
|
||||||
|
# logger.error(f"[LONG_TERM_AGGREGATE] Task failed - error={str(e)}", exc_info=True)
|
||||||
|
|
||||||
|
# return {
|
||||||
|
# "status": "FAILURE",
|
||||||
|
# "strategy": "aggregate",
|
||||||
|
# "error": str(e),
|
||||||
|
# "end_user_id": end_user_id,
|
||||||
|
# "config_id": config_id,
|
||||||
|
# "elapsed_time": elapsed_time,
|
||||||
|
# "task_id": self.request.id
|
||||||
|
# }
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ async def run_code(request: RunCodeRequest):
|
|||||||
"""Execute code in sandbox"""
|
"""Execute code in sandbox"""
|
||||||
if request.language == "python3":
|
if request.language == "python3":
|
||||||
return await run_python_code(request.code, request.preload, request.options)
|
return await run_python_code(request.code, request.preload, request.options)
|
||||||
elif request.language == "nodejs":
|
elif request.language == "javascript":
|
||||||
return await run_nodejs_code(request.code, request.preload, request.options)
|
return await run_nodejs_code(request.code, request.preload, request.options)
|
||||||
else:
|
else:
|
||||||
return error_response(-400, "unsupported language")
|
return error_response(-400, "unsupported language")
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface PageScrollListProps<T, Q = Record<string, unknown>> {
|
|||||||
query?: Q;
|
query?: Q;
|
||||||
column?: number;
|
column?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
needLoading?: boolean;
|
||||||
}
|
}
|
||||||
const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
|
const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
|
||||||
renderItem,
|
renderItem,
|
||||||
@@ -33,6 +34,7 @@ const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
|
|||||||
url,
|
url,
|
||||||
column = 4,
|
column = 4,
|
||||||
className = '',
|
className = '',
|
||||||
|
needLoading = true,
|
||||||
}: PageScrollListProps<T, Q>, ref: React.Ref<PageScrollListRef>) => {
|
}: PageScrollListProps<T, Q>, ref: React.Ref<PageScrollListRef>) => {
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
refresh,
|
refresh,
|
||||||
@@ -104,9 +106,10 @@ const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
|
|||||||
dataLength={data.length}
|
dataLength={data.length}
|
||||||
next={loadMoreData}
|
next={loadMoreData}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
loader={<PageLoading />}
|
loader={loading && needLoading ? <PageLoading /> : false}
|
||||||
// endMessage={<Divider plain>It is all, nothing more 🤐</Divider>}
|
// endMessage={<Divider plain>It is all, nothing more 🤐</Divider>}
|
||||||
scrollableTarget="scrollableDiv"
|
scrollableTarget="scrollableDiv"
|
||||||
|
className='rb:h-full!'
|
||||||
>
|
>
|
||||||
{data.length > 0 ? (
|
{data.length > 0 ? (
|
||||||
<List
|
<List
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
|||||||
import InitialValuePlugin from './plugin/InitialValuePlugin'
|
import InitialValuePlugin from './plugin/InitialValuePlugin'
|
||||||
import LineBreakPlugin from './plugin/LineBreakPlugin';
|
import LineBreakPlugin from './plugin/LineBreakPlugin';
|
||||||
import InsertTextPlugin from './plugin/InsertTextPlugin';
|
import InsertTextPlugin from './plugin/InsertTextPlugin';
|
||||||
|
import EditablePlugin from './plugin/EditablePlugin';
|
||||||
|
|
||||||
export interface EditorRef {
|
export interface EditorRef {
|
||||||
insertText: (text: string) => void;
|
insertText: (text: string) => void;
|
||||||
@@ -23,6 +24,7 @@ interface LexicalEditorProps {
|
|||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const theme = {
|
const theme = {
|
||||||
@@ -38,6 +40,7 @@ const EditorContent = forwardRef<EditorRef, LexicalEditorProps>(({
|
|||||||
value,
|
value,
|
||||||
placeholder = "请输入内容...",
|
placeholder = "请输入内容...",
|
||||||
onChange,
|
onChange,
|
||||||
|
disabled
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
|
|
||||||
@@ -92,7 +95,11 @@ const EditorContent = forwardRef<EditorRef, LexicalEditorProps>(({
|
|||||||
<RichTextPlugin
|
<RichTextPlugin
|
||||||
contentEditable={
|
contentEditable={
|
||||||
<ContentEditable
|
<ContentEditable
|
||||||
className={clsx("rb:outline-none rb:resize-none rb:text-[14px] rb:leading-5 rb:px-4 rb:py-5 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:overflow-auto", className)}
|
className={clsx(
|
||||||
|
"rb:outline-none rb:resize-none rb:text-[14px] rb:leading-5 rb:px-4 rb:py-5 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:overflow-auto",
|
||||||
|
disabled && "rb:cursor-not-allowed rb:bg-[#F6F8FC] rb:text-[#5B6167]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
placeholder={
|
placeholder={
|
||||||
@@ -105,6 +112,7 @@ const EditorContent = forwardRef<EditorRef, LexicalEditorProps>(({
|
|||||||
<LineBreakPlugin onChange={onChange} />
|
<LineBreakPlugin onChange={onChange} />
|
||||||
<InitialValuePlugin value={value} />
|
<InitialValuePlugin value={value} />
|
||||||
<InsertTextPlugin />
|
<InsertTextPlugin />
|
||||||
|
<EditablePlugin disabled={disabled} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -114,6 +122,7 @@ const Editor = forwardRef<EditorRef, LexicalEditorProps>((props, ref) => {
|
|||||||
namespace: 'Editor',
|
namespace: 'Editor',
|
||||||
theme,
|
theme,
|
||||||
nodes: [],
|
nodes: [],
|
||||||
|
editable: !props.disabled,
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ZhaoYing
|
||||||
|
* @Date: 2026-02-04 11:20:49
|
||||||
|
* @Last Modified by: ZhaoYing
|
||||||
|
* @Last Modified time: 2026-02-04 11:20:49
|
||||||
|
*/
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the EditablePlugin component
|
||||||
|
*/
|
||||||
|
interface EditablePluginProps {
|
||||||
|
/** Whether the editor should be disabled (read-only mode) */
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EditablePlugin - A Lexical editor plugin that controls the editable state of the editor
|
||||||
|
*
|
||||||
|
* This plugin allows you to dynamically toggle between editable and read-only modes.
|
||||||
|
* When disabled is true, the editor becomes read-only and users cannot modify content.
|
||||||
|
* When disabled is false or undefined, the editor is fully editable.
|
||||||
|
*
|
||||||
|
* @param {EditablePluginProps} props - Component props
|
||||||
|
* @param {boolean} [props.disabled] - Controls whether the editor is in read-only mode
|
||||||
|
* @returns {null} This plugin doesn't render any UI elements
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```tsx
|
||||||
|
* <LexicalComposer>
|
||||||
|
* <EditablePlugin disabled={isReadOnly} />
|
||||||
|
* </LexicalComposer>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export default function EditablePlugin({ disabled }: EditablePluginProps) {
|
||||||
|
// Get the editor instance from Lexical composer context
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
|
||||||
|
// Update editor's editable state whenever the disabled prop changes
|
||||||
|
useEffect(() => {
|
||||||
|
// Set editor to editable when disabled is false, read-only when disabled is true
|
||||||
|
editor.setEditable(!disabled);
|
||||||
|
}, [editor, disabled]);
|
||||||
|
|
||||||
|
// This plugin doesn't render any UI, it only manages editor state
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -64,7 +64,7 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
|
|||||||
...item,
|
...item,
|
||||||
config: {
|
config: {
|
||||||
similarity_threshold: 0.7,
|
similarity_threshold: 0.7,
|
||||||
strategy: "hybrid",
|
retrieve_type: "hybrid",
|
||||||
top_k: 3,
|
top_k: 3,
|
||||||
weight: 1,
|
weight: 1,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
|
|||||||
const handleDelete = (vo: any) => {
|
const handleDelete = (vo: any) => {
|
||||||
modal.confirm({
|
modal.confirm({
|
||||||
title: t('common.confirmDeleteDesc', { name: [vo.model_name, vo.api_key].join(' / ') }),
|
title: t('common.confirmDeleteDesc', { name: [vo.model_name, vo.api_key].join(' / ') }),
|
||||||
content: t('application.apiKeyDeleteContent'),
|
|
||||||
okText: t('common.delete'),
|
okText: t('common.delete'),
|
||||||
cancelText: t('common.cancel'),
|
cancelText: t('common.cancel'),
|
||||||
okType: 'danger',
|
okType: 'danger',
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ const History: React.FC<{ query: HistoryQuery; edit: (item: HistoryItem) => void
|
|||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
modal.confirm({
|
modal.confirm({
|
||||||
title: t('common.confirmDeleteDesc', { name: item.title }),
|
title: t('common.confirmDeleteDesc', { name: item.title }),
|
||||||
content: t('application.apiKeyDeleteContent'),
|
|
||||||
okText: t('common.delete'),
|
okText: t('common.delete'),
|
||||||
cancelText: t('common.cancel'),
|
cancelText: t('common.cancel'),
|
||||||
okType: 'danger',
|
okType: 'danger',
|
||||||
@@ -50,6 +49,7 @@ const History: React.FC<{ query: HistoryQuery; edit: (item: HistoryItem) => void
|
|||||||
url={getPromptReleaseListUrl}
|
url={getPromptReleaseListUrl}
|
||||||
query={query}
|
query={query}
|
||||||
column={3}
|
column={3}
|
||||||
|
needLoading={false}
|
||||||
renderItem={(item) => {
|
renderItem={(item) => {
|
||||||
const historyItem = item as unknown as HistoryItem;
|
const historyItem = item as unknown as HistoryItem;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -138,9 +138,9 @@ const Prompt: FC<{ editVo: HistoryItem | null; refresh: () => void; }> = ({ edit
|
|||||||
currentPromptValueRef.current = undefined;
|
currentPromptValueRef.current = undefined;
|
||||||
setChatList([])
|
setChatList([])
|
||||||
refresh()
|
refresh()
|
||||||
|
updateSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(values)
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form form={form}>
|
<Form form={form}>
|
||||||
@@ -199,12 +199,13 @@ const Prompt: FC<{ editVo: HistoryItem | null; refresh: () => void; }> = ({ edit
|
|||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
placeholder={t('prompt.promptPlaceholder')}
|
placeholder={t('prompt.promptPlaceholder')}
|
||||||
className="rb:h-[calc(100vh-260px)]"
|
className="rb:h-[calc(100vh-260px)]"
|
||||||
|
disabled={loading}
|
||||||
// onChange={(value) => form.setFieldValue('current_prompt', value)}
|
// onChange={(value) => form.setFieldValue('current_prompt', value)}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-6">
|
<div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-6">
|
||||||
<Button type="primary" block disabled={!values?.current_prompt} onClick={handleSave}>{t('common.save')}</Button>
|
<Button type="primary" block disabled={!values?.current_prompt || loading} onClick={handleSave}>{t('common.save')}</Button>
|
||||||
<Button block disabled={!values?.current_prompt} onClick={handleCopy}>{t('common.copy')}</Button>
|
<Button block disabled={!values?.current_prompt || loading} onClick={handleCopy}>{t('common.copy')}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
|||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
handleUpdate(formData)
|
handleUpdate(formData)
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
handleUpdate(formData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -139,6 +141,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
|
|||||||
label={t('space.spaceIcon')}
|
label={t('space.spaceIcon')}
|
||||||
valuePropName="fileList"
|
valuePropName="fileList"
|
||||||
hidden={currentStep === 1}
|
hidden={currentStep === 1}
|
||||||
|
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('space.spaceIcon') }) }]}
|
||||||
>
|
>
|
||||||
<UploadImages />
|
<UploadImages />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
{enableLineNumbers && <LineNumberPlugin />}
|
{enableLineNumbers && <LineNumberPlugin />}
|
||||||
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
||||||
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
||||||
<InitialValuePlugin value={value} options={options} enableJinja2={enableJinja2} />
|
<InitialValuePlugin key={language} value={value} options={options} enableLineNumbers={enableLineNumbers} />
|
||||||
{enableLineNumbers && <BlurPlugin />}
|
{enableLineNumbers && <BlurPlugin />}
|
||||||
</div>
|
</div>
|
||||||
</LexicalComposer>
|
</LexicalComposer>
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ export default function BlurPlugin() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否是粘贴操作导致的焦点变化
|
||||||
|
const relatedTarget = e.relatedTarget as HTMLElement;
|
||||||
|
if (!relatedTarget || relatedTarget === document.body) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
$setSelection(null);
|
$setSelection(null);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import { type Suggestion } from '../plugin/AutocompletePlugin'
|
|||||||
interface InitialValuePluginProps {
|
interface InitialValuePluginProps {
|
||||||
value: string;
|
value: string;
|
||||||
options?: Suggestion[];
|
options?: Suggestion[];
|
||||||
enableJinja2?: boolean;
|
enableLineNumbers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [], enableJinja2 = false }) => {
|
const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options = [], enableLineNumbers = false }) => {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
const prevValueRef = useRef<string>('');
|
const prevValueRef = useRef<string>('');
|
||||||
|
const prevEnableLineNumbersRef = useRef<boolean>(enableLineNumbers);
|
||||||
const isUserInputRef = useRef(false);
|
const isUserInputRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -32,7 +33,7 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
|||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value !== prevValueRef.current && !isUserInputRef.current) {
|
if ((value !== prevValueRef.current || enableLineNumbers !== prevEnableLineNumbersRef.current) && !isUserInputRef.current) {
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const root = $getRoot();
|
const root = $getRoot();
|
||||||
@@ -40,7 +41,7 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
|||||||
|
|
||||||
const parts = value.split(/(\{\{[^}]+\}\})/);
|
const parts = value.split(/(\{\{[^}]+\}\})/);
|
||||||
|
|
||||||
if (enableJinja2) {
|
if (enableLineNumbers) {
|
||||||
// Handle newlines properly in Jinja2 mode
|
// Handle newlines properly in Jinja2 mode
|
||||||
const lines = value.split('\n');
|
const lines = value.split('\n');
|
||||||
lines.forEach((line) => {
|
lines.forEach((line) => {
|
||||||
@@ -104,8 +105,9 @@ const InitialValuePlugin: React.FC<InitialValuePluginProps> = ({ value, options
|
|||||||
}
|
}
|
||||||
|
|
||||||
prevValueRef.current = value;
|
prevValueRef.current = value;
|
||||||
|
prevEnableLineNumbersRef.current = enableLineNumbers;
|
||||||
isUserInputRef.current = false;
|
isUserInputRef.current = false;
|
||||||
}, [value, options, editor, enableJinja2]);
|
}, [value, options, editor, enableLineNumbers]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
import { TextNode, $createTextNode, $getSelection, $isRangeSelection } from 'lexical';
|
import { TextNode, $createTextNode, $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW, PASTE_COMMAND } from 'lexical';
|
||||||
|
|
||||||
const JS_KEYWORDS = new Set([
|
const JS_KEYWORDS = new Set([
|
||||||
'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default',
|
'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default',
|
||||||
@@ -11,13 +11,31 @@ const JS_KEYWORDS = new Set([
|
|||||||
|
|
||||||
const JavaScriptHighlightPlugin = () => {
|
const JavaScriptHighlightPlugin = () => {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
|
const isPastingRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return editor.registerCommand(
|
||||||
|
PASTE_COMMAND,
|
||||||
|
() => {
|
||||||
|
isPastingRef.current = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
isPastingRef.current = false;
|
||||||
|
}, 100);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
COMMAND_PRIORITY_LOW
|
||||||
|
);
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
||||||
|
if (isPastingRef.current) return;
|
||||||
|
|
||||||
const text = textNode.getTextContent();
|
const text = textNode.getTextContent();
|
||||||
|
|
||||||
if (textNode.hasFormat('code')) return;
|
if (textNode.hasFormat('code')) return;
|
||||||
if (!needsHighlight(text)) return;
|
if (!needsHighlight(text)) return;
|
||||||
|
if (textNode.getStyle()) return;
|
||||||
|
|
||||||
const parent = textNode.getParent();
|
const parent = textNode.getParent();
|
||||||
if (!parent) return;
|
if (!parent) return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
import { TextNode, $createTextNode, $getSelection, $isRangeSelection } from 'lexical';
|
import { TextNode, $createTextNode, $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW, PASTE_COMMAND } from 'lexical';
|
||||||
|
|
||||||
const PYTHON_KEYWORDS = new Set([
|
const PYTHON_KEYWORDS = new Set([
|
||||||
'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue',
|
'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue',
|
||||||
@@ -11,12 +11,30 @@ const PYTHON_KEYWORDS = new Set([
|
|||||||
|
|
||||||
const Python3HighlightPlugin = () => {
|
const Python3HighlightPlugin = () => {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
|
const isPastingRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return editor.registerCommand(
|
||||||
|
PASTE_COMMAND,
|
||||||
|
() => {
|
||||||
|
isPastingRef.current = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
isPastingRef.current = false;
|
||||||
|
}, 100);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
COMMAND_PRIORITY_LOW
|
||||||
|
);
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
||||||
|
if (isPastingRef.current) return;
|
||||||
|
|
||||||
const text = textNode.getTextContent();
|
const text = textNode.getTextContent();
|
||||||
|
|
||||||
if (textNode.hasFormat('code')) return;
|
if (textNode.hasFormat('code')) return;
|
||||||
|
if (textNode.getStyle()) return;
|
||||||
if (!needsHighlight(text)) return;
|
if (!needsHighlight(text)) return;
|
||||||
|
|
||||||
const parent = textNode.getParent();
|
const parent = textNode.getParent();
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ const codeTemplate = {
|
|||||||
const CodeExecution: FC<CodeExecutionProps> = ({ options }) => {
|
const CodeExecution: FC<CodeExecutionProps> = ({ options }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const form = Form.useFormInstance()
|
const form = Form.useFormInstance()
|
||||||
const values = Form.useWatch([], form) || {}
|
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
const code = form.getFieldValue('code') || ''
|
const code = form.getFieldValue('code') || ''
|
||||||
@@ -66,7 +65,6 @@ const CodeExecution: FC<CodeExecutionProps> = ({ options }) => {
|
|||||||
form.setFieldValue('code', newTemplate)
|
form.setFieldValue('code', newTemplate)
|
||||||
}
|
}
|
||||||
const handleChangeLanguage = (value: string) => {
|
const handleChangeLanguage = (value: string) => {
|
||||||
form.setFieldValue('code', codeTemplate[value as keyof typeof codeTemplate])
|
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
input_variables: [{ name: 'arg1' }, { name: 'arg2' }],
|
input_variables: [{ name: 'arg1' }, { name: 'arg2' }],
|
||||||
code: codeTemplate[value as keyof typeof codeTemplate]
|
code: codeTemplate[value as keyof typeof codeTemplate]
|
||||||
@@ -109,8 +107,12 @@ const CodeExecution: FC<CodeExecutionProps> = ({ options }) => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Form.Item name="code" noStyle>
|
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.language !== curr.language}>
|
||||||
<Editor size="small" language={values.language} />
|
{() => (
|
||||||
|
<Form.Item name="code" noStyle>
|
||||||
|
<Editor size="small" language={form.getFieldValue('language')} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
|
|||||||
...item,
|
...item,
|
||||||
config: {
|
config: {
|
||||||
similarity_threshold: 0.7,
|
similarity_threshold: 0.7,
|
||||||
strategy: "hybrid",
|
retrieve_type: "hybrid",
|
||||||
top_k: 3,
|
top_k: 3,
|
||||||
weight: 1,
|
weight: 1,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export const useWorkflowGraph = ({
|
|||||||
nodeLibraryConfig.config[key].defaultValue = Object.entries(config[key]).map(([name, value]) => ({ name, value }))
|
nodeLibraryConfig.config[key].defaultValue = Object.entries(config[key]).map(([name, value]) => ({ name, value }))
|
||||||
} else if (type === 'code' && key === 'code' && config[key] && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) {
|
} else if (type === 'code' && key === 'code' && config[key] && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) {
|
||||||
try {
|
try {
|
||||||
nodeLibraryConfig.config[key].defaultValue = atob(config[key] as string)
|
nodeLibraryConfig.config[key].defaultValue = decodeURIComponent(atob(config[key] as string))
|
||||||
} catch {
|
} catch {
|
||||||
nodeLibraryConfig.config[key].defaultValue = config[key]
|
nodeLibraryConfig.config[key].defaultValue = config[key]
|
||||||
}
|
}
|
||||||
@@ -851,7 +851,7 @@ export const useWorkflowGraph = ({
|
|||||||
const code = data.config[key].defaultValue || ''
|
const code = data.config[key].defaultValue || ''
|
||||||
itemConfig = {
|
itemConfig = {
|
||||||
...itemConfig,
|
...itemConfig,
|
||||||
code: btoa(code || '')
|
code: btoa(encodeURIComponent(code || ''))
|
||||||
}
|
}
|
||||||
} else if (key === 'memory' && data.config[key] && 'defaultValue' in data.config[key]) {
|
} else if (key === 'memory' && data.config[key] && 'defaultValue' in data.config[key]) {
|
||||||
const { messages, ...rest } = data.config[key].defaultValue
|
const { messages, ...rest } = data.config[key].defaultValue
|
||||||
@@ -885,7 +885,7 @@ export const useWorkflowGraph = ({
|
|||||||
...itemConfig,
|
...itemConfig,
|
||||||
...(data.config[key].defaultValue || {}),
|
...(data.config[key].defaultValue || {}),
|
||||||
knowledge_bases: knowledge_bases?.map((vo: any) => {
|
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 }
|
const kb_config = vo.config || { similarity_threshold: vo.similarity_threshold, retrieve_type: vo.retrieve_type, top_k: vo.top_k, weight: vo.weight }
|
||||||
return { kb_id: vo.kb_id || vo.id, ...kb_config, }
|
return { kb_id: vo.kb_id || vo.id, ...kb_config, }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user