Merge branch 'refs/heads/develop' into fix/memory_mcp2_1
This commit is contained in:
11
api/app/cache/__init__.py
vendored
Normal file
11
api/app/cache/__init__.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Cache 缓存模块
|
||||
|
||||
提供各种缓存功能的统一入口
|
||||
"""
|
||||
from .memory import EmotionMemoryCache, ImplicitMemoryCache
|
||||
|
||||
__all__ = [
|
||||
"EmotionMemoryCache",
|
||||
"ImplicitMemoryCache",
|
||||
]
|
||||
12
api/app/cache/memory/__init__.py
vendored
Normal file
12
api/app/cache/memory/__init__.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Memory 缓存模块
|
||||
|
||||
提供记忆系统相关的缓存功能
|
||||
"""
|
||||
from .emotion_memory import EmotionMemoryCache
|
||||
from .implicit_memory import ImplicitMemoryCache
|
||||
|
||||
__all__ = [
|
||||
"EmotionMemoryCache",
|
||||
"ImplicitMemoryCache",
|
||||
]
|
||||
134
api/app/cache/memory/emotion_memory.py
vendored
Normal file
134
api/app/cache/memory/emotion_memory.py
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Emotion Suggestions Cache
|
||||
|
||||
情绪个性化建议缓存模块
|
||||
用于缓存用户的情绪个性化建议数据
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from app.aioRedis import aio_redis
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmotionMemoryCache:
|
||||
"""情绪建议缓存类"""
|
||||
|
||||
# Key 前缀
|
||||
PREFIX = "cache:memory:emotion_memory"
|
||||
|
||||
@classmethod
|
||||
def _get_key(cls, *parts: str) -> str:
|
||||
"""生成 Redis key
|
||||
|
||||
Args:
|
||||
*parts: key 的各个部分
|
||||
|
||||
Returns:
|
||||
完整的 Redis key
|
||||
"""
|
||||
return ":".join([cls.PREFIX] + list(parts))
|
||||
|
||||
@classmethod
|
||||
async def set_emotion_suggestions(
|
||||
cls,
|
||||
user_id: str,
|
||||
suggestions_data: Dict[str, Any],
|
||||
expire: int = 86400
|
||||
) -> bool:
|
||||
"""设置用户情绪建议缓存
|
||||
|
||||
Args:
|
||||
user_id: 用户ID(end_user_id)
|
||||
suggestions_data: 建议数据字典,包含:
|
||||
- health_summary: 健康状态摘要
|
||||
- suggestions: 建议列表
|
||||
- generated_at: 生成时间(可选)
|
||||
expire: 过期时间(秒),默认24小时(86400秒)
|
||||
|
||||
Returns:
|
||||
是否设置成功
|
||||
"""
|
||||
try:
|
||||
key = cls._get_key("suggestions", user_id)
|
||||
|
||||
# 添加生成时间戳
|
||||
if "generated_at" not in suggestions_data:
|
||||
suggestions_data["generated_at"] = datetime.now().isoformat()
|
||||
|
||||
# 添加缓存标记
|
||||
suggestions_data["cached"] = True
|
||||
|
||||
value = json.dumps(suggestions_data, ensure_ascii=False)
|
||||
await aio_redis.set(key, value, ex=expire)
|
||||
logger.info(f"设置情绪建议缓存成功: {key}, 过期时间: {expire}秒")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"设置情绪建议缓存失败: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def get_emotion_suggestions(cls, user_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""获取用户情绪建议缓存
|
||||
|
||||
Args:
|
||||
user_id: 用户ID(end_user_id)
|
||||
|
||||
Returns:
|
||||
建议数据字典,如果不存在或已过期返回 None
|
||||
"""
|
||||
try:
|
||||
key = cls._get_key("suggestions", user_id)
|
||||
value = await aio_redis.get(key)
|
||||
|
||||
if value:
|
||||
data = json.loads(value)
|
||||
logger.info(f"成功获取情绪建议缓存: {key}")
|
||||
return data
|
||||
|
||||
logger.info(f"情绪建议缓存不存在或已过期: {key}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"获取情绪建议缓存失败: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def delete_emotion_suggestions(cls, user_id: str) -> bool:
|
||||
"""删除用户情绪建议缓存
|
||||
|
||||
Args:
|
||||
user_id: 用户ID(end_user_id)
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
"""
|
||||
try:
|
||||
key = cls._get_key("suggestions", user_id)
|
||||
result = await aio_redis.delete(key)
|
||||
logger.info(f"删除情绪建议缓存: {key}, 结果: {result}")
|
||||
return result > 0
|
||||
except Exception as e:
|
||||
logger.error(f"删除情绪建议缓存失败: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def get_suggestions_ttl(cls, user_id: str) -> int:
|
||||
"""获取情绪建议缓存的剩余过期时间
|
||||
|
||||
Args:
|
||||
user_id: 用户ID(end_user_id)
|
||||
|
||||
Returns:
|
||||
剩余秒数,-1表示永不过期,-2表示key不存在
|
||||
"""
|
||||
try:
|
||||
key = cls._get_key("suggestions", user_id)
|
||||
ttl = await aio_redis.ttl(key)
|
||||
logger.debug(f"情绪建议缓存TTL: {key} = {ttl}秒")
|
||||
return ttl
|
||||
except Exception as e:
|
||||
logger.error(f"获取情绪建议缓存TTL失败: {e}")
|
||||
return -2
|
||||
136
api/app/cache/memory/implicit_memory.py
vendored
Normal file
136
api/app/cache/memory/implicit_memory.py
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Implicit Memory Profile Cache
|
||||
|
||||
隐式记忆用户画像缓存模块
|
||||
用于缓存用户的完整画像数据(偏好标签、四维画像、兴趣领域、行为习惯)
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from app.aioRedis import aio_redis
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImplicitMemoryCache:
|
||||
"""隐式记忆用户画像缓存类"""
|
||||
|
||||
# Key 前缀
|
||||
PREFIX = "cache:memory:implicit_memory"
|
||||
|
||||
@classmethod
|
||||
def _get_key(cls, *parts: str) -> str:
|
||||
"""生成 Redis key
|
||||
|
||||
Args:
|
||||
*parts: key 的各个部分
|
||||
|
||||
Returns:
|
||||
完整的 Redis key
|
||||
"""
|
||||
return ":".join([cls.PREFIX] + list(parts))
|
||||
|
||||
@classmethod
|
||||
async def set_user_profile(
|
||||
cls,
|
||||
user_id: str,
|
||||
profile_data: Dict[str, Any],
|
||||
expire: int = 86400
|
||||
) -> bool:
|
||||
"""设置用户完整画像缓存
|
||||
|
||||
Args:
|
||||
user_id: 用户ID(end_user_id)
|
||||
profile_data: 画像数据字典,包含:
|
||||
- preferences: 偏好标签列表
|
||||
- portrait: 四维画像对象
|
||||
- interest_areas: 兴趣领域分布对象
|
||||
- habits: 行为习惯列表
|
||||
- generated_at: 生成时间(可选)
|
||||
expire: 过期时间(秒),默认24小时(86400秒)
|
||||
|
||||
Returns:
|
||||
是否设置成功
|
||||
"""
|
||||
try:
|
||||
key = cls._get_key("profile", user_id)
|
||||
|
||||
# 添加生成时间戳
|
||||
if "generated_at" not in profile_data:
|
||||
profile_data["generated_at"] = datetime.now().isoformat()
|
||||
|
||||
# 添加缓存标记
|
||||
profile_data["cached"] = True
|
||||
|
||||
value = json.dumps(profile_data, ensure_ascii=False)
|
||||
await aio_redis.set(key, value, ex=expire)
|
||||
logger.info(f"设置用户画像缓存成功: {key}, 过期时间: {expire}秒")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"设置用户画像缓存失败: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def get_user_profile(cls, user_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""获取用户完整画像缓存
|
||||
|
||||
Args:
|
||||
user_id: 用户ID(end_user_id)
|
||||
|
||||
Returns:
|
||||
画像数据字典,如果不存在或已过期返回 None
|
||||
"""
|
||||
try:
|
||||
key = cls._get_key("profile", user_id)
|
||||
value = await aio_redis.get(key)
|
||||
|
||||
if value:
|
||||
data = json.loads(value)
|
||||
logger.info(f"成功获取用户画像缓存: {key}")
|
||||
return data
|
||||
|
||||
logger.info(f"用户画像缓存不存在或已过期: {key}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"获取用户画像缓存失败: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def delete_user_profile(cls, user_id: str) -> bool:
|
||||
"""删除用户完整画像缓存
|
||||
|
||||
Args:
|
||||
user_id: 用户ID(end_user_id)
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
"""
|
||||
try:
|
||||
key = cls._get_key("profile", user_id)
|
||||
result = await aio_redis.delete(key)
|
||||
logger.info(f"删除用户画像缓存: {key}, 结果: {result}")
|
||||
return result > 0
|
||||
except Exception as e:
|
||||
logger.error(f"删除用户画像缓存失败: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def get_profile_ttl(cls, user_id: str) -> int:
|
||||
"""获取用户画像缓存的剩余过期时间
|
||||
|
||||
Args:
|
||||
user_id: 用户ID(end_user_id)
|
||||
|
||||
Returns:
|
||||
剩余秒数,-1表示永不过期,-2表示key不存在
|
||||
"""
|
||||
try:
|
||||
key = cls._get_key("profile", user_id)
|
||||
ttl = await aio_redis.ttl(key)
|
||||
logger.debug(f"用户画像缓存TTL: {key} = {ttl}秒")
|
||||
return ttl
|
||||
except Exception as e:
|
||||
logger.error(f"获取用户画像缓存TTL失败: {e}")
|
||||
return -2
|
||||
@@ -231,9 +231,9 @@ async def get_emotion_suggestions(
|
||||
extra={"group_id": request.group_id}
|
||||
)
|
||||
return fail(
|
||||
BizCode.RESOURCE_NOT_FOUND,
|
||||
BizCode.NOT_FOUND,
|
||||
"建议缓存不存在或已过期,请调用 /generate_suggestions 接口生成新建议",
|
||||
None
|
||||
""
|
||||
)
|
||||
|
||||
api_logger.info(
|
||||
@@ -267,7 +267,7 @@ async def generate_emotion_suggestions(
|
||||
"""生成个性化情绪建议(调用LLM并缓存)
|
||||
|
||||
Args:
|
||||
request: 包含 group_id、可选的 config_id 和 force_refresh
|
||||
request: 包含 end_user_id
|
||||
db: 数据库会话
|
||||
current_user: 当前用户
|
||||
|
||||
@@ -275,47 +275,22 @@ async def generate_emotion_suggestions(
|
||||
新生成的个性化情绪建议响应
|
||||
"""
|
||||
try:
|
||||
# 验证 config_id(如果提供)
|
||||
# 获取终端用户关联的配置
|
||||
config_id = request.config_id
|
||||
if config_id is None:
|
||||
# 如果没有提供 config_id,尝试获取用户关联的配置
|
||||
try:
|
||||
from app.services.memory_agent_service import (
|
||||
get_end_user_connected_config,
|
||||
)
|
||||
connected_config = get_end_user_connected_config(request.group_id, db)
|
||||
config_id = connected_config.get("memory_config_id")
|
||||
except ValueError as e:
|
||||
return fail(BizCode.INVALID_PARAMETER, "无法获取用户关联的配置", str(e))
|
||||
else:
|
||||
# 如果提供了 config_id,验证其有效性
|
||||
from app.services.memory_config_service import MemoryConfigService
|
||||
try:
|
||||
config_service = MemoryConfigService(db)
|
||||
config = config_service.get_config_by_id(config_id)
|
||||
if not config:
|
||||
return fail(BizCode.INVALID_PARAMETER, "配置ID无效", f"配置 {config_id} 不存在")
|
||||
except Exception as e:
|
||||
return fail(BizCode.INVALID_PARAMETER, "配置ID验证失败", str(e))
|
||||
|
||||
api_logger.info(
|
||||
f"用户 {current_user.username} 请求生成个性化情绪建议",
|
||||
extra={
|
||||
"group_id": request.group_id,
|
||||
"config_id": config_id
|
||||
"end_user_id": request.end_user_id
|
||||
}
|
||||
)
|
||||
|
||||
# 调用服务层生成建议
|
||||
data = await emotion_service.generate_emotion_suggestions(
|
||||
end_user_id=request.group_id,
|
||||
end_user_id=request.end_user_id,
|
||||
db=db
|
||||
)
|
||||
|
||||
# 保存到缓存
|
||||
await emotion_service.save_suggestions_cache(
|
||||
end_user_id=request.group_id,
|
||||
end_user_id=request.end_user_id,
|
||||
suggestions_data=data,
|
||||
db=db,
|
||||
expires_hours=24
|
||||
@@ -324,7 +299,7 @@ async def generate_emotion_suggestions(
|
||||
api_logger.info(
|
||||
"个性化建议生成成功",
|
||||
extra={
|
||||
"group_id": request.group_id,
|
||||
"end_user_id": request.end_user_id,
|
||||
"suggestions_count": len(data.get("suggestions", []))
|
||||
}
|
||||
)
|
||||
@@ -334,7 +309,7 @@ async def generate_emotion_suggestions(
|
||||
except Exception as e:
|
||||
api_logger.error(
|
||||
f"生成个性化建议失败: {str(e)}",
|
||||
extra={"group_id": request.group_id},
|
||||
extra={"end_user_id": request.end_user_id},
|
||||
exc_info=True
|
||||
)
|
||||
raise HTTPException(
|
||||
|
||||
@@ -161,9 +161,9 @@ async def get_preference_tags(
|
||||
if cached_profile is None:
|
||||
api_logger.info(f"用户 {user_id} 的画像缓存不存在或已过期")
|
||||
return fail(
|
||||
BizCode.RESOURCE_NOT_FOUND,
|
||||
BizCode.NOT_FOUND,
|
||||
"画像缓存不存在或已过期,请调用 /generate_profile 接口生成新画像",
|
||||
None
|
||||
""
|
||||
)
|
||||
|
||||
# Extract preferences from cache
|
||||
@@ -232,9 +232,9 @@ async def get_dimension_portrait(
|
||||
if cached_profile is None:
|
||||
api_logger.info(f"用户 {user_id} 的画像缓存不存在或已过期")
|
||||
return fail(
|
||||
BizCode.RESOURCE_NOT_FOUND,
|
||||
BizCode.NOT_FOUND,
|
||||
"画像缓存不存在或已过期,请调用 /generate_profile 接口生成新画像",
|
||||
None
|
||||
""
|
||||
)
|
||||
|
||||
# Extract portrait from cache
|
||||
@@ -280,9 +280,9 @@ async def get_interest_area_distribution(
|
||||
if cached_profile is None:
|
||||
api_logger.info(f"用户 {user_id} 的画像缓存不存在或已过期")
|
||||
return fail(
|
||||
BizCode.RESOURCE_NOT_FOUND,
|
||||
BizCode.NOT_FOUND,
|
||||
"画像缓存不存在或已过期,请调用 /generate_profile 接口生成新画像",
|
||||
None
|
||||
""
|
||||
)
|
||||
|
||||
# Extract interest areas from cache
|
||||
@@ -332,9 +332,9 @@ async def get_behavior_habits(
|
||||
if cached_profile is None:
|
||||
api_logger.info(f"用户 {user_id} 的画像缓存不存在或已过期")
|
||||
return fail(
|
||||
BizCode.RESOURCE_NOT_FOUND,
|
||||
BizCode.NOT_FOUND,
|
||||
"画像缓存不存在或已过期,请调用 /generate_profile 接口生成新画像",
|
||||
None
|
||||
""
|
||||
)
|
||||
|
||||
# Extract habits from cache
|
||||
|
||||
@@ -8,9 +8,10 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.logging_config import get_business_logger
|
||||
from app.core.response_utils import success
|
||||
from app.db import get_db
|
||||
from app.db import get_db, get_db_read
|
||||
from app.dependencies import get_share_user_id, ShareTokenData
|
||||
from app.repositories import knowledge_repository
|
||||
from app.repositories.workflow_repository import WorkflowConfigRepository
|
||||
from app.schemas import release_share_schema, conversation_schema
|
||||
from app.schemas.response_schema import PageData, PageMeta
|
||||
from app.services import workspace_service
|
||||
@@ -19,7 +20,8 @@ from app.services.conversation_service import ConversationService
|
||||
from app.services.release_share_service import ReleaseShareService
|
||||
from app.services.shared_chat_service import SharedChatService
|
||||
from app.services.app_chat_service import AppChatService, get_app_chat_service
|
||||
from app.utils.app_config_utils import dict_to_multi_agent_config, workflow_config_4_app_release, agent_config_4_app_release, multi_agent_config_4_app_release
|
||||
from app.utils.app_config_utils import dict_to_multi_agent_config, workflow_config_4_app_release, \
|
||||
agent_config_4_app_release, multi_agent_config_4_app_release
|
||||
|
||||
router = APIRouter(prefix="/public/share", tags=["Public Share"])
|
||||
logger = get_business_logger()
|
||||
@@ -65,10 +67,10 @@ def get_or_generate_user_id(payload_user_id: str, request: Request) -> str:
|
||||
summary="获取访问 token"
|
||||
)
|
||||
def get_access_token(
|
||||
share_token: str,
|
||||
payload: release_share_schema.TokenRequest,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
share_token: str,
|
||||
payload: release_share_schema.TokenRequest,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取访问 token
|
||||
|
||||
@@ -113,9 +115,9 @@ def get_access_token(
|
||||
response_model=None
|
||||
)
|
||||
def get_shared_release(
|
||||
password: str = Query(None, description="访问密码(如果需要)"),
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
password: str = Query(None, description="访问密码(如果需要)"),
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取公开分享的发布版本信息
|
||||
|
||||
@@ -137,9 +139,9 @@ def get_shared_release(
|
||||
summary="验证访问密码"
|
||||
)
|
||||
def verify_password(
|
||||
payload: release_share_schema.PasswordVerifyRequest,
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
payload: release_share_schema.PasswordVerifyRequest,
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""验证分享的访问密码
|
||||
|
||||
@@ -159,11 +161,11 @@ def verify_password(
|
||||
summary="获取嵌入代码"
|
||||
)
|
||||
def get_embed_code(
|
||||
width: str = Query("100%", description="iframe 宽度"),
|
||||
height: str = Query("600px", description="iframe 高度"),
|
||||
request: Request = None,
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
width: str = Query("100%", description="iframe 宽度"),
|
||||
height: str = Query("600px", description="iframe 高度"),
|
||||
request: Request = None,
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取嵌入代码
|
||||
|
||||
@@ -183,7 +185,6 @@ def get_embed_code(
|
||||
return success(data=embed_code)
|
||||
|
||||
|
||||
|
||||
# ---------- 会话管理接口 ----------
|
||||
|
||||
@router.get(
|
||||
@@ -191,11 +192,11 @@ def get_embed_code(
|
||||
summary="获取会话列表"
|
||||
)
|
||||
def list_conversations(
|
||||
password: str = Query(None, description="访问密码"),
|
||||
page: int = Query(1, ge=1),
|
||||
pagesize: int = Query(20, ge=1, le=100),
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
password: str = Query(None, description="访问密码"),
|
||||
page: int = Query(1, ge=1),
|
||||
pagesize: int = Query(20, ge=1, le=100),
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取分享应用的会话列表
|
||||
|
||||
@@ -209,9 +210,9 @@ def list_conversations(
|
||||
from app.repositories.end_user_repository import EndUserRepository
|
||||
end_user_repo = EndUserRepository(db)
|
||||
new_end_user = end_user_repo.get_or_create_end_user(
|
||||
app_id=share.app_id,
|
||||
other_id=other_id
|
||||
)
|
||||
app_id=share.app_id,
|
||||
other_id=other_id
|
||||
)
|
||||
logger.debug(new_end_user.id)
|
||||
service = SharedChatService(db)
|
||||
conversations, total = service.list_conversations(
|
||||
@@ -233,10 +234,10 @@ def list_conversations(
|
||||
summary="获取会话详情(含消息)"
|
||||
)
|
||||
def get_conversation(
|
||||
conversation_id: uuid.UUID,
|
||||
password: str = Query(None, description="访问密码"),
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
conversation_id: uuid.UUID,
|
||||
password: str = Query(None, description="访问密码"),
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取会话详情和消息历史"""
|
||||
chat_service = SharedChatService(db)
|
||||
@@ -266,10 +267,10 @@ def get_conversation(
|
||||
summary="发送消息(支持流式和非流式)"
|
||||
)
|
||||
async def chat(
|
||||
payload: conversation_schema.ChatRequest,
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
app_chat_service: Annotated[AppChatService, Depends(get_app_chat_service)] = None,
|
||||
payload: conversation_schema.ChatRequest,
|
||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
app_chat_service: Annotated[AppChatService, Depends(get_app_chat_service)] = None,
|
||||
):
|
||||
"""发送消息并获取回复
|
||||
|
||||
@@ -313,7 +314,7 @@ async def chat(
|
||||
)
|
||||
end_user_id = str(new_end_user.id)
|
||||
|
||||
appid=share.app_id
|
||||
appid = share.app_id
|
||||
"""获取存储类型和工作空间的ID"""
|
||||
|
||||
# 直接通过 SQLAlchemy 查询 app
|
||||
@@ -425,16 +426,16 @@ async def chat(
|
||||
# )
|
||||
async def event_generator():
|
||||
async for event in app_chat_service.agnet_chat_stream(
|
||||
message=payload.message,
|
||||
conversation_id=conversation.id, # 使用已创建的会话 ID
|
||||
user_id= str(new_end_user.id), # 转换为字符串
|
||||
variables=payload.variables,
|
||||
web_search=payload.web_search,
|
||||
config=agent_config,
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id,
|
||||
workspace_id=workspace_id
|
||||
message=payload.message,
|
||||
conversation_id=conversation.id, # 使用已创建的会话 ID
|
||||
user_id=str(new_end_user.id), # 转换为字符串
|
||||
variables=payload.variables,
|
||||
web_search=payload.web_search,
|
||||
config=agent_config,
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id,
|
||||
workspace_id=workspace_id
|
||||
):
|
||||
yield event
|
||||
|
||||
@@ -481,15 +482,15 @@ async def chat(
|
||||
async def event_generator():
|
||||
async for event in app_chat_service.multi_agent_chat_stream(
|
||||
|
||||
message=payload.message,
|
||||
conversation_id=conversation.id, # 使用已创建的会话 ID
|
||||
user_id=str(new_end_user.id), # 转换为字符串
|
||||
variables=payload.variables,
|
||||
config=config,
|
||||
web_search=payload.web_search,
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id
|
||||
message=payload.message,
|
||||
conversation_id=conversation.id, # 使用已创建的会话 ID
|
||||
user_id=str(new_end_user.id), # 转换为字符串
|
||||
variables=payload.variables,
|
||||
config=config,
|
||||
web_search=payload.web_search,
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id
|
||||
):
|
||||
yield event
|
||||
|
||||
@@ -561,24 +562,27 @@ async def chat(
|
||||
|
||||
# return success(data=conversation_schema.ChatResponse(**result))
|
||||
elif app_type == AppType.WORKFLOW:
|
||||
|
||||
config = workflow_config_4_app_release(release)
|
||||
if not config.id:
|
||||
with get_db_read() as db:
|
||||
source_config = WorkflowConfigRepository(db).get_by_app_id(release.app_id)
|
||||
config.id = source_config.id
|
||||
config.id = uuid.UUID(config.id)
|
||||
if payload.stream:
|
||||
async def event_generator():
|
||||
|
||||
async for event in app_chat_service.workflow_chat_stream(
|
||||
|
||||
message=payload.message,
|
||||
conversation_id=conversation.id, # 使用已创建的会话 ID
|
||||
user_id=end_user_id, # 转换为字符串
|
||||
variables=payload.variables,
|
||||
config=config,
|
||||
web_search=payload.web_search,
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id,
|
||||
app_id=release.app_id,
|
||||
workspace_id=workspace_id
|
||||
message=payload.message,
|
||||
conversation_id=conversation.id, # 使用已创建的会话 ID
|
||||
user_id=end_user_id, # 转换为字符串
|
||||
variables=payload.variables,
|
||||
config=config,
|
||||
web_search=payload.web_search,
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id,
|
||||
app_id=release.app_id,
|
||||
workspace_id=workspace_id,
|
||||
release_id=release.id
|
||||
):
|
||||
event_type = event.get("event", "message")
|
||||
event_data = event.get("data", {})
|
||||
@@ -610,7 +614,8 @@ async def chat(
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id,
|
||||
app_id=release.app_id,
|
||||
workspace_id=workspace_id
|
||||
workspace_id=workspace_id,
|
||||
release_id=release.id
|
||||
)
|
||||
logger.debug(
|
||||
"工作流试运行返回结果",
|
||||
|
||||
@@ -242,8 +242,9 @@ async def chat(
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id,
|
||||
app_id=app.app_id,
|
||||
workspace_id=workspace_id
|
||||
app_id=app.id,
|
||||
workspace_id=workspace_id,
|
||||
release_id=app.current_release.id,
|
||||
):
|
||||
event_type = event.get("event", "message")
|
||||
event_data = event.get("data", {})
|
||||
@@ -274,8 +275,9 @@ async def chat(
|
||||
memory=payload.memory,
|
||||
storage_type=storage_type,
|
||||
user_rag_memory_id=user_rag_memory_id,
|
||||
app_id=app.app_id,
|
||||
workspace_id=workspace_id
|
||||
app_id=app.id,
|
||||
workspace_id=workspace_id,
|
||||
release_id=app.current_release.id
|
||||
)
|
||||
logger.debug(
|
||||
"工作流试运行返回结果",
|
||||
|
||||
@@ -38,6 +38,7 @@ class Settings:
|
||||
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
|
||||
REDIS_DB: int = int(os.getenv("REDIS_DB", "1"))
|
||||
REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "")
|
||||
|
||||
|
||||
# ElasticSearch configuration
|
||||
ELASTICSEARCH_HOST: str = os.getenv("ELASTICSEARCH_HOST", "https://127.0.0.1")
|
||||
|
||||
@@ -243,6 +243,33 @@ class QWenCV(GptV4):
|
||||
tmp_path = tmp.name
|
||||
|
||||
video_path = f"file://{tmp_path}"
|
||||
prompt_ch = """
|
||||
你是一名专业的视频转录助手,能够将视频文件的内容转写为文本,并**精确标记每句话或每个段落对应的时间戳**(开始时间-结束时间)。\n
|
||||
**任务要求**:
|
||||
1.输入是MP4等视频文件,解析带时间戳的文本。
|
||||
2.时间戳格式为 `[HH:MM:SS.mmm]`(毫秒可选),例如 `[00:01:23.456]`。
|
||||
3.时间戳需尽可能贴近实际视频的起止时间,误差不超过1秒。
|
||||
4.如果无法确定具体时间,请根据上下文合理估算。
|
||||
5.最后总结:这段视频的内容是什么?,并用恰当的句子总结这个视频。
|
||||
|
||||
**示例输出**:
|
||||
[00:00:00.000] 今天天气真好,
|
||||
[00:00:02.500] 我们一起去公园散步吧。
|
||||
[00:00:05.800] 公园里的花开得非常漂亮。
|
||||
这段视频的内容是关于如何在CREAMS系统中进行楼宇管理集合的编辑或删除操作。视频演示了 ..."""
|
||||
prompt_en = """
|
||||
You are a professional video transcription assistant, capable of transcribing the content of video files into text and **precisely marking the timestamp (start time-end time) corresponding to each sentence or paragraph**.
|
||||
**Task requirements**:
|
||||
1. Input is MP4 or other video files, and parse the text with timestamps.
|
||||
2. The timestamp format is `[HH:MM:SS.mmm]` (milliseconds are optional), for example, `[00:01:23.456]`.
|
||||
3. The timestamp should be as close as possible to the actual start and end time of the video, with an error not exceeding 1 second.
|
||||
4. If the specific time cannot be determined, please make a reasonable estimation based on the context.
|
||||
5. Final summary: What is the content of this video? Summarize this video in an appropriate sentence.
|
||||
|
||||
**Example output**:
|
||||
[00:00:00.000] The weather is really nice today, [00:00:02.500] let's go for a walk in the park together.
|
||||
[00:00:05.800] The flowers in the park are blooming beautifully.
|
||||
The content of this video is about how to edit or delete building management collections in the CREAMS system. The video demonstrates .."""
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
@@ -252,7 +279,7 @@ class QWenCV(GptV4):
|
||||
"fps": 2,
|
||||
},
|
||||
{
|
||||
"text": "视频的内容是什么?,并且,请用恰当的句子总结这个视频。" if self.lang.lower() == "chinese" else "What is the content of the video? And please summarize this video in proper sentences.",
|
||||
"text": prompt_ch if self.lang.lower() == "chinese" else prompt_en,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -60,6 +60,34 @@ class QWenSeq2txt(Base):
|
||||
from dashscope import MultiModalConversation
|
||||
|
||||
audio_path = f"file://{audio_path}"
|
||||
prompt_ch = """
|
||||
你是一名专业的音频转录助手,能够将MP3音频文件的内容转写为文本,并**精确标记每句话或每个段落对应的时间戳**(开始时间-结束时间)。\n
|
||||
**任务要求**:
|
||||
1.输入是MP3,解析带时间戳的文本。
|
||||
2.时间戳格式为 `[HH:MM:SS.mmm]`(毫秒可选),例如 `[00:01:23.456]`。
|
||||
3.时间戳需尽可能贴近实际语音的起止时间,误差不超过1秒。
|
||||
4.如果无法确定具体时间,请根据上下文合理估算。
|
||||
5.最后总结:这段音频在说什么?
|
||||
|
||||
**示例输出**:
|
||||
[00:00:00.000] 今天天气真好,
|
||||
[00:00:02.500] 我们一起去公园散步吧。
|
||||
[00:00:05.800] 公园里的花开得非常漂亮。
|
||||
这段音频讲述的是一个关于**“吃水不忘挖井人”**的感人故事,主 ..."""
|
||||
prompt_en = """
|
||||
You are a professional audio transcription assistant, capable of transcribing the content of MP3 audio files into text and **precisely marking the timestamps (start time - end time) corresponding to each sentence or paragraph**.
|
||||
**Task requirements**:
|
||||
1. Input is MP3, parse text with timestamps.
|
||||
2. The timestamp format is `[HH:MM:SS.mmm]` (milliseconds are optional), for example, `[00:01:23.456]`.
|
||||
3. The timestamp should be as close as possible to the actual start and end time of the voice, with an error not exceeding 1 second.
|
||||
4. If a specific time cannot be determined, please make a reasonable estimation based on the context.
|
||||
5. Final summary: What is this audio talking about?
|
||||
|
||||
**Example Output**:
|
||||
[00:00:00.000] The weather is really nice today,
|
||||
[00:00:02.500] let's go for a walk in the park together.
|
||||
[00:00:05.800] The flowers in the park are blooming beautifully.
|
||||
This audio tells a touching story about **"Remembering the one who dug the well when drinking water"** .."""
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
@@ -68,7 +96,7 @@ class QWenSeq2txt(Base):
|
||||
"audio": audio_path
|
||||
},
|
||||
{
|
||||
"text": "这段音频在说什么?" if self.lang.lower() == "chinese" else "What is this audio saying?",
|
||||
"text": prompt_ch if self.lang.lower() == "chinese" else prompt_en,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.graph.state import CompiledStateGraph
|
||||
|
||||
from app.core.workflow.graph_builder import GraphBuilder
|
||||
@@ -53,11 +54,11 @@ class WorkflowExecutor:
|
||||
self.edges = workflow_config.get("edges", [])
|
||||
self.execution_config = workflow_config.get("execution_config", {})
|
||||
|
||||
self.checkpoint_config = {
|
||||
"configurable": {
|
||||
self.checkpoint_config = RunnableConfig(
|
||||
configurable={
|
||||
"thread_id": uuid.uuid4(),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def _prepare_initial_state(self, input_data: dict[str, Any]) -> WorkflowState:
|
||||
"""准备初始状态(注入系统变量和会话变量)
|
||||
@@ -214,13 +215,13 @@ class WorkflowExecutor:
|
||||
return {
|
||||
"status": "completed",
|
||||
"output": final_output,
|
||||
"variables": result.get("variables", {}),
|
||||
"node_outputs": node_outputs,
|
||||
"messages": result.get("messages", []),
|
||||
"conversation_id": conversation_id,
|
||||
"elapsed_time": elapsed_time,
|
||||
"token_usage": token_usage,
|
||||
"error": result.get("error"),
|
||||
"variables": result.get("variables", {}),
|
||||
}
|
||||
|
||||
def build_graph(self, stream=False) -> CompiledStateGraph:
|
||||
@@ -326,11 +327,10 @@ class WorkflowExecutor:
|
||||
}
|
||||
|
||||
# 1. 构建图
|
||||
graph = self.build_graph(True)
|
||||
graph = self.build_graph(stream=True)
|
||||
|
||||
# 2. 初始化状态(自动注入系统变量)
|
||||
initial_state = self._prepare_initial_state(input_data)
|
||||
|
||||
# 3. Execute workflow
|
||||
try:
|
||||
chunk_count = 0
|
||||
@@ -346,14 +346,16 @@ class WorkflowExecutor:
|
||||
mode, data = event
|
||||
else:
|
||||
# Unexpected format, log and skip
|
||||
logger.warning(f"[STREAM] Unexpected event format: {type(event)}, value: {event}")
|
||||
logger.warning(f"[STREAM] Unexpected event format: {type(event)}, value: {event}"
|
||||
f"- execution_id: {self.execution_id}")
|
||||
continue
|
||||
|
||||
if mode == "custom":
|
||||
# Handle custom streaming events (chunks from nodes via stream writer)
|
||||
chunk_count += 1
|
||||
event_type = data.get("type", "node_chunk") # "message" or "node_chunk"
|
||||
logger.info(f"[CUSTOM] ✅ 收到 {event_type} #{chunk_count} from {data.get('node_id')}")
|
||||
logger.info(f"[CUSTOM] ✅ 收到 {event_type} #{chunk_count} from {data.get('node_id')}"
|
||||
f"- execution_id: {self.execution_id}")
|
||||
yield {
|
||||
"event": event_type, # "message" or "node_chunk"
|
||||
"data": {
|
||||
@@ -380,7 +382,8 @@ class WorkflowExecutor:
|
||||
variables_sys = variables.get("sys", {})
|
||||
conversation_id = input_data.get("conversation_id")
|
||||
execution_id = variables_sys.get("execution_id")
|
||||
logger.info(f"[DEBUG] Node starts execution: {node_name}")
|
||||
logger.info(f"[NODE-START] Node starts execution: {node_name} "
|
||||
f"- execution_id: {self.execution_id}")
|
||||
|
||||
yield {
|
||||
"event": "node_start",
|
||||
@@ -399,7 +402,8 @@ class WorkflowExecutor:
|
||||
variables_sys = variables.get("sys", {})
|
||||
conversation_id = input_data.get("conversation_id")
|
||||
execution_id = variables_sys.get("execution_id")
|
||||
logger.info(f"[DEBUG] Node execution completed: {node_name}")
|
||||
logger.info(f"[NODE-END] Node execution completed: {node_name} "
|
||||
f"- execution_id: {self.execution_id}")
|
||||
|
||||
yield {
|
||||
"event": "node_end",
|
||||
@@ -407,13 +411,15 @@ class WorkflowExecutor:
|
||||
"node_id": node_name,
|
||||
"conversation_id": conversation_id,
|
||||
"execution_id": execution_id,
|
||||
"timestamp": data.get("timestamp")
|
||||
"timestamp": data.get("timestamp"),
|
||||
"state": result.get("node_outputs", {}).get(node_name),
|
||||
}
|
||||
}
|
||||
|
||||
elif mode == "updates":
|
||||
# Handle state updates - store final state
|
||||
logger.debug(f"[UPDATES] 收到 state 更新 from {list(data.keys())}")
|
||||
logger.debug(f"[UPDATES] 收到 state 更新 from {list(data.keys())} "
|
||||
f"- execution_id: {self.execution_id}")
|
||||
|
||||
# 计算耗时
|
||||
end_time = datetime.datetime.now()
|
||||
@@ -421,7 +427,7 @@ class WorkflowExecutor:
|
||||
result = graph.get_state(self.checkpoint_config).values
|
||||
logger.info(
|
||||
f"Workflow execution completed (streaming), "
|
||||
f"total chunks: {chunk_count}, elapsed: {elapsed_time:.2f}s"
|
||||
f"total chunks: {chunk_count}, elapsed: {elapsed_time:.2f}s, execution_id: {self.execution_id}"
|
||||
)
|
||||
|
||||
# 发送 workflow_end 事件
|
||||
@@ -449,7 +455,8 @@ class WorkflowExecutor:
|
||||
}
|
||||
}
|
||||
|
||||
def _extract_final_output(self, node_outputs: dict[str, Any]) -> str | None:
|
||||
@staticmethod
|
||||
def _extract_final_output(node_outputs: dict[str, Any]) -> str | None:
|
||||
"""从节点输出中提取最终输出
|
||||
|
||||
优先级:
|
||||
@@ -473,7 +480,8 @@ class WorkflowExecutor:
|
||||
|
||||
return None
|
||||
|
||||
def _aggregate_token_usage(self, node_outputs: dict[str, Any]) -> dict[str, int] | None:
|
||||
@staticmethod
|
||||
def _aggregate_token_usage(node_outputs: dict[str, Any]) -> dict[str, int] | None:
|
||||
"""聚合所有节点的 token 使用情况
|
||||
|
||||
Args:
|
||||
|
||||
@@ -25,7 +25,7 @@ class WorkflowState(TypedDict):
|
||||
The state object passed between nodes in a workflow, containing messages, variables, node outputs, etc.
|
||||
"""
|
||||
# List of messages (append mode)
|
||||
messages: list[dict[str, str]]
|
||||
messages: Annotated[list[dict[str, str]], lambda x, y: y]
|
||||
|
||||
# Set of loop node IDs, used for assigning values in loop nodes
|
||||
cycle_nodes: list
|
||||
|
||||
@@ -21,6 +21,7 @@ class IterationRuntime:
|
||||
optional parallel execution, flattening of output, and loop control via
|
||||
the workflow state.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
graph: CompiledStateGraph,
|
||||
@@ -87,6 +88,7 @@ class IterationRuntime:
|
||||
self.result.append(output)
|
||||
if not result["looping"]:
|
||||
self.looping = False
|
||||
return result
|
||||
|
||||
def _create_iteration_tasks(self, array_obj, idx):
|
||||
"""
|
||||
@@ -124,7 +126,7 @@ class IterationRuntime:
|
||||
array_obj = VariablePool(self.state).get(input_expression)
|
||||
if not isinstance(array_obj, list):
|
||||
raise RuntimeError("Cannot iterate over a non-list variable")
|
||||
|
||||
child_state = []
|
||||
idx = 0
|
||||
if self.typed_config.parallel:
|
||||
# Execute iterations in parallel batches
|
||||
@@ -132,15 +134,14 @@ class IterationRuntime:
|
||||
tasks = self._create_iteration_tasks(array_obj, idx)
|
||||
logger.info(f"Iteration node {self.node_id}: running, concurrency {len(tasks)}")
|
||||
idx += self.typed_config.parallel_count
|
||||
await asyncio.gather(*tasks)
|
||||
logger.info(f"Iteration node {self.node_id}: execution completed")
|
||||
return self.result
|
||||
child_state.extend(await asyncio.gather(*tasks))
|
||||
else:
|
||||
# Execute iterations sequentially
|
||||
while idx < len(array_obj) and self.looping:
|
||||
logger.info(f"Iteration node {self.node_id}: running")
|
||||
item = array_obj[idx]
|
||||
result = await self.graph.ainvoke(self._init_iteration_state(item, idx))
|
||||
child_state.append(result)
|
||||
output = VariablePool(result).get(self.output_value)
|
||||
if isinstance(output, list) and self.typed_config.flatten:
|
||||
self.result.extend(output)
|
||||
@@ -150,5 +151,8 @@ class IterationRuntime:
|
||||
self.looping = False
|
||||
idx += 1
|
||||
|
||||
logger.info(f"Iteration node {self.node_id}: execution completed")
|
||||
return self.result
|
||||
logger.info(f"Iteration node {self.node_id}: execution completed")
|
||||
return {
|
||||
"output": self.result,
|
||||
"__child_state": child_state
|
||||
}
|
||||
|
||||
@@ -67,7 +67,9 @@ class LoopRuntime:
|
||||
variables=pool.get_all_conversation_vars(),
|
||||
node_outputs=pool.get_all_node_outputs(),
|
||||
system_vars=pool.get_all_system_vars(),
|
||||
) if variable.input_type == ValueInputType.VARIABLE else TypeTransformer.transform(variable.value, variable.type)
|
||||
)
|
||||
if variable.input_type == ValueInputType.VARIABLE
|
||||
else TypeTransformer.transform(variable.value, variable.type)
|
||||
for variable in self.typed_config.cycle_vars
|
||||
}
|
||||
self.state["node_outputs"][self.node_id] = {
|
||||
@@ -76,7 +78,9 @@ class LoopRuntime:
|
||||
variables=pool.get_all_conversation_vars(),
|
||||
node_outputs=pool.get_all_node_outputs(),
|
||||
system_vars=pool.get_all_system_vars(),
|
||||
) if variable.input_type == ValueInputType.VARIABLE else TypeTransformer.transform(variable.value, variable.type)
|
||||
)
|
||||
if variable.input_type == ValueInputType.VARIABLE
|
||||
else TypeTransformer.transform(variable.value, variable.type)
|
||||
for variable in self.typed_config.cycle_vars
|
||||
}
|
||||
loopstate = WorkflowState(
|
||||
@@ -171,10 +175,11 @@ class LoopRuntime:
|
||||
"""
|
||||
loopstate = self._init_loop_state()
|
||||
loop_time = self.typed_config.max_loop
|
||||
child_state = []
|
||||
while self.evaluate_conditional(loopstate) and loopstate["looping"] and loop_time > 0:
|
||||
logger.info(f"loop node {self.node_id}: running")
|
||||
await self.graph.ainvoke(loopstate)
|
||||
child_state.append(await self.graph.ainvoke(loopstate))
|
||||
loop_time -= 1
|
||||
|
||||
logger.info(f"loop node {self.node_id}: execution completed")
|
||||
return loopstate["runtime_vars"][self.node_id]
|
||||
return loopstate["runtime_vars"][self.node_id] | {"__child_state": child_state}
|
||||
|
||||
@@ -10,9 +10,8 @@ from app.core.workflow.nodes.base_node import BaseNode, WorkflowState
|
||||
from app.core.workflow.nodes.knowledge import KnowledgeRetrievalNodeConfig
|
||||
from app.db import get_db_read
|
||||
from app.models import knowledge_model, knowledgeshare_model, ModelType
|
||||
from app.repositories import knowledge_repository
|
||||
from app.repositories import knowledge_repository, knowledgeshare_repository
|
||||
from app.schemas.chunk_schema import RetrieveType
|
||||
from app.services import knowledge_service, knowledgeshare_service
|
||||
from app.services.model_service import ModelConfigService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -96,7 +95,7 @@ class KnowledgeRetrievalNode(BaseNode):
|
||||
|
||||
filters = self._build_kb_filter(kb_ids, knowledge_model.PermissionType.Share)
|
||||
|
||||
share_ids = knowledge_service.knowledge_repository.get_chunked_knowledgeids(
|
||||
share_ids = knowledge_repository.get_chunked_knowledgeids(
|
||||
db=db,
|
||||
filters=filters
|
||||
)
|
||||
@@ -105,7 +104,7 @@ class KnowledgeRetrievalNode(BaseNode):
|
||||
filters = [
|
||||
knowledgeshare_model.KnowledgeShare.target_kb_id.in_(kb_ids)
|
||||
]
|
||||
items = knowledgeshare_service.knowledgeshare_repository.get_source_kb_ids_by_target_kb_id(
|
||||
items = knowledgeshare_repository.get_source_kb_ids_by_target_kb_id(
|
||||
db=db,
|
||||
filters=filters
|
||||
)
|
||||
|
||||
@@ -66,7 +66,7 @@ class LLMNodeConfig(BaseNodeConfig):
|
||||
)
|
||||
|
||||
memory: MemoryWindowSetting = Field(
|
||||
...,
|
||||
default_factory=MemoryWindowSetting,
|
||||
description="对话上下文窗口"
|
||||
)
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ class LLMNode(BaseNode):
|
||||
"""
|
||||
|
||||
# 1. 处理消息格式(优先使用 messages)
|
||||
self.typed_config = LLMNodeConfig(**self.config)
|
||||
messages_config = self.typed_config.messages
|
||||
|
||||
if messages_config:
|
||||
@@ -167,7 +168,7 @@ class LLMNode(BaseNode):
|
||||
Returns:
|
||||
LLM 响应消息
|
||||
"""
|
||||
self.typed_config = LLMNodeConfig(**self.config)
|
||||
# self.typed_config = LLMNodeConfig(**self.config)
|
||||
llm, prompt_or_messages = self._prepare_llm(state, True)
|
||||
|
||||
logger.info(f"节点 {self.node_id} 开始执行 LLM 调用(非流式)")
|
||||
@@ -269,12 +270,16 @@ class LLMNode(BaseNode):
|
||||
chunk_count = 0
|
||||
|
||||
# 调用 LLM(流式,支持字符串或消息列表)
|
||||
async for chunk in llm.astream(prompt_or_messages):
|
||||
last_meta_data = {}
|
||||
async for chunk in llm.astream(prompt_or_messages, stream_usage=True):
|
||||
# 提取内容
|
||||
if hasattr(chunk, 'content'):
|
||||
content = chunk.content
|
||||
else:
|
||||
content = str(chunk)
|
||||
if hasattr(chunk, 'response_metadata'):
|
||||
if chunk.response_metadata:
|
||||
last_meta_data = chunk.response_metadata
|
||||
|
||||
# 只有当内容不为空时才处理
|
||||
if content:
|
||||
@@ -288,13 +293,10 @@ class LLMNode(BaseNode):
|
||||
logger.info(f"节点 {self.node_id} LLM 调用完成,输出长度: {len(full_response)}, 总 chunks: {chunk_count}")
|
||||
|
||||
# 构建完整的 AIMessage(包含元数据)
|
||||
if isinstance(last_chunk, AIMessage):
|
||||
final_message = AIMessage(
|
||||
content=full_response,
|
||||
response_metadata=last_chunk.response_metadata if hasattr(last_chunk, 'response_metadata') else {}
|
||||
)
|
||||
else:
|
||||
final_message = AIMessage(content=full_response)
|
||||
final_message = AIMessage(
|
||||
content=full_response,
|
||||
response_metadata=last_meta_data
|
||||
)
|
||||
|
||||
# yield 完成标记
|
||||
yield {"__final__": True, "result": final_message}
|
||||
|
||||
@@ -27,8 +27,6 @@ from .tool_model import (
|
||||
ToolExecution, ToolType, ToolStatus, AuthType, ExecutionStatus
|
||||
)
|
||||
from .memory_perceptual_model import MemoryPerceptualModel
|
||||
from .emotion_suggestions_cache_model import EmotionSuggestionsCache
|
||||
from .implicit_memory_cache_model import ImplicitMemoryCache
|
||||
|
||||
__all__ = [
|
||||
"Tenants",
|
||||
@@ -79,6 +77,4 @@ __all__ = [
|
||||
"AuthType",
|
||||
"ExecutionStatus",
|
||||
"MemoryPerceptualModel",
|
||||
"EmotionSuggestionsCache",
|
||||
"ImplicitMemoryCache"
|
||||
]
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
"""情绪建议缓存模型"""
|
||||
|
||||
import uuid
|
||||
import datetime
|
||||
from sqlalchemy import Column, String, Text, Integer, DateTime, JSON
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from app.db import Base
|
||||
|
||||
|
||||
class EmotionSuggestionsCache(Base):
|
||||
"""情绪建议缓存表
|
||||
|
||||
用于缓存个性化情绪建议,减少 LLM 调用成本,提升响应速度。
|
||||
"""
|
||||
__tablename__ = "emotion_suggestions_cache"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
||||
end_user_id = Column(String(255), nullable=False, unique=True, index=True, comment="终端用户ID(组ID)")
|
||||
health_summary = Column(Text, nullable=False, comment="健康状态摘要")
|
||||
suggestions = Column(JSON, nullable=False, comment="建议列表(JSON格式)")
|
||||
generated_at = Column(DateTime, nullable=False, default=datetime.datetime.now, comment="生成时间")
|
||||
expires_at = Column(DateTime, nullable=True, comment="过期时间")
|
||||
created_at = Column(DateTime, default=datetime.datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now)
|
||||
@@ -1,27 +0,0 @@
|
||||
"""隐性记忆缓存模型"""
|
||||
|
||||
import uuid
|
||||
import datetime
|
||||
from sqlalchemy import Column, String, Integer, DateTime, JSON
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from app.db import Base
|
||||
|
||||
|
||||
class ImplicitMemoryCache(Base):
|
||||
"""隐性记忆缓存表
|
||||
|
||||
用于缓存用户的完整隐性记忆画像,包括偏好标签、四维画像、兴趣领域和行为习惯。
|
||||
减少 LLM 调用成本,提升响应速度。
|
||||
"""
|
||||
__tablename__ = "implicit_memory_cache"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
||||
end_user_id = Column(String(255), nullable=False, unique=True, index=True, comment="终端用户ID")
|
||||
preferences = Column(JSON, nullable=False, comment="偏好标签列表(JSON格式)")
|
||||
portrait = Column(JSON, nullable=False, comment="四维画像对象(JSON格式)")
|
||||
interest_areas = Column(JSON, nullable=False, comment="兴趣领域分布对象(JSON格式)")
|
||||
habits = Column(JSON, nullable=False, comment="行为习惯列表(JSON格式)")
|
||||
generated_at = Column(DateTime, nullable=False, default=datetime.datetime.now, comment="生成时间")
|
||||
expires_at = Column(DateTime, nullable=True, comment="过期时间")
|
||||
created_at = Column(DateTime, default=datetime.datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now)
|
||||
@@ -75,6 +75,14 @@ class WorkflowExecution(Base):
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
|
||||
release_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("app_releases.id", ondelete="CASCADE"),
|
||||
nullable=True,
|
||||
index=True
|
||||
)
|
||||
|
||||
app_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("apps.id", ondelete="CASCADE"),
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
"""情绪建议缓存仓储层"""
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional, Dict, Any
|
||||
import datetime
|
||||
|
||||
from app.models.emotion_suggestions_cache_model import EmotionSuggestionsCache
|
||||
from app.core.logging_config import get_db_logger
|
||||
|
||||
# 获取数据库专用日志器
|
||||
db_logger = get_db_logger()
|
||||
|
||||
|
||||
class EmotionSuggestionsCacheRepository:
|
||||
"""情绪建议缓存仓储类"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def get_by_end_user_id(self, end_user_id: str) -> Optional[EmotionSuggestionsCache]:
|
||||
"""根据终端用户ID获取缓存
|
||||
|
||||
Args:
|
||||
end_user_id: 终端用户ID(组ID)
|
||||
|
||||
Returns:
|
||||
缓存记录,如果不存在返回 None
|
||||
"""
|
||||
try:
|
||||
cache = (
|
||||
self.db.query(EmotionSuggestionsCache)
|
||||
.filter(EmotionSuggestionsCache.end_user_id == end_user_id)
|
||||
.first()
|
||||
)
|
||||
if cache:
|
||||
db_logger.info(f"成功获取用户 {end_user_id} 的情绪建议缓存")
|
||||
else:
|
||||
db_logger.info(f"用户 {end_user_id} 的情绪建议缓存不存在")
|
||||
return cache
|
||||
except Exception as e:
|
||||
db_logger.error(f"获取用户 {end_user_id} 的情绪建议缓存失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def create_or_update(
|
||||
self,
|
||||
end_user_id: str,
|
||||
health_summary: str,
|
||||
suggestions: list,
|
||||
expires_hours: int = 24
|
||||
) -> EmotionSuggestionsCache:
|
||||
"""创建或更新缓存
|
||||
|
||||
Args:
|
||||
end_user_id: 终端用户ID(组ID)
|
||||
health_summary: 健康状态摘要
|
||||
suggestions: 建议列表
|
||||
expires_hours: 过期时间(小时),默认24小时
|
||||
|
||||
Returns:
|
||||
缓存记录
|
||||
"""
|
||||
try:
|
||||
# 查找现有记录
|
||||
cache = self.get_by_end_user_id(end_user_id)
|
||||
|
||||
now = datetime.datetime.now()
|
||||
expires_at = now + datetime.timedelta(hours=expires_hours)
|
||||
|
||||
if cache:
|
||||
# 更新现有记录
|
||||
cache.health_summary = health_summary
|
||||
cache.suggestions = suggestions
|
||||
cache.generated_at = now
|
||||
cache.expires_at = expires_at
|
||||
cache.updated_at = now
|
||||
db_logger.info(f"更新用户 {end_user_id} 的情绪建议缓存")
|
||||
else:
|
||||
# 创建新记录
|
||||
cache = EmotionSuggestionsCache(
|
||||
end_user_id=end_user_id,
|
||||
health_summary=health_summary,
|
||||
suggestions=suggestions,
|
||||
generated_at=now,
|
||||
expires_at=expires_at,
|
||||
created_at=now,
|
||||
updated_at=now
|
||||
)
|
||||
self.db.add(cache)
|
||||
db_logger.info(f"创建用户 {end_user_id} 的情绪建议缓存")
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(cache)
|
||||
return cache
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
db_logger.error(f"创建或更新用户 {end_user_id} 的情绪建议缓存失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def delete_by_end_user_id(self, end_user_id: str) -> bool:
|
||||
"""删除缓存
|
||||
|
||||
Args:
|
||||
end_user_id: 终端用户ID(组ID)
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
"""
|
||||
try:
|
||||
cache = self.get_by_end_user_id(end_user_id)
|
||||
if cache:
|
||||
self.db.delete(cache)
|
||||
self.db.commit()
|
||||
db_logger.info(f"删除用户 {end_user_id} 的情绪建议缓存")
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
db_logger.error(f"删除用户 {end_user_id} 的情绪建议缓存失败: {str(e)}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def is_expired(cache: EmotionSuggestionsCache) -> bool:
|
||||
"""检查缓存是否过期
|
||||
|
||||
Args:
|
||||
cache: 缓存记录
|
||||
|
||||
Returns:
|
||||
是否过期
|
||||
"""
|
||||
if cache.expires_at is None:
|
||||
return False
|
||||
return datetime.datetime.now() > cache.expires_at
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def get_cache_by_end_user_id(db: Session, end_user_id: str) -> Optional[EmotionSuggestionsCache]:
|
||||
"""根据终端用户ID获取缓存"""
|
||||
repo = EmotionSuggestionsCacheRepository(db)
|
||||
return repo.get_by_end_user_id(end_user_id)
|
||||
|
||||
|
||||
def create_or_update_cache(
|
||||
db: Session,
|
||||
end_user_id: str,
|
||||
health_summary: str,
|
||||
suggestions: list,
|
||||
expires_hours: int = 24
|
||||
) -> EmotionSuggestionsCache:
|
||||
"""创建或更新缓存"""
|
||||
repo = EmotionSuggestionsCacheRepository(db)
|
||||
return repo.create_or_update(end_user_id, health_summary, suggestions, expires_hours)
|
||||
|
||||
|
||||
def delete_cache_by_end_user_id(db: Session, end_user_id: str) -> bool:
|
||||
"""删除缓存"""
|
||||
repo = EmotionSuggestionsCacheRepository(db)
|
||||
return repo.delete_by_end_user_id(end_user_id)
|
||||
|
||||
|
||||
def is_cache_expired(cache: EmotionSuggestionsCache) -> bool:
|
||||
"""检查缓存是否过期"""
|
||||
return EmotionSuggestionsCacheRepository.is_expired(cache)
|
||||
@@ -1,175 +0,0 @@
|
||||
"""隐性记忆缓存仓储层"""
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional, Dict, Any
|
||||
import datetime
|
||||
|
||||
from app.models.implicit_memory_cache_model import ImplicitMemoryCache
|
||||
from app.core.logging_config import get_db_logger
|
||||
|
||||
# 获取数据库专用日志器
|
||||
db_logger = get_db_logger()
|
||||
|
||||
|
||||
class ImplicitMemoryCacheRepository:
|
||||
"""隐性记忆缓存仓储类"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def get_by_end_user_id(self, end_user_id: str) -> Optional[ImplicitMemoryCache]:
|
||||
"""根据终端用户ID获取缓存
|
||||
|
||||
Args:
|
||||
end_user_id: 终端用户ID
|
||||
|
||||
Returns:
|
||||
缓存记录,如果不存在返回 None
|
||||
"""
|
||||
try:
|
||||
cache = (
|
||||
self.db.query(ImplicitMemoryCache)
|
||||
.filter(ImplicitMemoryCache.end_user_id == end_user_id)
|
||||
.first()
|
||||
)
|
||||
if cache:
|
||||
db_logger.info(f"成功获取用户 {end_user_id} 的隐性记忆缓存")
|
||||
else:
|
||||
db_logger.info(f"用户 {end_user_id} 的隐性记忆缓存不存在")
|
||||
return cache
|
||||
except Exception as e:
|
||||
db_logger.error(f"获取用户 {end_user_id} 的隐性记忆缓存失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def create_or_update(
|
||||
self,
|
||||
end_user_id: str,
|
||||
preferences: list,
|
||||
portrait: dict,
|
||||
interest_areas: dict,
|
||||
habits: list,
|
||||
expires_hours: int = 168 # 默认7天
|
||||
) -> ImplicitMemoryCache:
|
||||
"""创建或更新缓存
|
||||
|
||||
Args:
|
||||
end_user_id: 终端用户ID
|
||||
preferences: 偏好标签列表
|
||||
portrait: 四维画像对象
|
||||
interest_areas: 兴趣领域分布对象
|
||||
habits: 行为习惯列表
|
||||
expires_hours: 过期时间(小时),默认168小时(7天)
|
||||
|
||||
Returns:
|
||||
缓存记录
|
||||
"""
|
||||
try:
|
||||
# 查找现有记录
|
||||
cache = self.get_by_end_user_id(end_user_id)
|
||||
|
||||
now = datetime.datetime.now()
|
||||
expires_at = now + datetime.timedelta(hours=expires_hours)
|
||||
|
||||
if cache:
|
||||
# 更新现有记录
|
||||
cache.preferences = preferences
|
||||
cache.portrait = portrait
|
||||
cache.interest_areas = interest_areas
|
||||
cache.habits = habits
|
||||
cache.generated_at = now
|
||||
cache.expires_at = expires_at
|
||||
cache.updated_at = now
|
||||
db_logger.info(f"更新用户 {end_user_id} 的隐性记忆缓存")
|
||||
else:
|
||||
# 创建新记录
|
||||
cache = ImplicitMemoryCache(
|
||||
end_user_id=end_user_id,
|
||||
preferences=preferences,
|
||||
portrait=portrait,
|
||||
interest_areas=interest_areas,
|
||||
habits=habits,
|
||||
generated_at=now,
|
||||
expires_at=expires_at,
|
||||
created_at=now,
|
||||
updated_at=now
|
||||
)
|
||||
self.db.add(cache)
|
||||
db_logger.info(f"创建用户 {end_user_id} 的隐性记忆缓存")
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(cache)
|
||||
return cache
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
db_logger.error(f"创建或更新用户 {end_user_id} 的隐性记忆缓存失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def delete_by_end_user_id(self, end_user_id: str) -> bool:
|
||||
"""删除缓存
|
||||
|
||||
Args:
|
||||
end_user_id: 终端用户ID
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
"""
|
||||
try:
|
||||
cache = self.get_by_end_user_id(end_user_id)
|
||||
if cache:
|
||||
self.db.delete(cache)
|
||||
self.db.commit()
|
||||
db_logger.info(f"删除用户 {end_user_id} 的隐性记忆缓存")
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
db_logger.error(f"删除用户 {end_user_id} 的隐性记忆缓存失败: {str(e)}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def is_expired(cache: ImplicitMemoryCache) -> bool:
|
||||
"""检查缓存是否过期
|
||||
|
||||
Args:
|
||||
cache: 缓存记录
|
||||
|
||||
Returns:
|
||||
是否过期
|
||||
"""
|
||||
if cache.expires_at is None:
|
||||
return False
|
||||
return datetime.datetime.now() > cache.expires_at
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def get_cache_by_end_user_id(db: Session, end_user_id: str) -> Optional[ImplicitMemoryCache]:
|
||||
"""根据终端用户ID获取缓存"""
|
||||
repo = ImplicitMemoryCacheRepository(db)
|
||||
return repo.get_by_end_user_id(end_user_id)
|
||||
|
||||
|
||||
def create_or_update_cache(
|
||||
db: Session,
|
||||
end_user_id: str,
|
||||
preferences: list,
|
||||
portrait: dict,
|
||||
interest_areas: dict,
|
||||
habits: list,
|
||||
expires_hours: int = 168
|
||||
) -> ImplicitMemoryCache:
|
||||
"""创建或更新缓存"""
|
||||
repo = ImplicitMemoryCacheRepository(db)
|
||||
return repo.create_or_update(
|
||||
end_user_id, preferences, portrait, interest_areas, habits, expires_hours
|
||||
)
|
||||
|
||||
|
||||
def delete_cache_by_end_user_id(db: Session, end_user_id: str) -> bool:
|
||||
"""删除缓存"""
|
||||
repo = ImplicitMemoryCacheRepository(db)
|
||||
return repo.delete_by_end_user_id(end_user_id)
|
||||
|
||||
|
||||
def is_cache_expired(cache: ImplicitMemoryCache) -> bool:
|
||||
"""检查缓存是否过期"""
|
||||
return ImplicitMemoryCacheRepository.is_expired(cache)
|
||||
@@ -34,5 +34,4 @@ class EmotionSuggestionsRequest(BaseModel):
|
||||
|
||||
class EmotionGenerateSuggestionsRequest(BaseModel):
|
||||
"""生成个性化情绪建议请求"""
|
||||
group_id: str = Field(..., description="组ID")
|
||||
config_id: Optional[int] = Field(None, description="配置ID(用于指定LLM模型)")
|
||||
end_user_id: str = Field(..., description="终端用户ID")
|
||||
|
||||
@@ -527,6 +527,7 @@ class AppChatService:
|
||||
conversation_id: uuid.UUID,
|
||||
config: WorkflowConfig,
|
||||
app_id: uuid.UUID,
|
||||
release_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
user_id: Optional[str] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
@@ -549,6 +550,7 @@ class AppChatService:
|
||||
payload=payload,
|
||||
config=config,
|
||||
workspace_id=workspace_id,
|
||||
release_id=release_id,
|
||||
)
|
||||
|
||||
async def workflow_chat_stream(
|
||||
@@ -557,6 +559,7 @@ class AppChatService:
|
||||
conversation_id: uuid.UUID,
|
||||
config: WorkflowConfig,
|
||||
app_id: uuid.UUID,
|
||||
release_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
user_id: str = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
@@ -565,7 +568,7 @@ class AppChatService:
|
||||
storage_type: Optional[str] = None,
|
||||
user_rag_memory_id: Optional[str] = None,
|
||||
|
||||
) -> AsyncGenerator[str, None]:
|
||||
) -> AsyncGenerator[dict, None]:
|
||||
"""聊天(流式)"""
|
||||
workflow_service = WorkflowService(self.db)
|
||||
payload = DraftRunRequest(
|
||||
@@ -580,6 +583,7 @@ class AppChatService:
|
||||
payload=payload,
|
||||
config=config,
|
||||
workspace_id=workspace_id,
|
||||
release_id=release_id
|
||||
):
|
||||
yield event
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ class AppService:
|
||||
Raises:
|
||||
ResourceNotFoundException: 当应用不存在时
|
||||
"""
|
||||
app = get_apps_by_id(self.db,app_id)
|
||||
app = get_apps_by_id(self.db, app_id)
|
||||
if not app:
|
||||
logger.warning("应用不存在", extra={"app_id": str(app_id)})
|
||||
raise ResourceNotFoundException("应用", str(app_id))
|
||||
@@ -227,7 +227,6 @@ class AppService:
|
||||
if not model_api_key:
|
||||
raise ResourceNotFoundException("模型配置", str(multi_agent_config.default_model_config_id))
|
||||
|
||||
|
||||
# 3. 检查子 Agent 配置
|
||||
if not multi_agent_config.sub_agents or len(multi_agent_config.sub_agents) == 0:
|
||||
raise BusinessException(
|
||||
@@ -281,10 +280,10 @@ class AppService:
|
||||
)
|
||||
|
||||
def _create_agent_config(
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
config_data: app_schema.AgentConfigCreate,
|
||||
now: datetime.datetime
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
config_data: app_schema.AgentConfigCreate,
|
||||
now: datetime.datetime
|
||||
) -> None:
|
||||
"""创建 Agent 配置(内部方法)
|
||||
|
||||
@@ -313,10 +312,10 @@ class AppService:
|
||||
logger.debug("Agent 配置已创建", extra={"app_id": str(app_id)})
|
||||
|
||||
def _create_multi_agent_config(
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
config_data: Dict[str, Any],
|
||||
now: datetime.datetime
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
config_data: Dict[str, Any],
|
||||
now: datetime.datetime
|
||||
) -> None:
|
||||
"""创建多 Agent 配置(内部方法)
|
||||
|
||||
@@ -411,9 +410,9 @@ class AppService:
|
||||
return 1 if max_ver is None else int(max_ver) + 1
|
||||
|
||||
def _convert_to_schema(
|
||||
self,
|
||||
app: App,
|
||||
current_workspace_id: uuid.UUID
|
||||
self,
|
||||
app: App,
|
||||
current_workspace_id: uuid.UUID
|
||||
) -> app_schema.App:
|
||||
"""将 App 模型转换为 Schema,并设置 is_shared 字段
|
||||
|
||||
@@ -447,9 +446,9 @@ class AppService:
|
||||
# ==================== 应用管理 ====================
|
||||
|
||||
def get_app(
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> App:
|
||||
"""获取应用详情
|
||||
|
||||
@@ -469,11 +468,11 @@ class AppService:
|
||||
return app
|
||||
|
||||
def create_app(
|
||||
self,
|
||||
*,
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
data: app_schema.AppCreate
|
||||
self,
|
||||
*,
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
data: app_schema.AppCreate
|
||||
) -> App:
|
||||
"""创建应用
|
||||
|
||||
@@ -535,11 +534,11 @@ class AppService:
|
||||
raise BusinessException(f"应用创建失败: {str(e)}", BizCode.INTERNAL_ERROR, cause=e)
|
||||
|
||||
def update_app(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
data: app_schema.AppUpdate,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
data: app_schema.AppUpdate,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> App:
|
||||
"""更新应用基本信息
|
||||
|
||||
@@ -578,10 +577,10 @@ class AppService:
|
||||
return app
|
||||
|
||||
def delete_app(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> None:
|
||||
"""删除应用
|
||||
|
||||
@@ -612,12 +611,12 @@ class AppService:
|
||||
)
|
||||
|
||||
def copy_app(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None,
|
||||
new_name: Optional[str] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None,
|
||||
new_name: Optional[str] = None
|
||||
) -> App:
|
||||
"""复制应用(包括基础信息和配置)
|
||||
|
||||
@@ -716,16 +715,16 @@ class AppService:
|
||||
raise BusinessException(f"应用复制失败: {str(e)}", BizCode.INTERNAL_ERROR, cause=e)
|
||||
|
||||
def list_apps(
|
||||
self,
|
||||
*,
|
||||
workspace_id: uuid.UUID,
|
||||
type: Optional[str] = None,
|
||||
visibility: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
include_shared: bool = True,
|
||||
page: int = 1,
|
||||
pagesize: int = 10,
|
||||
self,
|
||||
*,
|
||||
workspace_id: uuid.UUID,
|
||||
type: Optional[str] = None,
|
||||
visibility: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
include_shared: bool = True,
|
||||
page: int = 1,
|
||||
pagesize: int = 10,
|
||||
) -> Tuple[List[App], int]:
|
||||
"""列出工作空间中的应用(分页)
|
||||
|
||||
@@ -759,8 +758,7 @@ class AppService:
|
||||
)
|
||||
|
||||
# 构建查询条件
|
||||
filters = []
|
||||
filters.append(App.is_active == True)
|
||||
filters = [App.is_active == True]
|
||||
if type:
|
||||
filters.append(App.type == type)
|
||||
if visibility:
|
||||
@@ -813,9 +811,9 @@ class AppService:
|
||||
return items, int(total)
|
||||
|
||||
def get_apps_by_ids(
|
||||
self,
|
||||
app_ids: List[str],
|
||||
workspace_id: uuid.UUID
|
||||
self,
|
||||
app_ids: List[str],
|
||||
workspace_id: uuid.UUID
|
||||
) -> List[App]:
|
||||
"""根据ID列表获取应用
|
||||
|
||||
@@ -846,11 +844,11 @@ class AppService:
|
||||
# ==================== Agent 配置管理 ====================
|
||||
|
||||
def update_agent_config(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
data: app_schema.AgentConfigUpdate,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
data: app_schema.AgentConfigUpdate,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> AgentConfig:
|
||||
"""更新 Agent 配置
|
||||
|
||||
@@ -875,7 +873,8 @@ class AppService:
|
||||
|
||||
self._validate_workspace_access(app, workspace_id)
|
||||
|
||||
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active==True).order_by(AgentConfig.updated_at.desc())
|
||||
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by(
|
||||
AgentConfig.updated_at.desc())
|
||||
agent_cfg: Optional[AgentConfig] = self.db.scalars(stmt).first()
|
||||
now = datetime.datetime.now()
|
||||
|
||||
@@ -918,10 +917,10 @@ class AppService:
|
||||
return agent_cfg
|
||||
|
||||
def get_agent_config(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> AgentConfig:
|
||||
"""获取 Agent 配置
|
||||
|
||||
@@ -948,7 +947,12 @@ class AppService:
|
||||
# 只读操作,允许访问共享应用
|
||||
self._validate_app_accessible(app, workspace_id)
|
||||
|
||||
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by(AgentConfig.updated_at.desc())
|
||||
stmt = select(AgentConfig).where(
|
||||
AgentConfig.app_id == app_id,
|
||||
AgentConfig.is_active.is_(True)
|
||||
).order_by(
|
||||
AgentConfig.updated_at.desc()
|
||||
)
|
||||
config = self.db.scalars(stmt).first()
|
||||
|
||||
if config:
|
||||
@@ -1166,13 +1170,13 @@ class AppService:
|
||||
# ==================== 应用发布管理 ====================
|
||||
|
||||
def publish(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
publisher_id: uuid.UUID,
|
||||
version_name: str,
|
||||
workspace_id: Optional[uuid.UUID] = None,
|
||||
release_notes: Optional[str] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
publisher_id: uuid.UUID,
|
||||
version_name: str,
|
||||
workspace_id: Optional[uuid.UUID] = None,
|
||||
release_notes: Optional[str] = None
|
||||
) -> AppRelease:
|
||||
"""发布应用(创建不可变快照)
|
||||
|
||||
@@ -1200,7 +1204,8 @@ class AppService:
|
||||
default_model_config_id = None
|
||||
|
||||
if app.type == AppType.AGENT:
|
||||
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by(AgentConfig.updated_at.desc())
|
||||
stmt = select(AgentConfig).where(AgentConfig.app_id == app_id, AgentConfig.is_active == True).order_by(
|
||||
AgentConfig.updated_at.desc())
|
||||
agent_cfg = self.db.scalars(stmt).first()
|
||||
if not agent_cfg:
|
||||
raise BusinessException("Agent 应用缺少配置,无法发布", BizCode.AGENT_CONFIG_MISSING)
|
||||
@@ -1236,8 +1241,7 @@ class AppService:
|
||||
default_model_config_id = multi_agent_cfg.default_model_config_id
|
||||
|
||||
# 4. 构建配置快照
|
||||
|
||||
|
||||
|
||||
config = {
|
||||
"model_parameters": model_parameters_to_dict(multi_agent_cfg.model_parameters),
|
||||
"master_agent_id": str(multi_agent_cfg.master_agent_id),
|
||||
@@ -1264,6 +1268,7 @@ class AppService:
|
||||
raise BusinessException("应用缺少有效配置,无法发布", BizCode.CONFIG_MISSING)
|
||||
|
||||
config = {
|
||||
"id": str(workflow_cfg.id),
|
||||
"nodes": workflow_cfg.nodes,
|
||||
"edges": workflow_cfg.edges,
|
||||
"variables": workflow_cfg.variables,
|
||||
@@ -1285,7 +1290,7 @@ class AppService:
|
||||
id=uuid.uuid4(),
|
||||
app_id=app_id,
|
||||
version=version,
|
||||
version_name = version_name,
|
||||
version_name=version_name,
|
||||
release_notes=release_notes,
|
||||
name=app.name,
|
||||
description=app.description,
|
||||
@@ -1319,10 +1324,10 @@ class AppService:
|
||||
return release
|
||||
|
||||
def get_current_release(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> Optional[AppRelease]:
|
||||
"""获取当前发布版本
|
||||
|
||||
@@ -1349,10 +1354,10 @@ class AppService:
|
||||
return self.db.get(AppRelease, app.current_release_id)
|
||||
|
||||
def list_releases(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> List[AppRelease]:
|
||||
"""列出应用的所有发布版本(倒序)
|
||||
|
||||
@@ -1381,11 +1386,11 @@ class AppService:
|
||||
return list(self.db.scalars(stmt).all())
|
||||
|
||||
def rollback(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
version: int,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
version: int,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> AppRelease:
|
||||
"""回滚到指定版本
|
||||
|
||||
@@ -1434,12 +1439,12 @@ class AppService:
|
||||
# ==================== 应用分享功能 ====================
|
||||
|
||||
def share_app(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
target_workspace_ids: List[uuid.UUID],
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
target_workspace_ids: List[uuid.UUID],
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> AppShare:
|
||||
"""分享应用到其他工作空间
|
||||
|
||||
@@ -1457,7 +1462,6 @@ class AppService:
|
||||
BusinessException: 当应用不在指定工作空间或目标工作空间无效时
|
||||
"""
|
||||
|
||||
|
||||
logger.info(
|
||||
"分享应用",
|
||||
extra={
|
||||
@@ -1536,11 +1540,11 @@ class AppService:
|
||||
return shares
|
||||
|
||||
def unshare_app(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
target_workspace_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
target_workspace_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> None:
|
||||
"""取消应用分享
|
||||
|
||||
@@ -1594,10 +1598,10 @@ class AppService:
|
||||
)
|
||||
|
||||
def list_app_shares(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> List[AppShare]:
|
||||
"""列出应用的所有分享记录
|
||||
|
||||
@@ -1637,14 +1641,14 @@ class AppService:
|
||||
# ==================== 试运行功能 ====================
|
||||
|
||||
async def draft_run(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
message: str,
|
||||
conversation_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
message: str,
|
||||
conversation_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""试运行 Agent(使用当前草稿配置)
|
||||
|
||||
@@ -1736,14 +1740,14 @@ class AppService:
|
||||
return result
|
||||
|
||||
async def draft_run_stream(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
message: str,
|
||||
conversation_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
message: str,
|
||||
conversation_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
):
|
||||
"""试运行 Agent(流式返回)
|
||||
|
||||
@@ -1794,30 +1798,30 @@ class AppService:
|
||||
# 4. 调用流式试运行服务
|
||||
draft_service = DraftRunService(self.db)
|
||||
async for event in draft_service.run_stream(
|
||||
agent_config=agent_cfg,
|
||||
model_config=model_config,
|
||||
message=message,
|
||||
workspace_id=workspace_id,
|
||||
conversation_id=conversation_id,
|
||||
user_id=user_id,
|
||||
variables=variables
|
||||
agent_config=agent_cfg,
|
||||
model_config=model_config,
|
||||
message=message,
|
||||
workspace_id=workspace_id,
|
||||
conversation_id=conversation_id,
|
||||
user_id=user_id,
|
||||
variables=variables
|
||||
):
|
||||
yield event
|
||||
|
||||
# ==================== 多模型对比试运行 ====================
|
||||
|
||||
async def draft_run_compare(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
message: str,
|
||||
models: List[app_schema.ModelCompareItem],
|
||||
conversation_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
workspace_id: Optional[uuid.UUID] = None,
|
||||
parallel: bool = True,
|
||||
timeout: int = 60
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
message: str,
|
||||
models: List[app_schema.ModelCompareItem],
|
||||
conversation_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
workspace_id: Optional[uuid.UUID] = None,
|
||||
parallel: bool = True,
|
||||
timeout: int = 60
|
||||
) -> Dict[str, Any]:
|
||||
"""多模型对比试运行
|
||||
|
||||
@@ -1907,17 +1911,17 @@ class AppService:
|
||||
return result
|
||||
|
||||
async def draft_run_compare_stream(
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
message: str,
|
||||
models: List[app_schema.ModelCompareItem],
|
||||
conversation_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
workspace_id: Optional[uuid.UUID] = None,
|
||||
parallel: bool = True,
|
||||
timeout: int = 60
|
||||
self,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
message: str,
|
||||
models: List[app_schema.ModelCompareItem],
|
||||
conversation_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
workspace_id: Optional[uuid.UUID] = None,
|
||||
parallel: bool = True,
|
||||
timeout: int = 60
|
||||
):
|
||||
"""多模型对比试运行(流式返回)
|
||||
|
||||
@@ -1982,15 +1986,15 @@ class AppService:
|
||||
# 4. 调用 DraftRunService 的流式对比方法
|
||||
draft_service = DraftRunService(self.db)
|
||||
async for event in draft_service.run_compare_stream(
|
||||
agent_config=agent_cfg,
|
||||
models=model_configs,
|
||||
message=message,
|
||||
workspace_id=workspace_id,
|
||||
conversation_id=conversation_id,
|
||||
user_id=user_id,
|
||||
variables=variables,
|
||||
parallel=parallel,
|
||||
timeout=timeout
|
||||
agent_config=agent_cfg,
|
||||
models=model_configs,
|
||||
message=message,
|
||||
workspace_id=workspace_id,
|
||||
conversation_id=conversation_id,
|
||||
user_id=user_id,
|
||||
variables=variables,
|
||||
parallel=parallel,
|
||||
timeout=timeout
|
||||
):
|
||||
yield event
|
||||
|
||||
@@ -2009,7 +2013,8 @@ def create_app(db: Session, *, user_id: uuid.UUID, workspace_id: uuid.UUID, data
|
||||
return service.create_app(user_id=user_id, workspace_id=workspace_id, data=data)
|
||||
|
||||
|
||||
def update_app(db: Session, *, app_id: uuid.UUID, data: app_schema.AppUpdate, workspace_id: uuid.UUID | None = None) -> App:
|
||||
def update_app(db: Session, *, app_id: uuid.UUID, data: app_schema.AppUpdate,
|
||||
workspace_id: uuid.UUID | None = None) -> App:
|
||||
"""更新应用(向后兼容接口)"""
|
||||
service = AppService(db)
|
||||
return service.update_app(app_id=app_id, data=data, workspace_id=workspace_id)
|
||||
@@ -2021,12 +2026,15 @@ def delete_app(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID | None
|
||||
return service.delete_app(app_id=app_id, workspace_id=workspace_id)
|
||||
|
||||
|
||||
def update_agent_config(db: Session, *, app_id: uuid.UUID, data: app_schema.AgentConfigUpdate, workspace_id: uuid.UUID | None = None) -> AgentConfig:
|
||||
def update_agent_config(db: Session, *, app_id: uuid.UUID, data: app_schema.AgentConfigUpdate,
|
||||
workspace_id: uuid.UUID | None = None) -> AgentConfig:
|
||||
"""更新 Agent 配置(向后兼容接口)"""
|
||||
service = AppService(db)
|
||||
return service.update_agent_config(app_id=app_id, data=data, workspace_id=workspace_id)
|
||||
|
||||
def update_workflow_config(db: Session, *, app_id: uuid.UUID, data: WorkflowConfigUpdate, workspace_id: uuid.UUID | None = None) -> WorkflowConfig:
|
||||
|
||||
def update_workflow_config(db: Session, *, app_id: uuid.UUID, data: WorkflowConfigUpdate,
|
||||
workspace_id: uuid.UUID | None = None) -> WorkflowConfig:
|
||||
"""更新 Agent 配置(向后兼容接口)"""
|
||||
service = AppService(db)
|
||||
return service.update_workflow_config(app_id=app_id, data=data, workspace_id=workspace_id)
|
||||
@@ -2040,6 +2048,7 @@ def get_agent_config(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID
|
||||
service = AppService(db)
|
||||
return service.get_agent_config(app_id=app_id, workspace_id=workspace_id)
|
||||
|
||||
|
||||
def get_workflow_config(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID | None = None) -> WorkflowConfig:
|
||||
"""获取 Agent 配置(向后兼容接口)
|
||||
|
||||
@@ -2049,13 +2058,20 @@ def get_workflow_config(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UU
|
||||
return service.get_workflow_config(app_id=app_id, workspace_id=workspace_id)
|
||||
|
||||
|
||||
def publish(db: Session, *, app_id: uuid.UUID, publisher_id: uuid.UUID, workspace_id: uuid.UUID | None = None,version_name:str, release_notes: Optional[str] = None) -> AppRelease:
|
||||
def publish(db: Session, *, app_id: uuid.UUID, publisher_id: uuid.UUID, workspace_id: uuid.UUID | None = None,
|
||||
version_name: str, release_notes: Optional[str] = None) -> AppRelease:
|
||||
"""发布应用(向后兼容接口)"""
|
||||
service = AppService(db)
|
||||
return service.publish(app_id=app_id, publisher_id=publisher_id,version_name = version_name, workspace_id=workspace_id, release_notes=release_notes)
|
||||
return service.publish(app_id=app_id, publisher_id=publisher_id, version_name=version_name,
|
||||
workspace_id=workspace_id, release_notes=release_notes)
|
||||
|
||||
|
||||
def get_current_release(db: Session, *, app_id: uuid.UUID, workspace_id: uuid.UUID | None = None) -> Optional[AppRelease]:
|
||||
def get_current_release(
|
||||
db: Session,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID | None = None
|
||||
) -> Optional[AppRelease]:
|
||||
"""获取当前发布版本(向后兼容接口)"""
|
||||
service = AppService(db)
|
||||
return service.get_current_release(app_id=app_id, workspace_id=workspace_id)
|
||||
@@ -2074,16 +2090,16 @@ def rollback(db: Session, *, app_id: uuid.UUID, version: int, workspace_id: uuid
|
||||
|
||||
|
||||
def list_apps(
|
||||
db: Session,
|
||||
*,
|
||||
workspace_id: uuid.UUID,
|
||||
type: Optional[str] = None,
|
||||
visibility: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
include_shared: bool = True,
|
||||
page: int = 1,
|
||||
pagesize: int = 10,
|
||||
db: Session,
|
||||
*,
|
||||
workspace_id: uuid.UUID,
|
||||
type: Optional[str] = None,
|
||||
visibility: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
include_shared: bool = True,
|
||||
page: int = 1,
|
||||
pagesize: int = 10,
|
||||
) -> Tuple[List[App], int]:
|
||||
"""列出应用(向后兼容接口)"""
|
||||
service = AppService(db)
|
||||
@@ -2100,9 +2116,9 @@ def list_apps(
|
||||
|
||||
|
||||
def get_apps_by_ids(
|
||||
db: Session,
|
||||
app_ids: List[str],
|
||||
workspace_id: uuid.UUID
|
||||
db: Session,
|
||||
app_ids: List[str],
|
||||
workspace_id: uuid.UUID
|
||||
) -> List[App]:
|
||||
"""根据ID列表获取应用(向后兼容接口)"""
|
||||
service = AppService(db)
|
||||
@@ -2112,14 +2128,14 @@ def get_apps_by_ids(
|
||||
# ==================== 向后兼容的函数接口 ====================
|
||||
|
||||
async def draft_run(
|
||||
db: Session,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
message: str,
|
||||
conversation_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
db: Session,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
message: str,
|
||||
conversation_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""试运行 Agent(向后兼容接口)"""
|
||||
service = AppService(db)
|
||||
@@ -2134,30 +2150,28 @@ async def draft_run(
|
||||
|
||||
|
||||
async def draft_run_stream(
|
||||
db: Session,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
message: str,
|
||||
conversation_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
db: Session,
|
||||
*,
|
||||
app_id: uuid.UUID,
|
||||
message: str,
|
||||
conversation_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
workspace_id: Optional[uuid.UUID] = None
|
||||
):
|
||||
"""试运行 Agent 流式返回(向后兼容接口)"""
|
||||
service = AppService(db)
|
||||
async for event in service.draft_run_stream(
|
||||
app_id=app_id,
|
||||
message=message,
|
||||
conversation_id=conversation_id,
|
||||
user_id=user_id,
|
||||
variables=variables,
|
||||
workspace_id=workspace_id
|
||||
app_id=app_id,
|
||||
message=message,
|
||||
conversation_id=conversation_id,
|
||||
user_id=user_id,
|
||||
variables=variables,
|
||||
workspace_id=workspace_id
|
||||
):
|
||||
yield event
|
||||
|
||||
|
||||
|
||||
|
||||
# ==================== 依赖注入函数 ====================
|
||||
|
||||
def get_app_service(
|
||||
|
||||
@@ -711,45 +711,32 @@ class EmotionAnalyticsService:
|
||||
end_user_id: str,
|
||||
db: Session,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""从缓存获取个性化情绪建议
|
||||
"""从 Redis 缓存获取个性化情绪建议
|
||||
|
||||
Args:
|
||||
end_user_id: 宿主ID(用户组ID)
|
||||
db: 数据库会话
|
||||
db: 数据库会话(保留参数以保持接口兼容性)
|
||||
|
||||
Returns:
|
||||
Dict: 缓存的建议数据,如果不存在或已过期返回 None
|
||||
"""
|
||||
try:
|
||||
from app.repositories.emotion_suggestions_cache_repository import (
|
||||
EmotionSuggestionsCacheRepository,
|
||||
)
|
||||
from app.cache.memory.emotion_memory import EmotionMemoryCache
|
||||
|
||||
logger.info(f"尝试从缓存获取情绪建议: user={end_user_id}")
|
||||
logger.info(f"尝试从 Redis 缓存获取情绪建议: user={end_user_id}")
|
||||
|
||||
cache_repo = EmotionSuggestionsCacheRepository(db)
|
||||
cache = cache_repo.get_by_end_user_id(end_user_id)
|
||||
# 从 Redis 获取缓存
|
||||
cached_data = await EmotionMemoryCache.get_emotion_suggestions(end_user_id)
|
||||
|
||||
if cache is None:
|
||||
logger.info(f"用户 {end_user_id} 的建议缓存不存在")
|
||||
if cached_data is None:
|
||||
logger.info(f"用户 {end_user_id} 的建议缓存不存在或已过期")
|
||||
return None
|
||||
|
||||
# 检查是否过期
|
||||
if cache_repo.is_expired(cache):
|
||||
logger.info(f"用户 {end_user_id} 的建议缓存已过期")
|
||||
return None
|
||||
|
||||
logger.info(f"成功从缓存获取建议: user={end_user_id}")
|
||||
|
||||
return {
|
||||
"health_summary": cache.health_summary,
|
||||
"suggestions": cache.suggestions,
|
||||
"generated_at": cache.generated_at.isoformat(),
|
||||
"cached": True
|
||||
}
|
||||
logger.info(f"成功从 Redis 缓存获取建议: user={end_user_id}")
|
||||
return cached_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"从缓存获取建议失败: {str(e)}", exc_info=True)
|
||||
logger.error(f"从 Redis 缓存获取建议失败: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
async def save_suggestions_cache(
|
||||
@@ -759,30 +746,33 @@ class EmotionAnalyticsService:
|
||||
db: Session,
|
||||
expires_hours: int = 24
|
||||
) -> None:
|
||||
"""保存建议到缓存
|
||||
"""保存建议到 Redis 缓存
|
||||
|
||||
Args:
|
||||
end_user_id: 宿主ID(用户组ID)
|
||||
suggestions_data: 建议数据
|
||||
db: 数据库会话
|
||||
expires_hours: 过期时间(小时)
|
||||
db: 数据库会话(保留参数以保持接口兼容性)
|
||||
expires_hours: 过期时间(小时),默认24小时
|
||||
"""
|
||||
try:
|
||||
from app.repositories.emotion_suggestions_cache_repository import (
|
||||
EmotionSuggestionsCacheRepository,
|
||||
from app.cache.memory.emotion_memory import EmotionMemoryCache
|
||||
|
||||
logger.info(f"保存建议到 Redis 缓存: user={end_user_id}, expires={expires_hours}小时")
|
||||
|
||||
# 计算过期时间(秒)
|
||||
expire_seconds = expires_hours * 3600
|
||||
|
||||
# 保存到 Redis
|
||||
success = await EmotionMemoryCache.set_emotion_suggestions(
|
||||
user_id=end_user_id,
|
||||
suggestions_data=suggestions_data,
|
||||
expire=expire_seconds
|
||||
)
|
||||
|
||||
logger.info(f"保存建议到缓存: user={end_user_id}")
|
||||
|
||||
cache_repo = EmotionSuggestionsCacheRepository(db)
|
||||
cache_repo.create_or_update(
|
||||
end_user_id=end_user_id,
|
||||
health_summary=suggestions_data["health_summary"],
|
||||
suggestions=suggestions_data["suggestions"],
|
||||
expires_hours=expires_hours
|
||||
)
|
||||
|
||||
logger.info(f"建议缓存保存成功: user={end_user_id}")
|
||||
if success:
|
||||
logger.info(f"建议缓存保存成功: user={end_user_id}")
|
||||
else:
|
||||
logger.warning(f"建议缓存保存失败: user={end_user_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存建议缓存失败: {str(e)}", exc_info=True)
|
||||
|
||||
@@ -418,48 +418,32 @@ class ImplicitMemoryService:
|
||||
end_user_id: str,
|
||||
db: Session
|
||||
) -> Optional[dict]:
|
||||
"""从缓存获取完整用户画像
|
||||
"""从 Redis 缓存获取完整用户画像
|
||||
|
||||
Args:
|
||||
end_user_id: 终端用户ID
|
||||
db: 数据库会话
|
||||
db: 数据库会话(保留参数以保持接口兼容性)
|
||||
|
||||
Returns:
|
||||
Dict: 缓存的画像数据,如果不存在或已过期返回 None
|
||||
"""
|
||||
try:
|
||||
from app.repositories.implicit_memory_cache_repository import (
|
||||
ImplicitMemoryCacheRepository,
|
||||
)
|
||||
from app.cache.memory.implicit_memory import ImplicitMemoryCache
|
||||
|
||||
logger.info(f"尝试从缓存获取用户画像: user={end_user_id}")
|
||||
logger.info(f"尝试从 Redis 缓存获取用户画像: user={end_user_id}")
|
||||
|
||||
cache_repo = ImplicitMemoryCacheRepository(db)
|
||||
cache = cache_repo.get_by_end_user_id(end_user_id)
|
||||
# 从 Redis 获取缓存
|
||||
cached_data = await ImplicitMemoryCache.get_user_profile(end_user_id)
|
||||
|
||||
if cache is None:
|
||||
logger.info(f"用户 {end_user_id} 的画像缓存不存在")
|
||||
if cached_data is None:
|
||||
logger.info(f"用户 {end_user_id} 的画像缓存不存在或已过期")
|
||||
return None
|
||||
|
||||
# 检查是否过期
|
||||
if cache_repo.is_expired(cache):
|
||||
logger.info(f"用户 {end_user_id} 的画像缓存已过期")
|
||||
return None
|
||||
|
||||
logger.info(f"成功从缓存获取用户画像: user={end_user_id}")
|
||||
|
||||
return {
|
||||
"end_user_id": cache.end_user_id,
|
||||
"preferences": cache.preferences,
|
||||
"portrait": cache.portrait,
|
||||
"interest_areas": cache.interest_areas,
|
||||
"habits": cache.habits,
|
||||
"generated_at": cache.generated_at.isoformat(),
|
||||
"cached": True
|
||||
}
|
||||
logger.info(f"成功从 Redis 缓存获取用户画像: user={end_user_id}")
|
||||
return cached_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"从缓存获取用户画像失败: {str(e)}", exc_info=True)
|
||||
logger.error(f"从 Redis 缓存获取用户画像失败: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
async def save_profile_cache(
|
||||
@@ -469,32 +453,33 @@ class ImplicitMemoryService:
|
||||
db: Session,
|
||||
expires_hours: int = 168 # 默认7天
|
||||
) -> None:
|
||||
"""保存用户画像到缓存
|
||||
"""保存用户画像到 Redis 缓存
|
||||
|
||||
Args:
|
||||
end_user_id: 终端用户ID
|
||||
profile_data: 画像数据
|
||||
db: 数据库会话
|
||||
db: 数据库会话(保留参数以保持接口兼容性)
|
||||
expires_hours: 过期时间(小时),默认168小时(7天)
|
||||
"""
|
||||
try:
|
||||
from app.repositories.implicit_memory_cache_repository import (
|
||||
ImplicitMemoryCacheRepository,
|
||||
from app.cache.memory.implicit_memory import ImplicitMemoryCache
|
||||
|
||||
logger.info(f"保存用户画像到 Redis 缓存: user={end_user_id}, expires={expires_hours}小时")
|
||||
|
||||
# 计算过期时间(秒)
|
||||
expire_seconds = expires_hours * 3600
|
||||
|
||||
# 保存到 Redis
|
||||
success = await ImplicitMemoryCache.set_user_profile(
|
||||
user_id=end_user_id,
|
||||
profile_data=profile_data,
|
||||
expire=expire_seconds
|
||||
)
|
||||
|
||||
logger.info(f"保存用户画像到缓存: user={end_user_id}")
|
||||
|
||||
cache_repo = ImplicitMemoryCacheRepository(db)
|
||||
cache_repo.create_or_update(
|
||||
end_user_id=end_user_id,
|
||||
preferences=profile_data["preferences"],
|
||||
portrait=profile_data["portrait"],
|
||||
interest_areas=profile_data["interest_areas"],
|
||||
habits=profile_data["habits"],
|
||||
expires_hours=expires_hours
|
||||
)
|
||||
|
||||
logger.info(f"用户画像缓存保存成功: user={end_user_id}")
|
||||
if success:
|
||||
logger.info(f"用户画像缓存保存成功: user={end_user_id}")
|
||||
else:
|
||||
logger.warning(f"用户画像缓存保存失败: user={end_user_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存用户画像缓存失败: {str(e)}", exc_info=True)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import datetime
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Any, Annotated, AsyncGenerator
|
||||
from typing import Any, Annotated, AsyncGenerator, Optional
|
||||
|
||||
from deprecated import deprecated
|
||||
from fastapi import Depends
|
||||
@@ -14,15 +14,14 @@ from app.core.error_codes import BizCode
|
||||
from app.core.exceptions import BusinessException
|
||||
from app.core.workflow.validator import validate_workflow_config
|
||||
from app.db import get_db
|
||||
from app.models.conversation_model import Message
|
||||
from app.models.workflow_model import WorkflowConfig, WorkflowExecution
|
||||
from app.repositories.conversation_repository import MessageRepository
|
||||
from app.repositories.workflow_repository import (
|
||||
WorkflowConfigRepository,
|
||||
WorkflowExecutionRepository,
|
||||
WorkflowNodeExecutionRepository
|
||||
)
|
||||
from app.schemas import DraftRunRequest
|
||||
from app.services.conversation_service import ConversationService
|
||||
from app.services.multi_agent_service import convert_uuids_to_str
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -36,7 +35,7 @@ class WorkflowService:
|
||||
self.config_repo = WorkflowConfigRepository(db)
|
||||
self.execution_repo = WorkflowExecutionRepository(db)
|
||||
self.node_execution_repo = WorkflowNodeExecutionRepository(db)
|
||||
self.message_repo = MessageRepository(db)
|
||||
self.conversation_service = ConversationService(db)
|
||||
|
||||
# ==================== 配置管理 ====================
|
||||
|
||||
@@ -266,6 +265,7 @@ class WorkflowService:
|
||||
workflow_config_id: uuid.UUID,
|
||||
app_id: uuid.UUID,
|
||||
trigger_type: str,
|
||||
release_id: uuid.UUID | None = None,
|
||||
triggered_by: uuid.UUID | None = None,
|
||||
conversation_id: uuid.UUID | None = None,
|
||||
input_data: dict[str, Any] | None = None
|
||||
@@ -273,6 +273,7 @@ class WorkflowService:
|
||||
"""创建工作流执行记录
|
||||
|
||||
Args:
|
||||
release_id: 应用发布 ID
|
||||
workflow_config_id: 工作流配置 ID
|
||||
app_id: 应用 ID
|
||||
trigger_type: 触发类型
|
||||
@@ -289,6 +290,7 @@ class WorkflowService:
|
||||
execution = WorkflowExecution(
|
||||
workflow_config_id=workflow_config_id,
|
||||
app_id=app_id,
|
||||
release_id=release_id,
|
||||
conversation_id=conversation_id,
|
||||
execution_id=execution_id,
|
||||
trigger_type=trigger_type,
|
||||
@@ -337,6 +339,7 @@ class WorkflowService:
|
||||
self,
|
||||
execution_id: str,
|
||||
status: str,
|
||||
token_usage: int | None = None,
|
||||
output_data: dict[str, Any] | None = None,
|
||||
error_message: str | None = None,
|
||||
error_node_id: str | None = None
|
||||
@@ -346,6 +349,7 @@ class WorkflowService:
|
||||
Args:
|
||||
execution_id: 执行 ID
|
||||
status: 状态
|
||||
token_usage: token消耗
|
||||
output_data: 输出数据
|
||||
error_message: 错误信息
|
||||
error_node_id: 出错节点 ID
|
||||
@@ -364,6 +368,8 @@ class WorkflowService:
|
||||
)
|
||||
|
||||
execution.status = status
|
||||
if token_usage is not None:
|
||||
execution.token_usage = token_usage
|
||||
if output_data is not None:
|
||||
execution.output_data = convert_uuids_to_str(output_data)
|
||||
if error_message is not None:
|
||||
@@ -414,12 +420,14 @@ class WorkflowService:
|
||||
payload: DraftRunRequest,
|
||||
config: WorkflowConfig,
|
||||
workspace_id: uuid.UUID,
|
||||
release_id: uuid.UUID | None = None,
|
||||
):
|
||||
"""运行工作流
|
||||
|
||||
Args:
|
||||
workspace_id:
|
||||
config:
|
||||
release_id: 发布 ID
|
||||
workspace_id:工作空间 ID
|
||||
config: 配置
|
||||
payload:
|
||||
app_id: 应用 ID
|
||||
|
||||
@@ -463,7 +471,8 @@ class WorkflowService:
|
||||
trigger_type="manual",
|
||||
triggered_by=None,
|
||||
conversation_id=conversation_id_uuid,
|
||||
input_data=input_data
|
||||
input_data=input_data,
|
||||
release_id=release_id,
|
||||
)
|
||||
|
||||
# 3. 构建工作流配置字典
|
||||
@@ -507,20 +516,20 @@ class WorkflowService:
|
||||
|
||||
# 更新执行结果
|
||||
if result.get("status") == "completed":
|
||||
token_usage = result.get("token_usage", {}) or {}
|
||||
self.update_execution_status(
|
||||
execution.execution_id,
|
||||
"completed",
|
||||
output_data=result
|
||||
output_data=result,
|
||||
token_usage=token_usage.get("total_tokens", None)
|
||||
)
|
||||
final_messages = result.get("messages", [])[init_message_length:]
|
||||
for message in final_messages:
|
||||
message_obj = Message(
|
||||
self.conversation_service.add_message(
|
||||
conversation_id=conversation_id_uuid,
|
||||
role=message["role"],
|
||||
content=message["content"],
|
||||
content=message["content"]
|
||||
)
|
||||
self.message_repo.add_message(message_obj)
|
||||
self.db.commit()
|
||||
logger.info(f"Workflow Run Success, "
|
||||
f"execution_id: {execution.execution_id}, message count: {len(final_messages)}")
|
||||
else:
|
||||
@@ -562,10 +571,12 @@ class WorkflowService:
|
||||
payload: DraftRunRequest,
|
||||
config: WorkflowConfig,
|
||||
workspace_id: uuid.UUID,
|
||||
release_id: Optional[uuid.UUID] = None,
|
||||
):
|
||||
"""运行工作流(流式)
|
||||
|
||||
Args:
|
||||
release_id: 发布id
|
||||
workspace_id:
|
||||
app_id: 应用 ID
|
||||
payload: 请求对象(包含 message, variables, conversation_id 等)
|
||||
@@ -611,7 +622,8 @@ class WorkflowService:
|
||||
trigger_type="manual",
|
||||
triggered_by=None,
|
||||
conversation_id=conversation_id_uuid,
|
||||
input_data=input_data
|
||||
input_data=input_data,
|
||||
release_id=release_id,
|
||||
)
|
||||
|
||||
# 3. 构建工作流配置字典
|
||||
@@ -653,21 +665,21 @@ class WorkflowService:
|
||||
if event.get("event") == "workflow_end":
|
||||
|
||||
status = event.get("data", {}).get("status")
|
||||
token_usage = event.get("data", {}).get("token_usage", {}) or {}
|
||||
if status == "completed":
|
||||
self.update_execution_status(
|
||||
execution.execution_id,
|
||||
"completed",
|
||||
output_data=event.get("data")
|
||||
output_data=event.get("data"),
|
||||
token_usage=token_usage.get("total_tokens", None)
|
||||
)
|
||||
final_messages = event.get("data", {}).get("messages", [])[init_message_length:]
|
||||
for message in final_messages:
|
||||
message_obj = Message(
|
||||
self.conversation_service.add_message(
|
||||
conversation_id=conversation_id_uuid,
|
||||
role=message["role"],
|
||||
content=message["content"],
|
||||
content=message["content"]
|
||||
)
|
||||
self.message_repo.add_message(message_obj)
|
||||
self.db.commit()
|
||||
logger.info(f"Workflow Run Success, "
|
||||
f"execution_id: {execution.execution_id}, message count: {len(final_messages)}")
|
||||
elif status == "failed":
|
||||
@@ -784,10 +796,12 @@ class WorkflowService:
|
||||
|
||||
# 更新执行结果
|
||||
if result.get("status") == "completed":
|
||||
token_usage = result.get("data").get("token_usage", {}) or {}
|
||||
self.update_execution_status(
|
||||
execution.execution_id,
|
||||
"completed",
|
||||
output_data=result.get("node_outputs", {})
|
||||
output_data=result.get("node_outputs", {}),
|
||||
token_usage=token_usage.get("total_tokens", None)
|
||||
)
|
||||
else:
|
||||
self.update_execution_status(
|
||||
@@ -882,13 +896,14 @@ class WorkflowService:
|
||||
):
|
||||
# 直接转发事件(executor 已经返回正确格式)
|
||||
if event.get("event") == "workflow_end":
|
||||
|
||||
token_usage = event.get("data").get("token_usage", {}) or {}
|
||||
status = event.get("data", {}).get("status")
|
||||
if status == "completed":
|
||||
self.update_execution_status(
|
||||
execution_id,
|
||||
"completed",
|
||||
output_data=event.get("data")
|
||||
output_data=event.get("data"),
|
||||
token_usage=token_usage.get("total_tokens", None)
|
||||
)
|
||||
elif status == "failed":
|
||||
self.update_execution_status(
|
||||
|
||||
@@ -53,7 +53,7 @@ nodes:
|
||||
type: end
|
||||
name: 结束
|
||||
config:
|
||||
output: "{{ llm_qa.output }}"
|
||||
output: "{{llm_qa.output}}"
|
||||
position:
|
||||
x: 900
|
||||
y: 100
|
||||
|
||||
@@ -120,12 +120,9 @@ def multi_agent_config_4_app_release(release: AppRelease) -> MultiAgentConfig:
|
||||
|
||||
def workflow_config_4_app_release(release: AppRelease) -> WorkflowConfig:
|
||||
config_dict = release.config
|
||||
with get_db_read() as db:
|
||||
source_config = WorkflowConfigRepository(db).get_by_app_id(release.app_id)
|
||||
source_config_id = source_config.id
|
||||
|
||||
config = WorkflowConfig(
|
||||
id=source_config_id,
|
||||
id=config_dict.get("id"),
|
||||
app_id=release.app_id,
|
||||
nodes=config_dict.get("nodes", []),
|
||||
edges=config_dict.get("edges", []),
|
||||
|
||||
34
api/migrations/versions/8cd790908f92_202601191615.py
Normal file
34
api/migrations/versions/8cd790908f92_202601191615.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""202601191615
|
||||
|
||||
Revision ID: 8cd790908f92
|
||||
Revises: 1fd7d0e703b3
|
||||
Create Date: 2026-01-19 16:15:35.058649
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '8cd790908f92'
|
||||
down_revision: Union[str, None] = '1fd7d0e703b3'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('workflow_executions', sa.Column('release_id', sa.UUID(), nullable=True))
|
||||
op.create_index(op.f('ix_workflow_executions_release_id'), 'workflow_executions', ['release_id'], unique=False)
|
||||
op.create_foreign_key(None, 'workflow_executions', 'app_releases', ['release_id'], ['id'], ondelete='CASCADE')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'workflow_executions', type_='foreignkey')
|
||||
op.drop_index(op.f('ix_workflow_executions_release_id'), table_name='workflow_executions')
|
||||
op.drop_column('workflow_executions', 'release_id')
|
||||
# ### end Alembic commands ###
|
||||
@@ -18,174 +18,180 @@ import type { TestParams } from '@/views/MemoryConversation'
|
||||
import type { EndUser } from '@/views/UserMemoryDetail/types'
|
||||
import { handleSSE, type SSEMessage } from '@/utils/stream'
|
||||
|
||||
// 记忆对话
|
||||
// Memory conversation
|
||||
export const readService = (query: TestParams) => {
|
||||
return request.post('/memory/read_service', query)
|
||||
}
|
||||
/****************** 记忆看板 相关接口 *******************************/
|
||||
// 记忆看板-记忆总量
|
||||
/****************** Memory Dashboard APIs *******************************/
|
||||
// Memory Dashboard - Total memory count
|
||||
export const getTotalMemoryCount = () => {
|
||||
return request.get(`/dashboard/total_memory_count`)
|
||||
}
|
||||
// 记忆看板-知识库类型分布
|
||||
// Memory Dashboard - Knowledge base type distribution
|
||||
export const getKbTypes = () => {
|
||||
return request.get(`/memory/stats/types`)
|
||||
}
|
||||
// 记忆看板-热门记忆标签
|
||||
// Memory Dashboard - Hot memory tags
|
||||
export const getHotMemoryTags = () => {
|
||||
return request.get(`/memory-storage/analytics/hot_memory_tags`)
|
||||
}
|
||||
// 记忆看板-最近活动统计
|
||||
// Memory Dashboard - Recent activity statistics
|
||||
export const getRecentActivityStats = () => {
|
||||
return request.get(`/memory-storage/analytics/recent_activity_stats`)
|
||||
}
|
||||
// 记忆看板-记忆增长趋势
|
||||
// Memory Dashboard - Memory growth trend
|
||||
export const getMemoryIncrement = (limit: number) => {
|
||||
return request.get(`/dashboard/memory_increment`, { limit })
|
||||
}
|
||||
// 记忆看板-API调用趋势
|
||||
// Memory Dashboard - API call trend
|
||||
export const getApiTrend = () => {
|
||||
return request.get(`/dashboard/api_increment`)
|
||||
}
|
||||
// 记忆看板-总数据
|
||||
// Memory Dashboard - Total data
|
||||
export const getDashboardData = () => {
|
||||
return request.get(`/dashboard/dashboard_data`)
|
||||
}
|
||||
/*************** end 记忆看板 相关接口 ******************************/
|
||||
/*************** end Memory Dashboard APIs ******************************/
|
||||
|
||||
|
||||
/****************** 用户记忆 相关接口 *******************************/
|
||||
/****************** User Memory APIs *******************************/
|
||||
export const userMemoryListUrl = '/dashboard/end_users'
|
||||
export const getUserMemoryList = () => {
|
||||
return request.get(userMemoryListUrl)
|
||||
}
|
||||
// 用户记忆-用户记忆总量
|
||||
// User Memory - Total end users
|
||||
export const getTotalEndUsers = () => {
|
||||
return request.get(`/dashboard/total_end_users`)
|
||||
}
|
||||
// 用户记忆-用户详情
|
||||
// User Memory - User profile
|
||||
export const getUserProfile = (end_user_id: string) => {
|
||||
return request.get(`/memory/analytics/user_profile`, { end_user_id })
|
||||
}
|
||||
|
||||
// 用户记忆-记忆洞察
|
||||
// User Memory - Memory insight
|
||||
export const getMemoryInsightReport = (end_user_id: string) => {
|
||||
return request.get(`/memory-storage/analytics/memory_insight/report`, { end_user_id })
|
||||
}
|
||||
// 用户记忆-用户摘要
|
||||
// User Memory - User summary
|
||||
export const getUserSummary = (end_user_id: string) => {
|
||||
return request.get(`/memory-storage/analytics/user_summary`, { end_user_id })
|
||||
}
|
||||
// 记忆分类
|
||||
// Memory classification
|
||||
export const getNodeStatistics = (end_user_id: string) => {
|
||||
return request.get(`/memory-storage/analytics/node_statistics`, { end_user_id })
|
||||
}
|
||||
// 基本信息
|
||||
// Basic information
|
||||
export const getEndUserProfile = (end_user_id: string) => {
|
||||
return request.get(`/memory-storage/read_end_user/profile`, { end_user_id })
|
||||
}
|
||||
export const updatedEndUserProfile = (values: EndUser) => {
|
||||
return request.post(`/memory-storage/updated_end_user/profile`, values)
|
||||
}
|
||||
// 用户记忆-关系网络
|
||||
// User Memory - Relationship network
|
||||
export const getMemorySearchEdges = (end_user_id: string) => {
|
||||
return request.get(`/memory-storage/analytics/graph_data`, { end_user_id })
|
||||
}
|
||||
// 用户记忆-用户兴趣分布
|
||||
// User Memory - User interest distribution
|
||||
export const getHotMemoryTagsByUser = (end_user_id: string) => {
|
||||
return request.get(`/memory/analytics/hot_memory_tags/by_user`, { end_user_id })
|
||||
}
|
||||
// 用户记忆-记忆总量
|
||||
// User Memory - Total memory count
|
||||
export const getTotalMemoryCountByUser = (end_user_id: string) => {
|
||||
return request.get(`/memory-storage/search`, { end_user_id })
|
||||
}
|
||||
// RAG 用户记忆-记忆总量
|
||||
// RAG User Memory - Total memory count
|
||||
export const getTotalRagMemoryCountByUser = (end_user_id: string) => {
|
||||
return request.get(`/dashboard/current_user_rag_total_num`, { end_user_id })
|
||||
}
|
||||
// RAG 用户记忆-用户摘要
|
||||
// RAG User Memory - User summary
|
||||
export const getChunkSummaryTag = (end_user_id: string) => {
|
||||
return request.get(`/dashboard/chunk_summary_tag`, { end_user_id })
|
||||
}
|
||||
// RAG 用户记忆-记忆洞察
|
||||
// RAG User Memory - Memory insight
|
||||
export const getChunkInsight = (end_user_id: string) => {
|
||||
return request.get(`/dashboard/chunk_insight`, { end_user_id })
|
||||
}
|
||||
// RAG 用户记忆-存储内容
|
||||
// RAG User Memory - Storage content
|
||||
export const getRagContent = (end_user_id: string) => {
|
||||
return request.get(`/dashboard/rag_content`, { end_user_id, limit: 20 })
|
||||
}
|
||||
// 情感分布分析
|
||||
// Emotion distribution analysis
|
||||
export const getWordCloud = (group_id: string) => {
|
||||
return request.post(`/memory/emotion-memory/wordcloud`, { group_id, limit: 20 })
|
||||
}
|
||||
// 高频情绪关键词
|
||||
// High-frequency emotion keywords
|
||||
export const getEmotionTags = (group_id: string) => {
|
||||
return request.post(`/memory/emotion-memory/tags`, { group_id, limit: 20 })
|
||||
}
|
||||
// 情绪健康指数
|
||||
// Emotion health index
|
||||
export const getEmotionHealth = (group_id: string) => {
|
||||
return request.post(`/memory/emotion-memory/health`, { group_id, limit: 20 })
|
||||
}
|
||||
// 个性化建议
|
||||
// Personalized suggestions
|
||||
export const getEmotionSuggestions = (group_id: string) => {
|
||||
return request.post(`/memory/emotion-memory/suggestions`, { group_id, limit: 20 })
|
||||
}
|
||||
export const generateSuggestions = (end_user_id: string) => {
|
||||
return request.post(`/memory/emotion-memory/generate_suggestions`, { end_user_id })
|
||||
}
|
||||
export const analyticsRefresh = (end_user_id: string) => {
|
||||
return request.post('/memory-storage/analytics/generate_cache', { end_user_id })
|
||||
}
|
||||
// 遗忘
|
||||
// Forgetting stats
|
||||
export const getForgetStats = (group_id: string) => {
|
||||
return request.get(`/memory/forget-memory/stats`, { group_id })
|
||||
}
|
||||
// 隐性记忆-偏好
|
||||
// Implicit Memory - Preferences
|
||||
export const getImplicitPreferences = (end_user_id: string) => {
|
||||
return request.get(`/memory/implicit-memory/preferences/${end_user_id}`)
|
||||
}
|
||||
// 隐性记忆-核心特质
|
||||
// Implicit Memory - Core traits
|
||||
export const getImplicitPortrait = (end_user_id: string) => {
|
||||
return request.get(`/memory/implicit-memory/portrait/${end_user_id}`)
|
||||
}
|
||||
// 隐性记忆-兴趣领域分布
|
||||
// Implicit Memory - Interest areas distribution
|
||||
export const getImplicitInterestAreas = (end_user_id: string) => {
|
||||
return request.get(`/memory/implicit-memory/interest-areas/${end_user_id}`)
|
||||
}
|
||||
// 隐性记忆-用户习惯分析
|
||||
// Implicit Memory - User habits analysis
|
||||
export const getImplicitHabits = (end_user_id: string) => {
|
||||
return request.get(`/memory/implicit-memory/habits/${end_user_id}`)
|
||||
}
|
||||
// 短期记忆
|
||||
export const generateProfile = (end_user_id: string) => {
|
||||
return request.post(`/memory/implicit-memory/generate_profile`, { end_user_id })
|
||||
}
|
||||
// Short-term memory
|
||||
export const getShortTerm = (end_user_id: string) => {
|
||||
return request.get(`/memory/short/short_term`, { end_user_id })
|
||||
}
|
||||
// 感知记忆-视觉记忆
|
||||
// Perceptual Memory - Visual memory
|
||||
export const getPerceptualLastVisual = (end_user: string) => {
|
||||
return request.get(`/memory/perceptual/${end_user}/last_visual`)
|
||||
}
|
||||
// 感知记忆-音频记忆
|
||||
// Perceptual Memory - Audio memory
|
||||
export const getPerceptualLastListen = (end_user: string) => {
|
||||
return request.get(`/memory/perceptual/${end_user}/last_listen`)
|
||||
}
|
||||
// 感知记忆-文本记忆
|
||||
// Perceptual Memory - Text memory
|
||||
export const getPerceptualLastText = (end_user: string) => {
|
||||
return request.get(`/memory/perceptual/${end_user}/last_text`)
|
||||
}
|
||||
// 感知记忆-感知记忆时间线
|
||||
// Perceptual Memory - Perceptual memory timeline
|
||||
export const getPerceptualTimeline = (end_user: string) => {
|
||||
return request.get(`/memory/perceptual/${end_user}/timeline`)
|
||||
}
|
||||
// 情景记忆-总览
|
||||
// Episodic Memory - Overview
|
||||
export const getEpisodicOverview = (data: { end_user_id: string; time_range: string; episodic_type: string; } ) => {
|
||||
return request.post(`/memory/episodic-memory/overview`, data)
|
||||
}
|
||||
export const getEpisodicDetail = (data: { end_user_id: string; summary_id: string; } ) => {
|
||||
return request.post(`/memory/episodic-memory/details`, data)
|
||||
}
|
||||
// 关系演化
|
||||
// Relationship evolution
|
||||
export const getRelationshipEvolution = (data: { id: string; label: string; } ) => {
|
||||
return request.get(`/memory-storage/memory_space/relationship_evolution`, data)
|
||||
}
|
||||
// 共同记忆时间线
|
||||
// Shared memory timeline
|
||||
export const getTimelineMemories = (data: { id: string; label: string; }) => {
|
||||
return request.get(`/memory-storage/memory_space/timeline_memories`, data)
|
||||
}
|
||||
@@ -207,72 +213,72 @@ export const getConversationDetail = (end_user: string, conversation_id: string)
|
||||
export const forgetTrigger = (data: { max_merge_batch_size: number; min_days_since_access: number; end_user_id: string;}) => {
|
||||
return request.post(`/memory/forget-memory/trigger`, data)
|
||||
}
|
||||
/*************** end 用户记忆 相关接口 ******************************/
|
||||
/*************** end User Memory APIs ******************************/
|
||||
|
||||
/****************** 记忆管理 相关接口 *******************************/
|
||||
// 记忆管理-获取所有配置
|
||||
/****************** Memory Management APIs *******************************/
|
||||
// Memory Management - Get all configurations
|
||||
export const memoryConfigListUrl = '/memory-storage/read_all_config'
|
||||
export const getMemoryConfigList = () => {
|
||||
return request.get(memoryConfigListUrl)
|
||||
}
|
||||
// 记忆管理-创建配置
|
||||
// Memory Management - Create configuration
|
||||
export const createMemoryConfig = (values: MemoryFormData) => {
|
||||
return request.post('/memory-storage/create_config', values)
|
||||
}
|
||||
// 记忆管理-更新配置
|
||||
// Memory Management - Update configuration
|
||||
export const updateMemoryConfig = (values: MemoryFormData) => {
|
||||
return request.post('/memory-storage/update_config', values)
|
||||
}
|
||||
// 记忆管理-删除配置
|
||||
// Memory Management - Delete configuration
|
||||
export const deleteMemoryConfig = (config_id: number) => {
|
||||
return request.delete(`/memory-storage/delete_config?config_id=${config_id}`)
|
||||
}
|
||||
// 遗忘引擎-获取配置
|
||||
// Forgetting Engine - Get configuration
|
||||
export const getMemoryForgetConfig = (config_id: number | string) => {
|
||||
return request.get('/memory/forget-memory/read_config', { config_id })
|
||||
}
|
||||
// 遗忘引擎-更新配置
|
||||
// Forgetting Engine - Update configuration
|
||||
export const updateMemoryForgetConfig = (values: ForgetConfigForm) => {
|
||||
return request.post('/memory/forget-memory/update_config', values)
|
||||
}
|
||||
// 记忆萃取引擎-获取配置
|
||||
// Memory Extraction Engine - Get configuration
|
||||
export const getMemoryExtractionConfig = (config_id: number | string) => {
|
||||
return request.get('/memory-storage/read_config_extracted', { config_id: config_id })
|
||||
}
|
||||
// 记忆萃取引擎-更新配置
|
||||
// Memory Extraction Engine - Update configuration
|
||||
export const updateMemoryExtractionConfig = (values: ExtractionConfigForm) => {
|
||||
return request.post('/memory-storage/update_config_extracted', values)
|
||||
}
|
||||
// 记忆萃取引擎-试运行
|
||||
// Memory Extraction Engine - Pilot run
|
||||
export const pilotRunMemoryExtractionConfig = (values: { config_id: number | string; dialogue_text: string; }, onMessage?: (data: SSEMessage[]) => void) => {
|
||||
return handleSSE('/memory-storage/pilot_run', values, onMessage)
|
||||
}
|
||||
// 情绪引擎-获取配置
|
||||
// Emotion Engine - Get configuration
|
||||
export const getMemoryEmotionConfig = (config_id: number | string) => {
|
||||
return request.get('/memory/emotion/read_config', { config_id: config_id })
|
||||
}
|
||||
// 情绪引擎-更新配置
|
||||
// Emotion Engine - Update configuration
|
||||
export const updateMemoryEmotionConfig = (values: EmotionConfig) => {
|
||||
return request.post('/memory/emotion/updated_config', values)
|
||||
}
|
||||
// 反思引擎-获取配置
|
||||
// Reflection Engine - Get configuration
|
||||
export const getMemoryReflectionConfig = (config_id: number | string) => {
|
||||
return request.get('/memory/reflection/configs', { config_id: config_id })
|
||||
}
|
||||
// 反思引擎-更新配置
|
||||
// Reflection Engine - Update configuration
|
||||
export const updateMemoryReflectionConfig = (values: SelfReflectionEngineConfig) => {
|
||||
return request.post('/memory/reflection/save', values)
|
||||
}
|
||||
// 反思引擎-试运行
|
||||
// Reflection Engine - Pilot run
|
||||
export const pilotRunMemoryReflectionConfig = (values: { config_id: number | string; language_type: string; }) => {
|
||||
return request.get('/memory/reflection/run', values)
|
||||
}
|
||||
|
||||
/*************** end 记忆管理 相关接口 ******************************/
|
||||
/*************** end Memory Management APIs ******************************/
|
||||
|
||||
|
||||
/****************** API参数 相关接口 *******************************/
|
||||
/****************** API Parameters APIs *******************************/
|
||||
export const getMemoryApi = () => {
|
||||
return request.get('/memory/docs/api')
|
||||
}
|
||||
/*************** end API参数 相关接口 ******************************/
|
||||
/*************** end API Parameters APIs ******************************/
|
||||
21
web/src/assets/images/workflow/deleteBg.svg
Normal file
21
web/src/assets/images/workflow/deleteBg.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 33</title>
|
||||
<g id="工作流" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="工作流--流程控制-条件分支" transform="translate(-1752, -352)" stroke="#5B6167">
|
||||
<g id="编组-37" transform="translate(1480, 63)">
|
||||
<g id="编组-35" transform="translate(8, 195)">
|
||||
<g id="编组-33" transform="translate(264, 94)">
|
||||
<g id="编组-32" transform="translate(3, 3.5)">
|
||||
<line x1="-1.63757896e-14" y1="2" x2="10" y2="2" id="路径-29"></line>
|
||||
<polyline id="路径-30" stroke-linejoin="round" points="3 1.99990611 3 0 7 0 7 2"></polyline>
|
||||
<path d="M1.5,2.01228712 L1.5,8 C1.5,8.55228475 1.94771525,9 2.5,9 L7.5,9 C8.05228475,9 8.5,8.55228475 8.5,8 L8.5,2 L8.5,2" id="路径-31" stroke-linejoin="round"></path>
|
||||
<line x1="4" y1="4.00683364" x2="4" y2="7.00683364" id="路径-32"></line>
|
||||
<line x1="6" y1="4.00683364" x2="6" y2="7.00683364" id="路径-32"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
22
web/src/assets/images/workflow/deleteBg_hover.svg
Normal file
22
web/src/assets/images/workflow/deleteBg_hover.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 33</title>
|
||||
<g id="工作流" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="工作流--流程控制-条件分支" transform="translate(-1752, -420)">
|
||||
<g id="编组-37" transform="translate(1480, 63)">
|
||||
<g id="编组-35" transform="translate(8, 195)">
|
||||
<g id="编组-33" transform="translate(264, 162)">
|
||||
<rect id="矩形" fill="#FF5D34" opacity="0.116987" x="0" y="0" width="16" height="16" rx="4"></rect>
|
||||
<g id="编组-32" transform="translate(3, 3.5)" stroke="#FF5D34">
|
||||
<line x1="-1.63757896e-14" y1="2" x2="10" y2="2" id="路径-29"></line>
|
||||
<polyline id="路径-30" stroke-linejoin="round" points="3 1.99990611 3 0 7 0 7 2"></polyline>
|
||||
<path d="M1.5,2.01228712 L1.5,8 C1.5,8.55228475 1.94771525,9 2.5,9 L7.5,9 C8.05228475,9 8.5,8.55228475 8.5,8 L8.5,2 L8.5,2" id="路径-31" stroke-linejoin="round"></path>
|
||||
<line x1="4" y1="4.00683364" x2="4" y2="7.00683364" id="路径-32"></line>
|
||||
<line x1="6" y1="4.00683364" x2="6" y2="7.00683364" id="路径-32"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
18
web/src/assets/images/workflow/delete_cycle.svg
Normal file
18
web/src/assets/images/workflow/delete_cycle.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组 33</title>
|
||||
<g id="工作流" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="工作流--AI与认知处理-大语言模型" transform="translate(-1736, -662)" stroke="#212332" stroke-width="1.1">
|
||||
<g id="编组-34" transform="translate(1472, 64)">
|
||||
<g id="编组-3备份-10" transform="translate(12, 409)">
|
||||
<g id="选择备份" transform="translate(0, 177)">
|
||||
<g id="编组-33" transform="translate(252, 12)">
|
||||
<circle id="椭圆形" cx="8" cy="8" r="6.45"></circle>
|
||||
<line x1="6" y1="8" x2="10" y2="8" id="路径-10" stroke-linecap="round" stroke-linejoin="round"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
18
web/src/assets/images/workflow/recall.svg
Normal file
18
web/src/assets/images/workflow/recall.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>召回</title>
|
||||
<g id="工作流" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="工作流--AI与认知处理-知识检索" transform="translate(-1684, -330)" stroke="#212332">
|
||||
<g id="节点属性" transform="translate(1472, 64)">
|
||||
<g id="编组-30" transform="translate(12, 264)">
|
||||
<g id="编组-28" transform="translate(196, 0)">
|
||||
<g id="召回" transform="translate(4, 2)">
|
||||
<path d="M3.00336574,8.78243131 C3.31276784,9.47513403 3.79697125,10.0726443 4.39990015,10.5188863 M5.34710411,11.051994 C5.85707009,11.2602342 6.41513255,11.375 7,11.375 C7.46865477,11.375 7.92009851,11.3013108 8.34337671,11.1648869 M9.11238544,10.8321699 C9.85595277,10.4214229 10.4672402,9.80055221 10.8661626,9.04964308 M11.1846924,8.28028469 C11.3084287,7.87534253 11.375,7.44544554 11.375,7 C11.375,4.58375422 9.41624578,2.625 7,2.625 C5.30981329,2.625 3.84348335,3.58344477 3.11486142,4.98648308" id="形状"></path>
|
||||
<polyline id="路径-10" stroke-linejoin="round" points="2.48490579 2.81937431 2.86401413 5.38855621 5.4725833 4.82767632"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
18
web/src/assets/images/workflow/recall_hover.svg
Normal file
18
web/src/assets/images/workflow/recall_hover.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>召回</title>
|
||||
<g id="工作流" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="工作流--AI与认知处理-知识检索" transform="translate(-1684, -330)" stroke="#155EEF">
|
||||
<g id="节点属性" transform="translate(1472, 64)">
|
||||
<g id="编组-30" transform="translate(12, 264)">
|
||||
<g id="编组-28" transform="translate(196, 0)">
|
||||
<g id="召回" transform="translate(4, 2)">
|
||||
<path d="M3.00336574,8.78243131 C3.31276784,9.47513403 3.79697125,10.0726443 4.39990015,10.5188863 M5.34710411,11.051994 C5.85707009,11.2602342 6.41513255,11.375 7,11.375 C7.46865477,11.375 7.92009851,11.3013108 8.34337671,11.1648869 M9.11238544,10.8321699 C9.85595277,10.4214229 10.4672402,9.80055221 10.8661626,9.04964308 M11.1846924,8.28028469 C11.3084287,7.87534253 11.375,7.44544554 11.375,7 C11.375,4.58375422 9.41624578,2.625 7,2.625 C5.30981329,2.625 3.84348335,3.58344477 3.11486142,4.98648308" id="形状"></path>
|
||||
<polyline id="路径-10" stroke-linejoin="round" points="2.48490579 2.81937431 2.86401413 5.38855621 5.4725833 4.82767632"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
12
web/src/components/FormItem/DescWrapper.tsx
Normal file
12
web/src/components/FormItem/DescWrapper.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import clsx from "clsx";
|
||||
import type { FC, ReactNode } from "react";
|
||||
|
||||
const DescWrapper: FC<{desc: string | ReactNode, className?: string}> = ({desc, className}) => {
|
||||
return (
|
||||
<div className={clsx(className, "rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4 ")}>
|
||||
{desc}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DescWrapper
|
||||
13
web/src/components/FormItem/LabelWrapper.tsx
Normal file
13
web/src/components/FormItem/LabelWrapper.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import clsx from "clsx";
|
||||
import type { FC, ReactNode } from "react";
|
||||
|
||||
const LabelWrapper: FC<{ title: string | ReactNode, className?: string; children?: ReactNode}> = ({title, className, children}) => {
|
||||
return (
|
||||
<div className={clsx(className)}>
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5">{title}</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LabelWrapper
|
||||
45
web/src/components/FormItem/SwitchFormItem.tsx
Normal file
45
web/src/components/FormItem/SwitchFormItem.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Switch, Form, ConfigProvider } from "antd";
|
||||
import useSize from 'antd/lib/config-provider/hooks/useSize'
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { useContext } from "react";
|
||||
|
||||
import LabelWrapper from './LabelWrapper'
|
||||
import DescWrapper from './DescWrapper'
|
||||
|
||||
interface SwitchFormItemProps {
|
||||
title: string | ReactNode;
|
||||
desc?: string | ReactNode;
|
||||
name: string | string[];
|
||||
size?: 'small' | 'default'
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SwitchFormItem: FC<SwitchFormItemProps> = ({
|
||||
title,
|
||||
desc,
|
||||
name,
|
||||
size = 'default',
|
||||
className,
|
||||
disabled
|
||||
}) => {
|
||||
const componentSize = useSize()
|
||||
console.log('componentSize', componentSize)
|
||||
|
||||
return (
|
||||
<div className={`${className} rb:flex rb:items-center rb:justify-between`}>
|
||||
<LabelWrapper title={title}>
|
||||
{desc && <DescWrapper desc={desc} className="rb:mt-2" />}
|
||||
</LabelWrapper>
|
||||
<Form.Item
|
||||
name={name}
|
||||
valuePropName="checked"
|
||||
className="rb:mb-0!"
|
||||
>
|
||||
<Switch disabled={disabled} size={size} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SwitchFormItem
|
||||
@@ -54,7 +54,7 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
||||
key: '1',
|
||||
label: (<>
|
||||
<div>{user.username}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-[8px]">{user.email}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2">{user.email}</div>
|
||||
</>),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -150,9 +150,19 @@ const RbMarkdown: FC<RbMarkdownProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
// 处理键盘快捷键
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
|
||||
const selection = window.getSelection()
|
||||
if (selection && selection.toString()) {
|
||||
navigator.clipboard.writeText(selection.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 预览模式
|
||||
return (
|
||||
<div className="rb:relative">
|
||||
<div className="rb:relative" onKeyDown={handleKeyDown} tabIndex={0}>
|
||||
<style>{`
|
||||
.html-comment {
|
||||
color: #999;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.rb-modal .ant-modal-header {
|
||||
margin-bottom: 24px;
|
||||
.rb-modal .ant-modal-footer .ant-btn {
|
||||
height: 32px !important;
|
||||
padding: 0 15px !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
import { type FC } from 'react'
|
||||
import { Modal, type ModalProps } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import './index.css'
|
||||
const RbModal: FC<ModalProps> = ({
|
||||
onOk,
|
||||
onCancel,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, type FC, useCallback, useRef } from 'react';
|
||||
import { Input } from 'antd';
|
||||
import { Input, type InputProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import searchIcon from '@/assets/images/search.svg'
|
||||
|
||||
@@ -11,6 +11,7 @@ interface SearchInputProps {
|
||||
defaultValue?: string;
|
||||
style?: Record<string, string | number>;
|
||||
className?: string;
|
||||
size?: InputProps['size']
|
||||
}
|
||||
|
||||
const SearchInput: FC<SearchInputProps> = ({
|
||||
@@ -79,7 +80,7 @@ const SearchInput: FC<SearchInputProps> = ({
|
||||
return (
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<img src={searchIcon} alt="search" className="rb:w-[16px] rb:h-[16px] rb:mr-[4px]" />}
|
||||
prefix={<img src={searchIcon} alt="search" className="rb:w-4 rb:h-4 rb:mr-1" />}
|
||||
placeholder={placeholder || t('user.searchPlaceholder')}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -417,7 +417,8 @@ export const en = {
|
||||
refresh: 'Refresh',
|
||||
return: 'Return',
|
||||
statusEnabled: 'Available',
|
||||
statusDisabled: 'Unavailable'
|
||||
statusDisabled: 'Unavailable',
|
||||
remove: 'Remove',
|
||||
},
|
||||
model: {
|
||||
searchPlaceholder: 'search model…',
|
||||
@@ -1798,9 +1799,11 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
temperature: 'Temperature',
|
||||
max_tokens: 'Max Tokens',
|
||||
context: 'Context',
|
||||
contextPlaceholder: '{x} Set Variable',
|
||||
memory: 'Memory',
|
||||
enable_window: 'Memory Window',
|
||||
inner: 'Built-in',
|
||||
messagesPlaceholder: 'Write prompts here, type "{" to insert variables, type "insert" to insert',
|
||||
},
|
||||
start: {
|
||||
variables: 'Input Fields',
|
||||
@@ -1811,7 +1814,6 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
array: 'Dropdown Options',
|
||||
object: 'Object',
|
||||
|
||||
addVariable: 'Add Variable',
|
||||
editVariable: 'Edit Variable',
|
||||
variableType: 'Variable Type',
|
||||
variableName: 'Variable Name',
|
||||
@@ -1835,6 +1837,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
'parameter-extractor': {
|
||||
model_id: 'Model',
|
||||
text: 'Input Variable',
|
||||
textPlaceholder: '{x} Set Variable',
|
||||
params: 'Extract Parameters',
|
||||
prompt: 'Instruction',
|
||||
|
||||
@@ -1855,6 +1858,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
'array[number]': 'Array[Number]',
|
||||
'array[boolean]': 'Array[Boolean]',
|
||||
'array[object]': 'Array[Object]',
|
||||
addParams: 'Add Extract Variable',
|
||||
promptPlaceholder: 'Write prompts here, type "{" to insert variables, type "insert" to insert',
|
||||
},
|
||||
'var-aggregator': {
|
||||
group: 'Aggregation Group',
|
||||
@@ -1924,6 +1929,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
loop: {
|
||||
cycle_vars: 'Loop Variables',
|
||||
condition: 'Loop Termination Condition',
|
||||
addCondition: 'Add Condition',
|
||||
max_loop: 'Maximum Loop Count',
|
||||
},
|
||||
assigner: {
|
||||
@@ -1960,6 +1966,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
type: 'Type',
|
||||
value: 'Value',
|
||||
addCase: 'Add Condition',
|
||||
addVariable: 'Add Variables',
|
||||
},
|
||||
|
||||
clear: 'Clear',
|
||||
|
||||
@@ -965,7 +965,8 @@ export const zh = {
|
||||
refresh: '刷新',
|
||||
return: '返回',
|
||||
statusEnabled: '可用',
|
||||
statusDisabled: '不可用'
|
||||
statusDisabled: '不可用',
|
||||
remove: '删除',
|
||||
},
|
||||
product: {
|
||||
applicationManagement: '应用管理',
|
||||
@@ -1891,9 +1892,11 @@ export const zh = {
|
||||
temperature: '温度',
|
||||
max_tokens: '最大令牌数',
|
||||
context: '上下文',
|
||||
contextPlaceholder: '{x} 设置变量',
|
||||
memory: '记忆',
|
||||
enable_window: '记忆窗口',
|
||||
inner: '内置',
|
||||
messagesPlaceholder: '在此处编写提示,输入“{”插入变量,输入“insert”插入',
|
||||
},
|
||||
start: {
|
||||
variables: '输入字段',
|
||||
@@ -1904,7 +1907,6 @@ export const zh = {
|
||||
array: '下拉选项',
|
||||
object: '对象',
|
||||
|
||||
addVariable: '添加变量',
|
||||
editVariable: '编辑变量',
|
||||
variableType: '变量类型',
|
||||
variableName: '变量名称',
|
||||
@@ -1924,10 +1926,12 @@ export const zh = {
|
||||
query: '查询变量',
|
||||
knowledge_retrieval: '知识库',
|
||||
recallConfig: '召回测试',
|
||||
addKnowledge: '添加知识库'
|
||||
},
|
||||
'parameter-extractor': {
|
||||
model_id: '模型',
|
||||
text: '输入变量',
|
||||
textPlaceholder: '{x} 设置变量',
|
||||
params: '提取参数',
|
||||
prompt: '指令',
|
||||
|
||||
@@ -1948,6 +1952,8 @@ export const zh = {
|
||||
'array[number]': 'Array[Number]',
|
||||
'array[boolean]': 'Array[Boolean]',
|
||||
'array[object]': 'Array[Object]',
|
||||
addParams: '添加提取变量',
|
||||
promptPlaceholder: '在此处编写提示,输入“{”插入变量,输入“insert”插入',
|
||||
},
|
||||
'var-aggregator': {
|
||||
group: '聚合分组',
|
||||
@@ -2017,6 +2023,7 @@ export const zh = {
|
||||
loop: {
|
||||
cycle_vars: '循环变量',
|
||||
condition: '循环终止条件',
|
||||
addCondition: '添加条件',
|
||||
max_loop: '最大循环次数',
|
||||
},
|
||||
assigner: {
|
||||
@@ -2053,6 +2060,7 @@ export const zh = {
|
||||
type: '类型',
|
||||
value: '值',
|
||||
addCase: '添加条件',
|
||||
addVariable: '添加变量',
|
||||
},
|
||||
|
||||
clear: '清空',
|
||||
|
||||
@@ -21,6 +21,8 @@ export const lightTheme: ThemeConfig = {
|
||||
colorBorderSecondary: '#DFE4ED',
|
||||
// colorBgContainer: '#FBFDFF',
|
||||
colorError: '#FF5D34',
|
||||
sizeSM: 12,
|
||||
fontSizeSM: 12,
|
||||
},
|
||||
components: {
|
||||
Layout: {
|
||||
@@ -86,6 +88,7 @@ export const lightTheme: ThemeConfig = {
|
||||
rowSelectedBg: '#E9F1FF',
|
||||
rowSelectedHoverBg: '#F0F3F8',
|
||||
cellPaddingBlock: 8,
|
||||
cellFontSizeSM: 12,
|
||||
|
||||
// cellPaddingInline: 24,
|
||||
selectionColumnWidth: 48,
|
||||
@@ -95,6 +98,13 @@ export const lightTheme: ThemeConfig = {
|
||||
lastItemColor: '#212332',
|
||||
linkColor: '#5B6167',
|
||||
linkHoverColor: '#212332',
|
||||
},
|
||||
Input: {
|
||||
inputFontSizeSM: 12,
|
||||
controlHeightSM: 26
|
||||
},
|
||||
Select: {
|
||||
lineHeightSM: 26
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type FC, useEffect, useRef } from 'react';
|
||||
import { type FC, useRef } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Layout, Tabs, Dropdown, Button } from 'antd';
|
||||
import { Layout, Tabs, Dropdown, Button, Flex } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styles from '../index.module.css'
|
||||
@@ -141,10 +141,12 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
||||
{/* <Button type="primary">{t('workflow.export')}</Button> */}
|
||||
<img src={logoutIcon} className="rb:w-4 rb:h-4 rb:cursor-pointer" onClick={goToApplication} />
|
||||
</div>
|
||||
: <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={goToApplication}>
|
||||
<img src={logoutIcon} className="rb:mr-2 rb:w-4 rb:h-4" />
|
||||
{t('application.returnToApplicationList')}
|
||||
</div>
|
||||
: <Flex justify="flex-end">
|
||||
<div className="rb:h-8 rb:flex rb:items-center rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={goToApplication}>
|
||||
<img src={logoutIcon} className="rb:mr-2 rb:w-4 rb:h-4" />
|
||||
{t('application.returnToApplicationList')}
|
||||
</div>
|
||||
</Flex>
|
||||
}
|
||||
</Header>
|
||||
<ApplicationModal
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Row, Col, Form, Slider, Button, Alert, message, Switch, Space } from 'antd';
|
||||
import { Row, Col, Form, Slider, Button, Alert, message, Space } from 'antd';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import RbCard from '@/components/RbCard/Card';
|
||||
@@ -9,6 +9,7 @@ import type { ConfigForm } from './types'
|
||||
import CustomSelect from '@/components/CustomSelect';
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import Tag from '@/components/Tag'
|
||||
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
|
||||
|
||||
const configList = [
|
||||
{
|
||||
@@ -158,23 +159,17 @@ const EmotionEngine: React.FC = () => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-6">
|
||||
<div>
|
||||
<span className="rb:text-[14px] rb:font-medium rb:leading-5">{t(`emotionEngine.${config.key}`)}</span>
|
||||
<SwitchFormItem
|
||||
title={t(`emotionEngine.${config.key}`)}
|
||||
name={config.key}
|
||||
desc={<>
|
||||
{config.hasSubTitle && <div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`emotionEngine.${config.key}_subTitle`)}</div>}
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`emotionEngine.${config.key}_desc`)}</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
name={config.key}
|
||||
valuePropName="checked"
|
||||
className="rb:ml-2 rb:mb-0!"
|
||||
>
|
||||
<Switch
|
||||
disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>}
|
||||
className="rb:mb-6"
|
||||
disabled={!values?.emotion_enabled && config.key !== 'emotion_enabled'}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<Row gutter={16} className="rb:mt-3">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Row, Col, Form, Slider, Button, Space, message, Switch } from 'antd';
|
||||
import { Row, Col, Form, Slider, Button, Space, message } from 'antd';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import RbCard from '@/components/RbCard/Card';
|
||||
@@ -7,6 +7,7 @@ import strategyImpactSimulator from '@/assets/images/memory/strategyImpactSimula
|
||||
import LineChart from './components/LineChart'
|
||||
import { getMemoryForgetConfig, updateMemoryForgetConfig } from '@/api/memory'
|
||||
import type { ConfigForm } from './types'
|
||||
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
|
||||
|
||||
const configList = [
|
||||
{
|
||||
@@ -155,26 +156,12 @@ const ForgettingEngine: React.FC = () => {
|
||||
{configList.map(config => {
|
||||
if (config.type === 'button') {
|
||||
return (
|
||||
<div key={config.key} className="rb:mb-2">
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div>
|
||||
<span className="rb:text-[14px] rb:font-medium rb:leading-5">{t(`forgettingEngine.${config.key}`)}</span>
|
||||
</div>
|
||||
<Form.Item
|
||||
name={config.name}
|
||||
valuePropName="checked"
|
||||
className="rb:ml-2 rb:mb-0!"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="rb:flex rb:text-[12px] rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5">
|
||||
<Space size={4}>
|
||||
{config.range && <span>{t(`forgettingEngine.range`)}: {config.range?.join('-')}</span>}
|
||||
{config.type && <span>{t(`forgettingEngine.type`)}: {config.type}</span>}
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
<SwitchFormItem
|
||||
title={t(`forgettingEngine.${config.key}`)}
|
||||
name={config.name}
|
||||
desc={config.type && <span>{t(`forgettingEngine.type`)}: {config.type}</span>}
|
||||
className="rb:mb-2"
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
@@ -191,8 +178,6 @@ const ForgettingEngine: React.FC = () => {
|
||||
>
|
||||
{config.type === 'decimal'
|
||||
? <Slider tooltip={{ open: false }} max={config.range?.[1] || 1} min={config.range?.[0] || 0} step={config.step ?? 0.01} style={{ margin: '0' }} />
|
||||
: config.type === 'button'
|
||||
? <Switch />
|
||||
: null
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getModelList } from '@/api/models';
|
||||
import type { Model } from '@/views/ModelManagement/types'
|
||||
import { configList } from './constant'
|
||||
import Result from './components/Result'
|
||||
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
|
||||
|
||||
const keys = [
|
||||
// 'example',
|
||||
@@ -173,25 +174,18 @@ const MemoryExtractionEngine: FC = () => {
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-[22px]">{t(`memoryExtractionEngine.${vo.title}`)}</div>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-5.5">{t(`memoryExtractionEngine.${vo.title}`)}</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`memoryExtractionEngine.${vo.title}SubTitle`)}</div>
|
||||
|
||||
{vo.list.map(config => (
|
||||
<div key={config.label}>
|
||||
{config.control === 'button' &&
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mt-6">
|
||||
<div>
|
||||
<span className="rb:text-[14px] rb:font-medium rb:leading-5">-{t(`memoryExtractionEngine.${config.label}`)}</span>
|
||||
<ConfigDesc config={config} className="rb:ml-2" />
|
||||
</div>
|
||||
<Form.Item
|
||||
name={config.variableName}
|
||||
valuePropName="checked"
|
||||
className="rb:ml-2 rb:mb-0!"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<SwitchFormItem
|
||||
title={<>-{t(`memoryExtractionEngine.${config.label}`)}</>}
|
||||
name={config.variableName}
|
||||
desc={<ConfigDesc config={config} className="rb:ml-2" />}
|
||||
className="rb:mt-6"
|
||||
/>
|
||||
}
|
||||
{config.control === 'select' &&
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Row, Col, Form, App, Button, Switch, Space, Select } from 'antd';
|
||||
import { Row, Col, Form, App, Button, Space, Select } from 'antd';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -11,6 +11,7 @@ import CustomSelect from '@/components/CustomSelect';
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import Tag from '@/components/Tag'
|
||||
import { useI18n } from '@/store/locale';
|
||||
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
|
||||
|
||||
const configList = [
|
||||
// 启用反思引擎
|
||||
@@ -219,21 +220,16 @@ const SelfReflectionEngine: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-6">
|
||||
<div>
|
||||
<span className="rb:text-[14px] rb:font-medium rb:leading-5">{t(`reflectionEngine.${config.key}`)}</span>
|
||||
<SwitchFormItem
|
||||
title={t(`reflectionEngine.${config.key}`)}
|
||||
name={config.key}
|
||||
desc={<>
|
||||
{(config as any).hasSubTitle && <div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`reflectionEngine.${config.key}_subTitle`)}</div>}
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t(`reflectionEngine.${config.key}_desc`)}</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
name={config.key}
|
||||
valuePropName="checked"
|
||||
className="rb:ml-2 rb:mb-0!"
|
||||
>
|
||||
<Switch
|
||||
disabled={!values?.reflection_enabled && config.key !== 'reflection_enabled'} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>}
|
||||
className="rb:mb-6"
|
||||
disabled={!values?.reflection_enabled && config.key !== 'reflection_enabled'}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<Row gutter={16} className="rb:mt-3">
|
||||
|
||||
@@ -121,7 +121,7 @@ const EmotionTags: FC = () => {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
: <Empty size={88} className="rb:h-full" />
|
||||
: <Empty size={88} className="rb:h-full rb:mb-4" />
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useEffect, useState, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Space, Progress } from 'antd';
|
||||
@@ -20,7 +20,7 @@ interface HabitsItem {
|
||||
specific_examples: string[];
|
||||
}
|
||||
|
||||
const Habits: FC = () => {
|
||||
const Habits = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
@@ -43,6 +43,9 @@ const Habits: FC = () => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleRefresh: getData
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -80,5 +83,5 @@ const Habits: FC = () => {
|
||||
</RbCard>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
export default Habits
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useEffect, useState, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Progress } from 'antd';
|
||||
@@ -23,7 +23,7 @@ interface InterestAreasItem {
|
||||
art: Item;
|
||||
}
|
||||
|
||||
const InterestAreas: FC = () => {
|
||||
const InterestAreas = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
@@ -47,6 +47,9 @@ const InterestAreas: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleRefresh: getData
|
||||
}));
|
||||
return (
|
||||
<RbCard
|
||||
title={t('implicitDetail.interestAreas')}
|
||||
@@ -70,5 +73,5 @@ const InterestAreas: FC = () => {
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
})
|
||||
export default InterestAreas
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useEffect, useState, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Skeleton, Progress } from 'antd';
|
||||
@@ -25,7 +25,7 @@ interface PortraitItem {
|
||||
literature: Item;
|
||||
}
|
||||
|
||||
const Portrait: FC = () => {
|
||||
const Portrait = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
@@ -49,6 +49,9 @@ const Portrait: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleRefresh: getData
|
||||
}));
|
||||
return (
|
||||
<RbCard
|
||||
title={t('implicitDetail.portrait')}
|
||||
@@ -73,5 +76,5 @@ const Portrait: FC = () => {
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
})
|
||||
export default Portrait
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FC, useEffect, useState, useRef, useMemo } from 'react'
|
||||
import { useEffect, useState, useRef, useMemo, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Row, Col, Skeleton } from 'antd'
|
||||
@@ -31,7 +31,7 @@ const generateCategoryColors = (categories: string[]) => {
|
||||
return colors
|
||||
}
|
||||
|
||||
const Preferences: FC = () => {
|
||||
const Preferences = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const chartRef = useRef<HTMLDivElement>(null)
|
||||
@@ -138,6 +138,9 @@ const Preferences: FC = () => {
|
||||
return selectedWord !== null && data[selectedWord].tag_name ? <>{data[selectedWord].tag_name}{t('implicitDetail.preferencesDetail')}</> : ''
|
||||
}, [selectedWord, data, t])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleRefresh: getData
|
||||
}));
|
||||
return (
|
||||
<>
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-4 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('forgetDetail.overviewTitle')}</div>
|
||||
@@ -184,6 +187,6 @@ const Preferences: FC = () => {
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default Preferences
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useEffect, useState, forwardRef, useImperativeHandle } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
@@ -18,7 +18,7 @@ interface Suggestions {
|
||||
actionable_steps: string[];
|
||||
}>;
|
||||
}
|
||||
const Suggestions: FC = () => {
|
||||
const Suggestions = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const [suggestions, setSuggestions] = useState<Suggestions | null>(null)
|
||||
@@ -37,6 +37,9 @@ const Suggestions: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleRefresh: getSuggestionData
|
||||
}));
|
||||
return (
|
||||
<RbCard
|
||||
title={t('statementDetail.suggestions')}
|
||||
@@ -64,6 +67,6 @@ const Suggestions: FC = () => {
|
||||
}
|
||||
</RbCard>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default Suggestions
|
||||
@@ -1,34 +1,57 @@
|
||||
import { type FC } from 'react'
|
||||
import { forwardRef, useImperativeHandle, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Row, Col } from 'antd'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import Preferences from '../components/Preferences'
|
||||
import Portrait from '../components/Portrait'
|
||||
import InterestAreas from '../components/InterestAreas'
|
||||
import Habits from '../components/Habits'
|
||||
import {
|
||||
generateProfile,
|
||||
} from '@/api/memory'
|
||||
|
||||
const ImplicitDetail: FC = () => {
|
||||
const ImplicitDetail = forwardRef<{ handleRefresh: () => void; }>((_props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams()
|
||||
const preferencesRef = useRef<{ handleRefresh: () => void; }>(null)
|
||||
const portraitRef = useRef<{ handleRefresh: () => void; }>(null)
|
||||
const interestAreasRef = useRef<{ handleRefresh: () => void; }>(null)
|
||||
const habitsRef = useRef<{ handleRefresh: () => void; }>(null)
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (!id) return
|
||||
generateProfile(id)
|
||||
.then(() => {
|
||||
preferencesRef.current?.handleRefresh()
|
||||
portraitRef.current?.handleRefresh()
|
||||
interestAreasRef.current?.handleRefresh()
|
||||
habitsRef.current?.handleRefresh()
|
||||
})
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleRefresh
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:max-w-266 rb:mx-auto">
|
||||
<div className="rb:text-[#5B6167] rb:leading-5 rb:mt-3">{t('implicitDetail.title')}</div>
|
||||
|
||||
<Preferences />
|
||||
<Preferences ref={preferencesRef} />
|
||||
|
||||
<div className="rb:bg-[rgba(21,94,239,0.12)] rb:px-3 rb:py-2.5 rb:font-medium rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">{t('implicitDetail.portraitTitle')}</div>
|
||||
<div className="rb:my-3 rb:text-[#5B6167] rb:leading-5">{t('implicitDetail.portraitSubTitle')}</div>
|
||||
<Row gutter={[16, 16]} className="rb:mt-4">
|
||||
<Col span={12}>
|
||||
<Portrait />
|
||||
<Portrait ref={portraitRef} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<InterestAreas />
|
||||
<InterestAreas ref={interestAreasRef} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Habits />
|
||||
<Habits ref={habitsRef} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
export default ImplicitDetail
|
||||
@@ -1,13 +1,27 @@
|
||||
import { type FC } from 'react'
|
||||
import { forwardRef, useImperativeHandle, useRef } from 'react'
|
||||
import { Row, Col, Space } from 'antd';
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import WordCloud from '../components/WordCloud'
|
||||
import EmotionTags from '../components/EmotionTags'
|
||||
import Health from '../components/Health'
|
||||
import Suggestions from '../components/Suggestions'
|
||||
import { generateSuggestions } from '@/api/memory'
|
||||
|
||||
|
||||
const StatementDetail: FC = () => {
|
||||
const StatementDetail = forwardRef((_props, ref) => {
|
||||
const { id } = useParams()
|
||||
const suggestionsRef = useRef<{ handleRefresh: () => void; }>(null)
|
||||
const handleRefresh = () => {
|
||||
if (!id) return
|
||||
generateSuggestions(id)
|
||||
.then(() => {
|
||||
suggestionsRef.current?.handleRefresh()
|
||||
})
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleRefresh
|
||||
}));
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
@@ -18,10 +32,10 @@ const StatementDetail: FC = () => {
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Suggestions />
|
||||
<Suggestions ref={suggestionsRef} />
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default StatementDetail
|
||||
@@ -24,6 +24,8 @@ const Detail: FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const [name, setName] = useState<string>('')
|
||||
const forgetDetailRef = useRef<{ handleRefresh: () => void }>(null)
|
||||
const statementDetailRef = useRef<{ handleRefresh: () => void }>(null)
|
||||
const implicitDetailRef = useRef<{ handleRefresh: () => void }>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
@@ -45,7 +47,17 @@ const Detail: FC = () => {
|
||||
navigate(`/user-memory/detail/${id}/${key}`, { replace: true })
|
||||
}
|
||||
const handleRefresh = () => {
|
||||
forgetDetailRef.current?.handleRefresh()
|
||||
switch(type) {
|
||||
case 'FORGET_MEMORY':
|
||||
forgetDetailRef.current?.handleRefresh()
|
||||
break;
|
||||
case 'EMOTIONAL_MEMORY':
|
||||
statementDetailRef.current?.handleRefresh()
|
||||
break
|
||||
case 'IMPLICIT_MEMORY':
|
||||
implicitDetailRef.current?.handleRefresh()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'GRAPH') {
|
||||
@@ -67,16 +79,16 @@ const Detail: FC = () => {
|
||||
</div>
|
||||
</Dropdown>
|
||||
}
|
||||
extra={type === 'FORGET_MEMORY' &&
|
||||
extra={['FORGET_MEMORY', 'EMOTIONAL_MEMORY', 'IMPLICIT_MEMORY'].includes(type as string) &&
|
||||
<Button type="primary" ghost className="rb:group rb:h-6! rb:px-2!" onClick={handleRefresh}>
|
||||
<img src={refreshIcon} className="rb:w-4 rb:h-4" />
|
||||
{t('common.refresh')}
|
||||
</Button>}
|
||||
/>
|
||||
<div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4">
|
||||
{type === 'EMOTIONAL_MEMORY' && <StatementDetail />}
|
||||
{type === 'EMOTIONAL_MEMORY' && <StatementDetail ref={statementDetailRef} />}
|
||||
{type === 'FORGET_MEMORY' && <ForgetDetail ref={forgetDetailRef} />}
|
||||
{type === 'IMPLICIT_MEMORY' && <ImplicitDetail />}
|
||||
{type === 'IMPLICIT_MEMORY' && <ImplicitDetail ref={implicitDetailRef} />}
|
||||
{type === 'SHORT_TERM_MEMORY' && <ShortTermDetail />}
|
||||
{type === 'PERCEPTUAL_MEMORY' && <PerceptualDetail />}
|
||||
{type === 'EPISODIC_MEMORY' && <EpisodicDetail />}
|
||||
|
||||
@@ -2,12 +2,13 @@ import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, InputNumber, Checkbox } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { StartVariableItem, VariableConfigModalRef } from '../../types'
|
||||
import type { VariableConfigModalRef } from '../../types'
|
||||
import type { Variable } from '../Properties/VariableList/types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
|
||||
interface VariableEditModalProps {
|
||||
refresh: (values: StartVariableItem[]) => void;
|
||||
variables: StartVariableItem[]
|
||||
refresh: (values: Variable[]) => void;
|
||||
variables: Variable[]
|
||||
}
|
||||
|
||||
const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModalProps>(({
|
||||
@@ -15,9 +16,9 @@ const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModal
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<{variables: StartVariableItem[]}>();
|
||||
const [form] = Form.useForm<{variables: Variable[]}>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [initialValues, setInitialValues] = useState<StartVariableItem[]>([])
|
||||
const [initialValues, setInitialValues] = useState<Variable[]>([])
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
@@ -26,7 +27,7 @@ const VariableConfigModal = forwardRef<VariableConfigModalRef, VariableEditModal
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
const handleOpen = (values: StartVariableItem[]) => {
|
||||
const handleOpen = (values: Variable[]) => {
|
||||
setVisible(true);
|
||||
form.setFieldsValue({variables: values})
|
||||
setInitialValues([...values])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type FC, useState, useEffect } from 'react';
|
||||
import { type FC, useState, useEffect, useMemo } from 'react';
|
||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||
@@ -25,7 +25,11 @@ interface LexicalEditorProps {
|
||||
options: Suggestion[];
|
||||
variant?: 'outlined' | 'borderless';
|
||||
height?: number;
|
||||
fontSize?: number;
|
||||
lineHeight?: number;
|
||||
enableJinja2?: boolean;
|
||||
size?: 'default' | 'small';
|
||||
type?: 'input' | 'textarea'
|
||||
}
|
||||
|
||||
const theme = {
|
||||
@@ -51,8 +55,9 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
onChange,
|
||||
options,
|
||||
variant = 'borderless',
|
||||
height = 60,
|
||||
enableJinja2 = false,
|
||||
size = 'default',
|
||||
type = 'textarea'
|
||||
}) => {
|
||||
|
||||
const [_count, setCount] = useState(0);
|
||||
@@ -94,12 +99,9 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
display: flex;
|
||||
}
|
||||
.line-numbers {
|
||||
background-color: #f8f9fa;
|
||||
border-right: 1px solid #e1e4e8;
|
||||
color: #656d76;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
line-height: 16px;
|
||||
padding: 4px 8px;
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
@@ -142,6 +144,21 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
console.error(error);
|
||||
},
|
||||
};
|
||||
const minheight = useMemo(() => {
|
||||
if (type === 'input') {
|
||||
return `${size === 'small' ? 26 : 30}px`
|
||||
}
|
||||
return `${size === 'small' ? 60 : 120}px`
|
||||
}, [type, size])
|
||||
const fontSize = useMemo(() => {
|
||||
return `${size === 'small' ? 12 : 14}px`
|
||||
}, [size])
|
||||
const lineHeight = useMemo(() => {
|
||||
return `${size === 'small' ? 16 : 20}px`
|
||||
}, [size])
|
||||
const placeHolderMinheight = useMemo(() => {
|
||||
return `${size === 'small' ? 16 : 30}px`
|
||||
}, [type, size])
|
||||
|
||||
return (
|
||||
<LexicalComposer initialConfig={initialConfig}>
|
||||
@@ -152,7 +169,7 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
<div className="editor-with-line-numbers" style={{
|
||||
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
||||
borderRadius: '6px',
|
||||
minHeight: `${height}px`,
|
||||
minHeight: minheight,
|
||||
}}>
|
||||
<div className="line-numbers">
|
||||
<div>1</div>
|
||||
@@ -160,12 +177,12 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
<ContentEditable
|
||||
className="editor-content-with-numbers"
|
||||
style={{
|
||||
minHeight: `${height}px`,
|
||||
padding: '4px 11px',
|
||||
minHeight: minheight,
|
||||
padding: '4px 0',
|
||||
outline: 'none',
|
||||
resize: 'none',
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
fontSize: fontSize,
|
||||
lineHeight: lineHeight,
|
||||
border: 'none',
|
||||
}}
|
||||
/>
|
||||
@@ -173,14 +190,14 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
) : (
|
||||
<ContentEditable
|
||||
style={{
|
||||
minHeight: `${height}px`,
|
||||
minHeight: minheight,
|
||||
padding: variant === 'borderless' ? '0' : '4px 11px',
|
||||
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
||||
borderRadius: '6px',
|
||||
outline: 'none',
|
||||
resize: 'none',
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
fontSize: fontSize,
|
||||
lineHeight: lineHeight,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -188,12 +205,13 @@ const Editor: FC<LexicalEditorProps> =({
|
||||
placeholder={
|
||||
<div
|
||||
style={{
|
||||
minHeight: placeHolderMinheight,
|
||||
position: 'absolute',
|
||||
top: variant === 'borderless' ? '0' : '6px',
|
||||
left: enableJinja2 ? '59px' : (variant === 'borderless' ? '0' : '11px'),
|
||||
color: '#5B6167',
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
color: '#A8A9AA',
|
||||
fontSize: fontSize,
|
||||
lineHeight: placeHolderMinheight,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -9,12 +9,21 @@ const Jinja2HighlightPlugin = () => {
|
||||
return editor.registerNodeTransform(TextNode, (textNode: TextNode) => {
|
||||
const text = textNode.getTextContent();
|
||||
|
||||
if (containsJinja2Patterns(text)) {
|
||||
const parent = textNode.getParent();
|
||||
if (!parent) return;
|
||||
// Skip if node already has styling (prevent infinite recursion)
|
||||
if (textNode.getStyle()) return;
|
||||
|
||||
// Skip if no Jinja2 patterns found
|
||||
if (!containsJinja2Patterns(text)) return;
|
||||
|
||||
const parent = textNode.getParent();
|
||||
if (!parent) return;
|
||||
|
||||
const tokens = tokenizeJinja2(text);
|
||||
const newNodes = tokens.map(token => {
|
||||
const tokens = tokenizeJinja2(text);
|
||||
|
||||
// Skip if no meaningful tokenization (only one text token)
|
||||
if (tokens.length <= 1 || (tokens.length === 1 && tokens[0].type === 'text')) return;
|
||||
|
||||
const newNodes = tokens.map(token => {
|
||||
const newNode = $createTextNode(token.text);
|
||||
|
||||
switch (token.type) {
|
||||
@@ -30,16 +39,16 @@ const Jinja2HighlightPlugin = () => {
|
||||
newNode.setStyle('color: #008000');
|
||||
break;
|
||||
case 'brace-0':
|
||||
newNode.setStyle('color: #d73a49; font-family: monospace; font-weight: bold;');
|
||||
newNode.setStyle('color: #155EEF; font-family: monospace; font-weight: bold;');
|
||||
break;
|
||||
case 'brace-1':
|
||||
newNode.setStyle('color: #0366d6; font-family: monospace; font-weight: bold;');
|
||||
newNode.setStyle('color: #369F21; font-family: monospace; font-weight: bold;');
|
||||
break;
|
||||
case 'brace-2':
|
||||
newNode.setStyle('color: #28a745; font-family: monospace; font-weight: bold;');
|
||||
newNode.setStyle('color: #FF5D34; font-family: monospace; font-weight: bold;');
|
||||
break;
|
||||
case 'brace-3':
|
||||
newNode.setStyle('color: #6f42c1; font-family: monospace; font-weight: bold;');
|
||||
newNode.setStyle('color: #5B6167; font-family: monospace; font-weight: bold;');
|
||||
break;
|
||||
case 'expression-0':
|
||||
case 'expression-1':
|
||||
@@ -77,7 +86,6 @@ const Jinja2HighlightPlugin = () => {
|
||||
newNodes[i - 1].insertAfter(newNodes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input, Row, Col, Select, InputNumber, Radio } from 'antd'
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Form, Input, Select, InputNumber, Radio, Button, Space } from 'antd'
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
@@ -9,6 +8,7 @@ interface AssignmentListProps {
|
||||
value?: Array<{ variable_selector: string; operation: string[]; value: string;}>;
|
||||
parentName: string;
|
||||
options: Suggestion[];
|
||||
size?: 'small' | 'middle'
|
||||
}
|
||||
|
||||
const operationsObj = {
|
||||
@@ -31,6 +31,7 @@ const operationsObj = {
|
||||
const AssignmentList: FC<AssignmentListProps> = ({
|
||||
parentName,
|
||||
options = [],
|
||||
size = 'small'
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
@@ -39,109 +40,126 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div className="rb:flex rb:justify-between">
|
||||
{t(`workflow.config.assigner.${parentName}`)}
|
||||
<PlusOutlined onClick={() => add({ operation: 'cover'})} />
|
||||
</div>
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
const variableSelector = form.getFieldValue([parentName, name, 'variable_selector']);
|
||||
const selectedOption = options.find(option => `{{${option.value}}}` === variableSelector);
|
||||
const dataType = selectedOption?.dataType;
|
||||
const operationOptions = dataType === 'number' ? operationsObj.number : operationsObj.default;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:mb-4">
|
||||
<Row gutter={12} className="rb:mb-2!">
|
||||
<Col span={14}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'variable_selector']}
|
||||
noStyle
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.nodeData.type === 'loop' || vo.value.includes('conv.') || (vo.nodeData.type === 'iteration' && (vo.label === 'item' || vo.label === 'index')))}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([parentName, name, 'operation'], undefined);
|
||||
form.setFieldValue([parentName, name, 'value'], undefined);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'operation']}
|
||||
noStyle
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={operationOptions.map(op => ({
|
||||
...op,
|
||||
label: t(op.label)
|
||||
}))}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([parentName, name, 'value'], undefined);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2} className="rb:flex! rb:items-center rb:justify-end">
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2.5">
|
||||
<div className="rb:text-[12px] rb:leading-4.5 rb:font-medium">
|
||||
{t(`workflow.config.assigner.${parentName}`)}
|
||||
</div>
|
||||
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{(form) => {
|
||||
const operation = form.getFieldValue([parentName, name, 'operation']);
|
||||
if (operation === 'clear') return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() => add({ operation: 'cover' })}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Space size={10} direction="vertical" className="rb:w-full!">
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
const variableSelector = form.getFieldValue([parentName, name, 'variable_selector']);
|
||||
const selectedOption = options.find(option => `{{${option.value}}}` === variableSelector);
|
||||
const dataType = selectedOption?.dataType;
|
||||
const operationOptions = dataType === 'number' ? operationsObj.number : operationsObj.default;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:flex rb:items-start">
|
||||
<div className="rb:flex-1">
|
||||
<div className="rb:flex rb:gap-1 rb:mb-1">
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
name={[name, 'variable_selector']}
|
||||
noStyle
|
||||
>
|
||||
{dataType === 'number' && operation === 'cover'
|
||||
? <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={dataType ? options.filter(vo => vo.dataType === dataType) : options}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
: dataType === 'number'
|
||||
? <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([name, 'value'], value)}
|
||||
/>
|
||||
: operation === 'assign'
|
||||
? <>
|
||||
{dataType === 'boolean'
|
||||
? <Radio.Group block>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
: <Input.TextArea
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
rows={3}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
: <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={dataType ? options.filter(vo => vo.dataType === dataType) : options}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
}
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.nodeData.type === 'loop' || vo.value.includes('conv.') || (vo.nodeData.type === 'iteration' && (vo.label === 'item' || vo.label === 'index')))}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([parentName, name, 'operation'], undefined);
|
||||
form.setFieldValue([parentName, name, 'value'], undefined);
|
||||
}}
|
||||
size={size}
|
||||
className="rb:w-39!"
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'operation']}
|
||||
noStyle
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={operationOptions.map(op => ({
|
||||
...op,
|
||||
label: t(op.label)
|
||||
}))}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([parentName, name, 'value'], undefined);
|
||||
}}
|
||||
size={size}
|
||||
className="rb:w-24!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{(form) => {
|
||||
const operation = form.getFieldValue([parentName, name, 'operation']);
|
||||
if (operation === 'clear') return null;
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
noStyle
|
||||
>
|
||||
{dataType === 'number' && operation === 'cover'
|
||||
? <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={dataType ? options.filter(vo => vo.dataType === dataType) : options}
|
||||
popupMatchSelectWidth={false}
|
||||
size={size}
|
||||
/>
|
||||
: dataType === 'number'
|
||||
? <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([name, 'value'], value)}
|
||||
size={size}
|
||||
/>
|
||||
: operation === 'assign'
|
||||
? <>
|
||||
{dataType === 'boolean'
|
||||
? <Radio.Group block size={size}>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
: <Input.TextArea
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
rows={3}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
: <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={dataType ? options.filter(vo => vo.dataType === dataType) : options}
|
||||
popupMatchSelectWidth={false}
|
||||
size={size}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { type FC } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, Space, Row, Col, Divider, InputNumber, Radio, type SelectProps } from 'antd'
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Form, Button, Select, Space, Divider, InputNumber, Radio, type SelectProps } from 'antd'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
@@ -247,37 +246,40 @@ const CaseList: FC<CaseListProps> = ({
|
||||
{(conditionFields, { add: addCondition, remove: removeCondition }) => {
|
||||
const logicalOperator = form.getFieldValue(name)?.[caseIndex]?.logical_operator || 'and'
|
||||
return (
|
||||
<div className={clsx("rb:relative rb:mb-4 rb:border rb:border-gray-200 rb:rounded rb:p-3 rb:pl-5")}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||
<span className="rb:font-medium">
|
||||
{caseIndex === 0 ? 'IF' : 'ELIF'}<br/>
|
||||
{caseFields.length > 1 && <span className="rb:text-[10px] rb:text-[#5B6167]">{`CASE ${caseIndex + 1}`}</span>}
|
||||
</span>
|
||||
<div className={clsx("rb:relative")}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:leading-4.5">
|
||||
<span className="rb:font-medium ">{caseIndex === 0 ? 'IF' : 'ELIF'}</span>
|
||||
{caseFields.length > 1 && <span className="rb:text-[10px] rb:text-[#5B6167]"> ({`CASE ${caseIndex + 1}`})</span>}
|
||||
</div>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => addCondition({})}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addCase')}
|
||||
</Button>
|
||||
{caseFields.length > 1 && <DeleteOutlined
|
||||
className="rb:text-[12px]"
|
||||
onClick={() => handleRemoveCase(removeCase, caseField.name, caseIndex)}
|
||||
/>}
|
||||
{caseFields.length > 1 &&
|
||||
<Button
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
onClick={() => handleRemoveCase(removeCase, caseField.name, caseIndex)}
|
||||
>
|
||||
{t('common.remove')}
|
||||
</Button>
|
||||
}
|
||||
</Space>
|
||||
</div>
|
||||
{conditionFields?.length > 1 &&
|
||||
<>
|
||||
<div className="rb:absolute rb:w-3 rb:left-2 rb:top-15 rb:bottom-6 rb:z-10 rb:border rb:border-[#DFE4ED] rb:rounded-l-md rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[50%] rb:transform-[translateY(-50%)]]">
|
||||
{conditionFields?.length > 1 && <div className="rb:absolute rb:top-8 rb:bottom-4 rb:w-8.5 rb:h-[calc(100%-32px)]">
|
||||
<div className="rb:absolute rb:w-2.5 rb:h-[calc(50%-30px)] rb:left-5 rb:top-4 rb:z-10 rb:border-l rb:border-t rb:border-[#DFE4ED] rb:rounded-tl-[10px] rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[calc(50%-13px)]">
|
||||
<Form.Item name={[caseField.name, 'logical_operator']} noStyle >
|
||||
<Button size="small" className="rb:cursor-pointer" onClick={() => handleChangeLogicalOperator(caseIndex)}>{logicalOperator}</Button>
|
||||
<Button size="small" className="rb:text-[12px]! rb:py-px! rb:px-1! rb:w-8.5! rb:h-5!" onClick={() => handleChangeLogicalOperator(caseIndex)}>{logicalOperator}</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
<div className="rb:absolute rb:w-2.5 rb:h-[calc(50%-30px)] rb:left-5 rb:bottom-4 rb:z-10 rb:border-l rb:border-b rb:border-[#DFE4ED] rb:rounded-bl-[10px] rb:border-r-0"></div>
|
||||
</div>}
|
||||
{conditionFields.map((conditionField, conditionIndex) => {
|
||||
const cases = form.getFieldValue(name) || [];
|
||||
const currentCase = cases[caseIndex] || {};
|
||||
@@ -290,91 +292,86 @@ const CaseList: FC<CaseListProps> = ({
|
||||
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
|
||||
return (
|
||||
<div key={conditionField.key} className={clsx({
|
||||
"rb:mb-3": conditionIndex !== conditionFields.length - 1
|
||||
})}>
|
||||
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||
<Row gutter={12} className="rb:mb-1">
|
||||
<Col span={14}>
|
||||
<Form.Item name={[conditionField.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
size="small"
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={(val) => handleLeftFieldChange(caseIndex, conditionIndex, val)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name={[conditionField.name, 'operator']} noStyle>
|
||||
<Select
|
||||
options={operatorList.map(vo => ({
|
||||
...vo,
|
||||
label: t(String(vo?.label || ''))
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteOutlined
|
||||
className="rb:text-[12px]"
|
||||
onClick={() => removeCondition(conditionField.name)}
|
||||
<div key={conditionField.key} className="rb:flex rb:items-start rb:ml-9.5 rb:mb-4">
|
||||
<div className="rb:flex-1 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||
<div className={clsx("rb:flex rb:gap-1 rb:p-1", {
|
||||
'rb:border-b rb:border-b-[#DFE4ED]': !hideRightField
|
||||
})}>
|
||||
<Form.Item name={[conditionField.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
size="small"
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={(val) => handleLeftFieldChange(caseIndex, conditionIndex, val)}
|
||||
className="rb:bg-white! rb:w-29.5!"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
<Form.Item name={[conditionField.name, 'operator']} noStyle>
|
||||
<Select
|
||||
options={operatorList.map(vo => ({
|
||||
...vo,
|
||||
label: t(String(vo?.label || ''))
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
className="rb:bg-white! rb:w-22!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{!hideRightField && <>
|
||||
{!hideRightField && <div className="rb:p-1">
|
||||
{leftFieldType === 'number'
|
||||
? <Row>
|
||||
<Col span={12}>
|
||||
<Form.Item name={[conditionField.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
? <div className="rb:flex rb:items-center">
|
||||
<Form.Item name={[conditionField.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
onChange={() => handleInputTypeChange(caseIndex, conditionIndex)}
|
||||
className="rb:w-18!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Divider type="vertical" />
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
{inputType === 'Variable'
|
||||
?
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
|
||||
options={options.filter(vo => vo.dataType === 'number')}
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
onChange={() => handleInputTypeChange(caseIndex, conditionIndex)}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
{inputType === 'Variable'
|
||||
?
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.dataType === 'number')}
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
: <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)}
|
||||
/>
|
||||
: <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
}
|
||||
</Form.Item>
|
||||
</div>
|
||||
: <Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
{leftFieldType === 'boolean'
|
||||
? <Radio.Group block>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
: <Editor options={options} />
|
||||
: <Editor options={options} size="small" type="input" />
|
||||
}
|
||||
</Form.Item>
|
||||
}
|
||||
</>}
|
||||
</div>}
|
||||
</div>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => removeCondition(conditionField.name)}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@@ -388,6 +385,8 @@ const CaseList: FC<CaseListProps> = ({
|
||||
<Button
|
||||
type="dashed"
|
||||
block
|
||||
size="middle"
|
||||
className="rb:text-[12px]!"
|
||||
onClick={() => handleAddCase(addCase)}
|
||||
>
|
||||
+ ELIF
|
||||
@@ -395,9 +394,9 @@ const CaseList: FC<CaseListProps> = ({
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Divider />
|
||||
<div className="rb:font-medium">ELSE</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] ">{t('workflow.config.if-else.else_desc')}</div>
|
||||
|
||||
<div className="rb:font-medium rb:text-[12px] rb:mt-4 rb:leading-4.5">ELSE</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2 rb:leading-4.5">{t('workflow.config.if-else.else_desc')}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Space } from 'antd';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Graph, Node } from '@antv/x6';
|
||||
|
||||
import Editor from '../../Editor';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
|
||||
@@ -151,17 +151,15 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
|
||||
const contentLength = (currentItem.class_name || '').length;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-[#F8F9FB]">
|
||||
<div key={key} className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-2 rb:bg-[#F8F9FB]">
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div>{t('workflow.config.question-classifier.class_name')} {index + 1}</div>
|
||||
<div className="rb:text-[12px] rb:font-medium rb:py-1 rb:leading-2">{t('workflow.config.question-classifier.class_name')} {index + 1}</div>
|
||||
<div className="rb:flex rb:items-center rb:gap-1">
|
||||
<span className="rb:text-xs rb:text-gray-500">{contentLength}</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => handleRemoveCategory(remove, name, index)}
|
||||
/>
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
@@ -172,6 +170,7 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
|
||||
<Editor
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
options={options}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
@@ -179,8 +178,10 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
|
||||
|
||||
<Button
|
||||
type="dashed"
|
||||
size="middle"
|
||||
block
|
||||
onClick={() => handleAddCategory(add)}
|
||||
className="rb:w-full"
|
||||
className="rb:text-[12px]!"
|
||||
>
|
||||
+ {t('workflow.config.question-classifier.addClassName')}
|
||||
</Button>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { type FC } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, Row, Col, InputNumber, Radio, Input, type SelectProps } from 'antd'
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Form, Button, Select, InputNumber, Radio, Input, Divider, type SelectProps } from 'antd'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
import Editor from '../../Editor'
|
||||
|
||||
interface Case {
|
||||
logical_operator: 'and' | 'or';
|
||||
@@ -84,52 +83,64 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
return (
|
||||
<>
|
||||
<Form.List name={[parentName, 'expressions']}>
|
||||
{(fields, { add, remove }) => (
|
||||
<div>
|
||||
{(fields, { add, remove }) => {
|
||||
const logicalOperator = form.getFieldValue([parentName, 'logical_operator']);
|
||||
return (
|
||||
<div className="rb:relative">
|
||||
{fields.map((field, index) => {
|
||||
const expressions = form.getFieldValue([parentName, 'expressions']) || [];
|
||||
const currentExpression = expressions[index] || {};
|
||||
const currentOperator = currentExpression.operator;
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
|
||||
const leftFieldValue = currentExpression.left;
|
||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
||||
const leftFieldType = leftFieldOption?.dataType;
|
||||
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
|
||||
const logicalOperator = form.getFieldValue([parentName, 'logical_operator']);
|
||||
|
||||
return (
|
||||
<div key={field.key} className="rb:mb-3">
|
||||
{index > 0 && (<>
|
||||
<div className="rb:absolute rb:w-3 rb:left-2 rb:top-3.75 rb:bottom-3.75 rb:z-10 rb:border rb:border-[#DFE4ED] rb:rounded-l-md rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[50%] rb:transform-[translateY(-50%)]]">
|
||||
<Form.Item name={[parentName, 'logical_operator']} noStyle >
|
||||
<Button size="small" className="rb:cursor-pointer" onClick={handleChangeLogicalOperator}>{logicalOperator}</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>)}
|
||||
|
||||
<div className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-white rb:ml-6">
|
||||
<Row gutter={8} align="middle">
|
||||
<Col span={14}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||
{t('workflow.config.loop.condition')}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => add({})}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.loop.addCondition')}
|
||||
</Button>
|
||||
</div>
|
||||
{fields?.length > 1 && <div className="rb:absolute rb:top-8 rb:bottom-4 rb:w-8.5 rb:h-[calc(100%-32px)]">
|
||||
<div className="rb:absolute rb:w-2.5 rb:h-[calc(50%-30px)] rb:left-5 rb:top-4 rb:z-10 rb:border-l rb:border-t rb:border-[#DFE4ED] rb:rounded-tl-[10px] rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[calc(50%-13px)]">
|
||||
<Form.Item name={[parentName, 'logical_operator']} noStyle >
|
||||
<Button size="small" className="rb:text-[12px]! rb:py-px! rb:px-1! rb:w-8.5! rb:h-5!" onClick={handleChangeLogicalOperator}>{logicalOperator}</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="rb:absolute rb:w-2.5 rb:h-[calc(50%-30px)] rb:left-5 rb:bottom-4 rb:z-10 rb:border-l rb:border-b rb:border-[#DFE4ED] rb:rounded-bl-[10px] rb:border-r-0"></div>
|
||||
</div>}
|
||||
{fields.map((field, index) => {
|
||||
const expressions = form.getFieldValue([parentName, 'expressions']) || [];
|
||||
const currentExpression = expressions[index] || {};
|
||||
const currentOperator = currentExpression.operator;
|
||||
const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty';
|
||||
const leftFieldValue = currentExpression.left;
|
||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
||||
const leftFieldType = leftFieldOption?.dataType;
|
||||
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
|
||||
|
||||
return (
|
||||
<div key={field.key} className="rb:flex rb:items-start rb:ml-9.5 rb:mb-4">
|
||||
<div className="rb:flex-1 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||
<div className={clsx("rb:flex rb:gap-1 rb:p-1", {
|
||||
'rb:border-b rb:border-b-[#DFE4ED]': !hideRightField
|
||||
})}>
|
||||
<Form.Item name={[field.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
options={options.filter(vo =>
|
||||
vo.value.includes('sys.') ||
|
||||
vo.value.includes('conv.') ||
|
||||
options={options.filter(vo =>
|
||||
vo.value.includes('sys.') ||
|
||||
vo.value.includes('conv.') ||
|
||||
vo.nodeData.type === 'loop' ||
|
||||
(vo.nodeData.cycle && vo.nodeData.cycle === selectedNode?.id)
|
||||
)}
|
||||
size="small"
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
onChange={(val) => handleLeftFieldChange(index, val)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={8}>
|
||||
<Form.Item name={[field.name, 'operator']} noStyle>
|
||||
<Select
|
||||
options={operatorList.map(vo => ({
|
||||
@@ -138,84 +149,67 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteOutlined
|
||||
className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-red-500"
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
</Col>
|
||||
</div>
|
||||
|
||||
{!hideRightField && <>
|
||||
{!hideRightField && <div className="rb:p-1">
|
||||
{leftFieldType === 'number'
|
||||
? <Col span={24}><Row>
|
||||
<Col span={12}>
|
||||
<Form.Item name={[field.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
? <div className="rb:flex rb:items-center">
|
||||
<Form.Item name={[field.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={() => handleInputTypeChange(index)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Divider type="vertical" />
|
||||
<Form.Item name={[field.name, 'right']} noStyle>
|
||||
{inputType === 'Variable'
|
||||
?
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[{ value: 'Variable', label: 'Variable' }, { value: 'Constant', label: 'Constant' }]}
|
||||
options={options.filter(vo => vo.dataType === 'number')}
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={() => handleInputTypeChange(index)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name={[field.name, 'right']} noStyle>
|
||||
{inputType === 'Variable'
|
||||
?
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.dataType === 'number')}
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
: <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([parentName, 'expressions', index, 'right'], value)}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row></Col>
|
||||
: <Col span={24}>
|
||||
<Form.Item name={[field.name, 'right']} noStyle>
|
||||
{leftFieldType === 'boolean'
|
||||
? <Radio.Group block>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
: <Input placeholder={t('common.pleaseEnter')} />
|
||||
: <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([parentName, 'expressions', index, 'right'], value)}
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</div>
|
||||
: <Form.Item name={[field.name, 'right']} noStyle>
|
||||
{leftFieldType === 'boolean'
|
||||
? <Radio.Group block>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
: <Input placeholder={t('common.pleaseEnter')} />
|
||||
}
|
||||
</Form.Item>
|
||||
}
|
||||
</>}
|
||||
|
||||
</Row>
|
||||
</div>}
|
||||
</div>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(field.name)}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add({ left: '', operator: '', right: '' })}
|
||||
className="rb:w-full rb:ml-6 rb:mt-2"
|
||||
icon={<span>+</span>}
|
||||
>
|
||||
添加条件
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}}
|
||||
</Form.List>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Select, Row, Col, Input } from 'antd'
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Form, Select, Input, Button } from 'antd'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
@@ -20,6 +19,7 @@ interface CycleVarsListProps {
|
||||
parentName: string;
|
||||
selectedNode?: any;
|
||||
graphRef?: any;
|
||||
size?: 'small' | 'middle'
|
||||
}
|
||||
|
||||
const types = [
|
||||
@@ -37,7 +37,8 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
options,
|
||||
parentName,
|
||||
selectedNode,
|
||||
graphRef
|
||||
graphRef,
|
||||
size = 'middle'
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
@@ -78,62 +79,56 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
const availableOptions = getChildNodeVariables();
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||
<span className="rb:text-sm rb:font-medium">循环变量</span>
|
||||
<PlusOutlined className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-blue-500" onClick={() => add({ name: '', type: 'string', input_type: 'constant', value: '' })} />
|
||||
</div>
|
||||
{fields.map(({ key, name, ...field }, index) => {
|
||||
const currentInputType = value?.[index]?.input_type;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:mb-3 rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-3 rb:bg-white">
|
||||
<Row gutter={8} align="middle" className="rb:mb-2">
|
||||
<Col span={8}>
|
||||
<Form.Item name={[name, 'name']} noStyle>
|
||||
<Input size="small" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name={[name, 'type']} noStyle>
|
||||
<Select
|
||||
options={types.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.parameter-extractor.${key}`),
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name={[name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder="Constant"
|
||||
options={[
|
||||
{ label: 'Constant', value: 'constant' },
|
||||
{ label: 'Variable', value: 'variable' }
|
||||
]}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
// 重置 value 字段
|
||||
form.setFieldValue([parentName, index, 'value'], undefined);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<DeleteOutlined
|
||||
className="rb:text-gray-400 rb:cursor-pointer rb:hover:text-red-500"
|
||||
onClick={() => remove(name)}
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||
<span className="rb:text-[12px] rb:font-medium">{t('workflow.config.loop.cycle_vars')}</span>
|
||||
<Button
|
||||
onClick={() => add({ name: '', type: 'string', input_type: 'constant', value: '' })}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
</div>
|
||||
{fields.map(({ key, name }, index) => {
|
||||
const currentInputType = value?.[index]?.input_type;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:flex rb:items-start rb:mb-2">
|
||||
<div className="rb:flex-1 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||
<div className="rb:flex rb:gap-1 rb:p-1 rb:border-b rb:border-b-[#DFE4ED]">
|
||||
<Form.Item name={[name, 'name']} noStyle>
|
||||
<Input size={size} className="rb:w-23!" placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<Form.Item name={[name, 'type']} noStyle>
|
||||
<Select
|
||||
options={types.map(key => ({
|
||||
value: key,
|
||||
label: t(`workflow.config.parameter-extractor.${key}`),
|
||||
}))}
|
||||
size={size}
|
||||
popupMatchSelectWidth={false}
|
||||
className="rb:w-18.5!"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
<Form.Item name={[name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder="Constant"
|
||||
options={[
|
||||
{ label: 'Constant', value: 'constant' },
|
||||
{ label: 'Variable', value: 'variable' }
|
||||
]}
|
||||
size={size}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([parentName, index, 'value'], undefined);
|
||||
}}
|
||||
className="rb:w-18!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item name={[name, 'value']} noStyle>
|
||||
{currentInputType === 'variable' ? (
|
||||
@@ -145,22 +140,29 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
|
||||
return option.dataType === currentType
|
||||
})}
|
||||
variant="borderless"
|
||||
size="small"
|
||||
/>
|
||||
) : (
|
||||
<Input.TextArea
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
rows={3}
|
||||
className="rb:w-full"
|
||||
variant="borderless"
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</div>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input, Button, Row, Col } from 'antd'
|
||||
import { MinusCircleOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
@@ -9,13 +9,15 @@ interface GroupVariableListProps {
|
||||
value?: Array<{ key: string; value: string[]; }>;
|
||||
name: string;
|
||||
options: Suggestion[];
|
||||
isCanAdd: boolean
|
||||
isCanAdd: boolean;
|
||||
size: 'small' | 'middle'
|
||||
}
|
||||
|
||||
const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
name,
|
||||
options = [],
|
||||
isCanAdd = false
|
||||
isCanAdd = false,
|
||||
size = "middle"
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
@@ -54,6 +56,7 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={filteredOptions}
|
||||
mode="multiple"
|
||||
size={size}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
@@ -76,11 +79,15 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
]}
|
||||
noStyle
|
||||
>
|
||||
{isCanAdd ? <Input placeholder={t('common.pleaseEnter')} /> : t('workflow.config.var-aggregator.variable')}
|
||||
{isCanAdd ? <Input placeholder={t('common.pleaseEnter')} size={size} /> : t('workflow.config.var-aggregator.variable')}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
{isCanAdd && <Col span={12} className="rb:flex! rb:items-center rb:justify-end">
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</Col>}
|
||||
</Row>
|
||||
|
||||
@@ -104,16 +111,22 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
})()
|
||||
}
|
||||
mode="multiple"
|
||||
size={size}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{isCanAdd && <Form.Item noStyle>
|
||||
<Button type="dashed" onClick={() => add({ key: `Group${fields.length + 1}` })} block>
|
||||
+ {t('workflow.config.var-aggregator.addGroup')}
|
||||
</Button>
|
||||
</Form.Item>}
|
||||
|
||||
{isCanAdd && <Button
|
||||
type="dashed"
|
||||
block
|
||||
size="middle"
|
||||
className="rb:text-[12px]!"
|
||||
onClick={() => add({ key: `Group${fields.length + 1}` })}
|
||||
>
|
||||
+ {t('workflow.config.var-aggregator.addGroup')}
|
||||
</Button>}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
@@ -93,6 +93,7 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
||||
initialValues={{
|
||||
auth: 'none'
|
||||
}}
|
||||
size="middle"
|
||||
>
|
||||
<FormItem
|
||||
name="auth"
|
||||
@@ -102,6 +103,7 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
size="middle"
|
||||
options={[
|
||||
{ value: 'none', label: t('workflow.config.http-request.none') },
|
||||
{ value: 'api_key', label: t('workflow.config.http-request.apiKey') },
|
||||
@@ -117,6 +119,7 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
size="middle"
|
||||
options={[
|
||||
{ value: 'basic', label: t('workflow.config.http-request.basic') },
|
||||
{ value: 'bearer', label: t('workflow.config.http-request.bearer') },
|
||||
@@ -132,7 +135,7 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
||||
{ required: true, message: t('common.pleaseEnter') }
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
<Input size="middle" placeholder={t('common.pleaseEnter')} />
|
||||
</FormItem>
|
||||
}
|
||||
<FormItem
|
||||
@@ -142,7 +145,7 @@ const AuthConfigModal = forwardRef<AuthConfigModalRef, AuthConfigModalProps>(({
|
||||
{ required: true, message: t('common.pleaseEnter') }
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
<Input size="middle" placeholder={t('common.pleaseEnter')} />
|
||||
</FormItem>
|
||||
</>}
|
||||
</Form>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Select, Table, Form, type TableProps } from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin';
|
||||
import Empty from '@/components/Empty';
|
||||
import VariableSelect from '../VariableSelect';
|
||||
@@ -19,6 +18,7 @@ interface EditableTableProps {
|
||||
options?: Suggestion[];
|
||||
typeOptions?: { value: string, label: string }[]
|
||||
filterBooleanType?: boolean;
|
||||
size?: "small"
|
||||
}
|
||||
|
||||
const EditableTable: React.FC<EditableTableProps> = ({
|
||||
@@ -26,7 +26,8 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
title,
|
||||
options = [],
|
||||
typeOptions = [],
|
||||
filterBooleanType = false
|
||||
filterBooleanType = false,
|
||||
size = 'small'
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -38,21 +39,24 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
|
||||
const getColumns = (remove: (index: number) => void): TableProps<TableRow>['columns'] => {
|
||||
const hasType = typeOptions.length > 0;
|
||||
const baseWidth = hasType ? '35%' : '45%';
|
||||
const cellClassName="rb:p-1!"
|
||||
const contentClassName ="rb:w-[108px]! rb:text-[12px]!"
|
||||
|
||||
return [
|
||||
{
|
||||
title: t('workflow.config.name'),
|
||||
dataIndex: 'name',
|
||||
width: baseWidth,
|
||||
className: cellClassName,
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Form.Item name={[index, 'name']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
size="small"
|
||||
// size="small"
|
||||
options={options}
|
||||
filterBooleanType={filterBooleanType}
|
||||
popupMatchSelectWidth={false}
|
||||
className={contentClassName}
|
||||
size={size}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
@@ -61,18 +65,20 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
title: t('workflow.config.type'),
|
||||
dataIndex: 'type',
|
||||
width: '20%',
|
||||
className: cellClassName,
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{(form) => (
|
||||
<Form.Item name={[index, 'type']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
size="small"
|
||||
// size="small"
|
||||
options={typeOptions}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={() => {
|
||||
form.setFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'value'], undefined);
|
||||
}}
|
||||
size={size}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
@@ -82,7 +88,7 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
{
|
||||
title: t('workflow.config.value'),
|
||||
dataIndex: 'value',
|
||||
width: baseWidth,
|
||||
className: cellClassName,
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Form.Item
|
||||
shouldUpdate={(prevValues, currentValues) => {
|
||||
@@ -102,10 +108,12 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
<Form.Item name={[index, 'value']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
size="small"
|
||||
// size="small"
|
||||
options={filteredOptions}
|
||||
filterBooleanType={filterBooleanType}
|
||||
popupMatchSelectWidth={false}
|
||||
className={contentClassName}
|
||||
size={size}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
@@ -116,9 +124,12 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'actions',
|
||||
width: '10%',
|
||||
className: cellClassName,
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Button type="text" icon={<DeleteOutlined />} onClick={() => remove(index)} />
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(index)}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
];
|
||||
@@ -129,13 +140,11 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => {
|
||||
const AddButton = ({ block = false }: { block?: boolean }) => (
|
||||
<Button
|
||||
type={block ? "dashed" : "text"}
|
||||
<Button
|
||||
icon={block ? undefined : <PlusOutlined />}
|
||||
onClick={() => add(createNewRow())}
|
||||
size="small"
|
||||
block={block}
|
||||
className={block ? "rb:mt-1" : ""}
|
||||
className={block ? "rb:mt-1 rb:text-[12px]! rb:bg-transparent!" : "rb:text-[12px]!"}
|
||||
>
|
||||
{block && `+${t('common.add')}`}
|
||||
</Button>
|
||||
@@ -145,8 +154,8 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
<>
|
||||
{title && (
|
||||
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
|
||||
<div className="rb:font-medium">{title}</div>
|
||||
<AddButton />
|
||||
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5">{title}</div>
|
||||
<AddButton block={true} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -161,8 +170,9 @@ const EditableTable: React.FC<EditableTableProps> = ({
|
||||
columns={getColumns(remove)}
|
||||
pagination={false}
|
||||
size="small"
|
||||
rowClassName="rb:p-0! rb:bg-[#F6F8FC]!"
|
||||
locale={{ emptyText: <Empty size={88} /> }}
|
||||
scroll={{ x: 'max-content' }}
|
||||
style={{ width: '274px' }}
|
||||
/>
|
||||
|
||||
{!title && <AddButton block />}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type FC, useRef } from "react";
|
||||
import { type FC, useRef, useState } from "react";
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input } from 'antd'
|
||||
import { CaretDownOutlined, CaretRightOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import Editor from '../../Editor'
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import AuthConfigModal from './AuthConfigModal'
|
||||
@@ -65,15 +66,23 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
}
|
||||
}
|
||||
|
||||
console.log('HttpRequest', values)
|
||||
const [collapsed, setCollapsed] = useState(true)
|
||||
const handleToggle = () => {
|
||||
setCollapsed((prev: boolean) => !prev)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-4">
|
||||
<div>API</div>
|
||||
<Button onClick={handleChangeAuth}>{t('workflow.config.http-request.auth')}</Button>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-1">
|
||||
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5">API</div>
|
||||
<Button onClick={handleChangeAuth}
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<SettingOutlined />}
|
||||
className="rb:mt-1 rb:text-[12px]!"
|
||||
>{t('workflow.config.http-request.auth')}: {!values?.auth?.auth_type || values?.auth?.auth_type === 'none' ? t('workflow.config.http-request.none') : t('workflow.config.http-request.apiKey')}</Button>
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
<Row gutter={4}>
|
||||
<Col span={8}>
|
||||
<Form.Item name="method">
|
||||
<Select
|
||||
@@ -85,35 +94,43 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
{ label: 'PUT', value: 'PUT' },
|
||||
{ label: 'DELETE', value: 'DELETE' },
|
||||
]}
|
||||
className="rb:bg-transparent!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={16}>
|
||||
<Form.Item name="url">
|
||||
<Editor options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')} variant="outlined" />
|
||||
<Editor
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
variant="outlined"
|
||||
type="input"
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="auth" hidden>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="headers">
|
||||
<Form.Item name="headers" noStyle>
|
||||
<EditableTable
|
||||
size="small"
|
||||
parentName="headers"
|
||||
title="HEADERS"
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="params">
|
||||
<Form.Item name="params" noStyle>
|
||||
<EditableTable
|
||||
size="small"
|
||||
parentName="params"
|
||||
title="PARAMS"
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="BODY">
|
||||
<Form.Item label="BODY" className="rb:mb-0!">
|
||||
<Form.Item name={['body', 'content_type']}>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
@@ -131,6 +148,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
{values?.body?.content_type === 'form-data' &&
|
||||
<Form.Item name={['body', 'data']} noStyle>
|
||||
<EditableTable
|
||||
size="small"
|
||||
parentName={['body', 'data']}
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
typeOptions={[
|
||||
@@ -143,6 +161,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
{values?.body?.content_type === 'x-www-form-urlencoded' &&
|
||||
<Form.Item name={['body', 'data']} noStyle>
|
||||
<EditableTable
|
||||
size="small"
|
||||
parentName={['body', 'data']}
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
filterBooleanType={true}
|
||||
@@ -150,7 +169,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
</Form.Item>
|
||||
}
|
||||
{values?.body?.content_type === 'json' &&
|
||||
<Form.Item name={['body', 'data']}>
|
||||
<Form.Item name={['body', 'data']} noStyle>
|
||||
<MessageEditor
|
||||
key="json"
|
||||
parentName={['body', 'data']}
|
||||
@@ -161,7 +180,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
</Form.Item>
|
||||
}
|
||||
{values?.body?.content_type === 'raw' &&
|
||||
<Form.Item name={['body', 'data']}>
|
||||
<Form.Item name={['body', 'data']} noStyle>
|
||||
<MessageEditor
|
||||
key="raw"
|
||||
parentName={['body', 'data']}
|
||||
@@ -172,7 +191,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
</Form.Item>
|
||||
}
|
||||
{values?.body?.content_type === 'binary' &&
|
||||
<Form.Item name={['body', 'data']}>
|
||||
<Form.Item name={['body', 'data']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.dataType.includes('file'))}
|
||||
@@ -182,15 +201,19 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
}
|
||||
</Form.Item>
|
||||
<Divider />
|
||||
<Form.Item layout="horizontal" name="verify_ssl" label={t('workflow.config.http-request.verify_ssl')}>
|
||||
<Form.Item layout="horizontal" name="verify_ssl" label={t('workflow.config.http-request.verify_ssl')} className="rb:mb-0!">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Divider />
|
||||
<div>{t('workflow.config.http-request.timeouts')}</div>
|
||||
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5 rb:mb-2.5 rb:cursor-pointer" onClick={handleToggle}>
|
||||
{t('workflow.config.http-request.timeouts')}
|
||||
{collapsed ? <CaretRightOutlined /> : <CaretDownOutlined />}
|
||||
</div>
|
||||
<Form.Item
|
||||
name={['timeouts', 'connect_timeout']}
|
||||
label={t('workflow.config.http-request.connect_timeout')}
|
||||
hidden={collapsed}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
@@ -201,6 +224,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
<Form.Item
|
||||
name={['timeouts', 'read_timeout']}
|
||||
label={t('workflow.config.http-request.read_timeout')}
|
||||
hidden={collapsed}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
@@ -211,6 +235,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
<Form.Item
|
||||
name={['timeouts', 'write_timeout']}
|
||||
label={t('workflow.config.http-request.write_timeout')}
|
||||
hidden={collapsed}
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type FC, useRef, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Space, Button, List } from 'antd'
|
||||
import { Space, Button } from 'antd'
|
||||
import knowledgeEmpty from '@/assets/images/application/knowledgeEmpty.svg'
|
||||
import type {
|
||||
KnowledgeConfigForm,
|
||||
@@ -113,49 +113,65 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className="rb:flex rb:justify-between rb:items-center">
|
||||
<div>{t('application.knowledgeBaseAssociation')}</div>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||
{t('application.knowledgeBaseAssociation')}
|
||||
</div>
|
||||
|
||||
<Space>
|
||||
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleKnowledgeConfig}>{t('workflow.config.knowledge-retrieval.recallConfig')}</Button>
|
||||
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAddKnowledge}>+</Button>
|
||||
</Space>
|
||||
<Button
|
||||
onClick={handleKnowledgeConfig}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]! rb:group rb:gap-0.5!"
|
||||
size="small"
|
||||
>
|
||||
<div
|
||||
className="rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/recall.svg')] rb:group-hover:bg-[url('@/assets/images/workflow/recall_hover.svg')]"
|
||||
></div>
|
||||
{t('workflow.config.knowledge-retrieval.recallConfig')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{knowledgeList.length === 0
|
||||
? <Empty url={knowledgeEmpty} size={88} subTitle={t('application.knowledgeEmpty')} />
|
||||
:
|
||||
<List
|
||||
grid={{ gutter: 12, column: 1 }}
|
||||
dataSource={knowledgeList}
|
||||
renderItem={(item) => {
|
||||
if (!item.id) return null
|
||||
return (
|
||||
<List.Item>
|
||||
<div key={item.id} className="rb:flex rb:items-center rb:justify-between rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="rb:font-medium rb:leading-4">
|
||||
{item.name}
|
||||
<Tag color={item.status === 1 ? 'success' : item.status === 0 ? 'default' : 'error'} className="rb:ml-2">
|
||||
{item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')}
|
||||
</Tag>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5">{t('application.contains', {include_count: item.doc_num})}</div>
|
||||
</div>
|
||||
<Space size={12}>
|
||||
<div
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
onClick={() => handleEditKnowledge(item)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDeleteKnowledge(item.id)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
</List.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
<Space size={10} direction="vertical" className="rb:w-full!">
|
||||
<Button
|
||||
type="dashed"
|
||||
block
|
||||
size="middle"
|
||||
className="rb:text-[12px]!"
|
||||
onClick={handleAddKnowledge}
|
||||
>
|
||||
+ {t('workflow.config.knowledge-retrieval.addKnowledge')}
|
||||
</Button>
|
||||
|
||||
{knowledgeList.length === 0
|
||||
? <Empty url={knowledgeEmpty} size={88} subTitle={t('application.knowledgeEmpty')} />
|
||||
: knowledgeList.map(item => {
|
||||
if (!item.id) return null
|
||||
return (
|
||||
<div key={item.id} className="rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:py-2 rb:px-2.5 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="">
|
||||
<span className="rb:font-medium rb:leading-4">{item.name}</span>
|
||||
<Tag
|
||||
color={item.status === 1 ? 'success' : item.status === 0 ? 'default' : 'error'}
|
||||
className="rb:ml-1 rb:py-0! rb:px-1! rb:text-[12px] rb:leading-3.5!"
|
||||
>
|
||||
{item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')}
|
||||
</Tag>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5">{t('application.contains', { include_count: item.doc_num })}</div>
|
||||
</div>
|
||||
<Space size={12}>
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
onClick={() => handleEditKnowledge(item)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
onClick={() => handleDeleteKnowledge(item.id)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Space>
|
||||
{/* 全局设置 */}
|
||||
<KnowledgeGlobalConfigModal
|
||||
data={editConfig}
|
||||
|
||||
@@ -83,6 +83,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
size="middle"
|
||||
>
|
||||
{data && (
|
||||
<div className="rb:mb-6 rb:flex rb:items-center rb:justify-between rb:border rb:rounded-lg rb:p-[17px_16px] rb:cursor-pointer rb:bg-[#F0F3F8] rb:border-[#DFE4ED] rb:text-[#212332]">
|
||||
|
||||
@@ -70,6 +70,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
size="middle"
|
||||
>
|
||||
<div className="rb:text-[#5B6167] rb:mb-6">{t('application.globalConfigDesc')}</div>
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
|
||||
placeholder={t('knowledgeBase.searchPlaceholder')}
|
||||
onSearch={handleSearch}
|
||||
style={{ width: '100%' }}
|
||||
size="middle"
|
||||
/>
|
||||
{filterList.length === 0
|
||||
? <Empty />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { MinusCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, Input, Space, Row, Col } from 'antd';
|
||||
import { Button, Form, Input, Divider } from 'antd';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
@@ -16,43 +15,55 @@ const MappingList: React.FC<MappingListProps> = ({ name, options }) => {
|
||||
<Form.List name={name}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Row key={key} gutter={12} className="rb:mb-2">
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'name']}
|
||||
noStyle
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} data-field-type="mapping-name" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
noStyle
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block>
|
||||
+ {t('common.add')}
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||
{t('workflow.config.jinja-render.mapping')}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => add()}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<div key={key} className="rb:flex rb:items-center rb:gap-1 rb:mb-2">
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'name']}
|
||||
noStyle
|
||||
>
|
||||
<Input
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
size="small"
|
||||
className="rb:w-24!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
noStyle
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options}
|
||||
popupMatchSelectWidth={false}
|
||||
size="small"
|
||||
className="rb:w-39!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Divider />
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ const MemoryConfig: FC<{ options: Suggestion[]; parentName: string; }> = ({
|
||||
return (
|
||||
<>
|
||||
{values?.memory?.enable && <>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:py-1.5 rb:px-2 rb:bg-[#F6F8FC] rb:rounded-md rb:mb-2">
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:py-1.5 rb:px-2 rb:text-[12px] rb:bg-[#F6F8FC] rb:rounded-md rb:mb-2">
|
||||
{t('workflow.config.llm.memory')}
|
||||
<span>{t('workflow.config.llm.inner')}</span>
|
||||
</div>
|
||||
@@ -40,6 +40,7 @@ const MemoryConfig: FC<{ options: Suggestion[]; parentName: string; }> = ({
|
||||
isArray={false}
|
||||
parentName={[parentName, 'messages']}
|
||||
options={options}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { type FC, useMemo } from 'react';
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd';
|
||||
import { MinusCircleOutlined } from '@ant-design/icons';
|
||||
import Editor from '../Editor'
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
|
||||
interface MessageEditor {
|
||||
options: Suggestion[];
|
||||
title?: string
|
||||
title?: string;
|
||||
titleVariant?: 'outlined' | 'borderless';
|
||||
isArray?: boolean;
|
||||
parentName?: string | string[];
|
||||
label?: string;
|
||||
@@ -15,6 +16,7 @@ interface MessageEditor {
|
||||
value?: string;
|
||||
enableJinja2?: boolean;
|
||||
onChange?: (value?: string) => void;
|
||||
size?: 'small' | 'default'
|
||||
}
|
||||
const roleOptions = [
|
||||
// { label: 'SYSTEM', value: 'SYSTEM' },
|
||||
@@ -23,11 +25,13 @@ const roleOptions = [
|
||||
]
|
||||
const MessageEditor: FC<MessageEditor> = ({
|
||||
title,
|
||||
titleVariant = 'outlined',
|
||||
isArray = true,
|
||||
parentName = 'messages',
|
||||
placeholder,
|
||||
options,
|
||||
enableJinja2 = false,
|
||||
size = 'default'
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance();
|
||||
@@ -74,14 +78,16 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
|
||||
if (!isArray) {
|
||||
return (
|
||||
<Space size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white" data-editor-type={parentName === 'template' ? 'template' : undefined}>
|
||||
<Space size={8} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5" data-editor-type={parentName === 'template' ? 'template' : undefined}>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
{title ?? t('workflow.answerDesc')}
|
||||
<div className={clsx("rb:text-[12px] rb:font-medium rb:py-1 rb:leading-2", {
|
||||
'rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-sm rb:px-2': titleVariant === 'outlined'
|
||||
})}>{title ?? t('workflow.answerDesc')}</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name={parentName} noStyle>
|
||||
<Editor enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
<Editor size={size} enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
);
|
||||
@@ -90,7 +96,7 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
return (
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<Space size={12} direction="vertical" className="rb:w-full">
|
||||
<Space size={8} direction="vertical" className="rb:w-full">
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
const fieldValue = Array.isArray(parentName)
|
||||
? parentName.reduce((obj, key) => obj?.[key], values)
|
||||
@@ -99,16 +105,17 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
const currentRole = (fieldValue?.[name]?.role || 'USER').toUpperCase();
|
||||
|
||||
return (
|
||||
<Space key={key} size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5 rb:bg-white">
|
||||
<Space key={key} size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-2">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item {...restField} name={[name, 'role']} noStyle>
|
||||
{currentRole === 'SYSTEM' ? (
|
||||
<Input disabled />
|
||||
<Input disabled className="rb:font-medium!" />
|
||||
) : (
|
||||
<Select
|
||||
options={roleOptions}
|
||||
disabled={currentRole === 'SYSTEM'}
|
||||
className="rb:font-medium!"
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
@@ -116,20 +123,23 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
{currentRole !== 'SYSTEM' && (
|
||||
<Col span={12}>
|
||||
<div className="rb:h-full rb:flex rb:justify-end rb:items-center">
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/delete_cycle.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</div>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
<Form.Item {...restField} name={[name, 'content']} noStyle>
|
||||
<Editor enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
<Editor size={size} enableJinja2={enableJinja2} placeholder={placeholder} options={processedOptions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
);
|
||||
})}
|
||||
<Form.Item noStyle>
|
||||
<Button type="dashed" onClick={() => handleAdd(add)} block>
|
||||
+{t('workflow.addMessage')}
|
||||
<Button type="dashed" size="middle" className="rb:text-[12px]!" onClick={() => handleAdd(add)} block>
|
||||
+ {t('workflow.addMessage')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
@@ -74,6 +74,7 @@ const ParamEditModal = forwardRef<ParamEditModalRef, ParamEditModalProps>(({
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
size="middle"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
>
|
||||
<FormItem
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FC, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Space, List } from 'antd'
|
||||
import Empty from '@/components/Empty'
|
||||
import { Button, Space } from 'antd'
|
||||
|
||||
|
||||
import type { ParamItem, ParamEditModalRef } from './types'
|
||||
import ParamEditModal from './ParamEditModal'
|
||||
@@ -41,47 +41,37 @@ const ParamsList: FC<ParamsListProps> = ({
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className="rb:flex rb:justify-between rb:items-center">
|
||||
<div>{label}</div>
|
||||
|
||||
<Space>
|
||||
<Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAdd}>+</Button>
|
||||
</Space>
|
||||
<div className="rb:leading-4.25 rb:text-[12px] rb:font-medium rb:mb-2">
|
||||
{label}
|
||||
</div>
|
||||
|
||||
{value?.length === 0
|
||||
? <Empty size={88} />
|
||||
:
|
||||
<List
|
||||
grid={{ gutter: 12, column: 1 }}
|
||||
dataSource={value}
|
||||
renderItem={(item, index) => (
|
||||
<List.Item>
|
||||
<div key={index} className="rb:group rb:relative rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:cursor-pointer rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div className="rb:leading-4">
|
||||
<span className="rb:font-medium">{item.name}</span>
|
||||
<span className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular"> ({t(`workflow.config.parameter-extractor.${item.type}`)})</span>
|
||||
</div>
|
||||
<span className="rb:block rb:group-hover:hidden rb:text-[12px] rb:text-[#5B6167] rb:font-regular">{item.required ? t('workflow.config.parameter-extractor.required') : ''}</span>
|
||||
<Space size={10} direction="vertical" className="rb:w-full!">
|
||||
<Button type="dashed" block size="middle" className="rb:text-[12px]!" onClick={handleAdd}>+ {t('workflow.config.parameter-extractor.addParams')}</Button>
|
||||
|
||||
</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:wrap-break-word rb:line-clamp-1">{item.desc}</div>
|
||||
<Space size={12} className="rb:hidden! rb:group-hover:flex! rb:absolute rb:right-4 rb:top-[50%] rb:transform-[translateY(-50%)] rb:bg-white">
|
||||
<div
|
||||
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
onClick={() => handleEdit(index)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDelete(index)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
{value?.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rb:cursor-pointer rb:group rb:py-2 rb:pl-2.5 rb:pr-2 rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md"
|
||||
>
|
||||
<div>
|
||||
<span className="rb:font-medium">{item.name}</span>
|
||||
<span className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular"> ({t(`workflow.config.parameter-extractor.${item.type}`)}) {item.required ? t('workflow.config.parameter-extractor.required') : ''}</span>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4.25 rb:mt-0.5">{item.desc}</div>
|
||||
</div>
|
||||
|
||||
<Space size={8}>
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
onClick={() => handleEdit(index)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
onClick={() => handleDelete(index)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
|
||||
<ParamEditModal
|
||||
ref={paramEditModalRef}
|
||||
|
||||
55
web/src/views/Workflow/components/Properties/RbSlider.tsx
Normal file
55
web/src/views/Workflow/components/Properties/RbSlider.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import type { InputNumberProps, SliderSingleProps } from 'antd';
|
||||
import { Col, InputNumber, Row, Slider } from 'antd';
|
||||
|
||||
const RbSlider: React.FC<SliderSingleProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
min,
|
||||
max,
|
||||
step = 0.01,
|
||||
...props
|
||||
}) => {
|
||||
const [curValue, setCurValue] = useState<number | undefined>(0)
|
||||
useEffect(() => {
|
||||
setCurValue(value)
|
||||
}, [value])
|
||||
const handleSliderChange = (newValue: number) => {
|
||||
onChange && onChange(newValue);
|
||||
};
|
||||
|
||||
const handleInputChange: InputNumberProps['onChange'] = (newValue) => {
|
||||
onChange && onChange(newValue as number);
|
||||
};
|
||||
|
||||
return (
|
||||
<Row gutter={12}>
|
||||
<Col span={16}>
|
||||
<Slider
|
||||
{...props}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step as number}
|
||||
value={curValue}
|
||||
className="rb:my-0! rb:ml-2.5!"
|
||||
classNames={{
|
||||
rail: 'rb:h-[6px]!',
|
||||
track: 'rb:h-[6px]!'
|
||||
}}
|
||||
onChange={handleSliderChange}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<InputNumber
|
||||
min={min}
|
||||
max={max}
|
||||
step={step as number}
|
||||
value={curValue}
|
||||
onChange={handleInputChange}
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
export default RbSlider;
|
||||
@@ -171,6 +171,7 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||
label={t('workflow.config.tool.tool_id')}
|
||||
>
|
||||
<Cascader
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={optionList}
|
||||
loadData={loadData}
|
||||
onChange={handleChange}
|
||||
@@ -187,28 +188,30 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||
label={parameter.name}
|
||||
extra={parameter.type === 'boolean' ? undefined : parameter.description}
|
||||
rules={[
|
||||
{ required: parameter.required, message: t('workflow.config.tool.required') }
|
||||
{ required: parameter.required, message: t('common.pleaseEnter') }
|
||||
]}
|
||||
layout={parameter.type === 'boolean' ? 'horizontal' : 'vertical'}
|
||||
className={parameter.type === 'boolean' ? 'rb:mb-0!' : ''}
|
||||
>
|
||||
{parameter.type === 'string' && parameter.enum && parameter.enum.length > 0
|
||||
? <Select options={parameter.enum.map(vo => ({ value: vo, label: vo }))} placeholder={t('common.pleaseSelect')} />
|
||||
? <Select size="small" options={parameter.enum.map(vo => ({ value: vo, label: vo }))} placeholder={t('common.pleaseSelect')} />
|
||||
: parameter.type === 'boolean'
|
||||
? <Switch />
|
||||
? <Switch size="small" />
|
||||
: parameter.type === 'integer' || parameter.type === 'number'
|
||||
? <InputNumber
|
||||
min={parameter.minimum}
|
||||
max={parameter.maximum}
|
||||
step={parameter.type === 'integer' ? 1 : 0.01}
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
className="rb:w-full!"
|
||||
size="small"
|
||||
onChange={(value) => form.setFieldValue(['tool_parameters', parameter.name], value)}
|
||||
/>
|
||||
: <Editor
|
||||
height={32}
|
||||
variant="outlined"
|
||||
options={options}
|
||||
size="small"
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, Select, InputNumber, Checkbox, Tag } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { StartVariableItem, VariableEditModalRef } from '../../types'
|
||||
import type { Variable, VariableEditModalRef } from './types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import SortableList from '@/components/SortableList'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface VariableEditModalProps {
|
||||
refresh: (values: StartVariableItem) => void;
|
||||
refresh: (values: Variable) => void;
|
||||
}
|
||||
|
||||
const types = [
|
||||
@@ -36,9 +36,9 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<StartVariableItem>();
|
||||
const [form] = Form.useForm<Variable>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editVo, setEditVo] = useState<StartVariableItem | null>(null)
|
||||
const [editVo, setEditVo] = useState<Variable | null>(null)
|
||||
|
||||
const values = Form.useWatch([], form);
|
||||
|
||||
@@ -50,7 +50,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
|
||||
setEditVo(null)
|
||||
};
|
||||
|
||||
const handleOpen = (variable?: StartVariableItem) => {
|
||||
const handleOpen = (variable?: Variable) => {
|
||||
setVisible(true);
|
||||
if (variable) {
|
||||
setEditVo(variable || null)
|
||||
@@ -85,7 +85,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={editVo ? t('workflow.config.start.editVariable') : t('workflow.config.start.addVariable')}
|
||||
title={editVo ? t('workflow.config.start.editVariable') : t('workflow.config.addVariable')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
@@ -96,6 +96,7 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={initialValues}
|
||||
size="middle"
|
||||
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
|
||||
>
|
||||
{/* 变量类型 */}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { type FC, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Node } from '@antv/x6';
|
||||
import { Space, Button, Divider, App } from 'antd'
|
||||
import type { Variable, VariableEditModalRef } from './types'
|
||||
import type { NodeConfig } from '../../../types'
|
||||
import VariableEditModal from './VariableEditModal'
|
||||
|
||||
interface VariableListProps {
|
||||
selectedNode?: Node | null;
|
||||
config: NodeConfig;
|
||||
value?: Variable[];
|
||||
parentName: string;
|
||||
onChange?: (value: Variable[]) => void;
|
||||
}
|
||||
const VariableList: FC<VariableListProps> = ({
|
||||
value = [],
|
||||
onChange,
|
||||
selectedNode,
|
||||
config,
|
||||
parentName
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { modal } = App.useApp()
|
||||
const variableModalRef = useRef<VariableEditModalRef>(null)
|
||||
const [editIndex, setEditIndex] = useState<number | null>(null)
|
||||
|
||||
const handleAddVariable = () => {
|
||||
setEditIndex(null)
|
||||
variableModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleEditVariable = (index: number, vo: Variable) => {
|
||||
variableModalRef.current?.handleOpen(vo)
|
||||
setEditIndex(index)
|
||||
}
|
||||
const handleRefreshVariable = (variable: Variable) => {
|
||||
if (!selectedNode) return
|
||||
|
||||
if (editIndex !== null) {
|
||||
const list = [...value]
|
||||
list[editIndex] = variable
|
||||
onChange?.(list)
|
||||
} else {
|
||||
console.log('VariableList', value, variable)
|
||||
onChange?.([...value, variable])
|
||||
}
|
||||
}
|
||||
const handleDeleteVariable = (index: number, vo: Variable, e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!selectedNode) return
|
||||
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: vo.name }),
|
||||
okText: t('common.delete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
const list = [...value]
|
||||
list.splice(index, 1)
|
||||
onChange?.([...list])
|
||||
}
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Space size={10} direction="vertical" className="rb:w-full">
|
||||
<div className="rb:leading-4.25 rb:text-[12px] rb:font-medium">
|
||||
{t(`workflow.config.${selectedNode?.data?.type}.${parentName}`)}
|
||||
</div>
|
||||
<Button type="dashed" block size="middle" className="rb:text-[12px]!" onClick={handleAddVariable}>+ {t('workflow.config.addVariable')}</Button>
|
||||
{Array.isArray(value) && value?.map((vo, index) =>
|
||||
<div
|
||||
key={`${vo.name}}-${index}`}
|
||||
className="rb:cursor-pointer rb:group rb:py-2 rb:pl-2.5 rb:pr-2 rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md"
|
||||
onClick={() => handleEditVariable(index, vo)}
|
||||
>
|
||||
<span className="rb:font-medium">{vo.name}·{vo.description}</span>
|
||||
|
||||
<Space size={8}>
|
||||
{vo.required && <span className="rb:py-px rb:px-2 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-sm">{t('workflow.config.start.required')}</span>}
|
||||
<span className="rb:py-px rb:px-2 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-sm">{vo.type}</span>
|
||||
<div
|
||||
className="rb:size-3 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
|
||||
onClick={(e) => handleDeleteVariable(index, vo, e)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</Space>
|
||||
<Divider size="small" />
|
||||
<Space size={10} direction="vertical" className="rb:w-full">
|
||||
{config.sys?.map((vo, index) =>
|
||||
<div key={index} className="rb:py-2 rb:pl-2.5 rb:pr-2 rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||
<span className="rb:font-medium">sys.{vo.name}</span>
|
||||
<span className="rb:py-px rb:px-2 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-sm">{vo.type}</span>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
<VariableEditModal
|
||||
ref={variableModalRef}
|
||||
refresh={handleRefreshVariable}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default VariableList
|
||||
@@ -0,0 +1,23 @@
|
||||
export interface Variable {
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
description: string;
|
||||
max_length?: number;
|
||||
default?: string;
|
||||
readonly?: boolean;
|
||||
defaultValue?: any;
|
||||
value?: any;
|
||||
}
|
||||
export interface VariableEditModalRef {
|
||||
handleOpen: (values?: Variable) => void;
|
||||
}
|
||||
|
||||
export interface ApiExtensionModalData {
|
||||
name: string;
|
||||
apiEndpoint: string;
|
||||
apiKey: string;
|
||||
}
|
||||
export interface ApiExtensionModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ interface VariableSelectProps extends SelectProps {
|
||||
onChange?: (value: string) => void;
|
||||
allowClear?: boolean;
|
||||
filterBooleanType?: boolean;
|
||||
size?: 'small' | 'middle' | 'large'
|
||||
}
|
||||
|
||||
const VariableSelect: FC<VariableSelectProps> = ({
|
||||
@@ -18,7 +19,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
value,
|
||||
allowClear = true,
|
||||
onChange,
|
||||
size,
|
||||
size = 'middle',
|
||||
filterBooleanType = false,
|
||||
...resetPorps
|
||||
}) => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { type FC, useEffect, useState, useRef, useMemo } from "react";
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Graph, Node } from '@antv/x6';
|
||||
import { Form, Input, Button, Select, InputNumber, Slider, Space, Divider, App, Switch } from 'antd'
|
||||
import { Form, Input, Select, InputNumber, Switch } from 'antd'
|
||||
|
||||
import type { NodeConfig, NodeProperties, StartVariableItem, VariableEditModalRef, ChatVariable } from '../../types'
|
||||
import type { NodeConfig, NodeProperties, ChatVariable } from '../../types'
|
||||
import Empty from '@/components/Empty';
|
||||
import emptyIcon from '@/assets/images/workflow/empty.png'
|
||||
import CustomSelect from "@/components/CustomSelect";
|
||||
import VariableEditModal from './VariableEditModal';
|
||||
import MessageEditor from './MessageEditor'
|
||||
import Knowledge from './Knowledge/Knowledge';
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
@@ -23,7 +23,11 @@ import CycleVarsList from './CycleVarsList'
|
||||
import AssignmentList from './AssignmentList'
|
||||
import ToolConfig from './ToolConfig'
|
||||
import MemoryConfig from './MemoryConfig'
|
||||
import VariableList from './VariableList'
|
||||
// import { calculateVariableList } from './utils/variableListCalculator'
|
||||
import styles from './properties.module.css'
|
||||
import Editor from "../Editor";
|
||||
import RbSlider from './RbSlider'
|
||||
|
||||
interface PropertiesProps {
|
||||
selectedNode?: Node | null;
|
||||
@@ -42,12 +46,9 @@ const Properties: FC<PropertiesProps> = ({
|
||||
chatVariables
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { modal } = App.useApp()
|
||||
const [form] = Form.useForm<NodeConfig>();
|
||||
const [configs, setConfigs] = useState<Record<string,NodeConfig>>({} as Record<string,NodeConfig>)
|
||||
const values = Form.useWatch([], form);
|
||||
const variableModalRef = useRef<VariableEditModalRef>(null)
|
||||
const [editIndex, setEditIndex] = useState<number | null>(null)
|
||||
const [graphUpdateTrigger, setGraphUpdateTrigger] = useState(0)
|
||||
const prevMappingNamesRef = useRef<string[]>([])
|
||||
const prevTemplateVarsRef = useRef<string[]>([])
|
||||
@@ -243,49 +244,6 @@ const Properties: FC<PropertiesProps> = ({
|
||||
}
|
||||
}, [values, selectedNode, form])
|
||||
|
||||
const handleAddVariable = () => {
|
||||
setEditIndex(null)
|
||||
variableModalRef.current?.handleOpen()
|
||||
}
|
||||
const handleEditVariable = (index: number, vo: StartVariableItem) => {
|
||||
variableModalRef.current?.handleOpen(vo)
|
||||
setEditIndex(index)
|
||||
}
|
||||
const handleRefreshVariable = (value: StartVariableItem) => {
|
||||
if (!selectedNode) return
|
||||
|
||||
if (editIndex !== null) {
|
||||
const defaultValue = selectedNode.data.config.variables.defaultValue ?? []
|
||||
defaultValue[editIndex] = value
|
||||
selectedNode.data.config.variables.defaultValue = [...defaultValue]
|
||||
} else {
|
||||
const defaultValue = selectedNode.data.config.variables.defaultValue ?? []
|
||||
selectedNode.data.config.variables.defaultValue = [...defaultValue, value]
|
||||
}
|
||||
selectedNode?.setData({ ...selectedNode.data})
|
||||
|
||||
setConfigs({ ...selectedNode.data.config })
|
||||
}
|
||||
const handleDeleteVariable = (index: number, vo: StartVariableItem) => {
|
||||
if (!selectedNode) return
|
||||
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: vo.name }),
|
||||
okText: t('common.delete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
const defaultValue = selectedNode.data.config.variables.defaultValue ?? []
|
||||
defaultValue.splice(index, 1)
|
||||
selectedNode.data.config.variables.defaultValue = [...defaultValue]
|
||||
|
||||
selectedNode?.setData({ ...selectedNode.data })
|
||||
|
||||
setConfigs({ ...selectedNode.data.config })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const variableList = useMemo(() => {
|
||||
if (!selectedNode || !graphRef?.current) return [];
|
||||
|
||||
@@ -586,7 +544,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
break
|
||||
case 'question-classifier':
|
||||
const classNameKey = `${dataNodeId}_class_name`;
|
||||
const outputKey = `${dataNodeId}_output`;
|
||||
// const outputKey = `${dataNodeId}_output`;
|
||||
if (!addedKeys.has(classNameKey)) {
|
||||
addedKeys.add(classNameKey);
|
||||
variableList.push({
|
||||
@@ -1039,11 +997,11 @@ const Properties: FC<PropertiesProps> = ({
|
||||
console.log('variableList', variableList)
|
||||
|
||||
return (
|
||||
<div className="rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3">
|
||||
<div className="rb:font-medium rb:leading-5 rb:mb-3">{t('workflow.nodeProperties')}</div>
|
||||
<div className={clsx("rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3 rb:pb-6", styles.properties)}>
|
||||
<div className="rb:font-medium rb:leading-5 rb:pb-3 rb:mb-3 rb:border-b rb:border-b-[#DFE4ED]">{t('workflow.nodeProperties')}</div>
|
||||
{!selectedNode
|
||||
? <Empty url={emptyIcon} size={140} className="rb:h-full rb:mx-15" title={t('workflow.empty')} />
|
||||
: <Form form={form} layout="vertical" className="rb:h-[calc(100%-20px)] rb:overflow-y-auto">
|
||||
: <Form form={form} size="small" layout="vertical" className="rb:h-[calc(100%-20px)] rb:overflow-x-hidden rb:overflow-y-auto">
|
||||
<Form.Item name="name" label={t('workflow.nodeName')}>
|
||||
<Input
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
@@ -1073,46 +1031,13 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') {
|
||||
return (
|
||||
<div key={key}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2.75">
|
||||
<div className="rb:leading-5">
|
||||
{t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
</div>
|
||||
<Button style={{padding: '0 8px', height: '24px'}} onClick={handleAddVariable}>+{t('application.addVariables')}</Button>
|
||||
</div>
|
||||
|
||||
<Space size={4} direction="vertical" className="rb:w-full">
|
||||
{Array.isArray(config.defaultValue) && config.defaultValue?.map((vo, index) =>
|
||||
<div key={`${vo.name}}-${index}`} className="rb:p-[4px_8px] rb:text-[12px] rb:text-[#5B6167] rb:flex rb:items-center rb:justify-between rb:border rb:border-[#DFE4ED] rb:rounded-md rb:group rb:cursor-pointer">
|
||||
<span>{vo.name}·{vo.description}</span>
|
||||
|
||||
<div className="rb:group-hover:hidden rb:flex rb:items-center rb:gap-1">
|
||||
{vo.required && <span>{t('workflow.config.start.required')}</span>}
|
||||
{vo.type}
|
||||
</div>
|
||||
<Space className="rb:hidden! rb:group-hover:flex!">
|
||||
<div
|
||||
className="rb:w-4.5 rb:h-4.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
onClick={() => handleEditVariable(index, vo)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:w-4.5 rb:h-4.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDeleteVariable(index, vo)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
<Divider size="small" />
|
||||
{config.sys?.map((vo, index) =>
|
||||
<div key={index} className="rb:p-[4px_8px] rb:text-[12px] rb:text-[#5B6167] rb:flex rb:items-center rb:justify-between rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||
<div>
|
||||
<span>sys.{vo.name}</span>
|
||||
</div>
|
||||
{vo.type}
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
<Form.Item key={key} name={key}>
|
||||
<VariableList
|
||||
parentName={key}
|
||||
selectedNode={selectedNode}
|
||||
config={config}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1143,23 +1068,12 @@ const Properties: FC<PropertiesProps> = ({
|
||||
key={key}
|
||||
options={contextVariableList.filter(variable => variable.nodeData?.type !== 'knowledge-retrieval')}
|
||||
parentName={key}
|
||||
placeholder={t(config.placeholder || 'common.pleaseSelect')}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
if (selectedNode?.data?.type === 'end' && key === 'output') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<MessageEditor
|
||||
key={key}
|
||||
isArray={false}
|
||||
parentName={key}
|
||||
options={variableList.filter(variable => variable.nodeData?.type !== 'knowledge-retrieval')}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
if (config.type === 'define') {
|
||||
return null
|
||||
}
|
||||
@@ -1184,6 +1098,8 @@ const Properties: FC<PropertiesProps> = ({
|
||||
parentName={key}
|
||||
enableJinja2={config.enableJinja2 as boolean}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
titleVariant={config.titleVariant}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
@@ -1206,9 +1122,9 @@ const Properties: FC<PropertiesProps> = ({
|
||||
name={key}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
isCanAdd={!!(values as any)?.group}
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
)
|
||||
}
|
||||
if (config.type === 'caseList') {
|
||||
@@ -1226,9 +1142,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
if (config.type === 'mappingList') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}
|
||||
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
>
|
||||
<Form.Item key={key} name={key} noStyle>
|
||||
<MappingList name={key} options={getFilteredVariableList(selectedNode?.data?.type, key)} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -1238,6 +1152,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<CycleVarsList
|
||||
size="small"
|
||||
parentName={key}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
/>
|
||||
@@ -1276,87 +1191,14 @@ const Properties: FC<PropertiesProps> = ({
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
key={key}
|
||||
name={key}
|
||||
label={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
layout={config.type === 'switch' ? 'horizontal' : 'vertical'}
|
||||
>
|
||||
{config.type === 'input'
|
||||
? <Input placeholder={t('common.pleaseEnter')} />
|
||||
: config.type === 'textarea'
|
||||
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
: config.type === 'select'
|
||||
? <Select
|
||||
options={config.needTranslation ? (config.options || []).map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
/>
|
||||
: config.type === 'inputNumber'
|
||||
? <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue(key, value)}
|
||||
/>
|
||||
: config.type === 'slider'
|
||||
? <Slider min={config.min} max={config.max} step={config.step} />
|
||||
: config.type === 'customSelect'
|
||||
? <CustomSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
url={config.url as string}
|
||||
params={config.params}
|
||||
hasAll={false}
|
||||
valueKey={config.valueKey}
|
||||
labelKey={config.labelKey}
|
||||
/>
|
||||
: config.type === 'variableList'
|
||||
? <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={(() => {
|
||||
const baseVariableList = getFilteredVariableList(selectedNode?.data?.type, key);
|
||||
// Apply filtering if specified in config
|
||||
if (config.filterNodeTypes || config.filterVariableNames) {
|
||||
return baseVariableList.filter(variable => {
|
||||
const nodeTypeMatch = !config.filterNodeTypes ||
|
||||
(Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type));
|
||||
const variableNameMatch = !config.filterVariableNames ||
|
||||
(Array.isArray(config.filterVariableNames) && config.filterVariableNames.includes(variable.label));
|
||||
return nodeTypeMatch || variableNameMatch;
|
||||
});
|
||||
}
|
||||
// Filter child nodes for iteration output
|
||||
if (config.filterChildNodes && selectedNode) {
|
||||
const graph = graphRef.current;
|
||||
if (!graph) return [];
|
||||
|
||||
const nodes = graph.getNodes();
|
||||
|
||||
// Find child nodes whose cycle field equals parent node's ID
|
||||
const childNodes = nodes.filter(node => {
|
||||
const nodeData = node.getData();
|
||||
return nodeData?.cycle === selectedNode.id;
|
||||
});
|
||||
|
||||
return baseVariableList.filter(variable =>
|
||||
childNodes.some(node => node.id === variable.nodeData?.id) || selectedNode?.data?.type === 'iteration' && key === 'output' && variable.value.includes('sys.')
|
||||
);
|
||||
}
|
||||
return baseVariableList;
|
||||
})()
|
||||
}
|
||||
/>
|
||||
: config.type === 'switch'
|
||||
? <Switch onChange={key === 'group' ? () => { form.setFieldValue('group_variables', []) } : undefined} />
|
||||
: config.type === 'categoryList'
|
||||
? <CategoryList
|
||||
parentName={key}
|
||||
selectedNode={selectedNode}
|
||||
graphRef={graphRef}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
/>
|
||||
: config.type === 'conditionList'
|
||||
? <ConditionList
|
||||
if (config.type === 'conditionList') {
|
||||
return (
|
||||
<Form.Item
|
||||
key={key}
|
||||
name={key}
|
||||
noStyle
|
||||
>
|
||||
<ConditionList
|
||||
parentName={key}
|
||||
options={(() => {
|
||||
const cycleVars = values?.cycle_vars || [];
|
||||
@@ -1375,6 +1217,93 @@ const Properties: FC<PropertiesProps> = ({
|
||||
graphRef={graphRef}
|
||||
addBtnText={t('workflow.config.addCase')}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
key={key}
|
||||
name={key}
|
||||
label={key === 'parallel_count' ? <span className="rb:text-[10px] rb:text-[#5B6167] rb:leading-3.5 rb:-mb-1!">{t(`workflow.config.${selectedNode?.data?.type}.${key}`)}</span> : t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
layout={config.type === 'switch' ? 'horizontal' : 'vertical'}
|
||||
className={key === 'parallel_count' ? 'rb:-mt-3! rb:leading-3.5!' : ''}
|
||||
>
|
||||
{config.type === 'input'
|
||||
? <Input placeholder={t('common.pleaseEnter')} />
|
||||
: config.type === 'textarea'
|
||||
? <Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
: config.type === 'select'
|
||||
? <Select
|
||||
options={config.needTranslation ? (config.options || []).map(vo => ({ ...vo, label: t(vo.label) })) : config.options}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
/>
|
||||
: config.type === 'inputNumber'
|
||||
? <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue(key, value)}
|
||||
/>
|
||||
: config.type === 'slider'
|
||||
? <RbSlider min={config.min} max={config.max} step={config.step} />
|
||||
: config.type === 'customSelect'
|
||||
? <CustomSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
url={config.url as string}
|
||||
params={config.params}
|
||||
hasAll={false}
|
||||
valueKey={config.valueKey}
|
||||
labelKey={config.labelKey}
|
||||
size="small"
|
||||
/>
|
||||
: config.type === 'variableList'
|
||||
? <VariableSelect
|
||||
placeholder={t(config.placeholder || 'common.pleaseSelect')}
|
||||
options={(() => {
|
||||
const baseVariableList = getFilteredVariableList(selectedNode?.data?.type, key);
|
||||
// Apply filtering if specified in config
|
||||
if (config.filterNodeTypes || config.filterVariableNames) {
|
||||
return baseVariableList.filter(variable => {
|
||||
const nodeTypeMatch = !config.filterNodeTypes ||
|
||||
(Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type));
|
||||
const variableNameMatch = !config.filterVariableNames ||
|
||||
(Array.isArray(config.filterVariableNames) && config.filterVariableNames.includes(variable.label));
|
||||
return nodeTypeMatch || variableNameMatch;
|
||||
});
|
||||
}
|
||||
// Filter child nodes for iteration output
|
||||
if (config.filterChildNodes && selectedNode) {
|
||||
const graph = graphRef.current;
|
||||
if (!graph) return [];
|
||||
|
||||
const nodes = graph.getNodes();
|
||||
|
||||
// Find child nodes whose cycle field equals parent node's ID
|
||||
const childNodes = nodes.filter(node => {
|
||||
const nodeData = node.getData();
|
||||
return nodeData?.cycle === selectedNode.id;
|
||||
});
|
||||
|
||||
return baseVariableList.filter(variable =>
|
||||
childNodes.some(node => node.id === variable.nodeData?.id) || selectedNode?.data?.type === 'iteration' && key === 'output' && variable.value.includes('sys.')
|
||||
);
|
||||
}
|
||||
return baseVariableList;
|
||||
})()
|
||||
}
|
||||
size="small"
|
||||
/>
|
||||
: config.type === 'switch'
|
||||
? <Switch onChange={key === 'group' ? () => { form.setFieldValue('group_variables', []) } : undefined} />
|
||||
: config.type === 'categoryList'
|
||||
? <CategoryList
|
||||
parentName={key}
|
||||
selectedNode={selectedNode}
|
||||
graphRef={graphRef}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
/>
|
||||
: config.type === 'editor'
|
||||
? <Editor options={variableList} variant="outlined" size="small" />
|
||||
: null
|
||||
}
|
||||
</Form.Item>
|
||||
@@ -1383,11 +1312,6 @@ const Properties: FC<PropertiesProps> = ({
|
||||
}
|
||||
</Form>
|
||||
}
|
||||
|
||||
<VariableEditModal
|
||||
ref={variableModalRef}
|
||||
refresh={handleRefreshVariable}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
.properties :global(.ant-input-outlined),
|
||||
.properties :global(.ant-input-number-outlined),
|
||||
.properties :global(.ant-select-outlined:not(.ant-select-customize-input) .ant-select-selector),
|
||||
.properties :global(.ant-input-number-outlined),
|
||||
.properties :global(.ant-input-number-outlined .ant-input-number-handler-wrap) {
|
||||
background-color: transparent;
|
||||
}
|
||||
.properties :global(.ant-input-outlined.ant-input-disabled),
|
||||
.properties :global(.ant-input-outlined[disabled]) {
|
||||
background-color: #F6F8FC;
|
||||
}
|
||||
.properties :global(.ant-select-single.ant-select-sm){
|
||||
height: 28px;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table-thead>tr>th),
|
||||
.properties :global(.ant-table-wrapper .ant-table-thead>tr>td),
|
||||
.properties :global(.ant-table-wrapper .ant-table) {
|
||||
background-color: #F6F8FC;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table),
|
||||
.properties :global(.ant-table-container),
|
||||
.properties :global(.ant-table-wrapper table) {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table-container table>thead>tr:first-child>*:first-child) {
|
||||
border-start-start-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table-container table>thead>tr:first-child>*:last-child) {
|
||||
border-start-end-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-row:last-child .ant-table-cell:first-child) {
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-row:last-child .ant-table-cell:last-child) {
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table) {
|
||||
background: transparent;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table-container) {
|
||||
border-start-start-radius: 6px;
|
||||
border-start-end-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-container) {
|
||||
/* border-left: none;
|
||||
border-top: none;
|
||||
border-bottom: none; */
|
||||
border: none;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table-tbody>tr.ant-table-placeholder:hover>th),
|
||||
.properties :global(.ant-table-wrapper .ant-table-tbody>tr.ant-table-placeholder:hover>td),
|
||||
.properties :global(.ant-table-wrapper .ant-table-tbody>tr.ant-table-placeholder),
|
||||
.properties :global(.ant-table-wrapper .ant-table) {
|
||||
background-color: #F6F8FC;
|
||||
}
|
||||
.properties :global(.ant-form-item-horizontal.ant-form-item .ant-form-item-control-input-content:has(> .ant-switch:only-child, > .ant-rate:only-child)) {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
.properties :global(.ant-divider-horizontal.ant-divider-sm) {
|
||||
margin-block: 16px;
|
||||
}
|
||||
.properties :global(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.properties :global(.ant-form-item .ant-form-item-label>label) {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
}
|
||||
.properties :global(.ant-select-single.ant-select-sm .ant-select-selector),
|
||||
.properties :global(.ant-select-dropdown .ant-select-item),.properties :global(.ant-input-number-sm) {
|
||||
font-size: 12px;
|
||||
}
|
||||
.properties :global(.ant-input-number-out-of-range .ant-input-number-input-wrap input) {
|
||||
color: #212332;
|
||||
}
|
||||
.properties :global(.ant-slider-horizontal .ant-slider-step) {
|
||||
height: 6px;
|
||||
}
|
||||
.properties :global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input) .ant-select-selector) {
|
||||
padding: 0 4px 0 6px ;
|
||||
}
|
||||
.properties :global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input).ant-select-show-arrow .ant-select-selection-item),
|
||||
.properties :global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input).ant-select-show-arrow .ant-select-selection-placeholder) {
|
||||
padding-right: 10px;
|
||||
}
|
||||
.properties :global(.ant-select .ant-select-arrow) {
|
||||
font-size: 10px;
|
||||
inset-inline-end: 6px;
|
||||
}
|
||||
@@ -90,7 +90,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
type: "end", icon: endIcon,
|
||||
config: {
|
||||
output: {
|
||||
type: 'define'
|
||||
type: 'editor'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -125,6 +125,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
},
|
||||
context: {
|
||||
type: 'variableList',
|
||||
placeholder: 'workflow.config.llm.contextPlaceholder'
|
||||
},
|
||||
messages: {
|
||||
type: 'define',
|
||||
@@ -134,7 +135,8 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
content: undefined,
|
||||
readonly: true
|
||||
},
|
||||
]
|
||||
],
|
||||
placeholder: 'workflow.config.llm.messagesPlaceholder'
|
||||
},
|
||||
memory: {
|
||||
type: 'memoryConfig',
|
||||
@@ -170,7 +172,8 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
},
|
||||
text: {
|
||||
type: 'variableList',
|
||||
filterLoopIterationVars: true
|
||||
filterLoopIterationVars: true,
|
||||
placeholder: 'workflow.config.parameter-extractor.textPlaceholder'
|
||||
},
|
||||
params: {
|
||||
type: 'paramList',
|
||||
@@ -178,6 +181,8 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
prompt: {
|
||||
type: 'messageEditor',
|
||||
isArray: false,
|
||||
titleVariant: 'borderless',
|
||||
placeholder: 'workflow.config.parameter-extractor.promptPlaceholder'
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -189,7 +194,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
{ type: "memory-read", icon: memoryReadIcon,
|
||||
config: {
|
||||
message: {
|
||||
type: 'messageEditor',
|
||||
type: 'editor',
|
||||
isArray: false
|
||||
},
|
||||
config_id: {
|
||||
@@ -212,7 +217,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
{ type: "memory-write", icon: memoryWriteIcon,
|
||||
config: {
|
||||
message: {
|
||||
type: 'messageEditor',
|
||||
type: 'editor',
|
||||
isArray: false
|
||||
},
|
||||
config_id: {
|
||||
@@ -270,7 +275,8 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
},
|
||||
user_supplement_prompt: {
|
||||
type: 'messageEditor',
|
||||
isArray: false
|
||||
isArray: false,
|
||||
titleVariant: 'borderless'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -436,6 +442,7 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
type: 'messageEditor',
|
||||
isArray: false,
|
||||
enableJinja2: true,
|
||||
titleVariant: 'borderless',
|
||||
defaultValue: "{{arg1}}"
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
|
||||
import { Graph } from '@antv/x6';
|
||||
import type { KnowledgeConfig } from './components/Properties/Knowledge/types'
|
||||
import type { Variable } from './components/Properties/VariableList/types'
|
||||
export interface NodeConfig {
|
||||
type: 'input' | 'textarea' | 'select' | 'inputNumber' | 'slider' | 'customSelect' | 'define' | 'knowledge' | 'variableList' | string;
|
||||
placeholder?: string;
|
||||
titleVariant?: 'outlined' | 'borderless';
|
||||
options?: { label: string; value: string }[];
|
||||
|
||||
max?: number;
|
||||
@@ -14,7 +17,7 @@ export interface NodeConfig {
|
||||
valueKey?: string;
|
||||
labelKey?: string;
|
||||
|
||||
defaultValue?: any | StartVariableItem[];
|
||||
defaultValue?: any;
|
||||
|
||||
sys?: Array<{
|
||||
name: string;
|
||||
@@ -37,6 +40,7 @@ export interface NodeProperties {
|
||||
id?: string;
|
||||
config?: Record<string, NodeConfig>;
|
||||
hidden?: boolean;
|
||||
cycle?: string;
|
||||
}
|
||||
|
||||
export interface NodeLibrary {
|
||||
@@ -87,27 +91,12 @@ export interface WorkflowConfig {
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export interface VariableEditModalRef {
|
||||
handleOpen: (values?: StartVariableItem) => void;
|
||||
}
|
||||
export interface StartVariableItem {
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
description: string;
|
||||
max_length?: number;
|
||||
default?: string;
|
||||
readonly?: boolean;
|
||||
defaultValue?: any;
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export interface ChatRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
export type GraphRef = React.MutableRefObject<Graph | undefined>
|
||||
export interface VariableConfigModalRef {
|
||||
handleOpen: (values: StartVariableItem[]) => void;
|
||||
handleOpen: (values: Variable[]) => void;
|
||||
}
|
||||
|
||||
export interface ChatVariable {
|
||||
|
||||
Reference in New Issue
Block a user