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:
lixinyue
2026-02-04 13:43:15 +08:00
38 changed files with 563 additions and 109 deletions

View File

@@ -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

View File

@@ -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")

View File

@@ -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:

View File

@@ -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)

View File

@@ -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"
) )

View File

@@ -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,

View File

@@ -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:

View File

@@ -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但不提交事务

View File

@@ -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;
""" """

View File

@@ -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): # 删除配置参数模型(请求体)

View File

@@ -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}

View File

@@ -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,

View File

@@ -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 模型(只在需要时转换)

View File

@@ -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期望字符串

View File

@@ -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,

View File

@@ -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)

View File

@@ -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
# }

View File

@@ -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")

View File

@@ -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

View File

@@ -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);
}, },

View File

@@ -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;
}

View File

@@ -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,
} }

View File

@@ -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',

View File

@@ -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 (

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}); });

View File

@@ -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;
}; };

View File

@@ -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;

View File

@@ -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();

View File

@@ -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>

View File

@@ -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,
} }

View File

@@ -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, }
}) })
} }