Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
D
dify
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tech
dify
Commits
5ea7d4cb
Commit
5ea7d4cb
authored
Mar 11, 2024
by
takatost
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'feat/workflow-backend' into deploy/dev
parents
ef861e07
19c9091d
Changes
18
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
403 additions
and
135 deletions
+403
-135
__init__.py
api/controllers/console/__init__.py
+1
-1
workflow.py
api/controllers/console/app/workflow.py
+14
-7
errors.py
api/core/workflow/errors.py
+10
-0
base_node.py
api/core/workflow/nodes/base_node.py
+2
-2
code_node.py
api/core/workflow/nodes/code/code_node.py
+54
-5
entities.py
api/core/workflow/nodes/code/entities.py
+2
-2
direct_answer_node.py
api/core/workflow/nodes/direct_answer/direct_answer_node.py
+8
-2
end_node.py
api/core/workflow/nodes/end/end_node.py
+1
-1
http_request_node.py
api/core/workflow/nodes/http_request/http_request_node.py
+3
-3
llm_node.py
api/core/workflow/nodes/llm/llm_node.py
+1
-1
start_node.py
api/core/workflow/nodes/start/start_node.py
+1
-1
template_transform_node.py
...kflow/nodes/template_transform/template_transform_node.py
+2
-2
tool_node.py
api/core/workflow/nodes/tool/tool_node.py
+7
-3
workflow_engine_manager.py
api/core/workflow/workflow_engine_manager.py
+88
-0
workflow_run_fields.py
api/fields/workflow_run_fields.py
+3
-5
workflow_run_service.py
api/services/workflow_run_service.py
+7
-7
workflow_service.py
api/services/workflow_service.py
+85
-1
test_code.py
api/tests/integration_tests/workflow/nodes/test_code.py
+114
-92
No files found.
api/controllers/console/__init__.py
View file @
5ea7d4cb
...
@@ -8,7 +8,7 @@ api = ExternalApi(bp)
...
@@ -8,7 +8,7 @@ api = ExternalApi(bp)
from
.
import
admin
,
apikey
,
extension
,
feature
,
setup
,
version
,
ping
from
.
import
admin
,
apikey
,
extension
,
feature
,
setup
,
version
,
ping
# Import app controllers
# Import app controllers
from
.app
import
(
advanced_prompt_template
,
annotation
,
app
,
audio
,
completion
,
conversation
,
generator
,
message
,
from
.app
import
(
advanced_prompt_template
,
annotation
,
app
,
audio
,
completion
,
conversation
,
generator
,
message
,
model_config
,
site
,
statistic
,
workflow
,
workflow_app_log
)
model_config
,
site
,
statistic
,
workflow
,
workflow_
run
,
workflow_
app_log
)
# Import auth controllers
# Import auth controllers
from
.auth
import
activate
,
data_source_oauth
,
login
,
oauth
from
.auth
import
activate
,
data_source_oauth
,
login
,
oauth
# Import billing controllers
# Import billing controllers
...
...
api/controllers/console/app/workflow.py
View file @
5ea7d4cb
...
@@ -15,6 +15,7 @@ from controllers.console.setup import setup_required
...
@@ -15,6 +15,7 @@ from controllers.console.setup import setup_required
from
controllers.console.wraps
import
account_initialization_required
from
controllers.console.wraps
import
account_initialization_required
from
core.app.entities.app_invoke_entities
import
InvokeFrom
from
core.app.entities.app_invoke_entities
import
InvokeFrom
from
fields.workflow_fields
import
workflow_fields
from
fields.workflow_fields
import
workflow_fields
from
fields.workflow_run_fields
import
workflow_run_node_execution_fields
from
libs.helper
import
TimestampField
,
uuid_value
from
libs.helper
import
TimestampField
,
uuid_value
from
libs.login
import
current_user
,
login_required
from
libs.login
import
current_user
,
login_required
from
models.model
import
App
,
AppMode
from
models.model
import
App
,
AppMode
...
@@ -164,18 +165,24 @@ class DraftWorkflowNodeRunApi(Resource):
...
@@ -164,18 +165,24 @@ class DraftWorkflowNodeRunApi(Resource):
@
login_required
@
login_required
@
account_initialization_required
@
account_initialization_required
@
get_app_model
(
mode
=
[
AppMode
.
ADVANCED_CHAT
,
AppMode
.
WORKFLOW
])
@
get_app_model
(
mode
=
[
AppMode
.
ADVANCED_CHAT
,
AppMode
.
WORKFLOW
])
@
marshal_with
(
workflow_run_node_execution_fields
)
def
post
(
self
,
app_model
:
App
,
node_id
:
str
):
def
post
(
self
,
app_model
:
App
,
node_id
:
str
):
"""
"""
Run draft workflow node
Run draft workflow node
"""
"""
# TODO
parser
=
reqparse
.
RequestParser
()
parser
.
add_argument
(
'inputs'
,
type
=
dict
,
required
=
True
,
nullable
=
False
,
location
=
'json'
)
args
=
parser
.
parse_args
()
workflow_service
=
WorkflowService
()
workflow_service
=
WorkflowService
()
workflow_service
.
run_draft_workflow_node
(
app_model
=
app_model
,
node_id
=
node_id
,
account
=
current_user
)
workflow_node_execution
=
workflow_service
.
run_draft_workflow_node
(
app_model
=
app_model
,
node_id
=
node_id
,
user_inputs
=
args
.
get
(
'inputs'
),
account
=
current_user
)
# TODO
return
workflow_node_execution
return
{
"result"
:
"success"
}
class
PublishedWorkflowApi
(
Resource
):
class
PublishedWorkflowApi
(
Resource
):
...
@@ -291,7 +298,7 @@ api.add_resource(DraftWorkflowApi, '/apps/<uuid:app_id>/workflows/draft')
...
@@ -291,7 +298,7 @@ api.add_resource(DraftWorkflowApi, '/apps/<uuid:app_id>/workflows/draft')
api
.
add_resource
(
AdvancedChatDraftWorkflowRunApi
,
'/apps/<uuid:app_id>/advanced-chat/workflows/draft/run'
)
api
.
add_resource
(
AdvancedChatDraftWorkflowRunApi
,
'/apps/<uuid:app_id>/advanced-chat/workflows/draft/run'
)
api
.
add_resource
(
DraftWorkflowRunApi
,
'/apps/<uuid:app_id>/workflows/draft/run'
)
api
.
add_resource
(
DraftWorkflowRunApi
,
'/apps/<uuid:app_id>/workflows/draft/run'
)
api
.
add_resource
(
WorkflowTaskStopApi
,
'/apps/<uuid:app_id>/workflows/tasks/<string:task_id>/stop'
)
api
.
add_resource
(
WorkflowTaskStopApi
,
'/apps/<uuid:app_id>/workflows/tasks/<string:task_id>/stop'
)
api
.
add_resource
(
DraftWorkflowNodeRunApi
,
'/apps/<uuid:app_id>/workflows/draft/nodes/<
uuid
:node_id>/run'
)
api
.
add_resource
(
DraftWorkflowNodeRunApi
,
'/apps/<uuid:app_id>/workflows/draft/nodes/<
string
:node_id>/run'
)
api
.
add_resource
(
PublishedWorkflowApi
,
'/apps/<uuid:app_id>/workflows/published'
)
api
.
add_resource
(
PublishedWorkflowApi
,
'/apps/<uuid:app_id>/workflows/published'
)
api
.
add_resource
(
DefaultBlockConfigsApi
,
'/apps/<uuid:app_id>/workflows/default-workflow-block-configs'
)
api
.
add_resource
(
DefaultBlockConfigsApi
,
'/apps/<uuid:app_id>/workflows/default-workflow-block-configs'
)
api
.
add_resource
(
DefaultBlockConfigApi
,
'/apps/<uuid:app_id>/workflows/default-workflow-block-configs'
api
.
add_resource
(
DefaultBlockConfigApi
,
'/apps/<uuid:app_id>/workflows/default-workflow-block-configs'
...
...
api/core/workflow/errors.py
0 → 100644
View file @
5ea7d4cb
from
core.workflow.entities.node_entities
import
NodeType
class
WorkflowNodeRunFailedError
(
Exception
):
def
__init__
(
self
,
node_id
:
str
,
node_type
:
NodeType
,
node_title
:
str
,
error
:
str
):
self
.
node_id
=
node_id
self
.
node_type
=
node_type
self
.
node_title
=
node_title
self
.
error
=
error
super
()
.
__init__
(
f
"Node {node_title} run failed: {error}"
)
api/core/workflow/nodes/base_node.py
View file @
5ea7d4cb
...
@@ -108,7 +108,7 @@ class BaseNode(ABC):
...
@@ -108,7 +108,7 @@ class BaseNode(ABC):
)
)
@
classmethod
@
classmethod
def
extract_variable_selector_to_variable_mapping
(
cls
,
config
:
dict
)
->
dict
:
def
extract_variable_selector_to_variable_mapping
(
cls
,
config
:
dict
)
->
dict
[
str
,
list
[
str
]]
:
"""
"""
Extract variable selector to variable mapping
Extract variable selector to variable mapping
:param config: node config
:param config: node config
...
@@ -119,7 +119,7 @@ class BaseNode(ABC):
...
@@ -119,7 +119,7 @@ class BaseNode(ABC):
@
classmethod
@
classmethod
@
abstractmethod
@
abstractmethod
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
BaseNodeData
)
->
dict
[
list
[
str
],
str
]:
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
BaseNodeData
)
->
dict
[
str
,
list
[
str
]
]:
"""
"""
Extract variable selector to variable mapping
Extract variable selector to variable mapping
:param node_data: node data
:param node_data: node data
...
...
api/core/workflow/nodes/code/code_node.py
View file @
5ea7d4cb
...
@@ -153,11 +153,13 @@ class CodeNode(BaseNode):
...
@@ -153,11 +153,13 @@ class CodeNode(BaseNode):
raise
ValueError
(
f
'{variable} in input form is out of range.'
)
raise
ValueError
(
f
'{variable} in input form is out of range.'
)
if
isinstance
(
value
,
float
):
if
isinstance
(
value
,
float
):
value
=
round
(
value
,
MAX_PRECISION
)
# raise error if precision is too high
if
len
(
str
(
value
)
.
split
(
'.'
)[
1
])
>
MAX_PRECISION
:
raise
ValueError
(
f
'{variable} in output form has too high precision.'
)
return
value
return
value
def
_transform_result
(
self
,
result
:
dict
,
output_schema
:
dict
[
str
,
CodeNodeData
.
Output
],
def
_transform_result
(
self
,
result
:
dict
,
output_schema
:
Optional
[
dict
[
str
,
CodeNodeData
.
Output
]
],
prefix
:
str
=
''
,
prefix
:
str
=
''
,
depth
:
int
=
1
)
->
dict
:
depth
:
int
=
1
)
->
dict
:
"""
"""
...
@@ -170,6 +172,47 @@ class CodeNode(BaseNode):
...
@@ -170,6 +172,47 @@ class CodeNode(BaseNode):
raise
ValueError
(
"Depth limit reached, object too deep."
)
raise
ValueError
(
"Depth limit reached, object too deep."
)
transformed_result
=
{}
transformed_result
=
{}
if
output_schema
is
None
:
# validate output thought instance type
for
output_name
,
output_value
in
result
.
items
():
if
isinstance
(
output_value
,
dict
):
self
.
_transform_result
(
result
=
output_value
,
output_schema
=
None
,
prefix
=
f
'{prefix}.{output_name}'
if
prefix
else
output_name
,
depth
=
depth
+
1
)
elif
isinstance
(
output_value
,
int
|
float
):
self
.
_check_number
(
value
=
output_value
,
variable
=
f
'{prefix}.{output_name}'
if
prefix
else
output_name
)
elif
isinstance
(
output_value
,
str
):
self
.
_check_string
(
value
=
output_value
,
variable
=
f
'{prefix}.{output_name}'
if
prefix
else
output_name
)
elif
isinstance
(
output_value
,
list
):
if
all
(
isinstance
(
value
,
int
|
float
)
for
value
in
output_value
):
for
value
in
output_value
:
self
.
_check_number
(
value
=
value
,
variable
=
f
'{prefix}.{output_name}'
if
prefix
else
output_name
)
elif
all
(
isinstance
(
value
,
str
)
for
value
in
output_value
):
for
value
in
output_value
:
self
.
_check_string
(
value
=
value
,
variable
=
f
'{prefix}.{output_name}'
if
prefix
else
output_name
)
else
:
raise
ValueError
(
f
'Output {prefix}.{output_name} is not a valid array. make sure all elements are of the same type.'
)
else
:
raise
ValueError
(
f
'Output {prefix}.{output_name} is not a valid type.'
)
return
result
parameters_validated
=
{}
for
output_name
,
output_config
in
output_schema
.
items
():
for
output_name
,
output_config
in
output_schema
.
items
():
if
output_config
.
type
==
'object'
:
if
output_config
.
type
==
'object'
:
# check if output is object
# check if output is object
...
@@ -236,11 +279,17 @@ class CodeNode(BaseNode):
...
@@ -236,11 +279,17 @@ class CodeNode(BaseNode):
]
]
else
:
else
:
raise
ValueError
(
f
'Output type {output_config.type} is not supported.'
)
raise
ValueError
(
f
'Output type {output_config.type} is not supported.'
)
parameters_validated
[
output_name
]
=
True
# check if all output parameters are validated
if
len
(
parameters_validated
)
!=
len
(
result
):
raise
ValueError
(
'Not all output parameters are validated.'
)
return
transformed_result
return
transformed_result
@
classmethod
@
classmethod
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
CodeNodeData
)
->
dict
[
list
[
str
],
str
]:
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
CodeNodeData
)
->
dict
[
str
,
list
[
str
]
]:
"""
"""
Extract variable selector to variable mapping
Extract variable selector to variable mapping
:param node_data: node data
:param node_data: node data
...
@@ -248,5 +297,5 @@ class CodeNode(BaseNode):
...
@@ -248,5 +297,5 @@ class CodeNode(BaseNode):
"""
"""
return
{
return
{
variable_selector
.
value_selector
:
variable_selector
.
variable
for
variable_selector
in
node_data
.
variables
variable_selector
.
variable
:
variable_selector
.
value_selector
for
variable_selector
in
node_data
.
variables
}
}
\ No newline at end of file
api/core/workflow/nodes/code/entities.py
View file @
5ea7d4cb
from
typing
import
Literal
,
Union
from
typing
import
Literal
,
Optional
from
pydantic
import
BaseModel
from
pydantic
import
BaseModel
...
@@ -12,7 +12,7 @@ class CodeNodeData(BaseNodeData):
...
@@ -12,7 +12,7 @@ class CodeNodeData(BaseNodeData):
"""
"""
class
Output
(
BaseModel
):
class
Output
(
BaseModel
):
type
:
Literal
[
'string'
,
'number'
,
'object'
,
'array[string]'
,
'array[number]'
]
type
:
Literal
[
'string'
,
'number'
,
'object'
,
'array[string]'
,
'array[number]'
]
children
:
Union
[
None
,
dict
[
str
,
'
Output'
]]
children
:
Optional
[
dict
[
str
,
'CodeNodeData.
Output'
]]
variables
:
list
[
VariableSelector
]
variables
:
list
[
VariableSelector
]
answer
:
str
answer
:
str
...
...
api/core/workflow/nodes/direct_answer/direct_answer_node.py
View file @
5ea7d4cb
...
@@ -50,10 +50,16 @@ class DirectAnswerNode(BaseNode):
...
@@ -50,10 +50,16 @@ class DirectAnswerNode(BaseNode):
)
)
@
classmethod
@
classmethod
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
BaseNodeData
)
->
dict
[
list
[
str
],
str
]:
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
BaseNodeData
)
->
dict
[
str
,
list
[
str
]
]:
"""
"""
Extract variable selector to variable mapping
Extract variable selector to variable mapping
:param node_data: node data
:param node_data: node data
:return:
:return:
"""
"""
return
{}
node_data
=
cast
(
cls
.
_node_data_cls
,
node_data
)
variable_mapping
=
{}
for
variable_selector
in
node_data
.
variables
:
variable_mapping
[
variable_selector
.
variable
]
=
variable_selector
.
value_selector
return
variable_mapping
api/core/workflow/nodes/end/end_node.py
View file @
5ea7d4cb
...
@@ -56,7 +56,7 @@ class EndNode(BaseNode):
...
@@ -56,7 +56,7 @@ class EndNode(BaseNode):
)
)
@
classmethod
@
classmethod
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
BaseNodeData
)
->
dict
[
list
[
str
],
str
]:
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
BaseNodeData
)
->
dict
[
str
,
list
[
str
]
]:
"""
"""
Extract variable selector to variable mapping
Extract variable selector to variable mapping
:param node_data: node data
:param node_data: node data
...
...
api/core/workflow/nodes/http_request/http_request_node.py
View file @
5ea7d4cb
...
@@ -48,12 +48,12 @@ class HttpRequestNode(BaseNode):
...
@@ -48,12 +48,12 @@ class HttpRequestNode(BaseNode):
@
classmethod
@
classmethod
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
HttpRequestNodeData
)
->
dict
[
list
[
str
],
str
]:
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
HttpRequestNodeData
)
->
dict
[
str
,
list
[
str
]
]:
"""
"""
Extract variable selector to variable mapping
Extract variable selector to variable mapping
:param node_data: node data
:param node_data: node data
:return:
:return:
"""
"""
return
{
return
{
variable_selector
.
value_selector
:
variable_selector
.
variable
for
variable_selector
in
node_data
.
variables
variable_selector
.
variable
:
variable_selector
.
value_selector
for
variable_selector
in
node_data
.
variables
}
}
\ No newline at end of file
api/core/workflow/nodes/llm/llm_node.py
View file @
5ea7d4cb
...
@@ -23,7 +23,7 @@ class LLMNode(BaseNode):
...
@@ -23,7 +23,7 @@ class LLMNode(BaseNode):
pass
pass
@
classmethod
@
classmethod
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
BaseNodeData
)
->
dict
[
list
[
str
],
str
]:
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
BaseNodeData
)
->
dict
[
str
,
list
[
str
]
]:
"""
"""
Extract variable selector to variable mapping
Extract variable selector to variable mapping
:param node_data: node data
:param node_data: node data
...
...
api/core/workflow/nodes/start/start_node.py
View file @
5ea7d4cb
...
@@ -69,7 +69,7 @@ class StartNode(BaseNode):
...
@@ -69,7 +69,7 @@ class StartNode(BaseNode):
return
filtered_inputs
return
filtered_inputs
@
classmethod
@
classmethod
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
BaseNodeData
)
->
dict
[
list
[
str
],
str
]:
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
BaseNodeData
)
->
dict
[
str
,
list
[
str
]
]:
"""
"""
Extract variable selector to variable mapping
Extract variable selector to variable mapping
:param node_data: node data
:param node_data: node data
...
...
api/core/workflow/nodes/template_transform/template_transform_node.py
View file @
5ea7d4cb
...
@@ -72,12 +72,12 @@ class TemplateTransformNode(BaseNode):
...
@@ -72,12 +72,12 @@ class TemplateTransformNode(BaseNode):
)
)
@
classmethod
@
classmethod
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
TemplateTransformNodeData
)
->
dict
[
list
[
str
],
str
]:
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
TemplateTransformNodeData
)
->
dict
[
str
,
list
[
str
]
]:
"""
"""
Extract variable selector to variable mapping
Extract variable selector to variable mapping
:param node_data: node data
:param node_data: node data
:return:
:return:
"""
"""
return
{
return
{
variable_selector
.
va
lue_selector
:
variable_selector
.
variable
for
variable_selector
in
node_data
.
variables
variable_selector
.
va
riable
:
variable_selector
.
value_selector
for
variable_selector
in
node_data
.
variables
}
}
\ No newline at end of file
api/core/workflow/nodes/tool/tool_node.py
View file @
5ea7d4cb
...
@@ -42,7 +42,7 @@ class ToolNode(BaseNode):
...
@@ -42,7 +42,7 @@ class ToolNode(BaseNode):
try
:
try
:
# TODO: user_id
# TODO: user_id
messages
=
tool_runtime
.
invoke
(
None
,
parameters
)
messages
=
tool_runtime
.
invoke
(
self
.
user_id
,
parameters
)
except
Exception
as
e
:
except
Exception
as
e
:
return
NodeRunResult
(
return
NodeRunResult
(
status
=
WorkflowNodeExecutionStatus
.
FAILED
,
status
=
WorkflowNodeExecutionStatus
.
FAILED
,
...
@@ -133,8 +133,12 @@ class ToolNode(BaseNode):
...
@@ -133,8 +133,12 @@ class ToolNode(BaseNode):
@
classmethod
@
classmethod
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
BaseNodeData
)
->
dict
[
list
[
str
],
str
]:
def
_extract_variable_selector_to_variable_mapping
(
cls
,
node_data
:
BaseNodeData
)
->
dict
[
str
,
list
[
str
]
]:
"""
"""
Extract variable selector to variable mapping
Extract variable selector to variable mapping
"""
"""
pass
return
{
\ No newline at end of file
k
.
variable
:
k
.
value_selector
for
k
in
cast
(
ToolNodeData
,
node_data
)
.
tool_parameters
if
k
.
variable_type
==
'selector'
}
api/core/workflow/workflow_engine_manager.py
View file @
5ea7d4cb
...
@@ -6,6 +6,7 @@ from core.workflow.callbacks.base_workflow_callback import BaseWorkflowCallback
...
@@ -6,6 +6,7 @@ from core.workflow.callbacks.base_workflow_callback import BaseWorkflowCallback
from
core.workflow.entities.node_entities
import
NodeRunMetadataKey
,
NodeRunResult
,
NodeType
from
core.workflow.entities.node_entities
import
NodeRunMetadataKey
,
NodeRunResult
,
NodeType
from
core.workflow.entities.variable_pool
import
VariablePool
,
VariableValue
from
core.workflow.entities.variable_pool
import
VariablePool
,
VariableValue
from
core.workflow.entities.workflow_entities
import
WorkflowNodeAndResult
,
WorkflowRunState
from
core.workflow.entities.workflow_entities
import
WorkflowNodeAndResult
,
WorkflowRunState
from
core.workflow.errors
import
WorkflowNodeRunFailedError
from
core.workflow.nodes.base_node
import
BaseNode
,
UserFrom
from
core.workflow.nodes.base_node
import
BaseNode
,
UserFrom
from
core.workflow.nodes.code.code_node
import
CodeNode
from
core.workflow.nodes.code.code_node
import
CodeNode
from
core.workflow.nodes.direct_answer.direct_answer_node
import
DirectAnswerNode
from
core.workflow.nodes.direct_answer.direct_answer_node
import
DirectAnswerNode
...
@@ -180,6 +181,93 @@ class WorkflowEngineManager:
...
@@ -180,6 +181,93 @@ class WorkflowEngineManager:
callbacks
=
callbacks
callbacks
=
callbacks
)
)
def
single_step_run_workflow_node
(
self
,
workflow
:
Workflow
,
node_id
:
str
,
user_id
:
str
,
user_inputs
:
dict
)
->
tuple
[
BaseNode
,
NodeRunResult
]:
"""
Single step run workflow node
:param workflow: Workflow instance
:param node_id: node id
:param user_id: user id
:param user_inputs: user inputs
:return:
"""
# fetch node info from workflow graph
graph
=
workflow
.
graph_dict
if
not
graph
:
raise
ValueError
(
'workflow graph not found'
)
nodes
=
graph
.
get
(
'nodes'
)
if
not
nodes
:
raise
ValueError
(
'nodes not found in workflow graph'
)
# fetch node config from node id
node_config
=
None
for
node
in
nodes
:
if
node
.
get
(
'id'
)
==
node_id
:
node_config
=
node
break
if
not
node_config
:
raise
ValueError
(
'node id not found in workflow graph'
)
# Get node class
node_cls
=
node_classes
.
get
(
NodeType
.
value_of
(
node_config
.
get
(
'data'
,
{})
.
get
(
'type'
)))
# init workflow run state
node_instance
=
node_cls
(
tenant_id
=
workflow
.
tenant_id
,
app_id
=
workflow
.
app_id
,
workflow_id
=
workflow
.
id
,
user_id
=
user_id
,
user_from
=
UserFrom
.
ACCOUNT
,
config
=
node_config
)
try
:
# init variable pool
variable_pool
=
VariablePool
(
system_variables
=
{},
user_inputs
=
{}
)
# variable selector to variable mapping
try
:
variable_mapping
=
node_cls
.
extract_variable_selector_to_variable_mapping
(
node_config
)
except
NotImplementedError
:
variable_mapping
=
{}
for
variable_key
,
variable_selector
in
variable_mapping
.
items
():
if
variable_key
not
in
user_inputs
:
raise
ValueError
(
f
'Variable key {variable_key} not found in user inputs.'
)
# fetch variable node id from variable selector
variable_node_id
=
variable_selector
[
0
]
variable_key_list
=
variable_selector
[
1
:]
# append variable and value to variable pool
variable_pool
.
append_variable
(
node_id
=
variable_node_id
,
variable_key_list
=
variable_key_list
,
value
=
user_inputs
.
get
(
variable_key
)
)
# run node
node_run_result
=
node_instance
.
run
(
variable_pool
=
variable_pool
)
except
Exception
as
e
:
raise
WorkflowNodeRunFailedError
(
node_id
=
node_instance
.
node_id
,
node_type
=
node_instance
.
node_type
,
node_title
=
node_instance
.
node_data
.
title
,
error
=
str
(
e
)
)
return
node_instance
,
node_run_result
def
_workflow_run_success
(
self
,
callbacks
:
list
[
BaseWorkflowCallback
]
=
None
)
->
None
:
def
_workflow_run_success
(
self
,
callbacks
:
list
[
BaseWorkflowCallback
]
=
None
)
->
None
:
"""
"""
Workflow run success
Workflow run success
...
...
api/fields/workflow_run_fields.py
View file @
5ea7d4cb
...
@@ -34,11 +34,9 @@ workflow_run_for_list_fields = {
...
@@ -34,11 +34,9 @@ workflow_run_for_list_fields = {
}
}
workflow_run_pagination_fields
=
{
workflow_run_pagination_fields
=
{
'page'
:
fields
.
Integer
,
'limit'
:
fields
.
Integer
(
attribute
=
'limit'
),
'limit'
:
fields
.
Integer
(
attribute
=
'per_page'
),
'has_more'
:
fields
.
Boolean
(
attribute
=
'has_more'
),
'total'
:
fields
.
Integer
,
'data'
:
fields
.
List
(
fields
.
Nested
(
workflow_run_for_list_fields
),
attribute
=
'data'
)
'has_more'
:
fields
.
Boolean
(
attribute
=
'has_next'
),
'data'
:
fields
.
List
(
fields
.
Nested
(
workflow_run_for_list_fields
),
attribute
=
'items'
)
}
}
workflow_run_detail_fields
=
{
workflow_run_detail_fields
=
{
...
...
api/services/workflow_run_service.py
View file @
5ea7d4cb
...
@@ -34,26 +34,26 @@ class WorkflowRunService:
...
@@ -34,26 +34,26 @@ class WorkflowRunService:
if
not
last_workflow_run
:
if
not
last_workflow_run
:
raise
ValueError
(
'Last workflow run not exists'
)
raise
ValueError
(
'Last workflow run not exists'
)
conversatio
ns
=
base_query
.
filter
(
workflow_ru
ns
=
base_query
.
filter
(
WorkflowRun
.
created_at
<
last_workflow_run
.
created_at
,
WorkflowRun
.
created_at
<
last_workflow_run
.
created_at
,
WorkflowRun
.
id
!=
last_workflow_run
.
id
WorkflowRun
.
id
!=
last_workflow_run
.
id
)
.
order_by
(
WorkflowRun
.
created_at
.
desc
())
.
limit
(
limit
)
.
all
()
)
.
order_by
(
WorkflowRun
.
created_at
.
desc
())
.
limit
(
limit
)
.
all
()
else
:
else
:
conversatio
ns
=
base_query
.
order_by
(
WorkflowRun
.
created_at
.
desc
())
.
limit
(
limit
)
.
all
()
workflow_ru
ns
=
base_query
.
order_by
(
WorkflowRun
.
created_at
.
desc
())
.
limit
(
limit
)
.
all
()
has_more
=
False
has_more
=
False
if
len
(
conversatio
ns
)
==
limit
:
if
len
(
workflow_ru
ns
)
==
limit
:
current_page_first_
conversation
=
conversatio
ns
[
-
1
]
current_page_first_
workflow_run
=
workflow_ru
ns
[
-
1
]
rest_count
=
base_query
.
filter
(
rest_count
=
base_query
.
filter
(
WorkflowRun
.
created_at
<
current_page_first_
conversatio
n
.
created_at
,
WorkflowRun
.
created_at
<
current_page_first_
workflow_ru
n
.
created_at
,
WorkflowRun
.
id
!=
current_page_first_
conversatio
n
.
id
WorkflowRun
.
id
!=
current_page_first_
workflow_ru
n
.
id
)
.
count
()
)
.
count
()
if
rest_count
>
0
:
if
rest_count
>
0
:
has_more
=
True
has_more
=
True
return
InfiniteScrollPagination
(
return
InfiniteScrollPagination
(
data
=
conversatio
ns
,
data
=
workflow_ru
ns
,
limit
=
limit
,
limit
=
limit
,
has_more
=
has_more
has_more
=
has_more
)
)
...
...
api/services/workflow_service.py
View file @
5ea7d4cb
import
json
import
json
import
time
from
collections.abc
import
Generator
from
collections.abc
import
Generator
from
datetime
import
datetime
from
datetime
import
datetime
from
typing
import
Optional
,
Union
from
typing
import
Optional
,
Union
...
@@ -9,12 +10,21 @@ from core.app.apps.base_app_queue_manager import AppQueueManager
...
@@ -9,12 +10,21 @@ from core.app.apps.base_app_queue_manager import AppQueueManager
from
core.app.apps.workflow.app_config_manager
import
WorkflowAppConfigManager
from
core.app.apps.workflow.app_config_manager
import
WorkflowAppConfigManager
from
core.app.apps.workflow.app_generator
import
WorkflowAppGenerator
from
core.app.apps.workflow.app_generator
import
WorkflowAppGenerator
from
core.app.entities.app_invoke_entities
import
InvokeFrom
from
core.app.entities.app_invoke_entities
import
InvokeFrom
from
core.model_runtime.utils.encoders
import
jsonable_encoder
from
core.workflow.entities.node_entities
import
NodeType
from
core.workflow.entities.node_entities
import
NodeType
from
core.workflow.errors
import
WorkflowNodeRunFailedError
from
core.workflow.workflow_engine_manager
import
WorkflowEngineManager
from
core.workflow.workflow_engine_manager
import
WorkflowEngineManager
from
extensions.ext_database
import
db
from
extensions.ext_database
import
db
from
models.account
import
Account
from
models.account
import
Account
from
models.model
import
App
,
AppMode
,
EndUser
from
models.model
import
App
,
AppMode
,
EndUser
from
models.workflow
import
Workflow
,
WorkflowType
from
models.workflow
import
(
CreatedByRole
,
Workflow
,
WorkflowNodeExecution
,
WorkflowNodeExecutionStatus
,
WorkflowNodeExecutionTriggeredFrom
,
WorkflowType
,
)
from
services.workflow.workflow_converter
import
WorkflowConverter
from
services.workflow.workflow_converter
import
WorkflowConverter
...
@@ -214,6 +224,80 @@ class WorkflowService:
...
@@ -214,6 +224,80 @@ class WorkflowService:
"""
"""
AppQueueManager
.
set_stop_flag
(
task_id
,
invoke_from
,
user
.
id
)
AppQueueManager
.
set_stop_flag
(
task_id
,
invoke_from
,
user
.
id
)
def
run_draft_workflow_node
(
self
,
app_model
:
App
,
node_id
:
str
,
user_inputs
:
dict
,
account
:
Account
)
->
WorkflowNodeExecution
:
"""
Run draft workflow node
"""
# fetch draft workflow by app_model
draft_workflow
=
self
.
get_draft_workflow
(
app_model
=
app_model
)
if
not
draft_workflow
:
raise
ValueError
(
'Workflow not initialized'
)
# run draft workflow node
workflow_engine_manager
=
WorkflowEngineManager
()
start_at
=
time
.
perf_counter
()
try
:
node_instance
,
node_run_result
=
workflow_engine_manager
.
single_step_run_workflow_node
(
workflow
=
draft_workflow
,
node_id
=
node_id
,
user_inputs
=
user_inputs
,
user_id
=
account
.
id
,
)
except
WorkflowNodeRunFailedError
as
e
:
workflow_node_execution
=
WorkflowNodeExecution
(
tenant_id
=
app_model
.
tenant_id
,
app_id
=
app_model
.
id
,
workflow_id
=
draft_workflow
.
id
,
triggered_from
=
WorkflowNodeExecutionTriggeredFrom
.
SINGLE_STEP
.
value
,
index
=
1
,
node_id
=
e
.
node_id
,
node_type
=
e
.
node_type
.
value
,
title
=
e
.
node_title
,
status
=
WorkflowNodeExecutionStatus
.
FAILED
.
value
,
error
=
e
.
error
,
elapsed_time
=
time
.
perf_counter
()
-
start_at
,
created_by_role
=
CreatedByRole
.
ACCOUNT
.
value
,
created_by
=
account
.
id
,
created_at
=
datetime
.
utcnow
(),
finished_at
=
datetime
.
utcnow
()
)
db
.
session
.
add
(
workflow_node_execution
)
db
.
session
.
commit
()
return
workflow_node_execution
# create workflow node execution
workflow_node_execution
=
WorkflowNodeExecution
(
tenant_id
=
app_model
.
tenant_id
,
app_id
=
app_model
.
id
,
workflow_id
=
draft_workflow
.
id
,
triggered_from
=
WorkflowNodeExecutionTriggeredFrom
.
SINGLE_STEP
.
value
,
index
=
1
,
node_id
=
node_id
,
node_type
=
node_instance
.
node_type
.
value
,
title
=
node_instance
.
node_data
.
title
,
inputs
=
json
.
dumps
(
node_run_result
.
inputs
)
if
node_run_result
.
inputs
else
None
,
process_data
=
json
.
dumps
(
node_run_result
.
process_data
)
if
node_run_result
.
process_data
else
None
,
outputs
=
json
.
dumps
(
node_run_result
.
outputs
)
if
node_run_result
.
outputs
else
None
,
execution_metadata
=
(
json
.
dumps
(
jsonable_encoder
(
node_run_result
.
metadata
))
if
node_run_result
.
metadata
else
None
),
status
=
WorkflowNodeExecutionStatus
.
SUCCEEDED
.
value
,
elapsed_time
=
time
.
perf_counter
()
-
start_at
,
created_by_role
=
CreatedByRole
.
ACCOUNT
.
value
,
created_by
=
account
.
id
,
created_at
=
datetime
.
utcnow
(),
finished_at
=
datetime
.
utcnow
()
)
db
.
session
.
add
(
workflow_node_execution
)
db
.
session
.
commit
()
return
workflow_node_execution
def
convert_to_workflow
(
self
,
app_model
:
App
,
account
:
Account
)
->
App
:
def
convert_to_workflow
(
self
,
app_model
:
App
,
account
:
Account
)
->
App
:
"""
"""
Basic mode of chatbot app(expert mode) to workflow
Basic mode of chatbot app(expert mode) to workflow
...
...
api/tests/integration_tests/workflow/nodes/test_code.py
View file @
5ea7d4cb
import
pytest
import
pytest
from
core.app.entities.app_invoke_entities
import
InvokeFrom
from
core.workflow.entities.variable_pool
import
VariablePool
from
core.workflow.entities.variable_pool
import
VariablePool
from
core.workflow.nodes.code.code_node
import
CodeNode
from
core.workflow.nodes.code.code_node
import
CodeNode
from
models.workflow
import
WorkflowNodeExecutionStatus
,
WorkflowRunStatus
from
models.workflow
import
WorkflowNodeExecutionStatus
from
tests.integration_tests.workflow.nodes.__mock.code_executor
import
setup_code_executor_mock
from
tests.integration_tests.workflow.nodes.__mock.code_executor
import
setup_code_executor_mock
@
pytest
.
mark
.
parametrize
(
'setup_code_executor_mock'
,
[[
'none'
]],
indirect
=
True
)
@
pytest
.
mark
.
parametrize
(
'setup_code_executor_mock'
,
[[
'none'
]],
indirect
=
True
)
...
@@ -15,30 +16,37 @@ def test_execute_code(setup_code_executor_mock):
...
@@ -15,30 +16,37 @@ def test_execute_code(setup_code_executor_mock):
'''
'''
# trim first 4 spaces at the beginning of each line
# trim first 4 spaces at the beginning of each line
code
=
'
\n
'
.
join
([
line
[
4
:]
for
line
in
code
.
split
(
'
\n
'
)])
code
=
'
\n
'
.
join
([
line
[
4
:]
for
line
in
code
.
split
(
'
\n
'
)])
node
=
CodeNode
(
config
=
{
node
=
CodeNode
(
'id'
:
'1'
,
tenant_id
=
'1'
,
'data'
:
{
app_id
=
'1'
,
'outputs'
:
{
workflow_id
=
'1'
,
'result'
:
{
user_id
=
'1'
,
'type'
:
'number'
,
user_from
=
InvokeFrom
.
WEB_APP
,
config
=
{
'id'
:
'1'
,
'data'
:
{
'outputs'
:
{
'result'
:
{
'type'
:
'number'
,
},
},
},
}
,
'title'
:
'123'
,
'title'
:
'123'
,
'variables'
:
[
'variables'
:
[
{
{
'variable'
:
'args1'
,
'variable'
:
'args1'
,
'value_selector'
:
[
'1'
,
'123'
,
'args1'
]
,
'value_selector'
:
[
'1'
,
'123'
,
'args1'
]
,
}
,
},
{
{
'variable'
:
'args2'
,
'variable'
:
'args2'
,
'value_selector'
:
[
'1'
,
'123'
,
'args2'
]
'value_selector'
:
[
'1'
,
'123'
,
'args2'
]
}
}
],
]
,
'answer'
:
'123'
,
'answer'
:
'12
3'
,
'code_language'
:
'python
3'
,
'code_language'
:
'python3'
,
'code'
:
code
'code'
:
code
}
}
}
}
)
)
# construct variable pool
# construct variable pool
pool
=
VariablePool
(
system_variables
=
{},
user_inputs
=
{})
pool
=
VariablePool
(
system_variables
=
{},
user_inputs
=
{})
...
@@ -61,30 +69,37 @@ def test_execute_code_output_validator(setup_code_executor_mock):
...
@@ -61,30 +69,37 @@ def test_execute_code_output_validator(setup_code_executor_mock):
'''
'''
# trim first 4 spaces at the beginning of each line
# trim first 4 spaces at the beginning of each line
code
=
'
\n
'
.
join
([
line
[
4
:]
for
line
in
code
.
split
(
'
\n
'
)])
code
=
'
\n
'
.
join
([
line
[
4
:]
for
line
in
code
.
split
(
'
\n
'
)])
node
=
CodeNode
(
config
=
{
node
=
CodeNode
(
'id'
:
'1'
,
tenant_id
=
'1'
,
'data'
:
{
app_id
=
'1'
,
"outputs"
:
{
workflow_id
=
'1'
,
"result"
:
{
user_id
=
'1'
,
"type"
:
"string"
,
user_from
=
InvokeFrom
.
WEB_APP
,
},
config
=
{
}
,
'id'
:
'1'
,
'
title'
:
'123'
,
'
data'
:
{
'variables'
:
[
"outputs"
:
{
{
"result"
:
{
'variable'
:
'args1'
,
"type"
:
"string"
,
'value_selector'
:
[
'1'
,
'123'
,
'args1'
]
,
}
,
},
},
{
'title'
:
'123'
,
'variable'
:
'args2'
,
'variables'
:
[
'value_selector'
:
[
'1'
,
'123'
,
'args2'
]
{
}
'variable'
:
'args1'
,
],
'value_selector'
:
[
'1'
,
'123'
,
'args1'
],
'answer'
:
'123'
,
},
'code_language'
:
'python3'
,
{
'code'
:
code
'variable'
:
'args2'
,
'value_selector'
:
[
'1'
,
'123'
,
'args2'
]
}
],
'answer'
:
'123'
,
'code_language'
:
'python3'
,
'code'
:
code
}
}
}
}
)
)
# construct variable pool
# construct variable pool
pool
=
VariablePool
(
system_variables
=
{},
user_inputs
=
{})
pool
=
VariablePool
(
system_variables
=
{},
user_inputs
=
{})
...
@@ -108,60 +123,67 @@ def test_execute_code_output_validator_depth():
...
@@ -108,60 +123,67 @@ def test_execute_code_output_validator_depth():
'''
'''
# trim first 4 spaces at the beginning of each line
# trim first 4 spaces at the beginning of each line
code
=
'
\n
'
.
join
([
line
[
4
:]
for
line
in
code
.
split
(
'
\n
'
)])
code
=
'
\n
'
.
join
([
line
[
4
:]
for
line
in
code
.
split
(
'
\n
'
)])
node
=
CodeNode
(
config
=
{
node
=
CodeNode
(
'id'
:
'1'
,
tenant_id
=
'1'
,
'data'
:
{
app_id
=
'1'
,
"outputs"
:
{
workflow_id
=
'1'
,
"string_validator"
:
{
user_id
=
'1'
,
"type"
:
"string"
,
user_from
=
InvokeFrom
.
WEB_APP
,
},
config
=
{
"number_validator"
:
{
'id'
:
'1'
,
"type"
:
"number"
,
'data'
:
{
},
"outputs"
:
{
"number_array_validator"
:
{
"string_validator"
:
{
"type"
:
"array[number]"
,
"type"
:
"string"
,
},
},
"string_array_validator"
:
{
"number_validator"
:
{
"type"
:
"array[string]"
,
"type"
:
"number"
,
},
},
"object_validator"
:
{
"number_array_validator"
:
{
"type"
:
"object"
,
"type"
:
"array[number]"
,
"children"
:
{
},
"result"
:
{
"string_array_validator"
:
{
"type"
:
"number"
,
"type"
:
"array[string]"
,
},
},
"depth"
:
{
"object_validator"
:
{
"type"
:
"object"
,
"type"
:
"object"
,
"children"
:
{
"children"
:
{
"depth"
:
{
"result"
:
{
"type"
:
"object"
,
"type"
:
"number"
,
"children"
:
{
},
"depth"
:
{
"depth"
:
{
"type"
:
"number"
,
"type"
:
"object"
,
"children"
:
{
"depth"
:
{
"type"
:
"object"
,
"children"
:
{
"depth"
:
{
"type"
:
"number"
,
}
}
}
}
}
}
}
}
}
}
}
}
}
,
},
},
}
,
'title'
:
'123'
,
'title'
:
'123'
,
'variables'
:
[
'variables'
:
[
{
{
'variable'
:
'args1'
,
'variable'
:
'args1'
,
'value_selector'
:
[
'1'
,
'123'
,
'args1'
]
,
'value_selector'
:
[
'1'
,
'123'
,
'args1'
]
,
}
,
},
{
{
'variable'
:
'args2'
,
'variable'
:
'args2'
,
'value_selector'
:
[
'1'
,
'123'
,
'args2'
]
'value_selector'
:
[
'1'
,
'123'
,
'args2'
]
}
}
],
]
,
'answer'
:
'123'
,
'answer'
:
'12
3'
,
'code_language'
:
'python
3'
,
'code_language'
:
'python3'
,
'code'
:
code
'code'
:
code
}
}
}
}
)
)
# construct result
# construct result
result
=
{
result
=
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment