Commit 4432e055 authored by takatost's avatar takatost

add workflow app log api

parent 403c2f43
...@@ -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) 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): ...@@ -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): ...@@ -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): ...@@ -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')
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')
from flask_restful import fields
simple_end_user_fields = {
'id': fields.String,
'type': fields.String,
'is_anonymous': fields.Boolean,
'session_id': fields.String,
}
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 = { ...@@ -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
}
\ No newline at end of file
# -*- coding:utf-8 -*- from enum import Enum
\ No newline at end of file
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 ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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 ...@@ -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 ...@@ -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
......
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: ...@@ -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: ...@@ -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: ...@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment