Feat: conversation variable & variable assigner node (#7222)
Signed-off-by: -LAN- <laipz8200@outlook.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: -LAN- <laipz8200@outlook.com>
This commit is contained in:
parent
8b55bd5828
commit
935e72d449
128 changed files with 3354 additions and 683 deletions
|
|
@ -12,19 +12,14 @@ from configs.packaging import PackagingInfo
|
||||||
class DifyConfig(
|
class DifyConfig(
|
||||||
# Packaging info
|
# Packaging info
|
||||||
PackagingInfo,
|
PackagingInfo,
|
||||||
|
|
||||||
# Deployment configs
|
# Deployment configs
|
||||||
DeploymentConfig,
|
DeploymentConfig,
|
||||||
|
|
||||||
# Feature configs
|
# Feature configs
|
||||||
FeatureConfig,
|
FeatureConfig,
|
||||||
|
|
||||||
# Middleware configs
|
# Middleware configs
|
||||||
MiddlewareConfig,
|
MiddlewareConfig,
|
||||||
|
|
||||||
# Extra service configs
|
# Extra service configs
|
||||||
ExtraServiceConfig,
|
ExtraServiceConfig,
|
||||||
|
|
||||||
# Enterprise feature configs
|
# Enterprise feature configs
|
||||||
# **Before using, please contact business@dify.ai by email to inquire about licensing matters.**
|
# **Before using, please contact business@dify.ai by email to inquire about licensing matters.**
|
||||||
EnterpriseFeatureConfig,
|
EnterpriseFeatureConfig,
|
||||||
|
|
@ -36,7 +31,6 @@ class DifyConfig(
|
||||||
env_file='.env',
|
env_file='.env',
|
||||||
env_file_encoding='utf-8',
|
env_file_encoding='utf-8',
|
||||||
frozen=True,
|
frozen=True,
|
||||||
|
|
||||||
# ignore extra attributes
|
# ignore extra attributes
|
||||||
extra='ignore',
|
extra='ignore',
|
||||||
)
|
)
|
||||||
|
|
@ -67,3 +61,5 @@ class DifyConfig(
|
||||||
SSRF_PROXY_HTTPS_URL: str | None = None
|
SSRF_PROXY_HTTPS_URL: str | None = None
|
||||||
|
|
||||||
MODERATION_BUFFER_SIZE: int = Field(default=300, description='The buffer size for moderation.')
|
MODERATION_BUFFER_SIZE: int = Field(default=300, description='The buffer size for moderation.')
|
||||||
|
|
||||||
|
MAX_VARIABLE_SIZE: int = Field(default=5 * 1024, description='The maximum size of a variable. default is 5KB.')
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ from .app import (
|
||||||
audio,
|
audio,
|
||||||
completion,
|
completion,
|
||||||
conversation,
|
conversation,
|
||||||
|
conversation_variables,
|
||||||
generator,
|
generator,
|
||||||
message,
|
message,
|
||||||
model_config,
|
model_config,
|
||||||
|
|
|
||||||
61
api/controllers/console/app/conversation_variables.py
Normal file
61
api/controllers/console/app/conversation_variables.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
from flask_restful import Resource, marshal_with, reqparse
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from controllers.console import api
|
||||||
|
from controllers.console.app.wraps import get_app_model
|
||||||
|
from controllers.console.setup import setup_required
|
||||||
|
from controllers.console.wraps import account_initialization_required
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from fields.conversation_variable_fields import paginated_conversation_variable_fields
|
||||||
|
from libs.login import login_required
|
||||||
|
from models import ConversationVariable
|
||||||
|
from models.model import AppMode
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationVariablesApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@get_app_model(mode=AppMode.ADVANCED_CHAT)
|
||||||
|
@marshal_with(paginated_conversation_variable_fields)
|
||||||
|
def get(self, app_model):
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument('conversation_id', type=str, location='args')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
stmt = (
|
||||||
|
select(ConversationVariable)
|
||||||
|
.where(ConversationVariable.app_id == app_model.id)
|
||||||
|
.order_by(ConversationVariable.created_at)
|
||||||
|
)
|
||||||
|
if args['conversation_id']:
|
||||||
|
stmt = stmt.where(ConversationVariable.conversation_id == args['conversation_id'])
|
||||||
|
else:
|
||||||
|
raise ValueError('conversation_id is required')
|
||||||
|
|
||||||
|
# NOTE: This is a temporary solution to avoid performance issues.
|
||||||
|
page = 1
|
||||||
|
page_size = 100
|
||||||
|
stmt = stmt.limit(page_size).offset((page - 1) * page_size)
|
||||||
|
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
rows = session.scalars(stmt).all()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'page': page,
|
||||||
|
'limit': page_size,
|
||||||
|
'total': len(rows),
|
||||||
|
'has_more': False,
|
||||||
|
'data': [
|
||||||
|
{
|
||||||
|
'created_at': row.created_at,
|
||||||
|
'updated_at': row.updated_at,
|
||||||
|
**row.to_variable().model_dump(),
|
||||||
|
}
|
||||||
|
for row in rows
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(ConversationVariablesApi, '/apps/<uuid:app_id>/conversation-variables')
|
||||||
|
|
@ -74,6 +74,7 @@ class DraftWorkflowApi(Resource):
|
||||||
parser.add_argument('hash', type=str, required=False, location='json')
|
parser.add_argument('hash', type=str, required=False, location='json')
|
||||||
# TODO: set this to required=True after frontend is updated
|
# TODO: set this to required=True after frontend is updated
|
||||||
parser.add_argument('environment_variables', type=list, required=False, location='json')
|
parser.add_argument('environment_variables', type=list, required=False, location='json')
|
||||||
|
parser.add_argument('conversation_variables', type=list, required=False, location='json')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
elif 'text/plain' in content_type:
|
elif 'text/plain' in content_type:
|
||||||
try:
|
try:
|
||||||
|
|
@ -88,7 +89,8 @@ class DraftWorkflowApi(Resource):
|
||||||
'graph': data.get('graph'),
|
'graph': data.get('graph'),
|
||||||
'features': data.get('features'),
|
'features': data.get('features'),
|
||||||
'hash': data.get('hash'),
|
'hash': data.get('hash'),
|
||||||
'environment_variables': data.get('environment_variables')
|
'environment_variables': data.get('environment_variables'),
|
||||||
|
'conversation_variables': data.get('conversation_variables'),
|
||||||
}
|
}
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return {'message': 'Invalid JSON data'}, 400
|
return {'message': 'Invalid JSON data'}, 400
|
||||||
|
|
@ -100,6 +102,8 @@ class DraftWorkflowApi(Resource):
|
||||||
try:
|
try:
|
||||||
environment_variables_list = args.get('environment_variables') or []
|
environment_variables_list = args.get('environment_variables') or []
|
||||||
environment_variables = [factory.build_variable_from_mapping(obj) for obj in environment_variables_list]
|
environment_variables = [factory.build_variable_from_mapping(obj) for obj in environment_variables_list]
|
||||||
|
conversation_variables_list = args.get('conversation_variables') or []
|
||||||
|
conversation_variables = [factory.build_variable_from_mapping(obj) for obj in conversation_variables_list]
|
||||||
workflow = workflow_service.sync_draft_workflow(
|
workflow = workflow_service.sync_draft_workflow(
|
||||||
app_model=app_model,
|
app_model=app_model,
|
||||||
graph=args['graph'],
|
graph=args['graph'],
|
||||||
|
|
@ -107,6 +111,7 @@ class DraftWorkflowApi(Resource):
|
||||||
unique_hash=args.get('hash'),
|
unique_hash=args.get('hash'),
|
||||||
account=current_user,
|
account=current_user,
|
||||||
environment_variables=environment_variables,
|
environment_variables=environment_variables,
|
||||||
|
conversation_variables=conversation_variables,
|
||||||
)
|
)
|
||||||
except WorkflowHashNotEqualError:
|
except WorkflowHashNotEqualError:
|
||||||
raise DraftWorkflowNotSync()
|
raise DraftWorkflowNotSync()
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,9 @@ from typing import Any, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from core.file.file_obj import FileExtraConfig
|
||||||
from core.model_runtime.entities.message_entities import PromptMessageRole
|
from core.model_runtime.entities.message_entities import PromptMessageRole
|
||||||
from models.model import AppMode
|
from models import AppMode
|
||||||
|
|
||||||
|
|
||||||
class ModelConfigEntity(BaseModel):
|
class ModelConfigEntity(BaseModel):
|
||||||
|
|
@ -200,11 +201,6 @@ class TracingConfigEntity(BaseModel):
|
||||||
tracing_provider: str
|
tracing_provider: str
|
||||||
|
|
||||||
|
|
||||||
class FileExtraConfig(BaseModel):
|
|
||||||
"""
|
|
||||||
File Upload Entity.
|
|
||||||
"""
|
|
||||||
image_config: Optional[dict[str, Any]] = None
|
|
||||||
|
|
||||||
|
|
||||||
class AppAdditionalFeatures(BaseModel):
|
class AppAdditionalFeatures(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from core.app.app_config.entities import FileExtraConfig
|
from core.file.file_obj import FileExtraConfig
|
||||||
|
|
||||||
|
|
||||||
class FileUploadConfigManager:
|
class FileUploadConfigManager:
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,6 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
|
||||||
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
|
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
|
||||||
|
|
||||||
return self._generate(
|
return self._generate(
|
||||||
app_model=app_model,
|
|
||||||
workflow=workflow,
|
workflow=workflow,
|
||||||
user=user,
|
user=user,
|
||||||
invoke_from=invoke_from,
|
invoke_from=invoke_from,
|
||||||
|
|
@ -180,7 +179,6 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
|
||||||
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
|
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
|
||||||
|
|
||||||
return self._generate(
|
return self._generate(
|
||||||
app_model=app_model,
|
|
||||||
workflow=workflow,
|
workflow=workflow,
|
||||||
user=user,
|
user=user,
|
||||||
invoke_from=InvokeFrom.DEBUGGER,
|
invoke_from=InvokeFrom.DEBUGGER,
|
||||||
|
|
@ -189,12 +187,12 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
|
||||||
stream=stream
|
stream=stream
|
||||||
)
|
)
|
||||||
|
|
||||||
def _generate(self, app_model: App,
|
def _generate(self, *,
|
||||||
workflow: Workflow,
|
workflow: Workflow,
|
||||||
user: Union[Account, EndUser],
|
user: Union[Account, EndUser],
|
||||||
invoke_from: InvokeFrom,
|
invoke_from: InvokeFrom,
|
||||||
application_generate_entity: AdvancedChatAppGenerateEntity,
|
application_generate_entity: AdvancedChatAppGenerateEntity,
|
||||||
conversation: Conversation = None,
|
conversation: Conversation | None = None,
|
||||||
stream: bool = True) \
|
stream: bool = True) \
|
||||||
-> Union[dict, Generator[dict, None, None]]:
|
-> Union[dict, Generator[dict, None, None]]:
|
||||||
is_first_conversation = False
|
is_first_conversation = False
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import time
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from typing import Any, Optional, cast
|
from typing import Any, Optional, cast
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfig
|
from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfig
|
||||||
from core.app.apps.advanced_chat.workflow_event_trigger_callback import WorkflowEventTriggerCallback
|
from core.app.apps.advanced_chat.workflow_event_trigger_callback import WorkflowEventTriggerCallback
|
||||||
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
|
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
|
||||||
|
|
@ -17,11 +20,12 @@ from core.app.entities.queue_entities import QueueAnnotationReplyEvent, QueueSto
|
||||||
from core.moderation.base import ModerationException
|
from core.moderation.base import ModerationException
|
||||||
from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
|
from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
|
||||||
from core.workflow.entities.node_entities import SystemVariable
|
from core.workflow.entities.node_entities import SystemVariable
|
||||||
|
from core.workflow.entities.variable_pool import VariablePool
|
||||||
from core.workflow.nodes.base_node import UserFrom
|
from core.workflow.nodes.base_node import UserFrom
|
||||||
from core.workflow.workflow_engine_manager import WorkflowEngineManager
|
from core.workflow.workflow_engine_manager import WorkflowEngineManager
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from models.model import App, Conversation, EndUser, Message
|
from models.model import App, Conversation, EndUser, Message
|
||||||
from models.workflow import Workflow
|
from models.workflow import ConversationVariable, Workflow
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -31,10 +35,13 @@ class AdvancedChatAppRunner(AppRunner):
|
||||||
AdvancedChat Application Runner
|
AdvancedChat Application Runner
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def run(self, application_generate_entity: AdvancedChatAppGenerateEntity,
|
def run(
|
||||||
queue_manager: AppQueueManager,
|
self,
|
||||||
conversation: Conversation,
|
application_generate_entity: AdvancedChatAppGenerateEntity,
|
||||||
message: Message) -> None:
|
queue_manager: AppQueueManager,
|
||||||
|
conversation: Conversation,
|
||||||
|
message: Message,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Run application
|
Run application
|
||||||
:param application_generate_entity: application generate entity
|
:param application_generate_entity: application generate entity
|
||||||
|
|
@ -48,11 +55,11 @@ class AdvancedChatAppRunner(AppRunner):
|
||||||
|
|
||||||
app_record = db.session.query(App).filter(App.id == app_config.app_id).first()
|
app_record = db.session.query(App).filter(App.id == app_config.app_id).first()
|
||||||
if not app_record:
|
if not app_record:
|
||||||
raise ValueError("App not found")
|
raise ValueError('App not found')
|
||||||
|
|
||||||
workflow = self.get_workflow(app_model=app_record, workflow_id=app_config.workflow_id)
|
workflow = self.get_workflow(app_model=app_record, workflow_id=app_config.workflow_id)
|
||||||
if not workflow:
|
if not workflow:
|
||||||
raise ValueError("Workflow not initialized")
|
raise ValueError('Workflow not initialized')
|
||||||
|
|
||||||
inputs = application_generate_entity.inputs
|
inputs = application_generate_entity.inputs
|
||||||
query = application_generate_entity.query
|
query = application_generate_entity.query
|
||||||
|
|
@ -68,35 +75,66 @@ class AdvancedChatAppRunner(AppRunner):
|
||||||
|
|
||||||
# moderation
|
# moderation
|
||||||
if self.handle_input_moderation(
|
if self.handle_input_moderation(
|
||||||
queue_manager=queue_manager,
|
queue_manager=queue_manager,
|
||||||
app_record=app_record,
|
app_record=app_record,
|
||||||
app_generate_entity=application_generate_entity,
|
app_generate_entity=application_generate_entity,
|
||||||
inputs=inputs,
|
inputs=inputs,
|
||||||
query=query,
|
query=query,
|
||||||
message_id=message.id
|
message_id=message.id,
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
# annotation reply
|
# annotation reply
|
||||||
if self.handle_annotation_reply(
|
if self.handle_annotation_reply(
|
||||||
app_record=app_record,
|
app_record=app_record,
|
||||||
message=message,
|
message=message,
|
||||||
query=query,
|
query=query,
|
||||||
queue_manager=queue_manager,
|
queue_manager=queue_manager,
|
||||||
app_generate_entity=application_generate_entity
|
app_generate_entity=application_generate_entity,
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
workflow_callbacks: list[WorkflowCallback] = [WorkflowEventTriggerCallback(
|
workflow_callbacks: list[WorkflowCallback] = [
|
||||||
queue_manager=queue_manager,
|
WorkflowEventTriggerCallback(queue_manager=queue_manager, workflow=workflow)
|
||||||
workflow=workflow
|
]
|
||||||
)]
|
|
||||||
|
|
||||||
if bool(os.environ.get("DEBUG", 'False').lower() == 'true'):
|
if bool(os.environ.get('DEBUG', 'False').lower() == 'true'):
|
||||||
workflow_callbacks.append(WorkflowLoggingCallback())
|
workflow_callbacks.append(WorkflowLoggingCallback())
|
||||||
|
|
||||||
|
# Init conversation variables
|
||||||
|
stmt = select(ConversationVariable).where(
|
||||||
|
ConversationVariable.app_id == conversation.app_id, ConversationVariable.conversation_id == conversation.id
|
||||||
|
)
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
conversation_variables = session.scalars(stmt).all()
|
||||||
|
if not conversation_variables:
|
||||||
|
conversation_variables = [
|
||||||
|
ConversationVariable.from_variable(
|
||||||
|
app_id=conversation.app_id, conversation_id=conversation.id, variable=variable
|
||||||
|
)
|
||||||
|
for variable in workflow.conversation_variables
|
||||||
|
]
|
||||||
|
session.add_all(conversation_variables)
|
||||||
|
session.commit()
|
||||||
|
# Convert database entities to variables
|
||||||
|
conversation_variables = [item.to_variable() for item in conversation_variables]
|
||||||
|
|
||||||
|
# Create a variable pool.
|
||||||
|
system_inputs = {
|
||||||
|
SystemVariable.QUERY: query,
|
||||||
|
SystemVariable.FILES: files,
|
||||||
|
SystemVariable.CONVERSATION_ID: conversation.id,
|
||||||
|
SystemVariable.USER_ID: user_id,
|
||||||
|
}
|
||||||
|
variable_pool = VariablePool(
|
||||||
|
system_variables=system_inputs,
|
||||||
|
user_inputs=inputs,
|
||||||
|
environment_variables=workflow.environment_variables,
|
||||||
|
conversation_variables=conversation_variables,
|
||||||
|
)
|
||||||
|
|
||||||
# RUN WORKFLOW
|
# RUN WORKFLOW
|
||||||
workflow_engine_manager = WorkflowEngineManager()
|
workflow_engine_manager = WorkflowEngineManager()
|
||||||
workflow_engine_manager.run_workflow(
|
workflow_engine_manager.run_workflow(
|
||||||
|
|
@ -106,43 +144,30 @@ class AdvancedChatAppRunner(AppRunner):
|
||||||
if application_generate_entity.invoke_from in [InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER]
|
if application_generate_entity.invoke_from in [InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER]
|
||||||
else UserFrom.END_USER,
|
else UserFrom.END_USER,
|
||||||
invoke_from=application_generate_entity.invoke_from,
|
invoke_from=application_generate_entity.invoke_from,
|
||||||
user_inputs=inputs,
|
|
||||||
system_inputs={
|
|
||||||
SystemVariable.QUERY: query,
|
|
||||||
SystemVariable.FILES: files,
|
|
||||||
SystemVariable.CONVERSATION_ID: conversation.id,
|
|
||||||
SystemVariable.USER_ID: user_id
|
|
||||||
},
|
|
||||||
callbacks=workflow_callbacks,
|
callbacks=workflow_callbacks,
|
||||||
call_depth=application_generate_entity.call_depth
|
call_depth=application_generate_entity.call_depth,
|
||||||
|
variable_pool=variable_pool,
|
||||||
)
|
)
|
||||||
|
|
||||||
def single_iteration_run(self, app_id: str, workflow_id: str,
|
def single_iteration_run(
|
||||||
queue_manager: AppQueueManager,
|
self, app_id: str, workflow_id: str, queue_manager: AppQueueManager, inputs: dict, node_id: str, user_id: str
|
||||||
inputs: dict, node_id: str, user_id: str) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Single iteration run
|
Single iteration run
|
||||||
"""
|
"""
|
||||||
app_record: App = db.session.query(App).filter(App.id == app_id).first()
|
app_record: App = db.session.query(App).filter(App.id == app_id).first()
|
||||||
if not app_record:
|
if not app_record:
|
||||||
raise ValueError("App not found")
|
raise ValueError('App not found')
|
||||||
|
|
||||||
workflow = self.get_workflow(app_model=app_record, workflow_id=workflow_id)
|
workflow = self.get_workflow(app_model=app_record, workflow_id=workflow_id)
|
||||||
if not workflow:
|
if not workflow:
|
||||||
raise ValueError("Workflow not initialized")
|
raise ValueError('Workflow not initialized')
|
||||||
|
|
||||||
workflow_callbacks = [WorkflowEventTriggerCallback(
|
workflow_callbacks = [WorkflowEventTriggerCallback(queue_manager=queue_manager, workflow=workflow)]
|
||||||
queue_manager=queue_manager,
|
|
||||||
workflow=workflow
|
|
||||||
)]
|
|
||||||
|
|
||||||
workflow_engine_manager = WorkflowEngineManager()
|
workflow_engine_manager = WorkflowEngineManager()
|
||||||
workflow_engine_manager.single_step_run_iteration_workflow_node(
|
workflow_engine_manager.single_step_run_iteration_workflow_node(
|
||||||
workflow=workflow,
|
workflow=workflow, node_id=node_id, user_id=user_id, user_inputs=inputs, callbacks=workflow_callbacks
|
||||||
node_id=node_id,
|
|
||||||
user_id=user_id,
|
|
||||||
user_inputs=inputs,
|
|
||||||
callbacks=workflow_callbacks
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_workflow(self, app_model: App, workflow_id: str) -> Optional[Workflow]:
|
def get_workflow(self, app_model: App, workflow_id: str) -> Optional[Workflow]:
|
||||||
|
|
@ -150,22 +175,25 @@ class AdvancedChatAppRunner(AppRunner):
|
||||||
Get workflow
|
Get workflow
|
||||||
"""
|
"""
|
||||||
# fetch workflow by workflow_id
|
# fetch workflow by workflow_id
|
||||||
workflow = db.session.query(Workflow).filter(
|
workflow = (
|
||||||
Workflow.tenant_id == app_model.tenant_id,
|
db.session.query(Workflow)
|
||||||
Workflow.app_id == app_model.id,
|
.filter(
|
||||||
Workflow.id == workflow_id
|
Workflow.tenant_id == app_model.tenant_id, Workflow.app_id == app_model.id, Workflow.id == workflow_id
|
||||||
).first()
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
# return workflow
|
# return workflow
|
||||||
return workflow
|
return workflow
|
||||||
|
|
||||||
def handle_input_moderation(
|
def handle_input_moderation(
|
||||||
self, queue_manager: AppQueueManager,
|
self,
|
||||||
app_record: App,
|
queue_manager: AppQueueManager,
|
||||||
app_generate_entity: AdvancedChatAppGenerateEntity,
|
app_record: App,
|
||||||
inputs: Mapping[str, Any],
|
app_generate_entity: AdvancedChatAppGenerateEntity,
|
||||||
query: str,
|
inputs: Mapping[str, Any],
|
||||||
message_id: str
|
query: str,
|
||||||
|
message_id: str,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Handle input moderation
|
Handle input moderation
|
||||||
|
|
@ -192,17 +220,20 @@ class AdvancedChatAppRunner(AppRunner):
|
||||||
queue_manager=queue_manager,
|
queue_manager=queue_manager,
|
||||||
text=str(e),
|
text=str(e),
|
||||||
stream=app_generate_entity.stream,
|
stream=app_generate_entity.stream,
|
||||||
stopped_by=QueueStopEvent.StopBy.INPUT_MODERATION
|
stopped_by=QueueStopEvent.StopBy.INPUT_MODERATION,
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def handle_annotation_reply(self, app_record: App,
|
def handle_annotation_reply(
|
||||||
message: Message,
|
self,
|
||||||
query: str,
|
app_record: App,
|
||||||
queue_manager: AppQueueManager,
|
message: Message,
|
||||||
app_generate_entity: AdvancedChatAppGenerateEntity) -> bool:
|
query: str,
|
||||||
|
queue_manager: AppQueueManager,
|
||||||
|
app_generate_entity: AdvancedChatAppGenerateEntity,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Handle annotation reply
|
Handle annotation reply
|
||||||
:param app_record: app record
|
:param app_record: app record
|
||||||
|
|
@ -217,29 +248,27 @@ class AdvancedChatAppRunner(AppRunner):
|
||||||
message=message,
|
message=message,
|
||||||
query=query,
|
query=query,
|
||||||
user_id=app_generate_entity.user_id,
|
user_id=app_generate_entity.user_id,
|
||||||
invoke_from=app_generate_entity.invoke_from
|
invoke_from=app_generate_entity.invoke_from,
|
||||||
)
|
)
|
||||||
|
|
||||||
if annotation_reply:
|
if annotation_reply:
|
||||||
queue_manager.publish(
|
queue_manager.publish(
|
||||||
QueueAnnotationReplyEvent(message_annotation_id=annotation_reply.id),
|
QueueAnnotationReplyEvent(message_annotation_id=annotation_reply.id), PublishFrom.APPLICATION_MANAGER
|
||||||
PublishFrom.APPLICATION_MANAGER
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._stream_output(
|
self._stream_output(
|
||||||
queue_manager=queue_manager,
|
queue_manager=queue_manager,
|
||||||
text=annotation_reply.content,
|
text=annotation_reply.content,
|
||||||
stream=app_generate_entity.stream,
|
stream=app_generate_entity.stream,
|
||||||
stopped_by=QueueStopEvent.StopBy.ANNOTATION_REPLY
|
stopped_by=QueueStopEvent.StopBy.ANNOTATION_REPLY,
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _stream_output(self, queue_manager: AppQueueManager,
|
def _stream_output(
|
||||||
text: str,
|
self, queue_manager: AppQueueManager, text: str, stream: bool, stopped_by: QueueStopEvent.StopBy
|
||||||
stream: bool,
|
) -> None:
|
||||||
stopped_by: QueueStopEvent.StopBy) -> None:
|
|
||||||
"""
|
"""
|
||||||
Direct output
|
Direct output
|
||||||
:param queue_manager: application queue manager
|
:param queue_manager: application queue manager
|
||||||
|
|
@ -250,21 +279,10 @@ class AdvancedChatAppRunner(AppRunner):
|
||||||
if stream:
|
if stream:
|
||||||
index = 0
|
index = 0
|
||||||
for token in text:
|
for token in text:
|
||||||
queue_manager.publish(
|
queue_manager.publish(QueueTextChunkEvent(text=token), PublishFrom.APPLICATION_MANAGER)
|
||||||
QueueTextChunkEvent(
|
|
||||||
text=token
|
|
||||||
), PublishFrom.APPLICATION_MANAGER
|
|
||||||
)
|
|
||||||
index += 1
|
index += 1
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
else:
|
else:
|
||||||
queue_manager.publish(
|
queue_manager.publish(QueueTextChunkEvent(text=text), PublishFrom.APPLICATION_MANAGER)
|
||||||
QueueTextChunkEvent(
|
|
||||||
text=text
|
|
||||||
), PublishFrom.APPLICATION_MANAGER
|
|
||||||
)
|
|
||||||
|
|
||||||
queue_manager.publish(
|
queue_manager.publish(QueueStopEvent(stopped_by=stopped_by), PublishFrom.APPLICATION_MANAGER)
|
||||||
QueueStopEvent(stopped_by=stopped_by),
|
|
||||||
PublishFrom.APPLICATION_MANAGER
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from core.app.entities.app_invoke_entities import (
|
||||||
)
|
)
|
||||||
from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
|
from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
|
||||||
from core.workflow.entities.node_entities import SystemVariable
|
from core.workflow.entities.node_entities import SystemVariable
|
||||||
|
from core.workflow.entities.variable_pool import VariablePool
|
||||||
from core.workflow.nodes.base_node import UserFrom
|
from core.workflow.nodes.base_node import UserFrom
|
||||||
from core.workflow.workflow_engine_manager import WorkflowEngineManager
|
from core.workflow.workflow_engine_manager import WorkflowEngineManager
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
|
|
@ -26,8 +27,7 @@ class WorkflowAppRunner:
|
||||||
Workflow Application Runner
|
Workflow Application Runner
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def run(self, application_generate_entity: WorkflowAppGenerateEntity,
|
def run(self, application_generate_entity: WorkflowAppGenerateEntity, queue_manager: AppQueueManager) -> None:
|
||||||
queue_manager: AppQueueManager) -> None:
|
|
||||||
"""
|
"""
|
||||||
Run application
|
Run application
|
||||||
:param application_generate_entity: application generate entity
|
:param application_generate_entity: application generate entity
|
||||||
|
|
@ -47,25 +47,36 @@ class WorkflowAppRunner:
|
||||||
|
|
||||||
app_record = db.session.query(App).filter(App.id == app_config.app_id).first()
|
app_record = db.session.query(App).filter(App.id == app_config.app_id).first()
|
||||||
if not app_record:
|
if not app_record:
|
||||||
raise ValueError("App not found")
|
raise ValueError('App not found')
|
||||||
|
|
||||||
workflow = self.get_workflow(app_model=app_record, workflow_id=app_config.workflow_id)
|
workflow = self.get_workflow(app_model=app_record, workflow_id=app_config.workflow_id)
|
||||||
if not workflow:
|
if not workflow:
|
||||||
raise ValueError("Workflow not initialized")
|
raise ValueError('Workflow not initialized')
|
||||||
|
|
||||||
inputs = application_generate_entity.inputs
|
inputs = application_generate_entity.inputs
|
||||||
files = application_generate_entity.files
|
files = application_generate_entity.files
|
||||||
|
|
||||||
db.session.close()
|
db.session.close()
|
||||||
|
|
||||||
workflow_callbacks: list[WorkflowCallback] = [WorkflowEventTriggerCallback(
|
workflow_callbacks: list[WorkflowCallback] = [
|
||||||
queue_manager=queue_manager,
|
WorkflowEventTriggerCallback(queue_manager=queue_manager, workflow=workflow)
|
||||||
workflow=workflow
|
]
|
||||||
)]
|
|
||||||
|
|
||||||
if bool(os.environ.get("DEBUG", 'False').lower() == 'true'):
|
if bool(os.environ.get('DEBUG', 'False').lower() == 'true'):
|
||||||
workflow_callbacks.append(WorkflowLoggingCallback())
|
workflow_callbacks.append(WorkflowLoggingCallback())
|
||||||
|
|
||||||
|
# Create a variable pool.
|
||||||
|
system_inputs = {
|
||||||
|
SystemVariable.FILES: files,
|
||||||
|
SystemVariable.USER_ID: user_id,
|
||||||
|
}
|
||||||
|
variable_pool = VariablePool(
|
||||||
|
system_variables=system_inputs,
|
||||||
|
user_inputs=inputs,
|
||||||
|
environment_variables=workflow.environment_variables,
|
||||||
|
conversation_variables=[],
|
||||||
|
)
|
||||||
|
|
||||||
# RUN WORKFLOW
|
# RUN WORKFLOW
|
||||||
workflow_engine_manager = WorkflowEngineManager()
|
workflow_engine_manager = WorkflowEngineManager()
|
||||||
workflow_engine_manager.run_workflow(
|
workflow_engine_manager.run_workflow(
|
||||||
|
|
@ -75,44 +86,33 @@ class WorkflowAppRunner:
|
||||||
if application_generate_entity.invoke_from in [InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER]
|
if application_generate_entity.invoke_from in [InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER]
|
||||||
else UserFrom.END_USER,
|
else UserFrom.END_USER,
|
||||||
invoke_from=application_generate_entity.invoke_from,
|
invoke_from=application_generate_entity.invoke_from,
|
||||||
user_inputs=inputs,
|
|
||||||
system_inputs={
|
|
||||||
SystemVariable.FILES: files,
|
|
||||||
SystemVariable.USER_ID: user_id
|
|
||||||
},
|
|
||||||
callbacks=workflow_callbacks,
|
callbacks=workflow_callbacks,
|
||||||
call_depth=application_generate_entity.call_depth
|
call_depth=application_generate_entity.call_depth,
|
||||||
|
variable_pool=variable_pool,
|
||||||
)
|
)
|
||||||
|
|
||||||
def single_iteration_run(self, app_id: str, workflow_id: str,
|
def single_iteration_run(
|
||||||
queue_manager: AppQueueManager,
|
self, app_id: str, workflow_id: str, queue_manager: AppQueueManager, inputs: dict, node_id: str, user_id: str
|
||||||
inputs: dict, node_id: str, user_id: str) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Single iteration run
|
Single iteration run
|
||||||
"""
|
"""
|
||||||
app_record: App = db.session.query(App).filter(App.id == app_id).first()
|
app_record = db.session.query(App).filter(App.id == app_id).first()
|
||||||
if not app_record:
|
if not app_record:
|
||||||
raise ValueError("App not found")
|
raise ValueError('App not found')
|
||||||
|
|
||||||
if not app_record.workflow_id:
|
if not app_record.workflow_id:
|
||||||
raise ValueError("Workflow not initialized")
|
raise ValueError('Workflow not initialized')
|
||||||
|
|
||||||
workflow = self.get_workflow(app_model=app_record, workflow_id=workflow_id)
|
workflow = self.get_workflow(app_model=app_record, workflow_id=workflow_id)
|
||||||
if not workflow:
|
if not workflow:
|
||||||
raise ValueError("Workflow not initialized")
|
raise ValueError('Workflow not initialized')
|
||||||
|
|
||||||
workflow_callbacks = [WorkflowEventTriggerCallback(
|
workflow_callbacks = [WorkflowEventTriggerCallback(queue_manager=queue_manager, workflow=workflow)]
|
||||||
queue_manager=queue_manager,
|
|
||||||
workflow=workflow
|
|
||||||
)]
|
|
||||||
|
|
||||||
workflow_engine_manager = WorkflowEngineManager()
|
workflow_engine_manager = WorkflowEngineManager()
|
||||||
workflow_engine_manager.single_step_run_iteration_workflow_node(
|
workflow_engine_manager.single_step_run_iteration_workflow_node(
|
||||||
workflow=workflow,
|
workflow=workflow, node_id=node_id, user_id=user_id, user_inputs=inputs, callbacks=workflow_callbacks
|
||||||
node_id=node_id,
|
|
||||||
user_id=user_id,
|
|
||||||
user_inputs=inputs,
|
|
||||||
callbacks=workflow_callbacks
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_workflow(self, app_model: App, workflow_id: str) -> Optional[Workflow]:
|
def get_workflow(self, app_model: App, workflow_id: str) -> Optional[Workflow]:
|
||||||
|
|
@ -120,11 +120,13 @@ class WorkflowAppRunner:
|
||||||
Get workflow
|
Get workflow
|
||||||
"""
|
"""
|
||||||
# fetch workflow by workflow_id
|
# fetch workflow by workflow_id
|
||||||
workflow = db.session.query(Workflow).filter(
|
workflow = (
|
||||||
Workflow.tenant_id == app_model.tenant_id,
|
db.session.query(Workflow)
|
||||||
Workflow.app_id == app_model.id,
|
.filter(
|
||||||
Workflow.id == workflow_id
|
Workflow.tenant_id == app_model.tenant_id, Workflow.app_id == app_model.id, Workflow.id == workflow_id
|
||||||
).first()
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
# return workflow
|
# return workflow
|
||||||
return workflow
|
return workflow
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from .segment_group import SegmentGroup
|
from .segment_group import SegmentGroup
|
||||||
from .segments import (
|
from .segments import (
|
||||||
ArrayAnySegment,
|
ArrayAnySegment,
|
||||||
|
ArraySegment,
|
||||||
FileSegment,
|
FileSegment,
|
||||||
FloatSegment,
|
FloatSegment,
|
||||||
IntegerSegment,
|
IntegerSegment,
|
||||||
|
|
@ -50,4 +51,5 @@ __all__ = [
|
||||||
'ArrayNumberVariable',
|
'ArrayNumberVariable',
|
||||||
'ArrayObjectVariable',
|
'ArrayObjectVariable',
|
||||||
'ArrayFileVariable',
|
'ArrayFileVariable',
|
||||||
|
'ArraySegment',
|
||||||
]
|
]
|
||||||
|
|
|
||||||
2
api/core/app/segments/exc.py
Normal file
2
api/core/app/segments/exc.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
class VariableError(Exception):
|
||||||
|
pass
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from configs import dify_config
|
||||||
from core.file.file_obj import FileVar
|
from core.file.file_obj import FileVar
|
||||||
|
|
||||||
|
from .exc import VariableError
|
||||||
from .segments import (
|
from .segments import (
|
||||||
ArrayAnySegment,
|
ArrayAnySegment,
|
||||||
FileSegment,
|
FileSegment,
|
||||||
|
|
@ -29,39 +31,43 @@ from .variables import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_variable_from_mapping(m: Mapping[str, Any], /) -> Variable:
|
def build_variable_from_mapping(mapping: Mapping[str, Any], /) -> Variable:
|
||||||
if (value_type := m.get('value_type')) is None:
|
if (value_type := mapping.get('value_type')) is None:
|
||||||
raise ValueError('missing value type')
|
raise VariableError('missing value type')
|
||||||
if not m.get('name'):
|
if not mapping.get('name'):
|
||||||
raise ValueError('missing name')
|
raise VariableError('missing name')
|
||||||
if (value := m.get('value')) is None:
|
if (value := mapping.get('value')) is None:
|
||||||
raise ValueError('missing value')
|
raise VariableError('missing value')
|
||||||
match value_type:
|
match value_type:
|
||||||
case SegmentType.STRING:
|
case SegmentType.STRING:
|
||||||
return StringVariable.model_validate(m)
|
result = StringVariable.model_validate(mapping)
|
||||||
case SegmentType.SECRET:
|
case SegmentType.SECRET:
|
||||||
return SecretVariable.model_validate(m)
|
result = SecretVariable.model_validate(mapping)
|
||||||
case SegmentType.NUMBER if isinstance(value, int):
|
case SegmentType.NUMBER if isinstance(value, int):
|
||||||
return IntegerVariable.model_validate(m)
|
result = IntegerVariable.model_validate(mapping)
|
||||||
case SegmentType.NUMBER if isinstance(value, float):
|
case SegmentType.NUMBER if isinstance(value, float):
|
||||||
return FloatVariable.model_validate(m)
|
result = FloatVariable.model_validate(mapping)
|
||||||
case SegmentType.NUMBER if not isinstance(value, float | int):
|
case SegmentType.NUMBER if not isinstance(value, float | int):
|
||||||
raise ValueError(f'invalid number value {value}')
|
raise VariableError(f'invalid number value {value}')
|
||||||
case SegmentType.FILE:
|
case SegmentType.FILE:
|
||||||
return FileVariable.model_validate(m)
|
result = FileVariable.model_validate(mapping)
|
||||||
case SegmentType.OBJECT if isinstance(value, dict):
|
case SegmentType.OBJECT if isinstance(value, dict):
|
||||||
return ObjectVariable.model_validate(
|
result = ObjectVariable.model_validate(mapping)
|
||||||
{**m, 'value': {k: build_variable_from_mapping(v) for k, v in value.items()}}
|
|
||||||
)
|
|
||||||
case SegmentType.ARRAY_STRING if isinstance(value, list):
|
case SegmentType.ARRAY_STRING if isinstance(value, list):
|
||||||
return ArrayStringVariable.model_validate({**m, 'value': [build_variable_from_mapping(v) for v in value]})
|
result = ArrayStringVariable.model_validate(mapping)
|
||||||
case SegmentType.ARRAY_NUMBER if isinstance(value, list):
|
case SegmentType.ARRAY_NUMBER if isinstance(value, list):
|
||||||
return ArrayNumberVariable.model_validate({**m, 'value': [build_variable_from_mapping(v) for v in value]})
|
result = ArrayNumberVariable.model_validate(mapping)
|
||||||
case SegmentType.ARRAY_OBJECT if isinstance(value, list):
|
case SegmentType.ARRAY_OBJECT if isinstance(value, list):
|
||||||
return ArrayObjectVariable.model_validate({**m, 'value': [build_variable_from_mapping(v) for v in value]})
|
result = ArrayObjectVariable.model_validate(mapping)
|
||||||
case SegmentType.ARRAY_FILE if isinstance(value, list):
|
case SegmentType.ARRAY_FILE if isinstance(value, list):
|
||||||
return ArrayFileVariable.model_validate({**m, 'value': [build_variable_from_mapping(v) for v in value]})
|
mapping = dict(mapping)
|
||||||
raise ValueError(f'not supported value type {value_type}')
|
mapping['value'] = [{'value': v} for v in value]
|
||||||
|
result = ArrayFileVariable.model_validate(mapping)
|
||||||
|
case _:
|
||||||
|
raise VariableError(f'not supported value type {value_type}')
|
||||||
|
if result.size > dify_config.MAX_VARIABLE_SIZE:
|
||||||
|
raise VariableError(f'variable size {result.size} exceeds limit {dify_config.MAX_VARIABLE_SIZE}')
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def build_segment(value: Any, /) -> Segment:
|
def build_segment(value: Any, /) -> Segment:
|
||||||
|
|
@ -74,12 +80,9 @@ def build_segment(value: Any, /) -> Segment:
|
||||||
if isinstance(value, float):
|
if isinstance(value, float):
|
||||||
return FloatSegment(value=value)
|
return FloatSegment(value=value)
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
# TODO: Limit the depth of the object
|
|
||||||
return ObjectSegment(value=value)
|
return ObjectSegment(value=value)
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
# TODO: Limit the depth of the array
|
return ArrayAnySegment(value=value)
|
||||||
elements = [build_segment(v) for v in value]
|
|
||||||
return ArrayAnySegment(value=elements)
|
|
||||||
if isinstance(value, FileVar):
|
if isinstance(value, FileVar):
|
||||||
return FileSegment(value=value)
|
return FileSegment(value=value)
|
||||||
raise ValueError(f'not supported value {value}')
|
raise ValueError(f'not supported value {value}')
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import json
|
import json
|
||||||
|
import sys
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
@ -37,6 +38,10 @@ class Segment(BaseModel):
|
||||||
def markdown(self) -> str:
|
def markdown(self) -> str:
|
||||||
return str(self.value)
|
return str(self.value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self) -> int:
|
||||||
|
return sys.getsizeof(self.value)
|
||||||
|
|
||||||
def to_object(self) -> Any:
|
def to_object(self) -> Any:
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
|
@ -105,28 +110,25 @@ class ArraySegment(Segment):
|
||||||
def markdown(self) -> str:
|
def markdown(self) -> str:
|
||||||
return '\n'.join(['- ' + item.markdown for item in self.value])
|
return '\n'.join(['- ' + item.markdown for item in self.value])
|
||||||
|
|
||||||
def to_object(self):
|
|
||||||
return [v.to_object() for v in self.value]
|
|
||||||
|
|
||||||
|
|
||||||
class ArrayAnySegment(ArraySegment):
|
class ArrayAnySegment(ArraySegment):
|
||||||
value_type: SegmentType = SegmentType.ARRAY_ANY
|
value_type: SegmentType = SegmentType.ARRAY_ANY
|
||||||
value: Sequence[Segment]
|
value: Sequence[Any]
|
||||||
|
|
||||||
|
|
||||||
class ArrayStringSegment(ArraySegment):
|
class ArrayStringSegment(ArraySegment):
|
||||||
value_type: SegmentType = SegmentType.ARRAY_STRING
|
value_type: SegmentType = SegmentType.ARRAY_STRING
|
||||||
value: Sequence[StringSegment]
|
value: Sequence[str]
|
||||||
|
|
||||||
|
|
||||||
class ArrayNumberSegment(ArraySegment):
|
class ArrayNumberSegment(ArraySegment):
|
||||||
value_type: SegmentType = SegmentType.ARRAY_NUMBER
|
value_type: SegmentType = SegmentType.ARRAY_NUMBER
|
||||||
value: Sequence[FloatSegment | IntegerSegment]
|
value: Sequence[float | int]
|
||||||
|
|
||||||
|
|
||||||
class ArrayObjectSegment(ArraySegment):
|
class ArrayObjectSegment(ArraySegment):
|
||||||
value_type: SegmentType = SegmentType.ARRAY_OBJECT
|
value_type: SegmentType = SegmentType.ARRAY_OBJECT
|
||||||
value: Sequence[ObjectSegment]
|
value: Sequence[Mapping[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
class ArrayFileSegment(ArraySegment):
|
class ArrayFileSegment(ArraySegment):
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
import enum
|
import enum
|
||||||
from typing import Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from core.app.app_config.entities import FileExtraConfig
|
|
||||||
from core.file.tool_file_parser import ToolFileParser
|
from core.file.tool_file_parser import ToolFileParser
|
||||||
from core.file.upload_file_parser import UploadFileParser
|
from core.file.upload_file_parser import UploadFileParser
|
||||||
from core.model_runtime.entities.message_entities import ImagePromptMessageContent
|
from core.model_runtime.entities.message_entities import ImagePromptMessageContent
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from models.model import UploadFile
|
|
||||||
|
|
||||||
|
class FileExtraConfig(BaseModel):
|
||||||
|
"""
|
||||||
|
File Upload Entity.
|
||||||
|
"""
|
||||||
|
image_config: Optional[dict[str, Any]] = None
|
||||||
|
|
||||||
|
|
||||||
class FileType(enum.Enum):
|
class FileType(enum.Enum):
|
||||||
|
|
@ -114,6 +119,7 @@ class FileVar(BaseModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_data(self, force_url: bool = False) -> Optional[str]:
|
def _get_data(self, force_url: bool = False) -> Optional[str]:
|
||||||
|
from models.model import UploadFile
|
||||||
if self.type == FileType.IMAGE:
|
if self.type == FileType.IMAGE:
|
||||||
if self.transfer_method == FileTransferMethod.REMOTE_URL:
|
if self.transfer_method == FileTransferMethod.REMOTE_URL:
|
||||||
return self.url
|
return self.url
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@ from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from core.app.app_config.entities import FileExtraConfig
|
from core.file.file_obj import FileBelongsTo, FileExtraConfig, FileTransferMethod, FileType, FileVar
|
||||||
from core.file.file_obj import FileBelongsTo, FileTransferMethod, FileType, FileVar
|
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
from models.model import EndUser, MessageFile, UploadFile
|
from models.model import EndUser, MessageFile, UploadFile
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import base64
|
||||||
|
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs import rsa
|
from libs import rsa
|
||||||
from models.account import Tenant
|
|
||||||
|
|
||||||
|
|
||||||
def obfuscated_token(token: str):
|
def obfuscated_token(token: str):
|
||||||
|
|
@ -14,6 +13,7 @@ def obfuscated_token(token: str):
|
||||||
|
|
||||||
|
|
||||||
def encrypt_token(tenant_id: str, token: str):
|
def encrypt_token(tenant_id: str, token: str):
|
||||||
|
from models.account import Tenant
|
||||||
if not (tenant := db.session.query(Tenant).filter(Tenant.id == tenant_id).first()):
|
if not (tenant := db.session.query(Tenant).filter(Tenant.id == tenant_id).first()):
|
||||||
raise ValueError(f'Tenant with id {tenant_id} not found')
|
raise ValueError(f'Tenant with id {tenant_id} not found')
|
||||||
encrypted_token = rsa.encrypt(token, tenant.encrypt_public_key)
|
encrypted_token = rsa.encrypt(token, tenant.encrypt_public_key)
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,12 @@ class NodeType(Enum):
|
||||||
HTTP_REQUEST = 'http-request'
|
HTTP_REQUEST = 'http-request'
|
||||||
TOOL = 'tool'
|
TOOL = 'tool'
|
||||||
VARIABLE_AGGREGATOR = 'variable-aggregator'
|
VARIABLE_AGGREGATOR = 'variable-aggregator'
|
||||||
|
# TODO: merge this into VARIABLE_AGGREGATOR
|
||||||
VARIABLE_ASSIGNER = 'variable-assigner'
|
VARIABLE_ASSIGNER = 'variable-assigner'
|
||||||
LOOP = 'loop'
|
LOOP = 'loop'
|
||||||
ITERATION = 'iteration'
|
ITERATION = 'iteration'
|
||||||
PARAMETER_EXTRACTOR = 'parameter-extractor'
|
PARAMETER_EXTRACTOR = 'parameter-extractor'
|
||||||
|
CONVERSATION_VARIABLE_ASSIGNER = 'assigner'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def value_of(cls, value: str) -> 'NodeType':
|
def value_of(cls, value: str) -> 'NodeType':
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ VariableValue = Union[str, int, float, dict, list, FileVar]
|
||||||
|
|
||||||
SYSTEM_VARIABLE_NODE_ID = 'sys'
|
SYSTEM_VARIABLE_NODE_ID = 'sys'
|
||||||
ENVIRONMENT_VARIABLE_NODE_ID = 'env'
|
ENVIRONMENT_VARIABLE_NODE_ID = 'env'
|
||||||
|
CONVERSATION_VARIABLE_NODE_ID = 'conversation'
|
||||||
|
|
||||||
|
|
||||||
class VariablePool:
|
class VariablePool:
|
||||||
|
|
@ -21,6 +22,7 @@ class VariablePool:
|
||||||
system_variables: Mapping[SystemVariable, Any],
|
system_variables: Mapping[SystemVariable, Any],
|
||||||
user_inputs: Mapping[str, Any],
|
user_inputs: Mapping[str, Any],
|
||||||
environment_variables: Sequence[Variable],
|
environment_variables: Sequence[Variable],
|
||||||
|
conversation_variables: Sequence[Variable] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# system variables
|
# system variables
|
||||||
# for example:
|
# for example:
|
||||||
|
|
@ -44,9 +46,13 @@ class VariablePool:
|
||||||
self.add((SYSTEM_VARIABLE_NODE_ID, key.value), value)
|
self.add((SYSTEM_VARIABLE_NODE_ID, key.value), value)
|
||||||
|
|
||||||
# Add environment variables to the variable pool
|
# Add environment variables to the variable pool
|
||||||
for var in environment_variables or []:
|
for var in environment_variables:
|
||||||
self.add((ENVIRONMENT_VARIABLE_NODE_ID, var.name), var)
|
self.add((ENVIRONMENT_VARIABLE_NODE_ID, var.name), var)
|
||||||
|
|
||||||
|
# Add conversation variables to the variable pool
|
||||||
|
for var in conversation_variables or []:
|
||||||
|
self.add((CONVERSATION_VARIABLE_NODE_ID, var.name), var)
|
||||||
|
|
||||||
def add(self, selector: Sequence[str], value: Any, /) -> None:
|
def add(self, selector: Sequence[str], value: Any, /) -> None:
|
||||||
"""
|
"""
|
||||||
Adds a variable to the variable pool.
|
Adds a variable to the variable pool.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
|
||||||
from core.workflow.entities.base_node_data_entities import BaseIterationState, BaseNodeData
|
from core.workflow.entities.base_node_data_entities import BaseIterationState, BaseNodeData
|
||||||
from core.workflow.entities.node_entities import NodeRunResult, NodeType
|
from core.workflow.entities.node_entities import NodeRunResult, NodeType
|
||||||
from core.workflow.entities.variable_pool import VariablePool
|
from core.workflow.entities.variable_pool import VariablePool
|
||||||
|
from models import WorkflowNodeExecutionStatus
|
||||||
|
|
||||||
|
|
||||||
class UserFrom(Enum):
|
class UserFrom(Enum):
|
||||||
|
|
@ -91,14 +92,19 @@ class BaseNode(ABC):
|
||||||
:param variable_pool: variable pool
|
:param variable_pool: variable pool
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
result = self._run(
|
try:
|
||||||
variable_pool=variable_pool
|
result = self._run(
|
||||||
)
|
variable_pool=variable_pool
|
||||||
|
)
|
||||||
|
self.node_run_result = result
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
return NodeRunResult(
|
||||||
|
status=WorkflowNodeExecutionStatus.FAILED,
|
||||||
|
error=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
self.node_run_result = result
|
def publish_text_chunk(self, text: str, value_selector: list[str] | None = None) -> None:
|
||||||
return result
|
|
||||||
|
|
||||||
def publish_text_chunk(self, text: str, value_selector: list[str] = None) -> None:
|
|
||||||
"""
|
"""
|
||||||
Publish text chunk
|
Publish text chunk
|
||||||
:param text: chunk text
|
:param text: chunk text
|
||||||
|
|
|
||||||
109
api/core/workflow/nodes/variable_assigner/__init__.py
Normal file
109
api/core/workflow/nodes/variable_assigner/__init__.py
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Optional, cast
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from core.app.segments import SegmentType, Variable, factory
|
||||||
|
from core.workflow.entities.base_node_data_entities import BaseNodeData
|
||||||
|
from core.workflow.entities.node_entities import NodeRunResult, NodeType
|
||||||
|
from core.workflow.entities.variable_pool import VariablePool
|
||||||
|
from core.workflow.nodes.base_node import BaseNode
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from models import ConversationVariable, WorkflowNodeExecutionStatus
|
||||||
|
|
||||||
|
|
||||||
|
class VariableAssignerNodeError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WriteMode(str, Enum):
|
||||||
|
OVER_WRITE = 'over-write'
|
||||||
|
APPEND = 'append'
|
||||||
|
CLEAR = 'clear'
|
||||||
|
|
||||||
|
|
||||||
|
class VariableAssignerData(BaseNodeData):
|
||||||
|
title: str = 'Variable Assigner'
|
||||||
|
desc: Optional[str] = 'Assign a value to a variable'
|
||||||
|
assigned_variable_selector: Sequence[str]
|
||||||
|
write_mode: WriteMode
|
||||||
|
input_variable_selector: Sequence[str]
|
||||||
|
|
||||||
|
|
||||||
|
class VariableAssignerNode(BaseNode):
|
||||||
|
_node_data_cls: type[BaseNodeData] = VariableAssignerData
|
||||||
|
_node_type: NodeType = NodeType.CONVERSATION_VARIABLE_ASSIGNER
|
||||||
|
|
||||||
|
def _run(self, variable_pool: VariablePool) -> NodeRunResult:
|
||||||
|
data = cast(VariableAssignerData, self.node_data)
|
||||||
|
|
||||||
|
# Should be String, Number, Object, ArrayString, ArrayNumber, ArrayObject
|
||||||
|
original_variable = variable_pool.get(data.assigned_variable_selector)
|
||||||
|
if not isinstance(original_variable, Variable):
|
||||||
|
raise VariableAssignerNodeError('assigned variable not found')
|
||||||
|
|
||||||
|
match data.write_mode:
|
||||||
|
case WriteMode.OVER_WRITE:
|
||||||
|
income_value = variable_pool.get(data.input_variable_selector)
|
||||||
|
if not income_value:
|
||||||
|
raise VariableAssignerNodeError('input value not found')
|
||||||
|
updated_variable = original_variable.model_copy(update={'value': income_value.value})
|
||||||
|
|
||||||
|
case WriteMode.APPEND:
|
||||||
|
income_value = variable_pool.get(data.input_variable_selector)
|
||||||
|
if not income_value:
|
||||||
|
raise VariableAssignerNodeError('input value not found')
|
||||||
|
updated_value = original_variable.value + [income_value.value]
|
||||||
|
updated_variable = original_variable.model_copy(update={'value': updated_value})
|
||||||
|
|
||||||
|
case WriteMode.CLEAR:
|
||||||
|
income_value = get_zero_value(original_variable.value_type)
|
||||||
|
updated_variable = original_variable.model_copy(update={'value': income_value.to_object()})
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise VariableAssignerNodeError(f'unsupported write mode: {data.write_mode}')
|
||||||
|
|
||||||
|
# Over write the variable.
|
||||||
|
variable_pool.add(data.assigned_variable_selector, updated_variable)
|
||||||
|
|
||||||
|
# Update conversation variable.
|
||||||
|
# TODO: Find a better way to use the database.
|
||||||
|
conversation_id = variable_pool.get(['sys', 'conversation_id'])
|
||||||
|
if not conversation_id:
|
||||||
|
raise VariableAssignerNodeError('conversation_id not found')
|
||||||
|
update_conversation_variable(conversation_id=conversation_id.text, variable=updated_variable)
|
||||||
|
|
||||||
|
return NodeRunResult(
|
||||||
|
status=WorkflowNodeExecutionStatus.SUCCEEDED,
|
||||||
|
inputs={
|
||||||
|
'value': income_value.to_object(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_conversation_variable(conversation_id: str, variable: Variable):
|
||||||
|
stmt = select(ConversationVariable).where(
|
||||||
|
ConversationVariable.id == variable.id, ConversationVariable.conversation_id == conversation_id
|
||||||
|
)
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
row = session.scalar(stmt)
|
||||||
|
if not row:
|
||||||
|
raise VariableAssignerNodeError('conversation variable not found in the database')
|
||||||
|
row.data = variable.model_dump_json()
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def get_zero_value(t: SegmentType):
|
||||||
|
match t:
|
||||||
|
case SegmentType.ARRAY_OBJECT | SegmentType.ARRAY_STRING | SegmentType.ARRAY_NUMBER:
|
||||||
|
return factory.build_segment([])
|
||||||
|
case SegmentType.OBJECT:
|
||||||
|
return factory.build_segment({})
|
||||||
|
case SegmentType.STRING:
|
||||||
|
return factory.build_segment('')
|
||||||
|
case SegmentType.NUMBER:
|
||||||
|
return factory.build_segment(0)
|
||||||
|
case _:
|
||||||
|
raise VariableAssignerNodeError(f'unsupported variable type: {t}')
|
||||||
|
|
@ -4,12 +4,11 @@ from collections.abc import Mapping, Sequence
|
||||||
from typing import Any, Optional, cast
|
from typing import Any, Optional, cast
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from core.app.app_config.entities import FileExtraConfig
|
|
||||||
from core.app.apps.base_app_queue_manager import GenerateTaskStoppedException
|
from core.app.apps.base_app_queue_manager import GenerateTaskStoppedException
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
from core.file.file_obj import FileTransferMethod, FileType, FileVar
|
from core.file.file_obj import FileExtraConfig, FileTransferMethod, FileType, FileVar
|
||||||
from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
|
from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
|
||||||
from core.workflow.entities.node_entities import NodeRunMetadataKey, NodeRunResult, NodeType, SystemVariable
|
from core.workflow.entities.node_entities import NodeRunMetadataKey, NodeRunResult, NodeType
|
||||||
from core.workflow.entities.variable_pool import VariablePool, VariableValue
|
from core.workflow.entities.variable_pool import VariablePool, VariableValue
|
||||||
from core.workflow.entities.workflow_entities import WorkflowNodeAndResult, WorkflowRunState
|
from core.workflow.entities.workflow_entities import WorkflowNodeAndResult, WorkflowRunState
|
||||||
from core.workflow.errors import WorkflowNodeRunFailedError
|
from core.workflow.errors import WorkflowNodeRunFailedError
|
||||||
|
|
@ -30,6 +29,7 @@ from core.workflow.nodes.start.start_node import StartNode
|
||||||
from core.workflow.nodes.template_transform.template_transform_node import TemplateTransformNode
|
from core.workflow.nodes.template_transform.template_transform_node import TemplateTransformNode
|
||||||
from core.workflow.nodes.tool.tool_node import ToolNode
|
from core.workflow.nodes.tool.tool_node import ToolNode
|
||||||
from core.workflow.nodes.variable_aggregator.variable_aggregator_node import VariableAggregatorNode
|
from core.workflow.nodes.variable_aggregator.variable_aggregator_node import VariableAggregatorNode
|
||||||
|
from core.workflow.nodes.variable_assigner import VariableAssignerNode
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from models.workflow import (
|
from models.workflow import (
|
||||||
Workflow,
|
Workflow,
|
||||||
|
|
@ -51,7 +51,8 @@ node_classes: Mapping[NodeType, type[BaseNode]] = {
|
||||||
NodeType.VARIABLE_AGGREGATOR: VariableAggregatorNode,
|
NodeType.VARIABLE_AGGREGATOR: VariableAggregatorNode,
|
||||||
NodeType.VARIABLE_ASSIGNER: VariableAggregatorNode,
|
NodeType.VARIABLE_ASSIGNER: VariableAggregatorNode,
|
||||||
NodeType.ITERATION: IterationNode,
|
NodeType.ITERATION: IterationNode,
|
||||||
NodeType.PARAMETER_EXTRACTOR: ParameterExtractorNode
|
NodeType.PARAMETER_EXTRACTOR: ParameterExtractorNode,
|
||||||
|
NodeType.CONVERSATION_VARIABLE_ASSIGNER: VariableAssignerNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -94,10 +95,9 @@ class WorkflowEngineManager:
|
||||||
user_id: str,
|
user_id: str,
|
||||||
user_from: UserFrom,
|
user_from: UserFrom,
|
||||||
invoke_from: InvokeFrom,
|
invoke_from: InvokeFrom,
|
||||||
user_inputs: Mapping[str, Any],
|
|
||||||
system_inputs: Mapping[SystemVariable, Any],
|
|
||||||
callbacks: Sequence[WorkflowCallback],
|
callbacks: Sequence[WorkflowCallback],
|
||||||
call_depth: int = 0
|
call_depth: int = 0,
|
||||||
|
variable_pool: VariablePool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
:param workflow: Workflow instance
|
:param workflow: Workflow instance
|
||||||
|
|
@ -122,12 +122,6 @@ class WorkflowEngineManager:
|
||||||
if not isinstance(graph.get('edges'), list):
|
if not isinstance(graph.get('edges'), list):
|
||||||
raise ValueError('edges in workflow graph must be a list')
|
raise ValueError('edges in workflow graph must be a list')
|
||||||
|
|
||||||
# init variable pool
|
|
||||||
variable_pool = VariablePool(
|
|
||||||
system_variables=system_inputs,
|
|
||||||
user_inputs=user_inputs,
|
|
||||||
environment_variables=workflow.environment_variables,
|
|
||||||
)
|
|
||||||
|
|
||||||
workflow_call_max_depth = dify_config.WORKFLOW_CALL_MAX_DEPTH
|
workflow_call_max_depth = dify_config.WORKFLOW_CALL_MAX_DEPTH
|
||||||
if call_depth > workflow_call_max_depth:
|
if call_depth > workflow_call_max_depth:
|
||||||
|
|
@ -403,6 +397,7 @@ class WorkflowEngineManager:
|
||||||
system_variables={},
|
system_variables={},
|
||||||
user_inputs={},
|
user_inputs={},
|
||||||
environment_variables=workflow.environment_variables,
|
environment_variables=workflow.environment_variables,
|
||||||
|
conversation_variables=workflow.conversation_variables,
|
||||||
)
|
)
|
||||||
|
|
||||||
if node_cls is None:
|
if node_cls is None:
|
||||||
|
|
@ -468,6 +463,7 @@ class WorkflowEngineManager:
|
||||||
system_variables={},
|
system_variables={},
|
||||||
user_inputs={},
|
user_inputs={},
|
||||||
environment_variables=workflow.environment_variables,
|
environment_variables=workflow.environment_variables,
|
||||||
|
conversation_variables=workflow.conversation_variables,
|
||||||
)
|
)
|
||||||
|
|
||||||
# variable selector to variable mapping
|
# variable selector to variable mapping
|
||||||
|
|
|
||||||
21
api/fields/conversation_variable_fields.py
Normal file
21
api/fields/conversation_variable_fields.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
from flask_restful import fields
|
||||||
|
|
||||||
|
from libs.helper import TimestampField
|
||||||
|
|
||||||
|
conversation_variable_fields = {
|
||||||
|
'id': fields.String,
|
||||||
|
'name': fields.String,
|
||||||
|
'value_type': fields.String(attribute='value_type.value'),
|
||||||
|
'value': fields.String,
|
||||||
|
'description': fields.String,
|
||||||
|
'created_at': TimestampField,
|
||||||
|
'updated_at': TimestampField,
|
||||||
|
}
|
||||||
|
|
||||||
|
paginated_conversation_variable_fields = {
|
||||||
|
'page': fields.Integer,
|
||||||
|
'limit': fields.Integer,
|
||||||
|
'total': fields.Integer,
|
||||||
|
'has_more': fields.Boolean,
|
||||||
|
'data': fields.List(fields.Nested(conversation_variable_fields), attribute='data'),
|
||||||
|
}
|
||||||
|
|
@ -32,11 +32,12 @@ class EnvironmentVariableField(fields.Raw):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
environment_variable_fields = {
|
conversation_variable_fields = {
|
||||||
'id': fields.String,
|
'id': fields.String,
|
||||||
'name': fields.String,
|
'name': fields.String,
|
||||||
'value': fields.Raw,
|
|
||||||
'value_type': fields.String(attribute='value_type.value'),
|
'value_type': fields.String(attribute='value_type.value'),
|
||||||
|
'value': fields.Raw,
|
||||||
|
'description': fields.String,
|
||||||
}
|
}
|
||||||
|
|
||||||
workflow_fields = {
|
workflow_fields = {
|
||||||
|
|
@ -50,4 +51,5 @@ workflow_fields = {
|
||||||
'updated_at': TimestampField,
|
'updated_at': TimestampField,
|
||||||
'tool_published': fields.Boolean,
|
'tool_published': fields.Boolean,
|
||||||
'environment_variables': fields.List(EnvironmentVariableField()),
|
'environment_variables': fields.List(EnvironmentVariableField()),
|
||||||
|
'conversation_variables': fields.List(fields.Nested(conversation_variable_fields)),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
"""support conversation variables
|
||||||
|
|
||||||
|
Revision ID: 63a83fcf12ba
|
||||||
|
Revises: 1787fbae959a
|
||||||
|
Create Date: 2024-08-13 06:33:07.950379
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
import models as models
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '63a83fcf12ba'
|
||||||
|
down_revision = '1787fbae959a'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('workflow__conversation_variables',
|
||||||
|
sa.Column('id', models.types.StringUUID(), nullable=False),
|
||||||
|
sa.Column('conversation_id', models.types.StringUUID(), nullable=False),
|
||||||
|
sa.Column('app_id', models.types.StringUUID(), nullable=False),
|
||||||
|
sa.Column('data', sa.Text(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id', 'conversation_id', name=op.f('workflow__conversation_variables_pkey'))
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('workflow__conversation_variables', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('workflow__conversation_variables_app_id_idx'), ['app_id'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('workflow__conversation_variables_created_at_idx'), ['created_at'], unique=False)
|
||||||
|
|
||||||
|
with op.batch_alter_table('workflows', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('conversation_variables', sa.Text(), server_default='{}', nullable=False))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('workflows', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('conversation_variables')
|
||||||
|
|
||||||
|
with op.batch_alter_table('workflow__conversation_variables', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('workflow__conversation_variables_created_at_idx'))
|
||||||
|
batch_op.drop_index(batch_op.f('workflow__conversation_variables_app_id_idx'))
|
||||||
|
|
||||||
|
op.drop_table('workflow__conversation_variables')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from sqlalchemy import CHAR, TypeDecorator
|
from .model import AppMode
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from .types import StringUUID
|
||||||
|
from .workflow import ConversationVariable, WorkflowNodeExecutionStatus
|
||||||
|
|
||||||
|
__all__ = ['ConversationVariable', 'StringUUID', 'AppMode', 'WorkflowNodeExecutionStatus']
|
||||||
|
|
||||||
|
|
||||||
class CreatedByRole(Enum):
|
class CreatedByRole(Enum):
|
||||||
"""
|
"""
|
||||||
Enum class for createdByRole
|
Enum class for createdByRole
|
||||||
"""
|
"""
|
||||||
ACCOUNT = "account"
|
|
||||||
END_USER = "end_user"
|
ACCOUNT = 'account'
|
||||||
|
END_USER = 'end_user'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def value_of(cls, value: str) -> 'CreatedByRole':
|
def value_of(cls, value: str) -> 'CreatedByRole':
|
||||||
|
|
@ -23,49 +27,3 @@ class CreatedByRole(Enum):
|
||||||
if role.value == value:
|
if role.value == value:
|
||||||
return role
|
return role
|
||||||
raise ValueError(f'invalid createdByRole value {value}')
|
raise ValueError(f'invalid createdByRole value {value}')
|
||||||
|
|
||||||
|
|
||||||
class CreatedFrom(Enum):
|
|
||||||
"""
|
|
||||||
Enum class for createdFrom
|
|
||||||
"""
|
|
||||||
SERVICE_API = "service-api"
|
|
||||||
WEB_APP = "web-app"
|
|
||||||
EXPLORE = "explore"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def value_of(cls, value: str) -> 'CreatedFrom':
|
|
||||||
"""
|
|
||||||
Get value of given mode.
|
|
||||||
|
|
||||||
:param value: mode value
|
|
||||||
:return: mode
|
|
||||||
"""
|
|
||||||
for role in cls:
|
|
||||||
if role.value == value:
|
|
||||||
return role
|
|
||||||
raise ValueError(f'invalid createdFrom value {value}')
|
|
||||||
|
|
||||||
|
|
||||||
class StringUUID(TypeDecorator):
|
|
||||||
impl = CHAR
|
|
||||||
cache_ok = True
|
|
||||||
|
|
||||||
def process_bind_param(self, value, dialect):
|
|
||||||
if value is None:
|
|
||||||
return value
|
|
||||||
elif dialect.name == 'postgresql':
|
|
||||||
return str(value)
|
|
||||||
else:
|
|
||||||
return value.hex
|
|
||||||
|
|
||||||
def load_dialect_impl(self, dialect):
|
|
||||||
if dialect.name == 'postgresql':
|
|
||||||
return dialect.type_descriptor(UUID())
|
|
||||||
else:
|
|
||||||
return dialect.type_descriptor(CHAR(36))
|
|
||||||
|
|
||||||
def process_result_value(self, value, dialect):
|
|
||||||
if value is None:
|
|
||||||
return value
|
|
||||||
return str(value)
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ import json
|
||||||
from flask_login import UserMixin
|
from flask_login import UserMixin
|
||||||
|
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from models import StringUUID
|
|
||||||
|
from .types import StringUUID
|
||||||
|
|
||||||
|
|
||||||
class AccountStatus(str, enum.Enum):
|
class AccountStatus(str, enum.Enum):
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import enum
|
import enum
|
||||||
|
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from models import StringUUID
|
|
||||||
|
from .types import StringUUID
|
||||||
|
|
||||||
|
|
||||||
class APIBasedExtensionPoint(enum.Enum):
|
class APIBasedExtensionPoint(enum.Enum):
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,10 @@ from configs import dify_config
|
||||||
from core.rag.retrieval.retrival_methods import RetrievalMethod
|
from core.rag.retrieval.retrival_methods import RetrievalMethod
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from extensions.ext_storage import storage
|
from extensions.ext_storage import storage
|
||||||
from models import StringUUID
|
|
||||||
from models.account import Account
|
from .account import Account
|
||||||
from models.model import App, Tag, TagBinding, UploadFile
|
from .model import App, Tag, TagBinding, UploadFile
|
||||||
|
from .types import StringUUID
|
||||||
|
|
||||||
|
|
||||||
class Dataset(db.Model):
|
class Dataset(db.Model):
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ from core.file.upload_file_parser import UploadFileParser
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs.helper import generate_string
|
from libs.helper import generate_string
|
||||||
|
|
||||||
from . import StringUUID
|
|
||||||
from .account import Account, Tenant
|
from .account import Account, Tenant
|
||||||
|
from .types import StringUUID
|
||||||
|
|
||||||
|
|
||||||
class DifySetup(db.Model):
|
class DifySetup(db.Model):
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from models import StringUUID
|
|
||||||
|
from .types import StringUUID
|
||||||
|
|
||||||
|
|
||||||
class ProviderType(Enum):
|
class ProviderType(Enum):
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import json
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from models import StringUUID
|
|
||||||
|
from .types import StringUUID
|
||||||
|
|
||||||
|
|
||||||
class DataSourceOauthBinding(db.Model):
|
class DataSourceOauthBinding(db.Model):
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ import json
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from models import StringUUID
|
|
||||||
|
from .types import StringUUID
|
||||||
|
|
||||||
|
|
||||||
class ToolProviderName(Enum):
|
class ToolProviderName(Enum):
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@ from core.tools.entities.common_entities import I18nObject
|
||||||
from core.tools.entities.tool_bundle import ApiToolBundle
|
from core.tools.entities.tool_bundle import ApiToolBundle
|
||||||
from core.tools.entities.tool_entities import ApiProviderSchemaType, WorkflowToolParameterConfiguration
|
from core.tools.entities.tool_entities import ApiProviderSchemaType, WorkflowToolParameterConfiguration
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from models import StringUUID
|
|
||||||
from models.model import Account, App, Tenant
|
from .model import Account, App, Tenant
|
||||||
|
from .types import StringUUID
|
||||||
|
|
||||||
|
|
||||||
class BuiltinToolProvider(db.Model):
|
class BuiltinToolProvider(db.Model):
|
||||||
|
|
|
||||||
26
api/models/types.py
Normal file
26
api/models/types.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
from sqlalchemy import CHAR, TypeDecorator
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
|
||||||
|
|
||||||
|
class StringUUID(TypeDecorator):
|
||||||
|
impl = CHAR
|
||||||
|
cache_ok = True
|
||||||
|
|
||||||
|
def process_bind_param(self, value, dialect):
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
elif dialect.name == 'postgresql':
|
||||||
|
return str(value)
|
||||||
|
else:
|
||||||
|
return value.hex
|
||||||
|
|
||||||
|
def load_dialect_impl(self, dialect):
|
||||||
|
if dialect.name == 'postgresql':
|
||||||
|
return dialect.type_descriptor(UUID())
|
||||||
|
else:
|
||||||
|
return dialect.type_descriptor(CHAR(36))
|
||||||
|
|
||||||
|
def process_result_value(self, value, dialect):
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
return str(value)
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
|
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from models import StringUUID
|
|
||||||
from models.model import Message
|
from .model import Message
|
||||||
|
from .types import StringUUID
|
||||||
|
|
||||||
|
|
||||||
class SavedMessage(db.Model):
|
class SavedMessage(db.Model):
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,18 @@ from collections.abc import Mapping, Sequence
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
|
from sqlalchemy import func
|
||||||
|
from sqlalchemy.orm import Mapped
|
||||||
|
|
||||||
import contexts
|
import contexts
|
||||||
from constants import HIDDEN_VALUE
|
from constants import HIDDEN_VALUE
|
||||||
from core.app.segments import (
|
from core.app.segments import SecretVariable, Variable, factory
|
||||||
SecretVariable,
|
|
||||||
Variable,
|
|
||||||
factory,
|
|
||||||
)
|
|
||||||
from core.helper import encrypter
|
from core.helper import encrypter
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs import helper
|
from libs import helper
|
||||||
from models import StringUUID
|
|
||||||
from models.account import Account
|
from .account import Account
|
||||||
|
from .types import StringUUID
|
||||||
|
|
||||||
|
|
||||||
class CreatedByRole(Enum):
|
class CreatedByRole(Enum):
|
||||||
|
|
@ -122,6 +122,7 @@ class Workflow(db.Model):
|
||||||
updated_by = db.Column(StringUUID)
|
updated_by = db.Column(StringUUID)
|
||||||
updated_at = db.Column(db.DateTime)
|
updated_at = db.Column(db.DateTime)
|
||||||
_environment_variables = db.Column('environment_variables', db.Text, nullable=False, server_default='{}')
|
_environment_variables = db.Column('environment_variables', db.Text, nullable=False, server_default='{}')
|
||||||
|
_conversation_variables = db.Column('conversation_variables', db.Text, nullable=False, server_default='{}')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created_by_account(self):
|
def created_by_account(self):
|
||||||
|
|
@ -249,9 +250,27 @@ class Workflow(db.Model):
|
||||||
'graph': self.graph_dict,
|
'graph': self.graph_dict,
|
||||||
'features': self.features_dict,
|
'features': self.features_dict,
|
||||||
'environment_variables': [var.model_dump(mode='json') for var in environment_variables],
|
'environment_variables': [var.model_dump(mode='json') for var in environment_variables],
|
||||||
|
'conversation_variables': [var.model_dump(mode='json') for var in self.conversation_variables],
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@property
|
||||||
|
def conversation_variables(self) -> Sequence[Variable]:
|
||||||
|
# TODO: find some way to init `self._conversation_variables` when instance created.
|
||||||
|
if self._conversation_variables is None:
|
||||||
|
self._conversation_variables = '{}'
|
||||||
|
|
||||||
|
variables_dict: dict[str, Any] = json.loads(self._conversation_variables)
|
||||||
|
results = [factory.build_variable_from_mapping(v) for v in variables_dict.values()]
|
||||||
|
return results
|
||||||
|
|
||||||
|
@conversation_variables.setter
|
||||||
|
def conversation_variables(self, value: Sequence[Variable]) -> None:
|
||||||
|
self._conversation_variables = json.dumps(
|
||||||
|
{var.name: var.model_dump() for var in value},
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WorkflowRunTriggeredFrom(Enum):
|
class WorkflowRunTriggeredFrom(Enum):
|
||||||
"""
|
"""
|
||||||
|
|
@ -702,3 +721,34 @@ class WorkflowAppLog(db.Model):
|
||||||
created_by_role = CreatedByRole.value_of(self.created_by_role)
|
created_by_role = CreatedByRole.value_of(self.created_by_role)
|
||||||
return db.session.get(EndUser, self.created_by) \
|
return db.session.get(EndUser, self.created_by) \
|
||||||
if created_by_role == CreatedByRole.END_USER else None
|
if created_by_role == CreatedByRole.END_USER else None
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationVariable(db.Model):
|
||||||
|
__tablename__ = 'workflow__conversation_variables'
|
||||||
|
|
||||||
|
id: Mapped[str] = db.Column(StringUUID, primary_key=True)
|
||||||
|
conversation_id: Mapped[str] = db.Column(StringUUID, nullable=False, primary_key=True)
|
||||||
|
app_id: Mapped[str] = db.Column(StringUUID, nullable=False, index=True)
|
||||||
|
data = db.Column(db.Text, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False, index=True, server_default=db.text('CURRENT_TIMESTAMP(0)'))
|
||||||
|
updated_at = db.Column(db.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
|
||||||
|
|
||||||
|
def __init__(self, *, id: str, app_id: str, conversation_id: str, data: str) -> None:
|
||||||
|
self.id = id
|
||||||
|
self.app_id = app_id
|
||||||
|
self.conversation_id = conversation_id
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_variable(cls, *, app_id: str, conversation_id: str, variable: Variable) -> 'ConversationVariable':
|
||||||
|
obj = cls(
|
||||||
|
id=variable.id,
|
||||||
|
app_id=app_id,
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
data=variable.model_dump_json(),
|
||||||
|
)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def to_variable(self) -> Variable:
|
||||||
|
mapping = json.loads(self.data)
|
||||||
|
return factory.build_variable_from_mapping(mapping)
|
||||||
|
|
|
||||||
|
|
@ -238,6 +238,8 @@ class AppDslService:
|
||||||
# init draft workflow
|
# init draft workflow
|
||||||
environment_variables_list = workflow_data.get('environment_variables') or []
|
environment_variables_list = workflow_data.get('environment_variables') or []
|
||||||
environment_variables = [factory.build_variable_from_mapping(obj) for obj in environment_variables_list]
|
environment_variables = [factory.build_variable_from_mapping(obj) for obj in environment_variables_list]
|
||||||
|
conversation_variables_list = workflow_data.get('conversation_variables') or []
|
||||||
|
conversation_variables = [factory.build_variable_from_mapping(obj) for obj in conversation_variables_list]
|
||||||
workflow_service = WorkflowService()
|
workflow_service = WorkflowService()
|
||||||
draft_workflow = workflow_service.sync_draft_workflow(
|
draft_workflow = workflow_service.sync_draft_workflow(
|
||||||
app_model=app,
|
app_model=app,
|
||||||
|
|
@ -246,6 +248,7 @@ class AppDslService:
|
||||||
unique_hash=None,
|
unique_hash=None,
|
||||||
account=account,
|
account=account,
|
||||||
environment_variables=environment_variables,
|
environment_variables=environment_variables,
|
||||||
|
conversation_variables=conversation_variables,
|
||||||
)
|
)
|
||||||
workflow_service.publish_workflow(
|
workflow_service.publish_workflow(
|
||||||
app_model=app,
|
app_model=app,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ from core.app.app_config.entities import (
|
||||||
DatasetRetrieveConfigEntity,
|
DatasetRetrieveConfigEntity,
|
||||||
EasyUIBasedAppConfig,
|
EasyUIBasedAppConfig,
|
||||||
ExternalDataVariableEntity,
|
ExternalDataVariableEntity,
|
||||||
FileExtraConfig,
|
|
||||||
ModelConfigEntity,
|
ModelConfigEntity,
|
||||||
PromptTemplateEntity,
|
PromptTemplateEntity,
|
||||||
VariableEntity,
|
VariableEntity,
|
||||||
|
|
@ -14,6 +13,7 @@ from core.app.app_config.entities import (
|
||||||
from core.app.apps.agent_chat.app_config_manager import AgentChatAppConfigManager
|
from core.app.apps.agent_chat.app_config_manager import AgentChatAppConfigManager
|
||||||
from core.app.apps.chat.app_config_manager import ChatAppConfigManager
|
from core.app.apps.chat.app_config_manager import ChatAppConfigManager
|
||||||
from core.app.apps.completion.app_config_manager import CompletionAppConfigManager
|
from core.app.apps.completion.app_config_manager import CompletionAppConfigManager
|
||||||
|
from core.file.file_obj import FileExtraConfig
|
||||||
from core.helper import encrypter
|
from core.helper import encrypter
|
||||||
from core.model_runtime.entities.llm_entities import LLMMode
|
from core.model_runtime.entities.llm_entities import LLMMode
|
||||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ class WorkflowService:
|
||||||
unique_hash: Optional[str],
|
unique_hash: Optional[str],
|
||||||
account: Account,
|
account: Account,
|
||||||
environment_variables: Sequence[Variable],
|
environment_variables: Sequence[Variable],
|
||||||
|
conversation_variables: Sequence[Variable],
|
||||||
) -> Workflow:
|
) -> Workflow:
|
||||||
"""
|
"""
|
||||||
Sync draft workflow
|
Sync draft workflow
|
||||||
|
|
@ -99,7 +100,8 @@ class WorkflowService:
|
||||||
graph=json.dumps(graph),
|
graph=json.dumps(graph),
|
||||||
features=json.dumps(features),
|
features=json.dumps(features),
|
||||||
created_by=account.id,
|
created_by=account.id,
|
||||||
environment_variables=environment_variables
|
environment_variables=environment_variables,
|
||||||
|
conversation_variables=conversation_variables,
|
||||||
)
|
)
|
||||||
db.session.add(workflow)
|
db.session.add(workflow)
|
||||||
# update draft workflow if found
|
# update draft workflow if found
|
||||||
|
|
@ -109,6 +111,7 @@ class WorkflowService:
|
||||||
workflow.updated_by = account.id
|
workflow.updated_by = account.id
|
||||||
workflow.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
workflow.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
workflow.environment_variables = environment_variables
|
workflow.environment_variables = environment_variables
|
||||||
|
workflow.conversation_variables = conversation_variables
|
||||||
|
|
||||||
# commit db session changes
|
# commit db session changes
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
@ -145,7 +148,8 @@ class WorkflowService:
|
||||||
graph=draft_workflow.graph,
|
graph=draft_workflow.graph,
|
||||||
features=draft_workflow.features,
|
features=draft_workflow.features,
|
||||||
created_by=account.id,
|
created_by=account.id,
|
||||||
environment_variables=draft_workflow.environment_variables
|
environment_variables=draft_workflow.environment_variables,
|
||||||
|
conversation_variables=draft_workflow.conversation_variables,
|
||||||
)
|
)
|
||||||
|
|
||||||
# commit db session changes
|
# commit db session changes
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
|
from sqlalchemy import delete
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
|
|
@ -28,7 +30,7 @@ from models.model import (
|
||||||
)
|
)
|
||||||
from models.tools import WorkflowToolProvider
|
from models.tools import WorkflowToolProvider
|
||||||
from models.web import PinnedConversation, SavedMessage
|
from models.web import PinnedConversation, SavedMessage
|
||||||
from models.workflow import Workflow, WorkflowAppLog, WorkflowNodeExecution, WorkflowRun
|
from models.workflow import ConversationVariable, Workflow, WorkflowAppLog, WorkflowNodeExecution, WorkflowRun
|
||||||
|
|
||||||
|
|
||||||
@shared_task(queue='app_deletion', bind=True, max_retries=3)
|
@shared_task(queue='app_deletion', bind=True, max_retries=3)
|
||||||
|
|
@ -54,6 +56,7 @@ def remove_app_and_related_data_task(self, tenant_id: str, app_id: str):
|
||||||
_delete_app_tag_bindings(tenant_id, app_id)
|
_delete_app_tag_bindings(tenant_id, app_id)
|
||||||
_delete_end_users(tenant_id, app_id)
|
_delete_end_users(tenant_id, app_id)
|
||||||
_delete_trace_app_configs(tenant_id, app_id)
|
_delete_trace_app_configs(tenant_id, app_id)
|
||||||
|
_delete_conversation_variables(app_id=app_id)
|
||||||
|
|
||||||
end_at = time.perf_counter()
|
end_at = time.perf_counter()
|
||||||
logging.info(click.style(f'App and related data deleted: {app_id} latency: {end_at - start_at}', fg='green'))
|
logging.info(click.style(f'App and related data deleted: {app_id} latency: {end_at - start_at}', fg='green'))
|
||||||
|
|
@ -225,6 +228,13 @@ def _delete_app_conversations(tenant_id: str, app_id: str):
|
||||||
"conversation"
|
"conversation"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _delete_conversation_variables(*, app_id: str):
|
||||||
|
stmt = delete(ConversationVariable).where(ConversationVariable.app_id == app_id)
|
||||||
|
with db.engine.connect() as conn:
|
||||||
|
conn.execute(stmt)
|
||||||
|
conn.commit()
|
||||||
|
logging.info(click.style(f"Deleted conversation variables for app {app_id}", fg='green'))
|
||||||
|
|
||||||
|
|
||||||
def _delete_app_messages(tenant_id: str, app_id: str):
|
def _delete_app_messages(tenant_id: str, app_id: str):
|
||||||
def del_message(message_id: str):
|
def del_message(message_id: str):
|
||||||
|
|
@ -299,7 +309,7 @@ def _delete_trace_app_configs(tenant_id: str, app_id: str):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _delete_records(query_sql: str, params: dict, delete_func: callable, name: str) -> None:
|
def _delete_records(query_sql: str, params: dict, delete_func: Callable, name: str) -> None:
|
||||||
while True:
|
while True:
|
||||||
with db.engine.begin() as conn:
|
with db.engine.begin() as conn:
|
||||||
rs = conn.execute(db.text(query_sql), params)
|
rs = conn.execute(db.text(query_sql), params)
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,16 @@ from core.app.segments import (
|
||||||
ArrayNumberVariable,
|
ArrayNumberVariable,
|
||||||
ArrayObjectVariable,
|
ArrayObjectVariable,
|
||||||
ArrayStringVariable,
|
ArrayStringVariable,
|
||||||
|
FileSegment,
|
||||||
FileVariable,
|
FileVariable,
|
||||||
FloatVariable,
|
FloatVariable,
|
||||||
IntegerVariable,
|
IntegerVariable,
|
||||||
NoneSegment,
|
|
||||||
ObjectSegment,
|
ObjectSegment,
|
||||||
SecretVariable,
|
SecretVariable,
|
||||||
StringVariable,
|
StringVariable,
|
||||||
factory,
|
factory,
|
||||||
)
|
)
|
||||||
|
from core.app.segments.exc import VariableError
|
||||||
|
|
||||||
|
|
||||||
def test_string_variable():
|
def test_string_variable():
|
||||||
|
|
@ -44,7 +45,7 @@ def test_secret_variable():
|
||||||
|
|
||||||
def test_invalid_value_type():
|
def test_invalid_value_type():
|
||||||
test_data = {'value_type': 'unknown', 'name': 'test_invalid', 'value': 'value'}
|
test_data = {'value_type': 'unknown', 'name': 'test_invalid', 'value': 'value'}
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(VariableError):
|
||||||
factory.build_variable_from_mapping(test_data)
|
factory.build_variable_from_mapping(test_data)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -77,26 +78,14 @@ def test_object_variable():
|
||||||
'name': 'test_object',
|
'name': 'test_object',
|
||||||
'description': 'Description of the variable.',
|
'description': 'Description of the variable.',
|
||||||
'value': {
|
'value': {
|
||||||
'key1': {
|
'key1': 'text',
|
||||||
'id': str(uuid4()),
|
'key2': 2,
|
||||||
'value_type': 'string',
|
|
||||||
'name': 'text',
|
|
||||||
'value': 'text',
|
|
||||||
'description': 'Description of the variable.',
|
|
||||||
},
|
|
||||||
'key2': {
|
|
||||||
'id': str(uuid4()),
|
|
||||||
'value_type': 'number',
|
|
||||||
'name': 'number',
|
|
||||||
'value': 1,
|
|
||||||
'description': 'Description of the variable.',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
variable = factory.build_variable_from_mapping(mapping)
|
variable = factory.build_variable_from_mapping(mapping)
|
||||||
assert isinstance(variable, ObjectSegment)
|
assert isinstance(variable, ObjectSegment)
|
||||||
assert isinstance(variable.value['key1'], StringVariable)
|
assert isinstance(variable.value['key1'], str)
|
||||||
assert isinstance(variable.value['key2'], IntegerVariable)
|
assert isinstance(variable.value['key2'], int)
|
||||||
|
|
||||||
|
|
||||||
def test_array_string_variable():
|
def test_array_string_variable():
|
||||||
|
|
@ -106,26 +95,14 @@ def test_array_string_variable():
|
||||||
'name': 'test_array',
|
'name': 'test_array',
|
||||||
'description': 'Description of the variable.',
|
'description': 'Description of the variable.',
|
||||||
'value': [
|
'value': [
|
||||||
{
|
'text',
|
||||||
'id': str(uuid4()),
|
'text',
|
||||||
'value_type': 'string',
|
|
||||||
'name': 'text',
|
|
||||||
'value': 'text',
|
|
||||||
'description': 'Description of the variable.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': str(uuid4()),
|
|
||||||
'value_type': 'string',
|
|
||||||
'name': 'text',
|
|
||||||
'value': 'text',
|
|
||||||
'description': 'Description of the variable.',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
variable = factory.build_variable_from_mapping(mapping)
|
variable = factory.build_variable_from_mapping(mapping)
|
||||||
assert isinstance(variable, ArrayStringVariable)
|
assert isinstance(variable, ArrayStringVariable)
|
||||||
assert isinstance(variable.value[0], StringVariable)
|
assert isinstance(variable.value[0], str)
|
||||||
assert isinstance(variable.value[1], StringVariable)
|
assert isinstance(variable.value[1], str)
|
||||||
|
|
||||||
|
|
||||||
def test_array_number_variable():
|
def test_array_number_variable():
|
||||||
|
|
@ -135,26 +112,14 @@ def test_array_number_variable():
|
||||||
'name': 'test_array',
|
'name': 'test_array',
|
||||||
'description': 'Description of the variable.',
|
'description': 'Description of the variable.',
|
||||||
'value': [
|
'value': [
|
||||||
{
|
1,
|
||||||
'id': str(uuid4()),
|
2.0,
|
||||||
'value_type': 'number',
|
|
||||||
'name': 'number',
|
|
||||||
'value': 1,
|
|
||||||
'description': 'Description of the variable.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': str(uuid4()),
|
|
||||||
'value_type': 'number',
|
|
||||||
'name': 'number',
|
|
||||||
'value': 2.0,
|
|
||||||
'description': 'Description of the variable.',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
variable = factory.build_variable_from_mapping(mapping)
|
variable = factory.build_variable_from_mapping(mapping)
|
||||||
assert isinstance(variable, ArrayNumberVariable)
|
assert isinstance(variable, ArrayNumberVariable)
|
||||||
assert isinstance(variable.value[0], IntegerVariable)
|
assert isinstance(variable.value[0], int)
|
||||||
assert isinstance(variable.value[1], FloatVariable)
|
assert isinstance(variable.value[1], float)
|
||||||
|
|
||||||
|
|
||||||
def test_array_object_variable():
|
def test_array_object_variable():
|
||||||
|
|
@ -165,59 +130,23 @@ def test_array_object_variable():
|
||||||
'description': 'Description of the variable.',
|
'description': 'Description of the variable.',
|
||||||
'value': [
|
'value': [
|
||||||
{
|
{
|
||||||
'id': str(uuid4()),
|
'key1': 'text',
|
||||||
'value_type': 'object',
|
'key2': 1,
|
||||||
'name': 'object',
|
|
||||||
'description': 'Description of the variable.',
|
|
||||||
'value': {
|
|
||||||
'key1': {
|
|
||||||
'id': str(uuid4()),
|
|
||||||
'value_type': 'string',
|
|
||||||
'name': 'text',
|
|
||||||
'value': 'text',
|
|
||||||
'description': 'Description of the variable.',
|
|
||||||
},
|
|
||||||
'key2': {
|
|
||||||
'id': str(uuid4()),
|
|
||||||
'value_type': 'number',
|
|
||||||
'name': 'number',
|
|
||||||
'value': 1,
|
|
||||||
'description': 'Description of the variable.',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'id': str(uuid4()),
|
'key1': 'text',
|
||||||
'value_type': 'object',
|
'key2': 1,
|
||||||
'name': 'object',
|
|
||||||
'description': 'Description of the variable.',
|
|
||||||
'value': {
|
|
||||||
'key1': {
|
|
||||||
'id': str(uuid4()),
|
|
||||||
'value_type': 'string',
|
|
||||||
'name': 'text',
|
|
||||||
'value': 'text',
|
|
||||||
'description': 'Description of the variable.',
|
|
||||||
},
|
|
||||||
'key2': {
|
|
||||||
'id': str(uuid4()),
|
|
||||||
'value_type': 'number',
|
|
||||||
'name': 'number',
|
|
||||||
'value': 1,
|
|
||||||
'description': 'Description of the variable.',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
variable = factory.build_variable_from_mapping(mapping)
|
variable = factory.build_variable_from_mapping(mapping)
|
||||||
assert isinstance(variable, ArrayObjectVariable)
|
assert isinstance(variable, ArrayObjectVariable)
|
||||||
assert isinstance(variable.value[0], ObjectSegment)
|
assert isinstance(variable.value[0], dict)
|
||||||
assert isinstance(variable.value[1], ObjectSegment)
|
assert isinstance(variable.value[1], dict)
|
||||||
assert isinstance(variable.value[0].value['key1'], StringVariable)
|
assert isinstance(variable.value[0]['key1'], str)
|
||||||
assert isinstance(variable.value[0].value['key2'], IntegerVariable)
|
assert isinstance(variable.value[0]['key2'], int)
|
||||||
assert isinstance(variable.value[1].value['key1'], StringVariable)
|
assert isinstance(variable.value[1]['key1'], str)
|
||||||
assert isinstance(variable.value[1].value['key2'], IntegerVariable)
|
assert isinstance(variable.value[1]['key2'], int)
|
||||||
|
|
||||||
|
|
||||||
def test_file_variable():
|
def test_file_variable():
|
||||||
|
|
@ -257,51 +186,53 @@ def test_array_file_variable():
|
||||||
'value': [
|
'value': [
|
||||||
{
|
{
|
||||||
'id': str(uuid4()),
|
'id': str(uuid4()),
|
||||||
'name': 'file',
|
'tenant_id': 'tenant_id',
|
||||||
'value_type': 'file',
|
'type': 'image',
|
||||||
'value': {
|
'transfer_method': 'local_file',
|
||||||
'id': str(uuid4()),
|
'url': 'url',
|
||||||
'tenant_id': 'tenant_id',
|
'related_id': 'related_id',
|
||||||
'type': 'image',
|
'extra_config': {
|
||||||
'transfer_method': 'local_file',
|
'image_config': {
|
||||||
'url': 'url',
|
'width': 100,
|
||||||
'related_id': 'related_id',
|
'height': 100,
|
||||||
'extra_config': {
|
|
||||||
'image_config': {
|
|
||||||
'width': 100,
|
|
||||||
'height': 100,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
'filename': 'filename',
|
|
||||||
'extension': 'extension',
|
|
||||||
'mime_type': 'mime_type',
|
|
||||||
},
|
},
|
||||||
|
'filename': 'filename',
|
||||||
|
'extension': 'extension',
|
||||||
|
'mime_type': 'mime_type',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'id': str(uuid4()),
|
'id': str(uuid4()),
|
||||||
'name': 'file',
|
'tenant_id': 'tenant_id',
|
||||||
'value_type': 'file',
|
'type': 'image',
|
||||||
'value': {
|
'transfer_method': 'local_file',
|
||||||
'id': str(uuid4()),
|
'url': 'url',
|
||||||
'tenant_id': 'tenant_id',
|
'related_id': 'related_id',
|
||||||
'type': 'image',
|
'extra_config': {
|
||||||
'transfer_method': 'local_file',
|
'image_config': {
|
||||||
'url': 'url',
|
'width': 100,
|
||||||
'related_id': 'related_id',
|
'height': 100,
|
||||||
'extra_config': {
|
|
||||||
'image_config': {
|
|
||||||
'width': 100,
|
|
||||||
'height': 100,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
'filename': 'filename',
|
|
||||||
'extension': 'extension',
|
|
||||||
'mime_type': 'mime_type',
|
|
||||||
},
|
},
|
||||||
|
'filename': 'filename',
|
||||||
|
'extension': 'extension',
|
||||||
|
'mime_type': 'mime_type',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
variable = factory.build_variable_from_mapping(mapping)
|
variable = factory.build_variable_from_mapping(mapping)
|
||||||
assert isinstance(variable, ArrayFileVariable)
|
assert isinstance(variable, ArrayFileVariable)
|
||||||
assert isinstance(variable.value[0], FileVariable)
|
assert isinstance(variable.value[0], FileSegment)
|
||||||
assert isinstance(variable.value[1], FileVariable)
|
assert isinstance(variable.value[1], FileSegment)
|
||||||
|
|
||||||
|
|
||||||
|
def test_variable_cannot_large_than_5_kb():
|
||||||
|
with pytest.raises(VariableError):
|
||||||
|
factory.build_variable_from_mapping(
|
||||||
|
{
|
||||||
|
'id': str(uuid4()),
|
||||||
|
'value_type': 'string',
|
||||||
|
'name': 'test_text',
|
||||||
|
'value': 'a' * 1024 * 6,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from core.app.app_config.entities import FileExtraConfig, ModelConfigEntity
|
from core.app.app_config.entities import ModelConfigEntity
|
||||||
from core.file.file_obj import FileTransferMethod, FileType, FileVar
|
from core.file.file_obj import FileExtraConfig, FileTransferMethod, FileType, FileVar
|
||||||
from core.memory.token_buffer_memory import TokenBufferMemory
|
from core.memory.token_buffer_memory import TokenBufferMemory
|
||||||
from core.model_runtime.entities.message_entities import AssistantPromptMessage, PromptMessageRole, UserPromptMessage
|
from core.model_runtime.entities.message_entities import AssistantPromptMessage, PromptMessageRole, UserPromptMessage
|
||||||
from core.prompt.advanced_prompt_transform import AdvancedPromptTransform
|
from core.prompt.advanced_prompt_transform import AdvancedPromptTransform
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
from unittest import mock
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
|
from core.app.segments import ArrayStringVariable, StringVariable
|
||||||
|
from core.workflow.entities.node_entities import SystemVariable
|
||||||
|
from core.workflow.entities.variable_pool import VariablePool
|
||||||
|
from core.workflow.nodes.base_node import UserFrom
|
||||||
|
from core.workflow.nodes.variable_assigner import VariableAssignerNode, WriteMode
|
||||||
|
|
||||||
|
DEFAULT_NODE_ID = 'node_id'
|
||||||
|
|
||||||
|
|
||||||
|
def test_overwrite_string_variable():
|
||||||
|
conversation_variable = StringVariable(
|
||||||
|
id=str(uuid4()),
|
||||||
|
name='test_conversation_variable',
|
||||||
|
value='the first value',
|
||||||
|
)
|
||||||
|
|
||||||
|
input_variable = StringVariable(
|
||||||
|
id=str(uuid4()),
|
||||||
|
name='test_string_variable',
|
||||||
|
value='the second value',
|
||||||
|
)
|
||||||
|
|
||||||
|
node = VariableAssignerNode(
|
||||||
|
tenant_id='tenant_id',
|
||||||
|
app_id='app_id',
|
||||||
|
workflow_id='workflow_id',
|
||||||
|
user_id='user_id',
|
||||||
|
user_from=UserFrom.ACCOUNT,
|
||||||
|
invoke_from=InvokeFrom.DEBUGGER,
|
||||||
|
config={
|
||||||
|
'id': 'node_id',
|
||||||
|
'data': {
|
||||||
|
'assigned_variable_selector': ['conversation', conversation_variable.name],
|
||||||
|
'write_mode': WriteMode.OVER_WRITE.value,
|
||||||
|
'input_variable_selector': [DEFAULT_NODE_ID, input_variable.name],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
variable_pool = VariablePool(
|
||||||
|
system_variables={SystemVariable.CONVERSATION_ID: 'conversation_id'},
|
||||||
|
user_inputs={},
|
||||||
|
environment_variables=[],
|
||||||
|
conversation_variables=[conversation_variable],
|
||||||
|
)
|
||||||
|
variable_pool.add(
|
||||||
|
[DEFAULT_NODE_ID, input_variable.name],
|
||||||
|
input_variable,
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock.patch('core.workflow.nodes.variable_assigner.update_conversation_variable') as mock_run:
|
||||||
|
node.run(variable_pool)
|
||||||
|
mock_run.assert_called_once()
|
||||||
|
|
||||||
|
got = variable_pool.get(['conversation', conversation_variable.name])
|
||||||
|
assert got is not None
|
||||||
|
assert got.value == 'the second value'
|
||||||
|
assert got.to_object() == 'the second value'
|
||||||
|
|
||||||
|
|
||||||
|
def test_append_variable_to_array():
|
||||||
|
conversation_variable = ArrayStringVariable(
|
||||||
|
id=str(uuid4()),
|
||||||
|
name='test_conversation_variable',
|
||||||
|
value=['the first value'],
|
||||||
|
)
|
||||||
|
|
||||||
|
input_variable = StringVariable(
|
||||||
|
id=str(uuid4()),
|
||||||
|
name='test_string_variable',
|
||||||
|
value='the second value',
|
||||||
|
)
|
||||||
|
|
||||||
|
node = VariableAssignerNode(
|
||||||
|
tenant_id='tenant_id',
|
||||||
|
app_id='app_id',
|
||||||
|
workflow_id='workflow_id',
|
||||||
|
user_id='user_id',
|
||||||
|
user_from=UserFrom.ACCOUNT,
|
||||||
|
invoke_from=InvokeFrom.DEBUGGER,
|
||||||
|
config={
|
||||||
|
'id': 'node_id',
|
||||||
|
'data': {
|
||||||
|
'assigned_variable_selector': ['conversation', conversation_variable.name],
|
||||||
|
'write_mode': WriteMode.APPEND.value,
|
||||||
|
'input_variable_selector': [DEFAULT_NODE_ID, input_variable.name],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
variable_pool = VariablePool(
|
||||||
|
system_variables={SystemVariable.CONVERSATION_ID: 'conversation_id'},
|
||||||
|
user_inputs={},
|
||||||
|
environment_variables=[],
|
||||||
|
conversation_variables=[conversation_variable],
|
||||||
|
)
|
||||||
|
variable_pool.add(
|
||||||
|
[DEFAULT_NODE_ID, input_variable.name],
|
||||||
|
input_variable,
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock.patch('core.workflow.nodes.variable_assigner.update_conversation_variable') as mock_run:
|
||||||
|
node.run(variable_pool)
|
||||||
|
mock_run.assert_called_once()
|
||||||
|
|
||||||
|
got = variable_pool.get(['conversation', conversation_variable.name])
|
||||||
|
assert got is not None
|
||||||
|
assert got.to_object() == ['the first value', 'the second value']
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_array():
|
||||||
|
conversation_variable = ArrayStringVariable(
|
||||||
|
id=str(uuid4()),
|
||||||
|
name='test_conversation_variable',
|
||||||
|
value=['the first value'],
|
||||||
|
)
|
||||||
|
|
||||||
|
node = VariableAssignerNode(
|
||||||
|
tenant_id='tenant_id',
|
||||||
|
app_id='app_id',
|
||||||
|
workflow_id='workflow_id',
|
||||||
|
user_id='user_id',
|
||||||
|
user_from=UserFrom.ACCOUNT,
|
||||||
|
invoke_from=InvokeFrom.DEBUGGER,
|
||||||
|
config={
|
||||||
|
'id': 'node_id',
|
||||||
|
'data': {
|
||||||
|
'assigned_variable_selector': ['conversation', conversation_variable.name],
|
||||||
|
'write_mode': WriteMode.CLEAR.value,
|
||||||
|
'input_variable_selector': [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
variable_pool = VariablePool(
|
||||||
|
system_variables={SystemVariable.CONVERSATION_ID: 'conversation_id'},
|
||||||
|
user_inputs={},
|
||||||
|
environment_variables=[],
|
||||||
|
conversation_variables=[conversation_variable],
|
||||||
|
)
|
||||||
|
|
||||||
|
node.run(variable_pool)
|
||||||
|
|
||||||
|
got = variable_pool.get(['conversation', conversation_variable.name])
|
||||||
|
assert got is not None
|
||||||
|
assert got.to_object() == []
|
||||||
25
api/tests/unit_tests/models/test_conversation_variable.py
Normal file
25
api/tests/unit_tests/models/test_conversation_variable.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from core.app.segments import SegmentType, factory
|
||||||
|
from models import ConversationVariable
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_variable_and_to_variable():
|
||||||
|
variable = factory.build_variable_from_mapping(
|
||||||
|
{
|
||||||
|
'id': str(uuid4()),
|
||||||
|
'name': 'name',
|
||||||
|
'value_type': SegmentType.OBJECT,
|
||||||
|
'value': {
|
||||||
|
'key': {
|
||||||
|
'key': 'value',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
conversation_variable = ConversationVariable.from_variable(
|
||||||
|
app_id='app_id', conversation_id='conversation_id', variable=variable
|
||||||
|
)
|
||||||
|
|
||||||
|
assert conversation_variable.to_variable() == variable
|
||||||
|
|
@ -4,16 +4,19 @@ import cn from '@/utils/classnames'
|
||||||
type BadgeProps = {
|
type BadgeProps = {
|
||||||
className?: string
|
className?: string
|
||||||
text: string
|
text: string
|
||||||
|
uppercase?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Badge = ({
|
const Badge = ({
|
||||||
className,
|
className,
|
||||||
text,
|
text,
|
||||||
|
uppercase = true,
|
||||||
}: BadgeProps) => {
|
}: BadgeProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center px-[5px] h-5 rounded-[5px] border border-divider-deep system-2xs-medium-uppercase leading-3 text-text-tertiary',
|
'inline-flex items-center px-[5px] h-5 rounded-[5px] border border-divider-deep leading-3 text-text-tertiary',
|
||||||
|
uppercase ? 'system-2xs-medium-uppercase' : 'system-xs-medium',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="Icon L">
|
||||||
|
<g id="Vector">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.33463 3.33333C2.96643 3.33333 2.66796 3.63181 2.66796 4V10.6667C2.66796 11.0349 2.96643 11.3333 3.33463 11.3333H4.66796C5.03615 11.3333 5.33463 11.6318 5.33463 12V12.8225L7.65833 11.4283C7.76194 11.3662 7.8805 11.3333 8.00132 11.3333H12.0013C12.3695 11.3333 12.668 11.0349 12.668 10.6667C12.668 10.2985 12.9665 10 13.3347 10C13.7028 10 14.0013 10.2985 14.0013 10.6667C14.0013 11.7713 13.1058 12.6667 12.0013 12.6667H8.18598L5.01095 14.5717C4.805 14.6952 4.5485 14.6985 4.33949 14.5801C4.13049 14.4618 4.00129 14.2402 4.00129 14V12.6667H3.33463C2.23006 12.6667 1.33463 11.7713 1.33463 10.6667V4C1.33463 2.89543 2.23006 2 3.33463 2H6.66798C7.03617 2 7.33464 2.29848 7.33464 2.66667C7.33464 3.03486 7.03617 3.33333 6.66798 3.33333H3.33463Z" fill="#354052"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.74113 2.66667C8.74113 2.29848 9.03961 2 9.4078 2H10.331C10.9721 2 11.5177 2.43571 11.6859 3.04075L11.933 3.93004L12.8986 2.77189C13.3045 2.28508 13.9018 2 14.536 2H14.5954C14.9636 2 15.2621 2.29848 15.2621 2.66667C15.2621 3.03486 14.9636 3.33333 14.5954 3.33333H14.536C14.3048 3.33333 14.08 3.43702 13.9227 3.6257L12.367 5.49165L12.8609 7.2689C12.8746 7.31803 12.9105 7.33333 12.9312 7.33333H13.8543C14.2225 7.33333 14.521 7.63181 14.521 8C14.521 8.36819 14.2225 8.66667 13.8543 8.66667H12.9312C12.29 8.66667 11.7444 8.23095 11.5763 7.62591L11.3291 6.73654L10.3634 7.89478C9.95758 8.38159 9.36022 8.66667 8.72604 8.66667H8.66666C8.29847 8.66667 7.99999 8.36819 7.99999 8C7.99999 7.63181 8.29847 7.33333 8.66666 7.33333H8.72604C8.95723 7.33333 9.18204 7.22965 9.33935 7.04096L10.8951 5.17493L10.4012 3.39777C10.3876 3.34863 10.3516 3.33333 10.331 3.33333H9.4078C9.03961 3.33333 8.74113 3.03486 8.74113 2.66667Z" fill="#354052"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="21" height="8" viewBox="0 0 21 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.646446 3.64645C0.451185 3.84171 0.451185 4.15829 0.646446 4.35355L3.82843 7.53553C4.02369 7.7308 4.34027 7.7308 4.53553 7.53553C4.7308 7.34027 4.7308 7.02369 4.53553 6.82843L1.70711 4L4.53553 1.17157C4.7308 0.976311 4.7308 0.659728 4.53553 0.464466C4.34027 0.269204 4.02369 0.269204 3.82843 0.464466L0.646446 3.64645ZM21 3.5L1 3.5V4.5L21 4.5V3.5Z" fill="#101828" fill-opacity="0.3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 497 B |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="26" height="8" viewBox="0 0 26 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M25.3536 4.35355C25.5488 4.15829 25.5488 3.84171 25.3536 3.64644L22.1716 0.464465C21.9763 0.269202 21.6597 0.269202 21.4645 0.464465C21.2692 0.659727 21.2692 0.976309 21.4645 1.17157L24.2929 4L21.4645 6.82843C21.2692 7.02369 21.2692 7.34027 21.4645 7.53553C21.6597 7.73079 21.9763 7.73079 22.1716 7.53553L25.3536 4.35355ZM3.59058e-08 4.5L25 4.5L25 3.5L-3.59058e-08 3.5L3.59058e-08 4.5Z" fill="#101828" fill-opacity="0.3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 533 B |
|
|
@ -0,0 +1,9 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="variable assigner">
|
||||||
|
<g id="Vector">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.71438 4.42875C1.71438 3.22516 2.68954 2.25 3.89313 2.25C4.30734 2.25 4.64313 2.58579 4.64313 3C4.64313 3.41421 4.30734 3.75 3.89313 3.75C3.51796 3.75 3.21438 4.05359 3.21438 4.42875V7.28563C3.21438 7.48454 3.13536 7.6753 2.9947 7.81596L2.81066 8L2.9947 8.18404C3.13536 8.3247 3.21438 8.51546 3.21438 8.71437V11.5713C3.21438 11.9464 3.51796 12.25 3.89313 12.25C4.30734 12.25 4.64313 12.5858 4.64313 13C4.64313 13.4142 4.30734 13.75 3.89313 13.75C2.68954 13.75 1.71438 12.7748 1.71438 11.5713V9.02503L1.21967 8.53033C1.07902 8.38968 1 8.19891 1 8C1 7.80109 1.07902 7.61032 1.21967 7.46967L1.71438 6.97497V4.42875ZM11.3568 3C11.3568 2.58579 11.6925 2.25 12.1068 2.25C13.3103 2.25 14.2855 3.22516 14.2855 4.42875V6.97497L14.7802 7.46967C14.9209 7.61032 14.9999 7.80109 14.9999 8C14.9999 8.19891 14.9209 8.38968 14.7802 8.53033L14.2855 9.02503V11.5713C14.2855 12.7751 13.3095 13.75 12.1068 13.75C11.6925 13.75 11.3568 13.4142 11.3568 13C11.3568 12.5858 11.6925 12.25 12.1068 12.25C12.4815 12.25 12.7855 11.9462 12.7855 11.5713V8.71437C12.7855 8.51546 12.8645 8.3247 13.0052 8.18404L13.1892 8L13.0052 7.81596C12.8645 7.6753 12.7855 7.48454 12.7855 7.28563V4.42875C12.7855 4.05359 12.4819 3.75 12.1068 3.75C11.6925 3.75 11.3568 3.41421 11.3568 3Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.25 6C5.25 5.58579 5.58579 5.25 6 5.25H10C10.4142 5.25 10.75 5.58579 10.75 6C10.75 6.41421 10.4142 6.75 10 6.75H6C5.58579 6.75 5.25 6.41421 5.25 6Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.25 10C5.25 9.58579 5.58579 9.25 6 9.25H10C10.4142 9.25 10.75 9.58579 10.75 10C10.75 10.4142 10.4142 10.75 10 10.75H6C5.58579 10.75 5.25 10.4142 5.25 10Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1,57 @@
|
||||||
|
{
|
||||||
|
"icon": {
|
||||||
|
"type": "element",
|
||||||
|
"isRootNode": true,
|
||||||
|
"name": "svg",
|
||||||
|
"attributes": {
|
||||||
|
"width": "16",
|
||||||
|
"height": "16",
|
||||||
|
"viewBox": "0 0 16 16",
|
||||||
|
"fill": "none",
|
||||||
|
"xmlns": "http://www.w3.org/2000/svg"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "g",
|
||||||
|
"attributes": {
|
||||||
|
"id": "Icon L"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "g",
|
||||||
|
"attributes": {
|
||||||
|
"id": "Vector"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "path",
|
||||||
|
"attributes": {
|
||||||
|
"fill-rule": "evenodd",
|
||||||
|
"clip-rule": "evenodd",
|
||||||
|
"d": "M3.33463 3.33333C2.96643 3.33333 2.66796 3.63181 2.66796 4V10.6667C2.66796 11.0349 2.96643 11.3333 3.33463 11.3333H4.66796C5.03615 11.3333 5.33463 11.6318 5.33463 12V12.8225L7.65833 11.4283C7.76194 11.3662 7.8805 11.3333 8.00132 11.3333H12.0013C12.3695 11.3333 12.668 11.0349 12.668 10.6667C12.668 10.2985 12.9665 10 13.3347 10C13.7028 10 14.0013 10.2985 14.0013 10.6667C14.0013 11.7713 13.1058 12.6667 12.0013 12.6667H8.18598L5.01095 14.5717C4.805 14.6952 4.5485 14.6985 4.33949 14.5801C4.13049 14.4618 4.00129 14.2402 4.00129 14V12.6667H3.33463C2.23006 12.6667 1.33463 11.7713 1.33463 10.6667V4C1.33463 2.89543 2.23006 2 3.33463 2H6.66798C7.03617 2 7.33464 2.29848 7.33464 2.66667C7.33464 3.03486 7.03617 3.33333 6.66798 3.33333H3.33463Z",
|
||||||
|
"fill": "currentColor"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "path",
|
||||||
|
"attributes": {
|
||||||
|
"fill-rule": "evenodd",
|
||||||
|
"clip-rule": "evenodd",
|
||||||
|
"d": "M8.74113 2.66667C8.74113 2.29848 9.03961 2 9.4078 2H10.331C10.9721 2 11.5177 2.43571 11.6859 3.04075L11.933 3.93004L12.8986 2.77189C13.3045 2.28508 13.9018 2 14.536 2H14.5954C14.9636 2 15.2621 2.29848 15.2621 2.66667C15.2621 3.03486 14.9636 3.33333 14.5954 3.33333H14.536C14.3048 3.33333 14.08 3.43702 13.9227 3.6257L12.367 5.49165L12.8609 7.2689C12.8746 7.31803 12.9105 7.33333 12.9312 7.33333H13.8543C14.2225 7.33333 14.521 7.63181 14.521 8C14.521 8.36819 14.2225 8.66667 13.8543 8.66667H12.9312C12.29 8.66667 11.7444 8.23095 11.5763 7.62591L11.3291 6.73654L10.3634 7.89478C9.95758 8.38159 9.36022 8.66667 8.72604 8.66667H8.66666C8.29847 8.66667 7.99999 8.36819 7.99999 8C7.99999 7.63181 8.29847 7.33333 8.66666 7.33333H8.72604C8.95723 7.33333 9.18204 7.22965 9.33935 7.04096L10.8951 5.17493L10.4012 3.39777C10.3876 3.34863 10.3516 3.33333 10.331 3.33333H9.4078C9.03961 3.33333 8.74113 3.03486 8.74113 2.66667Z",
|
||||||
|
"fill": "currentColor"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "BubbleX"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// GENERATE BY script
|
||||||
|
// DON NOT EDIT IT MANUALLY
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import data from './BubbleX.json'
|
||||||
|
import IconBase from '@/app/components/base/icons/IconBase'
|
||||||
|
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||||
|
|
||||||
|
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||||
|
props,
|
||||||
|
ref,
|
||||||
|
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||||
|
|
||||||
|
Icon.displayName = 'BubbleX'
|
||||||
|
|
||||||
|
export default Icon
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"icon": {
|
||||||
|
"type": "element",
|
||||||
|
"isRootNode": true,
|
||||||
|
"name": "svg",
|
||||||
|
"attributes": {
|
||||||
|
"width": "21",
|
||||||
|
"height": "8",
|
||||||
|
"viewBox": "0 0 21 8",
|
||||||
|
"fill": "none",
|
||||||
|
"xmlns": "http://www.w3.org/2000/svg"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "path",
|
||||||
|
"attributes": {
|
||||||
|
"d": "M0.646446 3.64645C0.451185 3.84171 0.451185 4.15829 0.646446 4.35355L3.82843 7.53553C4.02369 7.7308 4.34027 7.7308 4.53553 7.53553C4.7308 7.34027 4.7308 7.02369 4.53553 6.82843L1.70711 4L4.53553 1.17157C4.7308 0.976311 4.7308 0.659728 4.53553 0.464466C4.34027 0.269204 4.02369 0.269204 3.82843 0.464466L0.646446 3.64645ZM21 3.5L1 3.5V4.5L21 4.5V3.5Z",
|
||||||
|
"fill": "currentColor",
|
||||||
|
"fill-opacity": "0.3"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "LongArrowLeft"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// GENERATE BY script
|
||||||
|
// DON NOT EDIT IT MANUALLY
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import data from './LongArrowLeft.json'
|
||||||
|
import IconBase from '@/app/components/base/icons/IconBase'
|
||||||
|
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||||
|
|
||||||
|
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||||
|
props,
|
||||||
|
ref,
|
||||||
|
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||||
|
|
||||||
|
Icon.displayName = 'LongArrowLeft'
|
||||||
|
|
||||||
|
export default Icon
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"icon": {
|
||||||
|
"type": "element",
|
||||||
|
"isRootNode": true,
|
||||||
|
"name": "svg",
|
||||||
|
"attributes": {
|
||||||
|
"width": "26",
|
||||||
|
"height": "8",
|
||||||
|
"viewBox": "0 0 26 8",
|
||||||
|
"fill": "none",
|
||||||
|
"xmlns": "http://www.w3.org/2000/svg"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "path",
|
||||||
|
"attributes": {
|
||||||
|
"d": "M25.3536 4.35355C25.5488 4.15829 25.5488 3.84171 25.3536 3.64644L22.1716 0.464465C21.9763 0.269202 21.6597 0.269202 21.4645 0.464465C21.2692 0.659727 21.2692 0.976309 21.4645 1.17157L24.2929 4L21.4645 6.82843C21.2692 7.02369 21.2692 7.34027 21.4645 7.53553C21.6597 7.73079 21.9763 7.73079 22.1716 7.53553L25.3536 4.35355ZM3.59058e-08 4.5L25 4.5L25 3.5L-3.59058e-08 3.5L3.59058e-08 4.5Z",
|
||||||
|
"fill": "currentColor",
|
||||||
|
"fill-opacity": "0.3"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "LongArrowRight"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// GENERATE BY script
|
||||||
|
// DON NOT EDIT IT MANUALLY
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import data from './LongArrowRight.json'
|
||||||
|
import IconBase from '@/app/components/base/icons/IconBase'
|
||||||
|
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||||
|
|
||||||
|
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||||
|
props,
|
||||||
|
ref,
|
||||||
|
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||||
|
|
||||||
|
Icon.displayName = 'LongArrowRight'
|
||||||
|
|
||||||
|
export default Icon
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
export { default as Apps02 } from './Apps02'
|
export { default as Apps02 } from './Apps02'
|
||||||
|
export { default as BubbleX } from './BubbleX'
|
||||||
export { default as Colors } from './Colors'
|
export { default as Colors } from './Colors'
|
||||||
export { default as DragHandle } from './DragHandle'
|
export { default as DragHandle } from './DragHandle'
|
||||||
export { default as Env } from './Env'
|
export { default as Env } from './Env'
|
||||||
export { default as Exchange02 } from './Exchange02'
|
export { default as Exchange02 } from './Exchange02'
|
||||||
export { default as FileCode } from './FileCode'
|
export { default as FileCode } from './FileCode'
|
||||||
export { default as Icon3Dots } from './Icon3Dots'
|
export { default as Icon3Dots } from './Icon3Dots'
|
||||||
|
export { default as LongArrowLeft } from './LongArrowLeft'
|
||||||
|
export { default as LongArrowRight } from './LongArrowRight'
|
||||||
export { default as Tools } from './Tools'
|
export { default as Tools } from './Tools'
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
{
|
||||||
|
"icon": {
|
||||||
|
"type": "element",
|
||||||
|
"isRootNode": true,
|
||||||
|
"name": "svg",
|
||||||
|
"attributes": {
|
||||||
|
"width": "16",
|
||||||
|
"height": "16",
|
||||||
|
"viewBox": "0 0 16 16",
|
||||||
|
"fill": "none",
|
||||||
|
"xmlns": "http://www.w3.org/2000/svg"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "g",
|
||||||
|
"attributes": {
|
||||||
|
"id": "variable assigner"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "g",
|
||||||
|
"attributes": {
|
||||||
|
"id": "Vector"
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "path",
|
||||||
|
"attributes": {
|
||||||
|
"fill-rule": "evenodd",
|
||||||
|
"clip-rule": "evenodd",
|
||||||
|
"d": "M1.71438 4.42875C1.71438 3.22516 2.68954 2.25 3.89313 2.25C4.30734 2.25 4.64313 2.58579 4.64313 3C4.64313 3.41421 4.30734 3.75 3.89313 3.75C3.51796 3.75 3.21438 4.05359 3.21438 4.42875V7.28563C3.21438 7.48454 3.13536 7.6753 2.9947 7.81596L2.81066 8L2.9947 8.18404C3.13536 8.3247 3.21438 8.51546 3.21438 8.71437V11.5713C3.21438 11.9464 3.51796 12.25 3.89313 12.25C4.30734 12.25 4.64313 12.5858 4.64313 13C4.64313 13.4142 4.30734 13.75 3.89313 13.75C2.68954 13.75 1.71438 12.7748 1.71438 11.5713V9.02503L1.21967 8.53033C1.07902 8.38968 1 8.19891 1 8C1 7.80109 1.07902 7.61032 1.21967 7.46967L1.71438 6.97497V4.42875ZM11.3568 3C11.3568 2.58579 11.6925 2.25 12.1068 2.25C13.3103 2.25 14.2855 3.22516 14.2855 4.42875V6.97497L14.7802 7.46967C14.9209 7.61032 14.9999 7.80109 14.9999 8C14.9999 8.19891 14.9209 8.38968 14.7802 8.53033L14.2855 9.02503V11.5713C14.2855 12.7751 13.3095 13.75 12.1068 13.75C11.6925 13.75 11.3568 13.4142 11.3568 13C11.3568 12.5858 11.6925 12.25 12.1068 12.25C12.4815 12.25 12.7855 11.9462 12.7855 11.5713V8.71437C12.7855 8.51546 12.8645 8.3247 13.0052 8.18404L13.1892 8L13.0052 7.81596C12.8645 7.6753 12.7855 7.48454 12.7855 7.28563V4.42875C12.7855 4.05359 12.4819 3.75 12.1068 3.75C11.6925 3.75 11.3568 3.41421 11.3568 3Z",
|
||||||
|
"fill": "currentColor"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "path",
|
||||||
|
"attributes": {
|
||||||
|
"fill-rule": "evenodd",
|
||||||
|
"clip-rule": "evenodd",
|
||||||
|
"d": "M5.25 6C5.25 5.58579 5.58579 5.25 6 5.25H10C10.4142 5.25 10.75 5.58579 10.75 6C10.75 6.41421 10.4142 6.75 10 6.75H6C5.58579 6.75 5.25 6.41421 5.25 6Z",
|
||||||
|
"fill": "currentColor"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"name": "path",
|
||||||
|
"attributes": {
|
||||||
|
"fill-rule": "evenodd",
|
||||||
|
"clip-rule": "evenodd",
|
||||||
|
"d": "M5.25 10C5.25 9.58579 5.58579 9.25 6 9.25H10C10.4142 9.25 10.75 9.58579 10.75 10C10.75 10.4142 10.4142 10.75 10 10.75H6C5.58579 10.75 5.25 10.4142 5.25 10Z",
|
||||||
|
"fill": "currentColor"
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "Assigner"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// GENERATE BY script
|
||||||
|
// DON NOT EDIT IT MANUALLY
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import data from './Assigner.json'
|
||||||
|
import IconBase from '@/app/components/base/icons/IconBase'
|
||||||
|
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||||
|
|
||||||
|
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||||
|
props,
|
||||||
|
ref,
|
||||||
|
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||||
|
|
||||||
|
Icon.displayName = 'Assigner'
|
||||||
|
|
||||||
|
export default Icon
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export { default as Answer } from './Answer'
|
export { default as Answer } from './Answer'
|
||||||
|
export { default as Assigner } from './Assigner'
|
||||||
export { default as Code } from './Code'
|
export { default as Code } from './Code'
|
||||||
export { default as End } from './End'
|
export { default as End } from './End'
|
||||||
export { default as Home } from './Home'
|
export { default as Home } from './Home'
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import type { SVGProps } from 'react'
|
import type { SVGProps } from 'react'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import s from './style.module.css'
|
import cn from 'classnames'
|
||||||
|
|
||||||
type InputProps = {
|
type InputProps = {
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
|
|
@ -27,10 +27,10 @@ const Input = ({ value, defaultValue, onChange, className = '', wrapperClassName
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<div className={`relative inline-flex w-full ${wrapperClassName}`}>
|
<div className={`relative inline-flex w-full ${wrapperClassName}`}>
|
||||||
{showPrefix && <span className={s.prefix}>{prefixIcon ?? <GlassIcon className='h-3.5 w-3.5 stroke-current text-gray-700 stroke-2' />}</span>}
|
{showPrefix && <span className='whitespace-nowrap absolute left-2 self-center'>{prefixIcon ?? <GlassIcon className='h-3.5 w-3.5 stroke-current text-gray-700 stroke-2' />}</span>}
|
||||||
<input
|
<input
|
||||||
type={type ?? 'text'}
|
type={type ?? 'text'}
|
||||||
className={`${s.input} ${showPrefix ? '!pl-7' : ''} ${className}`}
|
className={cn('inline-flex h-7 w-full py-1 px-2 rounded-lg text-xs leading-normal bg-gray-100 caret-primary-600 hover:bg-gray-100 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none focus:bg-white placeholder:text-gray-400', showPrefix ? '!pl-7' : '', className)}
|
||||||
placeholder={placeholder ?? (showPrefix ? t('common.operation.search') ?? '' : 'please input')}
|
placeholder={placeholder ?? (showPrefix ? t('common.operation.search') ?? '' : 'please input')}
|
||||||
value={localValue}
|
value={localValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
.input {
|
|
||||||
@apply inline-flex h-7 w-full py-1 px-2 rounded-lg text-xs leading-normal;
|
|
||||||
@apply bg-gray-100 caret-primary-600 hover:bg-gray-100 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none focus:bg-white placeholder:text-gray-400;
|
|
||||||
}
|
|
||||||
.prefix {
|
|
||||||
@apply whitespace-nowrap absolute left-2 self-center
|
|
||||||
}
|
|
||||||
|
|
@ -144,7 +144,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
|
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
|
||||||
<div className='relative h-full'>
|
<div className='relative min-h-5'>
|
||||||
<RichTextPlugin
|
<RichTextPlugin
|
||||||
contentEditable={<ContentEditable className={`${className} outline-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'} text-gray-700`} style={style || {}} />}
|
contentEditable={<ContentEditable className={`${className} outline-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'} text-gray-700`} style={style || {}} />}
|
||||||
placeholder={<Placeholder value={placeholder} className={placeholderClassName} compact={compact} />}
|
placeholder={<Placeholder value={placeholder} className={placeholderClassName} compact={compact} />}
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,10 @@ import {
|
||||||
} from './index'
|
} from './index'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||||
import { isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||||
|
|
||||||
type WorkflowVariableBlockComponentProps = {
|
type WorkflowVariableBlockComponentProps = {
|
||||||
|
|
@ -52,6 +52,7 @@ const WorkflowVariableBlockComponent = ({
|
||||||
const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState<WorkflowNodesMap>(workflowNodesMap)
|
const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState<WorkflowNodesMap>(workflowNodesMap)
|
||||||
const node = localWorkflowNodesMap![variables[0]]
|
const node = localWorkflowNodesMap![variables[0]]
|
||||||
const isEnv = isENV(variables)
|
const isEnv = isENV(variables)
|
||||||
|
const isChatVar = isConversationVar(variables)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor.hasNodes([WorkflowVariableBlockNode]))
|
if (!editor.hasNodes([WorkflowVariableBlockNode]))
|
||||||
|
|
@ -75,11 +76,11 @@ const WorkflowVariableBlockComponent = ({
|
||||||
className={cn(
|
className={cn(
|
||||||
'mx-0.5 relative group/wrap flex items-center h-[18px] pl-0.5 pr-[3px] rounded-[5px] border select-none',
|
'mx-0.5 relative group/wrap flex items-center h-[18px] pl-0.5 pr-[3px] rounded-[5px] border select-none',
|
||||||
isSelected ? ' border-[#84ADFF] bg-[#F5F8FF]' : ' border-black/5 bg-white',
|
isSelected ? ' border-[#84ADFF] bg-[#F5F8FF]' : ' border-black/5 bg-white',
|
||||||
!node && !isEnv && '!border-[#F04438] !bg-[#FEF3F2]',
|
!node && !isEnv && !isChatVar && '!border-[#F04438] !bg-[#FEF3F2]',
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
{!isEnv && (
|
{!isEnv && !isChatVar && (
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
{
|
{
|
||||||
node?.type && (
|
node?.type && (
|
||||||
|
|
@ -97,11 +98,12 @@ const WorkflowVariableBlockComponent = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='flex items-center text-primary-600'>
|
<div className='flex items-center text-primary-600'>
|
||||||
{!isEnv && <Variable02 className='shrink-0 w-3.5 h-3.5' />}
|
{!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5' />}
|
||||||
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||||
<div className={cn('shrink-0 ml-0.5 text-xs font-medium truncate', isEnv && 'text-gray-900')} title={varName}>{varName}</div>
|
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||||
|
<div className={cn('shrink-0 ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && 'text-gray-900')} title={varName}>{varName}</div>
|
||||||
{
|
{
|
||||||
!node && !isEnv && (
|
!node && !isEnv && !isChatVar && (
|
||||||
<RiErrorWarningFill className='ml-0.5 w-3 h-3 text-[#D92D20]' />
|
<RiErrorWarningFill className='ml-0.5 w-3 h-3 text-[#D92D20]' />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +111,7 @@ const WorkflowVariableBlockComponent = ({
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!node && !isEnv) {
|
if (!node && !isEnv && !isChatVar) {
|
||||||
return (
|
return (
|
||||||
<TooltipPlus popupContent={t('workflow.errorMsg.invalidVariable')}>
|
<TooltipPlus popupContent={t('workflow.errorMsg.invalidVariable')}>
|
||||||
{Item}
|
{Item}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { memo } from 'react'
|
||||||
import { BlockEnum } from './types'
|
import { BlockEnum } from './types'
|
||||||
import {
|
import {
|
||||||
Answer,
|
Answer,
|
||||||
|
Assigner,
|
||||||
Code,
|
Code,
|
||||||
End,
|
End,
|
||||||
Home,
|
Home,
|
||||||
|
|
@ -43,6 +44,7 @@ const getIcon = (type: BlockEnum, className: string) => {
|
||||||
[BlockEnum.TemplateTransform]: <TemplatingTransform className={className} />,
|
[BlockEnum.TemplateTransform]: <TemplatingTransform className={className} />,
|
||||||
[BlockEnum.VariableAssigner]: <VariableX className={className} />,
|
[BlockEnum.VariableAssigner]: <VariableX className={className} />,
|
||||||
[BlockEnum.VariableAggregator]: <VariableX className={className} />,
|
[BlockEnum.VariableAggregator]: <VariableX className={className} />,
|
||||||
|
[BlockEnum.Assigner]: <Assigner className={className} />,
|
||||||
[BlockEnum.Tool]: <VariableX className={className} />,
|
[BlockEnum.Tool]: <VariableX className={className} />,
|
||||||
[BlockEnum.Iteration]: <Iteration className={className} />,
|
[BlockEnum.Iteration]: <Iteration className={className} />,
|
||||||
[BlockEnum.ParameterExtractor]: <ParameterExtractor className={className} />,
|
[BlockEnum.ParameterExtractor]: <ParameterExtractor className={className} />,
|
||||||
|
|
@ -62,6 +64,7 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
|
||||||
[BlockEnum.TemplateTransform]: 'bg-[#2E90FA]',
|
[BlockEnum.TemplateTransform]: 'bg-[#2E90FA]',
|
||||||
[BlockEnum.VariableAssigner]: 'bg-[#2E90FA]',
|
[BlockEnum.VariableAssigner]: 'bg-[#2E90FA]',
|
||||||
[BlockEnum.VariableAggregator]: 'bg-[#2E90FA]',
|
[BlockEnum.VariableAggregator]: 'bg-[#2E90FA]',
|
||||||
|
[BlockEnum.Assigner]: 'bg-[#2E90FA]',
|
||||||
[BlockEnum.ParameterExtractor]: 'bg-[#2E90FA]',
|
[BlockEnum.ParameterExtractor]: 'bg-[#2E90FA]',
|
||||||
}
|
}
|
||||||
const BlockIcon: FC<BlockIconProps> = ({
|
const BlockIcon: FC<BlockIconProps> = ({
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,11 @@ export const BLOCKS: Block[] = [
|
||||||
type: BlockEnum.VariableAggregator,
|
type: BlockEnum.VariableAggregator,
|
||||||
title: 'Variable Aggregator',
|
title: 'Variable Aggregator',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
classification: BlockClassificationEnum.Transform,
|
||||||
|
type: BlockEnum.Assigner,
|
||||||
|
title: 'Variable Assigner',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
classification: BlockClassificationEnum.Transform,
|
classification: BlockClassificationEnum.Transform,
|
||||||
type: BlockEnum.ParameterExtractor,
|
type: BlockEnum.ParameterExtractor,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import HttpRequestDefault from './nodes/http/default'
|
||||||
import ParameterExtractorDefault from './nodes/parameter-extractor/default'
|
import ParameterExtractorDefault from './nodes/parameter-extractor/default'
|
||||||
import ToolDefault from './nodes/tool/default'
|
import ToolDefault from './nodes/tool/default'
|
||||||
import VariableAssignerDefault from './nodes/variable-assigner/default'
|
import VariableAssignerDefault from './nodes/variable-assigner/default'
|
||||||
|
import AssignerDefault from './nodes/assigner/default'
|
||||||
import EndNodeDefault from './nodes/end/default'
|
import EndNodeDefault from './nodes/end/default'
|
||||||
import IterationDefault from './nodes/iteration/default'
|
import IterationDefault from './nodes/iteration/default'
|
||||||
|
|
||||||
|
|
@ -133,6 +134,15 @@ export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
|
||||||
getAvailableNextNodes: VariableAssignerDefault.getAvailableNextNodes,
|
getAvailableNextNodes: VariableAssignerDefault.getAvailableNextNodes,
|
||||||
checkValid: VariableAssignerDefault.checkValid,
|
checkValid: VariableAssignerDefault.checkValid,
|
||||||
},
|
},
|
||||||
|
[BlockEnum.Assigner]: {
|
||||||
|
author: 'Dify',
|
||||||
|
about: '',
|
||||||
|
availablePrevNodes: [],
|
||||||
|
availableNextNodes: [],
|
||||||
|
getAvailablePrevNodes: AssignerDefault.getAvailablePrevNodes,
|
||||||
|
getAvailableNextNodes: AssignerDefault.getAvailableNextNodes,
|
||||||
|
checkValid: AssignerDefault.checkValid,
|
||||||
|
},
|
||||||
[BlockEnum.VariableAggregator]: {
|
[BlockEnum.VariableAggregator]: {
|
||||||
author: 'Dify',
|
author: 'Dify',
|
||||||
about: '',
|
about: '',
|
||||||
|
|
@ -268,6 +278,12 @@ export const NODES_INITIAL_DATA = {
|
||||||
output_type: '',
|
output_type: '',
|
||||||
...VariableAssignerDefault.defaultValue,
|
...VariableAssignerDefault.defaultValue,
|
||||||
},
|
},
|
||||||
|
[BlockEnum.Assigner]: {
|
||||||
|
type: BlockEnum.Assigner,
|
||||||
|
title: '',
|
||||||
|
desc: '',
|
||||||
|
...AssignerDefault.defaultValue,
|
||||||
|
},
|
||||||
[BlockEnum.Tool]: {
|
[BlockEnum.Tool]: {
|
||||||
type: BlockEnum.Tool,
|
type: BlockEnum.Tool,
|
||||||
title: '',
|
title: '',
|
||||||
|
|
|
||||||
24
web/app/components/workflow/header/chat-variable-button.tsx
Normal file
24
web/app/components/workflow/header/chat-variable-button.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { memo } from 'react'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
|
||||||
|
import { useStore } from '@/app/components/workflow/store'
|
||||||
|
|
||||||
|
const ChatVariableButton = ({ disabled }: { disabled: boolean }) => {
|
||||||
|
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
|
||||||
|
const setShowEnvPanel = useStore(s => s.setShowEnvPanel)
|
||||||
|
const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel)
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
setShowChatVariablePanel(true)
|
||||||
|
setShowEnvPanel(false)
|
||||||
|
setShowDebugAndPreviewPanel(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button className='p-2' disabled={disabled} onClick={handleClick}>
|
||||||
|
<BubbleX className='w-4 h-4 text-components-button-secondary-text' />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ChatVariableButton)
|
||||||
|
|
@ -1,21 +1,23 @@
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||||
import { useStore } from '@/app/components/workflow/store'
|
import { useStore } from '@/app/components/workflow/store'
|
||||||
import cn from '@/utils/classnames'
|
|
||||||
|
|
||||||
const EnvButton = () => {
|
const EnvButton = ({ disabled }: { disabled: boolean }) => {
|
||||||
|
const setShowChatVariablePanel = useStore(s => s.setShowChatVariablePanel)
|
||||||
const setShowEnvPanel = useStore(s => s.setShowEnvPanel)
|
const setShowEnvPanel = useStore(s => s.setShowEnvPanel)
|
||||||
const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel)
|
const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel)
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
setShowEnvPanel(true)
|
setShowEnvPanel(true)
|
||||||
|
setShowChatVariablePanel(false)
|
||||||
setShowDebugAndPreviewPanel(false)
|
setShowDebugAndPreviewPanel(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('relative flex items-center justify-center p-0.5 w-8 h-8 rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs cursor-pointer hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover')} onClick={handleClick}>
|
<Button className='p-2' disabled={disabled} onClick={handleClick}>
|
||||||
<Env className='w-4 h-4 text-components-button-secondary-text' />
|
<Env className='w-4 h-4 text-components-button-secondary-text' />
|
||||||
</div>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
import type { StartNodeType } from '../nodes/start/types'
|
import type { StartNodeType } from '../nodes/start/types'
|
||||||
import {
|
import {
|
||||||
useChecklistBeforePublish,
|
useChecklistBeforePublish,
|
||||||
|
useIsChatMode,
|
||||||
useNodesReadOnly,
|
useNodesReadOnly,
|
||||||
useNodesSyncDraft,
|
useNodesSyncDraft,
|
||||||
useWorkflowMode,
|
useWorkflowMode,
|
||||||
|
|
@ -31,6 +32,7 @@ import EditingTitle from './editing-title'
|
||||||
import RunningTitle from './running-title'
|
import RunningTitle from './running-title'
|
||||||
import RestoringTitle from './restoring-title'
|
import RestoringTitle from './restoring-title'
|
||||||
import ViewHistory from './view-history'
|
import ViewHistory from './view-history'
|
||||||
|
import ChatVariableButton from './chat-variable-button'
|
||||||
import EnvButton from './env-button'
|
import EnvButton from './env-button'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
|
|
@ -44,7 +46,8 @@ const Header: FC = () => {
|
||||||
const appDetail = useAppStore(s => s.appDetail)
|
const appDetail = useAppStore(s => s.appDetail)
|
||||||
const appSidebarExpand = useAppStore(s => s.appSidebarExpand)
|
const appSidebarExpand = useAppStore(s => s.appSidebarExpand)
|
||||||
const appID = appDetail?.id
|
const appID = appDetail?.id
|
||||||
const { getNodesReadOnly } = useNodesReadOnly()
|
const isChatMode = useIsChatMode()
|
||||||
|
const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly()
|
||||||
const publishedAt = useStore(s => s.publishedAt)
|
const publishedAt = useStore(s => s.publishedAt)
|
||||||
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
|
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
|
||||||
const toolPublished = useStore(s => s.toolPublished)
|
const toolPublished = useStore(s => s.toolPublished)
|
||||||
|
|
@ -165,7 +168,8 @@ const Header: FC = () => {
|
||||||
{
|
{
|
||||||
normal && (
|
normal && (
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<EnvButton />
|
{isChatMode && <ChatVariableButton disabled={nodesReadOnly} />}
|
||||||
|
<EnvButton disabled={nodesReadOnly} />
|
||||||
<div className='w-[1px] h-3.5 bg-gray-200'></div>
|
<div className='w-[1px] h-3.5 bg-gray-200'></div>
|
||||||
<RunAndHistory />
|
<RunAndHistory />
|
||||||
<Button className='text-components-button-secondary-text' onClick={handleShowFeatures}>
|
<Button className='text-components-button-secondary-text' onClick={handleShowFeatures}>
|
||||||
|
|
@ -176,7 +180,7 @@ const Header: FC = () => {
|
||||||
{...{
|
{...{
|
||||||
publishedAt,
|
publishedAt,
|
||||||
draftUpdatedAt,
|
draftUpdatedAt,
|
||||||
disabled: Boolean(getNodesReadOnly()),
|
disabled: nodesReadOnly,
|
||||||
toolPublished,
|
toolPublished,
|
||||||
inputs: variables,
|
inputs: variables,
|
||||||
onRefreshData: handleToolConfigureUpdate,
|
onRefreshData: handleToolConfigureUpdate,
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ export const useNodesSyncDraft = () => {
|
||||||
const [x, y, zoom] = transform
|
const [x, y, zoom] = transform
|
||||||
const {
|
const {
|
||||||
appId,
|
appId,
|
||||||
|
conversationVariables,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
syncWorkflowDraftHash,
|
syncWorkflowDraftHash,
|
||||||
} = workflowStore.getState()
|
} = workflowStore.getState()
|
||||||
|
|
@ -82,6 +83,7 @@ export const useNodesSyncDraft = () => {
|
||||||
file_upload: features.file,
|
file_upload: features.file,
|
||||||
},
|
},
|
||||||
environment_variables: environmentVariables,
|
environment_variables: environmentVariables,
|
||||||
|
conversation_variables: conversationVariables,
|
||||||
hash: syncWorkflowDraftHash,
|
hash: syncWorkflowDraftHash,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ export const useWorkflowUpdate = () => {
|
||||||
setIsSyncingWorkflowDraft,
|
setIsSyncingWorkflowDraft,
|
||||||
setEnvironmentVariables,
|
setEnvironmentVariables,
|
||||||
setEnvSecrets,
|
setEnvSecrets,
|
||||||
|
setConversationVariables,
|
||||||
} = workflowStore.getState()
|
} = workflowStore.getState()
|
||||||
setIsSyncingWorkflowDraft(true)
|
setIsSyncingWorkflowDraft(true)
|
||||||
fetchWorkflowDraft(`/apps/${appId}/workflows/draft`).then((response) => {
|
fetchWorkflowDraft(`/apps/${appId}/workflows/draft`).then((response) => {
|
||||||
|
|
@ -78,6 +79,8 @@ export const useWorkflowUpdate = () => {
|
||||||
return acc
|
return acc
|
||||||
}, {} as Record<string, string>))
|
}, {} as Record<string, string>))
|
||||||
setEnvironmentVariables(response.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [])
|
setEnvironmentVariables(response.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [])
|
||||||
|
// #TODO chatVar sync#
|
||||||
|
setConversationVariables(response.conversation_variables || [])
|
||||||
}).finally(() => setIsSyncingWorkflowDraft(false))
|
}).finally(() => setIsSyncingWorkflowDraft(false))
|
||||||
}, [handleUpdateWorkflowCanvas, workflowStore])
|
}, [handleUpdateWorkflowCanvas, workflowStore])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,11 @@ export const useWorkflowStartRun = () => {
|
||||||
setShowDebugAndPreviewPanel,
|
setShowDebugAndPreviewPanel,
|
||||||
setHistoryWorkflowData,
|
setHistoryWorkflowData,
|
||||||
setShowEnvPanel,
|
setShowEnvPanel,
|
||||||
|
setShowChatVariablePanel,
|
||||||
} = workflowStore.getState()
|
} = workflowStore.getState()
|
||||||
|
|
||||||
setShowEnvPanel(false)
|
setShowEnvPanel(false)
|
||||||
|
setShowChatVariablePanel(false)
|
||||||
|
|
||||||
if (showDebugAndPreviewPanel)
|
if (showDebugAndPreviewPanel)
|
||||||
handleCancelDebugAndPreviewPanel()
|
handleCancelDebugAndPreviewPanel()
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import type {
|
||||||
export const useWorkflowVariables = () => {
|
export const useWorkflowVariables = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const environmentVariables = useStore(s => s.environmentVariables)
|
const environmentVariables = useStore(s => s.environmentVariables)
|
||||||
|
const conversationVariables = useStore(s => s.conversationVariables)
|
||||||
|
|
||||||
const getNodeAvailableVars = useCallback(({
|
const getNodeAvailableVars = useCallback(({
|
||||||
parentNode,
|
parentNode,
|
||||||
|
|
@ -19,12 +20,14 @@ export const useWorkflowVariables = () => {
|
||||||
isChatMode,
|
isChatMode,
|
||||||
filterVar,
|
filterVar,
|
||||||
hideEnv,
|
hideEnv,
|
||||||
|
hideChatVar,
|
||||||
}: {
|
}: {
|
||||||
parentNode?: Node | null
|
parentNode?: Node | null
|
||||||
beforeNodes: Node[]
|
beforeNodes: Node[]
|
||||||
isChatMode: boolean
|
isChatMode: boolean
|
||||||
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
||||||
hideEnv?: boolean
|
hideEnv?: boolean
|
||||||
|
hideChatVar?: boolean
|
||||||
}): NodeOutPutVar[] => {
|
}): NodeOutPutVar[] => {
|
||||||
return toNodeAvailableVars({
|
return toNodeAvailableVars({
|
||||||
parentNode,
|
parentNode,
|
||||||
|
|
@ -32,9 +35,10 @@ export const useWorkflowVariables = () => {
|
||||||
beforeNodes,
|
beforeNodes,
|
||||||
isChatMode,
|
isChatMode,
|
||||||
environmentVariables: hideEnv ? [] : environmentVariables,
|
environmentVariables: hideEnv ? [] : environmentVariables,
|
||||||
|
conversationVariables: (isChatMode && !hideChatVar) ? conversationVariables : [],
|
||||||
filterVar,
|
filterVar,
|
||||||
})
|
})
|
||||||
}, [environmentVariables, t])
|
}, [conversationVariables, environmentVariables, t])
|
||||||
|
|
||||||
const getCurrentVariableType = useCallback(({
|
const getCurrentVariableType = useCallback(({
|
||||||
parentNode,
|
parentNode,
|
||||||
|
|
@ -59,8 +63,9 @@ export const useWorkflowVariables = () => {
|
||||||
isChatMode,
|
isChatMode,
|
||||||
isConstant,
|
isConstant,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
|
conversationVariables,
|
||||||
})
|
})
|
||||||
}, [environmentVariables])
|
}, [conversationVariables, environmentVariables])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getNodeAvailableVars,
|
getNodeAvailableVars,
|
||||||
|
|
|
||||||
|
|
@ -478,6 +478,8 @@ export const useWorkflowInit = () => {
|
||||||
return acc
|
return acc
|
||||||
}, {} as Record<string, string>),
|
}, {} as Record<string, string>),
|
||||||
environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [],
|
environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [],
|
||||||
|
// #TODO chatVar sync#
|
||||||
|
conversationVariables: res.conversation_variables || [],
|
||||||
})
|
})
|
||||||
setSyncWorkflowDraftHash(res.hash)
|
setSyncWorkflowDraftHash(res.hash)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
|
|
@ -498,6 +500,7 @@ export const useWorkflowInit = () => {
|
||||||
retriever_resource: { enabled: true },
|
retriever_resource: { enabled: true },
|
||||||
},
|
},
|
||||||
environment_variables: [],
|
environment_variables: [],
|
||||||
|
conversation_variables: [],
|
||||||
},
|
},
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
workflowStore.getState().setDraftUpdatedAt(res.updated_at)
|
workflowStore.getState().setDraftUpdatedAt(res.updated_at)
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ const AddVariablePopupWithPosition = ({
|
||||||
} as any,
|
} as any,
|
||||||
],
|
],
|
||||||
hideEnv: true,
|
hideEnv: true,
|
||||||
|
hideChatVar: true,
|
||||||
isChatMode,
|
isChatMode,
|
||||||
filterVar: filterVar(outputType as VarType),
|
filterVar: filterVar(outputType as VarType),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import { useFeatures } from '@/app/components/base/features/hooks'
|
||||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||||
|
import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
payload: InputVar
|
payload: InputVar
|
||||||
|
|
@ -56,22 +58,24 @@ const FormItem: FC<Props> = ({
|
||||||
}, [value, onChange])
|
}, [value, onChange])
|
||||||
const nodeKey = (() => {
|
const nodeKey = (() => {
|
||||||
if (typeof payload.label === 'object') {
|
if (typeof payload.label === 'object') {
|
||||||
const { nodeType, nodeName, variable } = payload.label
|
const { nodeType, nodeName, variable, isChatVar } = payload.label
|
||||||
return (
|
return (
|
||||||
<div className='h-full flex items-center'>
|
<div className='h-full flex items-center'>
|
||||||
<div className='flex items-center'>
|
{!isChatVar && (
|
||||||
<div className='p-[1px]'>
|
<div className='flex items-center'>
|
||||||
<VarBlockIcon type={nodeType || BlockEnum.Start} />
|
<div className='p-[1px]'>
|
||||||
|
<VarBlockIcon type={nodeType || BlockEnum.Start} />
|
||||||
|
</div>
|
||||||
|
<div className='mx-0.5 text-xs font-medium text-gray-700 max-w-[150px] truncate' title={nodeName}>
|
||||||
|
{nodeName}
|
||||||
|
</div>
|
||||||
|
<Line3 className='mr-0.5'></Line3>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx-0.5 text-xs font-medium text-gray-700 max-w-[150px] truncate' title={nodeName}>
|
)}
|
||||||
{nodeName}
|
|
||||||
</div>
|
|
||||||
<Line3 className='mr-0.5'></Line3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex items-center text-primary-600'>
|
<div className='flex items-center text-primary-600'>
|
||||||
<Variable02 className='w-3.5 h-3.5' />
|
{!isChatVar && <Variable02 className='w-3.5 h-3.5' />}
|
||||||
<div className='ml-0.5 text-xs font-medium max-w-[150px] truncate' title={variable} >
|
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||||
|
<div className={cn('ml-0.5 text-xs font-medium max-w-[150px] truncate', isChatVar && 'text-text-secondary')} title={variable} >
|
||||||
{variable}
|
{variable}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -86,7 +90,12 @@ const FormItem: FC<Props> = ({
|
||||||
const isIterator = type === InputVarType.iterator
|
const isIterator = type === InputVarType.iterator
|
||||||
return (
|
return (
|
||||||
<div className={`${className}`}>
|
<div className={`${className}`}>
|
||||||
{!isArrayLikeType && <div className='h-8 leading-8 text-[13px] font-medium text-gray-700 truncate'>{typeof payload.label === 'object' ? nodeKey : payload.label}</div>}
|
{!isArrayLikeType && (
|
||||||
|
<div className='h-6 mb-1 flex items-center gap-1 text-text-secondary system-sm-semibold'>
|
||||||
|
<div className='truncate'>{typeof payload.label === 'object' ? nodeKey : payload.label}</div>
|
||||||
|
{!payload.required && <span className='text-text-tertiary system-xs-regular'>{t('workflow.panel.optional')}</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className='grow'>
|
<div className='grow'>
|
||||||
{
|
{
|
||||||
type === InputVarType.textInput && (
|
type === InputVarType.textInput && (
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const CODE_EDITOR_LINE_HEIGHT = 18
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
value?: string | object
|
value?: string | object
|
||||||
placeholder?: string
|
placeholder?: JSX.Element | string
|
||||||
onChange?: (value: string) => void
|
onChange?: (value: string) => void
|
||||||
title?: JSX.Element
|
title?: JSX.Element
|
||||||
language: CodeLanguage
|
language: CodeLanguage
|
||||||
|
|
@ -167,7 +167,7 @@ const CodeEditor: FC<Props> = ({
|
||||||
}}
|
}}
|
||||||
onMount={handleEditorDidMount}
|
onMount={handleEditorDidMount}
|
||||||
/>
|
/>
|
||||||
{!outPutValue && <div className='pointer-events-none absolute left-[36px] top-0 leading-[18px] text-[13px] font-normal text-gray-300'>{placeholder}</div>}
|
{!outPutValue && !isFocus && <div className='pointer-events-none absolute left-[36px] top-0 leading-[18px] text-[13px] font-normal text-gray-300'>{placeholder}</div>}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ type Props = {
|
||||||
justVar?: boolean
|
justVar?: boolean
|
||||||
nodesOutputVars?: NodeOutPutVar[]
|
nodesOutputVars?: NodeOutPutVar[]
|
||||||
availableNodes?: Node[]
|
availableNodes?: Node[]
|
||||||
|
insertVarTipToLeft?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Editor: FC<Props> = ({
|
const Editor: FC<Props> = ({
|
||||||
|
|
@ -40,6 +41,7 @@ const Editor: FC<Props> = ({
|
||||||
readOnly,
|
readOnly,
|
||||||
nodesOutputVars,
|
nodesOutputVars,
|
||||||
availableNodes = [],
|
availableNodes = [],
|
||||||
|
insertVarTipToLeft,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
|
@ -106,12 +108,12 @@ const Editor: FC<Props> = ({
|
||||||
{/* to patch Editor not support dynamic change editable status */}
|
{/* to patch Editor not support dynamic change editable status */}
|
||||||
{readOnly && <div className='absolute inset-0 z-10'></div>}
|
{readOnly && <div className='absolute inset-0 z-10'></div>}
|
||||||
{isFocus && (
|
{isFocus && (
|
||||||
<div className='absolute z-10 top-[-9px] right-1'>
|
<div className={cn('absolute z-10', insertVarTipToLeft ? 'top-1.5 left-[-12px]' : ' top-[-9px] right-1')}>
|
||||||
<TooltipPlus
|
<TooltipPlus
|
||||||
popupContent={`${t('workflow.common.insertVarTip')}`}
|
popupContent={`${t('workflow.common.insertVarTip')}`}
|
||||||
>
|
>
|
||||||
<div className='p-0.5 rounded-[5px] shadow-lg cursor-pointer bg-white hover:bg-gray-100 border-[0.5px] border-black/5'>
|
<div className='p-0.5 rounded-[5px] shadow-lg cursor-pointer bg-white hover:bg-gray-100 border-[0.5px] border-black/5'>
|
||||||
<Variable02 className='w-3.5 h-3.5 text-gray-500' />
|
<Variable02 className='w-3.5 h-3.5 text-components-button-secondary-accent-text' />
|
||||||
</div>
|
</div>
|
||||||
</TooltipPlus>
|
</TooltipPlus>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ const OptionCard: FC<Props> = ({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center px-2 h-8 rounded-md system-sm-regular bg-components-option-card-option-bg border border-components-option-card-option-bg text-text-secondary cursor-default',
|
'flex items-center px-2 h-8 rounded-md system-sm-regular bg-components-option-card-option-bg border border-components-option-card-option-border text-text-secondary cursor-default',
|
||||||
(!selected && !disabled) && 'hover:bg-components-option-card-option-bg-hover hover:border-components-option-card-option-border-hover hover:shadow-xs cursor-pointer',
|
(!selected && !disabled) && 'hover:bg-components-option-card-option-bg-hover hover:border-components-option-card-option-border-hover hover:shadow-xs cursor-pointer',
|
||||||
selected && 'bg-components-option-card-option-selected-bg border-[1.5px] border-components-option-card-option-selected-border system-sm-medium shadow-xs',
|
selected && 'bg-components-option-card-option-selected-bg border-[1.5px] border-components-option-card-option-selected-border system-sm-medium shadow-xs',
|
||||||
disabled && 'text-text-disabled',
|
disabled && 'text-text-disabled',
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ import cn from 'classnames'
|
||||||
import { useWorkflow } from '../../../hooks'
|
import { useWorkflow } from '../../../hooks'
|
||||||
import { BlockEnum } from '../../../types'
|
import { BlockEnum } from '../../../types'
|
||||||
import { VarBlockIcon } from '../../../block-icon'
|
import { VarBlockIcon } from '../../../block-icon'
|
||||||
import { getNodeInfoById, isENV, isSystemVar } from './variable/utils'
|
import { getNodeInfoById, isConversationVar, isENV, isSystemVar } from './variable/utils'
|
||||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||||
type Props = {
|
type Props = {
|
||||||
nodeId: string
|
nodeId: string
|
||||||
value: string
|
value: string
|
||||||
|
|
@ -42,13 +42,14 @@ const ReadonlyInputWithSelectVar: FC<Props> = ({
|
||||||
const value = vars[index].split('.')
|
const value = vars[index].split('.')
|
||||||
const isSystem = isSystemVar(value)
|
const isSystem = isSystemVar(value)
|
||||||
const isEnv = isENV(value)
|
const isEnv = isENV(value)
|
||||||
|
const isChatVar = isConversationVar(value)
|
||||||
const node = (isSystem ? startNode : getNodeInfoById(availableNodes, value[0]))?.data
|
const node = (isSystem ? startNode : getNodeInfoById(availableNodes, value[0]))?.data
|
||||||
const varName = `${isSystem ? 'sys.' : ''}${value[value.length - 1]}`
|
const varName = `${isSystem ? 'sys.' : ''}${value[value.length - 1]}`
|
||||||
|
|
||||||
return (<span key={index}>
|
return (<span key={index}>
|
||||||
<span className='relative top-[-3px] leading-[16px]'>{str}</span>
|
<span className='relative top-[-3px] leading-[16px]'>{str}</span>
|
||||||
<div className=' inline-flex h-[16px] items-center px-1.5 rounded-[5px] bg-white'>
|
<div className=' inline-flex h-[16px] items-center px-1.5 rounded-[5px] bg-white'>
|
||||||
{!isEnv && (
|
{!isEnv && !isChatVar && (
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<div className='p-[1px]'>
|
<div className='p-[1px]'>
|
||||||
<VarBlockIcon
|
<VarBlockIcon
|
||||||
|
|
@ -61,9 +62,10 @@ const ReadonlyInputWithSelectVar: FC<Props> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='flex items-center text-primary-600'>
|
<div className='flex items-center text-primary-600'>
|
||||||
{!isEnv && <Variable02 className='shrink-0 w-3.5 h-3.5' />}
|
{!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5' />}
|
||||||
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||||
<div className={cn('max-w-[50px] ml-0.5 text-xs font-medium truncate', isEnv && 'text-gray-900')} title={varName}>{varName}</div>
|
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||||
|
<div className={cn('max-w-[50px] ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && 'text-gray-900')} title={varName}>{varName}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</span>)
|
</span>)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ type Item = {
|
||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
type Props = {
|
type Props = {
|
||||||
|
className?: string
|
||||||
trigger?: JSX.Element
|
trigger?: JSX.Element
|
||||||
DropDownIcon?: any
|
DropDownIcon?: any
|
||||||
noLeft?: boolean
|
noLeft?: boolean
|
||||||
|
|
@ -27,6 +28,7 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TypeSelector: FC<Props> = ({
|
const TypeSelector: FC<Props> = ({
|
||||||
|
className,
|
||||||
trigger,
|
trigger,
|
||||||
DropDownIcon = ChevronSelectorVertical,
|
DropDownIcon = ChevronSelectorVertical,
|
||||||
noLeft,
|
noLeft,
|
||||||
|
|
@ -50,11 +52,12 @@ const TypeSelector: FC<Props> = ({
|
||||||
setHide()
|
setHide()
|
||||||
}, ref)
|
}, ref)
|
||||||
return (
|
return (
|
||||||
<div className={cn(!trigger && !noLeft && 'left-[-8px]', 'relative')} ref={ref}>
|
<div className={cn(!trigger && !noLeft && 'left-[-8px]', 'relative select-none', className)} ref={ref}>
|
||||||
{trigger
|
{trigger
|
||||||
? (
|
? (
|
||||||
<div
|
<div
|
||||||
onClick={toggleShow}
|
onClick={toggleShow}
|
||||||
|
className={cn(!readonly && 'cursor-pointer')}
|
||||||
>
|
>
|
||||||
{trigger}
|
{trigger}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -63,13 +66,13 @@ const TypeSelector: FC<Props> = ({
|
||||||
<div
|
<div
|
||||||
onClick={toggleShow}
|
onClick={toggleShow}
|
||||||
className={cn(showOption && 'bg-black/5', 'flex items-center h-5 pl-1 pr-0.5 rounded-md text-xs font-semibold text-gray-700 cursor-pointer hover:bg-black/5')}>
|
className={cn(showOption && 'bg-black/5', 'flex items-center h-5 pl-1 pr-0.5 rounded-md text-xs font-semibold text-gray-700 cursor-pointer hover:bg-black/5')}>
|
||||||
<div className={cn(triggerClassName, 'text-xs font-semibold', uppercase && 'uppercase', noValue && 'text-gray-400')}>{!noValue ? item?.label : placeholder}</div>
|
<div className={cn('text-sm font-semibold', uppercase && 'uppercase', noValue && 'text-gray-400', triggerClassName)}>{!noValue ? item?.label : placeholder}</div>
|
||||||
{!readonly && <DropDownIcon className='w-3 h-3 ' />}
|
{!readonly && <DropDownIcon className='w-3 h-3 ' />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(showOption && !readonly) && (
|
{(showOption && !readonly) && (
|
||||||
<div className={cn(popupClassName, 'absolute z-10 top-[24px] w-[120px] p-1 border border-gray-200 shadow-lg rounded-lg bg-white')}>
|
<div className={cn('absolute z-10 top-[24px] w-[120px] p-1 border border-gray-200 shadow-lg rounded-lg bg-white select-none', popupClassName)}>
|
||||||
{list.map(item => (
|
{list.map(item => (
|
||||||
<div
|
<div
|
||||||
key={item.value}
|
key={item.value}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ import type {
|
||||||
import { BlockEnum } from '@/app/components/workflow/types'
|
import { BlockEnum } from '@/app/components/workflow/types'
|
||||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||||
import { isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
type VariableTagProps = {
|
type VariableTagProps = {
|
||||||
|
|
@ -30,12 +30,13 @@ const VariableTag = ({
|
||||||
return nodes.find(node => node.id === valueSelector[0])
|
return nodes.find(node => node.id === valueSelector[0])
|
||||||
}, [nodes, valueSelector])
|
}, [nodes, valueSelector])
|
||||||
const isEnv = isENV(valueSelector)
|
const isEnv = isENV(valueSelector)
|
||||||
|
const isChatVar = isConversationVar(valueSelector)
|
||||||
|
|
||||||
const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
|
const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='inline-flex items-center px-1.5 max-w-full h-6 text-xs rounded-md border-[0.5px] border-[rgba(16, 2440,0.08)] bg-white shadow-xs'>
|
<div className='inline-flex items-center px-1.5 max-w-full h-6 text-xs rounded-md border-[0.5px] border-[rgba(16, 2440,0.08)] bg-white shadow-xs'>
|
||||||
{!isEnv && (
|
{!isEnv && !isChatVar && (
|
||||||
<>
|
<>
|
||||||
{node && (
|
{node && (
|
||||||
<VarBlockIcon
|
<VarBlockIcon
|
||||||
|
|
@ -54,8 +55,9 @@ const VariableTag = ({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isEnv && <Env className='shrink-0 mr-0.5 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
{isEnv && <Env className='shrink-0 mr-0.5 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||||
|
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||||
<div
|
<div
|
||||||
className={cn('truncate text-text-accent font-medium', isEnv && 'text-text-secondary')}
|
className={cn('truncate text-text-accent font-medium', (isEnv || isChatVar) && 'text-text-secondary')}
|
||||||
title={variableName}
|
title={variableName}
|
||||||
>
|
>
|
||||||
{variableName}
|
{variableName}
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@ import type { Var } from '@/app/components/workflow/types'
|
||||||
import { SimpleSelect } from '@/app/components/base/select'
|
import { SimpleSelect } from '@/app/components/base/select'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
schema: CredentialFormSchema
|
schema: Partial<CredentialFormSchema>
|
||||||
readonly: boolean
|
readonly: boolean
|
||||||
value: string
|
value: string
|
||||||
onChange: (value: string | number, varKindType: VarKindType, varInfo?: Var) => void
|
onChange: (value: string | number, varKindType: VarKindType, varInfo?: Var) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConstantField: FC<Props> = ({
|
const ConstantField: FC<Props> = ({
|
||||||
schema,
|
schema = {} as CredentialFormSchema,
|
||||||
readonly,
|
readonly,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
|
@ -47,7 +47,7 @@ const ConstantField: FC<Props> = ({
|
||||||
{schema.type === FormTypeEnum.textNumber && (
|
{schema.type === FormTypeEnum.textNumber && (
|
||||||
<input
|
<input
|
||||||
type='number'
|
type='number'
|
||||||
className='w-full h-8 leading-8 pl-0.5 bg-transparent text-[13px] font-normal text-gray-900 placeholder:text-gray-400 focus:outline-none overflow-hidden'
|
className='w-full h-8 leading-8 p-2 rounded-lg bg-gray-100 text-[13px] font-normal text-gray-900 placeholder:text-gray-400 focus:outline-none overflow-hidden'
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleStaticChange}
|
onChange={handleStaticChange}
|
||||||
readOnly={readonly}
|
readOnly={readonly}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import type { ParameterExtractorNodeType } from '../../../parameter-extractor/ty
|
||||||
import type { IterationNodeType } from '../../../iteration/types'
|
import type { IterationNodeType } from '../../../iteration/types'
|
||||||
import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types'
|
import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types'
|
||||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||||
import type { EnvironmentVariable, Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
import type { ConversationVariable, EnvironmentVariable, Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||||
import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types'
|
import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types'
|
||||||
import {
|
import {
|
||||||
HTTP_REQUEST_OUTPUT_STRUCT,
|
HTTP_REQUEST_OUTPUT_STRUCT,
|
||||||
|
|
@ -38,6 +38,10 @@ export const isENV = (valueSelector: ValueSelector) => {
|
||||||
return valueSelector[0] === 'env'
|
return valueSelector[0] === 'env'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isConversationVar = (valueSelector: ValueSelector) => {
|
||||||
|
return valueSelector[0] === 'conversation'
|
||||||
|
}
|
||||||
|
|
||||||
const inputVarTypeToVarType = (type: InputVarType): VarType => {
|
const inputVarTypeToVarType = (type: InputVarType): VarType => {
|
||||||
if (type === InputVarType.number)
|
if (type === InputVarType.number)
|
||||||
return VarType.number
|
return VarType.number
|
||||||
|
|
@ -246,13 +250,32 @@ const formatItem = (
|
||||||
}) as Var[]
|
}) as Var[]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'conversation': {
|
||||||
|
res.vars = data.chatVarList.map((chatVar: ConversationVariable) => {
|
||||||
|
return {
|
||||||
|
variable: `conversation.${chatVar.name}`,
|
||||||
|
type: chatVar.value_type,
|
||||||
|
des: chatVar.description,
|
||||||
|
}
|
||||||
|
}) as Var[]
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selector = [id]
|
const selector = [id]
|
||||||
res.vars = res.vars.filter((v) => {
|
res.vars = res.vars.filter((v) => {
|
||||||
const { children } = v
|
const { children } = v
|
||||||
if (!children)
|
if (!children) {
|
||||||
return filterVar(v, selector)
|
return filterVar(v, (() => {
|
||||||
|
const variableArr = v.variable.split('.')
|
||||||
|
const [first, ..._other] = variableArr
|
||||||
|
if (first === 'sys' || first === 'env' || first === 'conversation')
|
||||||
|
return variableArr
|
||||||
|
|
||||||
|
return [...selector, ...variableArr]
|
||||||
|
})())
|
||||||
|
}
|
||||||
|
|
||||||
const obj = findExceptVarInObject(v, filterVar, selector)
|
const obj = findExceptVarInObject(v, filterVar, selector)
|
||||||
return obj?.children && obj?.children.length > 0
|
return obj?.children && obj?.children.length > 0
|
||||||
|
|
@ -271,6 +294,7 @@ export const toNodeOutputVars = (
|
||||||
isChatMode: boolean,
|
isChatMode: boolean,
|
||||||
filterVar = (_payload: Var, _selector: ValueSelector) => true,
|
filterVar = (_payload: Var, _selector: ValueSelector) => true,
|
||||||
environmentVariables: EnvironmentVariable[] = [],
|
environmentVariables: EnvironmentVariable[] = [],
|
||||||
|
conversationVariables: ConversationVariable[] = [],
|
||||||
): NodeOutPutVar[] => {
|
): NodeOutPutVar[] => {
|
||||||
// ENV_NODE data format
|
// ENV_NODE data format
|
||||||
const ENV_NODE = {
|
const ENV_NODE = {
|
||||||
|
|
@ -281,9 +305,19 @@ export const toNodeOutputVars = (
|
||||||
envList: environmentVariables,
|
envList: environmentVariables,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
// CHAT_VAR_NODE data format
|
||||||
|
const CHAT_VAR_NODE = {
|
||||||
|
id: 'conversation',
|
||||||
|
data: {
|
||||||
|
title: 'CONVERSATION',
|
||||||
|
type: 'conversation',
|
||||||
|
chatVarList: conversationVariables,
|
||||||
|
},
|
||||||
|
}
|
||||||
const res = [
|
const res = [
|
||||||
...nodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node.data.type)),
|
...nodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node.data.type)),
|
||||||
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
|
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
|
||||||
|
...((isChatMode && conversationVariables.length) > 0 ? [CHAT_VAR_NODE] : []),
|
||||||
].map((node) => {
|
].map((node) => {
|
||||||
return {
|
return {
|
||||||
...formatItem(node, isChatMode, filterVar),
|
...formatItem(node, isChatMode, filterVar),
|
||||||
|
|
@ -348,6 +382,7 @@ export const getVarType = ({
|
||||||
isChatMode,
|
isChatMode,
|
||||||
isConstant,
|
isConstant,
|
||||||
environmentVariables = [],
|
environmentVariables = [],
|
||||||
|
conversationVariables = [],
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
valueSelector: ValueSelector
|
valueSelector: ValueSelector
|
||||||
|
|
@ -357,6 +392,7 @@ export const getVarType = ({
|
||||||
isChatMode: boolean
|
isChatMode: boolean
|
||||||
isConstant?: boolean
|
isConstant?: boolean
|
||||||
environmentVariables?: EnvironmentVariable[]
|
environmentVariables?: EnvironmentVariable[]
|
||||||
|
conversationVariables?: ConversationVariable[]
|
||||||
}): VarType => {
|
}): VarType => {
|
||||||
if (isConstant)
|
if (isConstant)
|
||||||
return VarType.string
|
return VarType.string
|
||||||
|
|
@ -366,6 +402,7 @@ export const getVarType = ({
|
||||||
isChatMode,
|
isChatMode,
|
||||||
undefined,
|
undefined,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
|
conversationVariables,
|
||||||
)
|
)
|
||||||
|
|
||||||
const isIterationInnerVar = parentNode?.data.type === BlockEnum.Iteration
|
const isIterationInnerVar = parentNode?.data.type === BlockEnum.Iteration
|
||||||
|
|
@ -388,6 +425,7 @@ export const getVarType = ({
|
||||||
}
|
}
|
||||||
const isSystem = isSystemVar(valueSelector)
|
const isSystem = isSystemVar(valueSelector)
|
||||||
const isEnv = isENV(valueSelector)
|
const isEnv = isENV(valueSelector)
|
||||||
|
const isChatVar = isConversationVar(valueSelector)
|
||||||
const startNode = availableNodes.find((node: any) => {
|
const startNode = availableNodes.find((node: any) => {
|
||||||
return node.data.type === BlockEnum.Start
|
return node.data.type === BlockEnum.Start
|
||||||
})
|
})
|
||||||
|
|
@ -400,7 +438,7 @@ export const getVarType = ({
|
||||||
|
|
||||||
let type: VarType = VarType.string
|
let type: VarType = VarType.string
|
||||||
let curr: any = targetVar.vars
|
let curr: any = targetVar.vars
|
||||||
if (isSystem || isEnv) {
|
if (isSystem || isEnv || isChatVar) {
|
||||||
return curr.find((v: any) => v.variable === (valueSelector as ValueSelector).join('.'))?.type
|
return curr.find((v: any) => v.variable === (valueSelector as ValueSelector).join('.'))?.type
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
@ -426,6 +464,7 @@ export const toNodeAvailableVars = ({
|
||||||
beforeNodes,
|
beforeNodes,
|
||||||
isChatMode,
|
isChatMode,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
|
conversationVariables,
|
||||||
filterVar,
|
filterVar,
|
||||||
}: {
|
}: {
|
||||||
parentNode?: Node | null
|
parentNode?: Node | null
|
||||||
|
|
@ -435,6 +474,8 @@ export const toNodeAvailableVars = ({
|
||||||
isChatMode: boolean
|
isChatMode: boolean
|
||||||
// env
|
// env
|
||||||
environmentVariables?: EnvironmentVariable[]
|
environmentVariables?: EnvironmentVariable[]
|
||||||
|
// chat var
|
||||||
|
conversationVariables?: ConversationVariable[]
|
||||||
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
||||||
}): NodeOutPutVar[] => {
|
}): NodeOutPutVar[] => {
|
||||||
const beforeNodesOutputVars = toNodeOutputVars(
|
const beforeNodesOutputVars = toNodeOutputVars(
|
||||||
|
|
@ -442,6 +483,7 @@ export const toNodeAvailableVars = ({
|
||||||
isChatMode,
|
isChatMode,
|
||||||
filterVar,
|
filterVar,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
|
conversationVariables,
|
||||||
)
|
)
|
||||||
const isInIteration = parentNode?.data.type === BlockEnum.Iteration
|
const isInIteration = parentNode?.data.type === BlockEnum.Iteration
|
||||||
if (isInIteration) {
|
if (isInIteration) {
|
||||||
|
|
@ -453,6 +495,7 @@ export const toNodeAvailableVars = ({
|
||||||
availableNodes: beforeNodes,
|
availableNodes: beforeNodes,
|
||||||
isChatMode,
|
isChatMode,
|
||||||
environmentVariables,
|
environmentVariables,
|
||||||
|
conversationVariables,
|
||||||
})
|
})
|
||||||
const iterationVar = {
|
const iterationVar = {
|
||||||
nodeId: iterationNode?.id,
|
nodeId: iterationNode?.id,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
import { useStoreApi } from 'reactflow'
|
import { useStoreApi } from 'reactflow'
|
||||||
import VarReferencePopup from './var-reference-popup'
|
import VarReferencePopup from './var-reference-popup'
|
||||||
import { getNodeInfoById, isENV, isSystemVar } from './utils'
|
import { getNodeInfoById, isConversationVar, isENV, isSystemVar } from './utils'
|
||||||
import ConstantField from './constant-field'
|
import ConstantField from './constant-field'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||||
|
|
@ -17,7 +17,7 @@ import type { CredentialFormSchema } from '@/app/components/header/account-setti
|
||||||
import { BlockEnum } from '@/app/components/workflow/types'
|
import { BlockEnum } from '@/app/components/workflow/types'
|
||||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||||
import {
|
import {
|
||||||
PortalToFollowElem,
|
PortalToFollowElem,
|
||||||
|
|
@ -32,6 +32,7 @@ import {
|
||||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||||
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
|
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
|
||||||
import AddButton from '@/app/components/base/button/add-button'
|
import AddButton from '@/app/components/base/button/add-button'
|
||||||
|
import Badge from '@/app/components/base/badge'
|
||||||
const TRIGGER_DEFAULT_WIDTH = 227
|
const TRIGGER_DEFAULT_WIDTH = 227
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -49,7 +50,8 @@ type Props = {
|
||||||
availableNodes?: Node[]
|
availableNodes?: Node[]
|
||||||
availableVars?: NodeOutPutVar[]
|
availableVars?: NodeOutPutVar[]
|
||||||
isAddBtnTrigger?: boolean
|
isAddBtnTrigger?: boolean
|
||||||
schema?: CredentialFormSchema
|
schema?: Partial<CredentialFormSchema>
|
||||||
|
valueTypePlaceHolder?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const VarReferencePicker: FC<Props> = ({
|
const VarReferencePicker: FC<Props> = ({
|
||||||
|
|
@ -57,7 +59,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||||
readonly,
|
readonly,
|
||||||
className,
|
className,
|
||||||
isShowNodeName,
|
isShowNodeName,
|
||||||
value,
|
value = [],
|
||||||
onOpen = () => { },
|
onOpen = () => { },
|
||||||
onChange,
|
onChange,
|
||||||
isSupportConstantValue,
|
isSupportConstantValue,
|
||||||
|
|
@ -68,6 +70,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||||
availableVars,
|
availableVars,
|
||||||
isAddBtnTrigger,
|
isAddBtnTrigger,
|
||||||
schema,
|
schema,
|
||||||
|
valueTypePlaceHolder,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const store = useStoreApi()
|
const store = useStoreApi()
|
||||||
|
|
@ -99,7 +102,6 @@ const VarReferencePicker: FC<Props> = ({
|
||||||
|
|
||||||
const [varKindType, setVarKindType] = useState<VarKindType>(defaultVarKindType)
|
const [varKindType, setVarKindType] = useState<VarKindType>(defaultVarKindType)
|
||||||
const isConstant = isSupportConstantValue && varKindType === VarKindType.constant
|
const isConstant = isSupportConstantValue && varKindType === VarKindType.constant
|
||||||
|
|
||||||
const outputVars = useMemo(() => {
|
const outputVars = useMemo(() => {
|
||||||
if (availableVars)
|
if (availableVars)
|
||||||
return availableVars
|
return availableVars
|
||||||
|
|
@ -215,6 +217,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||||
})
|
})
|
||||||
|
|
||||||
const isEnv = isENV(value as ValueSelector)
|
const isEnv = isENV(value as ValueSelector)
|
||||||
|
const isChatVar = isConversationVar(value as ValueSelector)
|
||||||
|
|
||||||
// 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff
|
// 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff
|
||||||
const availableWidth = triggerWidth - 56
|
const availableWidth = triggerWidth - 56
|
||||||
|
|
@ -227,6 +230,8 @@ const VarReferencePicker: FC<Props> = ({
|
||||||
return [maxNodeNameWidth, maxVarNameWidth, maxTypeWidth]
|
return [maxNodeNameWidth, maxVarNameWidth, maxTypeWidth]
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
const WrapElem = isSupportConstantValue ? 'div' : PortalToFollowElemTrigger
|
||||||
|
const VarPickerWrap = !isSupportConstantValue ? 'div' : PortalToFollowElemTrigger
|
||||||
return (
|
return (
|
||||||
<div className={cn(className, !readonly && 'cursor-pointer')}>
|
<div className={cn(className, !readonly && 'cursor-pointer')}>
|
||||||
<PortalToFollowElem
|
<PortalToFollowElem
|
||||||
|
|
@ -234,7 +239,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||||
onOpenChange={setOpen}
|
onOpenChange={setOpen}
|
||||||
placement={isAddBtnTrigger ? 'bottom-end' : 'bottom-start'}
|
placement={isAddBtnTrigger ? 'bottom-end' : 'bottom-start'}
|
||||||
>
|
>
|
||||||
<PortalToFollowElemTrigger onClick={() => {
|
<WrapElem onClick={() => {
|
||||||
if (readonly)
|
if (readonly)
|
||||||
return
|
return
|
||||||
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
|
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
|
||||||
|
|
@ -245,23 +250,28 @@ const VarReferencePicker: FC<Props> = ({
|
||||||
<AddButton onClick={() => { }}></AddButton>
|
<AddButton onClick={() => { }}></AddButton>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: (<div ref={triggerRef} className={cn((open || isFocus) ? 'border-gray-300' : 'border-gray-100', 'relative group/wrap flex items-center w-full h-8 p-1 rounded-lg bg-gray-100 border')}>
|
: (<div ref={!isSupportConstantValue ? triggerRef : null} className={cn((open || isFocus) ? 'border-gray-300' : 'border-gray-100', 'relative group/wrap flex items-center w-full h-8', !isSupportConstantValue && 'p-1 rounded-lg bg-gray-100 border')}>
|
||||||
{isSupportConstantValue
|
{isSupportConstantValue
|
||||||
? <div onClick={(e) => {
|
? <div onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
setControlFocus(Date.now())
|
setControlFocus(Date.now())
|
||||||
}} className='mr-1 flex items-center space-x-1'>
|
}} className='h-full mr-1 flex items-center space-x-1'>
|
||||||
<TypeSelector
|
<TypeSelector
|
||||||
noLeft
|
noLeft
|
||||||
triggerClassName='!text-xs'
|
trigger={
|
||||||
|
<div className='flex items-center h-8 px-2 radius-md bg-components-input-bg-normal'>
|
||||||
|
<div className='mr-1 system-sm-regular text-components-input-text-filled'>{varKindTypes.find(item => item.value === varKindType)?.label}</div>
|
||||||
|
<RiArrowDownSLine className='w-4 h-4 text-text-quaternary' />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
popupClassName='top-8'
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
DropDownIcon={RiArrowDownSLine}
|
|
||||||
value={varKindType}
|
value={varKindType}
|
||||||
options={varKindTypes}
|
options={varKindTypes}
|
||||||
onChange={handleVarKindTypeChange}
|
onChange={handleVarKindTypeChange}
|
||||||
|
showChecked
|
||||||
/>
|
/>
|
||||||
<div className='h-4 w-px bg-black/5'></div>
|
|
||||||
</div>
|
</div>
|
||||||
: (!hasValue && <div className='ml-1.5 mr-1'>
|
: (!hasValue && <div className='ml-1.5 mr-1'>
|
||||||
<Variable02 className='w-3.5 h-3.5 text-gray-400' />
|
<Variable02 className='w-3.5 h-3.5 text-gray-400' />
|
||||||
|
|
@ -276,38 +286,51 @@ const VarReferencePicker: FC<Props> = ({
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<div className={cn('inline-flex h-full items-center px-1.5 rounded-[5px]', hasValue && 'bg-white')}>
|
<VarPickerWrap
|
||||||
{hasValue
|
onClick={() => {
|
||||||
? (
|
if (readonly)
|
||||||
<>
|
return
|
||||||
{isShowNodeName && !isEnv && (
|
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
|
||||||
<div className='flex items-center'>
|
}}
|
||||||
<div className='p-[1px]'>
|
className='grow h-full'
|
||||||
<VarBlockIcon
|
>
|
||||||
className='!text-gray-900'
|
<div ref={isSupportConstantValue ? triggerRef : null} className={cn('h-full', isSupportConstantValue && 'flex items-center pl-1 py-1 rounded-lg bg-gray-100')}>
|
||||||
type={outputVarNode?.type || BlockEnum.Start}
|
<div className={cn('h-full items-center px-1.5 rounded-[5px]', hasValue ? 'bg-white inline-flex' : 'flex')}>
|
||||||
/>
|
{hasValue
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
{isShowNodeName && !isEnv && !isChatVar && (
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<div className='p-[1px]'>
|
||||||
|
<VarBlockIcon
|
||||||
|
className='!text-gray-900'
|
||||||
|
type={outputVarNode?.type || BlockEnum.Start}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='mx-0.5 text-xs font-medium text-gray-700 truncate' title={outputVarNode?.title} style={{
|
||||||
|
maxWidth: maxNodeNameWidth,
|
||||||
|
}}>{outputVarNode?.title}</div>
|
||||||
|
<Line3 className='mr-0.5'></Line3>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='flex items-center text-primary-600'>
|
||||||
|
{!hasValue && <Variable02 className='w-3.5 h-3.5' />}
|
||||||
|
{isEnv && <Env className='w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||||
|
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||||
|
<div className={cn('ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && '!text-text-secondary')} title={varName} style={{
|
||||||
|
maxWidth: maxVarNameWidth,
|
||||||
|
}}>{varName}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx-0.5 text-xs font-medium text-gray-700 truncate' title={outputVarNode?.title} style={{
|
<div className='ml-0.5 text-xs font-normal text-gray-500 capitalize truncate' title={type} style={{
|
||||||
maxWidth: maxNodeNameWidth,
|
maxWidth: maxTypeWidth,
|
||||||
}}>{outputVarNode?.title}</div>
|
}}>{type}</div>
|
||||||
<Line3 className='mr-0.5'></Line3>
|
</>
|
||||||
</div>
|
)
|
||||||
)}
|
: <div className='text-[13px] font-normal text-gray-400'>{t('workflow.common.setVarValuePlaceholder')}</div>}
|
||||||
<div className='flex items-center text-primary-600'>
|
</div>
|
||||||
{!hasValue && <Variable02 className='w-3.5 h-3.5' />}
|
</div>
|
||||||
{isEnv && <Env className='w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
|
||||||
<div className={cn('ml-0.5 text-xs font-medium truncate', isEnv && '!text-gray-900')} title={varName} style={{
|
</VarPickerWrap>
|
||||||
maxWidth: maxVarNameWidth,
|
|
||||||
}}>{varName}</div>
|
|
||||||
</div>
|
|
||||||
<div className='ml-0.5 text-xs font-normal text-gray-500 capitalize truncate' title={type} style={{
|
|
||||||
maxWidth: maxTypeWidth,
|
|
||||||
}}>{type}</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
: <div className='text-[13px] font-normal text-gray-400'>{t('workflow.common.setVarValuePlaceholder')}</div>}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{(hasValue && !readonly) && (<div
|
{(hasValue && !readonly) && (<div
|
||||||
className='invisible group-hover/wrap:visible absolute h-5 right-1 top-[50%] translate-y-[-50%] group p-1 rounded-md hover:bg-black/5 cursor-pointer'
|
className='invisible group-hover/wrap:visible absolute h-5 right-1 top-[50%] translate-y-[-50%] group p-1 rounded-md hover:bg-black/5 cursor-pointer'
|
||||||
|
|
@ -315,8 +338,15 @@ const VarReferencePicker: FC<Props> = ({
|
||||||
>
|
>
|
||||||
<RiCloseLine className='w-3.5 h-3.5 text-gray-500 group-hover:text-gray-800' />
|
<RiCloseLine className='w-3.5 h-3.5 text-gray-500 group-hover:text-gray-800' />
|
||||||
</div>)}
|
</div>)}
|
||||||
|
{!hasValue && valueTypePlaceHolder && (
|
||||||
|
<Badge
|
||||||
|
className=' absolute right-1 top-[50%] translate-y-[-50%] capitalize'
|
||||||
|
text={valueTypePlaceHolder}
|
||||||
|
uppercase={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>)}
|
</div>)}
|
||||||
</PortalToFollowElemTrigger>
|
</WrapElem>
|
||||||
<PortalToFollowElemContent style={{
|
<PortalToFollowElemContent style={{
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
}}>
|
}}>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import {
|
||||||
PortalToFollowElemTrigger,
|
PortalToFollowElemTrigger,
|
||||||
} from '@/app/components/base/portal-to-follow-elem'
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||||
import { checkKeys } from '@/utils/var'
|
import { checkKeys } from '@/utils/var'
|
||||||
|
|
||||||
type ObjectChildrenProps = {
|
type ObjectChildrenProps = {
|
||||||
|
|
@ -51,6 +51,7 @@ const Item: FC<ItemProps> = ({
|
||||||
const isObj = itemData.type === VarType.object && itemData.children && itemData.children.length > 0
|
const isObj = itemData.type === VarType.object && itemData.children && itemData.children.length > 0
|
||||||
const isSys = itemData.variable.startsWith('sys.')
|
const isSys = itemData.variable.startsWith('sys.')
|
||||||
const isEnv = itemData.variable.startsWith('env.')
|
const isEnv = itemData.variable.startsWith('env.')
|
||||||
|
const isChatVar = itemData.variable.startsWith('conversation.')
|
||||||
const itemRef = useRef(null)
|
const itemRef = useRef(null)
|
||||||
const [isItemHovering, setIsItemHovering] = useState(false)
|
const [isItemHovering, setIsItemHovering] = useState(false)
|
||||||
const _ = useHover(itemRef, {
|
const _ = useHover(itemRef, {
|
||||||
|
|
@ -79,7 +80,7 @@ const Item: FC<ItemProps> = ({
|
||||||
}, [isHovering])
|
}, [isHovering])
|
||||||
const handleChosen = (e: React.MouseEvent) => {
|
const handleChosen = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (isSys || isEnv) { // system variable or environment variable
|
if (isSys || isEnv || isChatVar) { // system variable | environment variable | conversation variable
|
||||||
onChange([...objPath, ...itemData.variable.split('.')], itemData)
|
onChange([...objPath, ...itemData.variable.split('.')], itemData)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
@ -100,13 +101,21 @@ const Item: FC<ItemProps> = ({
|
||||||
isHovering && (isObj ? 'bg-primary-50' : 'bg-gray-50'),
|
isHovering && (isObj ? 'bg-primary-50' : 'bg-gray-50'),
|
||||||
'relative w-full flex items-center h-6 pl-3 rounded-md cursor-pointer')
|
'relative w-full flex items-center h-6 pl-3 rounded-md cursor-pointer')
|
||||||
}
|
}
|
||||||
// style={{ width: itemWidth || 252 }}
|
|
||||||
onClick={handleChosen}
|
onClick={handleChosen}
|
||||||
>
|
>
|
||||||
<div className='flex items-center w-0 grow'>
|
<div className='flex items-center w-0 grow'>
|
||||||
{!isEnv && <Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />}
|
{!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />}
|
||||||
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||||
<div title={itemData.variable} className='ml-1 w-0 grow truncate text-[13px] font-normal text-gray-900'>{!isEnv ? itemData.variable : itemData.variable.replace('env.', '')}</div>
|
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||||
|
{!isEnv && !isChatVar && (
|
||||||
|
<div title={itemData.variable} className='ml-1 w-0 grow truncate text-[13px] font-normal text-gray-900'>{itemData.variable}</div>
|
||||||
|
)}
|
||||||
|
{isEnv && (
|
||||||
|
<div title={itemData.variable} className='ml-1 w-0 grow truncate text-[13px] font-normal text-gray-900'>{itemData.variable.replace('env.', '')}</div>
|
||||||
|
)}
|
||||||
|
{isChatVar && (
|
||||||
|
<div title={itemData.des} className='ml-1 w-0 grow truncate text-[13px] font-normal text-gray-900'>{itemData.variable.replace('conversation.', '')}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='ml-1 shrink-0 text-xs font-normal text-gray-500 capitalize'>{itemData.type}</div>
|
<div className='ml-1 shrink-0 text-xs font-normal text-gray-500 capitalize'>{itemData.type}</div>
|
||||||
{isObj && (
|
{isObj && (
|
||||||
|
|
@ -211,7 +220,7 @@ const VarReferenceVars: FC<Props> = ({
|
||||||
const [searchText, setSearchText] = useState('')
|
const [searchText, setSearchText] = useState('')
|
||||||
|
|
||||||
const filteredVars = vars.filter((v) => {
|
const filteredVars = vars.filter((v) => {
|
||||||
const children = v.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.') || v.variable.startsWith('env.'))
|
const children = v.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.') || v.variable.startsWith('env.') || v.variable.startsWith('conversation.'))
|
||||||
return children.length > 0
|
return children.length > 0
|
||||||
}).filter((node) => {
|
}).filter((node) => {
|
||||||
if (!searchText)
|
if (!searchText)
|
||||||
|
|
@ -222,7 +231,7 @@ const VarReferenceVars: FC<Props> = ({
|
||||||
})
|
})
|
||||||
return children.length > 0
|
return children.length > 0
|
||||||
}).map((node) => {
|
}).map((node) => {
|
||||||
let vars = node.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.') || v.variable.startsWith('env.'))
|
let vars = node.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.') || v.variable.startsWith('env.') || v.variable.startsWith('conversation.'))
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
const searchTextLower = searchText.toLowerCase()
|
const searchTextLower = searchText.toLowerCase()
|
||||||
if (!node.title.toLowerCase().includes(searchTextLower))
|
if (!node.title.toLowerCase().includes(searchTextLower))
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
|
||||||
[BlockEnum.TemplateTransform]: 'template',
|
[BlockEnum.TemplateTransform]: 'template',
|
||||||
[BlockEnum.VariableAssigner]: 'variable_assigner',
|
[BlockEnum.VariableAssigner]: 'variable_assigner',
|
||||||
[BlockEnum.VariableAggregator]: 'variable_assigner',
|
[BlockEnum.VariableAggregator]: 'variable_assigner',
|
||||||
|
[BlockEnum.Assigner]: 'variable_assignment',
|
||||||
[BlockEnum.Iteration]: 'iteration',
|
[BlockEnum.Iteration]: 'iteration',
|
||||||
[BlockEnum.ParameterExtractor]: 'parameter_extractor',
|
[BlockEnum.ParameterExtractor]: 'parameter_extractor',
|
||||||
[BlockEnum.HttpRequest]: 'http_request',
|
[BlockEnum.HttpRequest]: 'http_request',
|
||||||
|
|
@ -43,6 +44,7 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
|
||||||
[BlockEnum.TemplateTransform]: 'template',
|
[BlockEnum.TemplateTransform]: 'template',
|
||||||
[BlockEnum.VariableAssigner]: 'variable-assigner',
|
[BlockEnum.VariableAssigner]: 'variable-assigner',
|
||||||
[BlockEnum.VariableAggregator]: 'variable-assigner',
|
[BlockEnum.VariableAggregator]: 'variable-assigner',
|
||||||
|
[BlockEnum.Assigner]: 'variable-assignment',
|
||||||
[BlockEnum.Iteration]: 'iteration',
|
[BlockEnum.Iteration]: 'iteration',
|
||||||
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
|
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
|
||||||
[BlockEnum.HttpRequest]: 'http-request',
|
[BlockEnum.HttpRequest]: 'http-request',
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@ import {
|
||||||
useNodeDataUpdate,
|
useNodeDataUpdate,
|
||||||
useWorkflow,
|
useWorkflow,
|
||||||
} from '@/app/components/workflow/hooks'
|
} from '@/app/components/workflow/hooks'
|
||||||
import { getNodeInfoById, isENV, isSystemVar, toNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
import { getNodeInfoById, isConversationVar, isENV, isSystemVar, toNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||||
|
|
||||||
import type { CommonNodeType, InputVar, ValueSelector, Var, Variable } from '@/app/components/workflow/types'
|
import type { CommonNodeType, InputVar, ValueSelector, Var, Variable } from '@/app/components/workflow/types'
|
||||||
import { BlockEnum, InputVarType, NodeRunningStatus, VarType } from '@/app/components/workflow/types'
|
import { BlockEnum, InputVarType, NodeRunningStatus, VarType } from '@/app/components/workflow/types'
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||||
import { getIterationSingleNodeRunUrl, singleNodeRun } from '@/service/workflow'
|
import { getIterationSingleNodeRunUrl, singleNodeRun } from '@/service/workflow'
|
||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
import LLMDefault from '@/app/components/workflow/nodes/llm/default'
|
import LLMDefault from '@/app/components/workflow/nodes/llm/default'
|
||||||
|
|
@ -95,12 +95,13 @@ const useOneStepRun = <T>({
|
||||||
}: Params<T>) => {
|
}: Params<T>) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { getBeforeNodesInSameBranch, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() as any
|
const { getBeforeNodesInSameBranch, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() as any
|
||||||
|
const conversationVariables = useStore(s => s.conversationVariables)
|
||||||
const isChatMode = useIsChatMode()
|
const isChatMode = useIsChatMode()
|
||||||
const isIteration = data.type === BlockEnum.Iteration
|
const isIteration = data.type === BlockEnum.Iteration
|
||||||
|
|
||||||
const availableNodes = getBeforeNodesInSameBranch(id)
|
const availableNodes = getBeforeNodesInSameBranch(id)
|
||||||
const availableNodesIncludeParent = getBeforeNodesInSameBranchIncludeParent(id)
|
const availableNodesIncludeParent = getBeforeNodesInSameBranchIncludeParent(id)
|
||||||
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode)
|
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables)
|
||||||
const getVar = (valueSelector: ValueSelector): Var | undefined => {
|
const getVar = (valueSelector: ValueSelector): Var | undefined => {
|
||||||
let res: Var | undefined
|
let res: Var | undefined
|
||||||
const isSystem = valueSelector[0] === 'sys'
|
const isSystem = valueSelector[0] === 'sys'
|
||||||
|
|
@ -116,7 +117,8 @@ const useOneStepRun = <T>({
|
||||||
|
|
||||||
valueSelector.slice(1).forEach((key, i) => {
|
valueSelector.slice(1).forEach((key, i) => {
|
||||||
const isLast = i === valueSelector.length - 2
|
const isLast = i === valueSelector.length - 2
|
||||||
curr = curr?.find((v: any) => v.variable === key)
|
// conversation variable is start with 'conversation.'
|
||||||
|
curr = curr?.find((v: any) => v.variable.replace('conversation.', '') === key)
|
||||||
if (isLast) {
|
if (isLast) {
|
||||||
res = curr
|
res = curr
|
||||||
}
|
}
|
||||||
|
|
@ -369,6 +371,7 @@ const useOneStepRun = <T>({
|
||||||
nodeType: varInfo?.type,
|
nodeType: varInfo?.type,
|
||||||
nodeName: varInfo?.title || availableNodesIncludeParent[0]?.data.title, // default start node title
|
nodeName: varInfo?.title || availableNodesIncludeParent[0]?.data.title, // default start node title
|
||||||
variable: isSystemVar(item) ? item.join('.') : item[item.length - 1],
|
variable: isSystemVar(item) ? item.join('.') : item[item.length - 1],
|
||||||
|
isChatVar: isConversationVar(item),
|
||||||
},
|
},
|
||||||
variable: `#${item.join('.')}#`,
|
variable: `#${item.join('.')}#`,
|
||||||
value_selector: item,
|
value_selector: item,
|
||||||
|
|
|
||||||
46
web/app/components/workflow/nodes/assigner/default.ts
Normal file
46
web/app/components/workflow/nodes/assigner/default.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { BlockEnum } from '../../types'
|
||||||
|
import type { NodeDefault } from '../../types'
|
||||||
|
import { type AssignerNodeType, WriteMode } from './types'
|
||||||
|
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
|
||||||
|
const i18nPrefix = 'workflow.errorMsg'
|
||||||
|
|
||||||
|
const nodeDefault: NodeDefault<AssignerNodeType> = {
|
||||||
|
defaultValue: {
|
||||||
|
assigned_variable_selector: [],
|
||||||
|
write_mode: WriteMode.Overwrite,
|
||||||
|
input_variable_selector: [],
|
||||||
|
},
|
||||||
|
getAvailablePrevNodes(isChatMode: boolean) {
|
||||||
|
const nodes = isChatMode
|
||||||
|
? ALL_CHAT_AVAILABLE_BLOCKS
|
||||||
|
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
|
||||||
|
return nodes
|
||||||
|
},
|
||||||
|
getAvailableNextNodes(isChatMode: boolean) {
|
||||||
|
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
|
||||||
|
return nodes
|
||||||
|
},
|
||||||
|
checkValid(payload: AssignerNodeType, t: any) {
|
||||||
|
let errorMessages = ''
|
||||||
|
const {
|
||||||
|
assigned_variable_selector: assignedVarSelector,
|
||||||
|
write_mode: writeMode,
|
||||||
|
input_variable_selector: toAssignerVarSelector,
|
||||||
|
} = payload
|
||||||
|
|
||||||
|
if (!errorMessages && !assignedVarSelector?.length)
|
||||||
|
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.assigner.assignedVariable') })
|
||||||
|
|
||||||
|
if (!errorMessages && writeMode !== WriteMode.Clear) {
|
||||||
|
if (!toAssignerVarSelector?.length)
|
||||||
|
errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.assigner.variable') })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: !errorMessages,
|
||||||
|
errorMessage: errorMessages,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default nodeDefault
|
||||||
47
web/app/components/workflow/nodes/assigner/node.tsx
Normal file
47
web/app/components/workflow/nodes/assigner/node.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import { useNodes } from 'reactflow'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import NodeVariableItem from '../variable-assigner/components/node-variable-item'
|
||||||
|
import { type AssignerNodeType } from './types'
|
||||||
|
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||||
|
import { BlockEnum, type Node, type NodeProps } from '@/app/components/workflow/types'
|
||||||
|
|
||||||
|
const i18nPrefix = 'workflow.nodes.assigner'
|
||||||
|
|
||||||
|
const NodeComponent: FC<NodeProps<AssignerNodeType>> = ({
|
||||||
|
data,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const nodes: Node[] = useNodes()
|
||||||
|
const { assigned_variable_selector: variable, write_mode: writeMode } = data
|
||||||
|
|
||||||
|
if (!variable || variable.length === 0)
|
||||||
|
return null
|
||||||
|
|
||||||
|
const isSystem = isSystemVar(variable)
|
||||||
|
const isEnv = isENV(variable)
|
||||||
|
const isChatVar = isConversationVar(variable)
|
||||||
|
|
||||||
|
const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0])
|
||||||
|
const varName = isSystem ? `sys.${variable[variable.length - 1]}` : variable.slice(1).join('.')
|
||||||
|
return (
|
||||||
|
<div className='relative px-3'>
|
||||||
|
<div className='mb-1 system-2xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.assignedVariable`)}</div>
|
||||||
|
<NodeVariableItem
|
||||||
|
node={node as Node}
|
||||||
|
isEnv={isEnv}
|
||||||
|
isChatVar={isChatVar}
|
||||||
|
varName={varName}
|
||||||
|
className='bg-workflow-block-parma-bg'
|
||||||
|
/>
|
||||||
|
<div className='my-2 flex justify-between items-center h-[22px] px-[5px] bg-workflow-block-parma-bg radius-sm'>
|
||||||
|
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.writeMode`)}</div>
|
||||||
|
<div className='system-xs-medium text-text-secondary'>{t(`${i18nPrefix}.${writeMode}`)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(NodeComponent)
|
||||||
87
web/app/components/workflow/nodes/assigner/panel.tsx
Normal file
87
web/app/components/workflow/nodes/assigner/panel.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
|
||||||
|
import OptionCard from '../_base/components/option-card'
|
||||||
|
import useConfig from './use-config'
|
||||||
|
import { WriteMode } from './types'
|
||||||
|
import type { AssignerNodeType } from './types'
|
||||||
|
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||||
|
import { type NodePanelProps } from '@/app/components/workflow/types'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
const i18nPrefix = 'workflow.nodes.assigner'
|
||||||
|
|
||||||
|
const Panel: FC<NodePanelProps<AssignerNodeType>> = ({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const {
|
||||||
|
readOnly,
|
||||||
|
inputs,
|
||||||
|
handleAssignedVarChanges,
|
||||||
|
isSupportAppend,
|
||||||
|
writeModeTypes,
|
||||||
|
handleWriteModeChange,
|
||||||
|
filterAssignedVar,
|
||||||
|
filterToAssignedVar,
|
||||||
|
handleToAssignedVarChange,
|
||||||
|
toAssignedVarType,
|
||||||
|
} = useConfig(id, data)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-2'>
|
||||||
|
<div className='px-4 pb-4 space-y-4'>
|
||||||
|
<Field
|
||||||
|
title={t(`${i18nPrefix}.assignedVariable`)}
|
||||||
|
>
|
||||||
|
<VarReferencePicker
|
||||||
|
readonly={readOnly}
|
||||||
|
nodeId={id}
|
||||||
|
isShowNodeName
|
||||||
|
value={inputs.assigned_variable_selector || []}
|
||||||
|
onChange={handleAssignedVarChanges}
|
||||||
|
filterVar={filterAssignedVar}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
title={t(`${i18nPrefix}.writeMode`)}
|
||||||
|
tooltip={t(`${i18nPrefix}.writeModeTip`)!}
|
||||||
|
>
|
||||||
|
<div className={cn('grid gap-2 grid-cols-3')}>
|
||||||
|
{writeModeTypes.map(type => (
|
||||||
|
<OptionCard
|
||||||
|
key={type}
|
||||||
|
title={t(`${i18nPrefix}.${type}`)}
|
||||||
|
onSelect={handleWriteModeChange(type)}
|
||||||
|
selected={inputs.write_mode === type}
|
||||||
|
disabled={!isSupportAppend && type === WriteMode.Append}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
{inputs.write_mode !== WriteMode.Clear && (
|
||||||
|
<Field
|
||||||
|
title={t(`${i18nPrefix}.setVariable`)}
|
||||||
|
>
|
||||||
|
<VarReferencePicker
|
||||||
|
readonly={readOnly}
|
||||||
|
nodeId={id}
|
||||||
|
isShowNodeName
|
||||||
|
value={inputs.input_variable_selector || []}
|
||||||
|
onChange={handleToAssignedVarChange}
|
||||||
|
filterVar={filterToAssignedVar}
|
||||||
|
valueTypePlaceHolder={toAssignedVarType}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(Panel)
|
||||||
13
web/app/components/workflow/nodes/assigner/types.ts
Normal file
13
web/app/components/workflow/nodes/assigner/types.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types'
|
||||||
|
|
||||||
|
export enum WriteMode {
|
||||||
|
Overwrite = 'over-write',
|
||||||
|
Append = 'append',
|
||||||
|
Clear = 'clear',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AssignerNodeType = CommonNodeType & {
|
||||||
|
assigned_variable_selector: ValueSelector
|
||||||
|
write_mode: WriteMode
|
||||||
|
input_variable_selector: ValueSelector
|
||||||
|
}
|
||||||
144
web/app/components/workflow/nodes/assigner/use-config.ts
Normal file
144
web/app/components/workflow/nodes/assigner/use-config.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
import { useCallback, useMemo } from 'react'
|
||||||
|
import produce from 'immer'
|
||||||
|
import { useStoreApi } from 'reactflow'
|
||||||
|
import { isEqual } from 'lodash-es'
|
||||||
|
import { VarType } from '../../types'
|
||||||
|
import type { ValueSelector, Var } from '../../types'
|
||||||
|
import { type AssignerNodeType, WriteMode } from './types'
|
||||||
|
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||||
|
import {
|
||||||
|
useIsChatMode,
|
||||||
|
useNodesReadOnly,
|
||||||
|
useWorkflow,
|
||||||
|
useWorkflowVariables,
|
||||||
|
} from '@/app/components/workflow/hooks'
|
||||||
|
|
||||||
|
const useConfig = (id: string, payload: AssignerNodeType) => {
|
||||||
|
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||||
|
const isChatMode = useIsChatMode()
|
||||||
|
|
||||||
|
const store = useStoreApi()
|
||||||
|
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||||
|
|
||||||
|
const {
|
||||||
|
getNodes,
|
||||||
|
} = store.getState()
|
||||||
|
const currentNode = getNodes().find(n => n.id === id)
|
||||||
|
const isInIteration = payload.isInIteration
|
||||||
|
const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null
|
||||||
|
const availableNodes = useMemo(() => {
|
||||||
|
return getBeforeNodesInSameBranch(id)
|
||||||
|
}, [getBeforeNodesInSameBranch, id])
|
||||||
|
const { inputs, setInputs } = useNodeCrud<AssignerNodeType>(id, payload)
|
||||||
|
|
||||||
|
const { getCurrentVariableType } = useWorkflowVariables()
|
||||||
|
const assignedVarType = getCurrentVariableType({
|
||||||
|
parentNode: iterationNode,
|
||||||
|
valueSelector: inputs.assigned_variable_selector || [],
|
||||||
|
availableNodes,
|
||||||
|
isChatMode,
|
||||||
|
isConstant: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isSupportAppend = useCallback((varType: VarType) => {
|
||||||
|
return [VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varType)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const isCurrSupportAppend = useMemo(() => isSupportAppend(assignedVarType), [assignedVarType, isSupportAppend])
|
||||||
|
|
||||||
|
const handleAssignedVarChanges = useCallback((variable: ValueSelector | string) => {
|
||||||
|
const newInputs = produce(inputs, (draft) => {
|
||||||
|
draft.assigned_variable_selector = variable as ValueSelector
|
||||||
|
draft.input_variable_selector = []
|
||||||
|
|
||||||
|
const newVarType = getCurrentVariableType({
|
||||||
|
parentNode: iterationNode,
|
||||||
|
valueSelector: draft.assigned_variable_selector || [],
|
||||||
|
availableNodes,
|
||||||
|
isChatMode,
|
||||||
|
isConstant: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (inputs.write_mode === WriteMode.Append && !isSupportAppend(newVarType))
|
||||||
|
draft.write_mode = WriteMode.Overwrite
|
||||||
|
})
|
||||||
|
setInputs(newInputs)
|
||||||
|
}, [inputs, setInputs, getCurrentVariableType, iterationNode, availableNodes, isChatMode, isSupportAppend])
|
||||||
|
|
||||||
|
const writeModeTypes = [WriteMode.Overwrite, WriteMode.Append, WriteMode.Clear]
|
||||||
|
|
||||||
|
const handleWriteModeChange = useCallback((writeMode: WriteMode) => {
|
||||||
|
return () => {
|
||||||
|
const newInputs = produce(inputs, (draft) => {
|
||||||
|
draft.write_mode = writeMode
|
||||||
|
if (inputs.write_mode === WriteMode.Clear)
|
||||||
|
draft.input_variable_selector = []
|
||||||
|
})
|
||||||
|
setInputs(newInputs)
|
||||||
|
}
|
||||||
|
}, [inputs, setInputs])
|
||||||
|
|
||||||
|
const toAssignedVarType = useMemo(() => {
|
||||||
|
const { write_mode } = inputs
|
||||||
|
if (write_mode === WriteMode.Overwrite)
|
||||||
|
return assignedVarType
|
||||||
|
if (write_mode === WriteMode.Append) {
|
||||||
|
if (assignedVarType === VarType.arrayString)
|
||||||
|
return VarType.string
|
||||||
|
if (assignedVarType === VarType.arrayNumber)
|
||||||
|
return VarType.number
|
||||||
|
if (assignedVarType === VarType.arrayObject)
|
||||||
|
return VarType.object
|
||||||
|
}
|
||||||
|
return VarType.string
|
||||||
|
}, [assignedVarType, inputs])
|
||||||
|
|
||||||
|
const filterAssignedVar = useCallback((varPayload: Var, selector: ValueSelector) => {
|
||||||
|
return selector.join('.').startsWith('conversation')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const filterToAssignedVar = useCallback((varPayload: Var, selector: ValueSelector) => {
|
||||||
|
if (isEqual(selector, inputs.assigned_variable_selector))
|
||||||
|
return false
|
||||||
|
|
||||||
|
if (inputs.write_mode === WriteMode.Overwrite) {
|
||||||
|
return varPayload.type === assignedVarType
|
||||||
|
}
|
||||||
|
else if (inputs.write_mode === WriteMode.Append) {
|
||||||
|
switch (assignedVarType) {
|
||||||
|
case VarType.arrayString:
|
||||||
|
return varPayload.type === VarType.string
|
||||||
|
case VarType.arrayNumber:
|
||||||
|
return varPayload.type === VarType.number
|
||||||
|
case VarType.arrayObject:
|
||||||
|
return varPayload.type === VarType.object
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, [inputs.assigned_variable_selector, inputs.write_mode, assignedVarType])
|
||||||
|
|
||||||
|
const handleToAssignedVarChange = useCallback((value: ValueSelector | string) => {
|
||||||
|
const newInputs = produce(inputs, (draft) => {
|
||||||
|
draft.input_variable_selector = value as ValueSelector
|
||||||
|
})
|
||||||
|
setInputs(newInputs)
|
||||||
|
}, [inputs, setInputs])
|
||||||
|
|
||||||
|
return {
|
||||||
|
readOnly,
|
||||||
|
inputs,
|
||||||
|
handleAssignedVarChanges,
|
||||||
|
assignedVarType,
|
||||||
|
isSupportAppend: isCurrSupportAppend,
|
||||||
|
writeModeTypes,
|
||||||
|
handleWriteModeChange,
|
||||||
|
filterAssignedVar,
|
||||||
|
filterToAssignedVar,
|
||||||
|
handleToAssignedVarChange,
|
||||||
|
toAssignedVarType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useConfig
|
||||||
5
web/app/components/workflow/nodes/assigner/utils.ts
Normal file
5
web/app/components/workflow/nodes/assigner/utils.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import type { AssignerNodeType } from './types'
|
||||||
|
|
||||||
|
export const checkNodeValid = (payload: AssignerNodeType) => {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,8 @@ import ToolNode from './tool/node'
|
||||||
import ToolPanel from './tool/panel'
|
import ToolPanel from './tool/panel'
|
||||||
import VariableAssignerNode from './variable-assigner/node'
|
import VariableAssignerNode from './variable-assigner/node'
|
||||||
import VariableAssignerPanel from './variable-assigner/panel'
|
import VariableAssignerPanel from './variable-assigner/panel'
|
||||||
|
import AssignerNode from './assigner/node'
|
||||||
|
import AssignerPanel from './assigner/panel'
|
||||||
import ParameterExtractorNode from './parameter-extractor/node'
|
import ParameterExtractorNode from './parameter-extractor/node'
|
||||||
import ParameterExtractorPanel from './parameter-extractor/panel'
|
import ParameterExtractorPanel from './parameter-extractor/panel'
|
||||||
import IterationNode from './iteration/node'
|
import IterationNode from './iteration/node'
|
||||||
|
|
@ -42,6 +44,7 @@ export const NodeComponentMap: Record<string, ComponentType<any>> = {
|
||||||
[BlockEnum.HttpRequest]: HttpNode,
|
[BlockEnum.HttpRequest]: HttpNode,
|
||||||
[BlockEnum.Tool]: ToolNode,
|
[BlockEnum.Tool]: ToolNode,
|
||||||
[BlockEnum.VariableAssigner]: VariableAssignerNode,
|
[BlockEnum.VariableAssigner]: VariableAssignerNode,
|
||||||
|
[BlockEnum.Assigner]: AssignerNode,
|
||||||
[BlockEnum.VariableAggregator]: VariableAssignerNode,
|
[BlockEnum.VariableAggregator]: VariableAssignerNode,
|
||||||
[BlockEnum.ParameterExtractor]: ParameterExtractorNode,
|
[BlockEnum.ParameterExtractor]: ParameterExtractorNode,
|
||||||
[BlockEnum.Iteration]: IterationNode,
|
[BlockEnum.Iteration]: IterationNode,
|
||||||
|
|
@ -61,6 +64,7 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
|
||||||
[BlockEnum.Tool]: ToolPanel,
|
[BlockEnum.Tool]: ToolPanel,
|
||||||
[BlockEnum.VariableAssigner]: VariableAssignerPanel,
|
[BlockEnum.VariableAssigner]: VariableAssignerPanel,
|
||||||
[BlockEnum.VariableAggregator]: VariableAssignerPanel,
|
[BlockEnum.VariableAggregator]: VariableAssignerPanel,
|
||||||
|
[BlockEnum.Assigner]: AssignerPanel,
|
||||||
[BlockEnum.ParameterExtractor]: ParameterExtractorPanel,
|
[BlockEnum.ParameterExtractor]: ParameterExtractorPanel,
|
||||||
[BlockEnum.Iteration]: IterationPanel,
|
[BlockEnum.Iteration]: IterationPanel,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import React from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import type { EndNodeType } from './types'
|
import type { EndNodeType } from './types'
|
||||||
import type { NodeProps, Variable } from '@/app/components/workflow/types'
|
import type { NodeProps, Variable } from '@/app/components/workflow/types'
|
||||||
import { isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||||
import {
|
import {
|
||||||
useIsChatMode,
|
useIsChatMode,
|
||||||
useWorkflow,
|
useWorkflow,
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||||
import { BlockEnum } from '@/app/components/workflow/types'
|
import { BlockEnum } from '@/app/components/workflow/types'
|
||||||
|
|
||||||
const Node: FC<NodeProps<EndNodeType>> = ({
|
const Node: FC<NodeProps<EndNodeType>> = ({
|
||||||
|
|
@ -44,6 +44,7 @@ const Node: FC<NodeProps<EndNodeType>> = ({
|
||||||
const node = getNode(value_selector[0])
|
const node = getNode(value_selector[0])
|
||||||
const isSystem = isSystemVar(value_selector)
|
const isSystem = isSystemVar(value_selector)
|
||||||
const isEnv = isENV(value_selector)
|
const isEnv = isENV(value_selector)
|
||||||
|
const isChatVar = isConversationVar(value_selector)
|
||||||
const varName = isSystem ? `sys.${value_selector[value_selector.length - 1]}` : value_selector[value_selector.length - 1]
|
const varName = isSystem ? `sys.${value_selector[value_selector.length - 1]}` : value_selector[value_selector.length - 1]
|
||||||
const varType = getCurrentVariableType({
|
const varType = getCurrentVariableType({
|
||||||
valueSelector: value_selector,
|
valueSelector: value_selector,
|
||||||
|
|
@ -53,7 +54,7 @@ const Node: FC<NodeProps<EndNodeType>> = ({
|
||||||
return (
|
return (
|
||||||
<div key={index} className='flex items-center h-6 justify-between bg-gray-100 rounded-md px-1 space-x-1 text-xs font-normal text-gray-700'>
|
<div key={index} className='flex items-center h-6 justify-between bg-gray-100 rounded-md px-1 space-x-1 text-xs font-normal text-gray-700'>
|
||||||
<div className='flex items-center text-xs font-medium text-gray-500'>
|
<div className='flex items-center text-xs font-medium text-gray-500'>
|
||||||
{!isEnv && (
|
{!isEnv && !isChatVar && (
|
||||||
<>
|
<>
|
||||||
<div className='p-[1px]'>
|
<div className='p-[1px]'>
|
||||||
<VarBlockIcon
|
<VarBlockIcon
|
||||||
|
|
@ -66,9 +67,11 @@ const Node: FC<NodeProps<EndNodeType>> = ({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className='flex items-center text-primary-600'>
|
<div className='flex items-center text-primary-600'>
|
||||||
{!isEnv && <Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />}
|
{!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />}
|
||||||
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||||
<div className={cn('max-w-[50px] ml-0.5 text-xs font-medium truncate', isEnv && '!max-w-[70px] text-gray-900')}>{varName}</div>
|
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||||
|
|
||||||
|
<div className={cn('max-w-[50px] ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && '!max-w-[70px] text-gray-900')}>{varName}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='text-xs font-normal text-gray-700'>
|
<div className='text-xs font-normal text-gray-700'>
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ type Props = {
|
||||||
onChange: (newList: KeyValue[]) => void
|
onChange: (newList: KeyValue[]) => void
|
||||||
onAdd: () => void
|
onAdd: () => void
|
||||||
// onSwitchToBulkEdit: () => void
|
// onSwitchToBulkEdit: () => void
|
||||||
|
keyNotSupportVar?: boolean
|
||||||
|
insertVarTipToLeft?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const KeyValueList: FC<Props> = ({
|
const KeyValueList: FC<Props> = ({
|
||||||
|
|
@ -26,6 +28,8 @@ const KeyValueList: FC<Props> = ({
|
||||||
onChange,
|
onChange,
|
||||||
onAdd,
|
onAdd,
|
||||||
// onSwitchToBulkEdit,
|
// onSwitchToBulkEdit,
|
||||||
|
keyNotSupportVar,
|
||||||
|
insertVarTipToLeft,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
|
@ -47,6 +51,9 @@ const KeyValueList: FC<Props> = ({
|
||||||
}
|
}
|
||||||
}, [list, onChange])
|
}, [list, onChange])
|
||||||
|
|
||||||
|
if (!Array.isArray(list))
|
||||||
|
return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='border border-gray-200 rounded-lg overflow-hidden'>
|
<div className='border border-gray-200 rounded-lg overflow-hidden'>
|
||||||
<div className='flex items-center h-7 leading-7 text-xs font-medium text-gray-500 uppercase'>
|
<div className='flex items-center h-7 leading-7 text-xs font-medium text-gray-500 uppercase'>
|
||||||
|
|
@ -79,6 +86,8 @@ const KeyValueList: FC<Props> = ({
|
||||||
onAdd={onAdd}
|
onAdd={onAdd}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
canRemove={list.length > 1}
|
canRemove={list.length > 1}
|
||||||
|
keyNotSupportVar={keyNotSupportVar}
|
||||||
|
insertVarTipToLeft={insertVarTipToLeft}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ type Props = {
|
||||||
onRemove?: () => void
|
onRemove?: () => void
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
|
insertVarTipToLeft?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputItem: FC<Props> = ({
|
const InputItem: FC<Props> = ({
|
||||||
|
|
@ -30,6 +31,7 @@ const InputItem: FC<Props> = ({
|
||||||
onRemove,
|
onRemove,
|
||||||
placeholder,
|
placeholder,
|
||||||
readOnly,
|
readOnly,
|
||||||
|
insertVarTipToLeft,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
|
@ -64,6 +66,7 @@ const InputItem: FC<Props> = ({
|
||||||
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
|
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
|
||||||
placeholderClassName='!leading-[21px]'
|
placeholderClassName='!leading-[21px]'
|
||||||
promptMinHeightClassName='h-full'
|
promptMinHeightClassName='h-full'
|
||||||
|
insertVarTipToLeft={insertVarTipToLeft}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: <div
|
: <div
|
||||||
|
|
@ -83,6 +86,7 @@ const InputItem: FC<Props> = ({
|
||||||
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
|
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
|
||||||
placeholderClassName='!leading-[21px]'
|
placeholderClassName='!leading-[21px]'
|
||||||
promptMinHeightClassName='h-full'
|
promptMinHeightClassName='h-full'
|
||||||
|
insertVarTipToLeft={insertVarTipToLeft}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import produce from 'immer'
|
||||||
import type { KeyValue } from '../../../types'
|
import type { KeyValue } from '../../../types'
|
||||||
import InputItem from './input-item'
|
import InputItem from './input-item'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
import Input from '@/app/components/base/input'
|
||||||
|
|
||||||
const i18nPrefix = 'workflow.nodes.http'
|
const i18nPrefix = 'workflow.nodes.http'
|
||||||
|
|
||||||
|
|
@ -20,6 +21,8 @@ type Props = {
|
||||||
onRemove: () => void
|
onRemove: () => void
|
||||||
isLastItem: boolean
|
isLastItem: boolean
|
||||||
onAdd: () => void
|
onAdd: () => void
|
||||||
|
keyNotSupportVar?: boolean
|
||||||
|
insertVarTipToLeft?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const KeyValueItem: FC<Props> = ({
|
const KeyValueItem: FC<Props> = ({
|
||||||
|
|
@ -33,6 +36,8 @@ const KeyValueItem: FC<Props> = ({
|
||||||
onRemove,
|
onRemove,
|
||||||
isLastItem,
|
isLastItem,
|
||||||
onAdd,
|
onAdd,
|
||||||
|
keyNotSupportVar,
|
||||||
|
insertVarTipToLeft,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
|
@ -51,15 +56,26 @@ const KeyValueItem: FC<Props> = ({
|
||||||
// group class name is for hover row show remove button
|
// group class name is for hover row show remove button
|
||||||
<div className={cn(className, 'group flex h-min-7 border-t border-gray-200')}>
|
<div className={cn(className, 'group flex h-min-7 border-t border-gray-200')}>
|
||||||
<div className='w-1/2 border-r border-gray-200'>
|
<div className='w-1/2 border-r border-gray-200'>
|
||||||
<InputItem
|
{!keyNotSupportVar
|
||||||
instanceId={`http-key-${instanceId}`}
|
? (
|
||||||
nodeId={nodeId}
|
<InputItem
|
||||||
value={payload.key}
|
instanceId={`http-key-${instanceId}`}
|
||||||
onChange={handleChange('key')}
|
nodeId={nodeId}
|
||||||
hasRemove={false}
|
value={payload.key}
|
||||||
placeholder={t(`${i18nPrefix}.key`)!}
|
onChange={handleChange('key')}
|
||||||
readOnly={readonly}
|
hasRemove={false}
|
||||||
/>
|
placeholder={t(`${i18nPrefix}.key`)!}
|
||||||
|
readOnly={readonly}
|
||||||
|
insertVarTipToLeft={insertVarTipToLeft}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<Input
|
||||||
|
className='rounded-none bg-white border-none system-sm-regular focus:ring-0 focus:bg-gray-100! hover:bg-gray-50'
|
||||||
|
value={payload.key}
|
||||||
|
onChange={handleChange('key')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='w-1/2'>
|
<div className='w-1/2'>
|
||||||
<InputItem
|
<InputItem
|
||||||
|
|
@ -71,6 +87,7 @@ const KeyValueItem: FC<Props> = ({
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
placeholder={t(`${i18nPrefix}.value`)!}
|
placeholder={t(`${i18nPrefix}.value`)!}
|
||||||
readOnly={readonly}
|
readOnly={readonly}
|
||||||
|
insertVarTipToLeft={insertVarTipToLeft}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ import {
|
||||||
isComparisonOperatorNeedTranslate,
|
isComparisonOperatorNeedTranslate,
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||||
|
|
||||||
type ConditionValueProps = {
|
type ConditionValueProps = {
|
||||||
variableSelector: string[]
|
variableSelector: string[]
|
||||||
|
|
@ -27,7 +27,8 @@ const ConditionValue = ({
|
||||||
const variableName = isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.')
|
const variableName = isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.')
|
||||||
const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator
|
const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator
|
||||||
const notHasValue = comparisonOperatorNotRequireValue(operator)
|
const notHasValue = comparisonOperatorNotRequireValue(operator)
|
||||||
|
const isEnvVar = isENV(variableSelector)
|
||||||
|
const isChatVar = isConversationVar(variableSelector)
|
||||||
const formatValue = useMemo(() => {
|
const formatValue = useMemo(() => {
|
||||||
if (notHasValue)
|
if (notHasValue)
|
||||||
return ''
|
return ''
|
||||||
|
|
@ -43,8 +44,10 @@ const ConditionValue = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center px-1 h-6 rounded-md bg-workflow-block-parma-bg'>
|
<div className='flex items-center px-1 h-6 rounded-md bg-workflow-block-parma-bg'>
|
||||||
{!isENV(variableSelector) && <Variable02 className='shrink-0 mr-1 w-3.5 h-3.5 text-text-accent' />}
|
{!isEnvVar && !isChatVar && <Variable02 className='shrink-0 mr-1 w-3.5 h-3.5 text-text-accent' />}
|
||||||
{isENV(variableSelector) && <Env className='shrink-0 mr-1 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
{isEnvVar && <Env className='shrink-0 mr-1 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||||
|
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'shrink-0 truncate text-xs font-medium text-text-accent',
|
'shrink-0 truncate text-xs font-medium text-text-accent',
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue