Skip to content

Commit 3254018

Browse files
authored
feat(workflow_service): workflow version control api. (#14860)
Signed-off-by: -LAN- <[email protected]>
1 parent f2b7df9 commit 3254018

File tree

9 files changed

+1742
-895
lines changed

9 files changed

+1742
-895
lines changed

api/controllers/console/app/workflow.py

Lines changed: 207 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import json
22
import logging
3+
from typing import cast
34

45
from flask import abort, request
56
from flask_restful import Resource, inputs, marshal_with, reqparse # type: ignore
7+
from sqlalchemy.orm import Session
68
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
79

810
import services
@@ -13,6 +15,7 @@
1315
from controllers.console.wraps import account_initialization_required, setup_required
1416
from core.app.apps.base_app_queue_manager import AppQueueManager
1517
from core.app.entities.app_invoke_entities import InvokeFrom
18+
from extensions.ext_database import db
1619
from factories import variable_factory
1720
from fields.workflow_fields import workflow_fields, workflow_pagination_fields
1821
from fields.workflow_run_fields import workflow_run_node_execution_fields
@@ -24,7 +27,7 @@
2427
from models.model import AppMode
2528
from services.app_generate_service import AppGenerateService
2629
from services.errors.app import WorkflowHashNotEqualError
27-
from services.workflow_service import WorkflowService
30+
from services.workflow_service import DraftWorkflowDeletionError, WorkflowInUseError, WorkflowService
2831

2932
logger = logging.getLogger(__name__)
3033

@@ -439,10 +442,38 @@ def post(self, app_model: App):
439442
if not isinstance(current_user, Account):
440443
raise Forbidden()
441444

445+
parser = reqparse.RequestParser()
446+
parser.add_argument("marked_name", type=str, required=False, default="", location="json")
447+
parser.add_argument("marked_comment", type=str, required=False, default="", location="json")
448+
args = parser.parse_args()
449+
450+
# Validate name and comment length
451+
if args.marked_name and len(args.marked_name) > 20:
452+
raise ValueError("Marked name cannot exceed 20 characters")
453+
if args.marked_comment and len(args.marked_comment) > 100:
454+
raise ValueError("Marked comment cannot exceed 100 characters")
455+
442456
workflow_service = WorkflowService()
443-
workflow = workflow_service.publish_workflow(app_model=app_model, account=current_user)
457+
with Session(db.engine) as session:
458+
workflow = workflow_service.publish_workflow(
459+
session=session,
460+
app_model=app_model,
461+
account=current_user,
462+
marked_name=args.marked_name or "",
463+
marked_comment=args.marked_comment or "",
464+
)
465+
466+
app_model.workflow_id = workflow.id
467+
db.session.commit()
468+
469+
workflow_created_at = TimestampField().format(workflow.created_at)
444470

445-
return {"result": "success", "created_at": TimestampField().format(workflow.created_at)}
471+
session.commit()
472+
473+
return {
474+
"result": "success",
475+
"created_at": workflow_created_at,
476+
}
446477

447478

448479
class DefaultBlockConfigsApi(Resource):
@@ -564,37 +595,193 @@ def get(self, app_model: App):
564595
parser = reqparse.RequestParser()
565596
parser.add_argument("page", type=inputs.int_range(1, 99999), required=False, default=1, location="args")
566597
parser.add_argument("limit", type=inputs.int_range(1, 100), required=False, default=20, location="args")
598+
parser.add_argument("user_id", type=str, required=False, location="args")
599+
parser.add_argument("named_only", type=inputs.boolean, required=False, default=False, location="args")
600+
args = parser.parse_args()
601+
page = int(args.get("page", 1))
602+
limit = int(args.get("limit", 10))
603+
user_id = args.get("user_id")
604+
named_only = args.get("named_only", False)
605+
606+
if user_id:
607+
if user_id != current_user.id:
608+
raise Forbidden()
609+
user_id = cast(str, user_id)
610+
611+
workflow_service = WorkflowService()
612+
with Session(db.engine) as session:
613+
workflows, has_more = workflow_service.get_all_published_workflow(
614+
session=session,
615+
app_model=app_model,
616+
page=page,
617+
limit=limit,
618+
user_id=user_id,
619+
named_only=named_only,
620+
)
621+
622+
return {
623+
"items": workflows,
624+
"page": page,
625+
"limit": limit,
626+
"has_more": has_more,
627+
}
628+
629+
630+
class WorkflowByIdApi(Resource):
631+
@setup_required
632+
@login_required
633+
@account_initialization_required
634+
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
635+
@marshal_with(workflow_fields)
636+
def patch(self, app_model: App, workflow_id: str):
637+
"""
638+
Update workflow attributes
639+
"""
640+
# Check permission
641+
if not current_user.is_editor:
642+
raise Forbidden()
643+
644+
if not isinstance(current_user, Account):
645+
raise Forbidden()
646+
647+
parser = reqparse.RequestParser()
648+
parser.add_argument("marked_name", type=str, required=False, location="json")
649+
parser.add_argument("marked_comment", type=str, required=False, location="json")
567650
args = parser.parse_args()
568-
page = args.get("page")
569-
limit = args.get("limit")
651+
652+
# Validate name and comment length
653+
if args.marked_name and len(args.marked_name) > 20:
654+
raise ValueError("Marked name cannot exceed 20 characters")
655+
if args.marked_comment and len(args.marked_comment) > 100:
656+
raise ValueError("Marked comment cannot exceed 100 characters")
657+
args = parser.parse_args()
658+
659+
# Prepare update data
660+
update_data = {}
661+
if args.get("marked_name") is not None:
662+
update_data["marked_name"] = args["marked_name"]
663+
if args.get("marked_comment") is not None:
664+
update_data["marked_comment"] = args["marked_comment"]
665+
666+
if not update_data:
667+
return {"message": "No valid fields to update"}, 400
668+
570669
workflow_service = WorkflowService()
571-
workflows, has_more = workflow_service.get_all_published_workflow(app_model=app_model, page=page, limit=limit)
572670

573-
return {"items": workflows, "page": page, "limit": limit, "has_more": has_more}
671+
# Create a session and manage the transaction
672+
with Session(db.engine, expire_on_commit=False) as session:
673+
workflow = workflow_service.update_workflow(
674+
session=session,
675+
workflow_id=workflow_id,
676+
tenant_id=app_model.tenant_id,
677+
account_id=current_user.id,
678+
data=update_data,
679+
)
680+
681+
if not workflow:
682+
raise NotFound("Workflow not found")
683+
684+
# Commit the transaction in the controller
685+
session.commit()
686+
687+
return workflow
688+
689+
@setup_required
690+
@login_required
691+
@account_initialization_required
692+
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
693+
def delete(self, app_model: App, workflow_id: str):
694+
"""
695+
Delete workflow
696+
"""
697+
# Check permission
698+
if not current_user.is_editor:
699+
raise Forbidden()
700+
701+
if not isinstance(current_user, Account):
702+
raise Forbidden()
703+
704+
workflow_service = WorkflowService()
705+
706+
# Create a session and manage the transaction
707+
with Session(db.engine) as session:
708+
try:
709+
workflow_service.delete_workflow(
710+
session=session, workflow_id=workflow_id, tenant_id=app_model.tenant_id
711+
)
712+
# Commit the transaction in the controller
713+
session.commit()
714+
except WorkflowInUseError as e:
715+
abort(400, description=str(e))
716+
except DraftWorkflowDeletionError as e:
717+
abort(400, description=str(e))
718+
except ValueError as e:
719+
raise NotFound(str(e))
720+
721+
return None, 204
574722

575723

576-
api.add_resource(DraftWorkflowApi, "/apps/<uuid:app_id>/workflows/draft")
577-
api.add_resource(WorkflowConfigApi, "/apps/<uuid:app_id>/workflows/draft/config")
578-
api.add_resource(AdvancedChatDraftWorkflowRunApi, "/apps/<uuid:app_id>/advanced-chat/workflows/draft/run")
579-
api.add_resource(DraftWorkflowRunApi, "/apps/<uuid:app_id>/workflows/draft/run")
580-
api.add_resource(WorkflowTaskStopApi, "/apps/<uuid:app_id>/workflow-runs/tasks/<string:task_id>/stop")
581-
api.add_resource(DraftWorkflowNodeRunApi, "/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/run")
724+
api.add_resource(
725+
DraftWorkflowApi,
726+
"/apps/<uuid:app_id>/workflows/draft",
727+
)
728+
api.add_resource(
729+
WorkflowConfigApi,
730+
"/apps/<uuid:app_id>/workflows/draft/config",
731+
)
732+
api.add_resource(
733+
AdvancedChatDraftWorkflowRunApi,
734+
"/apps/<uuid:app_id>/advanced-chat/workflows/draft/run",
735+
)
736+
api.add_resource(
737+
DraftWorkflowRunApi,
738+
"/apps/<uuid:app_id>/workflows/draft/run",
739+
)
740+
api.add_resource(
741+
WorkflowTaskStopApi,
742+
"/apps/<uuid:app_id>/workflow-runs/tasks/<string:task_id>/stop",
743+
)
744+
api.add_resource(
745+
DraftWorkflowNodeRunApi,
746+
"/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/run",
747+
)
582748
api.add_resource(
583749
AdvancedChatDraftRunIterationNodeApi,
584750
"/apps/<uuid:app_id>/advanced-chat/workflows/draft/iteration/nodes/<string:node_id>/run",
585751
)
586752
api.add_resource(
587-
WorkflowDraftRunIterationNodeApi, "/apps/<uuid:app_id>/workflows/draft/iteration/nodes/<string:node_id>/run"
753+
WorkflowDraftRunIterationNodeApi,
754+
"/apps/<uuid:app_id>/workflows/draft/iteration/nodes/<string:node_id>/run",
588755
)
589756
api.add_resource(
590757
AdvancedChatDraftRunLoopNodeApi,
591758
"/apps/<uuid:app_id>/advanced-chat/workflows/draft/loop/nodes/<string:node_id>/run",
592759
)
593-
api.add_resource(WorkflowDraftRunLoopNodeApi, "/apps/<uuid:app_id>/workflows/draft/loop/nodes/<string:node_id>/run")
594-
api.add_resource(PublishedWorkflowApi, "/apps/<uuid:app_id>/workflows/publish")
595-
api.add_resource(PublishedAllWorkflowApi, "/apps/<uuid:app_id>/workflows")
596-
api.add_resource(DefaultBlockConfigsApi, "/apps/<uuid:app_id>/workflows/default-workflow-block-configs")
597760
api.add_resource(
598-
DefaultBlockConfigApi, "/apps/<uuid:app_id>/workflows/default-workflow-block-configs/<string:block_type>"
761+
WorkflowDraftRunLoopNodeApi,
762+
"/apps/<uuid:app_id>/workflows/draft/loop/nodes/<string:node_id>/run",
763+
)
764+
api.add_resource(
765+
PublishedWorkflowApi,
766+
"/apps/<uuid:app_id>/workflows/publish",
767+
)
768+
api.add_resource(
769+
PublishedAllWorkflowApi,
770+
"/apps/<uuid:app_id>/workflows",
771+
)
772+
api.add_resource(
773+
DefaultBlockConfigsApi,
774+
"/apps/<uuid:app_id>/workflows/default-workflow-block-configs",
775+
)
776+
api.add_resource(
777+
DefaultBlockConfigApi,
778+
"/apps/<uuid:app_id>/workflows/default-workflow-block-configs/<string:block_type>",
779+
)
780+
api.add_resource(
781+
ConvertToWorkflowApi,
782+
"/apps/<uuid:app_id>/convert-to-workflow",
783+
)
784+
api.add_resource(
785+
WorkflowByIdApi,
786+
"/apps/<uuid:app_id>/workflows/<string:workflow_id>",
599787
)
600-
api.add_resource(ConvertToWorkflowApi, "/apps/<uuid:app_id>/convert-to-workflow")

api/fields/workflow_fields.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ def format(self, value):
4545
"graph": fields.Raw(attribute="graph_dict"),
4646
"features": fields.Raw(attribute="features_dict"),
4747
"hash": fields.String(attribute="unique_hash"),
48-
"version": fields.String(attribute="version"),
48+
"version": fields.String,
49+
"marked_name": fields.String,
50+
"marked_comment": fields.String,
4951
"created_by": fields.Nested(simple_account_fields, attribute="created_by_account"),
5052
"created_at": TimestampField,
5153
"updated_by": fields.Nested(simple_account_fields, attribute="updated_by_account", allow_null=True),
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""add marked_name and marked_comment in workflows
2+
3+
Revision ID: ee79d9b1c156
4+
Revises: 4413929e1ec2
5+
Create Date: 2025-03-03 14:36:05.750346
6+
7+
"""
8+
from alembic import op
9+
import models as models
10+
import sqlalchemy as sa
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = 'ee79d9b1c156'
15+
down_revision = '5511c782ee4c'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
with op.batch_alter_table('workflows', schema=None) as batch_op:
22+
batch_op.add_column(sa.Column('marked_name', sa.String(), nullable=False, server_default=''))
23+
batch_op.add_column(sa.Column('marked_comment', sa.String(), nullable=False, server_default=''))
24+
25+
26+
def downgrade():
27+
with op.batch_alter_table('workflows', schema=None) as batch_op:
28+
batch_op.drop_column('marked_comment')
29+
batch_op.drop_column('marked_name')

api/models/workflow.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from collections.abc import Mapping, Sequence
33
from datetime import UTC, datetime
44
from enum import Enum
5-
from typing import TYPE_CHECKING, Any, Optional, Union
5+
from typing import TYPE_CHECKING, Any, Optional, Self, Union
6+
from uuid import uuid4
67

78
if TYPE_CHECKING:
89
from models.model import AppMode
@@ -108,7 +109,9 @@ class Workflow(Base):
108109
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
109110
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
110111
type: Mapped[str] = mapped_column(db.String(255), nullable=False)
111-
version: Mapped[str] = mapped_column(db.String(255), nullable=False)
112+
version: Mapped[str]
113+
marked_name: Mapped[str] = mapped_column(default="", server_default="")
114+
marked_comment: Mapped[str] = mapped_column(default="", server_default="")
112115
graph: Mapped[str] = mapped_column(sa.Text)
113116
_features: Mapped[str] = mapped_column("features", sa.TEXT)
114117
created_by: Mapped[str] = mapped_column(StringUUID, nullable=False)
@@ -127,8 +130,9 @@ class Workflow(Base):
127130
"conversation_variables", db.Text, nullable=False, server_default="{}"
128131
)
129132

130-
def __init__(
131-
self,
133+
@classmethod
134+
def new(
135+
cls,
132136
*,
133137
tenant_id: str,
134138
app_id: str,
@@ -139,16 +143,25 @@ def __init__(
139143
created_by: str,
140144
environment_variables: Sequence[Variable],
141145
conversation_variables: Sequence[Variable],
142-
):
143-
self.tenant_id = tenant_id
144-
self.app_id = app_id
145-
self.type = type
146-
self.version = version
147-
self.graph = graph
148-
self.features = features
149-
self.created_by = created_by
150-
self.environment_variables = environment_variables or []
151-
self.conversation_variables = conversation_variables or []
146+
marked_name: str = "",
147+
marked_comment: str = "",
148+
) -> Self:
149+
workflow = Workflow()
150+
workflow.id = str(uuid4())
151+
workflow.tenant_id = tenant_id
152+
workflow.app_id = app_id
153+
workflow.type = type
154+
workflow.version = version
155+
workflow.graph = graph
156+
workflow.features = features
157+
workflow.created_by = created_by
158+
workflow.environment_variables = environment_variables or []
159+
workflow.conversation_variables = conversation_variables or []
160+
workflow.marked_name = marked_name
161+
workflow.marked_comment = marked_comment
162+
workflow.created_at = datetime.now(UTC).replace(tzinfo=None)
163+
workflow.updated_at = workflow.created_at
164+
return workflow
152165

153166
@property
154167
def created_by_account(self):

0 commit comments

Comments
 (0)