feat: get available evaluation workflow.

This commit is contained in:
FFXN 2026-03-30 18:18:57 +08:00
parent 3865483d95
commit a20c6bd4bc
3 changed files with 163 additions and 3 deletions

View File

@ -158,6 +158,7 @@ class WorkflowListQuery(BaseModel):
limit: int = Field(default=10, ge=1, le=100) limit: int = Field(default=10, ge=1, le=100)
user_id: str | None = None user_id: str | None = None
named_only: bool = False named_only: bool = False
keyword: str | None = Field(default=None, max_length=255)
class WorkflowUpdatePayload(BaseModel): class WorkflowUpdatePayload(BaseModel):

View File

@ -7,14 +7,15 @@ from typing import TYPE_CHECKING, ParamSpec, TypeVar, Union
from urllib.parse import quote from urllib.parse import quote
from flask import Response, request from flask import Response, request
from flask_restx import Resource, fields from flask_restx import Resource, fields, marshal
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from werkzeug.exceptions import BadRequest, NotFound from werkzeug.exceptions import BadRequest, Forbidden, NotFound
from controllers.common.schema import register_schema_models from controllers.common.schema import register_schema_models
from controllers.console import console_ns from controllers.console import console_ns
from controllers.console.app.workflow import WorkflowListQuery
from controllers.console.wraps import ( from controllers.console.wraps import (
account_initialization_required, account_initialization_required,
edit_permission_required, edit_permission_required,
@ -23,6 +24,7 @@ from controllers.console.wraps import (
from core.evaluation.entities.evaluation_entity import EvaluationCategory, EvaluationConfigData, EvaluationRunRequest from core.evaluation.entities.evaluation_entity import EvaluationCategory, EvaluationConfigData, EvaluationRunRequest
from extensions.ext_database import db from extensions.ext_database import db
from extensions.ext_storage import storage from extensions.ext_storage import storage
from fields.member_fields import simple_account_fields
from graphon.file import helpers as file_helpers from graphon.file import helpers as file_helpers
from libs.helper import TimestampField from libs.helper import TimestampField
from libs.login import current_account_with_tenant, login_required from libs.login import current_account_with_tenant, login_required
@ -36,6 +38,7 @@ from services.errors.evaluation import (
EvaluationNotFoundError, EvaluationNotFoundError,
) )
from services.evaluation_service import EvaluationService from services.evaluation_service import EvaluationService
from services.workflow_service import WorkflowService
if TYPE_CHECKING: if TYPE_CHECKING:
from models.evaluation import EvaluationRun, EvaluationRunItem from models.evaluation import EvaluationRun, EvaluationRunItem
@ -125,6 +128,33 @@ evaluation_detail_fields = {
evaluation_detail_model = console_ns.model("EvaluationDetail", evaluation_detail_fields) evaluation_detail_model = console_ns.model("EvaluationDetail", evaluation_detail_fields)
available_evaluation_workflow_list_fields = {
"id": fields.String,
"app_id": fields.String,
"app_name": fields.String,
"type": fields.String,
"version": fields.String,
"marked_name": fields.String,
"marked_comment": fields.String,
"hash": fields.String,
"created_by": fields.Nested(simple_account_fields),
"created_at": TimestampField,
"updated_by": fields.Nested(simple_account_fields, allow_null=True),
"updated_at": TimestampField,
}
available_evaluation_workflow_pagination_fields = {
"items": fields.List(fields.Nested(available_evaluation_workflow_list_fields)),
"page": fields.Integer,
"limit": fields.Integer,
"has_more": fields.Boolean,
}
available_evaluation_workflow_pagination_model = console_ns.model(
"AvailableEvaluationWorkflowPagination",
available_evaluation_workflow_pagination_fields,
)
def get_evaluation_target(view_func: Callable[P, R]): def get_evaluation_target(view_func: Callable[P, R]):
""" """
@ -601,6 +631,81 @@ class EvaluationVersionApi(Resource):
} }
@console_ns.route("/workspaces/current/available-evaluation-workflows")
class AvailableEvaluationWorkflowsApi(Resource):
@console_ns.expect(console_ns.models[WorkflowListQuery.__name__])
@console_ns.doc("list_available_evaluation_workflows")
@console_ns.doc(description="List published evaluation workflows in the current workspace (all apps)")
@console_ns.response(
200,
"Available evaluation workflows retrieved",
available_evaluation_workflow_pagination_model,
)
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
def get(self):
"""List published evaluation-type workflows for the current tenant (cross-app)."""
current_user, current_tenant_id = current_account_with_tenant()
args = WorkflowListQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore
page = args.page
limit = args.limit
user_id = args.user_id
named_only = args.named_only
keyword = args.keyword
if user_id and user_id != current_user.id:
raise Forbidden()
workflow_service = WorkflowService()
with Session(db.engine) as session:
workflows, has_more = workflow_service.list_published_evaluation_workflows(
session=session,
tenant_id=current_tenant_id,
page=page,
limit=limit,
user_id=user_id,
named_only=named_only,
keyword=keyword,
)
app_ids = {w.app_id for w in workflows}
if app_ids:
apps = session.scalars(select(App).where(App.id.in_(app_ids))).all()
app_names = {a.id: a.name for a in apps}
else:
app_names = {}
items = []
for wf in workflows:
items.append(
{
"id": wf.id,
"app_id": wf.app_id,
"app_name": app_names.get(wf.app_id, ""),
"type": wf.type.value,
"version": wf.version,
"marked_name": wf.marked_name,
"marked_comment": wf.marked_comment,
"hash": wf.unique_hash,
"created_by": wf.created_by_account,
"created_at": wf.created_at,
"updated_by": wf.updated_by_account,
"updated_at": wf.updated_at,
}
)
return (
marshal(
{"items": items, "page": page, "limit": limit, "has_more": has_more},
available_evaluation_workflow_pagination_fields,
),
200,
)
# ---- Serialization Helpers ---- # ---- Serialization Helpers ----

View File

@ -5,7 +5,7 @@ import uuid
from collections.abc import Callable, Generator, Mapping, Sequence from collections.abc import Callable, Generator, Mapping, Sequence
from typing import Any, cast from typing import Any, cast
from sqlalchemy import exists, select from sqlalchemy import and_, exists, or_, select
from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.orm import Session, sessionmaker
from configs import dify_config from configs import dify_config
@ -58,6 +58,7 @@ from graphon.variables import VariableBase
from graphon.variables.input_entities import VariableEntityType from graphon.variables.input_entities import VariableEntityType
from graphon.variables.variables import Variable from graphon.variables.variables import Variable
from libs.datetime_utils import naive_utc_now from libs.datetime_utils import naive_utc_now
from libs.helper import escape_like_pattern
from models import Account from models import Account
from models.human_input import HumanInputFormRecipient, RecipientType from models.human_input import HumanInputFormRecipient, RecipientType
from models.model import App, AppMode from models.model import App, AppMode
@ -240,6 +241,59 @@ class WorkflowService:
return workflows, has_more return workflows, has_more
def list_published_evaluation_workflows(
self,
*,
session: Session,
tenant_id: str,
page: int,
limit: int,
user_id: str | None,
named_only: bool = False,
keyword: str | None = None,
) -> tuple[Sequence[Workflow], bool]:
"""
List published evaluation-type workflows for a tenant (cross-app), excluding draft rows.
When ``keyword`` is non-empty, match workflows whose marked name or parent app name contains
the substring (case-insensitive, LIKE wildcards escaped).
"""
stmt = select(Workflow).where(
Workflow.tenant_id == tenant_id,
Workflow.type == WorkflowType.EVALUATION,
Workflow.version != Workflow.VERSION_DRAFT,
)
if user_id:
stmt = stmt.where(Workflow.created_by == user_id)
if named_only:
stmt = stmt.where(Workflow.marked_name != "")
keyword_stripped = keyword.strip() if keyword else ""
if keyword_stripped:
escaped = escape_like_pattern(keyword_stripped)
pattern = f"%{escaped}%"
stmt = stmt.join(
App,
and_(Workflow.app_id == App.id, App.tenant_id == tenant_id),
).where(
or_(
Workflow.marked_name.ilike(pattern, escape="\\"),
App.name.ilike(pattern, escape="\\"),
)
)
stmt = stmt.order_by(Workflow.created_at.desc()).limit(limit + 1).offset((page - 1) * limit)
workflows = session.scalars(stmt).all()
has_more = len(workflows) > limit
if has_more:
workflows = workflows[:-1]
return workflows, has_more
def sync_draft_workflow( def sync_draft_workflow(
self, self,
*, *,