Procházet zdrojové kódy

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>
tags/0.7.0
KVOJJJin před 1 rokem
rodič
revize
935e72d449
Žádný účet není propojen s e-mailovou adresou tvůrce revize
100 změnil soubory, kde provedl 1820 přidání a 549 odebrání
  1. 2
    6
      api/configs/app_config.py
  2. 1
    0
      api/controllers/console/__init__.py
  3. 61
    0
      api/controllers/console/app/conversation_variables.py
  4. 6
    1
      api/controllers/console/app/workflow.py
  5. 2
    6
      api/core/app/app_config/entities.py
  6. 1
    1
      api/core/app/app_config/features/file_upload/manager.py
  7. 2
    4
      api/core/app/apps/advanced_chat/app_generator.py
  8. 104
    86
      api/core/app/apps/advanced_chat/app_runner.py
  9. 40
    38
      api/core/app/apps/workflow/app_runner.py
  10. 2
    0
      api/core/app/segments/__init__.py
  11. 2
    0
      api/core/app/segments/exc.py
  12. 28
    25
      api/core/app/segments/factory.py
  13. 9
    7
      api/core/app/segments/segments.py
  14. 9
    3
      api/core/file/file_obj.py
  15. 1
    2
      api/core/file/message_file_parser.py
  16. 1
    1
      api/core/helper/encrypter.py
  17. 2
    0
      api/core/workflow/entities/node_entities.py
  18. 7
    1
      api/core/workflow/entities/variable_pool.py
  19. 14
    8
      api/core/workflow/nodes/base_node.py
  20. 109
    0
      api/core/workflow/nodes/variable_assigner/__init__.py
  21. 9
    13
      api/core/workflow/workflow_engine_manager.py
  22. 21
    0
      api/fields/conversation_variable_fields.py
  23. 4
    2
      api/fields/workflow_fields.py
  24. 51
    0
      api/migrations/versions/2024_08_13_0633-63a83fcf12ba_support_conversation_variables.py
  25. 8
    50
      api/models/__init__.py
  26. 2
    1
      api/models/account.py
  27. 2
    1
      api/models/api_based_extension.py
  28. 4
    3
      api/models/dataset.py
  29. 1
    1
      api/models/model.py
  30. 2
    1
      api/models/provider.py
  31. 2
    1
      api/models/source.py
  32. 2
    1
      api/models/tool.py
  33. 3
    2
      api/models/tools.py
  34. 26
    0
      api/models/types.py
  35. 3
    2
      api/models/web.py
  36. 57
    7
      api/models/workflow.py
  37. 3
    0
      api/services/app_dsl_service.py
  38. 1
    1
      api/services/workflow/workflow_converter.py
  39. 8
    4
      api/services/workflow_service.py
  40. 12
    2
      api/tasks/remove_app_and_related_data_task.py
  41. 63
    132
      api/tests/unit_tests/core/app/segments/test_factory.py
  42. 2
    2
      api/tests/unit_tests/core/prompt/test_advanced_prompt_transform.py
  43. 150
    0
      api/tests/unit_tests/core/workflow/nodes/test_variable_assigner.py
  44. 25
    0
      api/tests/unit_tests/models/test_conversation_variable.py
  45. 4
    1
      web/app/components/base/badge.tsx
  46. 8
    0
      web/app/components/base/icons/assets/vender/line/others/bubble-x.svg
  47. 3
    0
      web/app/components/base/icons/assets/vender/line/others/long-arrow-left.svg
  48. 3
    0
      web/app/components/base/icons/assets/vender/line/others/long-arrow-right.svg
  49. 9
    0
      web/app/components/base/icons/assets/vender/workflow/assigner.svg
  50. 57
    0
      web/app/components/base/icons/src/vender/line/others/BubbleX.json
  51. 16
    0
      web/app/components/base/icons/src/vender/line/others/BubbleX.tsx
  52. 27
    0
      web/app/components/base/icons/src/vender/line/others/LongArrowLeft.json
  53. 16
    0
      web/app/components/base/icons/src/vender/line/others/LongArrowLeft.tsx
  54. 27
    0
      web/app/components/base/icons/src/vender/line/others/LongArrowRight.json
  55. 16
    0
      web/app/components/base/icons/src/vender/line/others/LongArrowRight.tsx
  56. 3
    0
      web/app/components/base/icons/src/vender/line/others/index.ts
  57. 68
    0
      web/app/components/base/icons/src/vender/workflow/Assigner.json
  58. 16
    0
      web/app/components/base/icons/src/vender/workflow/Assigner.tsx
  59. 1
    0
      web/app/components/base/icons/src/vender/workflow/index.ts
  60. 3
    3
      web/app/components/base/input/index.tsx
  61. 0
    7
      web/app/components/base/input/style.module.css
  62. 1
    1
      web/app/components/base/prompt-editor/index.tsx
  63. 10
    8
      web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx
  64. 3
    0
      web/app/components/workflow/block-icon.tsx
  65. 5
    0
      web/app/components/workflow/block-selector/constants.tsx
  66. 16
    0
      web/app/components/workflow/constants.ts
  67. 24
    0
      web/app/components/workflow/header/chat-variable-button.tsx
  68. 6
    4
      web/app/components/workflow/header/env-button.tsx
  69. 7
    3
      web/app/components/workflow/header/index.tsx
  70. 2
    0
      web/app/components/workflow/hooks/use-nodes-sync-draft.ts
  71. 3
    0
      web/app/components/workflow/hooks/use-workflow-interactions.ts
  72. 2
    0
      web/app/components/workflow/hooks/use-workflow-start-run.tsx
  73. 7
    2
      web/app/components/workflow/hooks/use-workflow-variables.ts
  74. 3
    0
      web/app/components/workflow/hooks/use-workflow.ts
  75. 1
    0
      web/app/components/workflow/nodes/_base/components/add-variable-popup-with-position.tsx
  76. 22
    13
      web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx
  77. 2
    2
      web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx
  78. 4
    2
      web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx
  79. 1
    1
      web/app/components/workflow/nodes/_base/components/option-card.tsx
  80. 7
    5
      web/app/components/workflow/nodes/_base/components/readonly-input-with-select-var.tsx
  81. 6
    3
      web/app/components/workflow/nodes/_base/components/selector.tsx
  82. 6
    4
      web/app/components/workflow/nodes/_base/components/variable-tag.tsx
  83. 3
    3
      web/app/components/workflow/nodes/_base/components/variable/constant-field.tsx
  84. 47
    4
      web/app/components/workflow/nodes/_base/components/variable/utils.ts
  85. 73
    43
      web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx
  86. 16
    7
      web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx
  87. 2
    0
      web/app/components/workflow/nodes/_base/hooks/use-node-help-link.ts
  88. 7
    4
      web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts
  89. 46
    0
      web/app/components/workflow/nodes/assigner/default.ts
  90. 47
    0
      web/app/components/workflow/nodes/assigner/node.tsx
  91. 87
    0
      web/app/components/workflow/nodes/assigner/panel.tsx
  92. 13
    0
      web/app/components/workflow/nodes/assigner/types.ts
  93. 144
    0
      web/app/components/workflow/nodes/assigner/use-config.ts
  94. 5
    0
      web/app/components/workflow/nodes/assigner/utils.ts
  95. 4
    0
      web/app/components/workflow/nodes/constants.ts
  96. 8
    5
      web/app/components/workflow/nodes/end/node.tsx
  97. 9
    0
      web/app/components/workflow/nodes/http/components/key-value/key-value-edit/index.tsx
  98. 4
    0
      web/app/components/workflow/nodes/http/components/key-value/key-value-edit/input-item.tsx
  99. 26
    9
      web/app/components/workflow/nodes/http/components/key-value/key-value-edit/item.tsx
  100. 0
    0
      web/app/components/workflow/nodes/if-else/components/condition-value.tsx

+ 2
- 6
api/configs/app_config.py Zobrazit soubor

@@ -12,19 +12,14 @@ from configs.packaging import PackagingInfo
class DifyConfig(
# Packaging info
PackagingInfo,

# Deployment configs
DeploymentConfig,

# Feature configs
FeatureConfig,

# Middleware configs
MiddlewareConfig,

# Extra service configs
ExtraServiceConfig,

# Enterprise feature configs
# **Before using, please contact business@dify.ai by email to inquire about licensing matters.**
EnterpriseFeatureConfig,
@@ -36,7 +31,6 @@ class DifyConfig(
env_file='.env',
env_file_encoding='utf-8',
frozen=True,

# ignore extra attributes
extra='ignore',
)
@@ -67,3 +61,5 @@ class DifyConfig(
SSRF_PROXY_HTTPS_URL: str | None = None

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.')

+ 1
- 0
api/controllers/console/__init__.py Zobrazit soubor

@@ -17,6 +17,7 @@ from .app import (
audio,
completion,
conversation,
conversation_variables,
generator,
message,
model_config,

+ 61
- 0
api/controllers/console/app/conversation_variables.py Zobrazit soubor

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

+ 6
- 1
api/controllers/console/app/workflow.py Zobrazit soubor

@@ -74,6 +74,7 @@ class DraftWorkflowApi(Resource):
parser.add_argument('hash', type=str, required=False, location='json')
# TODO: set this to required=True after frontend is updated
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()
elif 'text/plain' in content_type:
try:
@@ -88,7 +89,8 @@ class DraftWorkflowApi(Resource):
'graph': data.get('graph'),
'features': data.get('features'),
'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:
return {'message': 'Invalid JSON data'}, 400
@@ -100,6 +102,8 @@ class DraftWorkflowApi(Resource):
try:
environment_variables_list = args.get('environment_variables') or []
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(
app_model=app_model,
graph=args['graph'],
@@ -107,6 +111,7 @@ class DraftWorkflowApi(Resource):
unique_hash=args.get('hash'),
account=current_user,
environment_variables=environment_variables,
conversation_variables=conversation_variables,
)
except WorkflowHashNotEqualError:
raise DraftWorkflowNotSync()

+ 2
- 6
api/core/app/app_config/entities.py Zobrazit soubor

@@ -3,8 +3,9 @@ from typing import Any, Optional

from pydantic import BaseModel

from core.file.file_obj import FileExtraConfig
from core.model_runtime.entities.message_entities import PromptMessageRole
from models.model import AppMode
from models import AppMode


class ModelConfigEntity(BaseModel):
@@ -200,11 +201,6 @@ class TracingConfigEntity(BaseModel):
tracing_provider: str


class FileExtraConfig(BaseModel):
"""
File Upload Entity.
"""
image_config: Optional[dict[str, Any]] = None


class AppAdditionalFeatures(BaseModel):

+ 1
- 1
api/core/app/app_config/features/file_upload/manager.py Zobrazit soubor

@@ -1,7 +1,7 @@
from collections.abc import Mapping
from typing import Any, Optional

from core.app.app_config.entities import FileExtraConfig
from core.file.file_obj import FileExtraConfig


class FileUploadConfigManager:

+ 2
- 4
api/core/app/apps/advanced_chat/app_generator.py Zobrazit soubor

@@ -113,7 +113,6 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)

return self._generate(
app_model=app_model,
workflow=workflow,
user=user,
invoke_from=invoke_from,
@@ -180,7 +179,6 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)

return self._generate(
app_model=app_model,
workflow=workflow,
user=user,
invoke_from=InvokeFrom.DEBUGGER,
@@ -189,12 +187,12 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
stream=stream
)

def _generate(self, app_model: App,
def _generate(self, *,
workflow: Workflow,
user: Union[Account, EndUser],
invoke_from: InvokeFrom,
application_generate_entity: AdvancedChatAppGenerateEntity,
conversation: Conversation = None,
conversation: Conversation | None = None,
stream: bool = True) \
-> Union[dict, Generator[dict, None, None]]:
is_first_conversation = False

+ 104
- 86
api/core/app/apps/advanced_chat/app_runner.py Zobrazit soubor

@@ -4,6 +4,9 @@ import time
from collections.abc import Mapping
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.workflow_event_trigger_callback import WorkflowEventTriggerCallback
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.workflow.callbacks.base_workflow_callback import WorkflowCallback
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.workflow_engine_manager import WorkflowEngineManager
from extensions.ext_database import db
from models.model import App, Conversation, EndUser, Message
from models.workflow import Workflow
from models.workflow import ConversationVariable, Workflow

logger = logging.getLogger(__name__)

@@ -31,10 +35,13 @@ class AdvancedChatAppRunner(AppRunner):
AdvancedChat Application Runner
"""

def run(self, application_generate_entity: AdvancedChatAppGenerateEntity,
queue_manager: AppQueueManager,
conversation: Conversation,
message: Message) -> None:
def run(
self,
application_generate_entity: AdvancedChatAppGenerateEntity,
queue_manager: AppQueueManager,
conversation: Conversation,
message: Message,
) -> None:
"""
Run application
: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()
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)
if not workflow:
raise ValueError("Workflow not initialized")
raise ValueError('Workflow not initialized')

inputs = application_generate_entity.inputs
query = application_generate_entity.query
@@ -68,35 +75,66 @@ class AdvancedChatAppRunner(AppRunner):

# moderation
if self.handle_input_moderation(
queue_manager=queue_manager,
app_record=app_record,
app_generate_entity=application_generate_entity,
inputs=inputs,
query=query,
message_id=message.id
queue_manager=queue_manager,
app_record=app_record,
app_generate_entity=application_generate_entity,
inputs=inputs,
query=query,
message_id=message.id,
):
return

# annotation reply
if self.handle_annotation_reply(
app_record=app_record,
message=message,
query=query,
queue_manager=queue_manager,
app_generate_entity=application_generate_entity
app_record=app_record,
message=message,
query=query,
queue_manager=queue_manager,
app_generate_entity=application_generate_entity,
):
return

db.session.close()

workflow_callbacks: list[WorkflowCallback] = [WorkflowEventTriggerCallback(
queue_manager=queue_manager,
workflow=workflow
)]
workflow_callbacks: list[WorkflowCallback] = [
WorkflowEventTriggerCallback(queue_manager=queue_manager, workflow=workflow)
]

if bool(os.environ.get("DEBUG", 'False').lower() == 'true'):
if bool(os.environ.get('DEBUG', 'False').lower() == 'true'):
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
workflow_engine_manager = WorkflowEngineManager()
workflow_engine_manager.run_workflow(
@@ -106,43 +144,30 @@ class AdvancedChatAppRunner(AppRunner):
if application_generate_entity.invoke_from in [InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER]
else UserFrom.END_USER,
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,
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,
queue_manager: AppQueueManager,
inputs: dict, node_id: str, user_id: str) -> None:
def single_iteration_run(
self, app_id: str, workflow_id: str, queue_manager: AppQueueManager, inputs: dict, node_id: str, user_id: str
) -> None:
"""
Single iteration run
"""
app_record: App = db.session.query(App).filter(App.id == app_id).first()
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)
if not workflow:
raise ValueError("Workflow not initialized")
workflow_callbacks = [WorkflowEventTriggerCallback(
queue_manager=queue_manager,
workflow=workflow
)]
raise ValueError('Workflow not initialized')

workflow_callbacks = [WorkflowEventTriggerCallback(queue_manager=queue_manager, workflow=workflow)]

workflow_engine_manager = WorkflowEngineManager()
workflow_engine_manager.single_step_run_iteration_workflow_node(
workflow=workflow,
node_id=node_id,
user_id=user_id,
user_inputs=inputs,
callbacks=workflow_callbacks
workflow=workflow, 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]:
@@ -150,22 +175,25 @@ class AdvancedChatAppRunner(AppRunner):
Get workflow
"""
# fetch workflow by workflow_id
workflow = db.session.query(Workflow).filter(
Workflow.tenant_id == app_model.tenant_id,
Workflow.app_id == app_model.id,
Workflow.id == workflow_id
).first()
workflow = (
db.session.query(Workflow)
.filter(
Workflow.tenant_id == app_model.tenant_id, Workflow.app_id == app_model.id, Workflow.id == workflow_id
)
.first()
)

# return workflow
return workflow

def handle_input_moderation(
self, queue_manager: AppQueueManager,
app_record: App,
app_generate_entity: AdvancedChatAppGenerateEntity,
inputs: Mapping[str, Any],
query: str,
message_id: str
self,
queue_manager: AppQueueManager,
app_record: App,
app_generate_entity: AdvancedChatAppGenerateEntity,
inputs: Mapping[str, Any],
query: str,
message_id: str,
) -> bool:
"""
Handle input moderation
@@ -192,17 +220,20 @@ class AdvancedChatAppRunner(AppRunner):
queue_manager=queue_manager,
text=str(e),
stream=app_generate_entity.stream,
stopped_by=QueueStopEvent.StopBy.INPUT_MODERATION
stopped_by=QueueStopEvent.StopBy.INPUT_MODERATION,
)
return True

return False

def handle_annotation_reply(self, app_record: App,
message: Message,
query: str,
queue_manager: AppQueueManager,
app_generate_entity: AdvancedChatAppGenerateEntity) -> bool:
def handle_annotation_reply(
self,
app_record: App,
message: Message,
query: str,
queue_manager: AppQueueManager,
app_generate_entity: AdvancedChatAppGenerateEntity,
) -> bool:
"""
Handle annotation reply
:param app_record: app record
@@ -217,29 +248,27 @@ class AdvancedChatAppRunner(AppRunner):
message=message,
query=query,
user_id=app_generate_entity.user_id,
invoke_from=app_generate_entity.invoke_from
invoke_from=app_generate_entity.invoke_from,
)

if annotation_reply:
queue_manager.publish(
QueueAnnotationReplyEvent(message_annotation_id=annotation_reply.id),
PublishFrom.APPLICATION_MANAGER
QueueAnnotationReplyEvent(message_annotation_id=annotation_reply.id), PublishFrom.APPLICATION_MANAGER
)

self._stream_output(
queue_manager=queue_manager,
text=annotation_reply.content,
stream=app_generate_entity.stream,
stopped_by=QueueStopEvent.StopBy.ANNOTATION_REPLY
stopped_by=QueueStopEvent.StopBy.ANNOTATION_REPLY,
)
return True

return False

def _stream_output(self, queue_manager: AppQueueManager,
text: str,
stream: bool,
stopped_by: QueueStopEvent.StopBy) -> None:
def _stream_output(
self, queue_manager: AppQueueManager, text: str, stream: bool, stopped_by: QueueStopEvent.StopBy
) -> None:
"""
Direct output
:param queue_manager: application queue manager
@@ -250,21 +279,10 @@ class AdvancedChatAppRunner(AppRunner):
if stream:
index = 0
for token in text:
queue_manager.publish(
QueueTextChunkEvent(
text=token
), PublishFrom.APPLICATION_MANAGER
)
queue_manager.publish(QueueTextChunkEvent(text=token), PublishFrom.APPLICATION_MANAGER)
index += 1
time.sleep(0.01)
else:
queue_manager.publish(
QueueTextChunkEvent(
text=text
), PublishFrom.APPLICATION_MANAGER
)
queue_manager.publish(QueueTextChunkEvent(text=text), PublishFrom.APPLICATION_MANAGER)

queue_manager.publish(
QueueStopEvent(stopped_by=stopped_by),
PublishFrom.APPLICATION_MANAGER
)
queue_manager.publish(QueueStopEvent(stopped_by=stopped_by), PublishFrom.APPLICATION_MANAGER)

+ 40
- 38
api/core/app/apps/workflow/app_runner.py Zobrazit soubor

@@ -12,6 +12,7 @@ from core.app.entities.app_invoke_entities import (
)
from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
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.workflow_engine_manager import WorkflowEngineManager
from extensions.ext_database import db
@@ -26,8 +27,7 @@ class WorkflowAppRunner:
Workflow Application Runner
"""

def run(self, application_generate_entity: WorkflowAppGenerateEntity,
queue_manager: AppQueueManager) -> None:
def run(self, application_generate_entity: WorkflowAppGenerateEntity, queue_manager: AppQueueManager) -> None:
"""
Run application
: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()
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)
if not workflow:
raise ValueError("Workflow not initialized")
raise ValueError('Workflow not initialized')

inputs = application_generate_entity.inputs
files = application_generate_entity.files

db.session.close()

workflow_callbacks: list[WorkflowCallback] = [WorkflowEventTriggerCallback(
queue_manager=queue_manager,
workflow=workflow
)]
workflow_callbacks: list[WorkflowCallback] = [
WorkflowEventTriggerCallback(queue_manager=queue_manager, workflow=workflow)
]

if bool(os.environ.get("DEBUG", 'False').lower() == 'true'):
if bool(os.environ.get('DEBUG', 'False').lower() == 'true'):
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
workflow_engine_manager = WorkflowEngineManager()
workflow_engine_manager.run_workflow(
@@ -75,44 +86,33 @@ class WorkflowAppRunner:
if application_generate_entity.invoke_from in [InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER]
else UserFrom.END_USER,
invoke_from=application_generate_entity.invoke_from,
user_inputs=inputs,
system_inputs={
SystemVariable.FILES: files,
SystemVariable.USER_ID: user_id
},
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,
queue_manager: AppQueueManager,
inputs: dict, node_id: str, user_id: str) -> None:
def single_iteration_run(
self, app_id: str, workflow_id: str, queue_manager: AppQueueManager, inputs: dict, node_id: str, user_id: str
) -> None:
"""
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:
raise ValueError("App not found")
raise ValueError('App not found')
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)
if not workflow:
raise ValueError("Workflow not initialized")
workflow_callbacks = [WorkflowEventTriggerCallback(
queue_manager=queue_manager,
workflow=workflow
)]
raise ValueError('Workflow not initialized')

workflow_callbacks = [WorkflowEventTriggerCallback(queue_manager=queue_manager, workflow=workflow)]

workflow_engine_manager = WorkflowEngineManager()
workflow_engine_manager.single_step_run_iteration_workflow_node(
workflow=workflow,
node_id=node_id,
user_id=user_id,
user_inputs=inputs,
callbacks=workflow_callbacks
workflow=workflow, 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]:
@@ -120,11 +120,13 @@ class WorkflowAppRunner:
Get workflow
"""
# fetch workflow by workflow_id
workflow = db.session.query(Workflow).filter(
Workflow.tenant_id == app_model.tenant_id,
Workflow.app_id == app_model.id,
Workflow.id == workflow_id
).first()
workflow = (
db.session.query(Workflow)
.filter(
Workflow.tenant_id == app_model.tenant_id, Workflow.app_id == app_model.id, Workflow.id == workflow_id
)
.first()
)

# return workflow
return workflow

+ 2
- 0
api/core/app/segments/__init__.py Zobrazit soubor

@@ -1,6 +1,7 @@
from .segment_group import SegmentGroup
from .segments import (
ArrayAnySegment,
ArraySegment,
FileSegment,
FloatSegment,
IntegerSegment,
@@ -50,4 +51,5 @@ __all__ = [
'ArrayNumberVariable',
'ArrayObjectVariable',
'ArrayFileVariable',
'ArraySegment',
]

+ 2
- 0
api/core/app/segments/exc.py Zobrazit soubor

@@ -0,0 +1,2 @@
class VariableError(Exception):
pass

+ 28
- 25
api/core/app/segments/factory.py Zobrazit soubor

@@ -1,8 +1,10 @@
from collections.abc import Mapping
from typing import Any

from configs import dify_config
from core.file.file_obj import FileVar

from .exc import VariableError
from .segments import (
ArrayAnySegment,
FileSegment,
@@ -29,39 +31,43 @@ from .variables import (
)


def build_variable_from_mapping(m: Mapping[str, Any], /) -> Variable:
if (value_type := m.get('value_type')) is None:
raise ValueError('missing value type')
if not m.get('name'):
raise ValueError('missing name')
if (value := m.get('value')) is None:
raise ValueError('missing value')
def build_variable_from_mapping(mapping: Mapping[str, Any], /) -> Variable:
if (value_type := mapping.get('value_type')) is None:
raise VariableError('missing value type')
if not mapping.get('name'):
raise VariableError('missing name')
if (value := mapping.get('value')) is None:
raise VariableError('missing value')
match value_type:
case SegmentType.STRING:
return StringVariable.model_validate(m)
result = StringVariable.model_validate(mapping)
case SegmentType.SECRET:
return SecretVariable.model_validate(m)
result = SecretVariable.model_validate(mapping)
case SegmentType.NUMBER if isinstance(value, int):
return IntegerVariable.model_validate(m)
result = IntegerVariable.model_validate(mapping)
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):
raise ValueError(f'invalid number value {value}')
raise VariableError(f'invalid number value {value}')
case SegmentType.FILE:
return FileVariable.model_validate(m)
result = FileVariable.model_validate(mapping)
case SegmentType.OBJECT if isinstance(value, dict):
return ObjectVariable.model_validate(
{**m, 'value': {k: build_variable_from_mapping(v) for k, v in value.items()}}
)
result = ObjectVariable.model_validate(mapping)
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):
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):
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):
return ArrayFileVariable.model_validate({**m, 'value': [build_variable_from_mapping(v) for v in value]})
raise ValueError(f'not supported value type {value_type}')
mapping = dict(mapping)
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:
@@ -74,12 +80,9 @@ def build_segment(value: Any, /) -> Segment:
if isinstance(value, float):
return FloatSegment(value=value)
if isinstance(value, dict):
# TODO: Limit the depth of the object
return ObjectSegment(value=value)
if isinstance(value, list):
# TODO: Limit the depth of the array
elements = [build_segment(v) for v in value]
return ArrayAnySegment(value=elements)
return ArrayAnySegment(value=value)
if isinstance(value, FileVar):
return FileSegment(value=value)
raise ValueError(f'not supported value {value}')

+ 9
- 7
api/core/app/segments/segments.py Zobrazit soubor

@@ -1,4 +1,5 @@
import json
import sys
from collections.abc import Mapping, Sequence
from typing import Any

@@ -37,6 +38,10 @@ class Segment(BaseModel):
def markdown(self) -> str:
return str(self.value)

@property
def size(self) -> int:
return sys.getsizeof(self.value)

def to_object(self) -> Any:
return self.value

@@ -105,28 +110,25 @@ class ArraySegment(Segment):
def markdown(self) -> str:
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):
value_type: SegmentType = SegmentType.ARRAY_ANY
value: Sequence[Segment]
value: Sequence[Any]


class ArrayStringSegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_STRING
value: Sequence[StringSegment]
value: Sequence[str]


class ArrayNumberSegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_NUMBER
value: Sequence[FloatSegment | IntegerSegment]
value: Sequence[float | int]


class ArrayObjectSegment(ArraySegment):
value_type: SegmentType = SegmentType.ARRAY_OBJECT
value: Sequence[ObjectSegment]
value: Sequence[Mapping[str, Any]]


class ArrayFileSegment(ArraySegment):

+ 9
- 3
api/core/file/file_obj.py Zobrazit soubor

@@ -1,14 +1,19 @@
import enum
from typing import Optional
from typing import Any, Optional

from pydantic import BaseModel

from core.app.app_config.entities import FileExtraConfig
from core.file.tool_file_parser import ToolFileParser
from core.file.upload_file_parser import UploadFileParser
from core.model_runtime.entities.message_entities import ImagePromptMessageContent
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):
@@ -114,6 +119,7 @@ class FileVar(BaseModel):
)

def _get_data(self, force_url: bool = False) -> Optional[str]:
from models.model import UploadFile
if self.type == FileType.IMAGE:
if self.transfer_method == FileTransferMethod.REMOTE_URL:
return self.url

+ 1
- 2
api/core/file/message_file_parser.py Zobrazit soubor

@@ -5,8 +5,7 @@ from urllib.parse import parse_qs, urlparse

import requests

from core.app.app_config.entities import FileExtraConfig
from core.file.file_obj import FileBelongsTo, FileTransferMethod, FileType, FileVar
from core.file.file_obj import FileBelongsTo, FileExtraConfig, FileTransferMethod, FileType, FileVar
from extensions.ext_database import db
from models.account import Account
from models.model import EndUser, MessageFile, UploadFile

+ 1
- 1
api/core/helper/encrypter.py Zobrazit soubor

@@ -2,7 +2,6 @@ import base64

from extensions.ext_database import db
from libs import rsa
from models.account import Tenant


def obfuscated_token(token: str):
@@ -14,6 +13,7 @@ def obfuscated_token(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()):
raise ValueError(f'Tenant with id {tenant_id} not found')
encrypted_token = rsa.encrypt(token, tenant.encrypt_public_key)

+ 2
- 0
api/core/workflow/entities/node_entities.py Zobrazit soubor

@@ -23,10 +23,12 @@ class NodeType(Enum):
HTTP_REQUEST = 'http-request'
TOOL = 'tool'
VARIABLE_AGGREGATOR = 'variable-aggregator'
# TODO: merge this into VARIABLE_AGGREGATOR
VARIABLE_ASSIGNER = 'variable-assigner'
LOOP = 'loop'
ITERATION = 'iteration'
PARAMETER_EXTRACTOR = 'parameter-extractor'
CONVERSATION_VARIABLE_ASSIGNER = 'assigner'

@classmethod
def value_of(cls, value: str) -> 'NodeType':

+ 7
- 1
api/core/workflow/entities/variable_pool.py Zobrazit soubor

@@ -13,6 +13,7 @@ VariableValue = Union[str, int, float, dict, list, FileVar]

SYSTEM_VARIABLE_NODE_ID = 'sys'
ENVIRONMENT_VARIABLE_NODE_ID = 'env'
CONVERSATION_VARIABLE_NODE_ID = 'conversation'


class VariablePool:
@@ -21,6 +22,7 @@ class VariablePool:
system_variables: Mapping[SystemVariable, Any],
user_inputs: Mapping[str, Any],
environment_variables: Sequence[Variable],
conversation_variables: Sequence[Variable] | None = None,
) -> None:
# system variables
# for example:
@@ -44,9 +46,13 @@ class VariablePool:
self.add((SYSTEM_VARIABLE_NODE_ID, key.value), value)

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

# 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:
"""
Adds a variable to the variable pool.

+ 14
- 8
api/core/workflow/nodes/base_node.py Zobrazit soubor

@@ -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.node_entities import NodeRunResult, NodeType
from core.workflow.entities.variable_pool import VariablePool
from models import WorkflowNodeExecutionStatus


class UserFrom(Enum):
@@ -91,14 +92,19 @@ class BaseNode(ABC):
:param variable_pool: variable pool
:return:
"""
result = self._run(
variable_pool=variable_pool
)

self.node_run_result = result
return result

def publish_text_chunk(self, text: str, value_selector: list[str] = None) -> None:
try:
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),
)

def publish_text_chunk(self, text: str, value_selector: list[str] | None = None) -> None:
"""
Publish text chunk
:param text: chunk text

+ 109
- 0
api/core/workflow/nodes/variable_assigner/__init__.py Zobrazit soubor

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

+ 9
- 13
api/core/workflow/workflow_engine_manager.py Zobrazit soubor

@@ -4,12 +4,11 @@ from collections.abc import Mapping, Sequence
from typing import Any, Optional, cast

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.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.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.workflow_entities import WorkflowNodeAndResult, WorkflowRunState
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.tool.tool_node import ToolNode
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 models.workflow import (
Workflow,
@@ -51,7 +51,8 @@ node_classes: Mapping[NodeType, type[BaseNode]] = {
NodeType.VARIABLE_AGGREGATOR: VariableAggregatorNode,
NodeType.VARIABLE_ASSIGNER: VariableAggregatorNode,
NodeType.ITERATION: IterationNode,
NodeType.PARAMETER_EXTRACTOR: ParameterExtractorNode
NodeType.PARAMETER_EXTRACTOR: ParameterExtractorNode,
NodeType.CONVERSATION_VARIABLE_ASSIGNER: VariableAssignerNode,
}

logger = logging.getLogger(__name__)
@@ -94,10 +95,9 @@ class WorkflowEngineManager:
user_id: str,
user_from: UserFrom,
invoke_from: InvokeFrom,
user_inputs: Mapping[str, Any],
system_inputs: Mapping[SystemVariable, Any],
callbacks: Sequence[WorkflowCallback],
call_depth: int = 0
call_depth: int = 0,
variable_pool: VariablePool,
) -> None:
"""
:param workflow: Workflow instance
@@ -122,12 +122,6 @@ class WorkflowEngineManager:
if not isinstance(graph.get('edges'), 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
if call_depth > workflow_call_max_depth:
@@ -403,6 +397,7 @@ class WorkflowEngineManager:
system_variables={},
user_inputs={},
environment_variables=workflow.environment_variables,
conversation_variables=workflow.conversation_variables,
)

if node_cls is None:
@@ -468,6 +463,7 @@ class WorkflowEngineManager:
system_variables={},
user_inputs={},
environment_variables=workflow.environment_variables,
conversation_variables=workflow.conversation_variables,
)

# variable selector to variable mapping

+ 21
- 0
api/fields/conversation_variable_fields.py Zobrazit soubor

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

+ 4
- 2
api/fields/workflow_fields.py Zobrazit soubor

@@ -32,11 +32,12 @@ class EnvironmentVariableField(fields.Raw):
return value


environment_variable_fields = {
conversation_variable_fields = {
'id': fields.String,
'name': fields.String,
'value': fields.Raw,
'value_type': fields.String(attribute='value_type.value'),
'value': fields.Raw,
'description': fields.String,
}

workflow_fields = {
@@ -50,4 +51,5 @@ workflow_fields = {
'updated_at': TimestampField,
'tool_published': fields.Boolean,
'environment_variables': fields.List(EnvironmentVariableField()),
'conversation_variables': fields.List(fields.Nested(conversation_variable_fields)),
}

+ 51
- 0
api/migrations/versions/2024_08_13_0633-63a83fcf12ba_support_conversation_variables.py Zobrazit soubor

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

+ 8
- 50
api/models/__init__.py Zobrazit soubor

@@ -1,15 +1,19 @@
from enum import Enum

from sqlalchemy import CHAR, TypeDecorator
from sqlalchemy.dialects.postgresql import UUID
from .model import AppMode
from .types import StringUUID
from .workflow import ConversationVariable, WorkflowNodeExecutionStatus

__all__ = ['ConversationVariable', 'StringUUID', 'AppMode', 'WorkflowNodeExecutionStatus']


class CreatedByRole(Enum):
"""
Enum class for createdByRole
"""
ACCOUNT = "account"
END_USER = "end_user"

ACCOUNT = 'account'
END_USER = 'end_user'

@classmethod
def value_of(cls, value: str) -> 'CreatedByRole':
@@ -23,49 +27,3 @@ class CreatedByRole(Enum):
if role.value == value:
return role
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)

+ 2
- 1
api/models/account.py Zobrazit soubor

@@ -4,7 +4,8 @@ import json
from flask_login import UserMixin

from extensions.ext_database import db
from models import StringUUID

from .types import StringUUID


class AccountStatus(str, enum.Enum):

+ 2
- 1
api/models/api_based_extension.py Zobrazit soubor

@@ -1,7 +1,8 @@
import enum

from extensions.ext_database import db
from models import StringUUID

from .types import StringUUID


class APIBasedExtensionPoint(enum.Enum):

+ 4
- 3
api/models/dataset.py Zobrazit soubor

@@ -16,9 +16,10 @@ from configs import dify_config
from core.rag.retrieval.retrival_methods import RetrievalMethod
from extensions.ext_database import db
from extensions.ext_storage import storage
from models import StringUUID
from models.account import Account
from models.model import App, Tag, TagBinding, UploadFile

from .account import Account
from .model import App, Tag, TagBinding, UploadFile
from .types import StringUUID


class Dataset(db.Model):

+ 1
- 1
api/models/model.py Zobrazit soubor

@@ -14,8 +14,8 @@ from core.file.upload_file_parser import UploadFileParser
from extensions.ext_database import db
from libs.helper import generate_string

from . import StringUUID
from .account import Account, Tenant
from .types import StringUUID


class DifySetup(db.Model):

+ 2
- 1
api/models/provider.py Zobrazit soubor

@@ -1,7 +1,8 @@
from enum import Enum

from extensions.ext_database import db
from models import StringUUID

from .types import StringUUID


class ProviderType(Enum):

+ 2
- 1
api/models/source.py Zobrazit soubor

@@ -3,7 +3,8 @@ import json
from sqlalchemy.dialects.postgresql import JSONB

from extensions.ext_database import db
from models import StringUUID

from .types import StringUUID


class DataSourceOauthBinding(db.Model):

+ 2
- 1
api/models/tool.py Zobrazit soubor

@@ -2,7 +2,8 @@ import json
from enum import Enum

from extensions.ext_database import db
from models import StringUUID

from .types import StringUUID


class ToolProviderName(Enum):

+ 3
- 2
api/models/tools.py Zobrazit soubor

@@ -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_entities import ApiProviderSchemaType, WorkflowToolParameterConfiguration
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):

+ 26
- 0
api/models/types.py Zobrazit soubor

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

+ 3
- 2
api/models/web.py Zobrazit soubor

@@ -1,7 +1,8 @@

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

+ 57
- 7
api/models/workflow.py Zobrazit soubor

@@ -3,18 +3,18 @@ from collections.abc import Mapping, Sequence
from enum import Enum
from typing import Any, Optional, Union

from sqlalchemy import func
from sqlalchemy.orm import Mapped

import contexts
from constants import HIDDEN_VALUE
from core.app.segments import (
SecretVariable,
Variable,
factory,
)
from core.app.segments import SecretVariable, Variable, factory
from core.helper import encrypter
from extensions.ext_database import db
from libs import helper
from models import StringUUID
from models.account import Account

from .account import Account
from .types import StringUUID


class CreatedByRole(Enum):
@@ -122,6 +122,7 @@ class Workflow(db.Model):
updated_by = db.Column(StringUUID)
updated_at = db.Column(db.DateTime)
_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
def created_by_account(self):
@@ -249,9 +250,27 @@ class Workflow(db.Model):
'graph': self.graph_dict,
'features': self.features_dict,
'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

@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):
"""
@@ -702,3 +721,34 @@ class WorkflowAppLog(db.Model):
created_by_role = CreatedByRole.value_of(self.created_by_role)
return db.session.get(EndUser, self.created_by) \
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)

+ 3
- 0
api/services/app_dsl_service.py Zobrazit soubor

@@ -238,6 +238,8 @@ class AppDslService:
# init draft workflow
environment_variables_list = workflow_data.get('environment_variables') or []
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()
draft_workflow = workflow_service.sync_draft_workflow(
app_model=app,
@@ -246,6 +248,7 @@ class AppDslService:
unique_hash=None,
account=account,
environment_variables=environment_variables,
conversation_variables=conversation_variables,
)
workflow_service.publish_workflow(
app_model=app,

+ 1
- 1
api/services/workflow/workflow_converter.py Zobrazit soubor

@@ -6,7 +6,6 @@ from core.app.app_config.entities import (
DatasetRetrieveConfigEntity,
EasyUIBasedAppConfig,
ExternalDataVariableEntity,
FileExtraConfig,
ModelConfigEntity,
PromptTemplateEntity,
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.chat.app_config_manager import ChatAppConfigManager
from core.app.apps.completion.app_config_manager import CompletionAppConfigManager
from core.file.file_obj import FileExtraConfig
from core.helper import encrypter
from core.model_runtime.entities.llm_entities import LLMMode
from core.model_runtime.utils.encoders import jsonable_encoder

+ 8
- 4
api/services/workflow_service.py Zobrazit soubor

@@ -72,6 +72,7 @@ class WorkflowService:
unique_hash: Optional[str],
account: Account,
environment_variables: Sequence[Variable],
conversation_variables: Sequence[Variable],
) -> Workflow:
"""
Sync draft workflow
@@ -99,7 +100,8 @@ class WorkflowService:
graph=json.dumps(graph),
features=json.dumps(features),
created_by=account.id,
environment_variables=environment_variables
environment_variables=environment_variables,
conversation_variables=conversation_variables,
)
db.session.add(workflow)
# update draft workflow if found
@@ -109,6 +111,7 @@ class WorkflowService:
workflow.updated_by = account.id
workflow.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
workflow.environment_variables = environment_variables
workflow.conversation_variables = conversation_variables

# commit db session changes
db.session.commit()
@@ -145,7 +148,8 @@ class WorkflowService:
graph=draft_workflow.graph,
features=draft_workflow.features,
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
@@ -336,8 +340,8 @@ class WorkflowService:
)
if not workflow_nodes:
return elapsed_time
for node in workflow_nodes:
elapsed_time += node.elapsed_time

return elapsed_time
return elapsed_time

+ 12
- 2
api/tasks/remove_app_and_related_data_task.py Zobrazit soubor

@@ -1,8 +1,10 @@
import logging
import time
from collections.abc import Callable

import click
from celery import shared_task
from sqlalchemy import delete
from sqlalchemy.exc import SQLAlchemyError

from extensions.ext_database import db
@@ -28,7 +30,7 @@ from models.model import (
)
from models.tools import WorkflowToolProvider
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)
@@ -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_end_users(tenant_id, app_id)
_delete_trace_app_configs(tenant_id, app_id)
_delete_conversation_variables(app_id=app_id)

end_at = time.perf_counter()
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"
)

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 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:
with db.engine.begin() as conn:
rs = conn.execute(db.text(query_sql), params)

+ 63
- 132
api/tests/unit_tests/core/app/segments/test_factory.py Zobrazit soubor

@@ -7,15 +7,16 @@ from core.app.segments import (
ArrayNumberVariable,
ArrayObjectVariable,
ArrayStringVariable,
FileSegment,
FileVariable,
FloatVariable,
IntegerVariable,
NoneSegment,
ObjectSegment,
SecretVariable,
StringVariable,
factory,
)
from core.app.segments.exc import VariableError


def test_string_variable():
@@ -44,7 +45,7 @@ def test_secret_variable():

def test_invalid_value_type():
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)


@@ -77,26 +78,14 @@ def test_object_variable():
'name': 'test_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.',
},
'key1': 'text',
'key2': 2,
},
}
variable = factory.build_variable_from_mapping(mapping)
assert isinstance(variable, ObjectSegment)
assert isinstance(variable.value['key1'], StringVariable)
assert isinstance(variable.value['key2'], IntegerVariable)
assert isinstance(variable.value['key1'], str)
assert isinstance(variable.value['key2'], int)


def test_array_string_variable():
@@ -106,26 +95,14 @@ def test_array_string_variable():
'name': 'test_array',
'description': 'Description of the variable.',
'value': [
{
'id': str(uuid4()),
'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.',
},
'text',
'text',
],
}
variable = factory.build_variable_from_mapping(mapping)
assert isinstance(variable, ArrayStringVariable)
assert isinstance(variable.value[0], StringVariable)
assert isinstance(variable.value[1], StringVariable)
assert isinstance(variable.value[0], str)
assert isinstance(variable.value[1], str)


def test_array_number_variable():
@@ -135,26 +112,14 @@ def test_array_number_variable():
'name': 'test_array',
'description': 'Description of the variable.',
'value': [
{
'id': str(uuid4()),
'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.',
},
1,
2.0,
],
}
variable = factory.build_variable_from_mapping(mapping)
assert isinstance(variable, ArrayNumberVariable)
assert isinstance(variable.value[0], IntegerVariable)
assert isinstance(variable.value[1], FloatVariable)
assert isinstance(variable.value[0], int)
assert isinstance(variable.value[1], float)


def test_array_object_variable():
@@ -165,59 +130,23 @@ def test_array_object_variable():
'description': 'Description of the variable.',
'value': [
{
'id': str(uuid4()),
'value_type': 'object',
'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.',
},
},
'key1': 'text',
'key2': 1,
},
{
'id': str(uuid4()),
'value_type': 'object',
'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.',
},
},
'key1': 'text',
'key2': 1,
},
],
}
variable = factory.build_variable_from_mapping(mapping)
assert isinstance(variable, ArrayObjectVariable)
assert isinstance(variable.value[0], ObjectSegment)
assert isinstance(variable.value[1], ObjectSegment)
assert isinstance(variable.value[0].value['key1'], StringVariable)
assert isinstance(variable.value[0].value['key2'], IntegerVariable)
assert isinstance(variable.value[1].value['key1'], StringVariable)
assert isinstance(variable.value[1].value['key2'], IntegerVariable)
assert isinstance(variable.value[0], dict)
assert isinstance(variable.value[1], dict)
assert isinstance(variable.value[0]['key1'], str)
assert isinstance(variable.value[0]['key2'], int)
assert isinstance(variable.value[1]['key1'], str)
assert isinstance(variable.value[1]['key2'], int)


def test_file_variable():
@@ -257,51 +186,53 @@ def test_array_file_variable():
'value': [
{
'id': str(uuid4()),
'name': 'file',
'value_type': 'file',
'value': {
'id': str(uuid4()),
'tenant_id': 'tenant_id',
'type': 'image',
'transfer_method': 'local_file',
'url': 'url',
'related_id': 'related_id',
'extra_config': {
'image_config': {
'width': 100,
'height': 100,
},
'tenant_id': 'tenant_id',
'type': 'image',
'transfer_method': 'local_file',
'url': 'url',
'related_id': 'related_id',
'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()),
'name': 'file',
'value_type': 'file',
'value': {
'id': str(uuid4()),
'tenant_id': 'tenant_id',
'type': 'image',
'transfer_method': 'local_file',
'url': 'url',
'related_id': 'related_id',
'extra_config': {
'image_config': {
'width': 100,
'height': 100,
},
'tenant_id': 'tenant_id',
'type': 'image',
'transfer_method': 'local_file',
'url': 'url',
'related_id': 'related_id',
'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)
assert isinstance(variable, ArrayFileVariable)
assert isinstance(variable.value[0], FileVariable)
assert isinstance(variable.value[1], FileVariable)
assert isinstance(variable.value[0], FileSegment)
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
- 2
api/tests/unit_tests/core/prompt/test_advanced_prompt_transform.py Zobrazit soubor

@@ -2,8 +2,8 @@ from unittest.mock import MagicMock

import pytest

from core.app.app_config.entities import FileExtraConfig, ModelConfigEntity
from core.file.file_obj import FileTransferMethod, FileType, FileVar
from core.app.app_config.entities import ModelConfigEntity
from core.file.file_obj import FileExtraConfig, FileTransferMethod, FileType, FileVar
from core.memory.token_buffer_memory import TokenBufferMemory
from core.model_runtime.entities.message_entities import AssistantPromptMessage, PromptMessageRole, UserPromptMessage
from core.prompt.advanced_prompt_transform import AdvancedPromptTransform

+ 150
- 0
api/tests/unit_tests/core/workflow/nodes/test_variable_assigner.py Zobrazit soubor

@@ -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
- 0
api/tests/unit_tests/models/test_conversation_variable.py Zobrazit soubor

@@ -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
- 1
web/app/components/base/badge.tsx Zobrazit soubor

@@ -4,16 +4,19 @@ import cn from '@/utils/classnames'
type BadgeProps = {
className?: string
text: string
uppercase?: boolean
}

const Badge = ({
className,
text,
uppercase = true,
}: BadgeProps) => {
return (
<div
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,
)}
>

+ 8
- 0
web/app/components/base/icons/assets/vender/line/others/bubble-x.svg Zobrazit soubor

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

+ 3
- 0
web/app/components/base/icons/assets/vender/line/others/long-arrow-left.svg Zobrazit soubor

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

+ 3
- 0
web/app/components/base/icons/assets/vender/line/others/long-arrow-right.svg Zobrazit soubor

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

+ 9
- 0
web/app/components/base/icons/assets/vender/workflow/assigner.svg Zobrazit soubor

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

+ 57
- 0
web/app/components/base/icons/src/vender/line/others/BubbleX.json Zobrazit soubor

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

+ 16
- 0
web/app/components/base/icons/src/vender/line/others/BubbleX.tsx Zobrazit soubor

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

+ 27
- 0
web/app/components/base/icons/src/vender/line/others/LongArrowLeft.json Zobrazit soubor

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

+ 16
- 0
web/app/components/base/icons/src/vender/line/others/LongArrowLeft.tsx Zobrazit soubor

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

+ 27
- 0
web/app/components/base/icons/src/vender/line/others/LongArrowRight.json Zobrazit soubor

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

+ 16
- 0
web/app/components/base/icons/src/vender/line/others/LongArrowRight.tsx Zobrazit soubor

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

+ 3
- 0
web/app/components/base/icons/src/vender/line/others/index.ts Zobrazit soubor

@@ -1,8 +1,11 @@
export { default as Apps02 } from './Apps02'
export { default as BubbleX } from './BubbleX'
export { default as Colors } from './Colors'
export { default as DragHandle } from './DragHandle'
export { default as Env } from './Env'
export { default as Exchange02 } from './Exchange02'
export { default as FileCode } from './FileCode'
export { default as Icon3Dots } from './Icon3Dots'
export { default as LongArrowLeft } from './LongArrowLeft'
export { default as LongArrowRight } from './LongArrowRight'
export { default as Tools } from './Tools'

+ 68
- 0
web/app/components/base/icons/src/vender/workflow/Assigner.json Zobrazit soubor

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

+ 16
- 0
web/app/components/base/icons/src/vender/workflow/Assigner.tsx Zobrazit soubor

@@ -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
- 0
web/app/components/base/icons/src/vender/workflow/index.ts Zobrazit soubor

@@ -1,4 +1,5 @@
export { default as Answer } from './Answer'
export { default as Assigner } from './Assigner'
export { default as Code } from './Code'
export { default as End } from './End'
export { default as Home } from './Home'

+ 3
- 3
web/app/components/base/input/index.tsx Zobrazit soubor

@@ -2,7 +2,7 @@
import type { SVGProps } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
import cn from 'classnames'

type InputProps = {
placeholder?: string
@@ -27,10 +27,10 @@ const Input = ({ value, defaultValue, onChange, className = '', wrapperClassName
const { t } = useTranslation()
return (
<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
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')}
value={localValue}
onChange={(e) => {

+ 0
- 7
web/app/components/base/input/style.module.css Zobrazit soubor

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

+ 1
- 1
web/app/components/base/prompt-editor/index.tsx Zobrazit soubor

@@ -144,7 +144,7 @@ const PromptEditor: FC<PromptEditorProps> = ({

return (
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
<div className='relative h-full'>
<div className='relative min-h-5'>
<RichTextPlugin
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} />}

+ 10
- 8
web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx Zobrazit soubor

@@ -21,10 +21,10 @@ import {
} from './index'
import cn from '@/utils/classnames'
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 { 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'

type WorkflowVariableBlockComponentProps = {
@@ -52,6 +52,7 @@ const WorkflowVariableBlockComponent = ({
const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState<WorkflowNodesMap>(workflowNodesMap)
const node = localWorkflowNodesMap![variables[0]]
const isEnv = isENV(variables)
const isChatVar = isConversationVar(variables)

useEffect(() => {
if (!editor.hasNodes([WorkflowVariableBlockNode]))
@@ -75,11 +76,11 @@ const WorkflowVariableBlockComponent = ({
className={cn(
'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',
!node && !isEnv && '!border-[#F04438] !bg-[#FEF3F2]',
!node && !isEnv && !isChatVar && '!border-[#F04438] !bg-[#FEF3F2]',
)}
ref={ref}
>
{!isEnv && (
{!isEnv && !isChatVar && (
<div className='flex items-center'>
{
node?.type && (
@@ -97,11 +98,12 @@ const WorkflowVariableBlockComponent = ({
</div>
)}
<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' />}
<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]' />
)
}
@@ -109,7 +111,7 @@ const WorkflowVariableBlockComponent = ({
</div>
)

if (!node && !isEnv) {
if (!node && !isEnv && !isChatVar) {
return (
<TooltipPlus popupContent={t('workflow.errorMsg.invalidVariable')}>
{Item}

+ 3
- 0
web/app/components/workflow/block-icon.tsx Zobrazit soubor

@@ -3,6 +3,7 @@ import { memo } from 'react'
import { BlockEnum } from './types'
import {
Answer,
Assigner,
Code,
End,
Home,
@@ -43,6 +44,7 @@ const getIcon = (type: BlockEnum, className: string) => {
[BlockEnum.TemplateTransform]: <TemplatingTransform className={className} />,
[BlockEnum.VariableAssigner]: <VariableX className={className} />,
[BlockEnum.VariableAggregator]: <VariableX className={className} />,
[BlockEnum.Assigner]: <Assigner className={className} />,
[BlockEnum.Tool]: <VariableX className={className} />,
[BlockEnum.Iteration]: <Iteration 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.VariableAssigner]: 'bg-[#2E90FA]',
[BlockEnum.VariableAggregator]: 'bg-[#2E90FA]',
[BlockEnum.Assigner]: 'bg-[#2E90FA]',
[BlockEnum.ParameterExtractor]: 'bg-[#2E90FA]',
}
const BlockIcon: FC<BlockIconProps> = ({

+ 5
- 0
web/app/components/workflow/block-selector/constants.tsx Zobrazit soubor

@@ -59,6 +59,11 @@ export const BLOCKS: Block[] = [
type: BlockEnum.VariableAggregator,
title: 'Variable Aggregator',
},
{
classification: BlockClassificationEnum.Transform,
type: BlockEnum.Assigner,
title: 'Variable Assigner',
},
{
classification: BlockClassificationEnum.Transform,
type: BlockEnum.ParameterExtractor,

+ 16
- 0
web/app/components/workflow/constants.ts Zobrazit soubor

@@ -12,6 +12,7 @@ import HttpRequestDefault from './nodes/http/default'
import ParameterExtractorDefault from './nodes/parameter-extractor/default'
import ToolDefault from './nodes/tool/default'
import VariableAssignerDefault from './nodes/variable-assigner/default'
import AssignerDefault from './nodes/assigner/default'
import EndNodeDefault from './nodes/end/default'
import IterationDefault from './nodes/iteration/default'

@@ -133,6 +134,15 @@ export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
getAvailableNextNodes: VariableAssignerDefault.getAvailableNextNodes,
checkValid: VariableAssignerDefault.checkValid,
},
[BlockEnum.Assigner]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: AssignerDefault.getAvailablePrevNodes,
getAvailableNextNodes: AssignerDefault.getAvailableNextNodes,
checkValid: AssignerDefault.checkValid,
},
[BlockEnum.VariableAggregator]: {
author: 'Dify',
about: '',
@@ -268,6 +278,12 @@ export const NODES_INITIAL_DATA = {
output_type: '',
...VariableAssignerDefault.defaultValue,
},
[BlockEnum.Assigner]: {
type: BlockEnum.Assigner,
title: '',
desc: '',
...AssignerDefault.defaultValue,
},
[BlockEnum.Tool]: {
type: BlockEnum.Tool,
title: '',

+ 24
- 0
web/app/components/workflow/header/chat-variable-button.tsx Zobrazit soubor

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

+ 6
- 4
web/app/components/workflow/header/env-button.tsx Zobrazit soubor

@@ -1,21 +1,23 @@
import { memo } from 'react'
import Button from '@/app/components/base/button'
import { Env } from '@/app/components/base/icons/src/vender/line/others'
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 setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel)

const handleClick = () => {
setShowEnvPanel(true)
setShowChatVariablePanel(false)
setShowDebugAndPreviewPanel(false)
}

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' />
</div>
</Button>
)
}


+ 7
- 3
web/app/components/workflow/header/index.tsx Zobrazit soubor

@@ -19,6 +19,7 @@ import {
import type { StartNodeType } from '../nodes/start/types'
import {
useChecklistBeforePublish,
useIsChatMode,
useNodesReadOnly,
useNodesSyncDraft,
useWorkflowMode,
@@ -31,6 +32,7 @@ import EditingTitle from './editing-title'
import RunningTitle from './running-title'
import RestoringTitle from './restoring-title'
import ViewHistory from './view-history'
import ChatVariableButton from './chat-variable-button'
import EnvButton from './env-button'
import Button from '@/app/components/base/button'
import { useStore as useAppStore } from '@/app/components/app/store'
@@ -44,7 +46,8 @@ const Header: FC = () => {
const appDetail = useAppStore(s => s.appDetail)
const appSidebarExpand = useAppStore(s => s.appSidebarExpand)
const appID = appDetail?.id
const { getNodesReadOnly } = useNodesReadOnly()
const isChatMode = useIsChatMode()
const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly()
const publishedAt = useStore(s => s.publishedAt)
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
const toolPublished = useStore(s => s.toolPublished)
@@ -165,7 +168,8 @@ const Header: FC = () => {
{
normal && (
<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>
<RunAndHistory />
<Button className='text-components-button-secondary-text' onClick={handleShowFeatures}>
@@ -176,7 +180,7 @@ const Header: FC = () => {
{...{
publishedAt,
draftUpdatedAt,
disabled: Boolean(getNodesReadOnly()),
disabled: nodesReadOnly,
toolPublished,
inputs: variables,
onRefreshData: handleToolConfigureUpdate,

+ 2
- 0
web/app/components/workflow/hooks/use-nodes-sync-draft.ts Zobrazit soubor

@@ -31,6 +31,7 @@ export const useNodesSyncDraft = () => {
const [x, y, zoom] = transform
const {
appId,
conversationVariables,
environmentVariables,
syncWorkflowDraftHash,
} = workflowStore.getState()
@@ -82,6 +83,7 @@ export const useNodesSyncDraft = () => {
file_upload: features.file,
},
environment_variables: environmentVariables,
conversation_variables: conversationVariables,
hash: syncWorkflowDraftHash,
},
}

+ 3
- 0
web/app/components/workflow/hooks/use-workflow-interactions.ts Zobrazit soubor

@@ -68,6 +68,7 @@ export const useWorkflowUpdate = () => {
setIsSyncingWorkflowDraft,
setEnvironmentVariables,
setEnvSecrets,
setConversationVariables,
} = workflowStore.getState()
setIsSyncingWorkflowDraft(true)
fetchWorkflowDraft(`/apps/${appId}/workflows/draft`).then((response) => {
@@ -78,6 +79,8 @@ export const useWorkflowUpdate = () => {
return acc
}, {} as Record<string, string>))
setEnvironmentVariables(response.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [])
// #TODO chatVar sync#
setConversationVariables(response.conversation_variables || [])
}).finally(() => setIsSyncingWorkflowDraft(false))
}, [handleUpdateWorkflowCanvas, workflowStore])


+ 2
- 0
web/app/components/workflow/hooks/use-workflow-start-run.tsx Zobrazit soubor

@@ -67,9 +67,11 @@ export const useWorkflowStartRun = () => {
setShowDebugAndPreviewPanel,
setHistoryWorkflowData,
setShowEnvPanel,
setShowChatVariablePanel,
} = workflowStore.getState()

setShowEnvPanel(false)
setShowChatVariablePanel(false)

if (showDebugAndPreviewPanel)
handleCancelDebugAndPreviewPanel()

+ 7
- 2
web/app/components/workflow/hooks/use-workflow-variables.ts Zobrazit soubor

@@ -12,6 +12,7 @@ import type {
export const useWorkflowVariables = () => {
const { t } = useTranslation()
const environmentVariables = useStore(s => s.environmentVariables)
const conversationVariables = useStore(s => s.conversationVariables)

const getNodeAvailableVars = useCallback(({
parentNode,
@@ -19,12 +20,14 @@ export const useWorkflowVariables = () => {
isChatMode,
filterVar,
hideEnv,
hideChatVar,
}: {
parentNode?: Node | null
beforeNodes: Node[]
isChatMode: boolean
filterVar: (payload: Var, selector: ValueSelector) => boolean
hideEnv?: boolean
hideChatVar?: boolean
}): NodeOutPutVar[] => {
return toNodeAvailableVars({
parentNode,
@@ -32,9 +35,10 @@ export const useWorkflowVariables = () => {
beforeNodes,
isChatMode,
environmentVariables: hideEnv ? [] : environmentVariables,
conversationVariables: (isChatMode && !hideChatVar) ? conversationVariables : [],
filterVar,
})
}, [environmentVariables, t])
}, [conversationVariables, environmentVariables, t])

const getCurrentVariableType = useCallback(({
parentNode,
@@ -59,8 +63,9 @@ export const useWorkflowVariables = () => {
isChatMode,
isConstant,
environmentVariables,
conversationVariables,
})
}, [environmentVariables])
}, [conversationVariables, environmentVariables])

return {
getNodeAvailableVars,

+ 3
- 0
web/app/components/workflow/hooks/use-workflow.ts Zobrazit soubor

@@ -478,6 +478,8 @@ export const useWorkflowInit = () => {
return acc
}, {} as Record<string, string>),
environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [],
// #TODO chatVar sync#
conversationVariables: res.conversation_variables || [],
})
setSyncWorkflowDraftHash(res.hash)
setIsLoading(false)
@@ -498,6 +500,7 @@ export const useWorkflowInit = () => {
retriever_resource: { enabled: true },
},
environment_variables: [],
conversation_variables: [],
},
}).then((res) => {
workflowStore.getState().setDraftUpdatedAt(res.updated_at)

+ 1
- 0
web/app/components/workflow/nodes/_base/components/add-variable-popup-with-position.tsx Zobrazit soubor

@@ -64,6 +64,7 @@ const AddVariablePopupWithPosition = ({
} as any,
],
hideEnv: true,
hideChatVar: true,
isChatMode,
filterVar: filterVar(outputType as VarType),
})

+ 22
- 13
web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx Zobrazit soubor

@@ -18,6 +18,8 @@ import { useFeatures } from '@/app/components/base/features/hooks'
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
import { Line3 } from '@/app/components/base/icons/src/public/common'
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 = {
payload: InputVar
@@ -56,22 +58,24 @@ const FormItem: FC<Props> = ({
}, [value, onChange])
const nodeKey = (() => {
if (typeof payload.label === 'object') {
const { nodeType, nodeName, variable } = payload.label
const { nodeType, nodeName, variable, isChatVar } = payload.label
return (
<div className='h-full flex items-center'>
<div className='flex items-center'>
<div className='p-[1px]'>
<VarBlockIcon type={nodeType || BlockEnum.Start} />
{!isChatVar && (
<div className='flex items-center'>
<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 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'>
<Variable02 className='w-3.5 h-3.5' />
<div className='ml-0.5 text-xs font-medium max-w-[150px] truncate' title={variable} >
{!isChatVar && <Variable02 className='w-3.5 h-3.5' />}
{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}
</div>
</div>
@@ -86,7 +90,12 @@ const FormItem: FC<Props> = ({
const isIterator = type === InputVarType.iterator
return (
<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'>
{
type === InputVarType.textInput && (

+ 2
- 2
web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx Zobrazit soubor

@@ -15,7 +15,7 @@ const CODE_EDITOR_LINE_HEIGHT = 18

export type Props = {
value?: string | object
placeholder?: string
placeholder?: JSX.Element | string
onChange?: (value: string) => void
title?: JSX.Element
language: CodeLanguage
@@ -167,7 +167,7 @@ const CodeEditor: FC<Props> = ({
}}
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>}
</>
)


+ 4
- 2
web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx Zobrazit soubor

@@ -26,6 +26,7 @@ type Props = {
justVar?: boolean
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
insertVarTipToLeft?: boolean
}

const Editor: FC<Props> = ({
@@ -40,6 +41,7 @@ const Editor: FC<Props> = ({
readOnly,
nodesOutputVars,
availableNodes = [],
insertVarTipToLeft,
}) => {
const { t } = useTranslation()

@@ -106,12 +108,12 @@ const Editor: FC<Props> = ({
{/* to patch Editor not support dynamic change editable status */}
{readOnly && <div className='absolute inset-0 z-10'></div>}
{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
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'>
<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>
</TooltipPlus>
</div>

+ 1
- 1
web/app/components/workflow/nodes/_base/components/option-card.tsx Zobrazit soubor

@@ -45,7 +45,7 @@ const OptionCard: FC<Props> = ({
return (
<div
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 && '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',

+ 7
- 5
web/app/components/workflow/nodes/_base/components/readonly-input-with-select-var.tsx Zobrazit soubor

@@ -5,10 +5,10 @@ import cn from 'classnames'
import { useWorkflow } from '../../../hooks'
import { BlockEnum } from '../../../types'
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 { 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 = {
nodeId: string
value: string
@@ -42,13 +42,14 @@ const ReadonlyInputWithSelectVar: FC<Props> = ({
const value = vars[index].split('.')
const isSystem = isSystemVar(value)
const isEnv = isENV(value)
const isChatVar = isConversationVar(value)
const node = (isSystem ? startNode : getNodeInfoById(availableNodes, value[0]))?.data
const varName = `${isSystem ? 'sys.' : ''}${value[value.length - 1]}`

return (<span key={index}>
<span className='relative top-[-3px] leading-[16px]'>{str}</span>
<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='p-[1px]'>
<VarBlockIcon
@@ -61,9 +62,10 @@ const ReadonlyInputWithSelectVar: FC<Props> = ({
</div>
)}
<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' />}
<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>
</span>)

+ 6
- 3
web/app/components/workflow/nodes/_base/components/selector.tsx Zobrazit soubor

@@ -10,6 +10,7 @@ type Item = {
label: string
}
type Props = {
className?: string
trigger?: JSX.Element
DropDownIcon?: any
noLeft?: boolean
@@ -27,6 +28,7 @@ type Props = {
}

const TypeSelector: FC<Props> = ({
className,
trigger,
DropDownIcon = ChevronSelectorVertical,
noLeft,
@@ -50,11 +52,12 @@ const TypeSelector: FC<Props> = ({
setHide()
}, ref)
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
? (
<div
onClick={toggleShow}
className={cn(!readonly && 'cursor-pointer')}
>
{trigger}
</div>
@@ -63,13 +66,13 @@ const TypeSelector: FC<Props> = ({
<div
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')}>
<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 ' />}
</div>
)}

{(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 => (
<div
key={item.value}

+ 6
- 4
web/app/components/workflow/nodes/_base/components/variable-tag.tsx Zobrazit soubor

@@ -10,8 +10,8 @@ import type {
import { BlockEnum } from '@/app/components/workflow/types'
import { Line3 } from '@/app/components/base/icons/src/public/common'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { Env } from '@/app/components/base/icons/src/vender/line/others'
import { isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import cn from '@/utils/classnames'

type VariableTagProps = {
@@ -30,12 +30,13 @@ const VariableTag = ({
return nodes.find(node => node.id === valueSelector[0])
}, [nodes, valueSelector])
const isEnv = isENV(valueSelector)
const isChatVar = isConversationVar(valueSelector)

const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')

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'>
{!isEnv && (
{!isEnv && !isChatVar && (
<>
{node && (
<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' />}
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
<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}
>
{variableName}

+ 3
- 3
web/app/components/workflow/nodes/_base/components/variable/constant-field.tsx Zobrazit soubor

@@ -9,14 +9,14 @@ import type { Var } from '@/app/components/workflow/types'
import { SimpleSelect } from '@/app/components/base/select'

type Props = {
schema: CredentialFormSchema
schema: Partial<CredentialFormSchema>
readonly: boolean
value: string
onChange: (value: string | number, varKindType: VarKindType, varInfo?: Var) => void
}

const ConstantField: FC<Props> = ({
schema,
schema = {} as CredentialFormSchema,
readonly,
value,
onChange,
@@ -47,7 +47,7 @@ const ConstantField: FC<Props> = ({
{schema.type === FormTypeEnum.textNumber && (
<input
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}
onChange={handleStaticChange}
readOnly={readonly}

+ 47
- 4
web/app/components/workflow/nodes/_base/components/variable/utils.ts Zobrazit soubor

@@ -15,7 +15,7 @@ import type { ParameterExtractorNodeType } from '../../../parameter-extractor/ty
import type { IterationNodeType } from '../../../iteration/types'
import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/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 {
HTTP_REQUEST_OUTPUT_STRUCT,
@@ -38,6 +38,10 @@ export const isENV = (valueSelector: ValueSelector) => {
return valueSelector[0] === 'env'
}

export const isConversationVar = (valueSelector: ValueSelector) => {
return valueSelector[0] === 'conversation'
}

const inputVarTypeToVarType = (type: InputVarType): VarType => {
if (type === InputVarType.number)
return VarType.number
@@ -246,13 +250,32 @@ const formatItem = (
}) as Var[]
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]
res.vars = res.vars.filter((v) => {
const { children } = v
if (!children)
return filterVar(v, selector)
if (!children) {
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)
return obj?.children && obj?.children.length > 0
@@ -271,6 +294,7 @@ export const toNodeOutputVars = (
isChatMode: boolean,
filterVar = (_payload: Var, _selector: ValueSelector) => true,
environmentVariables: EnvironmentVariable[] = [],
conversationVariables: ConversationVariable[] = [],
): NodeOutPutVar[] => {
// ENV_NODE data format
const ENV_NODE = {
@@ -281,9 +305,19 @@ export const toNodeOutputVars = (
envList: environmentVariables,
},
}
// CHAT_VAR_NODE data format
const CHAT_VAR_NODE = {
id: 'conversation',
data: {
title: 'CONVERSATION',
type: 'conversation',
chatVarList: conversationVariables,
},
}
const res = [
...nodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node.data.type)),
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
...((isChatMode && conversationVariables.length) > 0 ? [CHAT_VAR_NODE] : []),
].map((node) => {
return {
...formatItem(node, isChatMode, filterVar),
@@ -348,6 +382,7 @@ export const getVarType = ({
isChatMode,
isConstant,
environmentVariables = [],
conversationVariables = [],
}:
{
valueSelector: ValueSelector
@@ -357,6 +392,7 @@ export const getVarType = ({
isChatMode: boolean
isConstant?: boolean
environmentVariables?: EnvironmentVariable[]
conversationVariables?: ConversationVariable[]
}): VarType => {
if (isConstant)
return VarType.string
@@ -366,6 +402,7 @@ export const getVarType = ({
isChatMode,
undefined,
environmentVariables,
conversationVariables,
)

const isIterationInnerVar = parentNode?.data.type === BlockEnum.Iteration
@@ -388,6 +425,7 @@ export const getVarType = ({
}
const isSystem = isSystemVar(valueSelector)
const isEnv = isENV(valueSelector)
const isChatVar = isConversationVar(valueSelector)
const startNode = availableNodes.find((node: any) => {
return node.data.type === BlockEnum.Start
})
@@ -400,7 +438,7 @@ export const getVarType = ({

let type: VarType = VarType.string
let curr: any = targetVar.vars
if (isSystem || isEnv) {
if (isSystem || isEnv || isChatVar) {
return curr.find((v: any) => v.variable === (valueSelector as ValueSelector).join('.'))?.type
}
else {
@@ -426,6 +464,7 @@ export const toNodeAvailableVars = ({
beforeNodes,
isChatMode,
environmentVariables,
conversationVariables,
filterVar,
}: {
parentNode?: Node | null
@@ -435,6 +474,8 @@ export const toNodeAvailableVars = ({
isChatMode: boolean
// env
environmentVariables?: EnvironmentVariable[]
// chat var
conversationVariables?: ConversationVariable[]
filterVar: (payload: Var, selector: ValueSelector) => boolean
}): NodeOutPutVar[] => {
const beforeNodesOutputVars = toNodeOutputVars(
@@ -442,6 +483,7 @@ export const toNodeAvailableVars = ({
isChatMode,
filterVar,
environmentVariables,
conversationVariables,
)
const isInIteration = parentNode?.data.type === BlockEnum.Iteration
if (isInIteration) {
@@ -453,6 +495,7 @@ export const toNodeAvailableVars = ({
availableNodes: beforeNodes,
isChatMode,
environmentVariables,
conversationVariables,
})
const iterationVar = {
nodeId: iterationNode?.id,

+ 73
- 43
web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx Zobrazit soubor

@@ -9,7 +9,7 @@ import {
import produce from 'immer'
import { useStoreApi } from 'reactflow'
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 cn from '@/utils/classnames'
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 { VarBlockIcon } from '@/app/components/workflow/block-icon'
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 {
PortalToFollowElem,
@@ -32,6 +32,7 @@ import {
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
import AddButton from '@/app/components/base/button/add-button'
import Badge from '@/app/components/base/badge'
const TRIGGER_DEFAULT_WIDTH = 227

type Props = {
@@ -49,7 +50,8 @@ type Props = {
availableNodes?: Node[]
availableVars?: NodeOutPutVar[]
isAddBtnTrigger?: boolean
schema?: CredentialFormSchema
schema?: Partial<CredentialFormSchema>
valueTypePlaceHolder?: string
}

const VarReferencePicker: FC<Props> = ({
@@ -57,7 +59,7 @@ const VarReferencePicker: FC<Props> = ({
readonly,
className,
isShowNodeName,
value,
value = [],
onOpen = () => { },
onChange,
isSupportConstantValue,
@@ -68,6 +70,7 @@ const VarReferencePicker: FC<Props> = ({
availableVars,
isAddBtnTrigger,
schema,
valueTypePlaceHolder,
}) => {
const { t } = useTranslation()
const store = useStoreApi()
@@ -99,7 +102,6 @@ const VarReferencePicker: FC<Props> = ({

const [varKindType, setVarKindType] = useState<VarKindType>(defaultVarKindType)
const isConstant = isSupportConstantValue && varKindType === VarKindType.constant

const outputVars = useMemo(() => {
if (availableVars)
return availableVars
@@ -215,6 +217,7 @@ const VarReferencePicker: FC<Props> = ({
})

const isEnv = isENV(value as ValueSelector)
const isChatVar = isConversationVar(value as ValueSelector)

// 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff
const availableWidth = triggerWidth - 56
@@ -227,6 +230,8 @@ const VarReferencePicker: FC<Props> = ({
return [maxNodeNameWidth, maxVarNameWidth, maxTypeWidth]
})()

const WrapElem = isSupportConstantValue ? 'div' : PortalToFollowElemTrigger
const VarPickerWrap = !isSupportConstantValue ? 'div' : PortalToFollowElemTrigger
return (
<div className={cn(className, !readonly && 'cursor-pointer')}>
<PortalToFollowElem
@@ -234,7 +239,7 @@ const VarReferencePicker: FC<Props> = ({
onOpenChange={setOpen}
placement={isAddBtnTrigger ? 'bottom-end' : 'bottom-start'}
>
<PortalToFollowElemTrigger onClick={() => {
<WrapElem onClick={() => {
if (readonly)
return
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
@@ -245,23 +250,28 @@ const VarReferencePicker: FC<Props> = ({
<AddButton onClick={() => { }}></AddButton>
</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
? <div onClick={(e) => {
e.stopPropagation()
setOpen(false)
setControlFocus(Date.now())
}} className='mr-1 flex items-center space-x-1'>
}} className='h-full mr-1 flex items-center space-x-1'>
<TypeSelector
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}
DropDownIcon={RiArrowDownSLine}
value={varKindType}
options={varKindTypes}
onChange={handleVarKindTypeChange}
showChecked
/>
<div className='h-4 w-px bg-black/5'></div>
</div>
: (!hasValue && <div className='ml-1.5 mr-1'>
<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')}>
{hasValue
? (
<>
{isShowNodeName && !isEnv && (
<div className='flex items-center'>
<div className='p-[1px]'>
<VarBlockIcon
className='!text-gray-900'
type={outputVarNode?.type || BlockEnum.Start}
/>
<VarPickerWrap
onClick={() => {
if (readonly)
return
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
}}
className='grow h-full'
>
<div ref={isSupportConstantValue ? triggerRef : null} className={cn('h-full', isSupportConstantValue && 'flex items-center pl-1 py-1 rounded-lg bg-gray-100')}>
<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 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' />}
<div className={cn('ml-0.5 text-xs font-medium truncate', isEnv && '!text-gray-900')} title={varName} style={{
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>
<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>
</div>

</VarPickerWrap>
)}
{(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'
@@ -315,8 +338,15 @@ const VarReferencePicker: FC<Props> = ({
>
<RiCloseLine className='w-3.5 h-3.5 text-gray-500 group-hover:text-gray-800' />
</div>)}
{!hasValue && valueTypePlaceHolder && (
<Badge
className=' absolute right-1 top-[50%] translate-y-[-50%] capitalize'
text={valueTypePlaceHolder}
uppercase={false}
/>
)}
</div>)}
</PortalToFollowElemTrigger>
</WrapElem>
<PortalToFollowElemContent style={{
zIndex: 100,
}}>

+ 16
- 7
web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx Zobrazit soubor

@@ -16,7 +16,7 @@ import {
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
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'

type ObjectChildrenProps = {
@@ -51,6 +51,7 @@ const Item: FC<ItemProps> = ({
const isObj = itemData.type === VarType.object && itemData.children && itemData.children.length > 0
const isSys = itemData.variable.startsWith('sys.')
const isEnv = itemData.variable.startsWith('env.')
const isChatVar = itemData.variable.startsWith('conversation.')
const itemRef = useRef(null)
const [isItemHovering, setIsItemHovering] = useState(false)
const _ = useHover(itemRef, {
@@ -79,7 +80,7 @@ const Item: FC<ItemProps> = ({
}, [isHovering])
const handleChosen = (e: React.MouseEvent) => {
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)
}
else {
@@ -100,13 +101,21 @@ const Item: FC<ItemProps> = ({
isHovering && (isObj ? 'bg-primary-50' : 'bg-gray-50'),
'relative w-full flex items-center h-6 pl-3 rounded-md cursor-pointer')
}
// style={{ width: itemWidth || 252 }}
onClick={handleChosen}
>
<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' />}
<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 className='ml-1 shrink-0 text-xs font-normal text-gray-500 capitalize'>{itemData.type}</div>
{isObj && (
@@ -211,7 +220,7 @@ const VarReferenceVars: FC<Props> = ({
const [searchText, setSearchText] = useState('')

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
}).filter((node) => {
if (!searchText)
@@ -222,7 +231,7 @@ const VarReferenceVars: FC<Props> = ({
})
return children.length > 0
}).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) {
const searchTextLower = searchText.toLowerCase()
if (!node.title.toLowerCase().includes(searchTextLower))

+ 2
- 0
web/app/components/workflow/nodes/_base/hooks/use-node-help-link.ts Zobrazit soubor

@@ -24,6 +24,7 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
[BlockEnum.TemplateTransform]: 'template',
[BlockEnum.VariableAssigner]: 'variable_assigner',
[BlockEnum.VariableAggregator]: 'variable_assigner',
[BlockEnum.Assigner]: 'variable_assignment',
[BlockEnum.Iteration]: 'iteration',
[BlockEnum.ParameterExtractor]: 'parameter_extractor',
[BlockEnum.HttpRequest]: 'http_request',
@@ -43,6 +44,7 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => {
[BlockEnum.TemplateTransform]: 'template',
[BlockEnum.VariableAssigner]: 'variable-assigner',
[BlockEnum.VariableAggregator]: 'variable-assigner',
[BlockEnum.Assigner]: 'variable-assignment',
[BlockEnum.Iteration]: 'iteration',
[BlockEnum.ParameterExtractor]: 'parameter-extractor',
[BlockEnum.HttpRequest]: 'http-request',

+ 7
- 4
web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts Zobrazit soubor

@@ -7,12 +7,12 @@ import {
useNodeDataUpdate,
useWorkflow,
} 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 { BlockEnum, InputVarType, NodeRunningStatus, VarType } from '@/app/components/workflow/types'
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 Toast from '@/app/components/base/toast'
import LLMDefault from '@/app/components/workflow/nodes/llm/default'
@@ -95,12 +95,13 @@ const useOneStepRun = <T>({
}: Params<T>) => {
const { t } = useTranslation()
const { getBeforeNodesInSameBranch, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() as any
const conversationVariables = useStore(s => s.conversationVariables)
const isChatMode = useIsChatMode()
const isIteration = data.type === BlockEnum.Iteration

const availableNodes = getBeforeNodesInSameBranch(id)
const availableNodesIncludeParent = getBeforeNodesInSameBranchIncludeParent(id)
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode)
const allOutputVars = toNodeOutputVars(availableNodes, isChatMode, undefined, undefined, conversationVariables)
const getVar = (valueSelector: ValueSelector): Var | undefined => {
let res: Var | undefined
const isSystem = valueSelector[0] === 'sys'
@@ -116,7 +117,8 @@ const useOneStepRun = <T>({

valueSelector.slice(1).forEach((key, i) => {
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) {
res = curr
}
@@ -369,6 +371,7 @@ const useOneStepRun = <T>({
nodeType: varInfo?.type,
nodeName: varInfo?.title || availableNodesIncludeParent[0]?.data.title, // default start node title
variable: isSystemVar(item) ? item.join('.') : item[item.length - 1],
isChatVar: isConversationVar(item),
},
variable: `#${item.join('.')}#`,
value_selector: item,

+ 46
- 0
web/app/components/workflow/nodes/assigner/default.ts Zobrazit soubor

@@ -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
- 0
web/app/components/workflow/nodes/assigner/node.tsx Zobrazit soubor

@@ -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
- 0
web/app/components/workflow/nodes/assigner/panel.tsx Zobrazit soubor

@@ -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
- 0
web/app/components/workflow/nodes/assigner/types.ts Zobrazit soubor

@@ -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
- 0
web/app/components/workflow/nodes/assigner/use-config.ts Zobrazit soubor

@@ -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
- 0
web/app/components/workflow/nodes/assigner/utils.ts Zobrazit soubor

@@ -0,0 +1,5 @@
import type { AssignerNodeType } from './types'

export const checkNodeValid = (payload: AssignerNodeType) => {
return true
}

+ 4
- 0
web/app/components/workflow/nodes/constants.ts Zobrazit soubor

@@ -24,6 +24,8 @@ import ToolNode from './tool/node'
import ToolPanel from './tool/panel'
import VariableAssignerNode from './variable-assigner/node'
import VariableAssignerPanel from './variable-assigner/panel'
import AssignerNode from './assigner/node'
import AssignerPanel from './assigner/panel'
import ParameterExtractorNode from './parameter-extractor/node'
import ParameterExtractorPanel from './parameter-extractor/panel'
import IterationNode from './iteration/node'
@@ -42,6 +44,7 @@ export const NodeComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.HttpRequest]: HttpNode,
[BlockEnum.Tool]: ToolNode,
[BlockEnum.VariableAssigner]: VariableAssignerNode,
[BlockEnum.Assigner]: AssignerNode,
[BlockEnum.VariableAggregator]: VariableAssignerNode,
[BlockEnum.ParameterExtractor]: ParameterExtractorNode,
[BlockEnum.Iteration]: IterationNode,
@@ -61,6 +64,7 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.Tool]: ToolPanel,
[BlockEnum.VariableAssigner]: VariableAssignerPanel,
[BlockEnum.VariableAggregator]: VariableAssignerPanel,
[BlockEnum.Assigner]: AssignerPanel,
[BlockEnum.ParameterExtractor]: ParameterExtractorPanel,
[BlockEnum.Iteration]: IterationPanel,
}

+ 8
- 5
web/app/components/workflow/nodes/end/node.tsx Zobrazit soubor

@@ -3,7 +3,7 @@ import React from 'react'
import cn from 'classnames'
import type { EndNodeType } from './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 {
useIsChatMode,
useWorkflow,
@@ -12,7 +12,7 @@ import {
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
import { Line3 } from '@/app/components/base/icons/src/public/common'
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'

const Node: FC<NodeProps<EndNodeType>> = ({
@@ -44,6 +44,7 @@ const Node: FC<NodeProps<EndNodeType>> = ({
const node = getNode(value_selector[0])
const isSystem = isSystemVar(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 varType = getCurrentVariableType({
valueSelector: value_selector,
@@ -53,7 +54,7 @@ const Node: FC<NodeProps<EndNodeType>> = ({
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 className='flex items-center text-xs font-medium text-gray-500'>
{!isEnv && (
{!isEnv && !isChatVar && (
<>
<div className='p-[1px]'>
<VarBlockIcon
@@ -66,9 +67,11 @@ const Node: FC<NodeProps<EndNodeType>> = ({
</>
)}
<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' />}
<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 className='text-xs font-normal text-gray-700'>

+ 9
- 0
web/app/components/workflow/nodes/http/components/key-value/key-value-edit/index.tsx Zobrazit soubor

@@ -17,6 +17,8 @@ type Props = {
onChange: (newList: KeyValue[]) => void
onAdd: () => void
// onSwitchToBulkEdit: () => void
keyNotSupportVar?: boolean
insertVarTipToLeft?: boolean
}

const KeyValueList: FC<Props> = ({
@@ -26,6 +28,8 @@ const KeyValueList: FC<Props> = ({
onChange,
onAdd,
// onSwitchToBulkEdit,
keyNotSupportVar,
insertVarTipToLeft,
}) => {
const { t } = useTranslation()

@@ -47,6 +51,9 @@ const KeyValueList: FC<Props> = ({
}
}, [list, onChange])

if (!Array.isArray(list))
return null

return (
<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'>
@@ -79,6 +86,8 @@ const KeyValueList: FC<Props> = ({
onAdd={onAdd}
readonly={readonly}
canRemove={list.length > 1}
keyNotSupportVar={keyNotSupportVar}
insertVarTipToLeft={insertVarTipToLeft}
/>
))
}

+ 4
- 0
web/app/components/workflow/nodes/http/components/key-value/key-value-edit/input-item.tsx Zobrazit soubor

@@ -18,6 +18,7 @@ type Props = {
onRemove?: () => void
placeholder?: string
readOnly?: boolean
insertVarTipToLeft?: boolean
}

const InputItem: FC<Props> = ({
@@ -30,6 +31,7 @@ const InputItem: FC<Props> = ({
onRemove,
placeholder,
readOnly,
insertVarTipToLeft,
}) => {
const { t } = useTranslation()

@@ -64,6 +66,7 @@ const InputItem: FC<Props> = ({
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
placeholderClassName='!leading-[21px]'
promptMinHeightClassName='h-full'
insertVarTipToLeft={insertVarTipToLeft}
/>
)
: <div
@@ -83,6 +86,7 @@ const InputItem: FC<Props> = ({
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
placeholderClassName='!leading-[21px]'
promptMinHeightClassName='h-full'
insertVarTipToLeft={insertVarTipToLeft}
/>
)}


+ 26
- 9
web/app/components/workflow/nodes/http/components/key-value/key-value-edit/item.tsx Zobrazit soubor

@@ -6,6 +6,7 @@ import produce from 'immer'
import type { KeyValue } from '../../../types'
import InputItem from './input-item'
import cn from '@/utils/classnames'
import Input from '@/app/components/base/input'

const i18nPrefix = 'workflow.nodes.http'

@@ -20,6 +21,8 @@ type Props = {
onRemove: () => void
isLastItem: boolean
onAdd: () => void
keyNotSupportVar?: boolean
insertVarTipToLeft?: boolean
}

const KeyValueItem: FC<Props> = ({
@@ -33,6 +36,8 @@ const KeyValueItem: FC<Props> = ({
onRemove,
isLastItem,
onAdd,
keyNotSupportVar,
insertVarTipToLeft,
}) => {
const { t } = useTranslation()

@@ -51,15 +56,26 @@ const KeyValueItem: FC<Props> = ({
// 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='w-1/2 border-r border-gray-200'>
<InputItem
instanceId={`http-key-${instanceId}`}
nodeId={nodeId}
value={payload.key}
onChange={handleChange('key')}
hasRemove={false}
placeholder={t(`${i18nPrefix}.key`)!}
readOnly={readonly}
/>
{!keyNotSupportVar
? (
<InputItem
instanceId={`http-key-${instanceId}`}
nodeId={nodeId}
value={payload.key}
onChange={handleChange('key')}
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 className='w-1/2'>
<InputItem
@@ -71,6 +87,7 @@ const KeyValueItem: FC<Props> = ({
onRemove={onRemove}
placeholder={t(`${i18nPrefix}.value`)!}
readOnly={readonly}
insertVarTipToLeft={insertVarTipToLeft}
/>
</div>
</div>

+ 0
- 0
web/app/components/workflow/nodes/if-else/components/condition-value.tsx Zobrazit soubor


Některé soubory nejsou zobrazny, neboť je v této revizi změněno mnoho souborů

Načítá se…
Zrušit
Uložit