mirror of
https://github.com/langgenius/dify.git
synced 2026-04-28 03:36:36 +08:00
add workflow app log api
This commit is contained in:
parent
403c2f436d
commit
4432e055be
@ -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)
|
model_config, site, statistic, workflow, 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
|
||||||
|
|||||||
@ -40,9 +40,9 @@ class AppListApi(Resource):
|
|||||||
|
|
||||||
# get app list
|
# get app list
|
||||||
app_service = AppService()
|
app_service = AppService()
|
||||||
app_models = app_service.get_paginate_apps(current_user.current_tenant_id, args)
|
app_pagination = app_service.get_paginate_apps(current_user.current_tenant_id, args)
|
||||||
|
|
||||||
return app_models
|
return app_pagination
|
||||||
|
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@ -51,6 +51,41 @@ class DraftWorkflowApi(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PublishedWorkflowApi(Resource):
|
||||||
|
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||||
|
@marshal_with(workflow_fields)
|
||||||
|
def get(self, app_model: App):
|
||||||
|
"""
|
||||||
|
Get published workflow
|
||||||
|
"""
|
||||||
|
# fetch published workflow by app_model
|
||||||
|
workflow_service = WorkflowService()
|
||||||
|
workflow = workflow_service.get_published_workflow(app_model=app_model)
|
||||||
|
|
||||||
|
# return workflow, if not found, return None
|
||||||
|
return workflow
|
||||||
|
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||||
|
def post(self, app_model: App):
|
||||||
|
"""
|
||||||
|
Publish workflow
|
||||||
|
"""
|
||||||
|
workflow_service = WorkflowService()
|
||||||
|
workflow_service.publish_workflow(app_model=app_model, account=current_user)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"result": "success"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultBlockConfigApi(Resource):
|
class DefaultBlockConfigApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@ -88,5 +123,6 @@ class ConvertToWorkflowApi(Resource):
|
|||||||
|
|
||||||
|
|
||||||
api.add_resource(DraftWorkflowApi, '/apps/<uuid:app_id>/workflows/draft')
|
api.add_resource(DraftWorkflowApi, '/apps/<uuid:app_id>/workflows/draft')
|
||||||
|
api.add_resource(PublishedWorkflowApi, '/apps/<uuid:app_id>/workflows/published')
|
||||||
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.add_resource(ConvertToWorkflowApi, '/apps/<uuid:app_id>/convert-to-workflow')
|
api.add_resource(ConvertToWorkflowApi, '/apps/<uuid:app_id>/convert-to-workflow')
|
||||||
|
|||||||
41
api/controllers/console/app/workflow_app_log.py
Normal file
41
api/controllers/console/app/workflow_app_log.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from flask_restful import Resource, marshal_with, reqparse
|
||||||
|
from flask_restful.inputs import int_range
|
||||||
|
|
||||||
|
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 fields.workflow_app_log_fields import workflow_app_log_pagination_fields
|
||||||
|
from libs.login import login_required
|
||||||
|
from models.model import AppMode, App
|
||||||
|
from services.workflow_app_service import WorkflowAppService
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowAppLogApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@login_required
|
||||||
|
@account_initialization_required
|
||||||
|
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||||
|
@marshal_with(workflow_app_log_pagination_fields)
|
||||||
|
def get(self, app_model: App):
|
||||||
|
"""
|
||||||
|
Get workflow app logs
|
||||||
|
"""
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument('keyword', type=str, location='args')
|
||||||
|
parser.add_argument('status', type=str, choices=['succeeded', 'failed', 'stopped'], location='args')
|
||||||
|
parser.add_argument('page', type=int_range(1, 99999), default=1, location='args')
|
||||||
|
parser.add_argument('limit', type=int_range(1, 100), default=20, location='args')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# get paginate workflow app logs
|
||||||
|
workflow_app_service = WorkflowAppService()
|
||||||
|
workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_app_logs(
|
||||||
|
app_model=app_model,
|
||||||
|
args=args
|
||||||
|
)
|
||||||
|
|
||||||
|
return workflow_app_log_pagination
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(WorkflowAppLogApi, '/apps/<uuid:app_id>/workflow-app-logs')
|
||||||
8
api/fields/end_user_fields.py
Normal file
8
api/fields/end_user_fields.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from flask_restful import fields
|
||||||
|
|
||||||
|
simple_end_user_fields = {
|
||||||
|
'id': fields.String,
|
||||||
|
'type': fields.String,
|
||||||
|
'is_anonymous': fields.Boolean,
|
||||||
|
'session_id': fields.String,
|
||||||
|
}
|
||||||
25
api/fields/workflow_app_log_fields.py
Normal file
25
api/fields/workflow_app_log_fields.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from flask_restful import fields
|
||||||
|
|
||||||
|
from fields.end_user_fields import simple_end_user_fields
|
||||||
|
from fields.member_fields import simple_account_fields
|
||||||
|
from fields.workflow_fields import workflow_run_fields
|
||||||
|
from libs.helper import TimestampField
|
||||||
|
|
||||||
|
|
||||||
|
workflow_app_log_partial_fields = {
|
||||||
|
"id": fields.String,
|
||||||
|
"workflow_run": fields.Nested(workflow_run_fields, attribute='workflow_run', allow_null=True),
|
||||||
|
"created_from": fields.String,
|
||||||
|
"created_by_role": fields.String,
|
||||||
|
"created_by_account": fields.Nested(simple_account_fields, attribute='created_by_account', allow_null=True),
|
||||||
|
"created_by_end_user": fields.Nested(simple_end_user_fields, attribute='created_by_end_user', allow_null=True),
|
||||||
|
"created_at": TimestampField
|
||||||
|
}
|
||||||
|
|
||||||
|
workflow_app_log_pagination_fields = {
|
||||||
|
'page': fields.Integer,
|
||||||
|
'limit': fields.Integer(attribute='per_page'),
|
||||||
|
'total': fields.Integer,
|
||||||
|
'has_more': fields.Boolean(attribute='has_next'),
|
||||||
|
'data': fields.List(fields.Nested(workflow_app_log_partial_fields), attribute='items')
|
||||||
|
}
|
||||||
@ -13,3 +13,16 @@ workflow_fields = {
|
|||||||
'updated_by': fields.Nested(simple_account_fields, attribute='updated_by_account', allow_null=True),
|
'updated_by': fields.Nested(simple_account_fields, attribute='updated_by_account', allow_null=True),
|
||||||
'updated_at': TimestampField
|
'updated_at': TimestampField
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workflow_run_fields = {
|
||||||
|
"id": fields.String,
|
||||||
|
"version": fields.String,
|
||||||
|
"status": fields.String,
|
||||||
|
"error": fields.String,
|
||||||
|
"elapsed_time": fields.Float,
|
||||||
|
"total_tokens": fields.Integer,
|
||||||
|
"total_price": fields.Float,
|
||||||
|
"currency": fields.String,
|
||||||
|
"total_steps": fields.Integer,
|
||||||
|
"finished_at": TimestampField
|
||||||
|
}
|
||||||
@ -1 +1,44 @@
|
|||||||
# -*- coding:utf-8 -*-
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class CreatedByRole(Enum):
|
||||||
|
"""
|
||||||
|
Enum class for createdByRole
|
||||||
|
"""
|
||||||
|
ACCOUNT = "account"
|
||||||
|
END_USER = "end_user"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def value_of(cls, value: str) -> 'CreatedByRole':
|
||||||
|
"""
|
||||||
|
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 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}')
|
||||||
|
|||||||
@ -5,6 +5,7 @@ from sqlalchemy.dialects.postgresql import UUID
|
|||||||
|
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
|
from models.model import EndUser
|
||||||
|
|
||||||
|
|
||||||
class CreatedByRole(Enum):
|
class CreatedByRole(Enum):
|
||||||
@ -148,6 +149,7 @@ class WorkflowRunStatus(Enum):
|
|||||||
RUNNING = 'running'
|
RUNNING = 'running'
|
||||||
SUCCEEDED = 'succeeded'
|
SUCCEEDED = 'succeeded'
|
||||||
FAILED = 'failed'
|
FAILED = 'failed'
|
||||||
|
STOPPED = 'stopped'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def value_of(cls, value: str) -> 'WorkflowRunStatus':
|
def value_of(cls, value: str) -> 'WorkflowRunStatus':
|
||||||
@ -184,7 +186,7 @@ class WorkflowRun(db.Model):
|
|||||||
- version (string) Version
|
- version (string) Version
|
||||||
- graph (text) Workflow canvas configuration (JSON)
|
- graph (text) Workflow canvas configuration (JSON)
|
||||||
- inputs (text) Input parameters
|
- inputs (text) Input parameters
|
||||||
- status (string) Execution status, `running` / `succeeded` / `failed`
|
- status (string) Execution status, `running` / `succeeded` / `failed` / `stopped`
|
||||||
- outputs (text) `optional` Output content
|
- outputs (text) `optional` Output content
|
||||||
- error (string) `optional` Error reason
|
- error (string) `optional` Error reason
|
||||||
- elapsed_time (float) `optional` Time consumption (s)
|
- elapsed_time (float) `optional` Time consumption (s)
|
||||||
@ -366,3 +368,19 @@ class WorkflowAppLog(db.Model):
|
|||||||
created_by_role = db.Column(db.String(255), nullable=False)
|
created_by_role = db.Column(db.String(255), nullable=False)
|
||||||
created_by = db.Column(UUID, nullable=False)
|
created_by = db.Column(UUID, nullable=False)
|
||||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
|
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def workflow_run(self):
|
||||||
|
return WorkflowRun.query.get(self.workflow_run_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def created_by_account(self):
|
||||||
|
created_by_role = CreatedByRole.value_of(self.created_by_role)
|
||||||
|
return Account.query.get(self.created_by) \
|
||||||
|
if created_by_role == CreatedByRole.ACCOUNT else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def created_by_end_user(self):
|
||||||
|
created_by_role = CreatedByRole.value_of(self.created_by_role)
|
||||||
|
return EndUser.query.get(self.created_by) \
|
||||||
|
if created_by_role == CreatedByRole.END_USER else None
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from datetime import datetime
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
from flask_sqlalchemy.pagination import Pagination
|
||||||
|
|
||||||
from constants.model_template import default_app_templates
|
from constants.model_template import default_app_templates
|
||||||
from core.errors.error import ProviderTokenNotInitError
|
from core.errors.error import ProviderTokenNotInitError
|
||||||
@ -17,7 +18,7 @@ from services.workflow_service import WorkflowService
|
|||||||
|
|
||||||
|
|
||||||
class AppService:
|
class AppService:
|
||||||
def get_paginate_apps(self, tenant_id: str, args: dict) -> list[App]:
|
def get_paginate_apps(self, tenant_id: str, args: dict) -> Pagination:
|
||||||
"""
|
"""
|
||||||
Get app list with pagination
|
Get app list with pagination
|
||||||
:param tenant_id: tenant id
|
:param tenant_id: tenant id
|
||||||
|
|||||||
62
api/services/workflow_app_service.py
Normal file
62
api/services/workflow_app_service.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
from flask_sqlalchemy.pagination import Pagination
|
||||||
|
from sqlalchemy import or_, and_
|
||||||
|
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from models import CreatedByRole
|
||||||
|
from models.model import App, EndUser
|
||||||
|
from models.workflow import WorkflowAppLog, WorkflowRunStatus, WorkflowRun
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowAppService:
|
||||||
|
|
||||||
|
def get_paginate_workflow_app_logs(self, app_model: App, args: dict) -> Pagination:
|
||||||
|
"""
|
||||||
|
Get paginate workflow app logs
|
||||||
|
:param app: app model
|
||||||
|
:param args: request args
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
query = (
|
||||||
|
db.select(WorkflowAppLog)
|
||||||
|
.where(
|
||||||
|
WorkflowAppLog.tenant_id == app_model.tenant_id,
|
||||||
|
WorkflowAppLog.app_id == app_model.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
status = WorkflowRunStatus.value_of(args.get('status')) if args.get('status') else None
|
||||||
|
if args['keyword'] or status:
|
||||||
|
query = query.join(
|
||||||
|
WorkflowRun, WorkflowRun.id == WorkflowAppLog.workflow_run_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if args['keyword']:
|
||||||
|
keyword_val = f"%{args['keyword'][:30]}%"
|
||||||
|
keyword_conditions = [
|
||||||
|
WorkflowRun.inputs.ilike(keyword_val),
|
||||||
|
WorkflowRun.outputs.ilike(keyword_val),
|
||||||
|
# filter keyword by end user session id if created by end user role
|
||||||
|
and_(WorkflowRun.created_by_role == 'end_user', EndUser.session_id.ilike(keyword_val))
|
||||||
|
]
|
||||||
|
|
||||||
|
query = query.outerjoin(
|
||||||
|
EndUser,
|
||||||
|
and_(WorkflowRun.created_by == EndUser.id, WorkflowRun.created_by_role == CreatedByRole.END_USER.value)
|
||||||
|
).filter(or_(*keyword_conditions))
|
||||||
|
|
||||||
|
if status:
|
||||||
|
# join with workflow_run and filter by status
|
||||||
|
query = query.filter(
|
||||||
|
WorkflowRun.status == status.value
|
||||||
|
)
|
||||||
|
|
||||||
|
query = query.order_by(WorkflowAppLog.created_at.desc())
|
||||||
|
|
||||||
|
pagination = db.paginate(
|
||||||
|
query,
|
||||||
|
page=args['page'],
|
||||||
|
per_page=args['limit'],
|
||||||
|
error_out=False
|
||||||
|
)
|
||||||
|
|
||||||
|
return pagination
|
||||||
@ -15,7 +15,7 @@ class WorkflowService:
|
|||||||
Workflow Service
|
Workflow Service
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_draft_workflow(self, app_model: App) -> Workflow:
|
def get_draft_workflow(self, app_model: App) -> Optional[Workflow]:
|
||||||
"""
|
"""
|
||||||
Get draft workflow
|
Get draft workflow
|
||||||
"""
|
"""
|
||||||
@ -29,6 +29,26 @@ class WorkflowService:
|
|||||||
# return draft workflow
|
# return draft workflow
|
||||||
return workflow
|
return workflow
|
||||||
|
|
||||||
|
def get_published_workflow(self, app_model: App) -> Optional[Workflow]:
|
||||||
|
"""
|
||||||
|
Get published workflow
|
||||||
|
"""
|
||||||
|
app_model_config = app_model.app_model_config
|
||||||
|
|
||||||
|
if not app_model_config.workflow_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# fetch published 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 == app_model_config.workflow_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
# return published workflow
|
||||||
|
return workflow
|
||||||
|
|
||||||
|
|
||||||
def sync_draft_workflow(self, app_model: App, graph: dict, account: Account) -> Workflow:
|
def sync_draft_workflow(self, app_model: App, graph: dict, account: Account) -> Workflow:
|
||||||
"""
|
"""
|
||||||
Sync draft workflow
|
Sync draft workflow
|
||||||
@ -116,6 +136,8 @@ class WorkflowService:
|
|||||||
app_model.app_model_config_id = new_app_model_config.id
|
app_model.app_model_config_id = new_app_model_config.id
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
# TODO update app related datasets
|
||||||
|
|
||||||
# return new workflow
|
# return new workflow
|
||||||
return workflow
|
return workflow
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user