Merge branch 'main' into feat/new-login

* main: (141 commits)
  fix(workflow/hooks/use-shortcuts): resolve issue of copy shortcut not working in workflow debug and preview panel (#8249)
  chore: cleanup pycodestyle E rules (#8269)
  let claude models in bedrock support the response_format parameter (#8220)
  enhance: improve empty data display for detail panel (#8266)
  chore: remove useless code (#8198)
  chore: apply pep8-naming rules for naming convention (#8261)
  fix:ollama text embedding 500 error (#8252)
  Update Gitlab query field, add query by path (#8244)
  editor can also create api key (#8214)
  fix: upload img icon mis-align in the chat input area (#8263)
  fix: truthy value (#8208)
  fix(workflow): IF-ELSE nodes connected to the same subsequent node cause execution to stop (#8247)
  fix: workflow parallel limit in ifelse node (#8242)
  fix: CHECK_UPDATE_URL comment (#8235)
  fix:error when adding the ollama embedding model (#8236)
  fix: improving the regionalization of translation (#8231)
  feat: add from_variable_selector for stream chunk / message event (#8228)
  fix(workflow): answers are output simultaneously across different braches in the question classifier node. (#8225)
  fix(workflow): in multi-parallel execution with multiple conditional branches (#8221)
  fix(docker/docker-compose.yaml): Set default value for `REDIS_SENTINEL_SOCKET_TIMEOUT` and `CELERY_SENTINEL_SOCKET_TIMEOUT` (#8218)
  ...
pull/8487/head
Joe 2 years ago
commit b6abd5b314

@ -20,7 +20,7 @@ jobs:
- name: Check changed files - name: Check changed files
id: changed-files id: changed-files
uses: tj-actions/changed-files@v44 uses: tj-actions/changed-files@v45
with: with:
files: api/** files: api/**
@ -66,7 +66,7 @@ jobs:
- name: Check changed files - name: Check changed files
id: changed-files id: changed-files
uses: tj-actions/changed-files@v44 uses: tj-actions/changed-files@v45
with: with:
files: web/** files: web/**
@ -97,7 +97,7 @@ jobs:
- name: Check changed files - name: Check changed files
id: changed-files id: changed-files
uses: tj-actions/changed-files@v44 uses: tj-actions/changed-files@v45
with: with:
files: | files: |
**.sh **.sh
@ -107,7 +107,7 @@ jobs:
dev/** dev/**
- name: Super-linter - name: Super-linter
uses: super-linter/super-linter/slim@v6 uses: super-linter/super-linter/slim@v7
if: steps.changed-files.outputs.any_changed == 'true' if: steps.changed-files.outputs.any_changed == 'true'
env: env:
BASH_SEVERITY: warning BASH_SEVERITY: warning

@ -0,0 +1,54 @@
name: Check i18n Files and Create PR
on:
pull_request:
types: [closed]
branches: [main]
jobs:
check-and-update:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # last 2 commits
- name: Check for file changes in i18n/en-US
id: check_files
run: |
recent_commit_sha=$(git rev-parse HEAD)
second_recent_commit_sha=$(git rev-parse HEAD~1)
changed_files=$(git diff --name-only $recent_commit_sha $second_recent_commit_sha -- 'i18n/en-US/*.ts')
echo "Changed files: $changed_files"
if [ -n "$changed_files" ]; then
echo "FILES_CHANGED=true" >> $GITHUB_ENV
else
echo "FILES_CHANGED=false" >> $GITHUB_ENV
fi
- name: Set up Node.js
if: env.FILES_CHANGED == 'true'
uses: actions/setup-node@v2
with:
node-version: 'lts/*'
- name: Install dependencies
if: env.FILES_CHANGED == 'true'
run: yarn install --frozen-lockfile
- name: Run npm script
if: env.FILES_CHANGED == 'true'
run: npm run auto-gen-i18n
- name: Create Pull Request
if: env.FILES_CHANGED == 'true'
uses: peter-evans/create-pull-request@v6
with:
commit-message: Update i18n files based on en-US changes
title: 'chore: translate i18n files'
body: This PR was automatically created to update i18n files based on changes in en-US locale.
branch: chore/automated-i18n-updates

@ -8,7 +8,7 @@ In terms of licensing, please take a minute to read our short [License and Contr
## Before you jump in ## Before you jump in
[Find](https://github.com/langgenius/dify/issues?q=is:issue+is:closed) an existing issue, or [open](https://github.com/langgenius/dify/issues/new/choose) a new one. We categorize issues into 2 types: [Find](https://github.com/langgenius/dify/issues?q=is:issue+is:open) an existing issue, or [open](https://github.com/langgenius/dify/issues/new/choose) a new one. We categorize issues into 2 types:
### Feature requests: ### Feature requests:

@ -8,7 +8,7 @@
## 在开始之前 ## 在开始之前
[查找](https://github.com/langgenius/dify/issues?q=is:issue+is:closed)现有问题,或 [创建](https://github.com/langgenius/dify/issues/new/choose) 一个新问题。我们将问题分为两类: [查找](https://github.com/langgenius/dify/issues?q=is:issue+is:open)现有问题,或 [创建](https://github.com/langgenius/dify/issues/new/choose) 一个新问题。我们将问题分为两类:
### 功能请求: ### 功能请求:

@ -10,7 +10,7 @@ Dify にコントリビュートしたいとお考えなのですね。それは
## 飛び込む前に ## 飛び込む前に
[既存の Issue](https://github.com/langgenius/dify/issues?q=is:issue+is:closed) を探すか、[新しい Issue](https://github.com/langgenius/dify/issues/new/choose) を作成してください。私たちは Issue を 2 つのタイプに分類しています。 [既存の Issue](https://github.com/langgenius/dify/issues?q=is:issue+is:open) を探すか、[新しい Issue](https://github.com/langgenius/dify/issues/new/choose) を作成してください。私たちは Issue を 2 つのタイプに分類しています。
### 機能リクエスト ### 機能リクエスト

@ -8,7 +8,7 @@ Về vấn đề cấp phép, xin vui lòng dành chút thời gian đọc qua [
## Trước khi bắt đầu ## Trước khi bắt đầu
[Tìm kiếm](https://github.com/langgenius/dify/issues?q=is:issue+is:closed) một vấn đề hiện có, hoặc [tạo mới](https://github.com/langgenius/dify/issues/new/choose) một vấn đề. Chúng tôi phân loại các vấn đề thành 2 loại: [Tìm kiếm](https://github.com/langgenius/dify/issues?q=is:issue+is:open) một vấn đề hiện có, hoặc [tạo mới](https://github.com/langgenius/dify/issues/new/choose) một vấn đề. Chúng tôi phân loại các vấn đề thành 2 loại:
### Yêu cầu tính năng: ### Yêu cầu tính năng:

@ -4,7 +4,7 @@ Dify is licensed under the Apache License 2.0, with the following additional con
1. Dify may be utilized commercially, including as a backend service for other applications or as an application development platform for enterprises. Should the conditions below be met, a commercial license must be obtained from the producer: 1. Dify may be utilized commercially, including as a backend service for other applications or as an application development platform for enterprises. Should the conditions below be met, a commercial license must be obtained from the producer:
a. Multi-tenant SaaS service: Unless explicitly authorized by Dify in writing, you may not use the Dify source code to operate a multi-tenant environment. a. Multi-tenant service: Unless explicitly authorized by Dify in writing, you may not use the Dify source code to operate a multi-tenant environment.
- Tenant Definition: Within the context of Dify, one tenant corresponds to one workspace. The workspace provides a separated area for each tenant's data and configurations. - Tenant Definition: Within the context of Dify, one tenant corresponds to one workspace. The workspace provides a separated area for each tenant's data and configurations.
b. LOGO and copyright information: In the process of using Dify's frontend components, you may not remove or modify the LOGO or copyright information in the Dify console or applications. This restriction is inapplicable to uses of Dify that do not involve its frontend components. b. LOGO and copyright information: In the process of using Dify's frontend components, you may not remove or modify the LOGO or copyright information in the Dify console or applications. This restriction is inapplicable to uses of Dify that do not involve its frontend components.

@ -39,7 +39,7 @@ DB_DATABASE=dify
# Storage configuration # Storage configuration
# use for store upload files, private keys... # use for store upload files, private keys...
# storage type: local, s3, azure-blob, google-storage # storage type: local, s3, azure-blob, google-storage, tencent-cos, huawei-obs, volcengine-tos
STORAGE_TYPE=local STORAGE_TYPE=local
STORAGE_LOCAL_PATH=storage STORAGE_LOCAL_PATH=storage
S3_USE_AWS_MANAGED_IAM=false S3_USE_AWS_MANAGED_IAM=false
@ -60,7 +60,8 @@ ALIYUN_OSS_SECRET_KEY=your-secret-key
ALIYUN_OSS_ENDPOINT=your-endpoint ALIYUN_OSS_ENDPOINT=your-endpoint
ALIYUN_OSS_AUTH_VERSION=v1 ALIYUN_OSS_AUTH_VERSION=v1
ALIYUN_OSS_REGION=your-region ALIYUN_OSS_REGION=your-region
# Don't start with '/'. OSS doesn't support leading slash in object names.
ALIYUN_OSS_PATH=your-path
# Google Storage configuration # Google Storage configuration
GOOGLE_STORAGE_BUCKET_NAME=yout-bucket-name GOOGLE_STORAGE_BUCKET_NAME=yout-bucket-name
GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64=your-google-service-account-json-base64-string GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64=your-google-service-account-json-base64-string
@ -72,6 +73,12 @@ TENCENT_COS_SECRET_ID=your-secret-id
TENCENT_COS_REGION=your-region TENCENT_COS_REGION=your-region
TENCENT_COS_SCHEME=your-scheme TENCENT_COS_SCHEME=your-scheme
# Huawei OBS Storage Configuration
HUAWEI_OBS_BUCKET_NAME=your-bucket-name
HUAWEI_OBS_SECRET_KEY=your-secret-key
HUAWEI_OBS_ACCESS_KEY=your-access-key
HUAWEI_OBS_SERVER=your-server-url
# OCI Storage configuration # OCI Storage configuration
OCI_ENDPOINT=your-endpoint OCI_ENDPOINT=your-endpoint
OCI_BUCKET_NAME=your-bucket-name OCI_BUCKET_NAME=your-bucket-name
@ -79,6 +86,13 @@ OCI_ACCESS_KEY=your-access-key
OCI_SECRET_KEY=your-secret-key OCI_SECRET_KEY=your-secret-key
OCI_REGION=your-region OCI_REGION=your-region
# Volcengine tos Storage configuration
VOLCENGINE_TOS_ENDPOINT=your-endpoint
VOLCENGINE_TOS_BUCKET_NAME=your-bucket-name
VOLCENGINE_TOS_ACCESS_KEY=your-access-key
VOLCENGINE_TOS_SECRET_KEY=your-secret-key
VOLCENGINE_TOS_REGION=your-region
# CORS configuration # CORS configuration
WEB_API_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,* WEB_API_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
CONSOLE_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,* CONSOLE_CORS_ALLOW_ORIGINS=http://127.0.0.1:3000,*
@ -100,11 +114,10 @@ QDRANT_GRPC_ENABLED=false
QDRANT_GRPC_PORT=6334 QDRANT_GRPC_PORT=6334
# Milvus configuration # Milvus configuration
MILVUS_HOST=127.0.0.1 MILVUS_URI=http://127.0.0.1:19530
MILVUS_PORT=19530 MILVUS_TOKEN=
MILVUS_USER=root MILVUS_USER=root
MILVUS_PASSWORD=Milvus MILVUS_PASSWORD=Milvus
MILVUS_SECURE=false
# MyScale configuration # MyScale configuration
MYSCALE_HOST=127.0.0.1 MYSCALE_HOST=127.0.0.1

@ -55,7 +55,7 @@ RUN apt-get update \
&& echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list \ && echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list \
&& apt-get update \ && apt-get update \
# For Security # For Security
&& apt-get install -y --no-install-recommends zlib1g=1:1.3.dfsg+really1.3.1-1 expat=2.6.2-1 libldap-2.5-0=2.5.18+dfsg-2 perl=5.38.2-5 libsqlite3-0=3.46.0-1 \ && apt-get install -y --no-install-recommends zlib1g=1:1.3.dfsg+really1.3.1-1 expat=2.6.3-1 libldap-2.5-0=2.5.18+dfsg-3 perl=5.38.2-5 libsqlite3-0=3.46.0-1 \
&& apt-get autoremove -y \ && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

@ -559,8 +559,9 @@ def add_qdrant_doc_id_index(field: str):
@click.command("create-tenant", help="Create account and tenant.") @click.command("create-tenant", help="Create account and tenant.")
@click.option("--email", prompt=True, help="The email address of the tenant account.") @click.option("--email", prompt=True, help="The email address of the tenant account.")
@click.option("--name", prompt=True, help="The workspace name of the tenant account.")
@click.option("--language", prompt=True, help="Account language, default: en-US.") @click.option("--language", prompt=True, help="Account language, default: en-US.")
def create_tenant(email: str, language: Optional[str] = None): def create_tenant(email: str, language: Optional[str] = None, name: Optional[str] = None):
""" """
Create tenant account Create tenant account
""" """
@ -580,13 +581,15 @@ def create_tenant(email: str, language: Optional[str] = None):
if language not in languages: if language not in languages:
language = "en-US" language = "en-US"
name = name.strip()
# generate random password # generate random password
new_password = secrets.token_urlsafe(16) new_password = secrets.token_urlsafe(16)
# register account # register account
account = RegisterService.register(email=email, name=account_name, password=new_password, language=language) account = RegisterService.register(email=email, name=account_name, password=new_password, language=language)
TenantService.create_owner_tenant_if_not_exist(account) TenantService.create_owner_tenant_if_not_exist(account, name)
click.echo( click.echo(
click.style( click.style(

@ -1,4 +1,4 @@
from typing import Optional from typing import Annotated, Optional
from pydantic import ( from pydantic import (
AliasChoices, AliasChoices,
@ -55,7 +55,7 @@ class CodeExecutionSandboxConfig(BaseSettings):
""" """
CODE_EXECUTION_ENDPOINT: HttpUrl = Field( CODE_EXECUTION_ENDPOINT: HttpUrl = Field(
description="endpoint URL of code execution servcie", description="endpoint URL of code execution service",
default="http://sandbox:8194", default="http://sandbox:8194",
) )
@ -226,20 +226,17 @@ class HttpConfig(BaseSettings):
def WEB_API_CORS_ALLOW_ORIGINS(self) -> list[str]: def WEB_API_CORS_ALLOW_ORIGINS(self) -> list[str]:
return self.inner_WEB_API_CORS_ALLOW_ORIGINS.split(",") return self.inner_WEB_API_CORS_ALLOW_ORIGINS.split(",")
HTTP_REQUEST_MAX_CONNECT_TIMEOUT: NonNegativeInt = Field( HTTP_REQUEST_MAX_CONNECT_TIMEOUT: Annotated[
description="", PositiveInt, Field(ge=10, description="connect timeout in seconds for HTTP request")
default=300, ] = 10
)
HTTP_REQUEST_MAX_READ_TIMEOUT: NonNegativeInt = Field( HTTP_REQUEST_MAX_READ_TIMEOUT: Annotated[
description="", PositiveInt, Field(ge=60, description="read timeout in seconds for HTTP request")
default=600, ] = 60
)
HTTP_REQUEST_MAX_WRITE_TIMEOUT: NonNegativeInt = Field( HTTP_REQUEST_MAX_WRITE_TIMEOUT: Annotated[
description="", PositiveInt, Field(ge=10, description="read timeout in seconds for HTTP request")
default=600, ] = 20
)
HTTP_REQUEST_NODE_MAX_BINARY_SIZE: PositiveInt = Field( HTTP_REQUEST_NODE_MAX_BINARY_SIZE: PositiveInt = Field(
description="", description="",
@ -427,7 +424,7 @@ class MailConfig(BaseSettings):
""" """
MAIL_TYPE: Optional[str] = Field( MAIL_TYPE: Optional[str] = Field(
description="Mail provider type name, default to None, availabile values are `smtp` and `resend`.", description="Mail provider type name, default to None, available values are `smtp` and `resend`.",
default=None, default=None,
) )

@ -1,7 +1,7 @@
from typing import Any, Optional from typing import Any, Optional
from urllib.parse import quote_plus from urllib.parse import quote_plus
from pydantic import Field, NonNegativeInt, PositiveInt, computed_field from pydantic import Field, NonNegativeInt, PositiveFloat, PositiveInt, computed_field
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from configs.middleware.cache.redis_config import RedisConfig from configs.middleware.cache.redis_config import RedisConfig
@ -9,8 +9,10 @@ from configs.middleware.storage.aliyun_oss_storage_config import AliyunOSSStorag
from configs.middleware.storage.amazon_s3_storage_config import S3StorageConfig from configs.middleware.storage.amazon_s3_storage_config import S3StorageConfig
from configs.middleware.storage.azure_blob_storage_config import AzureBlobStorageConfig from configs.middleware.storage.azure_blob_storage_config import AzureBlobStorageConfig
from configs.middleware.storage.google_cloud_storage_config import GoogleCloudStorageConfig from configs.middleware.storage.google_cloud_storage_config import GoogleCloudStorageConfig
from configs.middleware.storage.huawei_obs_storage_config import HuaweiCloudOBSStorageConfig
from configs.middleware.storage.oci_storage_config import OCIStorageConfig from configs.middleware.storage.oci_storage_config import OCIStorageConfig
from configs.middleware.storage.tencent_cos_storage_config import TencentCloudCOSStorageConfig from configs.middleware.storage.tencent_cos_storage_config import TencentCloudCOSStorageConfig
from configs.middleware.storage.volcengine_tos_storage_config import VolcengineTOSStorageConfig
from configs.middleware.vdb.analyticdb_config import AnalyticdbConfig from configs.middleware.vdb.analyticdb_config import AnalyticdbConfig
from configs.middleware.vdb.chroma_config import ChromaConfig from configs.middleware.vdb.chroma_config import ChromaConfig
from configs.middleware.vdb.elasticsearch_config import ElasticsearchConfig from configs.middleware.vdb.elasticsearch_config import ElasticsearchConfig
@ -157,6 +159,21 @@ class CeleryConfig(DatabaseConfig):
default=None, default=None,
) )
CELERY_USE_SENTINEL: Optional[bool] = Field(
description="Whether to use Redis Sentinel mode",
default=False,
)
CELERY_SENTINEL_MASTER_NAME: Optional[str] = Field(
description="Redis Sentinel master name",
default=None,
)
CELERY_SENTINEL_SOCKET_TIMEOUT: Optional[PositiveFloat] = Field(
description="Redis Sentinel socket timeout",
default=0.1,
)
@computed_field @computed_field
@property @property
def CELERY_RESULT_BACKEND(self) -> str | None: def CELERY_RESULT_BACKEND(self) -> str | None:
@ -184,6 +201,8 @@ class MiddlewareConfig(
AzureBlobStorageConfig, AzureBlobStorageConfig,
GoogleCloudStorageConfig, GoogleCloudStorageConfig,
TencentCloudCOSStorageConfig, TencentCloudCOSStorageConfig,
HuaweiCloudOBSStorageConfig,
VolcengineTOSStorageConfig,
S3StorageConfig, S3StorageConfig,
OCIStorageConfig, OCIStorageConfig,
# configs of vdb and vdb providers # configs of vdb and vdb providers

@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from pydantic import Field, NonNegativeInt, PositiveInt from pydantic import Field, NonNegativeInt, PositiveFloat, PositiveInt
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
@ -38,3 +38,33 @@ class RedisConfig(BaseSettings):
description="whether to use SSL for Redis connection", description="whether to use SSL for Redis connection",
default=False, default=False,
) )
REDIS_USE_SENTINEL: Optional[bool] = Field(
description="Whether to use Redis Sentinel mode",
default=False,
)
REDIS_SENTINELS: Optional[str] = Field(
description="Redis Sentinel nodes",
default=None,
)
REDIS_SENTINEL_SERVICE_NAME: Optional[str] = Field(
description="Redis Sentinel service name",
default=None,
)
REDIS_SENTINEL_USERNAME: Optional[str] = Field(
description="Redis Sentinel username",
default=None,
)
REDIS_SENTINEL_PASSWORD: Optional[str] = Field(
description="Redis Sentinel password",
default=None,
)
REDIS_SENTINEL_SOCKET_TIMEOUT: Optional[PositiveFloat] = Field(
description="Redis Sentinel socket timeout",
default=0.1,
)

@ -38,3 +38,8 @@ class AliyunOSSStorageConfig(BaseSettings):
description="Aliyun OSS authentication version", description="Aliyun OSS authentication version",
default=None, default=None,
) )
ALIYUN_OSS_PATH: Optional[str] = Field(
description="Aliyun OSS path",
default=None,
)

@ -0,0 +1,29 @@
from typing import Optional
from pydantic import BaseModel, Field
class HuaweiCloudOBSStorageConfig(BaseModel):
"""
Huawei Cloud OBS storage configs
"""
HUAWEI_OBS_BUCKET_NAME: Optional[str] = Field(
description="Huawei Cloud OBS bucket name",
default=None,
)
HUAWEI_OBS_ACCESS_KEY: Optional[str] = Field(
description="Huawei Cloud OBS Access key",
default=None,
)
HUAWEI_OBS_SECRET_KEY: Optional[str] = Field(
description="Huawei Cloud OBS Secret key",
default=None,
)
HUAWEI_OBS_SERVER: Optional[str] = Field(
description="Huawei Cloud OBS server URL",
default=None,
)

@ -0,0 +1,34 @@
from typing import Optional
from pydantic import BaseModel, Field
class VolcengineTOSStorageConfig(BaseModel):
"""
Volcengine tos storage configs
"""
VOLCENGINE_TOS_BUCKET_NAME: Optional[str] = Field(
description="Volcengine TOS Bucket Name",
default=None,
)
VOLCENGINE_TOS_ACCESS_KEY: Optional[str] = Field(
description="Volcengine TOS Access Key",
default=None,
)
VOLCENGINE_TOS_SECRET_KEY: Optional[str] = Field(
description="Volcengine TOS Secret Key",
default=None,
)
VOLCENGINE_TOS_ENDPOINT: Optional[str] = Field(
description="Volcengine TOS Endpoint URL",
default=None,
)
VOLCENGINE_TOS_REGION: Optional[str] = Field(
description="Volcengine TOS Region",
default=None,
)

@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from pydantic import Field, PositiveInt from pydantic import Field
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
@ -9,14 +9,14 @@ class MilvusConfig(BaseSettings):
Milvus configs Milvus configs
""" """
MILVUS_HOST: Optional[str] = Field( MILVUS_URI: Optional[str] = Field(
description="Milvus host", description="Milvus uri",
default=None, default="http://127.0.0.1:19530",
) )
MILVUS_PORT: PositiveInt = Field( MILVUS_TOKEN: Optional[str] = Field(
description="Milvus RestFul API port", description="Milvus token",
default=9091, default=None,
) )
MILVUS_USER: Optional[str] = Field( MILVUS_USER: Optional[str] = Field(
@ -29,11 +29,6 @@ class MilvusConfig(BaseSettings):
default=None, default=None,
) )
MILVUS_SECURE: bool = Field(
description="whether to use SSL connection for Milvus",
default=False,
)
MILVUS_DATABASE: str = Field( MILVUS_DATABASE: str = Field(
description="Milvus database, default to `default`", description="Milvus database, default to `default`",
default="default", default="default",

@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
CURRENT_VERSION: str = Field( CURRENT_VERSION: str = Field(
description="Dify version", description="Dify version",
default="0.7.2", default="0.8.0",
) )
COMMIT_SHA: str = Field( COMMIT_SHA: str = Field(

File diff suppressed because one or more lines are too long

@ -57,7 +57,7 @@ class BaseApiKeyListResource(Resource):
def post(self, resource_id): def post(self, resource_id):
resource_id = str(resource_id) resource_id = str(resource_id)
_get_resource(resource_id, current_user.current_tenant_id, self.resource_model) _get_resource(resource_id, current_user.current_tenant_id, self.resource_model)
if not current_user.is_admin_or_owner: if not current_user.is_editor:
raise Forbidden() raise Forbidden()
current_key_count = ( current_key_count = (

@ -174,6 +174,7 @@ class AppApi(Resource):
parser.add_argument("icon", type=str, location="json") parser.add_argument("icon", type=str, location="json")
parser.add_argument("icon_background", type=str, location="json") parser.add_argument("icon_background", type=str, location="json")
parser.add_argument("max_active_requests", type=int, location="json") parser.add_argument("max_active_requests", type=int, location="json")
parser.add_argument("use_icon_as_answer_icon", type=bool, location="json")
args = parser.parse_args() args = parser.parse_args()
app_service = AppService() app_service = AppService()

@ -20,7 +20,7 @@ from fields.conversation_fields import (
conversation_pagination_fields, conversation_pagination_fields,
conversation_with_summary_pagination_fields, conversation_with_summary_pagination_fields,
) )
from libs.helper import datetime_string from libs.helper import DatetimeString
from libs.login import login_required from libs.login import login_required
from models.model import AppMode, Conversation, EndUser, Message, MessageAnnotation from models.model import AppMode, Conversation, EndUser, Message, MessageAnnotation
@ -36,8 +36,8 @@ class CompletionConversationApi(Resource):
raise Forbidden() raise Forbidden()
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("keyword", type=str, location="args") parser.add_argument("keyword", type=str, location="args")
parser.add_argument("start", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
parser.add_argument("end", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
parser.add_argument( parser.add_argument(
"annotation_status", type=str, choices=["annotated", "not_annotated", "all"], default="all", location="args" "annotation_status", type=str, choices=["annotated", "not_annotated", "all"], default="all", location="args"
) )
@ -143,8 +143,8 @@ class ChatConversationApi(Resource):
raise Forbidden() raise Forbidden()
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("keyword", type=str, location="args") parser.add_argument("keyword", type=str, location="args")
parser.add_argument("start", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
parser.add_argument("end", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
parser.add_argument( parser.add_argument(
"annotation_status", type=str, choices=["annotated", "not_annotated", "all"], default="all", location="args" "annotation_status", type=str, choices=["annotated", "not_annotated", "all"], default="all", location="args"
) )
@ -201,7 +201,11 @@ class ChatConversationApi(Resource):
start_datetime_timezone = timezone.localize(start_datetime) start_datetime_timezone = timezone.localize(start_datetime)
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone) start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
query = query.where(Conversation.created_at >= start_datetime_utc) match args["sort_by"]:
case "updated_at" | "-updated_at":
query = query.where(Conversation.updated_at >= start_datetime_utc)
case "created_at" | "-created_at" | _:
query = query.where(Conversation.created_at >= start_datetime_utc)
if args["end"]: if args["end"]:
end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M") end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
@ -210,7 +214,11 @@ class ChatConversationApi(Resource):
end_datetime_timezone = timezone.localize(end_datetime) end_datetime_timezone = timezone.localize(end_datetime)
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone) end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
query = query.where(Conversation.created_at < end_datetime_utc) match args["sort_by"]:
case "updated_at" | "-updated_at":
query = query.where(Conversation.updated_at <= end_datetime_utc)
case "created_at" | "-created_at" | _:
query = query.where(Conversation.created_at <= end_datetime_utc)
if args["annotation_status"] == "annotated": if args["annotation_status"] == "annotated":
query = query.options(joinedload(Conversation.message_annotations)).join( query = query.options(joinedload(Conversation.message_annotations)).join(

@ -34,6 +34,7 @@ def parse_app_site_args():
) )
parser.add_argument("prompt_public", type=bool, required=False, location="json") parser.add_argument("prompt_public", type=bool, required=False, location="json")
parser.add_argument("show_workflow_steps", type=bool, required=False, location="json") parser.add_argument("show_workflow_steps", type=bool, required=False, location="json")
parser.add_argument("use_icon_as_answer_icon", type=bool, required=False, location="json")
return parser.parse_args() return parser.parse_args()
@ -68,6 +69,7 @@ class AppSite(Resource):
"customize_token_strategy", "customize_token_strategy",
"prompt_public", "prompt_public",
"show_workflow_steps", "show_workflow_steps",
"use_icon_as_answer_icon",
]: ]:
value = args.get(attr_name) value = args.get(attr_name)
if value is not None: if value is not None:

@ -11,7 +11,7 @@ from controllers.console.app.wraps import get_app_model
from controllers.console.setup import setup_required from controllers.console.setup import setup_required
from controllers.console.wraps import account_initialization_required from controllers.console.wraps import account_initialization_required
from extensions.ext_database import db from extensions.ext_database import db
from libs.helper import datetime_string from libs.helper import DatetimeString
from libs.login import login_required from libs.login import login_required
from models.model import AppMode from models.model import AppMode
@ -25,8 +25,8 @@ class DailyMessageStatistic(Resource):
account = current_user account = current_user
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("start", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
parser.add_argument("end", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
args = parser.parse_args() args = parser.parse_args()
sql_query = """ sql_query = """
@ -79,8 +79,8 @@ class DailyConversationStatistic(Resource):
account = current_user account = current_user
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("start", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
parser.add_argument("end", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
args = parser.parse_args() args = parser.parse_args()
sql_query = """ sql_query = """
@ -133,8 +133,8 @@ class DailyTerminalsStatistic(Resource):
account = current_user account = current_user
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("start", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
parser.add_argument("end", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
args = parser.parse_args() args = parser.parse_args()
sql_query = """ sql_query = """
@ -187,8 +187,8 @@ class DailyTokenCostStatistic(Resource):
account = current_user account = current_user
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("start", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
parser.add_argument("end", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
args = parser.parse_args() args = parser.parse_args()
sql_query = """ sql_query = """
@ -245,8 +245,8 @@ class AverageSessionInteractionStatistic(Resource):
account = current_user account = current_user
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("start", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
parser.add_argument("end", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
args = parser.parse_args() args = parser.parse_args()
sql_query = """SELECT date(DATE_TRUNC('day', c.created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date, sql_query = """SELECT date(DATE_TRUNC('day', c.created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date,
@ -307,8 +307,8 @@ class UserSatisfactionRateStatistic(Resource):
account = current_user account = current_user
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("start", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
parser.add_argument("end", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
args = parser.parse_args() args = parser.parse_args()
sql_query = """ sql_query = """
@ -369,8 +369,8 @@ class AverageResponseTimeStatistic(Resource):
account = current_user account = current_user
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("start", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
parser.add_argument("end", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
args = parser.parse_args() args = parser.parse_args()
sql_query = """ sql_query = """
@ -425,8 +425,8 @@ class TokensPerSecondStatistic(Resource):
account = current_user account = current_user
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("start", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
parser.add_argument("end", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
args = parser.parse_args() args = parser.parse_args()
sql_query = """SELECT date(DATE_TRUNC('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date, sql_query = """SELECT date(DATE_TRUNC('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date,

@ -11,7 +11,7 @@ from controllers.console.app.wraps import get_app_model
from controllers.console.setup import setup_required from controllers.console.setup import setup_required
from controllers.console.wraps import account_initialization_required from controllers.console.wraps import account_initialization_required
from extensions.ext_database import db from extensions.ext_database import db
from libs.helper import datetime_string from libs.helper import DatetimeString
from libs.login import login_required from libs.login import login_required
from models.model import AppMode from models.model import AppMode
from models.workflow import WorkflowRunTriggeredFrom from models.workflow import WorkflowRunTriggeredFrom
@ -26,8 +26,8 @@ class WorkflowDailyRunsStatistic(Resource):
account = current_user account = current_user
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("start", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
parser.add_argument("end", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
args = parser.parse_args() args = parser.parse_args()
sql_query = """ sql_query = """
@ -86,8 +86,8 @@ class WorkflowDailyTerminalsStatistic(Resource):
account = current_user account = current_user
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("start", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
parser.add_argument("end", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
args = parser.parse_args() args = parser.parse_args()
sql_query = """ sql_query = """
@ -146,8 +146,8 @@ class WorkflowDailyTokenCostStatistic(Resource):
account = current_user account = current_user
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("start", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
parser.add_argument("end", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
args = parser.parse_args() args = parser.parse_args()
sql_query = """ sql_query = """
@ -213,8 +213,8 @@ class WorkflowAverageAppInteractionStatistic(Resource):
account = current_user account = current_user
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("start", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("start", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
parser.add_argument("end", type=datetime_string("%Y-%m-%d %H:%M"), location="args") parser.add_argument("end", type=DatetimeString("%Y-%m-%d %H:%M"), location="args")
args = parser.parse_args() args = parser.parse_args()
sql_query = """ sql_query = """

@ -7,7 +7,8 @@ from constants.languages import supported_language
from controllers.console import api from controllers.console import api
from controllers.console.error import AlreadyActivateError from controllers.console.error import AlreadyActivateError
from extensions.ext_database import db from extensions.ext_database import db
from libs.helper import email, get_remote_ip, str_len, timezone from libs.helper import StrLen, email, get_remote_ip, timezone
from libs.password import valid_password
from models.account import AccountStatus, Tenant from models.account import AccountStatus, Tenant
from services.account_service import AccountService, RegisterService from services.account_service import AccountService, RegisterService
@ -45,7 +46,8 @@ class ActivateApi(Resource):
parser.add_argument("workspace_id", type=str, required=False, nullable=True, location="json") parser.add_argument("workspace_id", type=str, required=False, nullable=True, location="json")
parser.add_argument("email", type=email, required=False, nullable=True, location="json") parser.add_argument("email", type=email, required=False, nullable=True, location="json")
parser.add_argument("token", type=str, required=True, nullable=False, location="json") parser.add_argument("token", type=str, required=True, nullable=False, location="json")
parser.add_argument("name", type=str_len(30), required=True, nullable=False, location="json") parser.add_argument("name", type=StrLen(30), required=True, nullable=False, location="json")
parser.add_argument("password", type=valid_password, required=True, nullable=False, location="json")
parser.add_argument( parser.add_argument(
"interface_language", type=supported_language, required=True, nullable=False, location="json" "interface_language", type=supported_language, required=True, nullable=False, location="json"
) )

@ -18,7 +18,7 @@ from core.model_runtime.entities.model_entities import ModelType
from core.provider_manager import ProviderManager from core.provider_manager import ProviderManager
from core.rag.datasource.vdb.vector_type import VectorType from core.rag.datasource.vdb.vector_type import VectorType
from core.rag.extractor.entity.extract_setting import ExtractSetting from core.rag.extractor.entity.extract_setting import ExtractSetting
from core.rag.retrieval.retrival_methods import RetrievalMethod from core.rag.retrieval.retrieval_methods import RetrievalMethod
from extensions.ext_database import db from extensions.ext_database import db
from fields.app_fields import related_app_list from fields.app_fields import related_app_list
from fields.dataset_fields import dataset_detail_fields, dataset_query_detail_fields from fields.dataset_fields import dataset_detail_fields, dataset_query_detail_fields
@ -122,6 +122,7 @@ class DatasetListApi(Resource):
name=args["name"], name=args["name"],
indexing_technique=args["indexing_technique"], indexing_technique=args["indexing_technique"],
account=current_user, account=current_user,
permission=DatasetPermissionEnum.ONLY_ME,
) )
except services.errors.dataset.DatasetNameDuplicateError: except services.errors.dataset.DatasetNameDuplicateError:
raise DatasetNameDuplicateError() raise DatasetNameDuplicateError()

@ -302,6 +302,8 @@ class DatasetInitApi(Resource):
"doc_language", type=str, default="English", required=False, nullable=False, location="json" "doc_language", type=str, default="English", required=False, nullable=False, location="json"
) )
parser.add_argument("retrieval_model", type=dict, required=False, nullable=False, location="json") parser.add_argument("retrieval_model", type=dict, required=False, nullable=False, location="json")
parser.add_argument("embedding_model", type=str, required=False, nullable=True, location="json")
parser.add_argument("embedding_model_provider", type=str, required=False, nullable=True, location="json")
args = parser.parse_args() args = parser.parse_args()
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator # The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
@ -309,6 +311,8 @@ class DatasetInitApi(Resource):
raise Forbidden() raise Forbidden()
if args["indexing_technique"] == "high_quality": if args["indexing_technique"] == "high_quality":
if args["embedding_model"] is None or args["embedding_model_provider"] is None:
raise ValueError("embedding model and embedding model provider are required for high quality indexing.")
try: try:
model_manager = ModelManager() model_manager = ModelManager()
model_manager.get_default_model_instance( model_manager.get_default_model_instance(

@ -39,7 +39,7 @@ class FileApi(Resource):
@login_required @login_required
@account_initialization_required @account_initialization_required
@marshal_with(file_fields) @marshal_with(file_fields)
@cloud_edition_billing_resource_check(resource="documents") @cloud_edition_billing_resource_check("documents")
def post(self): def post(self):
# get file from request # get file from request
file = request.files["file"] file = request.files["file"]

@ -35,6 +35,7 @@ class InstalledAppsListApi(Resource):
"uninstallable": current_tenant_id == installed_app.app_owner_tenant_id, "uninstallable": current_tenant_id == installed_app.app_owner_tenant_id,
} }
for installed_app in installed_apps for installed_app in installed_apps
if installed_app.app is not None
] ]
installed_apps.sort( installed_apps.sort(
key=lambda app: ( key=lambda app: (

@ -4,7 +4,7 @@ from flask import session
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse
from configs import dify_config from configs import dify_config
from libs.helper import str_len from libs.helper import StrLen
from models.model import DifySetup from models.model import DifySetup
from services.account_service import TenantService from services.account_service import TenantService
@ -28,7 +28,7 @@ class InitValidateAPI(Resource):
raise AlreadySetupError() raise AlreadySetupError()
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("password", type=str_len(30), required=True, location="json") parser.add_argument("password", type=StrLen(30), required=True, location="json")
input_password = parser.parse_args()["password"] input_password = parser.parse_args()["password"]
if input_password != os.environ.get("INIT_PASSWORD"): if input_password != os.environ.get("INIT_PASSWORD"):

@ -4,7 +4,7 @@ from flask import request
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse
from configs import dify_config from configs import dify_config
from libs.helper import email, get_remote_ip, str_len from libs.helper import StrLen, email, get_remote_ip
from libs.password import valid_password from libs.password import valid_password
from models.model import DifySetup from models.model import DifySetup
from services.account_service import RegisterService, TenantService from services.account_service import RegisterService, TenantService
@ -40,7 +40,7 @@ class SetupApi(Resource):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json") parser.add_argument("email", type=email, required=True, location="json")
parser.add_argument("name", type=str_len(30), required=True, location="json") parser.add_argument("name", type=StrLen(30), required=True, location="json")
parser.add_argument("password", type=valid_password, required=True, location="json") parser.add_argument("password", type=valid_password, required=True, location="json")
args = parser.parse_args() args = parser.parse_args()

@ -13,7 +13,7 @@ from services.tag_service import TagService
def _validate_name(name): def _validate_name(name):
if not name or len(name) < 1 or len(name) > 40: if not name or len(name) < 1 or len(name) > 50:
raise ValueError("Name must be between 1 to 50 characters.") raise ValueError("Name must be between 1 to 50 characters.")
return name return name

@ -46,9 +46,7 @@ def only_edition_self_hosted(view):
return decorated return decorated
def cloud_edition_billing_resource_check( def cloud_edition_billing_resource_check(resource: str):
resource: str, error_msg: str = "You have reached the limit of your subscription."
):
def interceptor(view): def interceptor(view):
@wraps(view) @wraps(view)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
@ -60,22 +58,22 @@ def cloud_edition_billing_resource_check(
documents_upload_quota = features.documents_upload_quota documents_upload_quota = features.documents_upload_quota
annotation_quota_limit = features.annotation_quota_limit annotation_quota_limit = features.annotation_quota_limit
if resource == "members" and 0 < members.limit <= members.size: if resource == "members" and 0 < members.limit <= members.size:
abort(403, error_msg) abort(403, "The number of members has reached the limit of your subscription.")
elif resource == "apps" and 0 < apps.limit <= apps.size: elif resource == "apps" and 0 < apps.limit <= apps.size:
abort(403, error_msg) abort(403, "The number of apps has reached the limit of your subscription.")
elif resource == "vector_space" and 0 < vector_space.limit <= vector_space.size: elif resource == "vector_space" and 0 < vector_space.limit <= vector_space.size:
abort(403, error_msg) abort(403, "The capacity of the vector space has reached the limit of your subscription.")
elif resource == "documents" and 0 < documents_upload_quota.limit <= documents_upload_quota.size: elif resource == "documents" and 0 < documents_upload_quota.limit <= documents_upload_quota.size:
# The api of file upload is used in the multiple places, so we need to check the source of the request from datasets # The api of file upload is used in the multiple places, so we need to check the source of the request from datasets
source = request.args.get("source") source = request.args.get("source")
if source == "datasets": if source == "datasets":
abort(403, error_msg) abort(403, "The number of documents has reached the limit of your subscription.")
else: else:
return view(*args, **kwargs) return view(*args, **kwargs)
elif resource == "workspace_custom" and not features.can_replace_logo: elif resource == "workspace_custom" and not features.can_replace_logo:
abort(403, error_msg) abort(403, "The workspace custom feature has reached the limit of your subscription.")
elif resource == "annotation" and 0 < annotation_quota_limit.limit < annotation_quota_limit.size: elif resource == "annotation" and 0 < annotation_quota_limit.limit < annotation_quota_limit.size:
abort(403, error_msg) abort(403, "The annotation quota has reached the limit of your subscription.")
else: else:
return view(*args, **kwargs) return view(*args, **kwargs)
@ -86,10 +84,7 @@ def cloud_edition_billing_resource_check(
return interceptor return interceptor
def cloud_edition_billing_knowledge_limit_check( def cloud_edition_billing_knowledge_limit_check(resource: str):
resource: str,
error_msg: str = "To unlock this feature and elevate your Dify experience, please upgrade to a paid plan.",
):
def interceptor(view): def interceptor(view):
@wraps(view) @wraps(view)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
@ -97,7 +92,10 @@ def cloud_edition_billing_knowledge_limit_check(
if features.billing.enabled: if features.billing.enabled:
if resource == "add_segment": if resource == "add_segment":
if features.billing.subscription.plan == "sandbox": if features.billing.subscription.plan == "sandbox":
abort(403, error_msg) abort(
403,
"To unlock this feature and elevate your Dify experience, please upgrade to a paid plan.",
)
else: else:
return view(*args, **kwargs) return view(*args, **kwargs)

@ -36,6 +36,10 @@ class SegmentApi(DatasetApiResource):
document = DocumentService.get_document(dataset.id, document_id) document = DocumentService.get_document(dataset.id, document_id)
if not document: if not document:
raise NotFound("Document not found.") raise NotFound("Document not found.")
if document.indexing_status != "completed":
raise NotFound("Document is not completed.")
if not document.enabled:
raise NotFound("Document is disabled.")
# check embedding model setting # check embedding model setting
if dataset.indexing_technique == "high_quality": if dataset.indexing_technique == "high_quality":
try: try:
@ -63,7 +67,7 @@ class SegmentApi(DatasetApiResource):
segments = SegmentService.multi_create_segment(args["segments"], document, dataset) segments = SegmentService.multi_create_segment(args["segments"], document, dataset)
return {"data": marshal(segments, segment_fields), "doc_form": document.doc_form}, 200 return {"data": marshal(segments, segment_fields), "doc_form": document.doc_form}, 200
else: else:
return {"error": "Segemtns is required"}, 400 return {"error": "Segments is required"}, 400
def get(self, tenant_id, dataset_id, document_id): def get(self, tenant_id, dataset_id, document_id):
"""Create single segment.""" """Create single segment."""

@ -83,9 +83,7 @@ def validate_app_token(view: Optional[Callable] = None, *, fetch_user_arg: Optio
return decorator(view) return decorator(view)
def cloud_edition_billing_resource_check( def cloud_edition_billing_resource_check(resource: str, api_token_type: str):
resource: str, api_token_type: str, error_msg: str = "You have reached the limit of your subscription."
):
def interceptor(view): def interceptor(view):
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
api_token = validate_and_get_api_token(api_token_type) api_token = validate_and_get_api_token(api_token_type)
@ -98,13 +96,13 @@ def cloud_edition_billing_resource_check(
documents_upload_quota = features.documents_upload_quota documents_upload_quota = features.documents_upload_quota
if resource == "members" and 0 < members.limit <= members.size: if resource == "members" and 0 < members.limit <= members.size:
raise Forbidden(error_msg) raise Forbidden("The number of members has reached the limit of your subscription.")
elif resource == "apps" and 0 < apps.limit <= apps.size: elif resource == "apps" and 0 < apps.limit <= apps.size:
raise Forbidden(error_msg) raise Forbidden("The number of apps has reached the limit of your subscription.")
elif resource == "vector_space" and 0 < vector_space.limit <= vector_space.size: elif resource == "vector_space" and 0 < vector_space.limit <= vector_space.size:
raise Forbidden(error_msg) raise Forbidden("The capacity of the vector space has reached the limit of your subscription.")
elif resource == "documents" and 0 < documents_upload_quota.limit <= documents_upload_quota.size: elif resource == "documents" and 0 < documents_upload_quota.limit <= documents_upload_quota.size:
raise Forbidden(error_msg) raise Forbidden("The number of documents has reached the limit of your subscription.")
else: else:
return view(*args, **kwargs) return view(*args, **kwargs)
@ -115,11 +113,7 @@ def cloud_edition_billing_resource_check(
return interceptor return interceptor
def cloud_edition_billing_knowledge_limit_check( def cloud_edition_billing_knowledge_limit_check(resource: str, api_token_type: str):
resource: str,
api_token_type: str,
error_msg: str = "To unlock this feature and elevate your Dify experience, please upgrade to a paid plan.",
):
def interceptor(view): def interceptor(view):
@wraps(view) @wraps(view)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
@ -128,7 +122,9 @@ def cloud_edition_billing_knowledge_limit_check(
if features.billing.enabled: if features.billing.enabled:
if resource == "add_segment": if resource == "add_segment":
if features.billing.subscription.plan == "sandbox": if features.billing.subscription.plan == "sandbox":
raise Forbidden(error_msg) raise Forbidden(
"To unlock this feature and elevate your Dify experience, please upgrade to a paid plan."
)
else: else:
return view(*args, **kwargs) return view(*args, **kwargs)

@ -39,6 +39,7 @@ class AppSiteApi(WebApiResource):
"default_language": fields.String, "default_language": fields.String,
"prompt_public": fields.Boolean, "prompt_public": fields.Boolean,
"show_workflow_steps": fields.Boolean, "show_workflow_steps": fields.Boolean,
"use_icon_as_answer_icon": fields.Boolean,
} }
app_fields = { app_fields = {

@ -1 +1 @@
import core.moderation.base import core.moderation.base

@ -1,6 +1,7 @@
import json import json
import logging import logging
import uuid import uuid
from collections.abc import Mapping, Sequence
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional, Union, cast from typing import Optional, Union, cast
@ -45,22 +46,25 @@ from models.tools import ToolConversationVariables
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BaseAgentRunner(AppRunner): class BaseAgentRunner(AppRunner):
def __init__(self, tenant_id: str, def __init__(
application_generate_entity: AgentChatAppGenerateEntity, self,
conversation: Conversation, tenant_id: str,
app_config: AgentChatAppConfig, application_generate_entity: AgentChatAppGenerateEntity,
model_config: ModelConfigWithCredentialsEntity, conversation: Conversation,
config: AgentEntity, app_config: AgentChatAppConfig,
queue_manager: AppQueueManager, model_config: ModelConfigWithCredentialsEntity,
message: Message, config: AgentEntity,
user_id: str, queue_manager: AppQueueManager,
memory: Optional[TokenBufferMemory] = None, message: Message,
prompt_messages: Optional[list[PromptMessage]] = None, user_id: str,
variables_pool: Optional[ToolRuntimeVariablePool] = None, memory: Optional[TokenBufferMemory] = None,
db_variables: Optional[ToolConversationVariables] = None, prompt_messages: Optional[list[PromptMessage]] = None,
model_instance: ModelInstance = None variables_pool: Optional[ToolRuntimeVariablePool] = None,
) -> None: db_variables: Optional[ToolConversationVariables] = None,
model_instance: ModelInstance = None,
) -> None:
""" """
Agent runner Agent runner
:param tenant_id: tenant id :param tenant_id: tenant id
@ -88,9 +92,7 @@ class BaseAgentRunner(AppRunner):
self.message = message self.message = message
self.user_id = user_id self.user_id = user_id
self.memory = memory self.memory = memory
self.history_prompt_messages = self.organize_agent_history( self.history_prompt_messages = self.organize_agent_history(prompt_messages=prompt_messages or [])
prompt_messages=prompt_messages or []
)
self.variables_pool = variables_pool self.variables_pool = variables_pool
self.db_variables_pool = db_variables self.db_variables_pool = db_variables
self.model_instance = model_instance self.model_instance = model_instance
@ -111,12 +113,16 @@ class BaseAgentRunner(AppRunner):
retrieve_config=app_config.dataset.retrieve_config if app_config.dataset else None, retrieve_config=app_config.dataset.retrieve_config if app_config.dataset else None,
return_resource=app_config.additional_features.show_retrieve_source, return_resource=app_config.additional_features.show_retrieve_source,
invoke_from=application_generate_entity.invoke_from, invoke_from=application_generate_entity.invoke_from,
hit_callback=hit_callback hit_callback=hit_callback,
) )
# get how many agent thoughts have been created # get how many agent thoughts have been created
self.agent_thought_count = db.session.query(MessageAgentThought).filter( self.agent_thought_count = (
MessageAgentThought.message_id == self.message.id, db.session.query(MessageAgentThought)
).count() .filter(
MessageAgentThought.message_id == self.message.id,
)
.count()
)
db.session.close() db.session.close()
# check if model supports stream tool call # check if model supports stream tool call
@ -135,25 +141,26 @@ class BaseAgentRunner(AppRunner):
self.query = None self.query = None
self._current_thoughts: list[PromptMessage] = [] self._current_thoughts: list[PromptMessage] = []
def _repack_app_generate_entity(self, app_generate_entity: AgentChatAppGenerateEntity) \ def _repack_app_generate_entity(
-> AgentChatAppGenerateEntity: self, app_generate_entity: AgentChatAppGenerateEntity
) -> AgentChatAppGenerateEntity:
""" """
Repack app generate entity Repack app generate entity
""" """
if app_generate_entity.app_config.prompt_template.simple_prompt_template is None: if app_generate_entity.app_config.prompt_template.simple_prompt_template is None:
app_generate_entity.app_config.prompt_template.simple_prompt_template = '' app_generate_entity.app_config.prompt_template.simple_prompt_template = ""
return app_generate_entity return app_generate_entity
def _convert_tool_to_prompt_message_tool(self, tool: AgentToolEntity) -> tuple[PromptMessageTool, Tool]: def _convert_tool_to_prompt_message_tool(self, tool: AgentToolEntity) -> tuple[PromptMessageTool, Tool]:
""" """
convert tool to prompt message tool convert tool to prompt message tool
""" """
tool_entity = ToolManager.get_agent_tool_runtime( tool_entity = ToolManager.get_agent_tool_runtime(
tenant_id=self.tenant_id, tenant_id=self.tenant_id,
app_id=self.app_config.app_id, app_id=self.app_config.app_id,
agent_tool=tool, agent_tool=tool,
invoke_from=self.application_generate_entity.invoke_from invoke_from=self.application_generate_entity.invoke_from,
) )
tool_entity.load_variables(self.variables_pool) tool_entity.load_variables(self.variables_pool)
@ -164,7 +171,7 @@ class BaseAgentRunner(AppRunner):
"type": "object", "type": "object",
"properties": {}, "properties": {},
"required": [], "required": [],
} },
) )
parameters = tool_entity.get_all_runtime_parameters() parameters = tool_entity.get_all_runtime_parameters()
@ -177,19 +184,19 @@ class BaseAgentRunner(AppRunner):
if parameter.type == ToolParameter.ToolParameterType.SELECT: if parameter.type == ToolParameter.ToolParameterType.SELECT:
enum = [option.value for option in parameter.options] enum = [option.value for option in parameter.options]
message_tool.parameters['properties'][parameter.name] = { message_tool.parameters["properties"][parameter.name] = {
"type": parameter_type, "type": parameter_type,
"description": parameter.llm_description or '', "description": parameter.llm_description or "",
} }
if len(enum) > 0: if len(enum) > 0:
message_tool.parameters['properties'][parameter.name]['enum'] = enum message_tool.parameters["properties"][parameter.name]["enum"] = enum
if parameter.required: if parameter.required:
message_tool.parameters['required'].append(parameter.name) message_tool.parameters["required"].append(parameter.name)
return message_tool, tool_entity return message_tool, tool_entity
def _convert_dataset_retriever_tool_to_prompt_message_tool(self, tool: DatasetRetrieverTool) -> PromptMessageTool: def _convert_dataset_retriever_tool_to_prompt_message_tool(self, tool: DatasetRetrieverTool) -> PromptMessageTool:
""" """
convert dataset retriever tool to prompt message tool convert dataset retriever tool to prompt message tool
@ -201,24 +208,24 @@ class BaseAgentRunner(AppRunner):
"type": "object", "type": "object",
"properties": {}, "properties": {},
"required": [], "required": [],
} },
) )
for parameter in tool.get_runtime_parameters(): for parameter in tool.get_runtime_parameters():
parameter_type = 'string' parameter_type = "string"
prompt_tool.parameters['properties'][parameter.name] = { prompt_tool.parameters["properties"][parameter.name] = {
"type": parameter_type, "type": parameter_type,
"description": parameter.llm_description or '', "description": parameter.llm_description or "",
} }
if parameter.required: if parameter.required:
if parameter.name not in prompt_tool.parameters['required']: if parameter.name not in prompt_tool.parameters["required"]:
prompt_tool.parameters['required'].append(parameter.name) prompt_tool.parameters["required"].append(parameter.name)
return prompt_tool return prompt_tool
def _init_prompt_tools(self) -> tuple[dict[str, Tool], list[PromptMessageTool]]: def _init_prompt_tools(self) -> tuple[Mapping[str, Tool], Sequence[PromptMessageTool]]:
""" """
Init tools Init tools
""" """
@ -261,51 +268,51 @@ class BaseAgentRunner(AppRunner):
enum = [] enum = []
if parameter.type == ToolParameter.ToolParameterType.SELECT: if parameter.type == ToolParameter.ToolParameterType.SELECT:
enum = [option.value for option in parameter.options] enum = [option.value for option in parameter.options]
prompt_tool.parameters['properties'][parameter.name] = { prompt_tool.parameters["properties"][parameter.name] = {
"type": parameter_type, "type": parameter_type,
"description": parameter.llm_description or '', "description": parameter.llm_description or "",
} }
if len(enum) > 0: if len(enum) > 0:
prompt_tool.parameters['properties'][parameter.name]['enum'] = enum prompt_tool.parameters["properties"][parameter.name]["enum"] = enum
if parameter.required: if parameter.required:
if parameter.name not in prompt_tool.parameters['required']: if parameter.name not in prompt_tool.parameters["required"]:
prompt_tool.parameters['required'].append(parameter.name) prompt_tool.parameters["required"].append(parameter.name)
return prompt_tool return prompt_tool
def create_agent_thought(self, message_id: str, message: str, def create_agent_thought(
tool_name: str, tool_input: str, messages_ids: list[str] self, message_id: str, message: str, tool_name: str, tool_input: str, messages_ids: list[str]
) -> MessageAgentThought: ) -> MessageAgentThought:
""" """
Create agent thought Create agent thought
""" """
thought = MessageAgentThought( thought = MessageAgentThought(
message_id=message_id, message_id=message_id,
message_chain_id=None, message_chain_id=None,
thought='', thought="",
tool=tool_name, tool=tool_name,
tool_labels_str='{}', tool_labels_str="{}",
tool_meta_str='{}', tool_meta_str="{}",
tool_input=tool_input, tool_input=tool_input,
message=message, message=message,
message_token=0, message_token=0,
message_unit_price=0, message_unit_price=0,
message_price_unit=0, message_price_unit=0,
message_files=json.dumps(messages_ids) if messages_ids else '', message_files=json.dumps(messages_ids) if messages_ids else "",
answer='', answer="",
observation='', observation="",
answer_token=0, answer_token=0,
answer_unit_price=0, answer_unit_price=0,
answer_price_unit=0, answer_price_unit=0,
tokens=0, tokens=0,
total_price=0, total_price=0,
position=self.agent_thought_count + 1, position=self.agent_thought_count + 1,
currency='USD', currency="USD",
latency=0, latency=0,
created_by_role='account', created_by_role="account",
created_by=self.user_id, created_by=self.user_id,
) )
@ -318,22 +325,22 @@ class BaseAgentRunner(AppRunner):
return thought return thought
def save_agent_thought(self, def save_agent_thought(
agent_thought: MessageAgentThought, self,
tool_name: str, agent_thought: MessageAgentThought,
tool_input: Union[str, dict], tool_name: str,
thought: str, tool_input: Union[str, dict],
observation: Union[str, dict], thought: str,
tool_invoke_meta: Union[str, dict], observation: Union[str, dict],
answer: str, tool_invoke_meta: Union[str, dict],
messages_ids: list[str], answer: str,
llm_usage: LLMUsage = None) -> MessageAgentThought: messages_ids: list[str],
llm_usage: LLMUsage = None,
) -> MessageAgentThought:
""" """
Save agent thought Save agent thought
""" """
agent_thought = db.session.query(MessageAgentThought).filter( agent_thought = db.session.query(MessageAgentThought).filter(MessageAgentThought.id == agent_thought.id).first()
MessageAgentThought.id == agent_thought.id
).first()
if thought is not None: if thought is not None:
agent_thought.thought = thought agent_thought.thought = thought
@ -356,7 +363,7 @@ class BaseAgentRunner(AppRunner):
observation = json.dumps(observation, ensure_ascii=False) observation = json.dumps(observation, ensure_ascii=False)
except Exception as e: except Exception as e:
observation = json.dumps(observation) observation = json.dumps(observation)
agent_thought.observation = observation agent_thought.observation = observation
if answer is not None: if answer is not None:
@ -364,7 +371,7 @@ class BaseAgentRunner(AppRunner):
if messages_ids is not None and len(messages_ids) > 0: if messages_ids is not None and len(messages_ids) > 0:
agent_thought.message_files = json.dumps(messages_ids) agent_thought.message_files = json.dumps(messages_ids)
if llm_usage: if llm_usage:
agent_thought.message_token = llm_usage.prompt_tokens agent_thought.message_token = llm_usage.prompt_tokens
agent_thought.message_price_unit = llm_usage.prompt_price_unit agent_thought.message_price_unit = llm_usage.prompt_price_unit
@ -377,7 +384,7 @@ class BaseAgentRunner(AppRunner):
# check if tool labels is not empty # check if tool labels is not empty
labels = agent_thought.tool_labels or {} labels = agent_thought.tool_labels or {}
tools = agent_thought.tool.split(';') if agent_thought.tool else [] tools = agent_thought.tool.split(";") if agent_thought.tool else []
for tool in tools: for tool in tools:
if not tool: if not tool:
continue continue
@ -386,7 +393,7 @@ class BaseAgentRunner(AppRunner):
if tool_label: if tool_label:
labels[tool] = tool_label.to_dict() labels[tool] = tool_label.to_dict()
else: else:
labels[tool] = {'en_US': tool, 'zh_Hans': tool} labels[tool] = {"en_US": tool, "zh_Hans": tool}
agent_thought.tool_labels_str = json.dumps(labels) agent_thought.tool_labels_str = json.dumps(labels)
@ -401,14 +408,18 @@ class BaseAgentRunner(AppRunner):
db.session.commit() db.session.commit()
db.session.close() db.session.close()
def update_db_variables(self, tool_variables: ToolRuntimeVariablePool, db_variables: ToolConversationVariables): def update_db_variables(self, tool_variables: ToolRuntimeVariablePool, db_variables: ToolConversationVariables):
""" """
convert tool variables to db variables convert tool variables to db variables
""" """
db_variables = db.session.query(ToolConversationVariables).filter( db_variables = (
ToolConversationVariables.conversation_id == self.message.conversation_id, db.session.query(ToolConversationVariables)
).first() .filter(
ToolConversationVariables.conversation_id == self.message.conversation_id,
)
.first()
)
db_variables.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) db_variables.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
db_variables.variables_str = json.dumps(jsonable_encoder(tool_variables.pool)) db_variables.variables_str = json.dumps(jsonable_encoder(tool_variables.pool))
@ -425,9 +436,14 @@ class BaseAgentRunner(AppRunner):
if isinstance(prompt_message, SystemPromptMessage): if isinstance(prompt_message, SystemPromptMessage):
result.append(prompt_message) result.append(prompt_message)
messages: list[Message] = db.session.query(Message).filter( messages: list[Message] = (
Message.conversation_id == self.message.conversation_id, db.session.query(Message)
).order_by(Message.created_at.asc()).all() .filter(
Message.conversation_id == self.message.conversation_id,
)
.order_by(Message.created_at.asc())
.all()
)
for message in messages: for message in messages:
if message.id == self.message.id: if message.id == self.message.id:
@ -439,13 +455,13 @@ class BaseAgentRunner(AppRunner):
for agent_thought in agent_thoughts: for agent_thought in agent_thoughts:
tools = agent_thought.tool tools = agent_thought.tool
if tools: if tools:
tools = tools.split(';') tools = tools.split(";")
tool_calls: list[AssistantPromptMessage.ToolCall] = [] tool_calls: list[AssistantPromptMessage.ToolCall] = []
tool_call_response: list[ToolPromptMessage] = [] tool_call_response: list[ToolPromptMessage] = []
try: try:
tool_inputs = json.loads(agent_thought.tool_input) tool_inputs = json.loads(agent_thought.tool_input)
except Exception as e: except Exception as e:
tool_inputs = { tool: {} for tool in tools } tool_inputs = {tool: {} for tool in tools}
try: try:
tool_responses = json.loads(agent_thought.observation) tool_responses = json.loads(agent_thought.observation)
except Exception as e: except Exception as e:
@ -454,27 +470,33 @@ class BaseAgentRunner(AppRunner):
for tool in tools: for tool in tools:
# generate a uuid for tool call # generate a uuid for tool call
tool_call_id = str(uuid.uuid4()) tool_call_id = str(uuid.uuid4())
tool_calls.append(AssistantPromptMessage.ToolCall( tool_calls.append(
id=tool_call_id, AssistantPromptMessage.ToolCall(
type='function', id=tool_call_id,
function=AssistantPromptMessage.ToolCall.ToolCallFunction( type="function",
function=AssistantPromptMessage.ToolCall.ToolCallFunction(
name=tool,
arguments=json.dumps(tool_inputs.get(tool, {})),
),
)
)
tool_call_response.append(
ToolPromptMessage(
content=tool_responses.get(tool, agent_thought.observation),
name=tool, name=tool,
arguments=json.dumps(tool_inputs.get(tool, {})), tool_call_id=tool_call_id,
) )
)) )
tool_call_response.append(ToolPromptMessage(
content=tool_responses.get(tool, agent_thought.observation), result.extend(
name=tool, [
tool_call_id=tool_call_id, AssistantPromptMessage(
)) content=agent_thought.thought,
tool_calls=tool_calls,
result.extend([ ),
AssistantPromptMessage( *tool_call_response,
content=agent_thought.thought, ]
tool_calls=tool_calls, )
),
*tool_call_response
])
if not tools: if not tools:
result.append(AssistantPromptMessage(content=agent_thought.thought)) result.append(AssistantPromptMessage(content=agent_thought.thought))
else: else:
@ -496,10 +518,7 @@ class BaseAgentRunner(AppRunner):
file_extra_config = FileUploadConfigManager.convert(message.app_model_config.to_dict()) file_extra_config = FileUploadConfigManager.convert(message.app_model_config.to_dict())
if file_extra_config: if file_extra_config:
file_objs = message_file_parser.transform_message_files( file_objs = message_file_parser.transform_message_files(files, file_extra_config)
files,
file_extra_config
)
else: else:
file_objs = [] file_objs = []

@ -25,17 +25,19 @@ from models.model import Message
class CotAgentRunner(BaseAgentRunner, ABC): class CotAgentRunner(BaseAgentRunner, ABC):
_is_first_iteration = True _is_first_iteration = True
_ignore_observation_providers = ['wenxin'] _ignore_observation_providers = ["wenxin"]
_historic_prompt_messages: list[PromptMessage] = None _historic_prompt_messages: list[PromptMessage] = None
_agent_scratchpad: list[AgentScratchpadUnit] = None _agent_scratchpad: list[AgentScratchpadUnit] = None
_instruction: str = None _instruction: str = None
_query: str = None _query: str = None
_prompt_messages_tools: list[PromptMessage] = None _prompt_messages_tools: list[PromptMessage] = None
def run(self, message: Message, def run(
query: str, self,
inputs: dict[str, str], message: Message,
) -> Union[Generator, LLMResult]: query: str,
inputs: dict[str, str],
) -> Union[Generator, LLMResult]:
""" """
Run Cot agent application Run Cot agent application
""" """
@ -46,17 +48,16 @@ class CotAgentRunner(BaseAgentRunner, ABC):
trace_manager = app_generate_entity.trace_manager trace_manager = app_generate_entity.trace_manager
# check model mode # check model mode
if 'Observation' not in app_generate_entity.model_conf.stop: if "Observation" not in app_generate_entity.model_conf.stop:
if app_generate_entity.model_conf.provider not in self._ignore_observation_providers: if app_generate_entity.model_conf.provider not in self._ignore_observation_providers:
app_generate_entity.model_conf.stop.append('Observation') app_generate_entity.model_conf.stop.append("Observation")
app_config = self.app_config app_config = self.app_config
# init instruction # init instruction
inputs = inputs or {} inputs = inputs or {}
instruction = app_config.prompt_template.simple_prompt_template instruction = app_config.prompt_template.simple_prompt_template
self._instruction = self._fill_in_inputs_from_external_data_tools( self._instruction = self._fill_in_inputs_from_external_data_tools(instruction, inputs)
instruction, inputs)
iteration_step = 1 iteration_step = 1
max_iteration_steps = min(app_config.agent.max_iteration, 5) + 1 max_iteration_steps = min(app_config.agent.max_iteration, 5) + 1
@ -65,16 +66,14 @@ class CotAgentRunner(BaseAgentRunner, ABC):
tool_instances, self._prompt_messages_tools = self._init_prompt_tools() tool_instances, self._prompt_messages_tools = self._init_prompt_tools()
function_call_state = True function_call_state = True
llm_usage = { llm_usage = {"usage": None}
'usage': None final_answer = ""
}
final_answer = ''
def increase_usage(final_llm_usage_dict: dict[str, LLMUsage], usage: LLMUsage): def increase_usage(final_llm_usage_dict: dict[str, LLMUsage], usage: LLMUsage):
if not final_llm_usage_dict['usage']: if not final_llm_usage_dict["usage"]:
final_llm_usage_dict['usage'] = usage final_llm_usage_dict["usage"] = usage
else: else:
llm_usage = final_llm_usage_dict['usage'] llm_usage = final_llm_usage_dict["usage"]
llm_usage.prompt_tokens += usage.prompt_tokens llm_usage.prompt_tokens += usage.prompt_tokens
llm_usage.completion_tokens += usage.completion_tokens llm_usage.completion_tokens += usage.completion_tokens
llm_usage.prompt_price += usage.prompt_price llm_usage.prompt_price += usage.prompt_price
@ -94,17 +93,13 @@ class CotAgentRunner(BaseAgentRunner, ABC):
message_file_ids = [] message_file_ids = []
agent_thought = self.create_agent_thought( agent_thought = self.create_agent_thought(
message_id=message.id, message_id=message.id, message="", tool_name="", tool_input="", messages_ids=message_file_ids
message='',
tool_name='',
tool_input='',
messages_ids=message_file_ids
) )
if iteration_step > 1: if iteration_step > 1:
self.queue_manager.publish(QueueAgentThoughtEvent( self.queue_manager.publish(
agent_thought_id=agent_thought.id QueueAgentThoughtEvent(agent_thought_id=agent_thought.id), PublishFrom.APPLICATION_MANAGER
), PublishFrom.APPLICATION_MANAGER) )
# recalc llm max tokens # recalc llm max tokens
prompt_messages = self._organize_prompt_messages() prompt_messages = self._organize_prompt_messages()
@ -125,21 +120,20 @@ class CotAgentRunner(BaseAgentRunner, ABC):
raise ValueError("failed to invoke llm") raise ValueError("failed to invoke llm")
usage_dict = {} usage_dict = {}
react_chunks = CotAgentOutputParser.handle_react_stream_output( react_chunks = CotAgentOutputParser.handle_react_stream_output(chunks, usage_dict)
chunks, usage_dict)
scratchpad = AgentScratchpadUnit( scratchpad = AgentScratchpadUnit(
agent_response='', agent_response="",
thought='', thought="",
action_str='', action_str="",
observation='', observation="",
action=None, action=None,
) )
# publish agent thought if it's first iteration # publish agent thought if it's first iteration
if iteration_step == 1: if iteration_step == 1:
self.queue_manager.publish(QueueAgentThoughtEvent( self.queue_manager.publish(
agent_thought_id=agent_thought.id QueueAgentThoughtEvent(agent_thought_id=agent_thought.id), PublishFrom.APPLICATION_MANAGER
), PublishFrom.APPLICATION_MANAGER) )
for chunk in react_chunks: for chunk in react_chunks:
if isinstance(chunk, AgentScratchpadUnit.Action): if isinstance(chunk, AgentScratchpadUnit.Action):
@ -154,61 +148,51 @@ class CotAgentRunner(BaseAgentRunner, ABC):
yield LLMResultChunk( yield LLMResultChunk(
model=self.model_config.model, model=self.model_config.model,
prompt_messages=prompt_messages, prompt_messages=prompt_messages,
system_fingerprint='', system_fingerprint="",
delta=LLMResultChunkDelta( delta=LLMResultChunkDelta(index=0, message=AssistantPromptMessage(content=chunk), usage=None),
index=0,
message=AssistantPromptMessage(
content=chunk
),
usage=None
)
) )
scratchpad.thought = scratchpad.thought.strip( scratchpad.thought = scratchpad.thought.strip() or "I am thinking about how to help you"
) or 'I am thinking about how to help you'
self._agent_scratchpad.append(scratchpad) self._agent_scratchpad.append(scratchpad)
# get llm usage # get llm usage
if 'usage' in usage_dict: if "usage" in usage_dict:
increase_usage(llm_usage, usage_dict['usage']) increase_usage(llm_usage, usage_dict["usage"])
else: else:
usage_dict['usage'] = LLMUsage.empty_usage() usage_dict["usage"] = LLMUsage.empty_usage()
self.save_agent_thought( self.save_agent_thought(
agent_thought=agent_thought, agent_thought=agent_thought,
tool_name=scratchpad.action.action_name if scratchpad.action else '', tool_name=scratchpad.action.action_name if scratchpad.action else "",
tool_input={ tool_input={scratchpad.action.action_name: scratchpad.action.action_input} if scratchpad.action else {},
scratchpad.action.action_name: scratchpad.action.action_input
} if scratchpad.action else {},
tool_invoke_meta={}, tool_invoke_meta={},
thought=scratchpad.thought, thought=scratchpad.thought,
observation='', observation="",
answer=scratchpad.agent_response, answer=scratchpad.agent_response,
messages_ids=[], messages_ids=[],
llm_usage=usage_dict['usage'] llm_usage=usage_dict["usage"],
) )
if not scratchpad.is_final(): if not scratchpad.is_final():
self.queue_manager.publish(QueueAgentThoughtEvent( self.queue_manager.publish(
agent_thought_id=agent_thought.id QueueAgentThoughtEvent(agent_thought_id=agent_thought.id), PublishFrom.APPLICATION_MANAGER
), PublishFrom.APPLICATION_MANAGER) )
if not scratchpad.action: if not scratchpad.action:
# failed to extract action, return final answer directly # failed to extract action, return final answer directly
final_answer = '' final_answer = ""
else: else:
if scratchpad.action.action_name.lower() == "final answer": if scratchpad.action.action_name.lower() == "final answer":
# action is final answer, return final answer directly # action is final answer, return final answer directly
try: try:
if isinstance(scratchpad.action.action_input, dict): if isinstance(scratchpad.action.action_input, dict):
final_answer = json.dumps( final_answer = json.dumps(scratchpad.action.action_input)
scratchpad.action.action_input)
elif isinstance(scratchpad.action.action_input, str): elif isinstance(scratchpad.action.action_input, str):
final_answer = scratchpad.action.action_input final_answer = scratchpad.action.action_input
else: else:
final_answer = f'{scratchpad.action.action_input}' final_answer = f"{scratchpad.action.action_input}"
except json.JSONDecodeError: except json.JSONDecodeError:
final_answer = f'{scratchpad.action.action_input}' final_answer = f"{scratchpad.action.action_input}"
else: else:
function_call_state = True function_call_state = True
# action is tool call, invoke tool # action is tool call, invoke tool
@ -224,21 +208,18 @@ class CotAgentRunner(BaseAgentRunner, ABC):
self.save_agent_thought( self.save_agent_thought(
agent_thought=agent_thought, agent_thought=agent_thought,
tool_name=scratchpad.action.action_name, tool_name=scratchpad.action.action_name,
tool_input={ tool_input={scratchpad.action.action_name: scratchpad.action.action_input},
scratchpad.action.action_name: scratchpad.action.action_input},
thought=scratchpad.thought, thought=scratchpad.thought,
observation={ observation={scratchpad.action.action_name: tool_invoke_response},
scratchpad.action.action_name: tool_invoke_response}, tool_invoke_meta={scratchpad.action.action_name: tool_invoke_meta.to_dict()},
tool_invoke_meta={
scratchpad.action.action_name: tool_invoke_meta.to_dict()},
answer=scratchpad.agent_response, answer=scratchpad.agent_response,
messages_ids=message_file_ids, messages_ids=message_file_ids,
llm_usage=usage_dict['usage'] llm_usage=usage_dict["usage"],
) )
self.queue_manager.publish(QueueAgentThoughtEvent( self.queue_manager.publish(
agent_thought_id=agent_thought.id QueueAgentThoughtEvent(agent_thought_id=agent_thought.id), PublishFrom.APPLICATION_MANAGER
), PublishFrom.APPLICATION_MANAGER) )
# update prompt tool message # update prompt tool message
for prompt_tool in self._prompt_messages_tools: for prompt_tool in self._prompt_messages_tools:
@ -250,44 +231,45 @@ class CotAgentRunner(BaseAgentRunner, ABC):
model=model_instance.model, model=model_instance.model,
prompt_messages=prompt_messages, prompt_messages=prompt_messages,
delta=LLMResultChunkDelta( delta=LLMResultChunkDelta(
index=0, index=0, message=AssistantPromptMessage(content=final_answer), usage=llm_usage["usage"]
message=AssistantPromptMessage(
content=final_answer
),
usage=llm_usage['usage']
), ),
system_fingerprint='' system_fingerprint="",
) )
# save agent thought # save agent thought
self.save_agent_thought( self.save_agent_thought(
agent_thought=agent_thought, agent_thought=agent_thought,
tool_name='', tool_name="",
tool_input={}, tool_input={},
tool_invoke_meta={}, tool_invoke_meta={},
thought=final_answer, thought=final_answer,
observation={}, observation={},
answer=final_answer, answer=final_answer,
messages_ids=[] messages_ids=[],
) )
self.update_db_variables(self.variables_pool, self.db_variables_pool) self.update_db_variables(self.variables_pool, self.db_variables_pool)
# publish end event # publish end event
self.queue_manager.publish(QueueMessageEndEvent(llm_result=LLMResult( self.queue_manager.publish(
model=model_instance.model, QueueMessageEndEvent(
prompt_messages=prompt_messages, llm_result=LLMResult(
message=AssistantPromptMessage( model=model_instance.model,
content=final_answer prompt_messages=prompt_messages,
message=AssistantPromptMessage(content=final_answer),
usage=llm_usage["usage"] if llm_usage["usage"] else LLMUsage.empty_usage(),
system_fingerprint="",
)
), ),
usage=llm_usage['usage'] if llm_usage['usage'] else LLMUsage.empty_usage(), PublishFrom.APPLICATION_MANAGER,
system_fingerprint='' )
)), PublishFrom.APPLICATION_MANAGER)
def _handle_invoke_action(
def _handle_invoke_action(self, action: AgentScratchpadUnit.Action, self,
tool_instances: dict[str, Tool], action: AgentScratchpadUnit.Action,
message_file_ids: list[str], tool_instances: dict[str, Tool],
trace_manager: Optional[TraceQueueManager] = None message_file_ids: list[str],
) -> tuple[str, ToolInvokeMeta]: trace_manager: Optional[TraceQueueManager] = None,
) -> tuple[str, ToolInvokeMeta]:
""" """
handle invoke action handle invoke action
:param action: action :param action: action
@ -326,13 +308,12 @@ class CotAgentRunner(BaseAgentRunner, ABC):
# publish files # publish files
for message_file_id, save_as in message_files: for message_file_id, save_as in message_files:
if save_as: if save_as:
self.variables_pool.set_file( self.variables_pool.set_file(tool_name=tool_call_name, value=message_file_id, name=save_as)
tool_name=tool_call_name, value=message_file_id, name=save_as)
# publish message file # publish message file
self.queue_manager.publish(QueueMessageFileEvent( self.queue_manager.publish(
message_file_id=message_file_id QueueMessageFileEvent(message_file_id=message_file_id), PublishFrom.APPLICATION_MANAGER
), PublishFrom.APPLICATION_MANAGER) )
# add message file ids # add message file ids
message_file_ids.append(message_file_id) message_file_ids.append(message_file_id)
@ -342,10 +323,7 @@ class CotAgentRunner(BaseAgentRunner, ABC):
""" """
convert dict to action convert dict to action
""" """
return AgentScratchpadUnit.Action( return AgentScratchpadUnit.Action(action_name=action["action"], action_input=action["action_input"])
action_name=action['action'],
action_input=action['action_input']
)
def _fill_in_inputs_from_external_data_tools(self, instruction: str, inputs: dict) -> str: def _fill_in_inputs_from_external_data_tools(self, instruction: str, inputs: dict) -> str:
""" """
@ -353,7 +331,7 @@ class CotAgentRunner(BaseAgentRunner, ABC):
""" """
for key, value in inputs.items(): for key, value in inputs.items():
try: try:
instruction = instruction.replace(f'{{{{{key}}}}}', str(value)) instruction = instruction.replace(f"{{{{{key}}}}}", str(value))
except Exception as e: except Exception as e:
continue continue
@ -370,14 +348,14 @@ class CotAgentRunner(BaseAgentRunner, ABC):
@abstractmethod @abstractmethod
def _organize_prompt_messages(self) -> list[PromptMessage]: def _organize_prompt_messages(self) -> list[PromptMessage]:
""" """
organize prompt messages organize prompt messages
""" """
def _format_assistant_message(self, agent_scratchpad: list[AgentScratchpadUnit]) -> str: def _format_assistant_message(self, agent_scratchpad: list[AgentScratchpadUnit]) -> str:
""" """
format assistant message format assistant message
""" """
message = '' message = ""
for scratchpad in agent_scratchpad: for scratchpad in agent_scratchpad:
if scratchpad.is_final(): if scratchpad.is_final():
message += f"Final Answer: {scratchpad.agent_response}" message += f"Final Answer: {scratchpad.agent_response}"
@ -390,9 +368,11 @@ class CotAgentRunner(BaseAgentRunner, ABC):
return message return message
def _organize_historic_prompt_messages(self, current_session_messages: list[PromptMessage] = None) -> list[PromptMessage]: def _organize_historic_prompt_messages(
self, current_session_messages: list[PromptMessage] = None
) -> list[PromptMessage]:
""" """
organize historic prompt messages organize historic prompt messages
""" """
result: list[PromptMessage] = [] result: list[PromptMessage] = []
scratchpads: list[AgentScratchpadUnit] = [] scratchpads: list[AgentScratchpadUnit] = []
@ -403,8 +383,8 @@ class CotAgentRunner(BaseAgentRunner, ABC):
if not current_scratchpad: if not current_scratchpad:
current_scratchpad = AgentScratchpadUnit( current_scratchpad = AgentScratchpadUnit(
agent_response=message.content, agent_response=message.content,
thought=message.content or 'I am thinking about how to help you', thought=message.content or "I am thinking about how to help you",
action_str='', action_str="",
action=None, action=None,
observation=None, observation=None,
) )
@ -413,12 +393,9 @@ class CotAgentRunner(BaseAgentRunner, ABC):
try: try:
current_scratchpad.action = AgentScratchpadUnit.Action( current_scratchpad.action = AgentScratchpadUnit.Action(
action_name=message.tool_calls[0].function.name, action_name=message.tool_calls[0].function.name,
action_input=json.loads( action_input=json.loads(message.tool_calls[0].function.arguments),
message.tool_calls[0].function.arguments)
)
current_scratchpad.action_str = json.dumps(
current_scratchpad.action.to_dict()
) )
current_scratchpad.action_str = json.dumps(current_scratchpad.action.to_dict())
except: except:
pass pass
elif isinstance(message, ToolPromptMessage): elif isinstance(message, ToolPromptMessage):
@ -426,23 +403,19 @@ class CotAgentRunner(BaseAgentRunner, ABC):
current_scratchpad.observation = message.content current_scratchpad.observation = message.content
elif isinstance(message, UserPromptMessage): elif isinstance(message, UserPromptMessage):
if scratchpads: if scratchpads:
result.append(AssistantPromptMessage( result.append(AssistantPromptMessage(content=self._format_assistant_message(scratchpads)))
content=self._format_assistant_message(scratchpads)
))
scratchpads = [] scratchpads = []
current_scratchpad = None current_scratchpad = None
result.append(message) result.append(message)
if scratchpads: if scratchpads:
result.append(AssistantPromptMessage( result.append(AssistantPromptMessage(content=self._format_assistant_message(scratchpads)))
content=self._format_assistant_message(scratchpads)
))
historic_prompts = AgentHistoryPromptTransform( historic_prompts = AgentHistoryPromptTransform(
model_config=self.model_config, model_config=self.model_config,
prompt_messages=current_session_messages or [], prompt_messages=current_session_messages or [],
history_messages=result, history_messages=result,
memory=self.memory memory=self.memory,
).get_prompt() ).get_prompt()
return historic_prompts return historic_prompts

@ -19,14 +19,15 @@ class CotChatAgentRunner(CotAgentRunner):
prompt_entity = self.app_config.agent.prompt prompt_entity = self.app_config.agent.prompt
first_prompt = prompt_entity.first_prompt first_prompt = prompt_entity.first_prompt
system_prompt = first_prompt \ system_prompt = (
.replace("{{instruction}}", self._instruction) \ first_prompt.replace("{{instruction}}", self._instruction)
.replace("{{tools}}", json.dumps(jsonable_encoder(self._prompt_messages_tools))) \ .replace("{{tools}}", json.dumps(jsonable_encoder(self._prompt_messages_tools)))
.replace("{{tool_names}}", ', '.join([tool.name for tool in self._prompt_messages_tools])) .replace("{{tool_names}}", ", ".join([tool.name for tool in self._prompt_messages_tools]))
)
return SystemPromptMessage(content=system_prompt) return SystemPromptMessage(content=system_prompt)
def _organize_user_query(self, query, prompt_messages: list[PromptMessage] = None) -> list[PromptMessage]: def _organize_user_query(self, query, prompt_messages: list[PromptMessage] = None) -> list[PromptMessage]:
""" """
Organize user query Organize user query
""" """
@ -43,7 +44,7 @@ class CotChatAgentRunner(CotAgentRunner):
def _organize_prompt_messages(self) -> list[PromptMessage]: def _organize_prompt_messages(self) -> list[PromptMessage]:
""" """
Organize Organize
""" """
# organize system prompt # organize system prompt
system_message = self._organize_system_prompt() system_message = self._organize_system_prompt()
@ -53,7 +54,7 @@ class CotChatAgentRunner(CotAgentRunner):
if not agent_scratchpad: if not agent_scratchpad:
assistant_messages = [] assistant_messages = []
else: else:
assistant_message = AssistantPromptMessage(content='') assistant_message = AssistantPromptMessage(content="")
for unit in agent_scratchpad: for unit in agent_scratchpad:
if unit.is_final(): if unit.is_final():
assistant_message.content += f"Final Answer: {unit.agent_response}" assistant_message.content += f"Final Answer: {unit.agent_response}"
@ -71,18 +72,15 @@ class CotChatAgentRunner(CotAgentRunner):
if assistant_messages: if assistant_messages:
# organize historic prompt messages # organize historic prompt messages
historic_messages = self._organize_historic_prompt_messages([ historic_messages = self._organize_historic_prompt_messages(
system_message, [system_message, *query_messages, *assistant_messages, UserPromptMessage(content="continue")]
*query_messages, )
*assistant_messages,
UserPromptMessage(content='continue')
])
messages = [ messages = [
system_message, system_message,
*historic_messages, *historic_messages,
*query_messages, *query_messages,
*assistant_messages, *assistant_messages,
UserPromptMessage(content='continue') UserPromptMessage(content="continue"),
] ]
else: else:
# organize historic prompt messages # organize historic prompt messages

@ -13,10 +13,12 @@ class CotCompletionAgentRunner(CotAgentRunner):
prompt_entity = self.app_config.agent.prompt prompt_entity = self.app_config.agent.prompt
first_prompt = prompt_entity.first_prompt first_prompt = prompt_entity.first_prompt
system_prompt = first_prompt.replace("{{instruction}}", self._instruction) \ system_prompt = (
.replace("{{tools}}", json.dumps(jsonable_encoder(self._prompt_messages_tools))) \ first_prompt.replace("{{instruction}}", self._instruction)
.replace("{{tool_names}}", ', '.join([tool.name for tool in self._prompt_messages_tools])) .replace("{{tools}}", json.dumps(jsonable_encoder(self._prompt_messages_tools)))
.replace("{{tool_names}}", ", ".join([tool.name for tool in self._prompt_messages_tools]))
)
return system_prompt return system_prompt
def _organize_historic_prompt(self, current_session_messages: list[PromptMessage] = None) -> str: def _organize_historic_prompt(self, current_session_messages: list[PromptMessage] = None) -> str:
@ -46,7 +48,7 @@ class CotCompletionAgentRunner(CotAgentRunner):
# organize current assistant messages # organize current assistant messages
agent_scratchpad = self._agent_scratchpad agent_scratchpad = self._agent_scratchpad
assistant_prompt = '' assistant_prompt = ""
for unit in agent_scratchpad: for unit in agent_scratchpad:
if unit.is_final(): if unit.is_final():
assistant_prompt += f"Final Answer: {unit.agent_response}" assistant_prompt += f"Final Answer: {unit.agent_response}"
@ -61,9 +63,10 @@ class CotCompletionAgentRunner(CotAgentRunner):
query_prompt = f"Question: {self._query}" query_prompt = f"Question: {self._query}"
# join all messages # join all messages
prompt = system_prompt \ prompt = (
.replace("{{historic_messages}}", historic_prompt) \ system_prompt.replace("{{historic_messages}}", historic_prompt)
.replace("{{agent_scratchpad}}", assistant_prompt) \ .replace("{{agent_scratchpad}}", assistant_prompt)
.replace("{{query}}", query_prompt) .replace("{{query}}", query_prompt)
)
return [UserPromptMessage(content=prompt)] return [UserPromptMessage(content=prompt)]

@ -8,6 +8,7 @@ class AgentToolEntity(BaseModel):
""" """
Agent Tool Entity. Agent Tool Entity.
""" """
provider_type: Literal["builtin", "api", "workflow"] provider_type: Literal["builtin", "api", "workflow"]
provider_id: str provider_id: str
tool_name: str tool_name: str
@ -18,6 +19,7 @@ class AgentPromptEntity(BaseModel):
""" """
Agent Prompt Entity. Agent Prompt Entity.
""" """
first_prompt: str first_prompt: str
next_iteration: str next_iteration: str
@ -31,6 +33,7 @@ class AgentScratchpadUnit(BaseModel):
""" """
Action Entity. Action Entity.
""" """
action_name: str action_name: str
action_input: Union[dict, str] action_input: Union[dict, str]
@ -39,8 +42,8 @@ class AgentScratchpadUnit(BaseModel):
Convert to dictionary. Convert to dictionary.
""" """
return { return {
'action': self.action_name, "action": self.action_name,
'action_input': self.action_input, "action_input": self.action_input,
} }
agent_response: Optional[str] = None agent_response: Optional[str] = None
@ -54,10 +57,10 @@ class AgentScratchpadUnit(BaseModel):
Check if the scratchpad unit is final. Check if the scratchpad unit is final.
""" """
return self.action is None or ( return self.action is None or (
'final' in self.action.action_name.lower() and "final" in self.action.action_name.lower() and "answer" in self.action.action_name.lower()
'answer' in self.action.action_name.lower()
) )
class AgentEntity(BaseModel): class AgentEntity(BaseModel):
""" """
Agent Entity. Agent Entity.
@ -67,8 +70,9 @@ class AgentEntity(BaseModel):
""" """
Agent Strategy. Agent Strategy.
""" """
CHAIN_OF_THOUGHT = 'chain-of-thought'
FUNCTION_CALLING = 'function-calling' CHAIN_OF_THOUGHT = "chain-of-thought"
FUNCTION_CALLING = "function-calling"
provider: str provider: str
model: str model: str

@ -24,11 +24,9 @@ from models.model import Message
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class FunctionCallAgentRunner(BaseAgentRunner):
def run(self, class FunctionCallAgentRunner(BaseAgentRunner):
message: Message, query: str, **kwargs: Any def run(self, message: Message, query: str, **kwargs: Any) -> Generator[LLMResultChunk, None, None]:
) -> Generator[LLMResultChunk, None, None]:
""" """
Run FunctionCall agent application Run FunctionCall agent application
""" """
@ -45,19 +43,17 @@ class FunctionCallAgentRunner(BaseAgentRunner):
# continue to run until there is not any tool call # continue to run until there is not any tool call
function_call_state = True function_call_state = True
llm_usage = { llm_usage = {"usage": None}
'usage': None final_answer = ""
}
final_answer = ''
# get tracing instance # get tracing instance
trace_manager = app_generate_entity.trace_manager trace_manager = app_generate_entity.trace_manager
def increase_usage(final_llm_usage_dict: dict[str, LLMUsage], usage: LLMUsage): def increase_usage(final_llm_usage_dict: dict[str, LLMUsage], usage: LLMUsage):
if not final_llm_usage_dict['usage']: if not final_llm_usage_dict["usage"]:
final_llm_usage_dict['usage'] = usage final_llm_usage_dict["usage"] = usage
else: else:
llm_usage = final_llm_usage_dict['usage'] llm_usage = final_llm_usage_dict["usage"]
llm_usage.prompt_tokens += usage.prompt_tokens llm_usage.prompt_tokens += usage.prompt_tokens
llm_usage.completion_tokens += usage.completion_tokens llm_usage.completion_tokens += usage.completion_tokens
llm_usage.prompt_price += usage.prompt_price llm_usage.prompt_price += usage.prompt_price
@ -75,11 +71,7 @@ class FunctionCallAgentRunner(BaseAgentRunner):
message_file_ids = [] message_file_ids = []
agent_thought = self.create_agent_thought( agent_thought = self.create_agent_thought(
message_id=message.id, message_id=message.id, message="", tool_name="", tool_input="", messages_ids=message_file_ids
message='',
tool_name='',
tool_input='',
messages_ids=message_file_ids
) )
# recalc llm max tokens # recalc llm max tokens
@ -99,11 +91,11 @@ class FunctionCallAgentRunner(BaseAgentRunner):
tool_calls: list[tuple[str, str, dict[str, Any]]] = [] tool_calls: list[tuple[str, str, dict[str, Any]]] = []
# save full response # save full response
response = '' response = ""
# save tool call names and inputs # save tool call names and inputs
tool_call_names = '' tool_call_names = ""
tool_call_inputs = '' tool_call_inputs = ""
current_llm_usage = None current_llm_usage = None
@ -111,24 +103,22 @@ class FunctionCallAgentRunner(BaseAgentRunner):
is_first_chunk = True is_first_chunk = True
for chunk in chunks: for chunk in chunks:
if is_first_chunk: if is_first_chunk:
self.queue_manager.publish(QueueAgentThoughtEvent( self.queue_manager.publish(
agent_thought_id=agent_thought.id QueueAgentThoughtEvent(agent_thought_id=agent_thought.id), PublishFrom.APPLICATION_MANAGER
), PublishFrom.APPLICATION_MANAGER) )
is_first_chunk = False is_first_chunk = False
# check if there is any tool call # check if there is any tool call
if self.check_tool_calls(chunk): if self.check_tool_calls(chunk):
function_call_state = True function_call_state = True
tool_calls.extend(self.extract_tool_calls(chunk)) tool_calls.extend(self.extract_tool_calls(chunk))
tool_call_names = ';'.join([tool_call[1] for tool_call in tool_calls]) tool_call_names = ";".join([tool_call[1] for tool_call in tool_calls])
try: try:
tool_call_inputs = json.dumps({ tool_call_inputs = json.dumps(
tool_call[1]: tool_call[2] for tool_call in tool_calls {tool_call[1]: tool_call[2] for tool_call in tool_calls}, ensure_ascii=False
}, ensure_ascii=False) )
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
# ensure ascii to avoid encoding error # ensure ascii to avoid encoding error
tool_call_inputs = json.dumps({ tool_call_inputs = json.dumps({tool_call[1]: tool_call[2] for tool_call in tool_calls})
tool_call[1]: tool_call[2] for tool_call in tool_calls
})
if chunk.delta.message and chunk.delta.message.content: if chunk.delta.message and chunk.delta.message.content:
if isinstance(chunk.delta.message.content, list): if isinstance(chunk.delta.message.content, list):
@ -148,16 +138,14 @@ class FunctionCallAgentRunner(BaseAgentRunner):
if self.check_blocking_tool_calls(result): if self.check_blocking_tool_calls(result):
function_call_state = True function_call_state = True
tool_calls.extend(self.extract_blocking_tool_calls(result)) tool_calls.extend(self.extract_blocking_tool_calls(result))
tool_call_names = ';'.join([tool_call[1] for tool_call in tool_calls]) tool_call_names = ";".join([tool_call[1] for tool_call in tool_calls])
try: try:
tool_call_inputs = json.dumps({ tool_call_inputs = json.dumps(
tool_call[1]: tool_call[2] for tool_call in tool_calls {tool_call[1]: tool_call[2] for tool_call in tool_calls}, ensure_ascii=False
}, ensure_ascii=False) )
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
# ensure ascii to avoid encoding error # ensure ascii to avoid encoding error
tool_call_inputs = json.dumps({ tool_call_inputs = json.dumps({tool_call[1]: tool_call[2] for tool_call in tool_calls})
tool_call[1]: tool_call[2] for tool_call in tool_calls
})
if result.usage: if result.usage:
increase_usage(llm_usage, result.usage) increase_usage(llm_usage, result.usage)
@ -171,12 +159,12 @@ class FunctionCallAgentRunner(BaseAgentRunner):
response += result.message.content response += result.message.content
if not result.message.content: if not result.message.content:
result.message.content = '' result.message.content = ""
self.queue_manager.publish(
QueueAgentThoughtEvent(agent_thought_id=agent_thought.id), PublishFrom.APPLICATION_MANAGER
)
self.queue_manager.publish(QueueAgentThoughtEvent(
agent_thought_id=agent_thought.id
), PublishFrom.APPLICATION_MANAGER)
yield LLMResultChunk( yield LLMResultChunk(
model=model_instance.model, model=model_instance.model,
prompt_messages=result.prompt_messages, prompt_messages=result.prompt_messages,
@ -185,32 +173,29 @@ class FunctionCallAgentRunner(BaseAgentRunner):
index=0, index=0,
message=result.message, message=result.message,
usage=result.usage, usage=result.usage,
) ),
) )
assistant_message = AssistantPromptMessage( assistant_message = AssistantPromptMessage(content="", tool_calls=[])
content='',
tool_calls=[]
)
if tool_calls: if tool_calls:
assistant_message.tool_calls=[ assistant_message.tool_calls = [
AssistantPromptMessage.ToolCall( AssistantPromptMessage.ToolCall(
id=tool_call[0], id=tool_call[0],
type='function', type="function",
function=AssistantPromptMessage.ToolCall.ToolCallFunction( function=AssistantPromptMessage.ToolCall.ToolCallFunction(
name=tool_call[1], name=tool_call[1], arguments=json.dumps(tool_call[2], ensure_ascii=False)
arguments=json.dumps(tool_call[2], ensure_ascii=False) ),
) )
) for tool_call in tool_calls for tool_call in tool_calls
] ]
else: else:
assistant_message.content = response assistant_message.content = response
self._current_thoughts.append(assistant_message) self._current_thoughts.append(assistant_message)
# save thought # save thought
self.save_agent_thought( self.save_agent_thought(
agent_thought=agent_thought, agent_thought=agent_thought,
tool_name=tool_call_names, tool_name=tool_call_names,
tool_input=tool_call_inputs, tool_input=tool_call_inputs,
thought=response, thought=response,
@ -218,13 +203,13 @@ class FunctionCallAgentRunner(BaseAgentRunner):
observation=None, observation=None,
answer=response, answer=response,
messages_ids=[], messages_ids=[],
llm_usage=current_llm_usage llm_usage=current_llm_usage,
) )
self.queue_manager.publish(QueueAgentThoughtEvent( self.queue_manager.publish(
agent_thought_id=agent_thought.id QueueAgentThoughtEvent(agent_thought_id=agent_thought.id), PublishFrom.APPLICATION_MANAGER
), PublishFrom.APPLICATION_MANAGER) )
final_answer += response + '\n' final_answer += response + "\n"
# call tools # call tools
tool_responses = [] tool_responses = []
@ -235,7 +220,7 @@ class FunctionCallAgentRunner(BaseAgentRunner):
"tool_call_id": tool_call_id, "tool_call_id": tool_call_id,
"tool_call_name": tool_call_name, "tool_call_name": tool_call_name,
"tool_response": f"there is not a tool named {tool_call_name}", "tool_response": f"there is not a tool named {tool_call_name}",
"meta": ToolInvokeMeta.error_instance(f"there is not a tool named {tool_call_name}").to_dict() "meta": ToolInvokeMeta.error_instance(f"there is not a tool named {tool_call_name}").to_dict(),
} }
else: else:
# invoke tool # invoke tool
@ -255,50 +240,49 @@ class FunctionCallAgentRunner(BaseAgentRunner):
self.variables_pool.set_file(tool_name=tool_call_name, value=message_file_id, name=save_as) self.variables_pool.set_file(tool_name=tool_call_name, value=message_file_id, name=save_as)
# publish message file # publish message file
self.queue_manager.publish(QueueMessageFileEvent( self.queue_manager.publish(
message_file_id=message_file_id QueueMessageFileEvent(message_file_id=message_file_id), PublishFrom.APPLICATION_MANAGER
), PublishFrom.APPLICATION_MANAGER) )
# add message file ids # add message file ids
message_file_ids.append(message_file_id) message_file_ids.append(message_file_id)
tool_response = { tool_response = {
"tool_call_id": tool_call_id, "tool_call_id": tool_call_id,
"tool_call_name": tool_call_name, "tool_call_name": tool_call_name,
"tool_response": tool_invoke_response, "tool_response": tool_invoke_response,
"meta": tool_invoke_meta.to_dict() "meta": tool_invoke_meta.to_dict(),
} }
tool_responses.append(tool_response) tool_responses.append(tool_response)
if tool_response['tool_response'] is not None: if tool_response["tool_response"] is not None:
self._current_thoughts.append( self._current_thoughts.append(
ToolPromptMessage( ToolPromptMessage(
content=tool_response['tool_response'], content=tool_response["tool_response"],
tool_call_id=tool_call_id, tool_call_id=tool_call_id,
name=tool_call_name, name=tool_call_name,
) )
) )
if len(tool_responses) > 0: if len(tool_responses) > 0:
# save agent thought # save agent thought
self.save_agent_thought( self.save_agent_thought(
agent_thought=agent_thought, agent_thought=agent_thought,
tool_name=None, tool_name=None,
tool_input=None, tool_input=None,
thought=None, thought=None,
tool_invoke_meta={ tool_invoke_meta={
tool_response['tool_call_name']: tool_response['meta'] tool_response["tool_call_name"]: tool_response["meta"] for tool_response in tool_responses
for tool_response in tool_responses
}, },
observation={ observation={
tool_response['tool_call_name']: tool_response['tool_response'] tool_response["tool_call_name"]: tool_response["tool_response"]
for tool_response in tool_responses for tool_response in tool_responses
}, },
answer=None, answer=None,
messages_ids=message_file_ids messages_ids=message_file_ids,
)
self.queue_manager.publish(
QueueAgentThoughtEvent(agent_thought_id=agent_thought.id), PublishFrom.APPLICATION_MANAGER
) )
self.queue_manager.publish(QueueAgentThoughtEvent(
agent_thought_id=agent_thought.id
), PublishFrom.APPLICATION_MANAGER)
# update prompt tool # update prompt tool
for prompt_tool in prompt_messages_tools: for prompt_tool in prompt_messages_tools:
@ -308,15 +292,18 @@ class FunctionCallAgentRunner(BaseAgentRunner):
self.update_db_variables(self.variables_pool, self.db_variables_pool) self.update_db_variables(self.variables_pool, self.db_variables_pool)
# publish end event # publish end event
self.queue_manager.publish(QueueMessageEndEvent(llm_result=LLMResult( self.queue_manager.publish(
model=model_instance.model, QueueMessageEndEvent(
prompt_messages=prompt_messages, llm_result=LLMResult(
message=AssistantPromptMessage( model=model_instance.model,
content=final_answer prompt_messages=prompt_messages,
message=AssistantPromptMessage(content=final_answer),
usage=llm_usage["usage"] if llm_usage["usage"] else LLMUsage.empty_usage(),
system_fingerprint="",
)
), ),
usage=llm_usage['usage'] if llm_usage['usage'] else LLMUsage.empty_usage(), PublishFrom.APPLICATION_MANAGER,
system_fingerprint='' )
)), PublishFrom.APPLICATION_MANAGER)
def check_tool_calls(self, llm_result_chunk: LLMResultChunk) -> bool: def check_tool_calls(self, llm_result_chunk: LLMResultChunk) -> bool:
""" """
@ -325,7 +312,7 @@ class FunctionCallAgentRunner(BaseAgentRunner):
if llm_result_chunk.delta.message.tool_calls: if llm_result_chunk.delta.message.tool_calls:
return True return True
return False return False
def check_blocking_tool_calls(self, llm_result: LLMResult) -> bool: def check_blocking_tool_calls(self, llm_result: LLMResult) -> bool:
""" """
Check if there is any blocking tool call in llm result Check if there is any blocking tool call in llm result
@ -334,7 +321,9 @@ class FunctionCallAgentRunner(BaseAgentRunner):
return True return True
return False return False
def extract_tool_calls(self, llm_result_chunk: LLMResultChunk) -> Union[None, list[tuple[str, str, dict[str, Any]]]]: def extract_tool_calls(
self, llm_result_chunk: LLMResultChunk
) -> Union[None, list[tuple[str, str, dict[str, Any]]]]:
""" """
Extract tool calls from llm result chunk Extract tool calls from llm result chunk
@ -344,17 +333,19 @@ class FunctionCallAgentRunner(BaseAgentRunner):
tool_calls = [] tool_calls = []
for prompt_message in llm_result_chunk.delta.message.tool_calls: for prompt_message in llm_result_chunk.delta.message.tool_calls:
args = {} args = {}
if prompt_message.function.arguments != '': if prompt_message.function.arguments != "":
args = json.loads(prompt_message.function.arguments) args = json.loads(prompt_message.function.arguments)
tool_calls.append(( tool_calls.append(
prompt_message.id, (
prompt_message.function.name, prompt_message.id,
args, prompt_message.function.name,
)) args,
)
)
return tool_calls return tool_calls
def extract_blocking_tool_calls(self, llm_result: LLMResult) -> Union[None, list[tuple[str, str, dict[str, Any]]]]: def extract_blocking_tool_calls(self, llm_result: LLMResult) -> Union[None, list[tuple[str, str, dict[str, Any]]]]:
""" """
Extract blocking tool calls from llm result Extract blocking tool calls from llm result
@ -365,18 +356,22 @@ class FunctionCallAgentRunner(BaseAgentRunner):
tool_calls = [] tool_calls = []
for prompt_message in llm_result.message.tool_calls: for prompt_message in llm_result.message.tool_calls:
args = {} args = {}
if prompt_message.function.arguments != '': if prompt_message.function.arguments != "":
args = json.loads(prompt_message.function.arguments) args = json.loads(prompt_message.function.arguments)
tool_calls.append(( tool_calls.append(
prompt_message.id, (
prompt_message.function.name, prompt_message.id,
args, prompt_message.function.name,
)) args,
)
)
return tool_calls return tool_calls
def _init_system_message(self, prompt_template: str, prompt_messages: list[PromptMessage] = None) -> list[PromptMessage]: def _init_system_message(
self, prompt_template: str, prompt_messages: list[PromptMessage] = None
) -> list[PromptMessage]:
""" """
Initialize system message Initialize system message
""" """
@ -384,13 +379,13 @@ class FunctionCallAgentRunner(BaseAgentRunner):
return [ return [
SystemPromptMessage(content=prompt_template), SystemPromptMessage(content=prompt_template),
] ]
if prompt_messages and not isinstance(prompt_messages[0], SystemPromptMessage) and prompt_template: if prompt_messages and not isinstance(prompt_messages[0], SystemPromptMessage) and prompt_template:
prompt_messages.insert(0, SystemPromptMessage(content=prompt_template)) prompt_messages.insert(0, SystemPromptMessage(content=prompt_template))
return prompt_messages return prompt_messages
def _organize_user_query(self, query, prompt_messages: list[PromptMessage] = None) -> list[PromptMessage]: def _organize_user_query(self, query, prompt_messages: list[PromptMessage] = None) -> list[PromptMessage]:
""" """
Organize user query Organize user query
""" """
@ -404,7 +399,7 @@ class FunctionCallAgentRunner(BaseAgentRunner):
prompt_messages.append(UserPromptMessage(content=query)) prompt_messages.append(UserPromptMessage(content=query))
return prompt_messages return prompt_messages
def _clear_user_prompt_image_messages(self, prompt_messages: list[PromptMessage]) -> list[PromptMessage]: def _clear_user_prompt_image_messages(self, prompt_messages: list[PromptMessage]) -> list[PromptMessage]:
""" """
As for now, gpt supports both fc and vision at the first iteration. As for now, gpt supports both fc and vision at the first iteration.
@ -415,17 +410,21 @@ class FunctionCallAgentRunner(BaseAgentRunner):
for prompt_message in prompt_messages: for prompt_message in prompt_messages:
if isinstance(prompt_message, UserPromptMessage): if isinstance(prompt_message, UserPromptMessage):
if isinstance(prompt_message.content, list): if isinstance(prompt_message.content, list):
prompt_message.content = '\n'.join([ prompt_message.content = "\n".join(
content.data if content.type == PromptMessageContentType.TEXT else [
'[image]' if content.type == PromptMessageContentType.IMAGE else content.data
'[file]' if content.type == PromptMessageContentType.TEXT
for content in prompt_message.content else "[image]"
]) if content.type == PromptMessageContentType.IMAGE
else "[file]"
for content in prompt_message.content
]
)
return prompt_messages return prompt_messages
def _organize_prompt_messages(self): def _organize_prompt_messages(self):
prompt_template = self.app_config.prompt_template.simple_prompt_template or '' prompt_template = self.app_config.prompt_template.simple_prompt_template or ""
self.history_prompt_messages = self._init_system_message(prompt_template, self.history_prompt_messages) self.history_prompt_messages = self._init_system_message(prompt_template, self.history_prompt_messages)
query_prompt_messages = self._organize_user_query(self.query, []) query_prompt_messages = self._organize_user_query(self.query, [])
@ -433,14 +432,10 @@ class FunctionCallAgentRunner(BaseAgentRunner):
model_config=self.model_config, model_config=self.model_config,
prompt_messages=[*query_prompt_messages, *self._current_thoughts], prompt_messages=[*query_prompt_messages, *self._current_thoughts],
history_messages=self.history_prompt_messages, history_messages=self.history_prompt_messages,
memory=self.memory memory=self.memory,
).get_prompt() ).get_prompt()
prompt_messages = [ prompt_messages = [*self.history_prompt_messages, *query_prompt_messages, *self._current_thoughts]
*self.history_prompt_messages,
*query_prompt_messages,
*self._current_thoughts
]
if len(self._current_thoughts) != 0: if len(self._current_thoughts) != 0:
# clear messages after the first iteration # clear messages after the first iteration
prompt_messages = self._clear_user_prompt_image_messages(prompt_messages) prompt_messages = self._clear_user_prompt_image_messages(prompt_messages)

@ -9,8 +9,9 @@ from core.model_runtime.entities.llm_entities import LLMResultChunk
class CotAgentOutputParser: class CotAgentOutputParser:
@classmethod @classmethod
def handle_react_stream_output(cls, llm_response: Generator[LLMResultChunk, None, None], usage_dict: dict) -> \ def handle_react_stream_output(
Generator[Union[str, AgentScratchpadUnit.Action], None, None]: cls, llm_response: Generator[LLMResultChunk, None, None], usage_dict: dict
) -> Generator[Union[str, AgentScratchpadUnit.Action], None, None]:
def parse_action(json_str): def parse_action(json_str):
try: try:
action = json.loads(json_str) action = json.loads(json_str)
@ -22,7 +23,7 @@ class CotAgentOutputParser:
action = action[0] action = action[0]
for key, value in action.items(): for key, value in action.items():
if 'input' in key.lower(): if "input" in key.lower():
action_input = value action_input = value
else: else:
action_name = value action_name = value
@ -33,37 +34,37 @@ class CotAgentOutputParser:
action_input=action_input, action_input=action_input,
) )
else: else:
return json_str or '' return json_str or ""
except: except:
return json_str or '' return json_str or ""
def extra_json_from_code_block(code_block) -> Generator[Union[dict, str], None, None]: def extra_json_from_code_block(code_block) -> Generator[Union[dict, str], None, None]:
code_blocks = re.findall(r'```(.*?)```', code_block, re.DOTALL) code_blocks = re.findall(r"```(.*?)```", code_block, re.DOTALL)
if not code_blocks: if not code_blocks:
return return
for block in code_blocks: for block in code_blocks:
json_text = re.sub(r'^[a-zA-Z]+\n', '', block.strip(), flags=re.MULTILINE) json_text = re.sub(r"^[a-zA-Z]+\n", "", block.strip(), flags=re.MULTILINE)
yield parse_action(json_text) yield parse_action(json_text)
code_block_cache = '' code_block_cache = ""
code_block_delimiter_count = 0 code_block_delimiter_count = 0
in_code_block = False in_code_block = False
json_cache = '' json_cache = ""
json_quote_count = 0 json_quote_count = 0
in_json = False in_json = False
got_json = False got_json = False
action_cache = '' action_cache = ""
action_str = 'action:' action_str = "action:"
action_idx = 0 action_idx = 0
thought_cache = '' thought_cache = ""
thought_str = 'thought:' thought_str = "thought:"
thought_idx = 0 thought_idx = 0
for response in llm_response: for response in llm_response:
if response.delta.usage: if response.delta.usage:
usage_dict['usage'] = response.delta.usage usage_dict["usage"] = response.delta.usage
response = response.delta.message.content response = response.delta.message.content
if not isinstance(response, str): if not isinstance(response, str):
continue continue
@ -72,24 +73,24 @@ class CotAgentOutputParser:
index = 0 index = 0
while index < len(response): while index < len(response):
steps = 1 steps = 1
delta = response[index:index+steps] delta = response[index : index + steps]
last_character = response[index-1] if index > 0 else '' last_character = response[index - 1] if index > 0 else ""
if delta == '`': if delta == "`":
code_block_cache += delta code_block_cache += delta
code_block_delimiter_count += 1 code_block_delimiter_count += 1
else: else:
if not in_code_block: if not in_code_block:
if code_block_delimiter_count > 0: if code_block_delimiter_count > 0:
yield code_block_cache yield code_block_cache
code_block_cache = '' code_block_cache = ""
else: else:
code_block_cache += delta code_block_cache += delta
code_block_delimiter_count = 0 code_block_delimiter_count = 0
if not in_code_block and not in_json: if not in_code_block and not in_json:
if delta.lower() == action_str[action_idx] and action_idx == 0: if delta.lower() == action_str[action_idx] and action_idx == 0:
if last_character not in ['\n', ' ', '']: if last_character not in ["\n", " ", ""]:
index += steps index += steps
yield delta yield delta
continue continue
@ -97,7 +98,7 @@ class CotAgentOutputParser:
action_cache += delta action_cache += delta
action_idx += 1 action_idx += 1
if action_idx == len(action_str): if action_idx == len(action_str):
action_cache = '' action_cache = ""
action_idx = 0 action_idx = 0
index += steps index += steps
continue continue
@ -105,18 +106,18 @@ class CotAgentOutputParser:
action_cache += delta action_cache += delta
action_idx += 1 action_idx += 1
if action_idx == len(action_str): if action_idx == len(action_str):
action_cache = '' action_cache = ""
action_idx = 0 action_idx = 0
index += steps index += steps
continue continue
else: else:
if action_cache: if action_cache:
yield action_cache yield action_cache
action_cache = '' action_cache = ""
action_idx = 0 action_idx = 0
if delta.lower() == thought_str[thought_idx] and thought_idx == 0: if delta.lower() == thought_str[thought_idx] and thought_idx == 0:
if last_character not in ['\n', ' ', '']: if last_character not in ["\n", " ", ""]:
index += steps index += steps
yield delta yield delta
continue continue
@ -124,7 +125,7 @@ class CotAgentOutputParser:
thought_cache += delta thought_cache += delta
thought_idx += 1 thought_idx += 1
if thought_idx == len(thought_str): if thought_idx == len(thought_str):
thought_cache = '' thought_cache = ""
thought_idx = 0 thought_idx = 0
index += steps index += steps
continue continue
@ -132,31 +133,31 @@ class CotAgentOutputParser:
thought_cache += delta thought_cache += delta
thought_idx += 1 thought_idx += 1
if thought_idx == len(thought_str): if thought_idx == len(thought_str):
thought_cache = '' thought_cache = ""
thought_idx = 0 thought_idx = 0
index += steps index += steps
continue continue
else: else:
if thought_cache: if thought_cache:
yield thought_cache yield thought_cache
thought_cache = '' thought_cache = ""
thought_idx = 0 thought_idx = 0
if code_block_delimiter_count == 3: if code_block_delimiter_count == 3:
if in_code_block: if in_code_block:
yield from extra_json_from_code_block(code_block_cache) yield from extra_json_from_code_block(code_block_cache)
code_block_cache = '' code_block_cache = ""
in_code_block = not in_code_block in_code_block = not in_code_block
code_block_delimiter_count = 0 code_block_delimiter_count = 0
if not in_code_block: if not in_code_block:
# handle single json # handle single json
if delta == '{': if delta == "{":
json_quote_count += 1 json_quote_count += 1
in_json = True in_json = True
json_cache += delta json_cache += delta
elif delta == '}': elif delta == "}":
json_cache += delta json_cache += delta
if json_quote_count > 0: if json_quote_count > 0:
json_quote_count -= 1 json_quote_count -= 1
@ -172,12 +173,12 @@ class CotAgentOutputParser:
if got_json: if got_json:
got_json = False got_json = False
yield parse_action(json_cache) yield parse_action(json_cache)
json_cache = '' json_cache = ""
json_quote_count = 0 json_quote_count = 0
in_json = False in_json = False
if not in_code_block and not in_json: if not in_code_block and not in_json:
yield delta.replace('`', '') yield delta.replace("`", "")
index += steps index += steps
@ -186,4 +187,3 @@ class CotAgentOutputParser:
if json_cache: if json_cache:
yield parse_action(json_cache) yield parse_action(json_cache)

@ -91,14 +91,14 @@ Begin! Reminder to ALWAYS respond with a valid json blob of a single action. Use
ENGLISH_REACT_CHAT_AGENT_SCRATCHPAD_TEMPLATES = "" ENGLISH_REACT_CHAT_AGENT_SCRATCHPAD_TEMPLATES = ""
REACT_PROMPT_TEMPLATES = { REACT_PROMPT_TEMPLATES = {
'english': { "english": {
'chat': { "chat": {
'prompt': ENGLISH_REACT_CHAT_PROMPT_TEMPLATES, "prompt": ENGLISH_REACT_CHAT_PROMPT_TEMPLATES,
'agent_scratchpad': ENGLISH_REACT_CHAT_AGENT_SCRATCHPAD_TEMPLATES "agent_scratchpad": ENGLISH_REACT_CHAT_AGENT_SCRATCHPAD_TEMPLATES,
},
"completion": {
"prompt": ENGLISH_REACT_COMPLETION_PROMPT_TEMPLATES,
"agent_scratchpad": ENGLISH_REACT_COMPLETION_AGENT_SCRATCHPAD_TEMPLATES,
}, },
'completion': {
'prompt': ENGLISH_REACT_COMPLETION_PROMPT_TEMPLATES,
'agent_scratchpad': ENGLISH_REACT_COMPLETION_AGENT_SCRATCHPAD_TEMPLATES
}
} }
} }

@ -26,34 +26,24 @@ class BaseAppConfigManager:
config_dict = dict(config_dict.items()) config_dict = dict(config_dict.items())
additional_features = AppAdditionalFeatures() additional_features = AppAdditionalFeatures()
additional_features.show_retrieve_source = RetrievalResourceConfigManager.convert( additional_features.show_retrieve_source = RetrievalResourceConfigManager.convert(config=config_dict)
config=config_dict
)
additional_features.file_upload = FileUploadConfigManager.convert( additional_features.file_upload = FileUploadConfigManager.convert(
config=config_dict, config=config_dict, is_vision=app_mode in [AppMode.CHAT, AppMode.COMPLETION, AppMode.AGENT_CHAT]
is_vision=app_mode in [AppMode.CHAT, AppMode.COMPLETION, AppMode.AGENT_CHAT]
) )
additional_features.opening_statement, additional_features.suggested_questions = \ additional_features.opening_statement, additional_features.suggested_questions = (
OpeningStatementConfigManager.convert( OpeningStatementConfigManager.convert(config=config_dict)
config=config_dict )
)
additional_features.suggested_questions_after_answer = SuggestedQuestionsAfterAnswerConfigManager.convert( additional_features.suggested_questions_after_answer = SuggestedQuestionsAfterAnswerConfigManager.convert(
config=config_dict config=config_dict
) )
additional_features.more_like_this = MoreLikeThisConfigManager.convert( additional_features.more_like_this = MoreLikeThisConfigManager.convert(config=config_dict)
config=config_dict
)
additional_features.speech_to_text = SpeechToTextConfigManager.convert( additional_features.speech_to_text = SpeechToTextConfigManager.convert(config=config_dict)
config=config_dict
)
additional_features.text_to_speech = TextToSpeechConfigManager.convert( additional_features.text_to_speech = TextToSpeechConfigManager.convert(config=config_dict)
config=config_dict
)
return additional_features return additional_features

@ -7,25 +7,24 @@ from core.moderation.factory import ModerationFactory
class SensitiveWordAvoidanceConfigManager: class SensitiveWordAvoidanceConfigManager:
@classmethod @classmethod
def convert(cls, config: dict) -> Optional[SensitiveWordAvoidanceEntity]: def convert(cls, config: dict) -> Optional[SensitiveWordAvoidanceEntity]:
sensitive_word_avoidance_dict = config.get('sensitive_word_avoidance') sensitive_word_avoidance_dict = config.get("sensitive_word_avoidance")
if not sensitive_word_avoidance_dict: if not sensitive_word_avoidance_dict:
return None return None
if sensitive_word_avoidance_dict.get('enabled'): if sensitive_word_avoidance_dict.get("enabled"):
return SensitiveWordAvoidanceEntity( return SensitiveWordAvoidanceEntity(
type=sensitive_word_avoidance_dict.get('type'), type=sensitive_word_avoidance_dict.get("type"),
config=sensitive_word_avoidance_dict.get('config'), config=sensitive_word_avoidance_dict.get("config"),
) )
else: else:
return None return None
@classmethod @classmethod
def validate_and_set_defaults(cls, tenant_id, config: dict, only_structure_validate: bool = False) \ def validate_and_set_defaults(
-> tuple[dict, list[str]]: cls, tenant_id, config: dict, only_structure_validate: bool = False
) -> tuple[dict, list[str]]:
if not config.get("sensitive_word_avoidance"): if not config.get("sensitive_word_avoidance"):
config["sensitive_word_avoidance"] = { config["sensitive_word_avoidance"] = {"enabled": False}
"enabled": False
}
if not isinstance(config["sensitive_word_avoidance"], dict): if not isinstance(config["sensitive_word_avoidance"], dict):
raise ValueError("sensitive_word_avoidance must be of dict type") raise ValueError("sensitive_word_avoidance must be of dict type")
@ -41,10 +40,6 @@ class SensitiveWordAvoidanceConfigManager:
typ = config["sensitive_word_avoidance"]["type"] typ = config["sensitive_word_avoidance"]["type"]
sensitive_word_avoidance_config = config["sensitive_word_avoidance"]["config"] sensitive_word_avoidance_config = config["sensitive_word_avoidance"]["config"]
ModerationFactory.validate_config( ModerationFactory.validate_config(name=typ, tenant_id=tenant_id, config=sensitive_word_avoidance_config)
name=typ,
tenant_id=tenant_id,
config=sensitive_word_avoidance_config
)
return config, ["sensitive_word_avoidance"] return config, ["sensitive_word_avoidance"]

@ -12,67 +12,70 @@ class AgentConfigManager:
:param config: model config args :param config: model config args
""" """
if 'agent_mode' in config and config['agent_mode'] \ if "agent_mode" in config and config["agent_mode"] and "enabled" in config["agent_mode"]:
and 'enabled' in config['agent_mode']: agent_dict = config.get("agent_mode", {})
agent_strategy = agent_dict.get("strategy", "cot")
agent_dict = config.get('agent_mode', {}) if agent_strategy == "function_call":
agent_strategy = agent_dict.get('strategy', 'cot')
if agent_strategy == 'function_call':
strategy = AgentEntity.Strategy.FUNCTION_CALLING strategy = AgentEntity.Strategy.FUNCTION_CALLING
elif agent_strategy == 'cot' or agent_strategy == 'react': elif agent_strategy == "cot" or agent_strategy == "react":
strategy = AgentEntity.Strategy.CHAIN_OF_THOUGHT strategy = AgentEntity.Strategy.CHAIN_OF_THOUGHT
else: else:
# old configs, try to detect default strategy # old configs, try to detect default strategy
if config['model']['provider'] == 'openai': if config["model"]["provider"] == "openai":
strategy = AgentEntity.Strategy.FUNCTION_CALLING strategy = AgentEntity.Strategy.FUNCTION_CALLING
else: else:
strategy = AgentEntity.Strategy.CHAIN_OF_THOUGHT strategy = AgentEntity.Strategy.CHAIN_OF_THOUGHT
agent_tools = [] agent_tools = []
for tool in agent_dict.get('tools', []): for tool in agent_dict.get("tools", []):
keys = tool.keys() keys = tool.keys()
if len(keys) >= 4: if len(keys) >= 4:
if "enabled" not in tool or not tool["enabled"]: if "enabled" not in tool or not tool["enabled"]:
continue continue
agent_tool_properties = { agent_tool_properties = {
'provider_type': tool['provider_type'], "provider_type": tool["provider_type"],
'provider_id': tool['provider_id'], "provider_id": tool["provider_id"],
'tool_name': tool['tool_name'], "tool_name": tool["tool_name"],
'tool_parameters': tool.get('tool_parameters', {}) "tool_parameters": tool.get("tool_parameters", {}),
} }
agent_tools.append(AgentToolEntity(**agent_tool_properties)) agent_tools.append(AgentToolEntity(**agent_tool_properties))
if 'strategy' in config['agent_mode'] and \ if "strategy" in config["agent_mode"] and config["agent_mode"]["strategy"] not in [
config['agent_mode']['strategy'] not in ['react_router', 'router']: "react_router",
agent_prompt = agent_dict.get('prompt', None) or {} "router",
]:
agent_prompt = agent_dict.get("prompt", None) or {}
# check model mode # check model mode
model_mode = config.get('model', {}).get('mode', 'completion') model_mode = config.get("model", {}).get("mode", "completion")
if model_mode == 'completion': if model_mode == "completion":
agent_prompt_entity = AgentPromptEntity( agent_prompt_entity = AgentPromptEntity(
first_prompt=agent_prompt.get('first_prompt', first_prompt=agent_prompt.get(
REACT_PROMPT_TEMPLATES['english']['completion']['prompt']), "first_prompt", REACT_PROMPT_TEMPLATES["english"]["completion"]["prompt"]
next_iteration=agent_prompt.get('next_iteration', ),
REACT_PROMPT_TEMPLATES['english']['completion'][ next_iteration=agent_prompt.get(
'agent_scratchpad']), "next_iteration", REACT_PROMPT_TEMPLATES["english"]["completion"]["agent_scratchpad"]
),
) )
else: else:
agent_prompt_entity = AgentPromptEntity( agent_prompt_entity = AgentPromptEntity(
first_prompt=agent_prompt.get('first_prompt', first_prompt=agent_prompt.get(
REACT_PROMPT_TEMPLATES['english']['chat']['prompt']), "first_prompt", REACT_PROMPT_TEMPLATES["english"]["chat"]["prompt"]
next_iteration=agent_prompt.get('next_iteration', ),
REACT_PROMPT_TEMPLATES['english']['chat']['agent_scratchpad']), next_iteration=agent_prompt.get(
"next_iteration", REACT_PROMPT_TEMPLATES["english"]["chat"]["agent_scratchpad"]
),
) )
return AgentEntity( return AgentEntity(
provider=config['model']['provider'], provider=config["model"]["provider"],
model=config['model']['name'], model=config["model"]["name"],
strategy=strategy, strategy=strategy,
prompt=agent_prompt_entity, prompt=agent_prompt_entity,
tools=agent_tools, tools=agent_tools,
max_iteration=agent_dict.get('max_iteration', 5) max_iteration=agent_dict.get("max_iteration", 5),
) )
return None return None

@ -15,39 +15,38 @@ class DatasetConfigManager:
:param config: model config args :param config: model config args
""" """
dataset_ids = [] dataset_ids = []
if 'datasets' in config.get('dataset_configs', {}): if "datasets" in config.get("dataset_configs", {}):
datasets = config.get('dataset_configs', {}).get('datasets', { datasets = config.get("dataset_configs", {}).get("datasets", {"strategy": "router", "datasets": []})
'strategy': 'router',
'datasets': []
})
for dataset in datasets.get('datasets', []): for dataset in datasets.get("datasets", []):
keys = list(dataset.keys()) keys = list(dataset.keys())
if len(keys) == 0 or keys[0] != 'dataset': if len(keys) == 0 or keys[0] != "dataset":
continue continue
dataset = dataset['dataset'] dataset = dataset["dataset"]
if 'enabled' not in dataset or not dataset['enabled']: if "enabled" not in dataset or not dataset["enabled"]:
continue continue
dataset_id = dataset.get('id', None) dataset_id = dataset.get("id", None)
if dataset_id: if dataset_id:
dataset_ids.append(dataset_id) dataset_ids.append(dataset_id)
if 'agent_mode' in config and config['agent_mode'] \ if (
and 'enabled' in config['agent_mode'] \ "agent_mode" in config
and config['agent_mode']['enabled']: and config["agent_mode"]
and "enabled" in config["agent_mode"]
and config["agent_mode"]["enabled"]
):
agent_dict = config.get("agent_mode", {})
agent_dict = config.get('agent_mode', {}) for tool in agent_dict.get("tools", []):
for tool in agent_dict.get('tools', []):
keys = tool.keys() keys = tool.keys()
if len(keys) == 1: if len(keys) == 1:
# old standard # old standard
key = list(tool.keys())[0] key = list(tool.keys())[0]
if key != 'dataset': if key != "dataset":
continue continue
tool_item = tool[key] tool_item = tool[key]
@ -55,30 +54,28 @@ class DatasetConfigManager:
if "enabled" not in tool_item or not tool_item["enabled"]: if "enabled" not in tool_item or not tool_item["enabled"]:
continue continue
dataset_id = tool_item['id'] dataset_id = tool_item["id"]
dataset_ids.append(dataset_id) dataset_ids.append(dataset_id)
if len(dataset_ids) == 0: if len(dataset_ids) == 0:
return None return None
# dataset configs # dataset configs
if 'dataset_configs' in config and config.get('dataset_configs'): if "dataset_configs" in config and config.get("dataset_configs"):
dataset_configs = config.get('dataset_configs') dataset_configs = config.get("dataset_configs")
else: else:
dataset_configs = { dataset_configs = {"retrieval_model": "multiple"}
'retrieval_model': 'multiple' query_variable = config.get("dataset_query_variable")
}
query_variable = config.get('dataset_query_variable')
if dataset_configs['retrieval_model'] == 'single': if dataset_configs["retrieval_model"] == "single":
return DatasetEntity( return DatasetEntity(
dataset_ids=dataset_ids, dataset_ids=dataset_ids,
retrieve_config=DatasetRetrieveConfigEntity( retrieve_config=DatasetRetrieveConfigEntity(
query_variable=query_variable, query_variable=query_variable,
retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.value_of( retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.value_of(
dataset_configs['retrieval_model'] dataset_configs["retrieval_model"]
) ),
) ),
) )
else: else:
return DatasetEntity( return DatasetEntity(
@ -86,15 +83,15 @@ class DatasetConfigManager:
retrieve_config=DatasetRetrieveConfigEntity( retrieve_config=DatasetRetrieveConfigEntity(
query_variable=query_variable, query_variable=query_variable,
retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.value_of( retrieve_strategy=DatasetRetrieveConfigEntity.RetrieveStrategy.value_of(
dataset_configs['retrieval_model'] dataset_configs["retrieval_model"]
), ),
top_k=dataset_configs.get('top_k', 4), top_k=dataset_configs.get("top_k", 4),
score_threshold=dataset_configs.get('score_threshold'), score_threshold=dataset_configs.get("score_threshold"),
reranking_model=dataset_configs.get('reranking_model'), reranking_model=dataset_configs.get("reranking_model"),
weights=dataset_configs.get('weights'), weights=dataset_configs.get("weights"),
reranking_enabled=dataset_configs.get('reranking_enabled', True), reranking_enabled=dataset_configs.get("reranking_enabled", True),
rerank_mode=dataset_configs.get('rerank_mode', 'reranking_model'), rerank_mode=dataset_configs.get("reranking_mode", "reranking_model"),
) ),
) )
@classmethod @classmethod
@ -111,13 +108,10 @@ class DatasetConfigManager:
# dataset_configs # dataset_configs
if not config.get("dataset_configs"): if not config.get("dataset_configs"):
config["dataset_configs"] = {'retrieval_model': 'single'} config["dataset_configs"] = {"retrieval_model": "single"}
if not config["dataset_configs"].get("datasets"): if not config["dataset_configs"].get("datasets"):
config["dataset_configs"]["datasets"] = { config["dataset_configs"]["datasets"] = {"strategy": "router", "datasets": []}
"strategy": "router",
"datasets": []
}
if not isinstance(config["dataset_configs"], dict): if not isinstance(config["dataset_configs"], dict):
raise ValueError("dataset_configs must be of object type") raise ValueError("dataset_configs must be of object type")
@ -125,8 +119,9 @@ class DatasetConfigManager:
if not isinstance(config["dataset_configs"], dict): if not isinstance(config["dataset_configs"], dict):
raise ValueError("dataset_configs must be of object type") raise ValueError("dataset_configs must be of object type")
need_manual_query_datasets = (config.get("dataset_configs") need_manual_query_datasets = config.get("dataset_configs") and config["dataset_configs"].get(
and config["dataset_configs"].get("datasets", {}).get("datasets")) "datasets", {}
).get("datasets")
if need_manual_query_datasets and app_mode == AppMode.COMPLETION: if need_manual_query_datasets and app_mode == AppMode.COMPLETION:
# Only check when mode is completion # Only check when mode is completion
@ -148,10 +143,7 @@ class DatasetConfigManager:
""" """
# Extract dataset config for legacy compatibility # Extract dataset config for legacy compatibility
if not config.get("agent_mode"): if not config.get("agent_mode"):
config["agent_mode"] = { config["agent_mode"] = {"enabled": False, "tools": []}
"enabled": False,
"tools": []
}
if not isinstance(config["agent_mode"], dict): if not isinstance(config["agent_mode"], dict):
raise ValueError("agent_mode must be of object type") raise ValueError("agent_mode must be of object type")
@ -188,7 +180,7 @@ class DatasetConfigManager:
if not isinstance(tool_item["enabled"], bool): if not isinstance(tool_item["enabled"], bool):
raise ValueError("enabled in agent_mode.tools must be of boolean type") raise ValueError("enabled in agent_mode.tools must be of boolean type")
if 'id' not in tool_item: if "id" not in tool_item:
raise ValueError("id is required in dataset") raise ValueError("id is required in dataset")
try: try:

@ -11,9 +11,7 @@ from core.provider_manager import ProviderManager
class ModelConfigConverter: class ModelConfigConverter:
@classmethod @classmethod
def convert(cls, app_config: EasyUIBasedAppConfig, def convert(cls, app_config: EasyUIBasedAppConfig, skip_check: bool = False) -> ModelConfigWithCredentialsEntity:
skip_check: bool = False) \
-> ModelConfigWithCredentialsEntity:
""" """
Convert app model config dict to entity. Convert app model config dict to entity.
:param app_config: app config :param app_config: app config
@ -25,9 +23,7 @@ class ModelConfigConverter:
provider_manager = ProviderManager() provider_manager = ProviderManager()
provider_model_bundle = provider_manager.get_provider_model_bundle( provider_model_bundle = provider_manager.get_provider_model_bundle(
tenant_id=app_config.tenant_id, tenant_id=app_config.tenant_id, provider=model_config.provider, model_type=ModelType.LLM
provider=model_config.provider,
model_type=ModelType.LLM
) )
provider_name = provider_model_bundle.configuration.provider.provider provider_name = provider_model_bundle.configuration.provider.provider
@ -38,8 +34,7 @@ class ModelConfigConverter:
# check model credentials # check model credentials
model_credentials = provider_model_bundle.configuration.get_current_credentials( model_credentials = provider_model_bundle.configuration.get_current_credentials(
model_type=ModelType.LLM, model_type=ModelType.LLM, model=model_config.model
model=model_config.model
) )
if model_credentials is None: if model_credentials is None:
@ -51,8 +46,7 @@ class ModelConfigConverter:
if not skip_check: if not skip_check:
# check model # check model
provider_model = provider_model_bundle.configuration.get_provider_model( provider_model = provider_model_bundle.configuration.get_provider_model(
model=model_config.model, model=model_config.model, model_type=ModelType.LLM
model_type=ModelType.LLM
) )
if provider_model is None: if provider_model is None:
@ -69,24 +63,18 @@ class ModelConfigConverter:
# model config # model config
completion_params = model_config.parameters completion_params = model_config.parameters
stop = [] stop = []
if 'stop' in completion_params: if "stop" in completion_params:
stop = completion_params['stop'] stop = completion_params["stop"]
del completion_params['stop'] del completion_params["stop"]
# get model mode # get model mode
model_mode = model_config.mode model_mode = model_config.mode
if not model_mode: if not model_mode:
mode_enum = model_type_instance.get_model_mode( mode_enum = model_type_instance.get_model_mode(model=model_config.model, credentials=model_credentials)
model=model_config.model,
credentials=model_credentials
)
model_mode = mode_enum.value model_mode = mode_enum.value
model_schema = model_type_instance.get_model_schema( model_schema = model_type_instance.get_model_schema(model_config.model, model_credentials)
model_config.model,
model_credentials
)
if not skip_check and not model_schema: if not skip_check and not model_schema:
raise ValueError(f"Model {model_name} not exist.") raise ValueError(f"Model {model_name} not exist.")

@ -13,23 +13,23 @@ class ModelConfigManager:
:param config: model config args :param config: model config args
""" """
# model config # model config
model_config = config.get('model') model_config = config.get("model")
if not model_config: if not model_config:
raise ValueError("model is required") raise ValueError("model is required")
completion_params = model_config.get('completion_params') completion_params = model_config.get("completion_params")
stop = [] stop = []
if 'stop' in completion_params: if "stop" in completion_params:
stop = completion_params['stop'] stop = completion_params["stop"]
del completion_params['stop'] del completion_params["stop"]
# get model mode # get model mode
model_mode = model_config.get('mode') model_mode = model_config.get("mode")
return ModelConfigEntity( return ModelConfigEntity(
provider=config['model']['provider'], provider=config["model"]["provider"],
model=config['model']['name'], model=config["model"]["name"],
mode=model_mode, mode=model_mode,
parameters=completion_params, parameters=completion_params,
stop=stop, stop=stop,
@ -43,7 +43,7 @@ class ModelConfigManager:
:param tenant_id: tenant id :param tenant_id: tenant id
:param config: app model config args :param config: app model config args
""" """
if 'model' not in config: if "model" not in config:
raise ValueError("model is required") raise ValueError("model is required")
if not isinstance(config["model"], dict): if not isinstance(config["model"], dict):
@ -52,17 +52,16 @@ class ModelConfigManager:
# model.provider # model.provider
provider_entities = model_provider_factory.get_providers() provider_entities = model_provider_factory.get_providers()
model_provider_names = [provider.provider for provider in provider_entities] model_provider_names = [provider.provider for provider in provider_entities]
if 'provider' not in config["model"] or config["model"]["provider"] not in model_provider_names: if "provider" not in config["model"] or config["model"]["provider"] not in model_provider_names:
raise ValueError(f"model.provider is required and must be in {str(model_provider_names)}") raise ValueError(f"model.provider is required and must be in {str(model_provider_names)}")
# model.name # model.name
if 'name' not in config["model"]: if "name" not in config["model"]:
raise ValueError("model.name is required") raise ValueError("model.name is required")
provider_manager = ProviderManager() provider_manager = ProviderManager()
models = provider_manager.get_configurations(tenant_id).get_models( models = provider_manager.get_configurations(tenant_id).get_models(
provider=config["model"]["provider"], provider=config["model"]["provider"], model_type=ModelType.LLM
model_type=ModelType.LLM
) )
if not models: if not models:
@ -80,12 +79,12 @@ class ModelConfigManager:
# model.mode # model.mode
if model_mode: if model_mode:
config['model']["mode"] = model_mode config["model"]["mode"] = model_mode
else: else:
config['model']["mode"] = "completion" config["model"]["mode"] = "completion"
# model.completion_params # model.completion_params
if 'completion_params' not in config["model"]: if "completion_params" not in config["model"]:
raise ValueError("model.completion_params is required") raise ValueError("model.completion_params is required")
config["model"]["completion_params"] = cls.validate_model_completion_params( config["model"]["completion_params"] = cls.validate_model_completion_params(
@ -101,7 +100,7 @@ class ModelConfigManager:
raise ValueError("model.completion_params must be of object type") raise ValueError("model.completion_params must be of object type")
# stop # stop
if 'stop' not in cp: if "stop" not in cp:
cp["stop"] = [] cp["stop"] = []
elif not isinstance(cp["stop"], list): elif not isinstance(cp["stop"], list):
raise ValueError("stop in model.completion_params must be of list type") raise ValueError("stop in model.completion_params must be of list type")

@ -14,39 +14,33 @@ class PromptTemplateConfigManager:
if not config.get("prompt_type"): if not config.get("prompt_type"):
raise ValueError("prompt_type is required") raise ValueError("prompt_type is required")
prompt_type = PromptTemplateEntity.PromptType.value_of(config['prompt_type']) prompt_type = PromptTemplateEntity.PromptType.value_of(config["prompt_type"])
if prompt_type == PromptTemplateEntity.PromptType.SIMPLE: if prompt_type == PromptTemplateEntity.PromptType.SIMPLE:
simple_prompt_template = config.get("pre_prompt", "") simple_prompt_template = config.get("pre_prompt", "")
return PromptTemplateEntity( return PromptTemplateEntity(prompt_type=prompt_type, simple_prompt_template=simple_prompt_template)
prompt_type=prompt_type,
simple_prompt_template=simple_prompt_template
)
else: else:
advanced_chat_prompt_template = None advanced_chat_prompt_template = None
chat_prompt_config = config.get("chat_prompt_config", {}) chat_prompt_config = config.get("chat_prompt_config", {})
if chat_prompt_config: if chat_prompt_config:
chat_prompt_messages = [] chat_prompt_messages = []
for message in chat_prompt_config.get("prompt", []): for message in chat_prompt_config.get("prompt", []):
chat_prompt_messages.append({ chat_prompt_messages.append(
"text": message["text"], {"text": message["text"], "role": PromptMessageRole.value_of(message["role"])}
"role": PromptMessageRole.value_of(message["role"]) )
})
advanced_chat_prompt_template = AdvancedChatPromptTemplateEntity( advanced_chat_prompt_template = AdvancedChatPromptTemplateEntity(messages=chat_prompt_messages)
messages=chat_prompt_messages
)
advanced_completion_prompt_template = None advanced_completion_prompt_template = None
completion_prompt_config = config.get("completion_prompt_config", {}) completion_prompt_config = config.get("completion_prompt_config", {})
if completion_prompt_config: if completion_prompt_config:
completion_prompt_template_params = { completion_prompt_template_params = {
'prompt': completion_prompt_config['prompt']['text'], "prompt": completion_prompt_config["prompt"]["text"],
} }
if 'conversation_histories_role' in completion_prompt_config: if "conversation_histories_role" in completion_prompt_config:
completion_prompt_template_params['role_prefix'] = { completion_prompt_template_params["role_prefix"] = {
'user': completion_prompt_config['conversation_histories_role']['user_prefix'], "user": completion_prompt_config["conversation_histories_role"]["user_prefix"],
'assistant': completion_prompt_config['conversation_histories_role']['assistant_prefix'] "assistant": completion_prompt_config["conversation_histories_role"]["assistant_prefix"],
} }
advanced_completion_prompt_template = AdvancedCompletionPromptTemplateEntity( advanced_completion_prompt_template = AdvancedCompletionPromptTemplateEntity(
@ -56,7 +50,7 @@ class PromptTemplateConfigManager:
return PromptTemplateEntity( return PromptTemplateEntity(
prompt_type=prompt_type, prompt_type=prompt_type,
advanced_chat_prompt_template=advanced_chat_prompt_template, advanced_chat_prompt_template=advanced_chat_prompt_template,
advanced_completion_prompt_template=advanced_completion_prompt_template advanced_completion_prompt_template=advanced_completion_prompt_template,
) )
@classmethod @classmethod
@ -72,7 +66,7 @@ class PromptTemplateConfigManager:
config["prompt_type"] = PromptTemplateEntity.PromptType.SIMPLE.value config["prompt_type"] = PromptTemplateEntity.PromptType.SIMPLE.value
prompt_type_vals = [typ.value for typ in PromptTemplateEntity.PromptType] prompt_type_vals = [typ.value for typ in PromptTemplateEntity.PromptType]
if config['prompt_type'] not in prompt_type_vals: if config["prompt_type"] not in prompt_type_vals:
raise ValueError(f"prompt_type must be in {prompt_type_vals}") raise ValueError(f"prompt_type must be in {prompt_type_vals}")
# chat_prompt_config # chat_prompt_config
@ -89,27 +83,28 @@ class PromptTemplateConfigManager:
if not isinstance(config["completion_prompt_config"], dict): if not isinstance(config["completion_prompt_config"], dict):
raise ValueError("completion_prompt_config must be of object type") raise ValueError("completion_prompt_config must be of object type")
if config['prompt_type'] == PromptTemplateEntity.PromptType.ADVANCED.value: if config["prompt_type"] == PromptTemplateEntity.PromptType.ADVANCED.value:
if not config['chat_prompt_config'] and not config['completion_prompt_config']: if not config["chat_prompt_config"] and not config["completion_prompt_config"]:
raise ValueError("chat_prompt_config or completion_prompt_config is required " raise ValueError(
"when prompt_type is advanced") "chat_prompt_config or completion_prompt_config is required " "when prompt_type is advanced"
)
model_mode_vals = [mode.value for mode in ModelMode] model_mode_vals = [mode.value for mode in ModelMode]
if config['model']["mode"] not in model_mode_vals: if config["model"]["mode"] not in model_mode_vals:
raise ValueError(f"model.mode must be in {model_mode_vals} when prompt_type is advanced") raise ValueError(f"model.mode must be in {model_mode_vals} when prompt_type is advanced")
if app_mode == AppMode.CHAT and config['model']["mode"] == ModelMode.COMPLETION.value: if app_mode == AppMode.CHAT and config["model"]["mode"] == ModelMode.COMPLETION.value:
user_prefix = config['completion_prompt_config']['conversation_histories_role']['user_prefix'] user_prefix = config["completion_prompt_config"]["conversation_histories_role"]["user_prefix"]
assistant_prefix = config['completion_prompt_config']['conversation_histories_role']['assistant_prefix'] assistant_prefix = config["completion_prompt_config"]["conversation_histories_role"]["assistant_prefix"]
if not user_prefix: if not user_prefix:
config['completion_prompt_config']['conversation_histories_role']['user_prefix'] = 'Human' config["completion_prompt_config"]["conversation_histories_role"]["user_prefix"] = "Human"
if not assistant_prefix: if not assistant_prefix:
config['completion_prompt_config']['conversation_histories_role']['assistant_prefix'] = 'Assistant' config["completion_prompt_config"]["conversation_histories_role"]["assistant_prefix"] = "Assistant"
if config['model']["mode"] == ModelMode.CHAT.value: if config["model"]["mode"] == ModelMode.CHAT.value:
prompt_list = config['chat_prompt_config']['prompt'] prompt_list = config["chat_prompt_config"]["prompt"]
if len(prompt_list) > 10: if len(prompt_list) > 10:
raise ValueError("prompt messages must be less than 10") raise ValueError("prompt messages must be less than 10")

@ -16,32 +16,30 @@ class BasicVariablesConfigManager:
variable_entities = [] variable_entities = []
# old external_data_tools # old external_data_tools
external_data_tools = config.get('external_data_tools', []) external_data_tools = config.get("external_data_tools", [])
for external_data_tool in external_data_tools: for external_data_tool in external_data_tools:
if 'enabled' not in external_data_tool or not external_data_tool['enabled']: if "enabled" not in external_data_tool or not external_data_tool["enabled"]:
continue continue
external_data_variables.append( external_data_variables.append(
ExternalDataVariableEntity( ExternalDataVariableEntity(
variable=external_data_tool['variable'], variable=external_data_tool["variable"],
type=external_data_tool['type'], type=external_data_tool["type"],
config=external_data_tool['config'] config=external_data_tool["config"],
) )
) )
# variables and external_data_tools # variables and external_data_tools
for variables in config.get('user_input_form', []): for variables in config.get("user_input_form", []):
variable_type = list(variables.keys())[0] variable_type = list(variables.keys())[0]
if variable_type == VariableEntityType.EXTERNAL_DATA_TOOL: if variable_type == VariableEntityType.EXTERNAL_DATA_TOOL:
variable = variables[variable_type] variable = variables[variable_type]
if 'config' not in variable: if "config" not in variable:
continue continue
external_data_variables.append( external_data_variables.append(
ExternalDataVariableEntity( ExternalDataVariableEntity(
variable=variable['variable'], variable=variable["variable"], type=variable["type"], config=variable["config"]
type=variable['type'],
config=variable['config']
) )
) )
elif variable_type in [ elif variable_type in [
@ -54,13 +52,13 @@ class BasicVariablesConfigManager:
variable_entities.append( variable_entities.append(
VariableEntity( VariableEntity(
type=variable_type, type=variable_type,
variable=variable.get('variable'), variable=variable.get("variable"),
description=variable.get('description'), description=variable.get("description"),
label=variable.get('label'), label=variable.get("label"),
required=variable.get('required', False), required=variable.get("required", False),
max_length=variable.get('max_length'), max_length=variable.get("max_length"),
options=variable.get('options'), options=variable.get("options"),
default=variable.get('default'), default=variable.get("default"),
) )
) )
@ -103,13 +101,13 @@ class BasicVariablesConfigManager:
raise ValueError("Keys in user_input_form list can only be 'text-input', 'paragraph' or 'select'") raise ValueError("Keys in user_input_form list can only be 'text-input', 'paragraph' or 'select'")
form_item = item[key] form_item = item[key]
if 'label' not in form_item: if "label" not in form_item:
raise ValueError("label is required in user_input_form") raise ValueError("label is required in user_input_form")
if not isinstance(form_item["label"], str): if not isinstance(form_item["label"], str):
raise ValueError("label in user_input_form must be of string type") raise ValueError("label in user_input_form must be of string type")
if 'variable' not in form_item: if "variable" not in form_item:
raise ValueError("variable is required in user_input_form") raise ValueError("variable is required in user_input_form")
if not isinstance(form_item["variable"], str): if not isinstance(form_item["variable"], str):
@ -117,26 +115,24 @@ class BasicVariablesConfigManager:
pattern = re.compile(r"^(?!\d)[\u4e00-\u9fa5A-Za-z0-9_\U0001F300-\U0001F64F\U0001F680-\U0001F6FF]{1,100}$") pattern = re.compile(r"^(?!\d)[\u4e00-\u9fa5A-Za-z0-9_\U0001F300-\U0001F64F\U0001F680-\U0001F6FF]{1,100}$")
if pattern.match(form_item["variable"]) is None: if pattern.match(form_item["variable"]) is None:
raise ValueError("variable in user_input_form must be a string, " raise ValueError("variable in user_input_form must be a string, " "and cannot start with a number")
"and cannot start with a number")
variables.append(form_item["variable"]) variables.append(form_item["variable"])
if 'required' not in form_item or not form_item["required"]: if "required" not in form_item or not form_item["required"]:
form_item["required"] = False form_item["required"] = False
if not isinstance(form_item["required"], bool): if not isinstance(form_item["required"], bool):
raise ValueError("required in user_input_form must be of boolean type") raise ValueError("required in user_input_form must be of boolean type")
if key == "select": if key == "select":
if 'options' not in form_item or not form_item["options"]: if "options" not in form_item or not form_item["options"]:
form_item["options"] = [] form_item["options"] = []
if not isinstance(form_item["options"], list): if not isinstance(form_item["options"], list):
raise ValueError("options in user_input_form must be a list of strings") raise ValueError("options in user_input_form must be a list of strings")
if "default" in form_item and form_item['default'] \ if "default" in form_item and form_item["default"] and form_item["default"] not in form_item["options"]:
and form_item["default"] not in form_item["options"]:
raise ValueError("default value in user_input_form must be in the options list") raise ValueError("default value in user_input_form must be in the options list")
return config, ["user_input_form"] return config, ["user_input_form"]
@ -168,10 +164,6 @@ class BasicVariablesConfigManager:
typ = tool["type"] typ = tool["type"]
config = tool["config"] config = tool["config"]
ExternalDataToolFactory.validate_config( ExternalDataToolFactory.validate_config(name=typ, tenant_id=tenant_id, config=config)
name=typ,
tenant_id=tenant_id,
config=config
)
return config, ["external_data_tools"] return config, ["external_data_tools"]

@ -12,6 +12,7 @@ class ModelConfigEntity(BaseModel):
""" """
Model Config Entity. Model Config Entity.
""" """
provider: str provider: str
model: str model: str
mode: Optional[str] = None mode: Optional[str] = None
@ -23,6 +24,7 @@ class AdvancedChatMessageEntity(BaseModel):
""" """
Advanced Chat Message Entity. Advanced Chat Message Entity.
""" """
text: str text: str
role: PromptMessageRole role: PromptMessageRole
@ -31,6 +33,7 @@ class AdvancedChatPromptTemplateEntity(BaseModel):
""" """
Advanced Chat Prompt Template Entity. Advanced Chat Prompt Template Entity.
""" """
messages: list[AdvancedChatMessageEntity] messages: list[AdvancedChatMessageEntity]
@ -43,6 +46,7 @@ class AdvancedCompletionPromptTemplateEntity(BaseModel):
""" """
Role Prefix Entity. Role Prefix Entity.
""" """
user: str user: str
assistant: str assistant: str
@ -60,11 +64,12 @@ class PromptTemplateEntity(BaseModel):
Prompt Type. Prompt Type.
'simple', 'advanced' 'simple', 'advanced'
""" """
SIMPLE = 'simple'
ADVANCED = 'advanced' SIMPLE = "simple"
ADVANCED = "advanced"
@classmethod @classmethod
def value_of(cls, value: str) -> 'PromptType': def value_of(cls, value: str) -> "PromptType":
""" """
Get value of given mode. Get value of given mode.
@ -74,7 +79,7 @@ class PromptTemplateEntity(BaseModel):
for mode in cls: for mode in cls:
if mode.value == value: if mode.value == value:
return mode return mode
raise ValueError(f'invalid prompt type value {value}') raise ValueError(f"invalid prompt type value {value}")
prompt_type: PromptType prompt_type: PromptType
simple_prompt_template: Optional[str] = None simple_prompt_template: Optional[str] = None
@ -110,6 +115,7 @@ class ExternalDataVariableEntity(BaseModel):
""" """
External Data Variable Entity. External Data Variable Entity.
""" """
variable: str variable: str
type: str type: str
config: dict[str, Any] = {} config: dict[str, Any] = {}
@ -125,11 +131,12 @@ class DatasetRetrieveConfigEntity(BaseModel):
Dataset Retrieve Strategy. Dataset Retrieve Strategy.
'single' or 'multiple' 'single' or 'multiple'
""" """
SINGLE = 'single'
MULTIPLE = 'multiple' SINGLE = "single"
MULTIPLE = "multiple"
@classmethod @classmethod
def value_of(cls, value: str) -> 'RetrieveStrategy': def value_of(cls, value: str) -> "RetrieveStrategy":
""" """
Get value of given mode. Get value of given mode.
@ -139,25 +146,24 @@ class DatasetRetrieveConfigEntity(BaseModel):
for mode in cls: for mode in cls:
if mode.value == value: if mode.value == value:
return mode return mode
raise ValueError(f'invalid retrieve strategy value {value}') raise ValueError(f"invalid retrieve strategy value {value}")
query_variable: Optional[str] = None # Only when app mode is completion query_variable: Optional[str] = None # Only when app mode is completion
retrieve_strategy: RetrieveStrategy retrieve_strategy: RetrieveStrategy
top_k: Optional[int] = None top_k: Optional[int] = None
score_threshold: Optional[float] = .0 score_threshold: Optional[float] = 0.0
rerank_mode: Optional[str] = 'reranking_model' rerank_mode: Optional[str] = "reranking_model"
reranking_model: Optional[dict] = None reranking_model: Optional[dict] = None
weights: Optional[dict] = None weights: Optional[dict] = None
reranking_enabled: Optional[bool] = True reranking_enabled: Optional[bool] = True
class DatasetEntity(BaseModel): class DatasetEntity(BaseModel):
""" """
Dataset Config Entity. Dataset Config Entity.
""" """
dataset_ids: list[str] dataset_ids: list[str]
retrieve_config: DatasetRetrieveConfigEntity retrieve_config: DatasetRetrieveConfigEntity
@ -166,6 +172,7 @@ class SensitiveWordAvoidanceEntity(BaseModel):
""" """
Sensitive Word Avoidance Entity. Sensitive Word Avoidance Entity.
""" """
type: str type: str
config: dict[str, Any] = {} config: dict[str, Any] = {}
@ -174,6 +181,7 @@ class TextToSpeechEntity(BaseModel):
""" """
Sensitive Word Avoidance Entity. Sensitive Word Avoidance Entity.
""" """
enabled: bool enabled: bool
voice: Optional[str] = None voice: Optional[str] = None
language: Optional[str] = None language: Optional[str] = None
@ -183,12 +191,11 @@ class TracingConfigEntity(BaseModel):
""" """
Tracing Config Entity. Tracing Config Entity.
""" """
enabled: bool enabled: bool
tracing_provider: str tracing_provider: str
class AppAdditionalFeatures(BaseModel): class AppAdditionalFeatures(BaseModel):
file_upload: Optional[FileExtraConfig] = None file_upload: Optional[FileExtraConfig] = None
opening_statement: Optional[str] = None opening_statement: Optional[str] = None
@ -200,10 +207,12 @@ class AppAdditionalFeatures(BaseModel):
text_to_speech: Optional[TextToSpeechEntity] = None text_to_speech: Optional[TextToSpeechEntity] = None
trace_config: Optional[TracingConfigEntity] = None trace_config: Optional[TracingConfigEntity] = None
class AppConfig(BaseModel): class AppConfig(BaseModel):
""" """
Application Config Entity. Application Config Entity.
""" """
tenant_id: str tenant_id: str
app_id: str app_id: str
app_mode: AppMode app_mode: AppMode
@ -216,15 +225,17 @@ class EasyUIBasedAppModelConfigFrom(Enum):
""" """
App Model Config From. App Model Config From.
""" """
ARGS = 'args'
APP_LATEST_CONFIG = 'app-latest-config' ARGS = "args"
CONVERSATION_SPECIFIC_CONFIG = 'conversation-specific-config' APP_LATEST_CONFIG = "app-latest-config"
CONVERSATION_SPECIFIC_CONFIG = "conversation-specific-config"
class EasyUIBasedAppConfig(AppConfig): class EasyUIBasedAppConfig(AppConfig):
""" """
Easy UI Based App Config Entity. Easy UI Based App Config Entity.
""" """
app_model_config_from: EasyUIBasedAppModelConfigFrom app_model_config_from: EasyUIBasedAppModelConfigFrom
app_model_config_id: str app_model_config_id: str
app_model_config_dict: dict app_model_config_dict: dict
@ -238,4 +249,5 @@ class WorkflowUIBasedAppConfig(AppConfig):
""" """
Workflow UI Based App Config Entity. Workflow UI Based App Config Entity.
""" """
workflow_id: str workflow_id: str

@ -13,21 +13,19 @@ class FileUploadConfigManager:
:param config: model config args :param config: model config args
:param is_vision: if True, the feature is vision feature :param is_vision: if True, the feature is vision feature
""" """
file_upload_dict = config.get('file_upload') file_upload_dict = config.get("file_upload")
if file_upload_dict: if file_upload_dict:
if file_upload_dict.get('image'): if file_upload_dict.get("image"):
if 'enabled' in file_upload_dict['image'] and file_upload_dict['image']['enabled']: if "enabled" in file_upload_dict["image"] and file_upload_dict["image"]["enabled"]:
image_config = { image_config = {
'number_limits': file_upload_dict['image']['number_limits'], "number_limits": file_upload_dict["image"]["number_limits"],
'transfer_methods': file_upload_dict['image']['transfer_methods'] "transfer_methods": file_upload_dict["image"]["transfer_methods"],
} }
if is_vision: if is_vision:
image_config['detail'] = file_upload_dict['image']['detail'] image_config["detail"] = file_upload_dict["image"]["detail"]
return FileExtraConfig( return FileExtraConfig(image_config=image_config)
image_config=image_config
)
return None return None
@ -49,21 +47,21 @@ class FileUploadConfigManager:
if not config["file_upload"].get("image"): if not config["file_upload"].get("image"):
config["file_upload"]["image"] = {"enabled": False} config["file_upload"]["image"] = {"enabled": False}
if config['file_upload']['image']['enabled']: if config["file_upload"]["image"]["enabled"]:
number_limits = config['file_upload']['image']['number_limits'] number_limits = config["file_upload"]["image"]["number_limits"]
if number_limits < 1 or number_limits > 6: if number_limits < 1 or number_limits > 6:
raise ValueError("number_limits must be in [1, 6]") raise ValueError("number_limits must be in [1, 6]")
if is_vision: if is_vision:
detail = config['file_upload']['image']['detail'] detail = config["file_upload"]["image"]["detail"]
if detail not in ['high', 'low']: if detail not in ["high", "low"]:
raise ValueError("detail must be in ['high', 'low']") raise ValueError("detail must be in ['high', 'low']")
transfer_methods = config['file_upload']['image']['transfer_methods'] transfer_methods = config["file_upload"]["image"]["transfer_methods"]
if not isinstance(transfer_methods, list): if not isinstance(transfer_methods, list):
raise ValueError("transfer_methods must be of list type") raise ValueError("transfer_methods must be of list type")
for method in transfer_methods: for method in transfer_methods:
if method not in ['remote_url', 'local_file']: if method not in ["remote_url", "local_file"]:
raise ValueError("transfer_methods must be in ['remote_url', 'local_file']") raise ValueError("transfer_methods must be in ['remote_url', 'local_file']")
return config, ["file_upload"] return config, ["file_upload"]

@ -7,9 +7,9 @@ class MoreLikeThisConfigManager:
:param config: model config args :param config: model config args
""" """
more_like_this = False more_like_this = False
more_like_this_dict = config.get('more_like_this') more_like_this_dict = config.get("more_like_this")
if more_like_this_dict: if more_like_this_dict:
if more_like_this_dict.get('enabled'): if more_like_this_dict.get("enabled"):
more_like_this = True more_like_this = True
return more_like_this return more_like_this
@ -22,9 +22,7 @@ class MoreLikeThisConfigManager:
:param config: app model config args :param config: app model config args
""" """
if not config.get("more_like_this"): if not config.get("more_like_this"):
config["more_like_this"] = { config["more_like_this"] = {"enabled": False}
"enabled": False
}
if not isinstance(config["more_like_this"], dict): if not isinstance(config["more_like_this"], dict):
raise ValueError("more_like_this must be of dict type") raise ValueError("more_like_this must be of dict type")

@ -1,5 +1,3 @@
class OpeningStatementConfigManager: class OpeningStatementConfigManager:
@classmethod @classmethod
def convert(cls, config: dict) -> tuple[str, list]: def convert(cls, config: dict) -> tuple[str, list]:
@ -9,10 +7,10 @@ class OpeningStatementConfigManager:
:param config: model config args :param config: model config args
""" """
# opening statement # opening statement
opening_statement = config.get('opening_statement') opening_statement = config.get("opening_statement")
# suggested questions # suggested questions
suggested_questions_list = config.get('suggested_questions') suggested_questions_list = config.get("suggested_questions")
return opening_statement, suggested_questions_list return opening_statement, suggested_questions_list

@ -2,9 +2,9 @@ class RetrievalResourceConfigManager:
@classmethod @classmethod
def convert(cls, config: dict) -> bool: def convert(cls, config: dict) -> bool:
show_retrieve_source = False show_retrieve_source = False
retriever_resource_dict = config.get('retriever_resource') retriever_resource_dict = config.get("retriever_resource")
if retriever_resource_dict: if retriever_resource_dict:
if retriever_resource_dict.get('enabled'): if retriever_resource_dict.get("enabled"):
show_retrieve_source = True show_retrieve_source = True
return show_retrieve_source return show_retrieve_source
@ -17,9 +17,7 @@ class RetrievalResourceConfigManager:
:param config: app model config args :param config: app model config args
""" """
if not config.get("retriever_resource"): if not config.get("retriever_resource"):
config["retriever_resource"] = { config["retriever_resource"] = {"enabled": False}
"enabled": False
}
if not isinstance(config["retriever_resource"], dict): if not isinstance(config["retriever_resource"], dict):
raise ValueError("retriever_resource must be of dict type") raise ValueError("retriever_resource must be of dict type")

@ -7,9 +7,9 @@ class SpeechToTextConfigManager:
:param config: model config args :param config: model config args
""" """
speech_to_text = False speech_to_text = False
speech_to_text_dict = config.get('speech_to_text') speech_to_text_dict = config.get("speech_to_text")
if speech_to_text_dict: if speech_to_text_dict:
if speech_to_text_dict.get('enabled'): if speech_to_text_dict.get("enabled"):
speech_to_text = True speech_to_text = True
return speech_to_text return speech_to_text
@ -22,9 +22,7 @@ class SpeechToTextConfigManager:
:param config: app model config args :param config: app model config args
""" """
if not config.get("speech_to_text"): if not config.get("speech_to_text"):
config["speech_to_text"] = { config["speech_to_text"] = {"enabled": False}
"enabled": False
}
if not isinstance(config["speech_to_text"], dict): if not isinstance(config["speech_to_text"], dict):
raise ValueError("speech_to_text must be of dict type") raise ValueError("speech_to_text must be of dict type")

@ -7,9 +7,9 @@ class SuggestedQuestionsAfterAnswerConfigManager:
:param config: model config args :param config: model config args
""" """
suggested_questions_after_answer = False suggested_questions_after_answer = False
suggested_questions_after_answer_dict = config.get('suggested_questions_after_answer') suggested_questions_after_answer_dict = config.get("suggested_questions_after_answer")
if suggested_questions_after_answer_dict: if suggested_questions_after_answer_dict:
if suggested_questions_after_answer_dict.get('enabled'): if suggested_questions_after_answer_dict.get("enabled"):
suggested_questions_after_answer = True suggested_questions_after_answer = True
return suggested_questions_after_answer return suggested_questions_after_answer
@ -22,15 +22,15 @@ class SuggestedQuestionsAfterAnswerConfigManager:
:param config: app model config args :param config: app model config args
""" """
if not config.get("suggested_questions_after_answer"): if not config.get("suggested_questions_after_answer"):
config["suggested_questions_after_answer"] = { config["suggested_questions_after_answer"] = {"enabled": False}
"enabled": False
}
if not isinstance(config["suggested_questions_after_answer"], dict): if not isinstance(config["suggested_questions_after_answer"], dict):
raise ValueError("suggested_questions_after_answer must be of dict type") raise ValueError("suggested_questions_after_answer must be of dict type")
if "enabled" not in config["suggested_questions_after_answer"] or not \ if (
config["suggested_questions_after_answer"]["enabled"]: "enabled" not in config["suggested_questions_after_answer"]
or not config["suggested_questions_after_answer"]["enabled"]
):
config["suggested_questions_after_answer"]["enabled"] = False config["suggested_questions_after_answer"]["enabled"] = False
if not isinstance(config["suggested_questions_after_answer"]["enabled"], bool): if not isinstance(config["suggested_questions_after_answer"]["enabled"], bool):

@ -10,13 +10,13 @@ class TextToSpeechConfigManager:
:param config: model config args :param config: model config args
""" """
text_to_speech = None text_to_speech = None
text_to_speech_dict = config.get('text_to_speech') text_to_speech_dict = config.get("text_to_speech")
if text_to_speech_dict: if text_to_speech_dict:
if text_to_speech_dict.get('enabled'): if text_to_speech_dict.get("enabled"):
text_to_speech = TextToSpeechEntity( text_to_speech = TextToSpeechEntity(
enabled=text_to_speech_dict.get('enabled'), enabled=text_to_speech_dict.get("enabled"),
voice=text_to_speech_dict.get('voice'), voice=text_to_speech_dict.get("voice"),
language=text_to_speech_dict.get('language'), language=text_to_speech_dict.get("language"),
) )
return text_to_speech return text_to_speech
@ -29,11 +29,7 @@ class TextToSpeechConfigManager:
:param config: app model config args :param config: app model config args
""" """
if not config.get("text_to_speech"): if not config.get("text_to_speech"):
config["text_to_speech"] = { config["text_to_speech"] = {"enabled": False, "voice": "", "language": ""}
"enabled": False,
"voice": "",
"language": ""
}
if not isinstance(config["text_to_speech"], dict): if not isinstance(config["text_to_speech"], dict):
raise ValueError("text_to_speech must be of dict type") raise ValueError("text_to_speech must be of dict type")

@ -1,4 +1,3 @@
from core.app.app_config.base_app_config_manager import BaseAppConfigManager from core.app.app_config.base_app_config_manager import BaseAppConfigManager
from core.app.app_config.common.sensitive_word_avoidance.manager import SensitiveWordAvoidanceConfigManager from core.app.app_config.common.sensitive_word_avoidance.manager import SensitiveWordAvoidanceConfigManager
from core.app.app_config.entities import WorkflowUIBasedAppConfig from core.app.app_config.entities import WorkflowUIBasedAppConfig
@ -19,13 +18,13 @@ class AdvancedChatAppConfig(WorkflowUIBasedAppConfig):
""" """
Advanced Chatbot App Config Entity. Advanced Chatbot App Config Entity.
""" """
pass pass
class AdvancedChatAppConfigManager(BaseAppConfigManager): class AdvancedChatAppConfigManager(BaseAppConfigManager):
@classmethod @classmethod
def get_app_config(cls, app_model: App, def get_app_config(cls, app_model: App, workflow: Workflow) -> AdvancedChatAppConfig:
workflow: Workflow) -> AdvancedChatAppConfig:
features_dict = workflow.features_dict features_dict = workflow.features_dict
app_mode = AppMode.value_of(app_model.mode) app_mode = AppMode.value_of(app_model.mode)
@ -34,13 +33,9 @@ class AdvancedChatAppConfigManager(BaseAppConfigManager):
app_id=app_model.id, app_id=app_model.id,
app_mode=app_mode, app_mode=app_mode,
workflow_id=workflow.id, workflow_id=workflow.id,
sensitive_word_avoidance=SensitiveWordAvoidanceConfigManager.convert( sensitive_word_avoidance=SensitiveWordAvoidanceConfigManager.convert(config=features_dict),
config=features_dict variables=WorkflowVariablesConfigManager.convert(workflow=workflow),
), additional_features=cls.convert_features(features_dict, app_mode),
variables=WorkflowVariablesConfigManager.convert(
workflow=workflow
),
additional_features=cls.convert_features(features_dict, app_mode)
) )
return app_config return app_config
@ -58,8 +53,7 @@ class AdvancedChatAppConfigManager(BaseAppConfigManager):
# file upload validation # file upload validation
config, current_related_config_keys = FileUploadConfigManager.validate_and_set_defaults( config, current_related_config_keys = FileUploadConfigManager.validate_and_set_defaults(
config=config, config=config, is_vision=False
is_vision=False
) )
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)
@ -69,7 +63,8 @@ class AdvancedChatAppConfigManager(BaseAppConfigManager):
# suggested_questions_after_answer # suggested_questions_after_answer
config, current_related_config_keys = SuggestedQuestionsAfterAnswerConfigManager.validate_and_set_defaults( config, current_related_config_keys = SuggestedQuestionsAfterAnswerConfigManager.validate_and_set_defaults(
config) config
)
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)
# speech_to_text # speech_to_text
@ -86,9 +81,7 @@ class AdvancedChatAppConfigManager(BaseAppConfigManager):
# moderation validation # moderation validation
config, current_related_config_keys = SensitiveWordAvoidanceConfigManager.validate_and_set_defaults( config, current_related_config_keys = SensitiveWordAvoidanceConfigManager.validate_and_set_defaults(
tenant_id=tenant_id, tenant_id=tenant_id, config=config, only_structure_validate=only_structure_validate
config=config,
only_structure_validate=only_structure_validate
) )
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)
@ -98,4 +91,3 @@ class AdvancedChatAppConfigManager(BaseAppConfigManager):
filtered_config = {key: config.get(key) for key in related_config_keys} filtered_config = {key: config.get(key) for key in related_config_keys}
return filtered_config return filtered_config

@ -4,12 +4,10 @@ import os
import threading import threading
import uuid import uuid
from collections.abc import Generator from collections.abc import Generator
from typing import Union from typing import Any, Literal, Optional, Union, overload
from flask import Flask, current_app from flask import Flask, current_app
from pydantic import ValidationError from pydantic import ValidationError
from sqlalchemy import select
from sqlalchemy.orm import Session
import contexts import contexts
from core.app.app_config.features.file_upload.manager import FileUploadConfigManager from core.app.app_config.features.file_upload.manager import FileUploadConfigManager
@ -17,36 +15,54 @@ from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfig
from core.app.apps.advanced_chat.app_runner import AdvancedChatAppRunner from core.app.apps.advanced_chat.app_runner import AdvancedChatAppRunner
from core.app.apps.advanced_chat.generate_response_converter import AdvancedChatAppGenerateResponseConverter from core.app.apps.advanced_chat.generate_response_converter import AdvancedChatAppGenerateResponseConverter
from core.app.apps.advanced_chat.generate_task_pipeline import AdvancedChatAppGenerateTaskPipeline from core.app.apps.advanced_chat.generate_task_pipeline import AdvancedChatAppGenerateTaskPipeline
from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedException, PublishFrom from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedError, PublishFrom
from core.app.apps.message_based_app_generator import MessageBasedAppGenerator from core.app.apps.message_based_app_generator import MessageBasedAppGenerator
from core.app.apps.message_based_app_queue_manager import MessageBasedAppQueueManager from core.app.apps.message_based_app_queue_manager import MessageBasedAppQueueManager
from core.app.entities.app_invoke_entities import ( from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom
AdvancedChatAppGenerateEntity,
InvokeFrom,
)
from core.app.entities.task_entities import ChatbotAppBlockingResponse, ChatbotAppStreamResponse from core.app.entities.task_entities import ChatbotAppBlockingResponse, ChatbotAppStreamResponse
from core.file.message_file_parser import MessageFileParser from core.file.message_file_parser import MessageFileParser
from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError from core.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError
from core.ops.ops_trace_manager import TraceQueueManager from core.ops.ops_trace_manager import TraceQueueManager
from core.workflow.entities.variable_pool import VariablePool
from core.workflow.enums import SystemVariableKey
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, Conversation, EndUser, Message from models.model import App, Conversation, EndUser, Message
from models.workflow import ConversationVariable, Workflow from models.workflow import Workflow
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AdvancedChatAppGenerator(MessageBasedAppGenerator): class AdvancedChatAppGenerator(MessageBasedAppGenerator):
@overload
def generate( def generate(
self, app_model: App, self,
app_model: App,
workflow: Workflow,
user: Union[Account, EndUser],
args: dict,
invoke_from: InvokeFrom,
stream: Literal[True] = True,
) -> Generator[str, None, None]: ...
@overload
def generate(
self,
app_model: App,
workflow: Workflow,
user: Union[Account, EndUser],
args: dict,
invoke_from: InvokeFrom,
stream: Literal[False] = False,
) -> dict: ...
def generate(
self,
app_model: App,
workflow: Workflow, workflow: Workflow,
user: Union[Account, EndUser], user: Union[Account, EndUser],
args: dict, args: dict,
invoke_from: InvokeFrom, invoke_from: InvokeFrom,
stream: bool = True, stream: bool = True,
): ) -> dict[str, Any] | Generator[str, Any, None]:
""" """
Generate App response. Generate App response.
@ -57,44 +73,37 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
:param invoke_from: invoke from source :param invoke_from: invoke from source
:param stream: is stream :param stream: is stream
""" """
if not args.get('query'): if not args.get("query"):
raise ValueError('query is required') raise ValueError("query is required")
query = args['query'] query = args["query"]
if not isinstance(query, str): if not isinstance(query, str):
raise ValueError('query must be a string') raise ValueError("query must be a string")
query = query.replace('\x00', '') query = query.replace("\x00", "")
inputs = args['inputs'] inputs = args["inputs"]
extras = { extras = {"auto_generate_conversation_name": args.get("auto_generate_name", False)}
"auto_generate_conversation_name": args.get('auto_generate_name', False)
}
# get conversation # get conversation
conversation = None conversation = None
conversation_id = args.get('conversation_id') conversation_id = args.get("conversation_id")
if conversation_id: if conversation_id:
conversation = self._get_conversation_by_user(app_model=app_model, conversation_id=conversation_id, user=user) conversation = self._get_conversation_by_user(
app_model=app_model, conversation_id=conversation_id, user=user
)
# parse files # parse files
files = args['files'] if args.get('files') else [] files = args["files"] if args.get("files") else []
message_file_parser = MessageFileParser(tenant_id=app_model.tenant_id, app_id=app_model.id) message_file_parser = MessageFileParser(tenant_id=app_model.tenant_id, app_id=app_model.id)
file_extra_config = FileUploadConfigManager.convert(workflow.features_dict, is_vision=False) file_extra_config = FileUploadConfigManager.convert(workflow.features_dict, is_vision=False)
if file_extra_config: if file_extra_config:
file_objs = message_file_parser.validate_and_transform_files_arg( file_objs = message_file_parser.validate_and_transform_files_arg(files, file_extra_config, user)
files,
file_extra_config,
user
)
else: else:
file_objs = [] file_objs = []
# convert to app config # convert to app config
app_config = AdvancedChatAppConfigManager.get_app_config( app_config = AdvancedChatAppConfigManager.get_app_config(app_model=app_model, workflow=workflow)
app_model=app_model,
workflow=workflow
)
# get tracing instance # get tracing instance
user_id = user.id if isinstance(user, Account) else user.session_id user_id = user.id if isinstance(user, Account) else user.session_id
@ -116,7 +125,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
stream=stream, stream=stream,
invoke_from=invoke_from, invoke_from=invoke_from,
extras=extras, extras=extras,
trace_manager=trace_manager trace_manager=trace_manager,
) )
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id) contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
@ -126,15 +135,12 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
invoke_from=invoke_from, invoke_from=invoke_from,
application_generate_entity=application_generate_entity, application_generate_entity=application_generate_entity,
conversation=conversation, conversation=conversation,
stream=stream stream=stream,
) )
def single_iteration_generate(self, app_model: App, def single_iteration_generate(
workflow: Workflow, self, app_model: App, workflow: Workflow, node_id: str, user: Account, args: dict, stream: bool = True
node_id: str, ) -> dict[str, Any] | Generator[str, Any, None]:
user: Account,
args: dict,
stream: bool = True):
""" """
Generate App response. Generate App response.
@ -146,43 +152,29 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
:param stream: is stream :param stream: is stream
""" """
if not node_id: if not node_id:
raise ValueError('node_id is required') raise ValueError("node_id is required")
if args.get('inputs') is None:
raise ValueError('inputs is required')
extras = { if args.get("inputs") is None:
"auto_generate_conversation_name": False raise ValueError("inputs is required")
}
# get conversation
conversation = None
conversation_id = args.get('conversation_id')
if conversation_id:
conversation = self._get_conversation_by_user(app_model=app_model, conversation_id=conversation_id, user=user)
# convert to app config # convert to app config
app_config = AdvancedChatAppConfigManager.get_app_config( app_config = AdvancedChatAppConfigManager.get_app_config(app_model=app_model, workflow=workflow)
app_model=app_model,
workflow=workflow
)
# init application generate entity # init application generate entity
application_generate_entity = AdvancedChatAppGenerateEntity( application_generate_entity = AdvancedChatAppGenerateEntity(
task_id=str(uuid.uuid4()), task_id=str(uuid.uuid4()),
app_config=app_config, app_config=app_config,
conversation_id=conversation.id if conversation else None, conversation_id=None,
inputs={}, inputs={},
query='', query="",
files=[], files=[],
user_id=user.id, user_id=user.id,
stream=stream, stream=stream,
invoke_from=InvokeFrom.DEBUGGER, invoke_from=InvokeFrom.DEBUGGER,
extras=extras, extras={"auto_generate_conversation_name": False},
single_iteration_run=AdvancedChatAppGenerateEntity.SingleIterationRunEntity( single_iteration_run=AdvancedChatAppGenerateEntity.SingleIterationRunEntity(
node_id=node_id, node_id=node_id, inputs=args["inputs"]
inputs=args['inputs'] ),
)
) )
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id) contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
@ -191,32 +183,42 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
user=user, user=user,
invoke_from=InvokeFrom.DEBUGGER, invoke_from=InvokeFrom.DEBUGGER,
application_generate_entity=application_generate_entity, application_generate_entity=application_generate_entity,
conversation=conversation, conversation=None,
stream=stream stream=stream,
) )
def _generate(self, *, def _generate(
workflow: Workflow, self,
user: Union[Account, EndUser], *,
invoke_from: InvokeFrom, workflow: Workflow,
application_generate_entity: AdvancedChatAppGenerateEntity, user: Union[Account, EndUser],
conversation: Conversation | None = None, invoke_from: InvokeFrom,
stream: bool = True): application_generate_entity: AdvancedChatAppGenerateEntity,
conversation: Optional[Conversation] = None,
stream: bool = True,
) -> dict[str, Any] | Generator[str, Any, None]:
"""
Generate App response.
:param workflow: Workflow
:param user: account or end user
:param invoke_from: invoke from source
:param application_generate_entity: application generate entity
:param conversation: conversation
:param stream: is stream
"""
is_first_conversation = False is_first_conversation = False
if not conversation: if not conversation:
is_first_conversation = True is_first_conversation = True
# init generate records # init generate records
( (conversation, message) = self._init_generate_records(application_generate_entity, conversation)
conversation,
message
) = self._init_generate_records(application_generate_entity, conversation)
if is_first_conversation: if is_first_conversation:
# update conversation features # update conversation features
conversation.override_model_configs = workflow.features conversation.override_model_configs = workflow.features
db.session.commit() db.session.commit()
# db.session.refresh(conversation) db.session.refresh(conversation)
# init queue manager # init queue manager
queue_manager = MessageBasedAppQueueManager( queue_manager = MessageBasedAppQueueManager(
@ -225,73 +227,21 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
invoke_from=application_generate_entity.invoke_from, invoke_from=application_generate_entity.invoke_from,
conversation_id=conversation.id, conversation_id=conversation.id,
app_mode=conversation.mode, app_mode=conversation.mode,
message_id=message.id message_id=message.id,
) )
# Init conversation variables
stmt = select(ConversationVariable).where(
ConversationVariable.app_id == conversation.app_id, ConversationVariable.conversation_id == conversation.id
)
with Session(db.engine) as session:
conversation_variables = session.scalars(stmt).all()
if not conversation_variables:
# Create conversation variables if they don't exist.
conversation_variables = [
ConversationVariable.from_variable(
app_id=conversation.app_id, conversation_id=conversation.id, variable=variable
)
for variable in workflow.conversation_variables
]
session.add_all(conversation_variables)
# Convert database entities to variables.
conversation_variables = [item.to_variable() for item in conversation_variables]
session.commit()
# Increment dialogue count.
conversation.dialogue_count += 1
conversation_id = conversation.id
conversation_dialogue_count = conversation.dialogue_count
db.session.commit()
db.session.refresh(conversation)
inputs = application_generate_entity.inputs
query = application_generate_entity.query
files = application_generate_entity.files
user_id = None
if application_generate_entity.invoke_from in [InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API]:
end_user = db.session.query(EndUser).filter(EndUser.id == application_generate_entity.user_id).first()
if end_user:
user_id = end_user.session_id
else:
user_id = application_generate_entity.user_id
# Create a variable pool.
system_inputs = {
SystemVariableKey.QUERY: query,
SystemVariableKey.FILES: files,
SystemVariableKey.CONVERSATION_ID: conversation_id,
SystemVariableKey.USER_ID: user_id,
SystemVariableKey.DIALOGUE_COUNT: conversation_dialogue_count,
}
variable_pool = VariablePool(
system_variables=system_inputs,
user_inputs=inputs,
environment_variables=workflow.environment_variables,
conversation_variables=conversation_variables,
)
contexts.workflow_variable_pool.set(variable_pool)
# new thread # new thread
worker_thread = threading.Thread(target=self._generate_worker, kwargs={ worker_thread = threading.Thread(
'flask_app': current_app._get_current_object(), target=self._generate_worker,
'application_generate_entity': application_generate_entity, kwargs={
'queue_manager': queue_manager, "flask_app": current_app._get_current_object(), # type: ignore
'message_id': message.id, "application_generate_entity": application_generate_entity,
'context': contextvars.copy_context(), "queue_manager": queue_manager,
}) "conversation_id": conversation.id,
"message_id": message.id,
"context": contextvars.copy_context(),
},
)
worker_thread.start() worker_thread.start()
@ -306,16 +256,17 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
stream=stream, stream=stream,
) )
return AdvancedChatAppGenerateResponseConverter.convert( return AdvancedChatAppGenerateResponseConverter.convert(response=response, invoke_from=invoke_from)
response=response,
invoke_from=invoke_from
)
def _generate_worker(self, flask_app: Flask, def _generate_worker(
application_generate_entity: AdvancedChatAppGenerateEntity, self,
queue_manager: AppQueueManager, flask_app: Flask,
message_id: str, application_generate_entity: AdvancedChatAppGenerateEntity,
context: contextvars.Context) -> None: queue_manager: AppQueueManager,
conversation_id: str,
message_id: str,
context: contextvars.Context,
) -> None:
""" """
Generate worker in a new thread. Generate worker in a new thread.
:param flask_app: Flask app :param flask_app: Flask app
@ -329,40 +280,30 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
var.set(val) var.set(val)
with flask_app.app_context(): with flask_app.app_context():
try: try:
runner = AdvancedChatAppRunner() # get conversation and message
if application_generate_entity.single_iteration_run: conversation = self._get_conversation(conversation_id)
single_iteration_run = application_generate_entity.single_iteration_run message = self._get_message(message_id)
runner.single_iteration_run(
app_id=application_generate_entity.app_config.app_id, # chatbot app
workflow_id=application_generate_entity.app_config.workflow_id, runner = AdvancedChatAppRunner(
queue_manager=queue_manager, application_generate_entity=application_generate_entity,
inputs=single_iteration_run.inputs, queue_manager=queue_manager,
node_id=single_iteration_run.node_id, conversation=conversation,
user_id=application_generate_entity.user_id message=message,
) )
else:
# get message runner.run()
message = self._get_message(message_id) except GenerateTaskStoppedError:
# chatbot app
runner = AdvancedChatAppRunner()
runner.run(
application_generate_entity=application_generate_entity,
queue_manager=queue_manager,
message=message
)
except GenerateTaskStoppedException:
pass pass
except InvokeAuthorizationError: except InvokeAuthorizationError:
queue_manager.publish_error( queue_manager.publish_error(
InvokeAuthorizationError('Incorrect API key provided'), InvokeAuthorizationError("Incorrect API key provided"), PublishFrom.APPLICATION_MANAGER
PublishFrom.APPLICATION_MANAGER
) )
except ValidationError as e: except ValidationError as e:
logger.exception("Validation Error when generating") logger.exception("Validation Error when generating")
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER) queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
except (ValueError, InvokeError) as e: except (ValueError, InvokeError) as e:
if os.environ.get("DEBUG", "false").lower() == 'true': if os.environ.get("DEBUG", "false").lower() == "true":
logger.exception("Error when generating") logger.exception("Error when generating")
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER) queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
except Exception as e: except Exception as e:
@ -408,7 +349,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
return generate_task_pipeline.process() return generate_task_pipeline.process()
except ValueError as e: except ValueError as e:
if e.args[0] == "I/O operation on closed file.": # ignore this error if e.args[0] == "I/O operation on closed file.": # ignore this error
raise GenerateTaskStoppedException() raise GenerateTaskStoppedError()
else: else:
logger.exception(e) logger.exception(e)
raise e raise e

@ -21,14 +21,11 @@ class AudioTrunk:
self.status = status self.status = status
def _invoiceTTS(text_content: str, model_instance, tenant_id: str, voice: str): def _invoice_tts(text_content: str, model_instance, tenant_id: str, voice: str):
if not text_content or text_content.isspace(): if not text_content or text_content.isspace():
return return
return model_instance.invoke_tts( return model_instance.invoke_tts(
content_text=text_content.strip(), content_text=text_content.strip(), user="responding_tts", tenant_id=tenant_id, voice=voice
user="responding_tts",
tenant_id=tenant_id,
voice=voice
) )
@ -44,28 +41,26 @@ def _process_future(future_queue, audio_queue):
except Exception as e: except Exception as e:
logging.getLogger(__name__).warning(e) logging.getLogger(__name__).warning(e)
break break
audio_queue.put(AudioTrunk("finish", b'')) audio_queue.put(AudioTrunk("finish", b""))
class AppGeneratorTTSPublisher: class AppGeneratorTTSPublisher:
def __init__(self, tenant_id: str, voice: str): def __init__(self, tenant_id: str, voice: str):
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.tenant_id = tenant_id self.tenant_id = tenant_id
self.msg_text = '' self.msg_text = ""
self._audio_queue = queue.Queue() self._audio_queue = queue.Queue()
self._msg_queue = queue.Queue() self._msg_queue = queue.Queue()
self.match = re.compile(r'[。.!?]') self.match = re.compile(r"[。.!?]")
self.model_manager = ModelManager() self.model_manager = ModelManager()
self.model_instance = self.model_manager.get_default_model_instance( self.model_instance = self.model_manager.get_default_model_instance(
tenant_id=self.tenant_id, tenant_id=self.tenant_id, model_type=ModelType.TTS
model_type=ModelType.TTS
) )
self.voices = self.model_instance.get_tts_voices() self.voices = self.model_instance.get_tts_voices()
values = [voice.get('value') for voice in self.voices] values = [voice.get("value") for voice in self.voices]
self.voice = voice self.voice = voice
if not voice or voice not in values: if not voice or voice not in values:
self.voice = self.voices[0].get('value') self.voice = self.voices[0].get("value")
self.MAX_SENTENCE = 2 self.MAX_SENTENCE = 2
self._last_audio_event = None self._last_audio_event = None
self._runtime_thread = threading.Thread(target=self._runtime).start() self._runtime_thread = threading.Thread(target=self._runtime).start()
@ -85,8 +80,9 @@ class AppGeneratorTTSPublisher:
message = self._msg_queue.get() message = self._msg_queue.get()
if message is None: if message is None:
if self.msg_text and len(self.msg_text.strip()) > 0: if self.msg_text and len(self.msg_text.strip()) > 0:
futures_result = self.executor.submit(_invoiceTTS, self.msg_text, futures_result = self.executor.submit(
self.model_instance, self.tenant_id, self.voice) _invoice_tts, self.msg_text, self.model_instance, self.tenant_id, self.voice
)
future_queue.put(futures_result) future_queue.put(futures_result)
break break
elif isinstance(message.event, QueueAgentMessageEvent | QueueLLMChunkEvent): elif isinstance(message.event, QueueAgentMessageEvent | QueueLLMChunkEvent):
@ -94,28 +90,27 @@ class AppGeneratorTTSPublisher:
elif isinstance(message.event, QueueTextChunkEvent): elif isinstance(message.event, QueueTextChunkEvent):
self.msg_text += message.event.text self.msg_text += message.event.text
elif isinstance(message.event, QueueNodeSucceededEvent): elif isinstance(message.event, QueueNodeSucceededEvent):
self.msg_text += message.event.outputs.get('output', '') self.msg_text += message.event.outputs.get("output", "")
self.last_message = message self.last_message = message
sentence_arr, text_tmp = self._extract_sentence(self.msg_text) sentence_arr, text_tmp = self._extract_sentence(self.msg_text)
if len(sentence_arr) >= min(self.MAX_SENTENCE, 7): if len(sentence_arr) >= min(self.MAX_SENTENCE, 7):
self.MAX_SENTENCE += 1 self.MAX_SENTENCE += 1
text_content = ''.join(sentence_arr) text_content = "".join(sentence_arr)
futures_result = self.executor.submit(_invoiceTTS, text_content, futures_result = self.executor.submit(
self.model_instance, _invoice_tts, text_content, self.model_instance, self.tenant_id, self.voice
self.tenant_id, )
self.voice)
future_queue.put(futures_result) future_queue.put(futures_result)
if text_tmp: if text_tmp:
self.msg_text = text_tmp self.msg_text = text_tmp
else: else:
self.msg_text = '' self.msg_text = ""
except Exception as e: except Exception as e:
self.logger.warning(e) self.logger.warning(e)
break break
future_queue.put(None) future_queue.put(None)
def checkAndGetAudio(self) -> AudioTrunk | None: def check_and_get_audio(self) -> AudioTrunk | None:
try: try:
if self._last_audio_event and self._last_audio_event.status == "finish": if self._last_audio_event and self._last_audio_event.status == "finish":
if self.executor: if self.executor:

@ -1,145 +1,197 @@
import logging import logging
import os import os
import time
from collections.abc import Mapping from collections.abc import Mapping
from typing import Any, Optional, cast from typing import Any, cast
from sqlalchemy import select
from sqlalchemy.orm import Session
from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfig from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfig
from core.app.apps.advanced_chat.workflow_event_trigger_callback import WorkflowEventTriggerCallback from core.app.apps.base_app_queue_manager import AppQueueManager
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom from core.app.apps.workflow_app_runner import WorkflowBasedAppRunner
from core.app.apps.base_app_runner import AppRunner
from core.app.apps.workflow_logging_callback import WorkflowLoggingCallback from core.app.apps.workflow_logging_callback import WorkflowLoggingCallback
from core.app.entities.app_invoke_entities import ( from core.app.entities.app_invoke_entities import (
AdvancedChatAppGenerateEntity, AdvancedChatAppGenerateEntity,
InvokeFrom, InvokeFrom,
) )
from core.app.entities.queue_entities import QueueAnnotationReplyEvent, QueueStopEvent, QueueTextChunkEvent from core.app.entities.queue_entities import (
from core.moderation.base import ModerationException QueueAnnotationReplyEvent,
QueueStopEvent,
QueueTextChunkEvent,
)
from core.moderation.base import ModerationError
from core.workflow.callbacks.base_workflow_callback import WorkflowCallback from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
from core.workflow.nodes.base_node import UserFrom from core.workflow.entities.node_entities import UserFrom
from core.workflow.workflow_engine_manager import WorkflowEngineManager from core.workflow.entities.variable_pool import VariablePool
from core.workflow.enums import SystemVariableKey
from core.workflow.workflow_entry import WorkflowEntry
from extensions.ext_database import db from extensions.ext_database import db
from models import App, Message, Workflow from models.model import App, Conversation, EndUser, Message
from models.workflow import ConversationVariable, WorkflowType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AdvancedChatAppRunner(AppRunner): class AdvancedChatAppRunner(WorkflowBasedAppRunner):
""" """
AdvancedChat Application Runner AdvancedChat Application Runner
""" """
def run( def __init__(
self, self,
application_generate_entity: AdvancedChatAppGenerateEntity, application_generate_entity: AdvancedChatAppGenerateEntity,
queue_manager: AppQueueManager, queue_manager: AppQueueManager,
conversation: Conversation,
message: Message, message: Message,
) -> None: ) -> None:
""" """
Run application
:param application_generate_entity: application generate entity :param application_generate_entity: application generate entity
:param queue_manager: application queue manager :param queue_manager: application queue manager
:param conversation: conversation :param conversation: conversation
:param message: message :param message: message
"""
super().__init__(queue_manager)
self.application_generate_entity = application_generate_entity
self.conversation = conversation
self.message = message
def run(self) -> None:
"""
Run application
:return: :return:
""" """
app_config = application_generate_entity.app_config app_config = self.application_generate_entity.app_config
app_config = cast(AdvancedChatAppConfig, app_config) app_config = cast(AdvancedChatAppConfig, app_config)
app_record = db.session.query(App).filter(App.id == app_config.app_id).first() app_record = db.session.query(App).filter(App.id == app_config.app_id).first()
if not app_record: if not app_record:
raise ValueError('App not found') raise ValueError("App not found")
workflow = self.get_workflow(app_model=app_record, workflow_id=app_config.workflow_id) workflow = self.get_workflow(app_model=app_record, workflow_id=app_config.workflow_id)
if not workflow: if not workflow:
raise ValueError('Workflow not initialized') raise ValueError("Workflow not initialized")
inputs = application_generate_entity.inputs user_id = None
query = application_generate_entity.query if self.application_generate_entity.invoke_from in [InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API]:
end_user = db.session.query(EndUser).filter(EndUser.id == self.application_generate_entity.user_id).first()
if end_user:
user_id = end_user.session_id
else:
user_id = self.application_generate_entity.user_id
# moderation workflow_callbacks: list[WorkflowCallback] = []
if self.handle_input_moderation( if bool(os.environ.get("DEBUG", "False").lower() == "true"):
queue_manager=queue_manager, workflow_callbacks.append(WorkflowLoggingCallback())
app_record=app_record,
app_generate_entity=application_generate_entity,
inputs=inputs,
query=query,
message_id=message.id,
):
return
# annotation reply if self.application_generate_entity.single_iteration_run:
if self.handle_annotation_reply( # if only single iteration run is requested
app_record=app_record, graph, variable_pool = self._get_graph_and_variable_pool_of_single_iteration(
message=message, workflow=workflow,
query=query, node_id=self.application_generate_entity.single_iteration_run.node_id,
queue_manager=queue_manager, user_inputs=self.application_generate_entity.single_iteration_run.inputs,
app_generate_entity=application_generate_entity, )
): else:
return inputs = self.application_generate_entity.inputs
query = self.application_generate_entity.query
files = self.application_generate_entity.files
db.session.close() # moderation
if self.handle_input_moderation(
app_record=app_record,
app_generate_entity=self.application_generate_entity,
inputs=inputs,
query=query,
message_id=self.message.id,
):
return
workflow_callbacks: list[WorkflowCallback] = [ # annotation reply
WorkflowEventTriggerCallback(queue_manager=queue_manager, workflow=workflow) if self.handle_annotation_reply(
] app_record=app_record,
message=self.message,
query=query,
app_generate_entity=self.application_generate_entity,
):
return
if bool(os.environ.get('DEBUG', 'False').lower() == 'true'): # Init conversation variables
workflow_callbacks.append(WorkflowLoggingCallback()) stmt = select(ConversationVariable).where(
ConversationVariable.app_id == self.conversation.app_id,
ConversationVariable.conversation_id == self.conversation.id,
)
with Session(db.engine) as session:
conversation_variables = session.scalars(stmt).all()
if not conversation_variables:
# Create conversation variables if they don't exist.
conversation_variables = [
ConversationVariable.from_variable(
app_id=self.conversation.app_id, conversation_id=self.conversation.id, variable=variable
)
for variable in workflow.conversation_variables
]
session.add_all(conversation_variables)
# Convert database entities to variables.
conversation_variables = [item.to_variable() for item in conversation_variables]
# RUN WORKFLOW session.commit()
workflow_engine_manager = WorkflowEngineManager()
workflow_engine_manager.run_workflow(
workflow=workflow,
user_id=application_generate_entity.user_id,
user_from=UserFrom.ACCOUNT
if application_generate_entity.invoke_from in [InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER]
else UserFrom.END_USER,
invoke_from=application_generate_entity.invoke_from,
callbacks=workflow_callbacks,
call_depth=application_generate_entity.call_depth,
)
def single_iteration_run( # Increment dialogue count.
self, app_id: str, workflow_id: str, queue_manager: AppQueueManager, inputs: dict, node_id: str, user_id: str self.conversation.dialogue_count += 1
) -> None:
"""
Single iteration run
"""
app_record = db.session.query(App).filter(App.id == app_id).first()
if not app_record:
raise ValueError('App not found')
workflow = self.get_workflow(app_model=app_record, workflow_id=workflow_id) conversation_dialogue_count = self.conversation.dialogue_count
if not workflow: db.session.commit()
raise ValueError('Workflow not initialized')
# Create a variable pool.
system_inputs = {
SystemVariableKey.QUERY: query,
SystemVariableKey.FILES: files,
SystemVariableKey.CONVERSATION_ID: self.conversation.id,
SystemVariableKey.USER_ID: user_id,
SystemVariableKey.DIALOGUE_COUNT: conversation_dialogue_count,
}
# init variable pool
variable_pool = VariablePool(
system_variables=system_inputs,
user_inputs=inputs,
environment_variables=workflow.environment_variables,
conversation_variables=conversation_variables,
)
workflow_callbacks = [WorkflowEventTriggerCallback(queue_manager=queue_manager, workflow=workflow)] # init graph
graph = self._init_graph(graph_config=workflow.graph_dict)
workflow_engine_manager = WorkflowEngineManager() db.session.close()
workflow_engine_manager.single_step_run_iteration_workflow_node(
workflow=workflow, node_id=node_id, user_id=user_id, user_inputs=inputs, callbacks=workflow_callbacks # RUN WORKFLOW
workflow_entry = WorkflowEntry(
tenant_id=workflow.tenant_id,
app_id=workflow.app_id,
workflow_id=workflow.id,
workflow_type=WorkflowType.value_of(workflow.type),
graph=graph,
graph_config=workflow.graph_dict,
user_id=self.application_generate_entity.user_id,
user_from=(
UserFrom.ACCOUNT
if self.application_generate_entity.invoke_from in [InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER]
else UserFrom.END_USER
),
invoke_from=self.application_generate_entity.invoke_from,
call_depth=self.application_generate_entity.call_depth,
variable_pool=variable_pool,
) )
def get_workflow(self, app_model: App, workflow_id: str) -> Optional[Workflow]: generator = workflow_entry.run(
""" callbacks=workflow_callbacks,
Get workflow
"""
# fetch 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 == workflow_id
)
.first()
) )
# return workflow for event in generator:
return workflow self._handle_event(workflow_entry, event)
def handle_input_moderation( def handle_input_moderation(
self, self,
queue_manager: AppQueueManager,
app_record: App, app_record: App,
app_generate_entity: AdvancedChatAppGenerateEntity, app_generate_entity: AdvancedChatAppGenerateEntity,
inputs: Mapping[str, Any], inputs: Mapping[str, Any],
@ -148,7 +200,6 @@ class AdvancedChatAppRunner(AppRunner):
) -> bool: ) -> bool:
""" """
Handle input moderation Handle input moderation
:param queue_manager: application queue manager
:param app_record: app record :param app_record: app record
:param app_generate_entity: application generate entity :param app_generate_entity: application generate entity
:param inputs: inputs :param inputs: inputs
@ -166,31 +217,20 @@ class AdvancedChatAppRunner(AppRunner):
query=query, query=query,
message_id=message_id, message_id=message_id,
) )
except ModerationException as e: except ModerationError as e:
self._stream_output( self._complete_with_stream_output(text=str(e), stopped_by=QueueStopEvent.StopBy.INPUT_MODERATION)
queue_manager=queue_manager,
text=str(e),
stream=app_generate_entity.stream,
stopped_by=QueueStopEvent.StopBy.INPUT_MODERATION,
)
return True return True
return False return False
def handle_annotation_reply( def handle_annotation_reply(
self, self, app_record: App, message: Message, query: str, app_generate_entity: AdvancedChatAppGenerateEntity
app_record: App,
message: Message,
query: str,
queue_manager: AppQueueManager,
app_generate_entity: AdvancedChatAppGenerateEntity,
) -> bool: ) -> bool:
""" """
Handle annotation reply Handle annotation reply
:param app_record: app record :param app_record: app record
:param message: message :param message: message
:param query: query :param query: query
:param queue_manager: application queue manager
:param app_generate_entity: application generate entity :param app_generate_entity: application generate entity
""" """
# annotation reply # annotation reply
@ -203,37 +243,21 @@ class AdvancedChatAppRunner(AppRunner):
) )
if annotation_reply: if annotation_reply:
queue_manager.publish( self._publish_event(QueueAnnotationReplyEvent(message_annotation_id=annotation_reply.id))
QueueAnnotationReplyEvent(message_annotation_id=annotation_reply.id), PublishFrom.APPLICATION_MANAGER
)
self._stream_output( self._complete_with_stream_output(
queue_manager=queue_manager, text=annotation_reply.content, stopped_by=QueueStopEvent.StopBy.ANNOTATION_REPLY
text=annotation_reply.content,
stream=app_generate_entity.stream,
stopped_by=QueueStopEvent.StopBy.ANNOTATION_REPLY,
) )
return True return True
return False return False
def _stream_output( def _complete_with_stream_output(self, text: str, stopped_by: QueueStopEvent.StopBy) -> None:
self, queue_manager: AppQueueManager, text: str, stream: bool, stopped_by: QueueStopEvent.StopBy
) -> None:
""" """
Direct output Direct output
:param queue_manager: application queue manager
:param text: text :param text: text
:param stream: stream
:return: :return:
""" """
if stream: self._publish_event(QueueTextChunkEvent(text=text))
index = 0
for token in text:
queue_manager.publish(QueueTextChunkEvent(text=token), PublishFrom.APPLICATION_MANAGER)
index += 1
time.sleep(0.01)
else:
queue_manager.publish(QueueTextChunkEvent(text=text), PublishFrom.APPLICATION_MANAGER)
queue_manager.publish(QueueStopEvent(stopped_by=stopped_by), PublishFrom.APPLICATION_MANAGER) self._publish_event(QueueStopEvent(stopped_by=stopped_by))

@ -28,15 +28,15 @@ class AdvancedChatAppGenerateResponseConverter(AppGenerateResponseConverter):
""" """
blocking_response = cast(ChatbotAppBlockingResponse, blocking_response) blocking_response = cast(ChatbotAppBlockingResponse, blocking_response)
response = { response = {
'event': 'message', "event": "message",
'task_id': blocking_response.task_id, "task_id": blocking_response.task_id,
'id': blocking_response.data.id, "id": blocking_response.data.id,
'message_id': blocking_response.data.message_id, "message_id": blocking_response.data.message_id,
'conversation_id': blocking_response.data.conversation_id, "conversation_id": blocking_response.data.conversation_id,
'mode': blocking_response.data.mode, "mode": blocking_response.data.mode,
'answer': blocking_response.data.answer, "answer": blocking_response.data.answer,
'metadata': blocking_response.data.metadata, "metadata": blocking_response.data.metadata,
'created_at': blocking_response.data.created_at "created_at": blocking_response.data.created_at,
} }
return response return response
@ -50,13 +50,15 @@ class AdvancedChatAppGenerateResponseConverter(AppGenerateResponseConverter):
""" """
response = cls.convert_blocking_full_response(blocking_response) response = cls.convert_blocking_full_response(blocking_response)
metadata = response.get('metadata', {}) metadata = response.get("metadata", {})
response['metadata'] = cls._get_simple_metadata(metadata) response["metadata"] = cls._get_simple_metadata(metadata)
return response return response
@classmethod @classmethod
def convert_stream_full_response(cls, stream_response: Generator[AppStreamResponse, None, None]) -> Generator[str, Any, None]: def convert_stream_full_response(
cls, stream_response: Generator[AppStreamResponse, None, None]
) -> Generator[str, Any, None]:
""" """
Convert stream full response. Convert stream full response.
:param stream_response: stream response :param stream_response: stream response
@ -67,14 +69,14 @@ class AdvancedChatAppGenerateResponseConverter(AppGenerateResponseConverter):
sub_stream_response = chunk.stream_response sub_stream_response = chunk.stream_response
if isinstance(sub_stream_response, PingStreamResponse): if isinstance(sub_stream_response, PingStreamResponse):
yield 'ping' yield "ping"
continue continue
response_chunk = { response_chunk = {
'event': sub_stream_response.event.value, "event": sub_stream_response.event.value,
'conversation_id': chunk.conversation_id, "conversation_id": chunk.conversation_id,
'message_id': chunk.message_id, "message_id": chunk.message_id,
'created_at': chunk.created_at "created_at": chunk.created_at,
} }
if isinstance(sub_stream_response, ErrorStreamResponse): if isinstance(sub_stream_response, ErrorStreamResponse):
@ -85,7 +87,9 @@ class AdvancedChatAppGenerateResponseConverter(AppGenerateResponseConverter):
yield json.dumps(response_chunk) yield json.dumps(response_chunk)
@classmethod @classmethod
def convert_stream_simple_response(cls, stream_response: Generator[AppStreamResponse, None, None]) -> Generator[str, Any, None]: def convert_stream_simple_response(
cls, stream_response: Generator[AppStreamResponse, None, None]
) -> Generator[str, Any, None]:
""" """
Convert stream simple response. Convert stream simple response.
:param stream_response: stream response :param stream_response: stream response
@ -96,20 +100,20 @@ class AdvancedChatAppGenerateResponseConverter(AppGenerateResponseConverter):
sub_stream_response = chunk.stream_response sub_stream_response = chunk.stream_response
if isinstance(sub_stream_response, PingStreamResponse): if isinstance(sub_stream_response, PingStreamResponse):
yield 'ping' yield "ping"
continue continue
response_chunk = { response_chunk = {
'event': sub_stream_response.event.value, "event": sub_stream_response.event.value,
'conversation_id': chunk.conversation_id, "conversation_id": chunk.conversation_id,
'message_id': chunk.message_id, "message_id": chunk.message_id,
'created_at': chunk.created_at "created_at": chunk.created_at,
} }
if isinstance(sub_stream_response, MessageEndStreamResponse): if isinstance(sub_stream_response, MessageEndStreamResponse):
sub_stream_response_dict = sub_stream_response.to_dict() sub_stream_response_dict = sub_stream_response.to_dict()
metadata = sub_stream_response_dict.get('metadata', {}) metadata = sub_stream_response_dict.get("metadata", {})
sub_stream_response_dict['metadata'] = cls._get_simple_metadata(metadata) sub_stream_response_dict["metadata"] = cls._get_simple_metadata(metadata)
response_chunk.update(sub_stream_response_dict) response_chunk.update(sub_stream_response_dict)
if isinstance(sub_stream_response, ErrorStreamResponse): if isinstance(sub_stream_response, ErrorStreamResponse):
data = cls._error_to_stream_response(sub_stream_response.err) data = cls._error_to_stream_response(sub_stream_response.err)

@ -2,9 +2,8 @@ import json
import logging import logging
import time import time
from collections.abc import Generator from collections.abc import Generator
from typing import Any, Optional, Union, cast from typing import Any, Optional, Union
import contexts
from constants.tts_auto_play_timeout import TTS_AUTO_PLAY_TIMEOUT, TTS_AUTO_PLAY_YIELD_CPU_TIME from constants.tts_auto_play_timeout import TTS_AUTO_PLAY_TIMEOUT, TTS_AUTO_PLAY_YIELD_CPU_TIME
from core.app.apps.advanced_chat.app_generator_tts_publisher import AppGeneratorTTSPublisher, AudioTrunk from core.app.apps.advanced_chat.app_generator_tts_publisher import AppGeneratorTTSPublisher, AudioTrunk
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
@ -22,6 +21,9 @@ from core.app.entities.queue_entities import (
QueueNodeFailedEvent, QueueNodeFailedEvent,
QueueNodeStartedEvent, QueueNodeStartedEvent,
QueueNodeSucceededEvent, QueueNodeSucceededEvent,
QueueParallelBranchRunFailedEvent,
QueueParallelBranchRunStartedEvent,
QueueParallelBranchRunSucceededEvent,
QueuePingEvent, QueuePingEvent,
QueueRetrieverResourcesEvent, QueueRetrieverResourcesEvent,
QueueStopEvent, QueueStopEvent,
@ -31,34 +33,28 @@ from core.app.entities.queue_entities import (
QueueWorkflowSucceededEvent, QueueWorkflowSucceededEvent,
) )
from core.app.entities.task_entities import ( from core.app.entities.task_entities import (
AdvancedChatTaskState,
ChatbotAppBlockingResponse, ChatbotAppBlockingResponse,
ChatbotAppStreamResponse, ChatbotAppStreamResponse,
ChatflowStreamGenerateRoute,
ErrorStreamResponse, ErrorStreamResponse,
MessageAudioEndStreamResponse, MessageAudioEndStreamResponse,
MessageAudioStreamResponse, MessageAudioStreamResponse,
MessageEndStreamResponse, MessageEndStreamResponse,
StreamResponse, StreamResponse,
WorkflowTaskState,
) )
from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline
from core.app.task_pipeline.message_cycle_manage import MessageCycleManage from core.app.task_pipeline.message_cycle_manage import MessageCycleManage
from core.app.task_pipeline.workflow_cycle_manage import WorkflowCycleManage from core.app.task_pipeline.workflow_cycle_manage import WorkflowCycleManage
from core.file.file_obj import FileVar
from core.model_runtime.entities.llm_entities import LLMUsage
from core.model_runtime.utils.encoders import jsonable_encoder from core.model_runtime.utils.encoders import jsonable_encoder
from core.ops.ops_trace_manager import TraceQueueManager from core.ops.ops_trace_manager import TraceQueueManager
from core.workflow.entities.node_entities import NodeType
from core.workflow.enums import SystemVariableKey from core.workflow.enums import SystemVariableKey
from core.workflow.nodes.answer.answer_node import AnswerNode from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState
from core.workflow.nodes.answer.entities import TextGenerateRouteChunk, VarGenerateRouteChunk
from events.message_event import message_was_created from events.message_event import message_was_created
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 Conversation, EndUser, Message from models.model import Conversation, EndUser, Message
from models.workflow import ( from models.workflow import (
Workflow, Workflow,
WorkflowNodeExecution,
WorkflowRunStatus, WorkflowRunStatus,
) )
@ -69,22 +65,22 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc
""" """
AdvancedChatAppGenerateTaskPipeline is a class that generate stream output and state management for Application. AdvancedChatAppGenerateTaskPipeline is a class that generate stream output and state management for Application.
""" """
_task_state: AdvancedChatTaskState
_task_state: WorkflowTaskState
_application_generate_entity: AdvancedChatAppGenerateEntity _application_generate_entity: AdvancedChatAppGenerateEntity
_workflow: Workflow _workflow: Workflow
_user: Union[Account, EndUser] _user: Union[Account, EndUser]
# Deprecated
_workflow_system_variables: dict[SystemVariableKey, Any] _workflow_system_variables: dict[SystemVariableKey, Any]
_iteration_nested_relations: dict[str, list[str]]
def __init__( def __init__(
self, application_generate_entity: AdvancedChatAppGenerateEntity, self,
workflow: Workflow, application_generate_entity: AdvancedChatAppGenerateEntity,
queue_manager: AppQueueManager, workflow: Workflow,
conversation: Conversation, queue_manager: AppQueueManager,
message: Message, conversation: Conversation,
user: Union[Account, EndUser], message: Message,
stream: bool, user: Union[Account, EndUser],
stream: bool,
) -> None: ) -> None:
""" """
Initialize AdvancedChatAppGenerateTaskPipeline. Initialize AdvancedChatAppGenerateTaskPipeline.
@ -106,7 +102,6 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc
self._workflow = workflow self._workflow = workflow
self._conversation = conversation self._conversation = conversation
self._message = message self._message = message
# Deprecated
self._workflow_system_variables = { self._workflow_system_variables = {
SystemVariableKey.QUERY: message.query, SystemVariableKey.QUERY: message.query,
SystemVariableKey.FILES: application_generate_entity.files, SystemVariableKey.FILES: application_generate_entity.files,
@ -114,12 +109,8 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc
SystemVariableKey.USER_ID: user_id, SystemVariableKey.USER_ID: user_id,
} }
self._task_state = AdvancedChatTaskState( self._task_state = WorkflowTaskState()
usage=LLMUsage.empty_usage()
)
self._iteration_nested_relations = self._get_iteration_nested_relations(self._workflow.graph_dict)
self._stream_generate_routes = self._get_stream_generate_routes()
self._conversation_name_generate_thread = None self._conversation_name_generate_thread = None
def process(self): def process(self):
@ -133,13 +124,11 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc
# start generate conversation name thread # start generate conversation name thread
self._conversation_name_generate_thread = self._generate_conversation_name( self._conversation_name_generate_thread = self._generate_conversation_name(
self._conversation, self._conversation, self._application_generate_entity.query
self._application_generate_entity.query
) )
generator = self._wrapper_process_stream_response( generator = self._wrapper_process_stream_response(trace_manager=self._application_generate_entity.trace_manager)
trace_manager=self._application_generate_entity.trace_manager
)
if self._stream: if self._stream:
return self._to_stream_response(generator) return self._to_stream_response(generator)
else: else:
@ -156,7 +145,7 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc
elif isinstance(stream_response, MessageEndStreamResponse): elif isinstance(stream_response, MessageEndStreamResponse):
extras = {} extras = {}
if stream_response.metadata: if stream_response.metadata:
extras['metadata'] = stream_response.metadata extras["metadata"] = stream_response.metadata
return ChatbotAppBlockingResponse( return ChatbotAppBlockingResponse(
task_id=stream_response.task_id, task_id=stream_response.task_id,
@ -167,15 +156,17 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc
message_id=self._message.id, message_id=self._message.id,
answer=self._task_state.answer, answer=self._task_state.answer,
created_at=int(self._message.created_at.timestamp()), created_at=int(self._message.created_at.timestamp()),
**extras **extras,
) ),
) )
else: else:
continue continue
raise Exception('Queue listening stopped unexpectedly.') raise Exception("Queue listening stopped unexpectedly.")
def _to_stream_response(self, generator: Generator[StreamResponse, None, None]) -> Generator[ChatbotAppStreamResponse, Any, None]: def _to_stream_response(
self, generator: Generator[StreamResponse, None, None]
) -> Generator[ChatbotAppStreamResponse, Any, None]:
""" """
To stream response. To stream response.
:return: :return:
@ -185,31 +176,35 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc
conversation_id=self._conversation.id, conversation_id=self._conversation.id,
message_id=self._message.id, message_id=self._message.id,
created_at=int(self._message.created_at.timestamp()), created_at=int(self._message.created_at.timestamp()),
stream_response=stream_response stream_response=stream_response,
) )
def _listenAudioMsg(self, publisher, task_id: str): def _listen_audio_msg(self, publisher, task_id: str):
if not publisher: if not publisher:
return None return None
audio_msg: AudioTrunk = publisher.checkAndGetAudio() audio_msg: AudioTrunk = publisher.check_and_get_audio()
if audio_msg and audio_msg.status != "finish": if audio_msg and audio_msg.status != "finish":
return MessageAudioStreamResponse(audio=audio_msg.audio, task_id=task_id) return MessageAudioStreamResponse(audio=audio_msg.audio, task_id=task_id)
return None return None
def _wrapper_process_stream_response(self, trace_manager: Optional[TraceQueueManager] = None) -> \ def _wrapper_process_stream_response(
Generator[StreamResponse, None, None]: self, trace_manager: Optional[TraceQueueManager] = None
) -> Generator[StreamResponse, None, None]:
publisher = None tts_publisher = None
task_id = self._application_generate_entity.task_id task_id = self._application_generate_entity.task_id
tenant_id = self._application_generate_entity.app_config.tenant_id tenant_id = self._application_generate_entity.app_config.tenant_id
features_dict = self._workflow.features_dict features_dict = self._workflow.features_dict
if features_dict.get('text_to_speech') and features_dict['text_to_speech'].get('enabled') and features_dict[ if (
'text_to_speech'].get('autoPlay') == 'enabled': features_dict.get("text_to_speech")
publisher = AppGeneratorTTSPublisher(tenant_id, features_dict['text_to_speech'].get('voice')) and features_dict["text_to_speech"].get("enabled")
for response in self._process_stream_response(publisher=publisher, trace_manager=trace_manager): and features_dict["text_to_speech"].get("autoPlay") == "enabled"
):
tts_publisher = AppGeneratorTTSPublisher(tenant_id, features_dict["text_to_speech"].get("voice"))
for response in self._process_stream_response(tts_publisher=tts_publisher, trace_manager=trace_manager):
while True: while True:
audio_response = self._listenAudioMsg(publisher, task_id=task_id) audio_response = self._listen_audio_msg(tts_publisher, task_id=task_id)
if audio_response: if audio_response:
yield audio_response yield audio_response
else: else:
@ -220,9 +215,9 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc
# timeout # timeout
while (time.time() - start_listener_time) < TTS_AUTO_PLAY_TIMEOUT: while (time.time() - start_listener_time) < TTS_AUTO_PLAY_TIMEOUT:
try: try:
if not publisher: if not tts_publisher:
break break
audio_trunk = publisher.checkAndGetAudio() audio_trunk = tts_publisher.check_and_get_audio()
if audio_trunk is None: if audio_trunk is None:
# release cpu # release cpu
# sleep 20 ms ( 40ms => 1280 byte audio file,20ms => 640 byte audio file) # sleep 20 ms ( 40ms => 1280 byte audio file,20ms => 640 byte audio file)
@ -236,38 +231,38 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
break break
yield MessageAudioEndStreamResponse(audio='', task_id=task_id) yield MessageAudioEndStreamResponse(audio="", task_id=task_id)
def _process_stream_response( def _process_stream_response(
self, self,
publisher: AppGeneratorTTSPublisher, tts_publisher: Optional[AppGeneratorTTSPublisher] = None,
trace_manager: Optional[TraceQueueManager] = None trace_manager: Optional[TraceQueueManager] = None,
) -> Generator[StreamResponse, None, None]: ) -> Generator[StreamResponse, None, None]:
""" """
Process stream response. Process stream response.
:return: :return:
""" """
for message in self._queue_manager.listen(): # init fake graph runtime state
if (message.event graph_runtime_state = None
and getattr(message.event, 'metadata', None) workflow_run = None
and message.event.metadata.get('is_answer_previous_node', False)
and publisher): for queue_message in self._queue_manager.listen():
publisher.publish(message=message) event = queue_message.event
elif (hasattr(message.event, 'execution_metadata')
and message.event.execution_metadata if isinstance(event, QueuePingEvent):
and message.event.execution_metadata.get('is_answer_previous_node', False) yield self._ping_stream_response()
and publisher): elif isinstance(event, QueueErrorEvent):
publisher.publish(message=message)
event = message.event
if isinstance(event, QueueErrorEvent):
err = self._handle_error(event, self._message) err = self._handle_error(event, self._message)
yield self._error_to_stream_response(err) yield self._error_to_stream_response(err)
break break
elif isinstance(event, QueueWorkflowStartedEvent): elif isinstance(event, QueueWorkflowStartedEvent):
workflow_run = self._handle_workflow_start() # override graph runtime state
graph_runtime_state = event.graph_runtime_state
self._message = db.session.query(Message).filter(Message.id == self._message.id).first() # init workflow run
workflow_run = self._handle_workflow_run_start()
self._refetch_message()
self._message.workflow_run_id = workflow_run.id self._message.workflow_run_id = workflow_run.id
db.session.commit() db.session.commit()
@ -275,137 +270,231 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc
db.session.close() db.session.close()
yield self._workflow_start_to_stream_response( yield self._workflow_start_to_stream_response(
task_id=self._application_generate_entity.task_id, task_id=self._application_generate_entity.task_id, workflow_run=workflow_run
workflow_run=workflow_run
) )
elif isinstance(event, QueueNodeStartedEvent): elif isinstance(event, QueueNodeStartedEvent):
workflow_node_execution = self._handle_node_start(event) if not workflow_run:
raise Exception("Workflow run not initialized.")
# search stream_generate_routes if node id is answer start at node workflow_node_execution = self._handle_node_execution_start(workflow_run=workflow_run, event=event)
if not self._task_state.current_stream_generate_state and event.node_id in self._stream_generate_routes:
self._task_state.current_stream_generate_state = self._stream_generate_routes[event.node_id] response = self._workflow_node_start_to_stream_response(
# reset current route position to 0 event=event,
self._task_state.current_stream_generate_state.current_route_position = 0 task_id=self._application_generate_entity.task_id,
workflow_node_execution=workflow_node_execution,
)
# generate stream outputs when node started if response:
yield from self._generate_stream_outputs_when_node_started() yield response
elif isinstance(event, QueueNodeSucceededEvent):
workflow_node_execution = self._handle_workflow_node_execution_success(event)
yield self._workflow_node_start_to_stream_response( response = self._workflow_node_finish_to_stream_response(
event=event, event=event,
task_id=self._application_generate_entity.task_id, task_id=self._application_generate_entity.task_id,
workflow_node_execution=workflow_node_execution workflow_node_execution=workflow_node_execution,
) )
elif isinstance(event, QueueNodeSucceededEvent | QueueNodeFailedEvent):
workflow_node_execution = self._handle_node_finished(event)
# stream outputs when node finished if response:
generator = self._generate_stream_outputs_when_node_finished() yield response
if generator: elif isinstance(event, QueueNodeFailedEvent):
yield from generator workflow_node_execution = self._handle_workflow_node_execution_failed(event)
yield self._workflow_node_finish_to_stream_response( response = self._workflow_node_finish_to_stream_response(
event=event,
task_id=self._application_generate_entity.task_id, task_id=self._application_generate_entity.task_id,
workflow_node_execution=workflow_node_execution workflow_node_execution=workflow_node_execution,
) )
if isinstance(event, QueueNodeFailedEvent): if response:
yield from self._handle_iteration_exception( yield response
task_id=self._application_generate_entity.task_id, elif isinstance(event, QueueParallelBranchRunStartedEvent):
error=f'Child node failed: {event.error}' if not workflow_run:
) raise Exception("Workflow run not initialized.")
elif isinstance(event, QueueIterationStartEvent | QueueIterationNextEvent | QueueIterationCompletedEvent):
if isinstance(event, QueueIterationNextEvent): yield self._workflow_parallel_branch_start_to_stream_response(
# clear ran node execution infos of current iteration task_id=self._application_generate_entity.task_id, workflow_run=workflow_run, event=event
iteration_relations = self._iteration_nested_relations.get(event.node_id)
if iteration_relations:
for node_id in iteration_relations:
self._task_state.ran_node_execution_infos.pop(node_id, None)
yield self._handle_iteration_to_stream_response(self._application_generate_entity.task_id, event)
self._handle_iteration_operation(event)
elif isinstance(event, QueueStopEvent | QueueWorkflowSucceededEvent | QueueWorkflowFailedEvent):
workflow_run = self._handle_workflow_finished(
event, conversation_id=self._conversation.id, trace_manager=trace_manager
) )
if workflow_run: elif isinstance(event, QueueParallelBranchRunSucceededEvent | QueueParallelBranchRunFailedEvent):
yield self._workflow_finish_to_stream_response( if not workflow_run:
task_id=self._application_generate_entity.task_id, raise Exception("Workflow run not initialized.")
workflow_run=workflow_run
)
if workflow_run.status == WorkflowRunStatus.FAILED.value: yield self._workflow_parallel_branch_finished_to_stream_response(
err_event = QueueErrorEvent(error=ValueError(f'Run failed: {workflow_run.error}')) task_id=self._application_generate_entity.task_id, workflow_run=workflow_run, event=event
yield self._error_to_stream_response(self._handle_error(err_event, self._message)) )
break elif isinstance(event, QueueIterationStartEvent):
if not workflow_run:
raise Exception("Workflow run not initialized.")
if isinstance(event, QueueStopEvent): yield self._workflow_iteration_start_to_stream_response(
# Save message task_id=self._application_generate_entity.task_id, workflow_run=workflow_run, event=event
self._save_message() )
elif isinstance(event, QueueIterationNextEvent):
if not workflow_run:
raise Exception("Workflow run not initialized.")
yield self._message_end_to_stream_response() yield self._workflow_iteration_next_to_stream_response(
break task_id=self._application_generate_entity.task_id, workflow_run=workflow_run, event=event
else: )
self._queue_manager.publish( elif isinstance(event, QueueIterationCompletedEvent):
QueueAdvancedChatMessageEndEvent(), if not workflow_run:
PublishFrom.TASK_PIPELINE raise Exception("Workflow run not initialized.")
yield self._workflow_iteration_completed_to_stream_response(
task_id=self._application_generate_entity.task_id, workflow_run=workflow_run, event=event
)
elif isinstance(event, QueueWorkflowSucceededEvent):
if not workflow_run:
raise Exception("Workflow run not initialized.")
if not graph_runtime_state:
raise Exception("Graph runtime state not initialized.")
workflow_run = self._handle_workflow_run_success(
workflow_run=workflow_run,
start_at=graph_runtime_state.start_at,
total_tokens=graph_runtime_state.total_tokens,
total_steps=graph_runtime_state.node_run_steps,
outputs=json.dumps(event.outputs) if event.outputs else None,
conversation_id=self._conversation.id,
trace_manager=trace_manager,
)
yield self._workflow_finish_to_stream_response(
task_id=self._application_generate_entity.task_id, workflow_run=workflow_run
)
self._queue_manager.publish(QueueAdvancedChatMessageEndEvent(), PublishFrom.TASK_PIPELINE)
elif isinstance(event, QueueWorkflowFailedEvent):
if not workflow_run:
raise Exception("Workflow run not initialized.")
if not graph_runtime_state:
raise Exception("Graph runtime state not initialized.")
workflow_run = self._handle_workflow_run_failed(
workflow_run=workflow_run,
start_at=graph_runtime_state.start_at,
total_tokens=graph_runtime_state.total_tokens,
total_steps=graph_runtime_state.node_run_steps,
status=WorkflowRunStatus.FAILED,
error=event.error,
conversation_id=self._conversation.id,
trace_manager=trace_manager,
)
yield self._workflow_finish_to_stream_response(
task_id=self._application_generate_entity.task_id, workflow_run=workflow_run
)
err_event = QueueErrorEvent(error=ValueError(f"Run failed: {workflow_run.error}"))
yield self._error_to_stream_response(self._handle_error(err_event, self._message))
break
elif isinstance(event, QueueStopEvent):
if workflow_run and graph_runtime_state:
workflow_run = self._handle_workflow_run_failed(
workflow_run=workflow_run,
start_at=graph_runtime_state.start_at,
total_tokens=graph_runtime_state.total_tokens,
total_steps=graph_runtime_state.node_run_steps,
status=WorkflowRunStatus.STOPPED,
error=event.get_stop_reason(),
conversation_id=self._conversation.id,
trace_manager=trace_manager,
)
yield self._workflow_finish_to_stream_response(
task_id=self._application_generate_entity.task_id, workflow_run=workflow_run
) )
elif isinstance(event, QueueAdvancedChatMessageEndEvent):
output_moderation_answer = self._handle_output_moderation_when_task_finished(self._task_state.answer)
if output_moderation_answer:
self._task_state.answer = output_moderation_answer
yield self._message_replace_to_stream_response(answer=output_moderation_answer)
# Save message # Save message
self._save_message() self._save_message(graph_runtime_state=graph_runtime_state)
yield self._message_end_to_stream_response() yield self._message_end_to_stream_response()
break
elif isinstance(event, QueueRetrieverResourcesEvent): elif isinstance(event, QueueRetrieverResourcesEvent):
self._handle_retriever_resources(event) self._handle_retriever_resources(event)
self._refetch_message()
self._message.message_metadata = (
json.dumps(jsonable_encoder(self._task_state.metadata)) if self._task_state.metadata else None
)
db.session.commit()
db.session.refresh(self._message)
db.session.close()
elif isinstance(event, QueueAnnotationReplyEvent): elif isinstance(event, QueueAnnotationReplyEvent):
self._handle_annotation_reply(event) self._handle_annotation_reply(event)
self._refetch_message()
self._message.message_metadata = (
json.dumps(jsonable_encoder(self._task_state.metadata)) if self._task_state.metadata else None
)
db.session.commit()
db.session.refresh(self._message)
db.session.close()
elif isinstance(event, QueueTextChunkEvent): elif isinstance(event, QueueTextChunkEvent):
delta_text = event.text delta_text = event.text
if delta_text is None: if delta_text is None:
continue continue
if not self._is_stream_out_support(
event=event
):
continue
# handle output moderation chunk # handle output moderation chunk
should_direct_answer = self._handle_output_moderation_chunk(delta_text) should_direct_answer = self._handle_output_moderation_chunk(delta_text)
if should_direct_answer: if should_direct_answer:
continue continue
# only publish tts message at text chunk streaming
if tts_publisher:
tts_publisher.publish(message=queue_message)
self._task_state.answer += delta_text self._task_state.answer += delta_text
yield self._message_to_stream_response(delta_text, self._message.id) yield self._message_to_stream_response(
answer=delta_text, message_id=self._message.id, from_variable_selector=event.from_variable_selector
)
elif isinstance(event, QueueMessageReplaceEvent): elif isinstance(event, QueueMessageReplaceEvent):
# published by moderation
yield self._message_replace_to_stream_response(answer=event.text) yield self._message_replace_to_stream_response(answer=event.text)
elif isinstance(event, QueuePingEvent): elif isinstance(event, QueueAdvancedChatMessageEndEvent):
yield self._ping_stream_response() if not graph_runtime_state:
raise Exception("Graph runtime state not initialized.")
output_moderation_answer = self._handle_output_moderation_when_task_finished(self._task_state.answer)
if output_moderation_answer:
self._task_state.answer = output_moderation_answer
yield self._message_replace_to_stream_response(answer=output_moderation_answer)
# Save message
self._save_message(graph_runtime_state=graph_runtime_state)
yield self._message_end_to_stream_response()
else: else:
continue continue
if publisher:
publisher.publish(None) # publish None when task finished
if tts_publisher:
tts_publisher.publish(None)
if self._conversation_name_generate_thread: if self._conversation_name_generate_thread:
self._conversation_name_generate_thread.join() self._conversation_name_generate_thread.join()
def _save_message(self) -> None: def _save_message(self, graph_runtime_state: Optional[GraphRuntimeState] = None) -> None:
""" """
Save message. Save message.
:return: :return:
""" """
self._message = db.session.query(Message).filter(Message.id == self._message.id).first() self._refetch_message()
self._message.answer = self._task_state.answer self._message.answer = self._task_state.answer
self._message.provider_response_latency = time.perf_counter() - self._start_at self._message.provider_response_latency = time.perf_counter() - self._start_at
self._message.message_metadata = json.dumps(jsonable_encoder(self._task_state.metadata)) \ self._message.message_metadata = (
if self._task_state.metadata else None json.dumps(jsonable_encoder(self._task_state.metadata)) if self._task_state.metadata else None
)
if self._task_state.metadata and self._task_state.metadata.get('usage'):
usage = LLMUsage(**self._task_state.metadata['usage'])
if graph_runtime_state and graph_runtime_state.llm_usage:
usage = graph_runtime_state.llm_usage
self._message.message_tokens = usage.prompt_tokens self._message.message_tokens = usage.prompt_tokens
self._message.message_unit_price = usage.prompt_unit_price self._message.message_unit_price = usage.prompt_unit_price
self._message.message_price_unit = usage.prompt_price_unit self._message.message_price_unit = usage.prompt_price_unit
@ -422,7 +511,7 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc
application_generate_entity=self._application_generate_entity, application_generate_entity=self._application_generate_entity,
conversation=self._conversation, conversation=self._conversation,
is_first_message=self._application_generate_entity.conversation_id is None, is_first_message=self._application_generate_entity.conversation_id is None,
extras=self._application_generate_entity.extras extras=self._application_generate_entity.extras,
) )
def _message_end_to_stream_response(self) -> MessageEndStreamResponse: def _message_end_to_stream_response(self) -> MessageEndStreamResponse:
@ -432,331 +521,15 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc
""" """
extras = {} extras = {}
if self._task_state.metadata: if self._task_state.metadata:
extras['metadata'] = self._task_state.metadata extras["metadata"] = self._task_state.metadata.copy()
if "annotation_reply" in extras["metadata"]:
del extras["metadata"]["annotation_reply"]
return MessageEndStreamResponse( return MessageEndStreamResponse(
task_id=self._application_generate_entity.task_id, task_id=self._application_generate_entity.task_id, id=self._message.id, **extras
id=self._message.id,
**extras
) )
def _get_stream_generate_routes(self) -> dict[str, ChatflowStreamGenerateRoute]:
"""
Get stream generate routes.
:return:
"""
# find all answer nodes
graph = self._workflow.graph_dict
answer_node_configs = [
node for node in graph['nodes']
if node.get('data', {}).get('type') == NodeType.ANSWER.value
]
# parse stream output node value selectors of answer nodes
stream_generate_routes = {}
for node_config in answer_node_configs:
# get generate route for stream output
answer_node_id = node_config['id']
generate_route = AnswerNode.extract_generate_route_selectors(node_config)
start_node_ids = self._get_answer_start_at_node_ids(graph, answer_node_id)
if not start_node_ids:
continue
for start_node_id in start_node_ids:
stream_generate_routes[start_node_id] = ChatflowStreamGenerateRoute(
answer_node_id=answer_node_id,
generate_route=generate_route
)
return stream_generate_routes
def _get_answer_start_at_node_ids(self, graph: dict, target_node_id: str) \
-> list[str]:
"""
Get answer start at node id.
:param graph: graph
:param target_node_id: target node ID
:return:
"""
nodes = graph.get('nodes')
edges = graph.get('edges')
# fetch all ingoing edges from source node
ingoing_edges = []
for edge in edges:
if edge.get('target') == target_node_id:
ingoing_edges.append(edge)
if not ingoing_edges:
# check if it's the first node in the iteration
target_node = next((node for node in nodes if node.get('id') == target_node_id), None)
if not target_node:
return []
node_iteration_id = target_node.get('data', {}).get('iteration_id')
# get iteration start node id
for node in nodes:
if node.get('id') == node_iteration_id:
if node.get('data', {}).get('start_node_id') == target_node_id:
return [target_node_id]
return []
start_node_ids = []
for ingoing_edge in ingoing_edges:
source_node_id = ingoing_edge.get('source')
source_node = next((node for node in nodes if node.get('id') == source_node_id), None)
if not source_node:
continue
node_type = source_node.get('data', {}).get('type')
node_iteration_id = source_node.get('data', {}).get('iteration_id')
iteration_start_node_id = None
if node_iteration_id:
iteration_node = next((node for node in nodes if node.get('id') == node_iteration_id), None)
iteration_start_node_id = iteration_node.get('data', {}).get('start_node_id')
if node_type in [
NodeType.ANSWER.value,
NodeType.IF_ELSE.value,
NodeType.QUESTION_CLASSIFIER.value,
NodeType.ITERATION.value,
NodeType.LOOP.value
]:
start_node_id = target_node_id
start_node_ids.append(start_node_id)
elif node_type == NodeType.START.value or \
node_iteration_id is not None and iteration_start_node_id == source_node.get('id'):
start_node_id = source_node_id
start_node_ids.append(start_node_id)
else:
sub_start_node_ids = self._get_answer_start_at_node_ids(graph, source_node_id)
if sub_start_node_ids:
start_node_ids.extend(sub_start_node_ids)
return start_node_ids
def _get_iteration_nested_relations(self, graph: dict) -> dict[str, list[str]]:
"""
Get iteration nested relations.
:param graph: graph
:return:
"""
nodes = graph.get('nodes')
iteration_ids = [node.get('id') for node in nodes
if node.get('data', {}).get('type') in [
NodeType.ITERATION.value,
NodeType.LOOP.value,
]]
return {
iteration_id: [
node.get('id') for node in nodes if node.get('data', {}).get('iteration_id') == iteration_id
] for iteration_id in iteration_ids
}
def _generate_stream_outputs_when_node_started(self) -> Generator:
"""
Generate stream outputs.
:return:
"""
if self._task_state.current_stream_generate_state:
route_chunks = self._task_state.current_stream_generate_state.generate_route[
self._task_state.current_stream_generate_state.current_route_position:
]
for route_chunk in route_chunks:
if route_chunk.type == 'text':
route_chunk = cast(TextGenerateRouteChunk, route_chunk)
# handle output moderation chunk
should_direct_answer = self._handle_output_moderation_chunk(route_chunk.text)
if should_direct_answer:
continue
self._task_state.answer += route_chunk.text
yield self._message_to_stream_response(route_chunk.text, self._message.id)
else:
break
self._task_state.current_stream_generate_state.current_route_position += 1
# all route chunks are generated
if self._task_state.current_stream_generate_state.current_route_position == len(
self._task_state.current_stream_generate_state.generate_route
):
self._task_state.current_stream_generate_state = None
def _generate_stream_outputs_when_node_finished(self) -> Optional[Generator]:
"""
Generate stream outputs.
:return:
"""
if not self._task_state.current_stream_generate_state:
return
route_chunks = self._task_state.current_stream_generate_state.generate_route[
self._task_state.current_stream_generate_state.current_route_position:]
for route_chunk in route_chunks:
if route_chunk.type == 'text':
route_chunk = cast(TextGenerateRouteChunk, route_chunk)
self._task_state.answer += route_chunk.text
yield self._message_to_stream_response(route_chunk.text, self._message.id)
else:
value = None
route_chunk = cast(VarGenerateRouteChunk, route_chunk)
value_selector = route_chunk.value_selector
if not value_selector:
self._task_state.current_stream_generate_state.current_route_position += 1
continue
route_chunk_node_id = value_selector[0]
if route_chunk_node_id == 'sys':
# system variable
value = contexts.workflow_variable_pool.get().get(value_selector)
if value:
value = value.text
elif route_chunk_node_id in self._iteration_nested_relations:
# it's a iteration variable
if not self._iteration_state or route_chunk_node_id not in self._iteration_state.current_iterations:
continue
iteration_state = self._iteration_state.current_iterations[route_chunk_node_id]
iterator = iteration_state.inputs
if not iterator:
continue
iterator_selector = iterator.get('iterator_selector', [])
if value_selector[1] == 'index':
value = iteration_state.current_index
elif value_selector[1] == 'item':
value = iterator_selector[iteration_state.current_index] if iteration_state.current_index < len(
iterator_selector
) else None
else:
# check chunk node id is before current node id or equal to current node id
if route_chunk_node_id not in self._task_state.ran_node_execution_infos:
break
latest_node_execution_info = self._task_state.latest_node_execution_info
# get route chunk node execution info
route_chunk_node_execution_info = self._task_state.ran_node_execution_infos[route_chunk_node_id]
if (route_chunk_node_execution_info.node_type == NodeType.LLM
and latest_node_execution_info.node_type == NodeType.LLM):
# only LLM support chunk stream output
self._task_state.current_stream_generate_state.current_route_position += 1
continue
# get route chunk node execution
route_chunk_node_execution = db.session.query(WorkflowNodeExecution).filter(
WorkflowNodeExecution.id == route_chunk_node_execution_info.workflow_node_execution_id
).first()
outputs = route_chunk_node_execution.outputs_dict
# get value from outputs
value = None
for key in value_selector[1:]:
if not value:
value = outputs.get(key) if outputs else None
else:
value = value.get(key)
if value is not None:
text = ''
if isinstance(value, str | int | float):
text = str(value)
elif isinstance(value, FileVar):
# convert file to markdown
text = value.to_markdown()
elif isinstance(value, dict):
# handle files
file_vars = self._fetch_files_from_variable_value(value)
if file_vars:
file_var = file_vars[0]
try:
file_var_obj = FileVar(**file_var)
# convert file to markdown
text = file_var_obj.to_markdown()
except Exception as e:
logger.error(f'Error creating file var: {e}')
if not text:
# other types
text = json.dumps(value, ensure_ascii=False)
elif isinstance(value, list):
# handle files
file_vars = self._fetch_files_from_variable_value(value)
for file_var in file_vars:
try:
file_var_obj = FileVar(**file_var)
except Exception as e:
logger.error(f'Error creating file var: {e}')
continue
# convert file to markdown
text = file_var_obj.to_markdown() + ' '
text = text.strip()
if not text and value:
# other types
text = json.dumps(value, ensure_ascii=False)
if text:
self._task_state.answer += text
yield self._message_to_stream_response(text, self._message.id)
self._task_state.current_stream_generate_state.current_route_position += 1
# all route chunks are generated
if self._task_state.current_stream_generate_state.current_route_position == len(
self._task_state.current_stream_generate_state.generate_route
):
self._task_state.current_stream_generate_state = None
def _is_stream_out_support(self, event: QueueTextChunkEvent) -> bool:
"""
Is stream out support
:param event: queue text chunk event
:return:
"""
if not event.metadata:
return True
if 'node_id' not in event.metadata:
return True
node_type = event.metadata.get('node_type')
stream_output_value_selector = event.metadata.get('value_selector')
if not stream_output_value_selector:
return False
if not self._task_state.current_stream_generate_state:
return False
route_chunk = self._task_state.current_stream_generate_state.generate_route[
self._task_state.current_stream_generate_state.current_route_position]
if route_chunk.type != 'var':
return False
if node_type != NodeType.LLM:
# only LLM support chunk stream output
return False
route_chunk = cast(VarGenerateRouteChunk, route_chunk)
value_selector = route_chunk.value_selector
# check chunk node id is before current node id or equal to current node id
if value_selector != stream_output_value_selector:
return False
return True
def _handle_output_moderation_chunk(self, text: str) -> bool: def _handle_output_moderation_chunk(self, text: str) -> bool:
""" """
Handle output moderation chunk. Handle output moderation chunk.
@ -768,17 +541,23 @@ class AdvancedChatAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCyc
# stop subscribe new token when output moderation should direct output # stop subscribe new token when output moderation should direct output
self._task_state.answer = self._output_moderation_handler.get_final_output() self._task_state.answer = self._output_moderation_handler.get_final_output()
self._queue_manager.publish( self._queue_manager.publish(
QueueTextChunkEvent( QueueTextChunkEvent(text=self._task_state.answer), PublishFrom.TASK_PIPELINE
text=self._task_state.answer
), PublishFrom.TASK_PIPELINE
) )
self._queue_manager.publish( self._queue_manager.publish(
QueueStopEvent(stopped_by=QueueStopEvent.StopBy.OUTPUT_MODERATION), QueueStopEvent(stopped_by=QueueStopEvent.StopBy.OUTPUT_MODERATION), PublishFrom.TASK_PIPELINE
PublishFrom.TASK_PIPELINE
) )
return True return True
else: else:
self._output_moderation_handler.append_new_token(text) self._output_moderation_handler.append_new_token(text)
return False return False
def _refetch_message(self) -> None:
"""
Refetch message.
:return:
"""
message = db.session.query(Message).filter(Message.id == self._message.id).first()
if message:
self._message = message

@ -1,203 +0,0 @@
from typing import Any, Optional
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
from core.app.entities.queue_entities import (
AppQueueEvent,
QueueIterationCompletedEvent,
QueueIterationNextEvent,
QueueIterationStartEvent,
QueueNodeFailedEvent,
QueueNodeStartedEvent,
QueueNodeSucceededEvent,
QueueTextChunkEvent,
QueueWorkflowFailedEvent,
QueueWorkflowStartedEvent,
QueueWorkflowSucceededEvent,
)
from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
from core.workflow.entities.base_node_data_entities import BaseNodeData
from core.workflow.entities.node_entities import NodeType
from models.workflow import Workflow
class WorkflowEventTriggerCallback(WorkflowCallback):
def __init__(self, queue_manager: AppQueueManager, workflow: Workflow):
self._queue_manager = queue_manager
def on_workflow_run_started(self) -> None:
"""
Workflow run started
"""
self._queue_manager.publish(
QueueWorkflowStartedEvent(),
PublishFrom.APPLICATION_MANAGER
)
def on_workflow_run_succeeded(self) -> None:
"""
Workflow run succeeded
"""
self._queue_manager.publish(
QueueWorkflowSucceededEvent(),
PublishFrom.APPLICATION_MANAGER
)
def on_workflow_run_failed(self, error: str) -> None:
"""
Workflow run failed
"""
self._queue_manager.publish(
QueueWorkflowFailedEvent(
error=error
),
PublishFrom.APPLICATION_MANAGER
)
def on_workflow_node_execute_started(self, node_id: str,
node_type: NodeType,
node_data: BaseNodeData,
node_run_index: int = 1,
predecessor_node_id: Optional[str] = None) -> None:
"""
Workflow node execute started
"""
self._queue_manager.publish(
QueueNodeStartedEvent(
node_id=node_id,
node_type=node_type,
node_data=node_data,
node_run_index=node_run_index,
predecessor_node_id=predecessor_node_id
),
PublishFrom.APPLICATION_MANAGER
)
def on_workflow_node_execute_succeeded(self, node_id: str,
node_type: NodeType,
node_data: BaseNodeData,
inputs: Optional[dict] = None,
process_data: Optional[dict] = None,
outputs: Optional[dict] = None,
execution_metadata: Optional[dict] = None) -> None:
"""
Workflow node execute succeeded
"""
self._queue_manager.publish(
QueueNodeSucceededEvent(
node_id=node_id,
node_type=node_type,
node_data=node_data,
inputs=inputs,
process_data=process_data,
outputs=outputs,
execution_metadata=execution_metadata
),
PublishFrom.APPLICATION_MANAGER
)
def on_workflow_node_execute_failed(self, node_id: str,
node_type: NodeType,
node_data: BaseNodeData,
error: str,
inputs: Optional[dict] = None,
outputs: Optional[dict] = None,
process_data: Optional[dict] = None) -> None:
"""
Workflow node execute failed
"""
self._queue_manager.publish(
QueueNodeFailedEvent(
node_id=node_id,
node_type=node_type,
node_data=node_data,
inputs=inputs,
outputs=outputs,
process_data=process_data,
error=error
),
PublishFrom.APPLICATION_MANAGER
)
def on_node_text_chunk(self, node_id: str, text: str, metadata: Optional[dict] = None) -> None:
"""
Publish text chunk
"""
self._queue_manager.publish(
QueueTextChunkEvent(
text=text,
metadata={
"node_id": node_id,
**metadata
}
), PublishFrom.APPLICATION_MANAGER
)
def on_workflow_iteration_started(self,
node_id: str,
node_type: NodeType,
node_run_index: int = 1,
node_data: Optional[BaseNodeData] = None,
inputs: dict = None,
predecessor_node_id: Optional[str] = None,
metadata: Optional[dict] = None) -> None:
"""
Publish iteration started
"""
self._queue_manager.publish(
QueueIterationStartEvent(
node_id=node_id,
node_type=node_type,
node_run_index=node_run_index,
node_data=node_data,
inputs=inputs,
predecessor_node_id=predecessor_node_id,
metadata=metadata
),
PublishFrom.APPLICATION_MANAGER
)
def on_workflow_iteration_next(self, node_id: str,
node_type: NodeType,
index: int,
node_run_index: int,
output: Optional[Any]) -> None:
"""
Publish iteration next
"""
self._queue_manager._publish(
QueueIterationNextEvent(
node_id=node_id,
node_type=node_type,
index=index,
node_run_index=node_run_index,
output=output
),
PublishFrom.APPLICATION_MANAGER
)
def on_workflow_iteration_completed(self, node_id: str,
node_type: NodeType,
node_run_index: int,
outputs: dict) -> None:
"""
Publish iteration completed
"""
self._queue_manager._publish(
QueueIterationCompletedEvent(
node_id=node_id,
node_type=node_type,
node_run_index=node_run_index,
outputs=outputs
),
PublishFrom.APPLICATION_MANAGER
)
def on_event(self, event: AppQueueEvent) -> None:
"""
Publish event
"""
self._queue_manager.publish(
event,
PublishFrom.APPLICATION_MANAGER
)

@ -28,15 +28,19 @@ class AgentChatAppConfig(EasyUIBasedAppConfig):
""" """
Agent Chatbot App Config Entity. Agent Chatbot App Config Entity.
""" """
agent: Optional[AgentEntity] = None agent: Optional[AgentEntity] = None
class AgentChatAppConfigManager(BaseAppConfigManager): class AgentChatAppConfigManager(BaseAppConfigManager):
@classmethod @classmethod
def get_app_config(cls, app_model: App, def get_app_config(
app_model_config: AppModelConfig, cls,
conversation: Optional[Conversation] = None, app_model: App,
override_config_dict: Optional[dict] = None) -> AgentChatAppConfig: app_model_config: AppModelConfig,
conversation: Optional[Conversation] = None,
override_config_dict: Optional[dict] = None,
) -> AgentChatAppConfig:
""" """
Convert app model config to agent chat app config Convert app model config to agent chat app config
:param app_model: app model :param app_model: app model
@ -66,22 +70,12 @@ class AgentChatAppConfigManager(BaseAppConfigManager):
app_model_config_from=config_from, app_model_config_from=config_from,
app_model_config_id=app_model_config.id, app_model_config_id=app_model_config.id,
app_model_config_dict=config_dict, app_model_config_dict=config_dict,
model=ModelConfigManager.convert( model=ModelConfigManager.convert(config=config_dict),
config=config_dict prompt_template=PromptTemplateConfigManager.convert(config=config_dict),
), sensitive_word_avoidance=SensitiveWordAvoidanceConfigManager.convert(config=config_dict),
prompt_template=PromptTemplateConfigManager.convert( dataset=DatasetConfigManager.convert(config=config_dict),
config=config_dict agent=AgentConfigManager.convert(config=config_dict),
), additional_features=cls.convert_features(config_dict, app_mode),
sensitive_word_avoidance=SensitiveWordAvoidanceConfigManager.convert(
config=config_dict
),
dataset=DatasetConfigManager.convert(
config=config_dict
),
agent=AgentConfigManager.convert(
config=config_dict
),
additional_features=cls.convert_features(config_dict, app_mode)
) )
app_config.variables, app_config.external_data_variables = BasicVariablesConfigManager.convert( app_config.variables, app_config.external_data_variables = BasicVariablesConfigManager.convert(
@ -128,7 +122,8 @@ class AgentChatAppConfigManager(BaseAppConfigManager):
# suggested_questions_after_answer # suggested_questions_after_answer
config, current_related_config_keys = SuggestedQuestionsAfterAnswerConfigManager.validate_and_set_defaults( config, current_related_config_keys = SuggestedQuestionsAfterAnswerConfigManager.validate_and_set_defaults(
config) config
)
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)
# speech_to_text # speech_to_text
@ -145,13 +140,15 @@ class AgentChatAppConfigManager(BaseAppConfigManager):
# dataset configs # dataset configs
# dataset_query_variable # dataset_query_variable
config, current_related_config_keys = DatasetConfigManager.validate_and_set_defaults(tenant_id, app_mode, config, current_related_config_keys = DatasetConfigManager.validate_and_set_defaults(
config) tenant_id, app_mode, config
)
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)
# moderation validation # moderation validation
config, current_related_config_keys = SensitiveWordAvoidanceConfigManager.validate_and_set_defaults(tenant_id, config, current_related_config_keys = SensitiveWordAvoidanceConfigManager.validate_and_set_defaults(
config) tenant_id, config
)
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)
related_config_keys = list(set(related_config_keys)) related_config_keys = list(set(related_config_keys))
@ -170,10 +167,7 @@ class AgentChatAppConfigManager(BaseAppConfigManager):
:param config: app model config args :param config: app model config args
""" """
if not config.get("agent_mode"): if not config.get("agent_mode"):
config["agent_mode"] = { config["agent_mode"] = {"enabled": False, "tools": []}
"enabled": False,
"tools": []
}
if not isinstance(config["agent_mode"], dict): if not isinstance(config["agent_mode"], dict):
raise ValueError("agent_mode must be of object type") raise ValueError("agent_mode must be of object type")
@ -187,8 +181,9 @@ class AgentChatAppConfigManager(BaseAppConfigManager):
if not config["agent_mode"].get("strategy"): if not config["agent_mode"].get("strategy"):
config["agent_mode"]["strategy"] = PlanningStrategy.ROUTER.value config["agent_mode"]["strategy"] = PlanningStrategy.ROUTER.value
if config["agent_mode"]["strategy"] not in [member.value for member in if config["agent_mode"]["strategy"] not in [
list(PlanningStrategy.__members__.values())]: member.value for member in list(PlanningStrategy.__members__.values())
]:
raise ValueError("strategy in agent_mode must be in the specified strategy list") raise ValueError("strategy in agent_mode must be in the specified strategy list")
if not config["agent_mode"].get("tools"): if not config["agent_mode"].get("tools"):
@ -210,7 +205,7 @@ class AgentChatAppConfigManager(BaseAppConfigManager):
raise ValueError("enabled in agent_mode.tools must be of boolean type") raise ValueError("enabled in agent_mode.tools must be of boolean type")
if key == "dataset": if key == "dataset":
if 'id' not in tool_item: if "id" not in tool_item:
raise ValueError("id is required in dataset") raise ValueError("id is required in dataset")
try: try:

@ -3,7 +3,7 @@ import os
import threading import threading
import uuid import uuid
from collections.abc import Generator from collections.abc import Generator
from typing import Any, Union from typing import Any, Literal, Union, overload
from flask import Flask, current_app from flask import Flask, current_app
from pydantic import ValidationError from pydantic import ValidationError
@ -13,7 +13,7 @@ from core.app.app_config.features.file_upload.manager import FileUploadConfigMan
from core.app.apps.agent_chat.app_config_manager import AgentChatAppConfigManager from core.app.apps.agent_chat.app_config_manager import AgentChatAppConfigManager
from core.app.apps.agent_chat.app_runner import AgentChatAppRunner from core.app.apps.agent_chat.app_runner import AgentChatAppRunner
from core.app.apps.agent_chat.generate_response_converter import AgentChatAppGenerateResponseConverter from core.app.apps.agent_chat.generate_response_converter import AgentChatAppGenerateResponseConverter
from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedException, PublishFrom from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedError, PublishFrom
from core.app.apps.message_based_app_generator import MessageBasedAppGenerator from core.app.apps.message_based_app_generator import MessageBasedAppGenerator
from core.app.apps.message_based_app_queue_manager import MessageBasedAppQueueManager from core.app.apps.message_based_app_queue_manager import MessageBasedAppQueueManager
from core.app.entities.app_invoke_entities import AgentChatAppGenerateEntity, InvokeFrom from core.app.entities.app_invoke_entities import AgentChatAppGenerateEntity, InvokeFrom
@ -28,12 +28,29 @@ logger = logging.getLogger(__name__)
class AgentChatAppGenerator(MessageBasedAppGenerator): class AgentChatAppGenerator(MessageBasedAppGenerator):
def generate(self, app_model: App, @overload
user: Union[Account, EndUser], def generate(
args: Any, self,
invoke_from: InvokeFrom, app_model: App,
stream: bool = True) \ user: Union[Account, EndUser],
-> Union[dict, Generator[dict, None, None]]: args: dict,
invoke_from: InvokeFrom,
stream: Literal[True] = True,
) -> Generator[dict, None, None]: ...
@overload
def generate(
self,
app_model: App,
user: Union[Account, EndUser],
args: dict,
invoke_from: InvokeFrom,
stream: Literal[False] = False,
) -> dict: ...
def generate(
self, app_model: App, user: Union[Account, EndUser], args: Any, invoke_from: InvokeFrom, stream: bool = True
) -> Union[dict, Generator[dict, None, None]]:
""" """
Generate App response. Generate App response.
@ -44,60 +61,48 @@ class AgentChatAppGenerator(MessageBasedAppGenerator):
:param stream: is stream :param stream: is stream
""" """
if not stream: if not stream:
raise ValueError('Agent Chat App does not support blocking mode') raise ValueError("Agent Chat App does not support blocking mode")
if not args.get('query'): if not args.get("query"):
raise ValueError('query is required') raise ValueError("query is required")
query = args['query'] query = args["query"]
if not isinstance(query, str): if not isinstance(query, str):
raise ValueError('query must be a string') raise ValueError("query must be a string")
query = query.replace('\x00', '') query = query.replace("\x00", "")
inputs = args['inputs'] inputs = args["inputs"]
extras = { extras = {"auto_generate_conversation_name": args.get("auto_generate_name", True)}
"auto_generate_conversation_name": args.get('auto_generate_name', True)
}
# get conversation # get conversation
conversation = None conversation = None
if args.get('conversation_id'): if args.get("conversation_id"):
conversation = self._get_conversation_by_user(app_model, args.get('conversation_id'), user) conversation = self._get_conversation_by_user(app_model, args.get("conversation_id"), user)
# get app model config # get app model config
app_model_config = self._get_app_model_config( app_model_config = self._get_app_model_config(app_model=app_model, conversation=conversation)
app_model=app_model,
conversation=conversation
)
# validate override model config # validate override model config
override_model_config_dict = None override_model_config_dict = None
if args.get('model_config'): if args.get("model_config"):
if invoke_from != InvokeFrom.DEBUGGER: if invoke_from != InvokeFrom.DEBUGGER:
raise ValueError('Only in App debug mode can override model config') raise ValueError("Only in App debug mode can override model config")
# validate config # validate config
override_model_config_dict = AgentChatAppConfigManager.config_validate( override_model_config_dict = AgentChatAppConfigManager.config_validate(
tenant_id=app_model.tenant_id, tenant_id=app_model.tenant_id, config=args.get("model_config")
config=args.get('model_config')
) )
# always enable retriever resource in debugger mode # always enable retriever resource in debugger mode
override_model_config_dict["retriever_resource"] = { override_model_config_dict["retriever_resource"] = {"enabled": True}
"enabled": True
}
# parse files # parse files
files = args['files'] if args.get('files') else [] files = args["files"] if args.get("files") else []
message_file_parser = MessageFileParser(tenant_id=app_model.tenant_id, app_id=app_model.id) message_file_parser = MessageFileParser(tenant_id=app_model.tenant_id, app_id=app_model.id)
file_extra_config = FileUploadConfigManager.convert(override_model_config_dict or app_model_config.to_dict()) file_extra_config = FileUploadConfigManager.convert(override_model_config_dict or app_model_config.to_dict())
if file_extra_config: if file_extra_config:
file_objs = message_file_parser.validate_and_transform_files_arg( file_objs = message_file_parser.validate_and_transform_files_arg(files, file_extra_config, user)
files,
file_extra_config,
user
)
else: else:
file_objs = [] file_objs = []
@ -106,7 +111,7 @@ class AgentChatAppGenerator(MessageBasedAppGenerator):
app_model=app_model, app_model=app_model,
app_model_config=app_model_config, app_model_config=app_model_config,
conversation=conversation, conversation=conversation,
override_config_dict=override_model_config_dict override_config_dict=override_model_config_dict,
) )
# get tracing instance # get tracing instance
@ -127,14 +132,11 @@ class AgentChatAppGenerator(MessageBasedAppGenerator):
invoke_from=invoke_from, invoke_from=invoke_from,
extras=extras, extras=extras,
call_depth=0, call_depth=0,
trace_manager=trace_manager trace_manager=trace_manager,
) )
# init generate records # init generate records
( (conversation, message) = self._init_generate_records(application_generate_entity, conversation)
conversation,
message
) = self._init_generate_records(application_generate_entity, conversation)
# init queue manager # init queue manager
queue_manager = MessageBasedAppQueueManager( queue_manager = MessageBasedAppQueueManager(
@ -143,17 +145,20 @@ class AgentChatAppGenerator(MessageBasedAppGenerator):
invoke_from=application_generate_entity.invoke_from, invoke_from=application_generate_entity.invoke_from,
conversation_id=conversation.id, conversation_id=conversation.id,
app_mode=conversation.mode, app_mode=conversation.mode,
message_id=message.id message_id=message.id,
) )
# new thread # new thread
worker_thread = threading.Thread(target=self._generate_worker, kwargs={ worker_thread = threading.Thread(
'flask_app': current_app._get_current_object(), target=self._generate_worker,
'application_generate_entity': application_generate_entity, kwargs={
'queue_manager': queue_manager, "flask_app": current_app._get_current_object(),
'conversation_id': conversation.id, "application_generate_entity": application_generate_entity,
'message_id': message.id, "queue_manager": queue_manager,
}) "conversation_id": conversation.id,
"message_id": message.id,
},
)
worker_thread.start() worker_thread.start()
@ -167,13 +172,11 @@ class AgentChatAppGenerator(MessageBasedAppGenerator):
stream=stream, stream=stream,
) )
return AgentChatAppGenerateResponseConverter.convert( return AgentChatAppGenerateResponseConverter.convert(response=response, invoke_from=invoke_from)
response=response,
invoke_from=invoke_from
)
def _generate_worker( def _generate_worker(
self, flask_app: Flask, self,
flask_app: Flask,
application_generate_entity: AgentChatAppGenerateEntity, application_generate_entity: AgentChatAppGenerateEntity,
queue_manager: AppQueueManager, queue_manager: AppQueueManager,
conversation_id: str, conversation_id: str,
@ -202,18 +205,17 @@ class AgentChatAppGenerator(MessageBasedAppGenerator):
conversation=conversation, conversation=conversation,
message=message, message=message,
) )
except GenerateTaskStoppedException: except GenerateTaskStoppedError:
pass pass
except InvokeAuthorizationError: except InvokeAuthorizationError:
queue_manager.publish_error( queue_manager.publish_error(
InvokeAuthorizationError('Incorrect API key provided'), InvokeAuthorizationError("Incorrect API key provided"), PublishFrom.APPLICATION_MANAGER
PublishFrom.APPLICATION_MANAGER
) )
except ValidationError as e: except ValidationError as e:
logger.exception("Validation Error when generating") logger.exception("Validation Error when generating")
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER) queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
except (ValueError, InvokeError) as e: except (ValueError, InvokeError) as e:
if os.environ.get("DEBUG") and os.environ.get("DEBUG").lower() == 'true': if os.environ.get("DEBUG") and os.environ.get("DEBUG").lower() == "true":
logger.exception("Error when generating") logger.exception("Error when generating")
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER) queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
except Exception as e: except Exception as e:

@ -15,7 +15,7 @@ from core.model_manager import ModelInstance
from core.model_runtime.entities.llm_entities import LLMMode, LLMUsage from core.model_runtime.entities.llm_entities import LLMMode, LLMUsage
from core.model_runtime.entities.model_entities import ModelFeature, ModelPropertyKey from core.model_runtime.entities.model_entities import ModelFeature, ModelPropertyKey
from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
from core.moderation.base import ModerationException from core.moderation.base import ModerationError
from core.tools.entities.tool_entities import ToolRuntimeVariablePool from core.tools.entities.tool_entities import ToolRuntimeVariablePool
from extensions.ext_database import db from extensions.ext_database import db
from models.model import App, Conversation, Message, MessageAgentThought from models.model import App, Conversation, Message, MessageAgentThought
@ -30,7 +30,8 @@ class AgentChatAppRunner(AppRunner):
""" """
def run( def run(
self, application_generate_entity: AgentChatAppGenerateEntity, self,
application_generate_entity: AgentChatAppGenerateEntity,
queue_manager: AppQueueManager, queue_manager: AppQueueManager,
conversation: Conversation, conversation: Conversation,
message: Message, message: Message,
@ -65,7 +66,7 @@ class AgentChatAppRunner(AppRunner):
prompt_template_entity=app_config.prompt_template, prompt_template_entity=app_config.prompt_template,
inputs=inputs, inputs=inputs,
files=files, files=files,
query=query query=query,
) )
memory = None memory = None
@ -73,13 +74,10 @@ class AgentChatAppRunner(AppRunner):
# get memory of conversation (read-only) # get memory of conversation (read-only)
model_instance = ModelInstance( model_instance = ModelInstance(
provider_model_bundle=application_generate_entity.model_conf.provider_model_bundle, provider_model_bundle=application_generate_entity.model_conf.provider_model_bundle,
model=application_generate_entity.model_conf.model model=application_generate_entity.model_conf.model,
) )
memory = TokenBufferMemory( memory = TokenBufferMemory(conversation=conversation, model_instance=model_instance)
conversation=conversation,
model_instance=model_instance
)
# organize all inputs and template to prompt messages # organize all inputs and template to prompt messages
# Include: prompt template, inputs, query(optional), files(optional) # Include: prompt template, inputs, query(optional), files(optional)
@ -91,7 +89,7 @@ class AgentChatAppRunner(AppRunner):
inputs=inputs, inputs=inputs,
files=files, files=files,
query=query, query=query,
memory=memory memory=memory,
) )
# moderation # moderation
@ -103,15 +101,15 @@ class AgentChatAppRunner(AppRunner):
app_generate_entity=application_generate_entity, app_generate_entity=application_generate_entity,
inputs=inputs, inputs=inputs,
query=query, query=query,
message_id=message.id message_id=message.id,
) )
except ModerationException as e: except ModerationError as e:
self.direct_output( self.direct_output(
queue_manager=queue_manager, queue_manager=queue_manager,
app_generate_entity=application_generate_entity, app_generate_entity=application_generate_entity,
prompt_messages=prompt_messages, prompt_messages=prompt_messages,
text=str(e), text=str(e),
stream=application_generate_entity.stream stream=application_generate_entity.stream,
) )
return return
@ -122,13 +120,13 @@ class AgentChatAppRunner(AppRunner):
message=message, message=message,
query=query, query=query,
user_id=application_generate_entity.user_id, user_id=application_generate_entity.user_id,
invoke_from=application_generate_entity.invoke_from invoke_from=application_generate_entity.invoke_from,
) )
if annotation_reply: if annotation_reply:
queue_manager.publish( queue_manager.publish(
QueueAnnotationReplyEvent(message_annotation_id=annotation_reply.id), QueueAnnotationReplyEvent(message_annotation_id=annotation_reply.id),
PublishFrom.APPLICATION_MANAGER PublishFrom.APPLICATION_MANAGER,
) )
self.direct_output( self.direct_output(
@ -136,7 +134,7 @@ class AgentChatAppRunner(AppRunner):
app_generate_entity=application_generate_entity, app_generate_entity=application_generate_entity,
prompt_messages=prompt_messages, prompt_messages=prompt_messages,
text=annotation_reply.content, text=annotation_reply.content,
stream=application_generate_entity.stream stream=application_generate_entity.stream,
) )
return return
@ -148,7 +146,7 @@ class AgentChatAppRunner(AppRunner):
app_id=app_record.id, app_id=app_record.id,
external_data_tools=external_data_tools, external_data_tools=external_data_tools,
inputs=inputs, inputs=inputs,
query=query query=query,
) )
# reorganize all inputs and template to prompt messages # reorganize all inputs and template to prompt messages
@ -161,14 +159,14 @@ class AgentChatAppRunner(AppRunner):
inputs=inputs, inputs=inputs,
files=files, files=files,
query=query, query=query,
memory=memory memory=memory,
) )
# check hosting moderation # check hosting moderation
hosting_moderation_result = self.check_hosting_moderation( hosting_moderation_result = self.check_hosting_moderation(
application_generate_entity=application_generate_entity, application_generate_entity=application_generate_entity,
queue_manager=queue_manager, queue_manager=queue_manager,
prompt_messages=prompt_messages prompt_messages=prompt_messages,
) )
if hosting_moderation_result: if hosting_moderation_result:
@ -177,9 +175,9 @@ class AgentChatAppRunner(AppRunner):
agent_entity = app_config.agent agent_entity = app_config.agent
# load tool variables # load tool variables
tool_conversation_variables = self._load_tool_variables(conversation_id=conversation.id, tool_conversation_variables = self._load_tool_variables(
user_id=application_generate_entity.user_id, conversation_id=conversation.id, user_id=application_generate_entity.user_id, tenant_id=app_config.tenant_id
tenant_id=app_config.tenant_id) )
# convert db variables to tool variables # convert db variables to tool variables
tool_variables = self._convert_db_variables_to_tool_variables(tool_conversation_variables) tool_variables = self._convert_db_variables_to_tool_variables(tool_conversation_variables)
@ -187,7 +185,7 @@ class AgentChatAppRunner(AppRunner):
# init model instance # init model instance
model_instance = ModelInstance( model_instance = ModelInstance(
provider_model_bundle=application_generate_entity.model_conf.provider_model_bundle, provider_model_bundle=application_generate_entity.model_conf.provider_model_bundle,
model=application_generate_entity.model_conf.model model=application_generate_entity.model_conf.model,
) )
prompt_message, _ = self.organize_prompt_messages( prompt_message, _ = self.organize_prompt_messages(
app_record=app_record, app_record=app_record,
@ -238,7 +236,7 @@ class AgentChatAppRunner(AppRunner):
prompt_messages=prompt_message, prompt_messages=prompt_message,
variables_pool=tool_variables, variables_pool=tool_variables,
db_variables=tool_conversation_variables, db_variables=tool_conversation_variables,
model_instance=model_instance model_instance=model_instance,
) )
invoke_result = runner.run( invoke_result = runner.run(
@ -252,17 +250,21 @@ class AgentChatAppRunner(AppRunner):
invoke_result=invoke_result, invoke_result=invoke_result,
queue_manager=queue_manager, queue_manager=queue_manager,
stream=application_generate_entity.stream, stream=application_generate_entity.stream,
agent=True agent=True,
) )
def _load_tool_variables(self, conversation_id: str, user_id: str, tenant_id: str) -> ToolConversationVariables: def _load_tool_variables(self, conversation_id: str, user_id: str, tenant_id: str) -> ToolConversationVariables:
""" """
load tool variables from database load tool variables from database
""" """
tool_variables: ToolConversationVariables = db.session.query(ToolConversationVariables).filter( tool_variables: ToolConversationVariables = (
ToolConversationVariables.conversation_id == conversation_id, db.session.query(ToolConversationVariables)
ToolConversationVariables.tenant_id == tenant_id .filter(
).first() ToolConversationVariables.conversation_id == conversation_id,
ToolConversationVariables.tenant_id == tenant_id,
)
.first()
)
if tool_variables: if tool_variables:
# save tool variables to session, so that we can update it later # save tool variables to session, so that we can update it later
@ -273,34 +275,40 @@ class AgentChatAppRunner(AppRunner):
conversation_id=conversation_id, conversation_id=conversation_id,
user_id=user_id, user_id=user_id,
tenant_id=tenant_id, tenant_id=tenant_id,
variables_str='[]', variables_str="[]",
) )
db.session.add(tool_variables) db.session.add(tool_variables)
db.session.commit() db.session.commit()
return tool_variables return tool_variables
def _convert_db_variables_to_tool_variables(self, db_variables: ToolConversationVariables) -> ToolRuntimeVariablePool: def _convert_db_variables_to_tool_variables(
self, db_variables: ToolConversationVariables
) -> ToolRuntimeVariablePool:
""" """
convert db variables to tool variables convert db variables to tool variables
""" """
return ToolRuntimeVariablePool(**{ return ToolRuntimeVariablePool(
'conversation_id': db_variables.conversation_id, **{
'user_id': db_variables.user_id, "conversation_id": db_variables.conversation_id,
'tenant_id': db_variables.tenant_id, "user_id": db_variables.user_id,
'pool': db_variables.variables "tenant_id": db_variables.tenant_id,
}) "pool": db_variables.variables,
}
def _get_usage_of_all_agent_thoughts(self, model_config: ModelConfigWithCredentialsEntity, )
message: Message) -> LLMUsage:
def _get_usage_of_all_agent_thoughts(
self, model_config: ModelConfigWithCredentialsEntity, message: Message
) -> LLMUsage:
""" """
Get usage of all agent thoughts Get usage of all agent thoughts
:param model_config: model config :param model_config: model config
:param message: message :param message: message
:return: :return:
""" """
agent_thoughts = (db.session.query(MessageAgentThought) agent_thoughts = (
.filter(MessageAgentThought.message_id == message.id).all()) db.session.query(MessageAgentThought).filter(MessageAgentThought.message_id == message.id).all()
)
all_message_tokens = 0 all_message_tokens = 0
all_answer_tokens = 0 all_answer_tokens = 0
@ -312,8 +320,5 @@ class AgentChatAppRunner(AppRunner):
model_type_instance = cast(LargeLanguageModel, model_type_instance) model_type_instance = cast(LargeLanguageModel, model_type_instance)
return model_type_instance._calc_response_usage( return model_type_instance._calc_response_usage(
model_config.model, model_config.model, model_config.credentials, all_message_tokens, all_answer_tokens
model_config.credentials,
all_message_tokens,
all_answer_tokens
) )

@ -23,15 +23,15 @@ class AgentChatAppGenerateResponseConverter(AppGenerateResponseConverter):
:return: :return:
""" """
response = { response = {
'event': 'message', "event": "message",
'task_id': blocking_response.task_id, "task_id": blocking_response.task_id,
'id': blocking_response.data.id, "id": blocking_response.data.id,
'message_id': blocking_response.data.message_id, "message_id": blocking_response.data.message_id,
'conversation_id': blocking_response.data.conversation_id, "conversation_id": blocking_response.data.conversation_id,
'mode': blocking_response.data.mode, "mode": blocking_response.data.mode,
'answer': blocking_response.data.answer, "answer": blocking_response.data.answer,
'metadata': blocking_response.data.metadata, "metadata": blocking_response.data.metadata,
'created_at': blocking_response.data.created_at "created_at": blocking_response.data.created_at,
} }
return response return response
@ -45,14 +45,15 @@ class AgentChatAppGenerateResponseConverter(AppGenerateResponseConverter):
""" """
response = cls.convert_blocking_full_response(blocking_response) response = cls.convert_blocking_full_response(blocking_response)
metadata = response.get('metadata', {}) metadata = response.get("metadata", {})
response['metadata'] = cls._get_simple_metadata(metadata) response["metadata"] = cls._get_simple_metadata(metadata)
return response return response
@classmethod @classmethod
def convert_stream_full_response(cls, stream_response: Generator[ChatbotAppStreamResponse, None, None]) \ def convert_stream_full_response(
-> Generator[str, None, None]: cls, stream_response: Generator[ChatbotAppStreamResponse, None, None]
) -> Generator[str, None, None]:
""" """
Convert stream full response. Convert stream full response.
:param stream_response: stream response :param stream_response: stream response
@ -63,14 +64,14 @@ class AgentChatAppGenerateResponseConverter(AppGenerateResponseConverter):
sub_stream_response = chunk.stream_response sub_stream_response = chunk.stream_response
if isinstance(sub_stream_response, PingStreamResponse): if isinstance(sub_stream_response, PingStreamResponse):
yield 'ping' yield "ping"
continue continue
response_chunk = { response_chunk = {
'event': sub_stream_response.event.value, "event": sub_stream_response.event.value,
'conversation_id': chunk.conversation_id, "conversation_id": chunk.conversation_id,
'message_id': chunk.message_id, "message_id": chunk.message_id,
'created_at': chunk.created_at "created_at": chunk.created_at,
} }
if isinstance(sub_stream_response, ErrorStreamResponse): if isinstance(sub_stream_response, ErrorStreamResponse):
@ -81,8 +82,9 @@ class AgentChatAppGenerateResponseConverter(AppGenerateResponseConverter):
yield json.dumps(response_chunk) yield json.dumps(response_chunk)
@classmethod @classmethod
def convert_stream_simple_response(cls, stream_response: Generator[ChatbotAppStreamResponse, None, None]) \ def convert_stream_simple_response(
-> Generator[str, None, None]: cls, stream_response: Generator[ChatbotAppStreamResponse, None, None]
) -> Generator[str, None, None]:
""" """
Convert stream simple response. Convert stream simple response.
:param stream_response: stream response :param stream_response: stream response
@ -93,20 +95,20 @@ class AgentChatAppGenerateResponseConverter(AppGenerateResponseConverter):
sub_stream_response = chunk.stream_response sub_stream_response = chunk.stream_response
if isinstance(sub_stream_response, PingStreamResponse): if isinstance(sub_stream_response, PingStreamResponse):
yield 'ping' yield "ping"
continue continue
response_chunk = { response_chunk = {
'event': sub_stream_response.event.value, "event": sub_stream_response.event.value,
'conversation_id': chunk.conversation_id, "conversation_id": chunk.conversation_id,
'message_id': chunk.message_id, "message_id": chunk.message_id,
'created_at': chunk.created_at "created_at": chunk.created_at,
} }
if isinstance(sub_stream_response, MessageEndStreamResponse): if isinstance(sub_stream_response, MessageEndStreamResponse):
sub_stream_response_dict = sub_stream_response.to_dict() sub_stream_response_dict = sub_stream_response.to_dict()
metadata = sub_stream_response_dict.get('metadata', {}) metadata = sub_stream_response_dict.get("metadata", {})
sub_stream_response_dict['metadata'] = cls._get_simple_metadata(metadata) sub_stream_response_dict["metadata"] = cls._get_simple_metadata(metadata)
response_chunk.update(sub_stream_response_dict) response_chunk.update(sub_stream_response_dict)
if isinstance(sub_stream_response, ErrorStreamResponse): if isinstance(sub_stream_response, ErrorStreamResponse):
data = cls._error_to_stream_response(sub_stream_response.err) data = cls._error_to_stream_response(sub_stream_response.err)

@ -13,32 +13,33 @@ class AppGenerateResponseConverter(ABC):
_blocking_response_type: type[AppBlockingResponse] _blocking_response_type: type[AppBlockingResponse]
@classmethod @classmethod
def convert(cls, response: Union[ def convert(
AppBlockingResponse, cls, response: Union[AppBlockingResponse, Generator[AppStreamResponse, Any, None]], invoke_from: InvokeFrom
Generator[AppStreamResponse, Any, None] ) -> dict[str, Any] | Generator[str, Any, None]:
], invoke_from: InvokeFrom):
if invoke_from in [InvokeFrom.DEBUGGER, InvokeFrom.SERVICE_API]: if invoke_from in [InvokeFrom.DEBUGGER, InvokeFrom.SERVICE_API]:
if isinstance(response, AppBlockingResponse): if isinstance(response, AppBlockingResponse):
return cls.convert_blocking_full_response(response) return cls.convert_blocking_full_response(response)
else: else:
def _generate_full_response() -> Generator[str, Any, None]: def _generate_full_response() -> Generator[str, Any, None]:
for chunk in cls.convert_stream_full_response(response): for chunk in cls.convert_stream_full_response(response):
if chunk == 'ping': if chunk == "ping":
yield f'event: {chunk}\n\n' yield f"event: {chunk}\n\n"
else: else:
yield f'data: {chunk}\n\n' yield f"data: {chunk}\n\n"
return _generate_full_response() return _generate_full_response()
else: else:
if isinstance(response, AppBlockingResponse): if isinstance(response, AppBlockingResponse):
return cls.convert_blocking_simple_response(response) return cls.convert_blocking_simple_response(response)
else: else:
def _generate_simple_response() -> Generator[str, Any, None]: def _generate_simple_response() -> Generator[str, Any, None]:
for chunk in cls.convert_stream_simple_response(response): for chunk in cls.convert_stream_simple_response(response):
if chunk == 'ping': if chunk == "ping":
yield f'event: {chunk}\n\n' yield f"event: {chunk}\n\n"
else: else:
yield f'data: {chunk}\n\n' yield f"data: {chunk}\n\n"
return _generate_simple_response() return _generate_simple_response()
@ -54,14 +55,16 @@ class AppGenerateResponseConverter(ABC):
@classmethod @classmethod
@abstractmethod @abstractmethod
def convert_stream_full_response(cls, stream_response: Generator[AppStreamResponse, None, None]) \ def convert_stream_full_response(
-> Generator[str, None, None]: cls, stream_response: Generator[AppStreamResponse, None, None]
) -> Generator[str, None, None]:
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
@abstractmethod @abstractmethod
def convert_stream_simple_response(cls, stream_response: Generator[AppStreamResponse, None, None]) \ def convert_stream_simple_response(
-> Generator[str, None, None]: cls, stream_response: Generator[AppStreamResponse, None, None]
) -> Generator[str, None, None]:
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
@ -72,24 +75,26 @@ class AppGenerateResponseConverter(ABC):
:return: :return:
""" """
# show_retrieve_source # show_retrieve_source
if 'retriever_resources' in metadata: if "retriever_resources" in metadata:
metadata['retriever_resources'] = [] metadata["retriever_resources"] = []
for resource in metadata['retriever_resources']: for resource in metadata["retriever_resources"]:
metadata['retriever_resources'].append({ metadata["retriever_resources"].append(
'segment_id': resource['segment_id'], {
'position': resource['position'], "segment_id": resource["segment_id"],
'document_name': resource['document_name'], "position": resource["position"],
'score': resource['score'], "document_name": resource["document_name"],
'content': resource['content'], "score": resource["score"],
}) "content": resource["content"],
}
)
# show annotation reply # show annotation reply
if 'annotation_reply' in metadata: if "annotation_reply" in metadata:
del metadata['annotation_reply'] del metadata["annotation_reply"]
# show usage # show usage
if 'usage' in metadata: if "usage" in metadata:
del metadata['usage'] del metadata["usage"]
return metadata return metadata
@ -101,16 +106,16 @@ class AppGenerateResponseConverter(ABC):
:return: :return:
""" """
error_responses = { error_responses = {
ValueError: {'code': 'invalid_param', 'status': 400}, ValueError: {"code": "invalid_param", "status": 400},
ProviderTokenNotInitError: {'code': 'provider_not_initialize', 'status': 400}, ProviderTokenNotInitError: {"code": "provider_not_initialize", "status": 400},
QuotaExceededError: { QuotaExceededError: {
'code': 'provider_quota_exceeded', "code": "provider_quota_exceeded",
'message': "Your quota for Dify Hosted Model Provider has been exhausted. " "message": "Your quota for Dify Hosted Model Provider has been exhausted. "
"Please go to Settings -> Model Provider to complete your own provider credentials.", "Please go to Settings -> Model Provider to complete your own provider credentials.",
'status': 400 "status": 400,
}, },
ModelCurrentlyNotSupportError: {'code': 'model_currently_not_support', 'status': 400}, ModelCurrentlyNotSupportError: {"code": "model_currently_not_support", "status": 400},
InvokeError: {'code': 'completion_request_error', 'status': 400} InvokeError: {"code": "completion_request_error", "status": 400},
} }
# Determine the response based on the type of exception # Determine the response based on the type of exception
@ -120,13 +125,13 @@ class AppGenerateResponseConverter(ABC):
data = v data = v
if data: if data:
data.setdefault('message', getattr(e, 'description', str(e))) data.setdefault("message", getattr(e, "description", str(e)))
else: else:
logging.error(e) logging.error(e)
data = { data = {
'code': 'internal_server_error', "code": "internal_server_error",
'message': 'Internal Server Error, please contact support.', "message": "Internal Server Error, please contact support.",
'status': 500 "status": 500,
} }
return data return data

@ -16,10 +16,10 @@ class BaseAppGenerator:
def _validate_input(self, *, inputs: Mapping[str, Any], var: VariableEntity): def _validate_input(self, *, inputs: Mapping[str, Any], var: VariableEntity):
user_input_value = inputs.get(var.variable) user_input_value = inputs.get(var.variable)
if var.required and not user_input_value: if var.required and not user_input_value:
raise ValueError(f'{var.variable} is required in input form') raise ValueError(f"{var.variable} is required in input form")
if not var.required and not user_input_value: if not var.required and not user_input_value:
# TODO: should we return None here if the default value is None? # TODO: should we return None here if the default value is None?
return var.default or '' return var.default or ""
if ( if (
var.type var.type
in ( in (
@ -34,7 +34,7 @@ class BaseAppGenerator:
if var.type == VariableEntityType.NUMBER and isinstance(user_input_value, str): if var.type == VariableEntityType.NUMBER and isinstance(user_input_value, str):
# may raise ValueError if user_input_value is not a valid number # may raise ValueError if user_input_value is not a valid number
try: try:
if '.' in user_input_value: if "." in user_input_value:
return float(user_input_value) return float(user_input_value)
else: else:
return int(user_input_value) return int(user_input_value)
@ -43,14 +43,14 @@ class BaseAppGenerator:
if var.type == VariableEntityType.SELECT: if var.type == VariableEntityType.SELECT:
options = var.options or [] options = var.options or []
if user_input_value not in options: if user_input_value not in options:
raise ValueError(f'{var.variable} in input form must be one of the following: {options}') raise ValueError(f"{var.variable} in input form must be one of the following: {options}")
elif var.type in (VariableEntityType.TEXT_INPUT, VariableEntityType.PARAGRAPH): elif var.type in (VariableEntityType.TEXT_INPUT, VariableEntityType.PARAGRAPH):
if var.max_length and user_input_value and len(user_input_value) > var.max_length: if var.max_length and user_input_value and len(user_input_value) > var.max_length:
raise ValueError(f'{var.variable} in input form must be less than {var.max_length} characters') raise ValueError(f"{var.variable} in input form must be less than {var.max_length} characters")
return user_input_value return user_input_value
def _sanitize_value(self, value: Any) -> Any: def _sanitize_value(self, value: Any) -> Any:
if isinstance(value, str): if isinstance(value, str):
return value.replace('\x00', '') return value.replace("\x00", "")
return value return value

@ -24,9 +24,7 @@ class PublishFrom(Enum):
class AppQueueManager: class AppQueueManager:
def __init__(self, task_id: str, def __init__(self, task_id: str, user_id: str, invoke_from: InvokeFrom) -> None:
user_id: str,
invoke_from: InvokeFrom) -> None:
if not user_id: if not user_id:
raise ValueError("user is required") raise ValueError("user is required")
@ -34,9 +32,10 @@ class AppQueueManager:
self._user_id = user_id self._user_id = user_id
self._invoke_from = invoke_from self._invoke_from = invoke_from
user_prefix = 'account' if self._invoke_from in [InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER] else 'end-user' user_prefix = "account" if self._invoke_from in [InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER] else "end-user"
redis_client.setex(AppQueueManager._generate_task_belong_cache_key(self._task_id), 1800, redis_client.setex(
f"{user_prefix}-{self._user_id}") AppQueueManager._generate_task_belong_cache_key(self._task_id), 1800, f"{user_prefix}-{self._user_id}"
)
q = queue.Queue() q = queue.Queue()
@ -66,8 +65,7 @@ class AppQueueManager:
# publish two messages to make sure the client can receive the stop signal # publish two messages to make sure the client can receive the stop signal
# and stop listening after the stop signal processed # and stop listening after the stop signal processed
self.publish( self.publish(
QueueStopEvent(stopped_by=QueueStopEvent.StopBy.USER_MANUAL), QueueStopEvent(stopped_by=QueueStopEvent.StopBy.USER_MANUAL), PublishFrom.TASK_PIPELINE
PublishFrom.TASK_PIPELINE
) )
if elapsed_time // 10 > last_ping_time: if elapsed_time // 10 > last_ping_time:
@ -88,9 +86,7 @@ class AppQueueManager:
:param pub_from: publish from :param pub_from: publish from
:return: :return:
""" """
self.publish(QueueErrorEvent( self.publish(QueueErrorEvent(error=e), pub_from)
error=e
), pub_from)
def publish(self, event: AppQueueEvent, pub_from: PublishFrom) -> None: def publish(self, event: AppQueueEvent, pub_from: PublishFrom) -> None:
""" """
@ -122,8 +118,8 @@ class AppQueueManager:
if result is None: if result is None:
return return
user_prefix = 'account' if invoke_from in [InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER] else 'end-user' user_prefix = "account" if invoke_from in [InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER] else "end-user"
if result.decode('utf-8') != f"{user_prefix}-{user_id}": if result.decode("utf-8") != f"{user_prefix}-{user_id}":
return return
stopped_cache_key = cls._generate_stopped_cache_key(task_id) stopped_cache_key = cls._generate_stopped_cache_key(task_id)
@ -168,10 +164,12 @@ class AppQueueManager:
for item in data: for item in data:
self._check_for_sqlalchemy_models(item) self._check_for_sqlalchemy_models(item)
else: else:
if isinstance(data, DeclarativeMeta) or hasattr(data, '_sa_instance_state'): if isinstance(data, DeclarativeMeta) or hasattr(data, "_sa_instance_state"):
raise TypeError("Critical Error: Passing SQLAlchemy Model instances " raise TypeError(
"that cause thread safety issues is not allowed.") "Critical Error: Passing SQLAlchemy Model instances "
"that cause thread safety issues is not allowed."
)
class GenerateTaskStoppedException(Exception): class GenerateTaskStoppedError(Exception):
pass pass

@ -1,6 +1,6 @@
import time import time
from collections.abc import Generator from collections.abc import Generator, Mapping
from typing import TYPE_CHECKING, Optional, Union from typing import TYPE_CHECKING, Any, Optional, Union
from core.app.app_config.entities import ExternalDataVariableEntity, PromptTemplateEntity from core.app.app_config.entities import ExternalDataVariableEntity, PromptTemplateEntity
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
@ -31,12 +31,15 @@ if TYPE_CHECKING:
class AppRunner: class AppRunner:
def get_pre_calculate_rest_tokens(self, app_record: App, def get_pre_calculate_rest_tokens(
model_config: ModelConfigWithCredentialsEntity, self,
prompt_template_entity: PromptTemplateEntity, app_record: App,
inputs: dict[str, str], model_config: ModelConfigWithCredentialsEntity,
files: list["FileVar"], prompt_template_entity: PromptTemplateEntity,
query: Optional[str] = None) -> int: inputs: dict[str, str],
files: list["FileVar"],
query: Optional[str] = None,
) -> int:
""" """
Get pre calculate rest tokens Get pre calculate rest tokens
:param app_record: app record :param app_record: app record
@ -49,18 +52,20 @@ class AppRunner:
""" """
# Invoke model # Invoke model
model_instance = ModelInstance( model_instance = ModelInstance(
provider_model_bundle=model_config.provider_model_bundle, provider_model_bundle=model_config.provider_model_bundle, model=model_config.model
model=model_config.model
) )
model_context_tokens = model_config.model_schema.model_properties.get(ModelPropertyKey.CONTEXT_SIZE) model_context_tokens = model_config.model_schema.model_properties.get(ModelPropertyKey.CONTEXT_SIZE)
max_tokens = 0 max_tokens = 0
for parameter_rule in model_config.model_schema.parameter_rules: for parameter_rule in model_config.model_schema.parameter_rules:
if (parameter_rule.name == 'max_tokens' if parameter_rule.name == "max_tokens" or (
or (parameter_rule.use_template and parameter_rule.use_template == 'max_tokens')): parameter_rule.use_template and parameter_rule.use_template == "max_tokens"
max_tokens = (model_config.parameters.get(parameter_rule.name) ):
or model_config.parameters.get(parameter_rule.use_template)) or 0 max_tokens = (
model_config.parameters.get(parameter_rule.name)
or model_config.parameters.get(parameter_rule.use_template)
) or 0
if model_context_tokens is None: if model_context_tokens is None:
return -1 return -1
@ -75,36 +80,39 @@ class AppRunner:
prompt_template_entity=prompt_template_entity, prompt_template_entity=prompt_template_entity,
inputs=inputs, inputs=inputs,
files=files, files=files,
query=query query=query,
) )
prompt_tokens = model_instance.get_llm_num_tokens( prompt_tokens = model_instance.get_llm_num_tokens(prompt_messages)
prompt_messages
)
rest_tokens = model_context_tokens - max_tokens - prompt_tokens rest_tokens = model_context_tokens - max_tokens - prompt_tokens
if rest_tokens < 0: if rest_tokens < 0:
raise InvokeBadRequestError("Query or prefix prompt is too long, you can reduce the prefix prompt, " raise InvokeBadRequestError(
"or shrink the max token, or switch to a llm with a larger token limit size.") "Query or prefix prompt is too long, you can reduce the prefix prompt, "
"or shrink the max token, or switch to a llm with a larger token limit size."
)
return rest_tokens return rest_tokens
def recalc_llm_max_tokens(self, model_config: ModelConfigWithCredentialsEntity, def recalc_llm_max_tokens(
prompt_messages: list[PromptMessage]): self, model_config: ModelConfigWithCredentialsEntity, prompt_messages: list[PromptMessage]
):
# recalc max_tokens if sum(prompt_token + max_tokens) over model token limit # recalc max_tokens if sum(prompt_token + max_tokens) over model token limit
model_instance = ModelInstance( model_instance = ModelInstance(
provider_model_bundle=model_config.provider_model_bundle, provider_model_bundle=model_config.provider_model_bundle, model=model_config.model
model=model_config.model
) )
model_context_tokens = model_config.model_schema.model_properties.get(ModelPropertyKey.CONTEXT_SIZE) model_context_tokens = model_config.model_schema.model_properties.get(ModelPropertyKey.CONTEXT_SIZE)
max_tokens = 0 max_tokens = 0
for parameter_rule in model_config.model_schema.parameter_rules: for parameter_rule in model_config.model_schema.parameter_rules:
if (parameter_rule.name == 'max_tokens' if parameter_rule.name == "max_tokens" or (
or (parameter_rule.use_template and parameter_rule.use_template == 'max_tokens')): parameter_rule.use_template and parameter_rule.use_template == "max_tokens"
max_tokens = (model_config.parameters.get(parameter_rule.name) ):
or model_config.parameters.get(parameter_rule.use_template)) or 0 max_tokens = (
model_config.parameters.get(parameter_rule.name)
or model_config.parameters.get(parameter_rule.use_template)
) or 0
if model_context_tokens is None: if model_context_tokens is None:
return -1 return -1
@ -112,27 +120,28 @@ class AppRunner:
if max_tokens is None: if max_tokens is None:
max_tokens = 0 max_tokens = 0
prompt_tokens = model_instance.get_llm_num_tokens( prompt_tokens = model_instance.get_llm_num_tokens(prompt_messages)
prompt_messages
)
if prompt_tokens + max_tokens > model_context_tokens: if prompt_tokens + max_tokens > model_context_tokens:
max_tokens = max(model_context_tokens - prompt_tokens, 16) max_tokens = max(model_context_tokens - prompt_tokens, 16)
for parameter_rule in model_config.model_schema.parameter_rules: for parameter_rule in model_config.model_schema.parameter_rules:
if (parameter_rule.name == 'max_tokens' if parameter_rule.name == "max_tokens" or (
or (parameter_rule.use_template and parameter_rule.use_template == 'max_tokens')): parameter_rule.use_template and parameter_rule.use_template == "max_tokens"
):
model_config.parameters[parameter_rule.name] = max_tokens model_config.parameters[parameter_rule.name] = max_tokens
def organize_prompt_messages(self, app_record: App, def organize_prompt_messages(
model_config: ModelConfigWithCredentialsEntity, self,
prompt_template_entity: PromptTemplateEntity, app_record: App,
inputs: dict[str, str], model_config: ModelConfigWithCredentialsEntity,
files: list["FileVar"], prompt_template_entity: PromptTemplateEntity,
query: Optional[str] = None, inputs: dict[str, str],
context: Optional[str] = None, files: list["FileVar"],
memory: Optional[TokenBufferMemory] = None) \ query: Optional[str] = None,
-> tuple[list[PromptMessage], Optional[list[str]]]: context: Optional[str] = None,
memory: Optional[TokenBufferMemory] = None,
) -> tuple[list[PromptMessage], Optional[list[str]]]:
""" """
Organize prompt messages Organize prompt messages
:param context: :param context:
@ -152,60 +161,54 @@ class AppRunner:
app_mode=AppMode.value_of(app_record.mode), app_mode=AppMode.value_of(app_record.mode),
prompt_template_entity=prompt_template_entity, prompt_template_entity=prompt_template_entity,
inputs=inputs, inputs=inputs,
query=query if query else '', query=query if query else "",
files=files, files=files,
context=context, context=context,
memory=memory, memory=memory,
model_config=model_config model_config=model_config,
) )
else: else:
memory_config = MemoryConfig( memory_config = MemoryConfig(window=MemoryConfig.WindowConfig(enabled=False))
window=MemoryConfig.WindowConfig(
enabled=False
)
)
model_mode = ModelMode.value_of(model_config.mode) model_mode = ModelMode.value_of(model_config.mode)
if model_mode == ModelMode.COMPLETION: if model_mode == ModelMode.COMPLETION:
advanced_completion_prompt_template = prompt_template_entity.advanced_completion_prompt_template advanced_completion_prompt_template = prompt_template_entity.advanced_completion_prompt_template
prompt_template = CompletionModelPromptTemplate( prompt_template = CompletionModelPromptTemplate(text=advanced_completion_prompt_template.prompt)
text=advanced_completion_prompt_template.prompt
)
if advanced_completion_prompt_template.role_prefix: if advanced_completion_prompt_template.role_prefix:
memory_config.role_prefix = MemoryConfig.RolePrefix( memory_config.role_prefix = MemoryConfig.RolePrefix(
user=advanced_completion_prompt_template.role_prefix.user, user=advanced_completion_prompt_template.role_prefix.user,
assistant=advanced_completion_prompt_template.role_prefix.assistant assistant=advanced_completion_prompt_template.role_prefix.assistant,
) )
else: else:
prompt_template = [] prompt_template = []
for message in prompt_template_entity.advanced_chat_prompt_template.messages: for message in prompt_template_entity.advanced_chat_prompt_template.messages:
prompt_template.append(ChatModelMessage( prompt_template.append(ChatModelMessage(text=message.text, role=message.role))
text=message.text,
role=message.role
))
prompt_transform = AdvancedPromptTransform() prompt_transform = AdvancedPromptTransform()
prompt_messages = prompt_transform.get_prompt( prompt_messages = prompt_transform.get_prompt(
prompt_template=prompt_template, prompt_template=prompt_template,
inputs=inputs, inputs=inputs,
query=query if query else '', query=query if query else "",
files=files, files=files,
context=context, context=context,
memory_config=memory_config, memory_config=memory_config,
memory=memory, memory=memory,
model_config=model_config model_config=model_config,
) )
stop = model_config.stop stop = model_config.stop
return prompt_messages, stop return prompt_messages, stop
def direct_output(self, queue_manager: AppQueueManager, def direct_output(
app_generate_entity: EasyUIBasedAppGenerateEntity, self,
prompt_messages: list, queue_manager: AppQueueManager,
text: str, app_generate_entity: EasyUIBasedAppGenerateEntity,
stream: bool, prompt_messages: list,
usage: Optional[LLMUsage] = None) -> None: text: str,
stream: bool,
usage: Optional[LLMUsage] = None,
) -> None:
""" """
Direct output Direct output
:param queue_manager: application queue manager :param queue_manager: application queue manager
@ -222,17 +225,10 @@ class AppRunner:
chunk = LLMResultChunk( chunk = LLMResultChunk(
model=app_generate_entity.model_conf.model, model=app_generate_entity.model_conf.model,
prompt_messages=prompt_messages, prompt_messages=prompt_messages,
delta=LLMResultChunkDelta( delta=LLMResultChunkDelta(index=index, message=AssistantPromptMessage(content=token)),
index=index,
message=AssistantPromptMessage(content=token)
)
) )
queue_manager.publish( queue_manager.publish(QueueLLMChunkEvent(chunk=chunk), PublishFrom.APPLICATION_MANAGER)
QueueLLMChunkEvent(
chunk=chunk
), PublishFrom.APPLICATION_MANAGER
)
index += 1 index += 1
time.sleep(0.01) time.sleep(0.01)
@ -242,15 +238,19 @@ class AppRunner:
model=app_generate_entity.model_conf.model, model=app_generate_entity.model_conf.model,
prompt_messages=prompt_messages, prompt_messages=prompt_messages,
message=AssistantPromptMessage(content=text), message=AssistantPromptMessage(content=text),
usage=usage if usage else LLMUsage.empty_usage() usage=usage if usage else LLMUsage.empty_usage(),
), ),
), PublishFrom.APPLICATION_MANAGER ),
PublishFrom.APPLICATION_MANAGER,
) )
def _handle_invoke_result(self, invoke_result: Union[LLMResult, Generator], def _handle_invoke_result(
queue_manager: AppQueueManager, self,
stream: bool, invoke_result: Union[LLMResult, Generator],
agent: bool = False) -> None: queue_manager: AppQueueManager,
stream: bool,
agent: bool = False,
) -> None:
""" """
Handle invoke result Handle invoke result
:param invoke_result: invoke result :param invoke_result: invoke result
@ -260,21 +260,13 @@ class AppRunner:
:return: :return:
""" """
if not stream: if not stream:
self._handle_invoke_result_direct( self._handle_invoke_result_direct(invoke_result=invoke_result, queue_manager=queue_manager, agent=agent)
invoke_result=invoke_result,
queue_manager=queue_manager,
agent=agent
)
else: else:
self._handle_invoke_result_stream( self._handle_invoke_result_stream(invoke_result=invoke_result, queue_manager=queue_manager, agent=agent)
invoke_result=invoke_result,
queue_manager=queue_manager,
agent=agent
)
def _handle_invoke_result_direct(self, invoke_result: LLMResult, def _handle_invoke_result_direct(
queue_manager: AppQueueManager, self, invoke_result: LLMResult, queue_manager: AppQueueManager, agent: bool
agent: bool) -> None: ) -> None:
""" """
Handle invoke result direct Handle invoke result direct
:param invoke_result: invoke result :param invoke_result: invoke result
@ -285,12 +277,13 @@ class AppRunner:
queue_manager.publish( queue_manager.publish(
QueueMessageEndEvent( QueueMessageEndEvent(
llm_result=invoke_result, llm_result=invoke_result,
), PublishFrom.APPLICATION_MANAGER ),
PublishFrom.APPLICATION_MANAGER,
) )
def _handle_invoke_result_stream(self, invoke_result: Generator, def _handle_invoke_result_stream(
queue_manager: AppQueueManager, self, invoke_result: Generator, queue_manager: AppQueueManager, agent: bool
agent: bool) -> None: ) -> None:
""" """
Handle invoke result Handle invoke result
:param invoke_result: invoke result :param invoke_result: invoke result
@ -300,21 +293,13 @@ class AppRunner:
""" """
model = None model = None
prompt_messages = [] prompt_messages = []
text = '' text = ""
usage = None usage = None
for result in invoke_result: for result in invoke_result:
if not agent: if not agent:
queue_manager.publish( queue_manager.publish(QueueLLMChunkEvent(chunk=result), PublishFrom.APPLICATION_MANAGER)
QueueLLMChunkEvent(
chunk=result
), PublishFrom.APPLICATION_MANAGER
)
else: else:
queue_manager.publish( queue_manager.publish(QueueAgentMessageEvent(chunk=result), PublishFrom.APPLICATION_MANAGER)
QueueAgentMessageEvent(
chunk=result
), PublishFrom.APPLICATION_MANAGER
)
text += result.delta.message.content text += result.delta.message.content
@ -331,25 +316,24 @@ class AppRunner:
usage = LLMUsage.empty_usage() usage = LLMUsage.empty_usage()
llm_result = LLMResult( llm_result = LLMResult(
model=model, model=model, prompt_messages=prompt_messages, message=AssistantPromptMessage(content=text), usage=usage
prompt_messages=prompt_messages,
message=AssistantPromptMessage(content=text),
usage=usage
) )
queue_manager.publish( queue_manager.publish(
QueueMessageEndEvent( QueueMessageEndEvent(
llm_result=llm_result, llm_result=llm_result,
), PublishFrom.APPLICATION_MANAGER ),
PublishFrom.APPLICATION_MANAGER,
) )
def moderation_for_inputs( def moderation_for_inputs(
self, app_id: str, self,
tenant_id: str, app_id: str,
app_generate_entity: AppGenerateEntity, tenant_id: str,
inputs: dict, app_generate_entity: AppGenerateEntity,
query: str, inputs: Mapping[str, Any],
message_id: str, query: str,
message_id: str,
) -> tuple[bool, dict, str]: ) -> tuple[bool, dict, str]:
""" """
Process sensitive_word_avoidance. Process sensitive_word_avoidance.
@ -367,14 +351,17 @@ class AppRunner:
tenant_id=tenant_id, tenant_id=tenant_id,
app_config=app_generate_entity.app_config, app_config=app_generate_entity.app_config,
inputs=inputs, inputs=inputs,
query=query if query else '', query=query if query else "",
message_id=message_id, message_id=message_id,
trace_manager=app_generate_entity.trace_manager trace_manager=app_generate_entity.trace_manager,
) )
def check_hosting_moderation(self, application_generate_entity: EasyUIBasedAppGenerateEntity, def check_hosting_moderation(
queue_manager: AppQueueManager, self,
prompt_messages: list[PromptMessage]) -> bool: application_generate_entity: EasyUIBasedAppGenerateEntity,
queue_manager: AppQueueManager,
prompt_messages: list[PromptMessage],
) -> bool:
""" """
Check hosting moderation Check hosting moderation
:param application_generate_entity: application generate entity :param application_generate_entity: application generate entity
@ -384,8 +371,7 @@ class AppRunner:
""" """
hosting_moderation_feature = HostingModerationFeature() hosting_moderation_feature = HostingModerationFeature()
moderation_result = hosting_moderation_feature.check( moderation_result = hosting_moderation_feature.check(
application_generate_entity=application_generate_entity, application_generate_entity=application_generate_entity, prompt_messages=prompt_messages
prompt_messages=prompt_messages
) )
if moderation_result: if moderation_result:
@ -393,18 +379,20 @@ class AppRunner:
queue_manager=queue_manager, queue_manager=queue_manager,
app_generate_entity=application_generate_entity, app_generate_entity=application_generate_entity,
prompt_messages=prompt_messages, prompt_messages=prompt_messages,
text="I apologize for any confusion, " \ text="I apologize for any confusion, " "but I'm an AI assistant to be helpful, harmless, and honest.",
"but I'm an AI assistant to be helpful, harmless, and honest.", stream=application_generate_entity.stream,
stream=application_generate_entity.stream
) )
return moderation_result return moderation_result
def fill_in_inputs_from_external_data_tools(self, tenant_id: str, def fill_in_inputs_from_external_data_tools(
app_id: str, self,
external_data_tools: list[ExternalDataVariableEntity], tenant_id: str,
inputs: dict, app_id: str,
query: str) -> dict: external_data_tools: list[ExternalDataVariableEntity],
inputs: dict,
query: str,
) -> dict:
""" """
Fill in variable inputs from external data tools if exists. Fill in variable inputs from external data tools if exists.
@ -417,18 +405,12 @@ class AppRunner:
""" """
external_data_fetch_feature = ExternalDataFetch() external_data_fetch_feature = ExternalDataFetch()
return external_data_fetch_feature.fetch( return external_data_fetch_feature.fetch(
tenant_id=tenant_id, tenant_id=tenant_id, app_id=app_id, external_data_tools=external_data_tools, inputs=inputs, query=query
app_id=app_id,
external_data_tools=external_data_tools,
inputs=inputs,
query=query
) )
def query_app_annotations_to_reply(self, app_record: App, def query_app_annotations_to_reply(
message: Message, self, app_record: App, message: Message, query: str, user_id: str, invoke_from: InvokeFrom
query: str, ) -> Optional[MessageAnnotation]:
user_id: str,
invoke_from: InvokeFrom) -> Optional[MessageAnnotation]:
""" """
Query app annotations to reply Query app annotations to reply
:param app_record: app record :param app_record: app record
@ -440,9 +422,5 @@ class AppRunner:
""" """
annotation_reply_feature = AnnotationReplyFeature() annotation_reply_feature = AnnotationReplyFeature()
return annotation_reply_feature.query( return annotation_reply_feature.query(
app_record=app_record, app_record=app_record, message=message, query=query, user_id=user_id, invoke_from=invoke_from
message=message,
query=query,
user_id=user_id,
invoke_from=invoke_from
) )

@ -22,15 +22,19 @@ class ChatAppConfig(EasyUIBasedAppConfig):
""" """
Chatbot App Config Entity. Chatbot App Config Entity.
""" """
pass pass
class ChatAppConfigManager(BaseAppConfigManager): class ChatAppConfigManager(BaseAppConfigManager):
@classmethod @classmethod
def get_app_config(cls, app_model: App, def get_app_config(
app_model_config: AppModelConfig, cls,
conversation: Optional[Conversation] = None, app_model: App,
override_config_dict: Optional[dict] = None) -> ChatAppConfig: app_model_config: AppModelConfig,
conversation: Optional[Conversation] = None,
override_config_dict: Optional[dict] = None,
) -> ChatAppConfig:
""" """
Convert app model config to chat app config Convert app model config to chat app config
:param app_model: app model :param app_model: app model
@ -51,7 +55,7 @@ class ChatAppConfigManager(BaseAppConfigManager):
config_dict = app_model_config_dict.copy() config_dict = app_model_config_dict.copy()
else: else:
if not override_config_dict: if not override_config_dict:
raise Exception('override_config_dict is required when config_from is ARGS') raise Exception("override_config_dict is required when config_from is ARGS")
config_dict = override_config_dict config_dict = override_config_dict
@ -63,19 +67,11 @@ class ChatAppConfigManager(BaseAppConfigManager):
app_model_config_from=config_from, app_model_config_from=config_from,
app_model_config_id=app_model_config.id, app_model_config_id=app_model_config.id,
app_model_config_dict=config_dict, app_model_config_dict=config_dict,
model=ModelConfigManager.convert( model=ModelConfigManager.convert(config=config_dict),
config=config_dict prompt_template=PromptTemplateConfigManager.convert(config=config_dict),
), sensitive_word_avoidance=SensitiveWordAvoidanceConfigManager.convert(config=config_dict),
prompt_template=PromptTemplateConfigManager.convert( dataset=DatasetConfigManager.convert(config=config_dict),
config=config_dict additional_features=cls.convert_features(config_dict, app_mode),
),
sensitive_word_avoidance=SensitiveWordAvoidanceConfigManager.convert(
config=config_dict
),
dataset=DatasetConfigManager.convert(
config=config_dict
),
additional_features=cls.convert_features(config_dict, app_mode)
) )
app_config.variables, app_config.external_data_variables = BasicVariablesConfigManager.convert( app_config.variables, app_config.external_data_variables = BasicVariablesConfigManager.convert(
@ -113,8 +109,9 @@ class ChatAppConfigManager(BaseAppConfigManager):
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)
# dataset_query_variable # dataset_query_variable
config, current_related_config_keys = DatasetConfigManager.validate_and_set_defaults(tenant_id, app_mode, config, current_related_config_keys = DatasetConfigManager.validate_and_set_defaults(
config) tenant_id, app_mode, config
)
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)
# opening_statement # opening_statement
@ -123,7 +120,8 @@ class ChatAppConfigManager(BaseAppConfigManager):
# suggested_questions_after_answer # suggested_questions_after_answer
config, current_related_config_keys = SuggestedQuestionsAfterAnswerConfigManager.validate_and_set_defaults( config, current_related_config_keys = SuggestedQuestionsAfterAnswerConfigManager.validate_and_set_defaults(
config) config
)
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)
# speech_to_text # speech_to_text
@ -139,8 +137,9 @@ class ChatAppConfigManager(BaseAppConfigManager):
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)
# moderation validation # moderation validation
config, current_related_config_keys = SensitiveWordAvoidanceConfigManager.validate_and_set_defaults(tenant_id, config, current_related_config_keys = SensitiveWordAvoidanceConfigManager.validate_and_set_defaults(
config) tenant_id, config
)
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)
related_config_keys = list(set(related_config_keys)) related_config_keys = list(set(related_config_keys))

@ -3,14 +3,14 @@ import os
import threading import threading
import uuid import uuid
from collections.abc import Generator from collections.abc import Generator
from typing import Any, Union from typing import Any, Literal, Union, overload
from flask import Flask, current_app from flask import Flask, current_app
from pydantic import ValidationError from pydantic import ValidationError
from core.app.app_config.easy_ui_based_app.model_config.converter import ModelConfigConverter from core.app.app_config.easy_ui_based_app.model_config.converter import ModelConfigConverter
from core.app.app_config.features.file_upload.manager import FileUploadConfigManager from core.app.app_config.features.file_upload.manager import FileUploadConfigManager
from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedException, PublishFrom from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedError, PublishFrom
from core.app.apps.chat.app_config_manager import ChatAppConfigManager from core.app.apps.chat.app_config_manager import ChatAppConfigManager
from core.app.apps.chat.app_runner import ChatAppRunner from core.app.apps.chat.app_runner import ChatAppRunner
from core.app.apps.chat.generate_response_converter import ChatAppGenerateResponseConverter from core.app.apps.chat.generate_response_converter import ChatAppGenerateResponseConverter
@ -28,13 +28,34 @@ logger = logging.getLogger(__name__)
class ChatAppGenerator(MessageBasedAppGenerator): class ChatAppGenerator(MessageBasedAppGenerator):
@overload
def generate( def generate(
self, app_model: App, self,
app_model: App,
user: Union[Account, EndUser],
args: Any,
invoke_from: InvokeFrom,
stream: Literal[True] = True,
) -> Generator[str, None, None]: ...
@overload
def generate(
self,
app_model: App,
user: Union[Account, EndUser],
args: Any,
invoke_from: InvokeFrom,
stream: Literal[False] = False,
) -> dict: ...
def generate(
self,
app_model: App,
user: Union[Account, EndUser], user: Union[Account, EndUser],
args: Any, args: Any,
invoke_from: InvokeFrom, invoke_from: InvokeFrom,
stream: bool = True, stream: bool = True,
) -> Union[dict, Generator[dict, None, None]]: ) -> Union[dict, Generator[str, None, None]]:
""" """
Generate App response. Generate App response.
@ -44,58 +65,46 @@ class ChatAppGenerator(MessageBasedAppGenerator):
:param invoke_from: invoke from source :param invoke_from: invoke from source
:param stream: is stream :param stream: is stream
""" """
if not args.get('query'): if not args.get("query"):
raise ValueError('query is required') raise ValueError("query is required")
query = args['query'] query = args["query"]
if not isinstance(query, str): if not isinstance(query, str):
raise ValueError('query must be a string') raise ValueError("query must be a string")
query = query.replace('\x00', '') query = query.replace("\x00", "")
inputs = args['inputs'] inputs = args["inputs"]
extras = { extras = {"auto_generate_conversation_name": args.get("auto_generate_name", True)}
"auto_generate_conversation_name": args.get('auto_generate_name', True)
}
# get conversation # get conversation
conversation = None conversation = None
if args.get('conversation_id'): if args.get("conversation_id"):
conversation = self._get_conversation_by_user(app_model, args.get('conversation_id'), user) conversation = self._get_conversation_by_user(app_model, args.get("conversation_id"), user)
# get app model config # get app model config
app_model_config = self._get_app_model_config( app_model_config = self._get_app_model_config(app_model=app_model, conversation=conversation)
app_model=app_model,
conversation=conversation
)
# validate override model config # validate override model config
override_model_config_dict = None override_model_config_dict = None
if args.get('model_config'): if args.get("model_config"):
if invoke_from != InvokeFrom.DEBUGGER: if invoke_from != InvokeFrom.DEBUGGER:
raise ValueError('Only in App debug mode can override model config') raise ValueError("Only in App debug mode can override model config")
# validate config # validate config
override_model_config_dict = ChatAppConfigManager.config_validate( override_model_config_dict = ChatAppConfigManager.config_validate(
tenant_id=app_model.tenant_id, tenant_id=app_model.tenant_id, config=args.get("model_config")
config=args.get('model_config')
) )
# always enable retriever resource in debugger mode # always enable retriever resource in debugger mode
override_model_config_dict["retriever_resource"] = { override_model_config_dict["retriever_resource"] = {"enabled": True}
"enabled": True
}
# parse files # parse files
files = args['files'] if args.get('files') else [] files = args["files"] if args.get("files") else []
message_file_parser = MessageFileParser(tenant_id=app_model.tenant_id, app_id=app_model.id) message_file_parser = MessageFileParser(tenant_id=app_model.tenant_id, app_id=app_model.id)
file_extra_config = FileUploadConfigManager.convert(override_model_config_dict or app_model_config.to_dict()) file_extra_config = FileUploadConfigManager.convert(override_model_config_dict or app_model_config.to_dict())
if file_extra_config: if file_extra_config:
file_objs = message_file_parser.validate_and_transform_files_arg( file_objs = message_file_parser.validate_and_transform_files_arg(files, file_extra_config, user)
files,
file_extra_config,
user
)
else: else:
file_objs = [] file_objs = []
@ -104,7 +113,7 @@ class ChatAppGenerator(MessageBasedAppGenerator):
app_model=app_model, app_model=app_model,
app_model_config=app_model_config, app_model_config=app_model_config,
conversation=conversation, conversation=conversation,
override_config_dict=override_model_config_dict override_config_dict=override_model_config_dict,
) )
# get tracing instance # get tracing instance
@ -123,14 +132,11 @@ class ChatAppGenerator(MessageBasedAppGenerator):
stream=stream, stream=stream,
invoke_from=invoke_from, invoke_from=invoke_from,
extras=extras, extras=extras,
trace_manager=trace_manager trace_manager=trace_manager,
) )
# init generate records # init generate records
( (conversation, message) = self._init_generate_records(application_generate_entity, conversation)
conversation,
message
) = self._init_generate_records(application_generate_entity, conversation)
# init queue manager # init queue manager
queue_manager = MessageBasedAppQueueManager( queue_manager = MessageBasedAppQueueManager(
@ -139,17 +145,20 @@ class ChatAppGenerator(MessageBasedAppGenerator):
invoke_from=application_generate_entity.invoke_from, invoke_from=application_generate_entity.invoke_from,
conversation_id=conversation.id, conversation_id=conversation.id,
app_mode=conversation.mode, app_mode=conversation.mode,
message_id=message.id message_id=message.id,
) )
# new thread # new thread
worker_thread = threading.Thread(target=self._generate_worker, kwargs={ worker_thread = threading.Thread(
'flask_app': current_app._get_current_object(), target=self._generate_worker,
'application_generate_entity': application_generate_entity, kwargs={
'queue_manager': queue_manager, "flask_app": current_app._get_current_object(),
'conversation_id': conversation.id, "application_generate_entity": application_generate_entity,
'message_id': message.id, "queue_manager": queue_manager,
}) "conversation_id": conversation.id,
"message_id": message.id,
},
)
worker_thread.start() worker_thread.start()
@ -163,16 +172,16 @@ class ChatAppGenerator(MessageBasedAppGenerator):
stream=stream, stream=stream,
) )
return ChatAppGenerateResponseConverter.convert( return ChatAppGenerateResponseConverter.convert(response=response, invoke_from=invoke_from)
response=response,
invoke_from=invoke_from
)
def _generate_worker(self, flask_app: Flask, def _generate_worker(
application_generate_entity: ChatAppGenerateEntity, self,
queue_manager: AppQueueManager, flask_app: Flask,
conversation_id: str, application_generate_entity: ChatAppGenerateEntity,
message_id: str) -> None: queue_manager: AppQueueManager,
conversation_id: str,
message_id: str,
) -> None:
""" """
Generate worker in a new thread. Generate worker in a new thread.
:param flask_app: Flask app :param flask_app: Flask app
@ -194,20 +203,19 @@ class ChatAppGenerator(MessageBasedAppGenerator):
application_generate_entity=application_generate_entity, application_generate_entity=application_generate_entity,
queue_manager=queue_manager, queue_manager=queue_manager,
conversation=conversation, conversation=conversation,
message=message message=message,
) )
except GenerateTaskStoppedException: except GenerateTaskStoppedError:
pass pass
except InvokeAuthorizationError: except InvokeAuthorizationError:
queue_manager.publish_error( queue_manager.publish_error(
InvokeAuthorizationError('Incorrect API key provided'), InvokeAuthorizationError("Incorrect API key provided"), PublishFrom.APPLICATION_MANAGER
PublishFrom.APPLICATION_MANAGER
) )
except ValidationError as e: except ValidationError as e:
logger.exception("Validation Error when generating") logger.exception("Validation Error when generating")
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER) queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
except (ValueError, InvokeError) as e: except (ValueError, InvokeError) as e:
if os.environ.get("DEBUG") and os.environ.get("DEBUG").lower() == 'true': if os.environ.get("DEBUG") and os.environ.get("DEBUG").lower() == "true":
logger.exception("Error when generating") logger.exception("Error when generating")
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER) queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
except Exception as e: except Exception as e:

@ -11,7 +11,7 @@ from core.app.entities.queue_entities import QueueAnnotationReplyEvent
from core.callback_handler.index_tool_callback_handler import DatasetIndexToolCallbackHandler from core.callback_handler.index_tool_callback_handler import DatasetIndexToolCallbackHandler
from core.memory.token_buffer_memory import TokenBufferMemory from core.memory.token_buffer_memory import TokenBufferMemory
from core.model_manager import ModelInstance from core.model_manager import ModelInstance
from core.moderation.base import ModerationException from core.moderation.base import ModerationError
from core.rag.retrieval.dataset_retrieval import DatasetRetrieval from core.rag.retrieval.dataset_retrieval import DatasetRetrieval
from extensions.ext_database import db from extensions.ext_database import db
from models.model import App, Conversation, Message from models.model import App, Conversation, Message
@ -24,10 +24,13 @@ class ChatAppRunner(AppRunner):
Chat Application Runner Chat Application Runner
""" """
def run(self, application_generate_entity: ChatAppGenerateEntity, def run(
queue_manager: AppQueueManager, self,
conversation: Conversation, application_generate_entity: ChatAppGenerateEntity,
message: Message) -> None: queue_manager: AppQueueManager,
conversation: Conversation,
message: Message,
) -> None:
""" """
Run application Run application
:param application_generate_entity: application generate entity :param application_generate_entity: application generate entity
@ -58,7 +61,7 @@ class ChatAppRunner(AppRunner):
prompt_template_entity=app_config.prompt_template, prompt_template_entity=app_config.prompt_template,
inputs=inputs, inputs=inputs,
files=files, files=files,
query=query query=query,
) )
memory = None memory = None
@ -66,13 +69,10 @@ class ChatAppRunner(AppRunner):
# get memory of conversation (read-only) # get memory of conversation (read-only)
model_instance = ModelInstance( model_instance = ModelInstance(
provider_model_bundle=application_generate_entity.model_conf.provider_model_bundle, provider_model_bundle=application_generate_entity.model_conf.provider_model_bundle,
model=application_generate_entity.model_conf.model model=application_generate_entity.model_conf.model,
) )
memory = TokenBufferMemory( memory = TokenBufferMemory(conversation=conversation, model_instance=model_instance)
conversation=conversation,
model_instance=model_instance
)
# organize all inputs and template to prompt messages # organize all inputs and template to prompt messages
# Include: prompt template, inputs, query(optional), files(optional) # Include: prompt template, inputs, query(optional), files(optional)
@ -84,7 +84,7 @@ class ChatAppRunner(AppRunner):
inputs=inputs, inputs=inputs,
files=files, files=files,
query=query, query=query,
memory=memory memory=memory,
) )
# moderation # moderation
@ -96,15 +96,15 @@ class ChatAppRunner(AppRunner):
app_generate_entity=application_generate_entity, app_generate_entity=application_generate_entity,
inputs=inputs, inputs=inputs,
query=query, query=query,
message_id=message.id message_id=message.id,
) )
except ModerationException as e: except ModerationError as e:
self.direct_output( self.direct_output(
queue_manager=queue_manager, queue_manager=queue_manager,
app_generate_entity=application_generate_entity, app_generate_entity=application_generate_entity,
prompt_messages=prompt_messages, prompt_messages=prompt_messages,
text=str(e), text=str(e),
stream=application_generate_entity.stream stream=application_generate_entity.stream,
) )
return return
@ -115,13 +115,13 @@ class ChatAppRunner(AppRunner):
message=message, message=message,
query=query, query=query,
user_id=application_generate_entity.user_id, user_id=application_generate_entity.user_id,
invoke_from=application_generate_entity.invoke_from invoke_from=application_generate_entity.invoke_from,
) )
if annotation_reply: if annotation_reply:
queue_manager.publish( queue_manager.publish(
QueueAnnotationReplyEvent(message_annotation_id=annotation_reply.id), QueueAnnotationReplyEvent(message_annotation_id=annotation_reply.id),
PublishFrom.APPLICATION_MANAGER PublishFrom.APPLICATION_MANAGER,
) )
self.direct_output( self.direct_output(
@ -129,7 +129,7 @@ class ChatAppRunner(AppRunner):
app_generate_entity=application_generate_entity, app_generate_entity=application_generate_entity,
prompt_messages=prompt_messages, prompt_messages=prompt_messages,
text=annotation_reply.content, text=annotation_reply.content,
stream=application_generate_entity.stream stream=application_generate_entity.stream,
) )
return return
@ -141,7 +141,7 @@ class ChatAppRunner(AppRunner):
app_id=app_record.id, app_id=app_record.id,
external_data_tools=external_data_tools, external_data_tools=external_data_tools,
inputs=inputs, inputs=inputs,
query=query query=query,
) )
# get context from datasets # get context from datasets
@ -152,7 +152,7 @@ class ChatAppRunner(AppRunner):
app_record.id, app_record.id,
message.id, message.id,
application_generate_entity.user_id, application_generate_entity.user_id,
application_generate_entity.invoke_from application_generate_entity.invoke_from,
) )
dataset_retrieval = DatasetRetrieval(application_generate_entity) dataset_retrieval = DatasetRetrieval(application_generate_entity)
@ -181,29 +181,26 @@ class ChatAppRunner(AppRunner):
files=files, files=files,
query=query, query=query,
context=context, context=context,
memory=memory memory=memory,
) )
# check hosting moderation # check hosting moderation
hosting_moderation_result = self.check_hosting_moderation( hosting_moderation_result = self.check_hosting_moderation(
application_generate_entity=application_generate_entity, application_generate_entity=application_generate_entity,
queue_manager=queue_manager, queue_manager=queue_manager,
prompt_messages=prompt_messages prompt_messages=prompt_messages,
) )
if hosting_moderation_result: if hosting_moderation_result:
return return
# Re-calculate the max tokens if sum(prompt_token + max_tokens) over model token limit # Re-calculate the max tokens if sum(prompt_token + max_tokens) over model token limit
self.recalc_llm_max_tokens( self.recalc_llm_max_tokens(model_config=application_generate_entity.model_conf, prompt_messages=prompt_messages)
model_config=application_generate_entity.model_conf,
prompt_messages=prompt_messages
)
# Invoke model # Invoke model
model_instance = ModelInstance( model_instance = ModelInstance(
provider_model_bundle=application_generate_entity.model_conf.provider_model_bundle, provider_model_bundle=application_generate_entity.model_conf.provider_model_bundle,
model=application_generate_entity.model_conf.model model=application_generate_entity.model_conf.model,
) )
db.session.close() db.session.close()
@ -218,7 +215,5 @@ class ChatAppRunner(AppRunner):
# handle invoke result # handle invoke result
self._handle_invoke_result( self._handle_invoke_result(
invoke_result=invoke_result, invoke_result=invoke_result, queue_manager=queue_manager, stream=application_generate_entity.stream
queue_manager=queue_manager,
stream=application_generate_entity.stream
) )

@ -23,15 +23,15 @@ class ChatAppGenerateResponseConverter(AppGenerateResponseConverter):
:return: :return:
""" """
response = { response = {
'event': 'message', "event": "message",
'task_id': blocking_response.task_id, "task_id": blocking_response.task_id,
'id': blocking_response.data.id, "id": blocking_response.data.id,
'message_id': blocking_response.data.message_id, "message_id": blocking_response.data.message_id,
'conversation_id': blocking_response.data.conversation_id, "conversation_id": blocking_response.data.conversation_id,
'mode': blocking_response.data.mode, "mode": blocking_response.data.mode,
'answer': blocking_response.data.answer, "answer": blocking_response.data.answer,
'metadata': blocking_response.data.metadata, "metadata": blocking_response.data.metadata,
'created_at': blocking_response.data.created_at "created_at": blocking_response.data.created_at,
} }
return response return response
@ -45,14 +45,15 @@ class ChatAppGenerateResponseConverter(AppGenerateResponseConverter):
""" """
response = cls.convert_blocking_full_response(blocking_response) response = cls.convert_blocking_full_response(blocking_response)
metadata = response.get('metadata', {}) metadata = response.get("metadata", {})
response['metadata'] = cls._get_simple_metadata(metadata) response["metadata"] = cls._get_simple_metadata(metadata)
return response return response
@classmethod @classmethod
def convert_stream_full_response(cls, stream_response: Generator[ChatbotAppStreamResponse, None, None]) \ def convert_stream_full_response(
-> Generator[str, None, None]: cls, stream_response: Generator[ChatbotAppStreamResponse, None, None]
) -> Generator[str, None, None]:
""" """
Convert stream full response. Convert stream full response.
:param stream_response: stream response :param stream_response: stream response
@ -63,14 +64,14 @@ class ChatAppGenerateResponseConverter(AppGenerateResponseConverter):
sub_stream_response = chunk.stream_response sub_stream_response = chunk.stream_response
if isinstance(sub_stream_response, PingStreamResponse): if isinstance(sub_stream_response, PingStreamResponse):
yield 'ping' yield "ping"
continue continue
response_chunk = { response_chunk = {
'event': sub_stream_response.event.value, "event": sub_stream_response.event.value,
'conversation_id': chunk.conversation_id, "conversation_id": chunk.conversation_id,
'message_id': chunk.message_id, "message_id": chunk.message_id,
'created_at': chunk.created_at "created_at": chunk.created_at,
} }
if isinstance(sub_stream_response, ErrorStreamResponse): if isinstance(sub_stream_response, ErrorStreamResponse):
@ -81,8 +82,9 @@ class ChatAppGenerateResponseConverter(AppGenerateResponseConverter):
yield json.dumps(response_chunk) yield json.dumps(response_chunk)
@classmethod @classmethod
def convert_stream_simple_response(cls, stream_response: Generator[ChatbotAppStreamResponse, None, None]) \ def convert_stream_simple_response(
-> Generator[str, None, None]: cls, stream_response: Generator[ChatbotAppStreamResponse, None, None]
) -> Generator[str, None, None]:
""" """
Convert stream simple response. Convert stream simple response.
:param stream_response: stream response :param stream_response: stream response
@ -93,20 +95,20 @@ class ChatAppGenerateResponseConverter(AppGenerateResponseConverter):
sub_stream_response = chunk.stream_response sub_stream_response = chunk.stream_response
if isinstance(sub_stream_response, PingStreamResponse): if isinstance(sub_stream_response, PingStreamResponse):
yield 'ping' yield "ping"
continue continue
response_chunk = { response_chunk = {
'event': sub_stream_response.event.value, "event": sub_stream_response.event.value,
'conversation_id': chunk.conversation_id, "conversation_id": chunk.conversation_id,
'message_id': chunk.message_id, "message_id": chunk.message_id,
'created_at': chunk.created_at "created_at": chunk.created_at,
} }
if isinstance(sub_stream_response, MessageEndStreamResponse): if isinstance(sub_stream_response, MessageEndStreamResponse):
sub_stream_response_dict = sub_stream_response.to_dict() sub_stream_response_dict = sub_stream_response.to_dict()
metadata = sub_stream_response_dict.get('metadata', {}) metadata = sub_stream_response_dict.get("metadata", {})
sub_stream_response_dict['metadata'] = cls._get_simple_metadata(metadata) sub_stream_response_dict["metadata"] = cls._get_simple_metadata(metadata)
response_chunk.update(sub_stream_response_dict) response_chunk.update(sub_stream_response_dict)
if isinstance(sub_stream_response, ErrorStreamResponse): if isinstance(sub_stream_response, ErrorStreamResponse):
data = cls._error_to_stream_response(sub_stream_response.err) data = cls._error_to_stream_response(sub_stream_response.err)

@ -17,14 +17,15 @@ class CompletionAppConfig(EasyUIBasedAppConfig):
""" """
Completion App Config Entity. Completion App Config Entity.
""" """
pass pass
class CompletionAppConfigManager(BaseAppConfigManager): class CompletionAppConfigManager(BaseAppConfigManager):
@classmethod @classmethod
def get_app_config(cls, app_model: App, def get_app_config(
app_model_config: AppModelConfig, cls, app_model: App, app_model_config: AppModelConfig, override_config_dict: Optional[dict] = None
override_config_dict: Optional[dict] = None) -> CompletionAppConfig: ) -> CompletionAppConfig:
""" """
Convert app model config to completion app config Convert app model config to completion app config
:param app_model: app model :param app_model: app model
@ -51,19 +52,11 @@ class CompletionAppConfigManager(BaseAppConfigManager):
app_model_config_from=config_from, app_model_config_from=config_from,
app_model_config_id=app_model_config.id, app_model_config_id=app_model_config.id,
app_model_config_dict=config_dict, app_model_config_dict=config_dict,
model=ModelConfigManager.convert( model=ModelConfigManager.convert(config=config_dict),
config=config_dict prompt_template=PromptTemplateConfigManager.convert(config=config_dict),
), sensitive_word_avoidance=SensitiveWordAvoidanceConfigManager.convert(config=config_dict),
prompt_template=PromptTemplateConfigManager.convert( dataset=DatasetConfigManager.convert(config=config_dict),
config=config_dict additional_features=cls.convert_features(config_dict, app_mode),
),
sensitive_word_avoidance=SensitiveWordAvoidanceConfigManager.convert(
config=config_dict
),
dataset=DatasetConfigManager.convert(
config=config_dict
),
additional_features=cls.convert_features(config_dict, app_mode)
) )
app_config.variables, app_config.external_data_variables = BasicVariablesConfigManager.convert( app_config.variables, app_config.external_data_variables = BasicVariablesConfigManager.convert(
@ -101,8 +94,9 @@ class CompletionAppConfigManager(BaseAppConfigManager):
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)
# dataset_query_variable # dataset_query_variable
config, current_related_config_keys = DatasetConfigManager.validate_and_set_defaults(tenant_id, app_mode, config, current_related_config_keys = DatasetConfigManager.validate_and_set_defaults(
config) tenant_id, app_mode, config
)
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)
# text_to_speech # text_to_speech
@ -114,8 +108,9 @@ class CompletionAppConfigManager(BaseAppConfigManager):
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)
# moderation validation # moderation validation
config, current_related_config_keys = SensitiveWordAvoidanceConfigManager.validate_and_set_defaults(tenant_id, config, current_related_config_keys = SensitiveWordAvoidanceConfigManager.validate_and_set_defaults(
config) tenant_id, config
)
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)
related_config_keys = list(set(related_config_keys)) related_config_keys = list(set(related_config_keys))

@ -3,14 +3,14 @@ import os
import threading import threading
import uuid import uuid
from collections.abc import Generator from collections.abc import Generator
from typing import Any, Union from typing import Any, Literal, Union, overload
from flask import Flask, current_app from flask import Flask, current_app
from pydantic import ValidationError from pydantic import ValidationError
from core.app.app_config.easy_ui_based_app.model_config.converter import ModelConfigConverter from core.app.app_config.easy_ui_based_app.model_config.converter import ModelConfigConverter
from core.app.app_config.features.file_upload.manager import FileUploadConfigManager from core.app.app_config.features.file_upload.manager import FileUploadConfigManager
from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedException, PublishFrom from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedError, PublishFrom
from core.app.apps.completion.app_config_manager import CompletionAppConfigManager from core.app.apps.completion.app_config_manager import CompletionAppConfigManager
from core.app.apps.completion.app_runner import CompletionAppRunner from core.app.apps.completion.app_runner import CompletionAppRunner
from core.app.apps.completion.generate_response_converter import CompletionAppGenerateResponseConverter from core.app.apps.completion.generate_response_converter import CompletionAppGenerateResponseConverter
@ -30,12 +30,29 @@ logger = logging.getLogger(__name__)
class CompletionAppGenerator(MessageBasedAppGenerator): class CompletionAppGenerator(MessageBasedAppGenerator):
def generate(self, app_model: App, @overload
user: Union[Account, EndUser], def generate(
args: Any, self,
invoke_from: InvokeFrom, app_model: App,
stream: bool = True) \ user: Union[Account, EndUser],
-> Union[dict, Generator[dict, None, None]]: args: dict,
invoke_from: InvokeFrom,
stream: Literal[True] = True,
) -> Generator[str, None, None]: ...
@overload
def generate(
self,
app_model: App,
user: Union[Account, EndUser],
args: dict,
invoke_from: InvokeFrom,
stream: Literal[False] = False,
) -> dict: ...
def generate(
self, app_model: App, user: Union[Account, EndUser], args: Any, invoke_from: InvokeFrom, stream: bool = True
) -> Union[dict, Generator[str, None, None]]:
""" """
Generate App response. Generate App response.
@ -45,12 +62,12 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
:param invoke_from: invoke from source :param invoke_from: invoke from source
:param stream: is stream :param stream: is stream
""" """
query = args['query'] query = args["query"]
if not isinstance(query, str): if not isinstance(query, str):
raise ValueError('query must be a string') raise ValueError("query must be a string")
query = query.replace('\x00', '') query = query.replace("\x00", "")
inputs = args['inputs'] inputs = args["inputs"]
extras = {} extras = {}
@ -58,41 +75,31 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
conversation = None conversation = None
# get app model config # get app model config
app_model_config = self._get_app_model_config( app_model_config = self._get_app_model_config(app_model=app_model, conversation=conversation)
app_model=app_model,
conversation=conversation
)
# validate override model config # validate override model config
override_model_config_dict = None override_model_config_dict = None
if args.get('model_config'): if args.get("model_config"):
if invoke_from != InvokeFrom.DEBUGGER: if invoke_from != InvokeFrom.DEBUGGER:
raise ValueError('Only in App debug mode can override model config') raise ValueError("Only in App debug mode can override model config")
# validate config # validate config
override_model_config_dict = CompletionAppConfigManager.config_validate( override_model_config_dict = CompletionAppConfigManager.config_validate(
tenant_id=app_model.tenant_id, tenant_id=app_model.tenant_id, config=args.get("model_config")
config=args.get('model_config')
) )
# parse files # parse files
files = args['files'] if args.get('files') else [] files = args["files"] if args.get("files") else []
message_file_parser = MessageFileParser(tenant_id=app_model.tenant_id, app_id=app_model.id) message_file_parser = MessageFileParser(tenant_id=app_model.tenant_id, app_id=app_model.id)
file_extra_config = FileUploadConfigManager.convert(override_model_config_dict or app_model_config.to_dict()) file_extra_config = FileUploadConfigManager.convert(override_model_config_dict or app_model_config.to_dict())
if file_extra_config: if file_extra_config:
file_objs = message_file_parser.validate_and_transform_files_arg( file_objs = message_file_parser.validate_and_transform_files_arg(files, file_extra_config, user)
files,
file_extra_config,
user
)
else: else:
file_objs = [] file_objs = []
# convert to app config # convert to app config
app_config = CompletionAppConfigManager.get_app_config( app_config = CompletionAppConfigManager.get_app_config(
app_model=app_model, app_model=app_model, app_model_config=app_model_config, override_config_dict=override_model_config_dict
app_model_config=app_model_config,
override_config_dict=override_model_config_dict
) )
# get tracing instance # get tracing instance
@ -110,14 +117,11 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
stream=stream, stream=stream,
invoke_from=invoke_from, invoke_from=invoke_from,
extras=extras, extras=extras,
trace_manager=trace_manager trace_manager=trace_manager,
) )
# init generate records # init generate records
( (conversation, message) = self._init_generate_records(application_generate_entity)
conversation,
message
) = self._init_generate_records(application_generate_entity)
# init queue manager # init queue manager
queue_manager = MessageBasedAppQueueManager( queue_manager = MessageBasedAppQueueManager(
@ -126,16 +130,19 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
invoke_from=application_generate_entity.invoke_from, invoke_from=application_generate_entity.invoke_from,
conversation_id=conversation.id, conversation_id=conversation.id,
app_mode=conversation.mode, app_mode=conversation.mode,
message_id=message.id message_id=message.id,
) )
# new thread # new thread
worker_thread = threading.Thread(target=self._generate_worker, kwargs={ worker_thread = threading.Thread(
'flask_app': current_app._get_current_object(), target=self._generate_worker,
'application_generate_entity': application_generate_entity, kwargs={
'queue_manager': queue_manager, "flask_app": current_app._get_current_object(),
'message_id': message.id, "application_generate_entity": application_generate_entity,
}) "queue_manager": queue_manager,
"message_id": message.id,
},
)
worker_thread.start() worker_thread.start()
@ -149,15 +156,15 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
stream=stream, stream=stream,
) )
return CompletionAppGenerateResponseConverter.convert( return CompletionAppGenerateResponseConverter.convert(response=response, invoke_from=invoke_from)
response=response,
invoke_from=invoke_from
)
def _generate_worker(self, flask_app: Flask, def _generate_worker(
application_generate_entity: CompletionAppGenerateEntity, self,
queue_manager: AppQueueManager, flask_app: Flask,
message_id: str) -> None: application_generate_entity: CompletionAppGenerateEntity,
queue_manager: AppQueueManager,
message_id: str,
) -> None:
""" """
Generate worker in a new thread. Generate worker in a new thread.
:param flask_app: Flask app :param flask_app: Flask app
@ -176,20 +183,19 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
runner.run( runner.run(
application_generate_entity=application_generate_entity, application_generate_entity=application_generate_entity,
queue_manager=queue_manager, queue_manager=queue_manager,
message=message message=message,
) )
except GenerateTaskStoppedException: except GenerateTaskStoppedError:
pass pass
except InvokeAuthorizationError: except InvokeAuthorizationError:
queue_manager.publish_error( queue_manager.publish_error(
InvokeAuthorizationError('Incorrect API key provided'), InvokeAuthorizationError("Incorrect API key provided"), PublishFrom.APPLICATION_MANAGER
PublishFrom.APPLICATION_MANAGER
) )
except ValidationError as e: except ValidationError as e:
logger.exception("Validation Error when generating") logger.exception("Validation Error when generating")
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER) queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
except (ValueError, InvokeError) as e: except (ValueError, InvokeError) as e:
if os.environ.get("DEBUG") and os.environ.get("DEBUG").lower() == 'true': if os.environ.get("DEBUG") and os.environ.get("DEBUG").lower() == "true":
logger.exception("Error when generating") logger.exception("Error when generating")
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER) queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
except Exception as e: except Exception as e:
@ -198,12 +204,14 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
finally: finally:
db.session.close() db.session.close()
def generate_more_like_this(self, app_model: App, def generate_more_like_this(
message_id: str, self,
user: Union[Account, EndUser], app_model: App,
invoke_from: InvokeFrom, message_id: str,
stream: bool = True) \ user: Union[Account, EndUser],
-> Union[dict, Generator[dict, None, None]]: invoke_from: InvokeFrom,
stream: bool = True,
) -> Union[dict, Generator[str, None, None]]:
""" """
Generate App response. Generate App response.
@ -213,13 +221,17 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
:param invoke_from: invoke from source :param invoke_from: invoke from source
:param stream: is stream :param stream: is stream
""" """
message = db.session.query(Message).filter( message = (
Message.id == message_id, db.session.query(Message)
Message.app_id == app_model.id, .filter(
Message.from_source == ('api' if isinstance(user, EndUser) else 'console'), Message.id == message_id,
Message.from_end_user_id == (user.id if isinstance(user, EndUser) else None), Message.app_id == app_model.id,
Message.from_account_id == (user.id if isinstance(user, Account) else None), Message.from_source == ("api" if isinstance(user, EndUser) else "console"),
).first() Message.from_end_user_id == (user.id if isinstance(user, EndUser) else None),
Message.from_account_id == (user.id if isinstance(user, Account) else None),
)
.first()
)
if not message: if not message:
raise MessageNotExistsError() raise MessageNotExistsError()
@ -232,29 +244,23 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
app_model_config = message.app_model_config app_model_config = message.app_model_config
override_model_config_dict = app_model_config.to_dict() override_model_config_dict = app_model_config.to_dict()
model_dict = override_model_config_dict['model'] model_dict = override_model_config_dict["model"]
completion_params = model_dict.get('completion_params') completion_params = model_dict.get("completion_params")
completion_params['temperature'] = 0.9 completion_params["temperature"] = 0.9
model_dict['completion_params'] = completion_params model_dict["completion_params"] = completion_params
override_model_config_dict['model'] = model_dict override_model_config_dict["model"] = model_dict
# parse files # parse files
message_file_parser = MessageFileParser(tenant_id=app_model.tenant_id, app_id=app_model.id) message_file_parser = MessageFileParser(tenant_id=app_model.tenant_id, app_id=app_model.id)
file_extra_config = FileUploadConfigManager.convert(override_model_config_dict or app_model_config.to_dict()) file_extra_config = FileUploadConfigManager.convert(override_model_config_dict or app_model_config.to_dict())
if file_extra_config: if file_extra_config:
file_objs = message_file_parser.validate_and_transform_files_arg( file_objs = message_file_parser.validate_and_transform_files_arg(message.files, file_extra_config, user)
message.files,
file_extra_config,
user
)
else: else:
file_objs = [] file_objs = []
# convert to app config # convert to app config
app_config = CompletionAppConfigManager.get_app_config( app_config = CompletionAppConfigManager.get_app_config(
app_model=app_model, app_model=app_model, app_model_config=app_model_config, override_config_dict=override_model_config_dict
app_model_config=app_model_config,
override_config_dict=override_model_config_dict
) )
# init application generate entity # init application generate entity
@ -268,14 +274,11 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
user_id=user.id, user_id=user.id,
stream=stream, stream=stream,
invoke_from=invoke_from, invoke_from=invoke_from,
extras={} extras={},
) )
# init generate records # init generate records
( (conversation, message) = self._init_generate_records(application_generate_entity)
conversation,
message
) = self._init_generate_records(application_generate_entity)
# init queue manager # init queue manager
queue_manager = MessageBasedAppQueueManager( queue_manager = MessageBasedAppQueueManager(
@ -284,16 +287,19 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
invoke_from=application_generate_entity.invoke_from, invoke_from=application_generate_entity.invoke_from,
conversation_id=conversation.id, conversation_id=conversation.id,
app_mode=conversation.mode, app_mode=conversation.mode,
message_id=message.id message_id=message.id,
) )
# new thread # new thread
worker_thread = threading.Thread(target=self._generate_worker, kwargs={ worker_thread = threading.Thread(
'flask_app': current_app._get_current_object(), target=self._generate_worker,
'application_generate_entity': application_generate_entity, kwargs={
'queue_manager': queue_manager, "flask_app": current_app._get_current_object(),
'message_id': message.id, "application_generate_entity": application_generate_entity,
}) "queue_manager": queue_manager,
"message_id": message.id,
},
)
worker_thread.start() worker_thread.start()
@ -307,7 +313,4 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
stream=stream, stream=stream,
) )
return CompletionAppGenerateResponseConverter.convert( return CompletionAppGenerateResponseConverter.convert(response=response, invoke_from=invoke_from)
response=response,
invoke_from=invoke_from
)

@ -9,7 +9,7 @@ from core.app.entities.app_invoke_entities import (
) )
from core.callback_handler.index_tool_callback_handler import DatasetIndexToolCallbackHandler from core.callback_handler.index_tool_callback_handler import DatasetIndexToolCallbackHandler
from core.model_manager import ModelInstance from core.model_manager import ModelInstance
from core.moderation.base import ModerationException from core.moderation.base import ModerationError
from core.rag.retrieval.dataset_retrieval import DatasetRetrieval from core.rag.retrieval.dataset_retrieval import DatasetRetrieval
from extensions.ext_database import db from extensions.ext_database import db
from models.model import App, Message from models.model import App, Message
@ -22,9 +22,9 @@ class CompletionAppRunner(AppRunner):
Completion Application Runner Completion Application Runner
""" """
def run(self, application_generate_entity: CompletionAppGenerateEntity, def run(
queue_manager: AppQueueManager, self, application_generate_entity: CompletionAppGenerateEntity, queue_manager: AppQueueManager, message: Message
message: Message) -> None: ) -> None:
""" """
Run application Run application
:param application_generate_entity: application generate entity :param application_generate_entity: application generate entity
@ -54,7 +54,7 @@ class CompletionAppRunner(AppRunner):
prompt_template_entity=app_config.prompt_template, prompt_template_entity=app_config.prompt_template,
inputs=inputs, inputs=inputs,
files=files, files=files,
query=query query=query,
) )
# organize all inputs and template to prompt messages # organize all inputs and template to prompt messages
@ -65,7 +65,7 @@ class CompletionAppRunner(AppRunner):
prompt_template_entity=app_config.prompt_template, prompt_template_entity=app_config.prompt_template,
inputs=inputs, inputs=inputs,
files=files, files=files,
query=query query=query,
) )
# moderation # moderation
@ -77,15 +77,15 @@ class CompletionAppRunner(AppRunner):
app_generate_entity=application_generate_entity, app_generate_entity=application_generate_entity,
inputs=inputs, inputs=inputs,
query=query, query=query,
message_id=message.id message_id=message.id,
) )
except ModerationException as e: except ModerationError as e:
self.direct_output( self.direct_output(
queue_manager=queue_manager, queue_manager=queue_manager,
app_generate_entity=application_generate_entity, app_generate_entity=application_generate_entity,
prompt_messages=prompt_messages, prompt_messages=prompt_messages,
text=str(e), text=str(e),
stream=application_generate_entity.stream stream=application_generate_entity.stream,
) )
return return
@ -97,7 +97,7 @@ class CompletionAppRunner(AppRunner):
app_id=app_record.id, app_id=app_record.id,
external_data_tools=external_data_tools, external_data_tools=external_data_tools,
inputs=inputs, inputs=inputs,
query=query query=query,
) )
# get context from datasets # get context from datasets
@ -108,7 +108,7 @@ class CompletionAppRunner(AppRunner):
app_record.id, app_record.id,
message.id, message.id,
application_generate_entity.user_id, application_generate_entity.user_id,
application_generate_entity.invoke_from application_generate_entity.invoke_from,
) )
dataset_config = app_config.dataset dataset_config = app_config.dataset
@ -126,7 +126,7 @@ class CompletionAppRunner(AppRunner):
invoke_from=application_generate_entity.invoke_from, invoke_from=application_generate_entity.invoke_from,
show_retrieve_source=app_config.additional_features.show_retrieve_source, show_retrieve_source=app_config.additional_features.show_retrieve_source,
hit_callback=hit_callback, hit_callback=hit_callback,
message_id=message.id message_id=message.id,
) )
# reorganize all inputs and template to prompt messages # reorganize all inputs and template to prompt messages
@ -139,29 +139,26 @@ class CompletionAppRunner(AppRunner):
inputs=inputs, inputs=inputs,
files=files, files=files,
query=query, query=query,
context=context context=context,
) )
# check hosting moderation # check hosting moderation
hosting_moderation_result = self.check_hosting_moderation( hosting_moderation_result = self.check_hosting_moderation(
application_generate_entity=application_generate_entity, application_generate_entity=application_generate_entity,
queue_manager=queue_manager, queue_manager=queue_manager,
prompt_messages=prompt_messages prompt_messages=prompt_messages,
) )
if hosting_moderation_result: if hosting_moderation_result:
return return
# Re-calculate the max tokens if sum(prompt_token + max_tokens) over model token limit # Re-calculate the max tokens if sum(prompt_token + max_tokens) over model token limit
self.recalc_llm_max_tokens( self.recalc_llm_max_tokens(model_config=application_generate_entity.model_conf, prompt_messages=prompt_messages)
model_config=application_generate_entity.model_conf,
prompt_messages=prompt_messages
)
# Invoke model # Invoke model
model_instance = ModelInstance( model_instance = ModelInstance(
provider_model_bundle=application_generate_entity.model_conf.provider_model_bundle, provider_model_bundle=application_generate_entity.model_conf.provider_model_bundle,
model=application_generate_entity.model_conf.model model=application_generate_entity.model_conf.model,
) )
db.session.close() db.session.close()
@ -176,8 +173,5 @@ class CompletionAppRunner(AppRunner):
# handle invoke result # handle invoke result
self._handle_invoke_result( self._handle_invoke_result(
invoke_result=invoke_result, invoke_result=invoke_result, queue_manager=queue_manager, stream=application_generate_entity.stream
queue_manager=queue_manager,
stream=application_generate_entity.stream
) )

@ -23,14 +23,14 @@ class CompletionAppGenerateResponseConverter(AppGenerateResponseConverter):
:return: :return:
""" """
response = { response = {
'event': 'message', "event": "message",
'task_id': blocking_response.task_id, "task_id": blocking_response.task_id,
'id': blocking_response.data.id, "id": blocking_response.data.id,
'message_id': blocking_response.data.message_id, "message_id": blocking_response.data.message_id,
'mode': blocking_response.data.mode, "mode": blocking_response.data.mode,
'answer': blocking_response.data.answer, "answer": blocking_response.data.answer,
'metadata': blocking_response.data.metadata, "metadata": blocking_response.data.metadata,
'created_at': blocking_response.data.created_at "created_at": blocking_response.data.created_at,
} }
return response return response
@ -44,14 +44,15 @@ class CompletionAppGenerateResponseConverter(AppGenerateResponseConverter):
""" """
response = cls.convert_blocking_full_response(blocking_response) response = cls.convert_blocking_full_response(blocking_response)
metadata = response.get('metadata', {}) metadata = response.get("metadata", {})
response['metadata'] = cls._get_simple_metadata(metadata) response["metadata"] = cls._get_simple_metadata(metadata)
return response return response
@classmethod @classmethod
def convert_stream_full_response(cls, stream_response: Generator[CompletionAppStreamResponse, None, None]) \ def convert_stream_full_response(
-> Generator[str, None, None]: cls, stream_response: Generator[CompletionAppStreamResponse, None, None]
) -> Generator[str, None, None]:
""" """
Convert stream full response. Convert stream full response.
:param stream_response: stream response :param stream_response: stream response
@ -62,13 +63,13 @@ class CompletionAppGenerateResponseConverter(AppGenerateResponseConverter):
sub_stream_response = chunk.stream_response sub_stream_response = chunk.stream_response
if isinstance(sub_stream_response, PingStreamResponse): if isinstance(sub_stream_response, PingStreamResponse):
yield 'ping' yield "ping"
continue continue
response_chunk = { response_chunk = {
'event': sub_stream_response.event.value, "event": sub_stream_response.event.value,
'message_id': chunk.message_id, "message_id": chunk.message_id,
'created_at': chunk.created_at "created_at": chunk.created_at,
} }
if isinstance(sub_stream_response, ErrorStreamResponse): if isinstance(sub_stream_response, ErrorStreamResponse):
@ -79,8 +80,9 @@ class CompletionAppGenerateResponseConverter(AppGenerateResponseConverter):
yield json.dumps(response_chunk) yield json.dumps(response_chunk)
@classmethod @classmethod
def convert_stream_simple_response(cls, stream_response: Generator[CompletionAppStreamResponse, None, None]) \ def convert_stream_simple_response(
-> Generator[str, None, None]: cls, stream_response: Generator[CompletionAppStreamResponse, None, None]
) -> Generator[str, None, None]:
""" """
Convert stream simple response. Convert stream simple response.
:param stream_response: stream response :param stream_response: stream response
@ -91,19 +93,19 @@ class CompletionAppGenerateResponseConverter(AppGenerateResponseConverter):
sub_stream_response = chunk.stream_response sub_stream_response = chunk.stream_response
if isinstance(sub_stream_response, PingStreamResponse): if isinstance(sub_stream_response, PingStreamResponse):
yield 'ping' yield "ping"
continue continue
response_chunk = { response_chunk = {
'event': sub_stream_response.event.value, "event": sub_stream_response.event.value,
'message_id': chunk.message_id, "message_id": chunk.message_id,
'created_at': chunk.created_at "created_at": chunk.created_at,
} }
if isinstance(sub_stream_response, MessageEndStreamResponse): if isinstance(sub_stream_response, MessageEndStreamResponse):
sub_stream_response_dict = sub_stream_response.to_dict() sub_stream_response_dict = sub_stream_response.to_dict()
metadata = sub_stream_response_dict.get('metadata', {}) metadata = sub_stream_response_dict.get("metadata", {})
sub_stream_response_dict['metadata'] = cls._get_simple_metadata(metadata) sub_stream_response_dict["metadata"] = cls._get_simple_metadata(metadata)
response_chunk.update(sub_stream_response_dict) response_chunk.update(sub_stream_response_dict)
if isinstance(sub_stream_response, ErrorStreamResponse): if isinstance(sub_stream_response, ErrorStreamResponse):
data = cls._error_to_stream_response(sub_stream_response.err) data = cls._error_to_stream_response(sub_stream_response.err)

@ -8,7 +8,7 @@ from sqlalchemy import and_
from core.app.app_config.entities import EasyUIBasedAppModelConfigFrom from core.app.app_config.entities import EasyUIBasedAppModelConfigFrom
from core.app.apps.base_app_generator import BaseAppGenerator from core.app.apps.base_app_generator import BaseAppGenerator
from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedException from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedError
from core.app.entities.app_invoke_entities import ( from core.app.entities.app_invoke_entities import (
AdvancedChatAppGenerateEntity, AdvancedChatAppGenerateEntity,
AgentChatAppGenerateEntity, AgentChatAppGenerateEntity,
@ -35,23 +35,23 @@ logger = logging.getLogger(__name__)
class MessageBasedAppGenerator(BaseAppGenerator): class MessageBasedAppGenerator(BaseAppGenerator):
def _handle_response( def _handle_response(
self, application_generate_entity: Union[ self,
ChatAppGenerateEntity, application_generate_entity: Union[
CompletionAppGenerateEntity, ChatAppGenerateEntity,
AgentChatAppGenerateEntity, CompletionAppGenerateEntity,
AdvancedChatAppGenerateEntity AgentChatAppGenerateEntity,
], AdvancedChatAppGenerateEntity,
queue_manager: AppQueueManager, ],
conversation: Conversation, queue_manager: AppQueueManager,
message: Message, conversation: Conversation,
user: Union[Account, EndUser], message: Message,
stream: bool = False, user: Union[Account, EndUser],
stream: bool = False,
) -> Union[ ) -> Union[
ChatbotAppBlockingResponse, ChatbotAppBlockingResponse,
CompletionAppBlockingResponse, CompletionAppBlockingResponse,
Generator[Union[ChatbotAppStreamResponse, CompletionAppStreamResponse], None, None] Generator[Union[ChatbotAppStreamResponse, CompletionAppStreamResponse], None, None],
]: ]:
""" """
Handle response. Handle response.
@ -70,24 +70,25 @@ class MessageBasedAppGenerator(BaseAppGenerator):
conversation=conversation, conversation=conversation,
message=message, message=message,
user=user, user=user,
stream=stream stream=stream,
) )
try: try:
return generate_task_pipeline.process() return generate_task_pipeline.process()
except ValueError as e: except ValueError as e:
if e.args[0] == "I/O operation on closed file.": # ignore this error if e.args[0] == "I/O operation on closed file.": # ignore this error
raise GenerateTaskStoppedException() raise GenerateTaskStoppedError()
else: else:
logger.exception(e) logger.exception(e)
raise e raise e
def _get_conversation_by_user(self, app_model: App, conversation_id: str, def _get_conversation_by_user(
user: Union[Account, EndUser]) -> Conversation: self, app_model: App, conversation_id: str, user: Union[Account, EndUser]
) -> Conversation:
conversation_filter = [ conversation_filter = [
Conversation.id == conversation_id, Conversation.id == conversation_id,
Conversation.app_id == app_model.id, Conversation.app_id == app_model.id,
Conversation.status == 'normal' Conversation.status == "normal",
] ]
if isinstance(user, Account): if isinstance(user, Account):
@ -100,19 +101,18 @@ class MessageBasedAppGenerator(BaseAppGenerator):
if not conversation: if not conversation:
raise ConversationNotExistsError() raise ConversationNotExistsError()
if conversation.status != 'normal': if conversation.status != "normal":
raise ConversationCompletedError() raise ConversationCompletedError()
return conversation return conversation
def _get_app_model_config(self, app_model: App, def _get_app_model_config(self, app_model: App, conversation: Optional[Conversation] = None) -> AppModelConfig:
conversation: Optional[Conversation] = None) \
-> AppModelConfig:
if conversation: if conversation:
app_model_config = db.session.query(AppModelConfig).filter( app_model_config = (
AppModelConfig.id == conversation.app_model_config_id, db.session.query(AppModelConfig)
AppModelConfig.app_id == app_model.id .filter(AppModelConfig.id == conversation.app_model_config_id, AppModelConfig.app_id == app_model.id)
).first() .first()
)
if not app_model_config: if not app_model_config:
raise AppModelConfigBrokenError() raise AppModelConfigBrokenError()
@ -127,15 +127,16 @@ class MessageBasedAppGenerator(BaseAppGenerator):
return app_model_config return app_model_config
def _init_generate_records(self, def _init_generate_records(
application_generate_entity: Union[ self,
ChatAppGenerateEntity, application_generate_entity: Union[
CompletionAppGenerateEntity, ChatAppGenerateEntity,
AgentChatAppGenerateEntity, CompletionAppGenerateEntity,
AdvancedChatAppGenerateEntity AgentChatAppGenerateEntity,
], AdvancedChatAppGenerateEntity,
conversation: Optional[Conversation] = None) \ ],
-> tuple[Conversation, Message]: conversation: Optional[Conversation] = None,
) -> tuple[Conversation, Message]:
""" """
Initialize generate records Initialize generate records
:param application_generate_entity: application generate entity :param application_generate_entity: application generate entity
@ -148,10 +149,10 @@ class MessageBasedAppGenerator(BaseAppGenerator):
end_user_id = None end_user_id = None
account_id = None account_id = None
if application_generate_entity.invoke_from in [InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API]: if application_generate_entity.invoke_from in [InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API]:
from_source = 'api' from_source = "api"
end_user_id = application_generate_entity.user_id end_user_id = application_generate_entity.user_id
else: else:
from_source = 'console' from_source = "console"
account_id = application_generate_entity.user_id account_id = application_generate_entity.user_id
if isinstance(application_generate_entity, AdvancedChatAppGenerateEntity): if isinstance(application_generate_entity, AdvancedChatAppGenerateEntity):
@ -164,8 +165,11 @@ class MessageBasedAppGenerator(BaseAppGenerator):
model_provider = application_generate_entity.model_conf.provider model_provider = application_generate_entity.model_conf.provider
model_id = application_generate_entity.model_conf.model model_id = application_generate_entity.model_conf.model
override_model_configs = None override_model_configs = None
if app_config.app_model_config_from == EasyUIBasedAppModelConfigFrom.ARGS \ if app_config.app_model_config_from == EasyUIBasedAppModelConfigFrom.ARGS and app_config.app_mode in [
and app_config.app_mode in [AppMode.AGENT_CHAT, AppMode.CHAT, AppMode.COMPLETION]: AppMode.AGENT_CHAT,
AppMode.CHAT,
AppMode.COMPLETION,
]:
override_model_configs = app_config.app_model_config_dict override_model_configs = app_config.app_model_config_dict
# get conversation introduction # get conversation introduction
@ -179,12 +183,12 @@ class MessageBasedAppGenerator(BaseAppGenerator):
model_id=model_id, model_id=model_id,
override_model_configs=json.dumps(override_model_configs) if override_model_configs else None, override_model_configs=json.dumps(override_model_configs) if override_model_configs else None,
mode=app_config.app_mode.value, mode=app_config.app_mode.value,
name='New conversation', name="New conversation",
inputs=application_generate_entity.inputs, inputs=application_generate_entity.inputs,
introduction=introduction, introduction=introduction,
system_instruction="", system_instruction="",
system_instruction_tokens=0, system_instruction_tokens=0,
status='normal', status="normal",
invoke_from=application_generate_entity.invoke_from.value, invoke_from=application_generate_entity.invoke_from.value,
from_source=from_source, from_source=from_source,
from_end_user_id=end_user_id, from_end_user_id=end_user_id,
@ -216,11 +220,11 @@ class MessageBasedAppGenerator(BaseAppGenerator):
answer_price_unit=0, answer_price_unit=0,
provider_response_latency=0, provider_response_latency=0,
total_price=0, total_price=0,
currency='USD', currency="USD",
invoke_from=application_generate_entity.invoke_from.value, invoke_from=application_generate_entity.invoke_from.value,
from_source=from_source, from_source=from_source,
from_end_user_id=end_user_id, from_end_user_id=end_user_id,
from_account_id=account_id from_account_id=account_id,
) )
db.session.add(message) db.session.add(message)
@ -232,10 +236,10 @@ class MessageBasedAppGenerator(BaseAppGenerator):
message_id=message.id, message_id=message.id,
type=file.type.value, type=file.type.value,
transfer_method=file.transfer_method.value, transfer_method=file.transfer_method.value,
belongs_to='user', belongs_to="user",
url=file.url, url=file.url,
upload_file_id=file.related_id, upload_file_id=file.related_id,
created_by_role=('account' if account_id else 'end_user'), created_by_role=("account" if account_id else "end_user"),
created_by=account_id or end_user_id, created_by=account_id or end_user_id,
) )
db.session.add(message_file) db.session.add(message_file)
@ -269,11 +273,7 @@ class MessageBasedAppGenerator(BaseAppGenerator):
:param conversation_id: conversation id :param conversation_id: conversation id
:return: conversation :return: conversation
""" """
conversation = ( conversation = db.session.query(Conversation).filter(Conversation.id == conversation_id).first()
db.session.query(Conversation)
.filter(Conversation.id == conversation_id)
.first()
)
if not conversation: if not conversation:
raise ConversationNotExistsError() raise ConversationNotExistsError()
@ -286,10 +286,6 @@ class MessageBasedAppGenerator(BaseAppGenerator):
:param message_id: message id :param message_id: message id
:return: message :return: message
""" """
message = ( message = db.session.query(Message).filter(Message.id == message_id).first()
db.session.query(Message)
.filter(Message.id == message_id)
.first()
)
return message return message

@ -1,4 +1,4 @@
from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedException, PublishFrom from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedError, PublishFrom
from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.app_invoke_entities import InvokeFrom
from core.app.entities.queue_entities import ( from core.app.entities.queue_entities import (
AppQueueEvent, AppQueueEvent,
@ -12,12 +12,9 @@ from core.app.entities.queue_entities import (
class MessageBasedAppQueueManager(AppQueueManager): class MessageBasedAppQueueManager(AppQueueManager):
def __init__(self, task_id: str, def __init__(
user_id: str, self, task_id: str, user_id: str, invoke_from: InvokeFrom, conversation_id: str, app_mode: str, message_id: str
invoke_from: InvokeFrom, ) -> None:
conversation_id: str,
app_mode: str,
message_id: str) -> None:
super().__init__(task_id, user_id, invoke_from) super().__init__(task_id, user_id, invoke_from)
self._conversation_id = str(conversation_id) self._conversation_id = str(conversation_id)
@ -30,7 +27,7 @@ class MessageBasedAppQueueManager(AppQueueManager):
message_id=self._message_id, message_id=self._message_id,
conversation_id=self._conversation_id, conversation_id=self._conversation_id,
app_mode=self._app_mode, app_mode=self._app_mode,
event=event event=event,
) )
def _publish(self, event: AppQueueEvent, pub_from: PublishFrom) -> None: def _publish(self, event: AppQueueEvent, pub_from: PublishFrom) -> None:
@ -45,17 +42,15 @@ class MessageBasedAppQueueManager(AppQueueManager):
message_id=self._message_id, message_id=self._message_id,
conversation_id=self._conversation_id, conversation_id=self._conversation_id,
app_mode=self._app_mode, app_mode=self._app_mode,
event=event event=event,
) )
self._q.put(message) self._q.put(message)
if isinstance(event, QueueStopEvent if isinstance(
| QueueErrorEvent event, QueueStopEvent | QueueErrorEvent | QueueMessageEndEvent | QueueAdvancedChatMessageEndEvent
| QueueMessageEndEvent ):
| QueueAdvancedChatMessageEndEvent):
self.stop_listen() self.stop_listen()
if pub_from == PublishFrom.APPLICATION_MANAGER and self._is_stopped(): if pub_from == PublishFrom.APPLICATION_MANAGER and self._is_stopped():
raise GenerateTaskStoppedException() raise GenerateTaskStoppedError()

@ -12,6 +12,7 @@ class WorkflowAppConfig(WorkflowUIBasedAppConfig):
""" """
Workflow App Config Entity. Workflow App Config Entity.
""" """
pass pass
@ -26,13 +27,9 @@ class WorkflowAppConfigManager(BaseAppConfigManager):
app_id=app_model.id, app_id=app_model.id,
app_mode=app_mode, app_mode=app_mode,
workflow_id=workflow.id, workflow_id=workflow.id,
sensitive_word_avoidance=SensitiveWordAvoidanceConfigManager.convert( sensitive_word_avoidance=SensitiveWordAvoidanceConfigManager.convert(config=features_dict),
config=features_dict variables=WorkflowVariablesConfigManager.convert(workflow=workflow),
), additional_features=cls.convert_features(features_dict, app_mode),
variables=WorkflowVariablesConfigManager.convert(
workflow=workflow
),
additional_features=cls.convert_features(features_dict, app_mode)
) )
return app_config return app_config
@ -50,8 +47,7 @@ class WorkflowAppConfigManager(BaseAppConfigManager):
# file upload validation # file upload validation
config, current_related_config_keys = FileUploadConfigManager.validate_and_set_defaults( config, current_related_config_keys = FileUploadConfigManager.validate_and_set_defaults(
config=config, config=config, is_vision=False
is_vision=False
) )
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)
@ -61,9 +57,7 @@ class WorkflowAppConfigManager(BaseAppConfigManager):
# moderation validation # moderation validation
config, current_related_config_keys = SensitiveWordAvoidanceConfigManager.validate_and_set_defaults( config, current_related_config_keys = SensitiveWordAvoidanceConfigManager.validate_and_set_defaults(
tenant_id=tenant_id, tenant_id=tenant_id, config=config, only_structure_validate=only_structure_validate
config=config,
only_structure_validate=only_structure_validate
) )
related_config_keys.extend(current_related_config_keys) related_config_keys.extend(current_related_config_keys)

@ -4,7 +4,7 @@ import os
import threading import threading
import uuid import uuid
from collections.abc import Generator from collections.abc import Generator
from typing import Union from typing import Any, Literal, Optional, Union, overload
from flask import Flask, current_app from flask import Flask, current_app
from pydantic import ValidationError from pydantic import ValidationError
@ -12,7 +12,7 @@ from pydantic import ValidationError
import contexts import contexts
from core.app.app_config.features.file_upload.manager import FileUploadConfigManager from core.app.app_config.features.file_upload.manager import FileUploadConfigManager
from core.app.apps.base_app_generator import BaseAppGenerator from core.app.apps.base_app_generator import BaseAppGenerator
from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedException, PublishFrom from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedError, PublishFrom
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_queue_manager import WorkflowAppQueueManager from core.app.apps.workflow.app_queue_manager import WorkflowAppQueueManager
from core.app.apps.workflow.app_runner import WorkflowAppRunner from core.app.apps.workflow.app_runner import WorkflowAppRunner
@ -32,14 +32,42 @@ logger = logging.getLogger(__name__)
class WorkflowAppGenerator(BaseAppGenerator): class WorkflowAppGenerator(BaseAppGenerator):
@overload
def generate( def generate(
self, app_model: App, self,
app_model: App,
workflow: Workflow,
user: Union[Account, EndUser],
args: dict,
invoke_from: InvokeFrom,
stream: Literal[True] = True,
call_depth: int = 0,
workflow_thread_pool_id: Optional[str] = None,
) -> Generator[str, None, None]: ...
@overload
def generate(
self,
app_model: App,
workflow: Workflow,
user: Union[Account, EndUser],
args: dict,
invoke_from: InvokeFrom,
stream: Literal[False] = False,
call_depth: int = 0,
workflow_thread_pool_id: Optional[str] = None,
) -> dict: ...
def generate(
self,
app_model: App,
workflow: Workflow, workflow: Workflow,
user: Union[Account, EndUser], user: Union[Account, EndUser],
args: dict, args: dict,
invoke_from: InvokeFrom, invoke_from: InvokeFrom,
stream: bool = True, stream: bool = True,
call_depth: int = 0, call_depth: int = 0,
workflow_thread_pool_id: Optional[str] = None,
): ):
""" """
Generate App response. Generate App response.
@ -51,27 +79,21 @@ class WorkflowAppGenerator(BaseAppGenerator):
:param invoke_from: invoke from source :param invoke_from: invoke from source
:param stream: is stream :param stream: is stream
:param call_depth: call depth :param call_depth: call depth
:param workflow_thread_pool_id: workflow thread pool id
""" """
inputs = args['inputs'] inputs = args["inputs"]
# parse files # parse files
files = args['files'] if args.get('files') else [] files = args["files"] if args.get("files") else []
message_file_parser = MessageFileParser(tenant_id=app_model.tenant_id, app_id=app_model.id) message_file_parser = MessageFileParser(tenant_id=app_model.tenant_id, app_id=app_model.id)
file_extra_config = FileUploadConfigManager.convert(workflow.features_dict, is_vision=False) file_extra_config = FileUploadConfigManager.convert(workflow.features_dict, is_vision=False)
if file_extra_config: if file_extra_config:
file_objs = message_file_parser.validate_and_transform_files_arg( file_objs = message_file_parser.validate_and_transform_files_arg(files, file_extra_config, user)
files,
file_extra_config,
user
)
else: else:
file_objs = [] file_objs = []
# convert to app config # convert to app config
app_config = WorkflowAppConfigManager.get_app_config( app_config = WorkflowAppConfigManager.get_app_config(app_model=app_model, workflow=workflow)
app_model=app_model,
workflow=workflow
)
# get tracing instance # get tracing instance
user_id = user.id if isinstance(user, Account) else user.session_id user_id = user.id if isinstance(user, Account) else user.session_id
@ -87,7 +109,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
stream=stream, stream=stream,
invoke_from=invoke_from, invoke_from=invoke_from,
call_depth=call_depth, call_depth=call_depth,
trace_manager=trace_manager trace_manager=trace_manager,
) )
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id) contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
@ -98,16 +120,20 @@ class WorkflowAppGenerator(BaseAppGenerator):
application_generate_entity=application_generate_entity, application_generate_entity=application_generate_entity,
invoke_from=invoke_from, invoke_from=invoke_from,
stream=stream, stream=stream,
workflow_thread_pool_id=workflow_thread_pool_id,
) )
def _generate( def _generate(
self, app_model: App, self,
*,
app_model: App,
workflow: Workflow, workflow: Workflow,
user: Union[Account, EndUser], user: Union[Account, EndUser],
application_generate_entity: WorkflowAppGenerateEntity, application_generate_entity: WorkflowAppGenerateEntity,
invoke_from: InvokeFrom, invoke_from: InvokeFrom,
stream: bool = True, stream: bool = True,
) -> Union[dict, Generator[dict, None, None]]: workflow_thread_pool_id: Optional[str] = None,
) -> dict[str, Any] | Generator[str, None, None]:
""" """
Generate App response. Generate App response.
@ -117,22 +143,27 @@ class WorkflowAppGenerator(BaseAppGenerator):
:param application_generate_entity: application generate entity :param application_generate_entity: application generate entity
:param invoke_from: invoke from source :param invoke_from: invoke from source
:param stream: is stream :param stream: is stream
:param workflow_thread_pool_id: workflow thread pool id
""" """
# init queue manager # init queue manager
queue_manager = WorkflowAppQueueManager( queue_manager = WorkflowAppQueueManager(
task_id=application_generate_entity.task_id, task_id=application_generate_entity.task_id,
user_id=application_generate_entity.user_id, user_id=application_generate_entity.user_id,
invoke_from=application_generate_entity.invoke_from, invoke_from=application_generate_entity.invoke_from,
app_mode=app_model.mode app_mode=app_model.mode,
) )
# new thread # new thread
worker_thread = threading.Thread(target=self._generate_worker, kwargs={ worker_thread = threading.Thread(
'flask_app': current_app._get_current_object(), target=self._generate_worker,
'application_generate_entity': application_generate_entity, kwargs={
'queue_manager': queue_manager, "flask_app": current_app._get_current_object(), # type: ignore
'context': contextvars.copy_context() "application_generate_entity": application_generate_entity,
}) "queue_manager": queue_manager,
"context": contextvars.copy_context(),
"workflow_thread_pool_id": workflow_thread_pool_id,
},
)
worker_thread.start() worker_thread.start()
@ -145,17 +176,11 @@ class WorkflowAppGenerator(BaseAppGenerator):
stream=stream, stream=stream,
) )
return WorkflowAppGenerateResponseConverter.convert( return WorkflowAppGenerateResponseConverter.convert(response=response, invoke_from=invoke_from)
response=response,
invoke_from=invoke_from
)
def single_iteration_generate(self, app_model: App, def single_iteration_generate(
workflow: Workflow, self, app_model: App, workflow: Workflow, node_id: str, user: Account, args: dict, stream: bool = True
node_id: str, ) -> dict[str, Any] | Generator[str, Any, None]:
user: Account,
args: dict,
stream: bool = True):
""" """
Generate App response. Generate App response.
@ -167,20 +192,13 @@ class WorkflowAppGenerator(BaseAppGenerator):
:param stream: is stream :param stream: is stream
""" """
if not node_id: if not node_id:
raise ValueError('node_id is required') raise ValueError("node_id is required")
if args.get('inputs') is None:
raise ValueError('inputs is required')
extras = { if args.get("inputs") is None:
"auto_generate_conversation_name": False raise ValueError("inputs is required")
}
# convert to app config # convert to app config
app_config = WorkflowAppConfigManager.get_app_config( app_config = WorkflowAppConfigManager.get_app_config(app_model=app_model, workflow=workflow)
app_model=app_model,
workflow=workflow
)
# init application generate entity # init application generate entity
application_generate_entity = WorkflowAppGenerateEntity( application_generate_entity = WorkflowAppGenerateEntity(
@ -191,11 +209,10 @@ class WorkflowAppGenerator(BaseAppGenerator):
user_id=user.id, user_id=user.id,
stream=stream, stream=stream,
invoke_from=InvokeFrom.DEBUGGER, invoke_from=InvokeFrom.DEBUGGER,
extras=extras, extras={"auto_generate_conversation_name": False},
single_iteration_run=WorkflowAppGenerateEntity.SingleIterationRunEntity( single_iteration_run=WorkflowAppGenerateEntity.SingleIterationRunEntity(
node_id=node_id, node_id=node_id, inputs=args["inputs"]
inputs=args['inputs'] ),
)
) )
contexts.tenant_id.set(application_generate_entity.app_config.tenant_id) contexts.tenant_id.set(application_generate_entity.app_config.tenant_id)
@ -205,18 +222,23 @@ class WorkflowAppGenerator(BaseAppGenerator):
user=user, user=user,
invoke_from=InvokeFrom.DEBUGGER, invoke_from=InvokeFrom.DEBUGGER,
application_generate_entity=application_generate_entity, application_generate_entity=application_generate_entity,
stream=stream stream=stream,
) )
def _generate_worker(self, flask_app: Flask, def _generate_worker(
application_generate_entity: WorkflowAppGenerateEntity, self,
queue_manager: AppQueueManager, flask_app: Flask,
context: contextvars.Context) -> None: application_generate_entity: WorkflowAppGenerateEntity,
queue_manager: AppQueueManager,
context: contextvars.Context,
workflow_thread_pool_id: Optional[str] = None,
) -> None:
""" """
Generate worker in a new thread. Generate worker in a new thread.
:param flask_app: Flask app :param flask_app: Flask app
:param application_generate_entity: application generate entity :param application_generate_entity: application generate entity
:param queue_manager: queue manager :param queue_manager: queue manager
:param workflow_thread_pool_id: workflow thread pool id
:return: :return:
""" """
for var, val in context.items(): for var, val in context.items():
@ -224,50 +246,40 @@ class WorkflowAppGenerator(BaseAppGenerator):
with flask_app.app_context(): with flask_app.app_context():
try: try:
# workflow app # workflow app
runner = WorkflowAppRunner() runner = WorkflowAppRunner(
if application_generate_entity.single_iteration_run: application_generate_entity=application_generate_entity,
single_iteration_run = application_generate_entity.single_iteration_run queue_manager=queue_manager,
runner.single_iteration_run( workflow_thread_pool_id=workflow_thread_pool_id,
app_id=application_generate_entity.app_config.app_id, )
workflow_id=application_generate_entity.app_config.workflow_id,
queue_manager=queue_manager, runner.run()
inputs=single_iteration_run.inputs, except GenerateTaskStoppedError:
node_id=single_iteration_run.node_id,
user_id=application_generate_entity.user_id
)
else:
runner.run(
application_generate_entity=application_generate_entity,
queue_manager=queue_manager
)
except GenerateTaskStoppedException:
pass pass
except InvokeAuthorizationError: except InvokeAuthorizationError:
queue_manager.publish_error( queue_manager.publish_error(
InvokeAuthorizationError('Incorrect API key provided'), InvokeAuthorizationError("Incorrect API key provided"), PublishFrom.APPLICATION_MANAGER
PublishFrom.APPLICATION_MANAGER
) )
except ValidationError as e: except ValidationError as e:
logger.exception("Validation Error when generating") logger.exception("Validation Error when generating")
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER) queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
except (ValueError, InvokeError) as e: except (ValueError, InvokeError) as e:
if os.environ.get("DEBUG") and os.environ.get("DEBUG").lower() == 'true': if os.environ.get("DEBUG") and os.environ.get("DEBUG", "false").lower() == "true":
logger.exception("Error when generating") logger.exception("Error when generating")
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER) queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
except Exception as e: except Exception as e:
logger.exception("Unknown Error when generating") logger.exception("Unknown Error when generating")
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER) queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
finally: finally:
db.session.remove() db.session.close()
def _handle_response(self, application_generate_entity: WorkflowAppGenerateEntity, def _handle_response(
workflow: Workflow, self,
queue_manager: AppQueueManager, application_generate_entity: WorkflowAppGenerateEntity,
user: Union[Account, EndUser], workflow: Workflow,
stream: bool = False) -> Union[ queue_manager: AppQueueManager,
WorkflowAppBlockingResponse, user: Union[Account, EndUser],
Generator[WorkflowAppStreamResponse, None, None] stream: bool = False,
]: ) -> Union[WorkflowAppBlockingResponse, Generator[WorkflowAppStreamResponse, None, None]]:
""" """
Handle response. Handle response.
:param application_generate_entity: application generate entity :param application_generate_entity: application generate entity
@ -283,14 +295,14 @@ class WorkflowAppGenerator(BaseAppGenerator):
workflow=workflow, workflow=workflow,
queue_manager=queue_manager, queue_manager=queue_manager,
user=user, user=user,
stream=stream stream=stream,
) )
try: try:
return generate_task_pipeline.process() return generate_task_pipeline.process()
except ValueError as e: except ValueError as e:
if e.args[0] == "I/O operation on closed file.": # ignore this error if e.args[0] == "I/O operation on closed file.": # ignore this error
raise GenerateTaskStoppedException() raise GenerateTaskStoppedError()
else: else:
logger.exception(e) logger.exception(e)
raise e raise e

@ -1,4 +1,4 @@
from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedException, PublishFrom from core.app.apps.base_app_queue_manager import AppQueueManager, GenerateTaskStoppedError, PublishFrom
from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.app_invoke_entities import InvokeFrom
from core.app.entities.queue_entities import ( from core.app.entities.queue_entities import (
AppQueueEvent, AppQueueEvent,
@ -12,10 +12,7 @@ from core.app.entities.queue_entities import (
class WorkflowAppQueueManager(AppQueueManager): class WorkflowAppQueueManager(AppQueueManager):
def __init__(self, task_id: str, def __init__(self, task_id: str, user_id: str, invoke_from: InvokeFrom, app_mode: str) -> None:
user_id: str,
invoke_from: InvokeFrom,
app_mode: str) -> None:
super().__init__(task_id, user_id, invoke_from) super().__init__(task_id, user_id, invoke_from)
self._app_mode = app_mode self._app_mode = app_mode
@ -27,20 +24,19 @@ class WorkflowAppQueueManager(AppQueueManager):
:param pub_from: :param pub_from:
:return: :return:
""" """
message = WorkflowQueueMessage( message = WorkflowQueueMessage(task_id=self._task_id, app_mode=self._app_mode, event=event)
task_id=self._task_id,
app_mode=self._app_mode,
event=event
)
self._q.put(message) self._q.put(message)
if isinstance(event, QueueStopEvent if isinstance(
| QueueErrorEvent event,
| QueueMessageEndEvent QueueStopEvent
| QueueWorkflowSucceededEvent | QueueErrorEvent
| QueueWorkflowFailedEvent): | QueueMessageEndEvent
| QueueWorkflowSucceededEvent
| QueueWorkflowFailedEvent,
):
self.stop_listen() self.stop_listen()
if pub_from == PublishFrom.APPLICATION_MANAGER and self._is_stopped(): if pub_from == PublishFrom.APPLICATION_MANAGER and self._is_stopped():
raise GenerateTaskStoppedException() raise GenerateTaskStoppedError()

@ -4,129 +4,125 @@ from typing import Optional, cast
from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.apps.base_app_queue_manager import AppQueueManager
from core.app.apps.workflow.app_config_manager import WorkflowAppConfig from core.app.apps.workflow.app_config_manager import WorkflowAppConfig
from core.app.apps.workflow.workflow_event_trigger_callback import WorkflowEventTriggerCallback from core.app.apps.workflow_app_runner import WorkflowBasedAppRunner
from core.app.apps.workflow_logging_callback import WorkflowLoggingCallback from core.app.apps.workflow_logging_callback import WorkflowLoggingCallback
from core.app.entities.app_invoke_entities import ( from core.app.entities.app_invoke_entities import (
InvokeFrom, InvokeFrom,
WorkflowAppGenerateEntity, WorkflowAppGenerateEntity,
) )
from core.workflow.callbacks.base_workflow_callback import WorkflowCallback from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
from core.workflow.entities.node_entities import UserFrom
from core.workflow.entities.variable_pool import VariablePool from core.workflow.entities.variable_pool import VariablePool
from core.workflow.enums import SystemVariableKey from core.workflow.enums import SystemVariableKey
from core.workflow.nodes.base_node import UserFrom from core.workflow.workflow_entry import WorkflowEntry
from core.workflow.workflow_engine_manager import WorkflowEngineManager
from extensions.ext_database import db from extensions.ext_database import db
from models.model import App, EndUser from models.model import App, EndUser
from models.workflow import Workflow from models.workflow import WorkflowType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class WorkflowAppRunner: class WorkflowAppRunner(WorkflowBasedAppRunner):
""" """
Workflow Application Runner Workflow Application Runner
""" """
def run(self, application_generate_entity: WorkflowAppGenerateEntity, queue_manager: AppQueueManager) -> None: def __init__(
self,
application_generate_entity: WorkflowAppGenerateEntity,
queue_manager: AppQueueManager,
workflow_thread_pool_id: Optional[str] = None,
) -> None:
"""
:param application_generate_entity: application generate entity
:param queue_manager: application queue manager
:param workflow_thread_pool_id: workflow thread pool id
"""
self.application_generate_entity = application_generate_entity
self.queue_manager = queue_manager
self.workflow_thread_pool_id = workflow_thread_pool_id
def run(self) -> None:
""" """
Run application Run application
:param application_generate_entity: application generate entity :param application_generate_entity: application generate entity
:param queue_manager: application queue manager :param queue_manager: application queue manager
:return: :return:
""" """
app_config = application_generate_entity.app_config app_config = self.application_generate_entity.app_config
app_config = cast(WorkflowAppConfig, app_config) app_config = cast(WorkflowAppConfig, app_config)
user_id = None user_id = None
if application_generate_entity.invoke_from in [InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API]: if self.application_generate_entity.invoke_from in [InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API]:
end_user = db.session.query(EndUser).filter(EndUser.id == application_generate_entity.user_id).first() end_user = db.session.query(EndUser).filter(EndUser.id == self.application_generate_entity.user_id).first()
if end_user: if end_user:
user_id = end_user.session_id user_id = end_user.session_id
else: else:
user_id = application_generate_entity.user_id user_id = self.application_generate_entity.user_id
app_record = db.session.query(App).filter(App.id == app_config.app_id).first() app_record = db.session.query(App).filter(App.id == app_config.app_id).first()
if not app_record: if not app_record:
raise ValueError('App not found') raise ValueError("App not found")
workflow = self.get_workflow(app_model=app_record, workflow_id=app_config.workflow_id) workflow = self.get_workflow(app_model=app_record, workflow_id=app_config.workflow_id)
if not workflow: if not workflow:
raise ValueError('Workflow not initialized') raise ValueError("Workflow not initialized")
inputs = application_generate_entity.inputs
files = application_generate_entity.files
db.session.close() db.session.close()
workflow_callbacks: list[WorkflowCallback] = [ workflow_callbacks: list[WorkflowCallback] = []
WorkflowEventTriggerCallback(queue_manager=queue_manager, workflow=workflow) if bool(os.environ.get("DEBUG", "False").lower() == "true"):
]
if bool(os.environ.get('DEBUG', 'False').lower() == 'true'):
workflow_callbacks.append(WorkflowLoggingCallback()) workflow_callbacks.append(WorkflowLoggingCallback())
# Create a variable pool. # if only single iteration run is requested
system_inputs = { if self.application_generate_entity.single_iteration_run:
SystemVariableKey.FILES: files, # if only single iteration run is requested
SystemVariableKey.USER_ID: user_id, graph, variable_pool = self._get_graph_and_variable_pool_of_single_iteration(
} workflow=workflow,
variable_pool = VariablePool( node_id=self.application_generate_entity.single_iteration_run.node_id,
system_variables=system_inputs, user_inputs=self.application_generate_entity.single_iteration_run.inputs,
user_inputs=inputs, )
environment_variables=workflow.environment_variables, else:
conversation_variables=[], inputs = self.application_generate_entity.inputs
) files = self.application_generate_entity.files
# Create a variable pool.
system_inputs = {
SystemVariableKey.FILES: files,
SystemVariableKey.USER_ID: user_id,
}
variable_pool = VariablePool(
system_variables=system_inputs,
user_inputs=inputs,
environment_variables=workflow.environment_variables,
conversation_variables=[],
)
# init graph
graph = self._init_graph(graph_config=workflow.graph_dict)
# RUN WORKFLOW # RUN WORKFLOW
workflow_engine_manager = WorkflowEngineManager() workflow_entry = WorkflowEntry(
workflow_engine_manager.run_workflow( tenant_id=workflow.tenant_id,
workflow=workflow, app_id=workflow.app_id,
user_id=application_generate_entity.user_id, workflow_id=workflow.id,
user_from=UserFrom.ACCOUNT workflow_type=WorkflowType.value_of(workflow.type),
if application_generate_entity.invoke_from in [InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER] graph=graph,
else UserFrom.END_USER, graph_config=workflow.graph_dict,
invoke_from=application_generate_entity.invoke_from, user_id=self.application_generate_entity.user_id,
callbacks=workflow_callbacks, user_from=(
call_depth=application_generate_entity.call_depth, UserFrom.ACCOUNT
if self.application_generate_entity.invoke_from in [InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER]
else UserFrom.END_USER
),
invoke_from=self.application_generate_entity.invoke_from,
call_depth=self.application_generate_entity.call_depth,
variable_pool=variable_pool, variable_pool=variable_pool,
thread_pool_id=self.workflow_thread_pool_id,
) )
def single_iteration_run( generator = workflow_entry.run(callbacks=workflow_callbacks)
self, app_id: str, workflow_id: str, queue_manager: AppQueueManager, inputs: dict, node_id: str, user_id: str
) -> None:
"""
Single iteration run
"""
app_record = db.session.query(App).filter(App.id == app_id).first()
if not app_record:
raise ValueError('App not found')
if not app_record.workflow_id:
raise ValueError('Workflow not initialized')
workflow = self.get_workflow(app_model=app_record, workflow_id=workflow_id)
if not workflow:
raise ValueError('Workflow not initialized')
workflow_callbacks = [WorkflowEventTriggerCallback(queue_manager=queue_manager, workflow=workflow)]
workflow_engine_manager = WorkflowEngineManager()
workflow_engine_manager.single_step_run_iteration_workflow_node(
workflow=workflow, node_id=node_id, user_id=user_id, user_inputs=inputs, callbacks=workflow_callbacks
)
def get_workflow(self, app_model: App, workflow_id: str) -> Optional[Workflow]:
"""
Get workflow
"""
# fetch 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 == workflow_id
)
.first()
)
# return workflow for event in generator:
return workflow self._handle_event(workflow_entry, event)

@ -35,8 +35,9 @@ class WorkflowAppGenerateResponseConverter(AppGenerateResponseConverter):
return cls.convert_blocking_full_response(blocking_response) return cls.convert_blocking_full_response(blocking_response)
@classmethod @classmethod
def convert_stream_full_response(cls, stream_response: Generator[WorkflowAppStreamResponse, None, None]) \ def convert_stream_full_response(
-> Generator[str, None, None]: cls, stream_response: Generator[WorkflowAppStreamResponse, None, None]
) -> Generator[str, None, None]:
""" """
Convert stream full response. Convert stream full response.
:param stream_response: stream response :param stream_response: stream response
@ -47,12 +48,12 @@ class WorkflowAppGenerateResponseConverter(AppGenerateResponseConverter):
sub_stream_response = chunk.stream_response sub_stream_response = chunk.stream_response
if isinstance(sub_stream_response, PingStreamResponse): if isinstance(sub_stream_response, PingStreamResponse):
yield 'ping' yield "ping"
continue continue
response_chunk = { response_chunk = {
'event': sub_stream_response.event.value, "event": sub_stream_response.event.value,
'workflow_run_id': chunk.workflow_run_id, "workflow_run_id": chunk.workflow_run_id,
} }
if isinstance(sub_stream_response, ErrorStreamResponse): if isinstance(sub_stream_response, ErrorStreamResponse):
@ -63,8 +64,9 @@ class WorkflowAppGenerateResponseConverter(AppGenerateResponseConverter):
yield json.dumps(response_chunk) yield json.dumps(response_chunk)
@classmethod @classmethod
def convert_stream_simple_response(cls, stream_response: Generator[WorkflowAppStreamResponse, None, None]) \ def convert_stream_simple_response(
-> Generator[str, None, None]: cls, stream_response: Generator[WorkflowAppStreamResponse, None, None]
) -> Generator[str, None, None]:
""" """
Convert stream simple response. Convert stream simple response.
:param stream_response: stream response :param stream_response: stream response
@ -75,12 +77,12 @@ class WorkflowAppGenerateResponseConverter(AppGenerateResponseConverter):
sub_stream_response = chunk.stream_response sub_stream_response = chunk.stream_response
if isinstance(sub_stream_response, PingStreamResponse): if isinstance(sub_stream_response, PingStreamResponse):
yield 'ping' yield "ping"
continue continue
response_chunk = { response_chunk = {
'event': sub_stream_response.event.value, "event": sub_stream_response.event.value,
'workflow_run_id': chunk.workflow_run_id, "workflow_run_id": chunk.workflow_run_id,
} }
if isinstance(sub_stream_response, ErrorStreamResponse): if isinstance(sub_stream_response, ErrorStreamResponse):

@ -1,3 +1,4 @@
import json
import logging import logging
import time import time
from collections.abc import Generator from collections.abc import Generator
@ -15,10 +16,12 @@ from core.app.entities.queue_entities import (
QueueIterationCompletedEvent, QueueIterationCompletedEvent,
QueueIterationNextEvent, QueueIterationNextEvent,
QueueIterationStartEvent, QueueIterationStartEvent,
QueueMessageReplaceEvent,
QueueNodeFailedEvent, QueueNodeFailedEvent,
QueueNodeStartedEvent, QueueNodeStartedEvent,
QueueNodeSucceededEvent, QueueNodeSucceededEvent,
QueueParallelBranchRunFailedEvent,
QueueParallelBranchRunStartedEvent,
QueueParallelBranchRunSucceededEvent,
QueuePingEvent, QueuePingEvent,
QueueStopEvent, QueueStopEvent,
QueueTextChunkEvent, QueueTextChunkEvent,
@ -32,19 +35,16 @@ from core.app.entities.task_entities import (
MessageAudioStreamResponse, MessageAudioStreamResponse,
StreamResponse, StreamResponse,
TextChunkStreamResponse, TextChunkStreamResponse,
TextReplaceStreamResponse,
WorkflowAppBlockingResponse, WorkflowAppBlockingResponse,
WorkflowAppStreamResponse, WorkflowAppStreamResponse,
WorkflowFinishStreamResponse, WorkflowFinishStreamResponse,
WorkflowStreamGenerateNodes, WorkflowStartStreamResponse,
WorkflowTaskState, WorkflowTaskState,
) )
from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline
from core.app.task_pipeline.workflow_cycle_manage import WorkflowCycleManage from core.app.task_pipeline.workflow_cycle_manage import WorkflowCycleManage
from core.ops.ops_trace_manager import TraceQueueManager from core.ops.ops_trace_manager import TraceQueueManager
from core.workflow.entities.node_entities import NodeType
from core.workflow.enums import SystemVariableKey from core.workflow.enums import SystemVariableKey
from core.workflow.nodes.end.end_node import EndNode
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 from models.model import EndUser
@ -52,8 +52,8 @@ from models.workflow import (
Workflow, Workflow,
WorkflowAppLog, WorkflowAppLog,
WorkflowAppLogCreatedFrom, WorkflowAppLogCreatedFrom,
WorkflowNodeExecution,
WorkflowRun, WorkflowRun,
WorkflowRunStatus,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -63,18 +63,21 @@ class WorkflowAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCycleMa
""" """
WorkflowAppGenerateTaskPipeline is a class that generate stream output and state management for Application. WorkflowAppGenerateTaskPipeline is a class that generate stream output and state management for Application.
""" """
_workflow: Workflow _workflow: Workflow
_user: Union[Account, EndUser] _user: Union[Account, EndUser]
_task_state: WorkflowTaskState _task_state: WorkflowTaskState
_application_generate_entity: WorkflowAppGenerateEntity _application_generate_entity: WorkflowAppGenerateEntity
_workflow_system_variables: dict[SystemVariableKey, Any] _workflow_system_variables: dict[SystemVariableKey, Any]
_iteration_nested_relations: dict[str, list[str]]
def __init__(self, application_generate_entity: WorkflowAppGenerateEntity, def __init__(
workflow: Workflow, self,
queue_manager: AppQueueManager, application_generate_entity: WorkflowAppGenerateEntity,
user: Union[Account, EndUser], workflow: Workflow,
stream: bool) -> None: queue_manager: AppQueueManager,
user: Union[Account, EndUser],
stream: bool,
) -> None:
""" """
Initialize GenerateTaskPipeline. Initialize GenerateTaskPipeline.
:param application_generate_entity: application generate entity :param application_generate_entity: application generate entity
@ -93,14 +96,10 @@ class WorkflowAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCycleMa
self._workflow = workflow self._workflow = workflow
self._workflow_system_variables = { self._workflow_system_variables = {
SystemVariableKey.FILES: application_generate_entity.files, SystemVariableKey.FILES: application_generate_entity.files,
SystemVariableKey.USER_ID: user_id SystemVariableKey.USER_ID: user_id,
} }
self._task_state = WorkflowTaskState( self._task_state = WorkflowTaskState()
iteration_nested_node_ids=[]
)
self._stream_generate_nodes = self._get_stream_generate_nodes()
self._iteration_nested_relations = self._get_iteration_nested_relations(self._workflow.graph_dict)
def process(self) -> Union[WorkflowAppBlockingResponse, Generator[WorkflowAppStreamResponse, None, None]]: def process(self) -> Union[WorkflowAppBlockingResponse, Generator[WorkflowAppStreamResponse, None, None]]:
""" """
@ -111,16 +110,13 @@ class WorkflowAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCycleMa
db.session.refresh(self._user) db.session.refresh(self._user)
db.session.close() db.session.close()
generator = self._wrapper_process_stream_response( generator = self._wrapper_process_stream_response(trace_manager=self._application_generate_entity.trace_manager)
trace_manager=self._application_generate_entity.trace_manager
)
if self._stream: if self._stream:
return self._to_stream_response(generator) return self._to_stream_response(generator)
else: else:
return self._to_blocking_response(generator) return self._to_blocking_response(generator)
def _to_blocking_response(self, generator: Generator[StreamResponse, None, None]) \ def _to_blocking_response(self, generator: Generator[StreamResponse, None, None]) -> WorkflowAppBlockingResponse:
-> WorkflowAppBlockingResponse:
""" """
To blocking response. To blocking response.
:return: :return:
@ -129,66 +125,69 @@ class WorkflowAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCycleMa
if isinstance(stream_response, ErrorStreamResponse): if isinstance(stream_response, ErrorStreamResponse):
raise stream_response.err raise stream_response.err
elif isinstance(stream_response, WorkflowFinishStreamResponse): elif isinstance(stream_response, WorkflowFinishStreamResponse):
workflow_run = db.session.query(WorkflowRun).filter(
WorkflowRun.id == self._task_state.workflow_run_id).first()
response = WorkflowAppBlockingResponse( response = WorkflowAppBlockingResponse(
task_id=self._application_generate_entity.task_id, task_id=self._application_generate_entity.task_id,
workflow_run_id=workflow_run.id, workflow_run_id=stream_response.data.id,
data=WorkflowAppBlockingResponse.Data( data=WorkflowAppBlockingResponse.Data(
id=workflow_run.id, id=stream_response.data.id,
workflow_id=workflow_run.workflow_id, workflow_id=stream_response.data.workflow_id,
status=workflow_run.status, status=stream_response.data.status,
outputs=workflow_run.outputs_dict, outputs=stream_response.data.outputs,
error=workflow_run.error, error=stream_response.data.error,
elapsed_time=workflow_run.elapsed_time, elapsed_time=stream_response.data.elapsed_time,
total_tokens=workflow_run.total_tokens, total_tokens=stream_response.data.total_tokens,
total_steps=workflow_run.total_steps, total_steps=stream_response.data.total_steps,
created_at=int(workflow_run.created_at.timestamp()), created_at=int(stream_response.data.created_at),
finished_at=int(workflow_run.finished_at.timestamp()) finished_at=int(stream_response.data.finished_at),
) ),
) )
return response return response
else: else:
continue continue
raise Exception('Queue listening stopped unexpectedly.') raise Exception("Queue listening stopped unexpectedly.")
def _to_stream_response(self, generator: Generator[StreamResponse, None, None]) \ def _to_stream_response(
-> Generator[WorkflowAppStreamResponse, None, None]: self, generator: Generator[StreamResponse, None, None]
) -> Generator[WorkflowAppStreamResponse, None, None]:
""" """
To stream response. To stream response.
:return: :return:
""" """
workflow_run_id = None
for stream_response in generator: for stream_response in generator:
yield WorkflowAppStreamResponse( if isinstance(stream_response, WorkflowStartStreamResponse):
workflow_run_id=self._task_state.workflow_run_id, workflow_run_id = stream_response.workflow_run_id
stream_response=stream_response
) yield WorkflowAppStreamResponse(workflow_run_id=workflow_run_id, stream_response=stream_response)
def _listenAudioMsg(self, publisher, task_id: str): def _listen_audio_msg(self, publisher, task_id: str):
if not publisher: if not publisher:
return None return None
audio_msg: AudioTrunk = publisher.checkAndGetAudio() audio_msg: AudioTrunk = publisher.check_and_get_audio()
if audio_msg and audio_msg.status != "finish": if audio_msg and audio_msg.status != "finish":
return MessageAudioStreamResponse(audio=audio_msg.audio, task_id=task_id) return MessageAudioStreamResponse(audio=audio_msg.audio, task_id=task_id)
return None return None
def _wrapper_process_stream_response(self, trace_manager: Optional[TraceQueueManager] = None) -> \ def _wrapper_process_stream_response(
Generator[StreamResponse, None, None]: self, trace_manager: Optional[TraceQueueManager] = None
) -> Generator[StreamResponse, None, None]:
publisher = None tts_publisher = None
task_id = self._application_generate_entity.task_id task_id = self._application_generate_entity.task_id
tenant_id = self._application_generate_entity.app_config.tenant_id tenant_id = self._application_generate_entity.app_config.tenant_id
features_dict = self._workflow.features_dict features_dict = self._workflow.features_dict
if features_dict.get('text_to_speech') and features_dict['text_to_speech'].get('enabled') and features_dict[ if (
'text_to_speech'].get('autoPlay') == 'enabled': features_dict.get("text_to_speech")
publisher = AppGeneratorTTSPublisher(tenant_id, features_dict['text_to_speech'].get('voice')) and features_dict["text_to_speech"].get("enabled")
for response in self._process_stream_response(publisher=publisher, trace_manager=trace_manager): and features_dict["text_to_speech"].get("autoPlay") == "enabled"
):
tts_publisher = AppGeneratorTTSPublisher(tenant_id, features_dict["text_to_speech"].get("voice"))
for response in self._process_stream_response(tts_publisher=tts_publisher, trace_manager=trace_manager):
while True: while True:
audio_response = self._listenAudioMsg(publisher, task_id=task_id) audio_response = self._listen_audio_msg(tts_publisher, task_id=task_id)
if audio_response: if audio_response:
yield audio_response yield audio_response
else: else:
@ -198,9 +197,9 @@ class WorkflowAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCycleMa
start_listener_time = time.time() start_listener_time = time.time()
while (time.time() - start_listener_time) < TTS_AUTO_PLAY_TIMEOUT: while (time.time() - start_listener_time) < TTS_AUTO_PLAY_TIMEOUT:
try: try:
if not publisher: if not tts_publisher:
break break
audio_trunk = publisher.checkAndGetAudio() audio_trunk = tts_publisher.check_and_get_audio()
if audio_trunk is None: if audio_trunk is None:
# release cpu # release cpu
# sleep 20 ms ( 40ms => 1280 byte audio file,20ms => 640 byte audio file) # sleep 20 ms ( 40ms => 1280 byte audio file,20ms => 640 byte audio file)
@ -213,105 +212,178 @@ class WorkflowAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCycleMa
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
break break
yield MessageAudioEndStreamResponse(audio='', task_id=task_id) yield MessageAudioEndStreamResponse(audio="", task_id=task_id)
def _process_stream_response( def _process_stream_response(
self, self,
publisher: AppGeneratorTTSPublisher, tts_publisher: Optional[AppGeneratorTTSPublisher] = None,
trace_manager: Optional[TraceQueueManager] = None trace_manager: Optional[TraceQueueManager] = None,
) -> Generator[StreamResponse, None, None]: ) -> Generator[StreamResponse, None, None]:
""" """
Process stream response. Process stream response.
:return: :return:
""" """
for message in self._queue_manager.listen(): graph_runtime_state = None
if publisher: workflow_run = None
publisher.publish(message=message)
event = message.event
if isinstance(event, QueueErrorEvent): for queue_message in self._queue_manager.listen():
event = queue_message.event
if isinstance(event, QueuePingEvent):
yield self._ping_stream_response()
elif isinstance(event, QueueErrorEvent):
err = self._handle_error(event) err = self._handle_error(event)
yield self._error_to_stream_response(err) yield self._error_to_stream_response(err)
break break
elif isinstance(event, QueueWorkflowStartedEvent): elif isinstance(event, QueueWorkflowStartedEvent):
workflow_run = self._handle_workflow_start() # override graph runtime state
graph_runtime_state = event.graph_runtime_state
# init workflow run
workflow_run = self._handle_workflow_run_start()
yield self._workflow_start_to_stream_response( yield self._workflow_start_to_stream_response(
task_id=self._application_generate_entity.task_id, task_id=self._application_generate_entity.task_id, workflow_run=workflow_run
workflow_run=workflow_run
) )
elif isinstance(event, QueueNodeStartedEvent): elif isinstance(event, QueueNodeStartedEvent):
workflow_node_execution = self._handle_node_start(event) if not workflow_run:
raise Exception("Workflow run not initialized.")
workflow_node_execution = self._handle_node_execution_start(workflow_run=workflow_run, event=event)
# search stream_generate_routes if node id is answer start at node response = self._workflow_node_start_to_stream_response(
if not self._task_state.current_stream_generate_state and event.node_id in self._stream_generate_nodes: event=event,
self._task_state.current_stream_generate_state = self._stream_generate_nodes[event.node_id] task_id=self._application_generate_entity.task_id,
workflow_node_execution=workflow_node_execution,
)
# generate stream outputs when node started if response:
yield from self._generate_stream_outputs_when_node_started() yield response
elif isinstance(event, QueueNodeSucceededEvent):
workflow_node_execution = self._handle_workflow_node_execution_success(event)
yield self._workflow_node_start_to_stream_response( response = self._workflow_node_finish_to_stream_response(
event=event, event=event,
task_id=self._application_generate_entity.task_id, task_id=self._application_generate_entity.task_id,
workflow_node_execution=workflow_node_execution workflow_node_execution=workflow_node_execution,
) )
elif isinstance(event, QueueNodeSucceededEvent | QueueNodeFailedEvent):
workflow_node_execution = self._handle_node_finished(event)
yield self._workflow_node_finish_to_stream_response( if response:
yield response
elif isinstance(event, QueueNodeFailedEvent):
workflow_node_execution = self._handle_workflow_node_execution_failed(event)
response = self._workflow_node_finish_to_stream_response(
event=event,
task_id=self._application_generate_entity.task_id, task_id=self._application_generate_entity.task_id,
workflow_node_execution=workflow_node_execution workflow_node_execution=workflow_node_execution,
)
if response:
yield response
elif isinstance(event, QueueParallelBranchRunStartedEvent):
if not workflow_run:
raise Exception("Workflow run not initialized.")
yield self._workflow_parallel_branch_start_to_stream_response(
task_id=self._application_generate_entity.task_id, workflow_run=workflow_run, event=event
)
elif isinstance(event, QueueParallelBranchRunSucceededEvent | QueueParallelBranchRunFailedEvent):
if not workflow_run:
raise Exception("Workflow run not initialized.")
yield self._workflow_parallel_branch_finished_to_stream_response(
task_id=self._application_generate_entity.task_id, workflow_run=workflow_run, event=event
) )
elif isinstance(event, QueueIterationStartEvent):
if not workflow_run:
raise Exception("Workflow run not initialized.")
if isinstance(event, QueueNodeFailedEvent): yield self._workflow_iteration_start_to_stream_response(
yield from self._handle_iteration_exception( task_id=self._application_generate_entity.task_id, workflow_run=workflow_run, event=event
task_id=self._application_generate_entity.task_id, )
error=f'Child node failed: {event.error}' elif isinstance(event, QueueIterationNextEvent):
) if not workflow_run:
elif isinstance(event, QueueIterationStartEvent | QueueIterationNextEvent | QueueIterationCompletedEvent): raise Exception("Workflow run not initialized.")
if isinstance(event, QueueIterationNextEvent):
# clear ran node execution infos of current iteration yield self._workflow_iteration_next_to_stream_response(
iteration_relations = self._iteration_nested_relations.get(event.node_id) task_id=self._application_generate_entity.task_id, workflow_run=workflow_run, event=event
if iteration_relations: )
for node_id in iteration_relations: elif isinstance(event, QueueIterationCompletedEvent):
self._task_state.ran_node_execution_infos.pop(node_id, None) if not workflow_run:
raise Exception("Workflow run not initialized.")
yield self._handle_iteration_to_stream_response(self._application_generate_entity.task_id, event)
self._handle_iteration_operation(event) yield self._workflow_iteration_completed_to_stream_response(
elif isinstance(event, QueueStopEvent | QueueWorkflowSucceededEvent | QueueWorkflowFailedEvent): task_id=self._application_generate_entity.task_id, workflow_run=workflow_run, event=event
workflow_run = self._handle_workflow_finished( )
event, trace_manager=trace_manager elif isinstance(event, QueueWorkflowSucceededEvent):
if not workflow_run:
raise Exception("Workflow run not initialized.")
if not graph_runtime_state:
raise Exception("Graph runtime state not initialized.")
workflow_run = self._handle_workflow_run_success(
workflow_run=workflow_run,
start_at=graph_runtime_state.start_at,
total_tokens=graph_runtime_state.total_tokens,
total_steps=graph_runtime_state.node_run_steps,
outputs=json.dumps(event.outputs)
if isinstance(event, QueueWorkflowSucceededEvent) and event.outputs
else None,
conversation_id=None,
trace_manager=trace_manager,
) )
# save workflow app log # save workflow app log
self._save_workflow_app_log(workflow_run) self._save_workflow_app_log(workflow_run)
yield self._workflow_finish_to_stream_response( yield self._workflow_finish_to_stream_response(
task_id=self._application_generate_entity.task_id, task_id=self._application_generate_entity.task_id, workflow_run=workflow_run
workflow_run=workflow_run )
elif isinstance(event, QueueWorkflowFailedEvent | QueueStopEvent):
if not workflow_run:
raise Exception("Workflow run not initialized.")
if not graph_runtime_state:
raise Exception("Graph runtime state not initialized.")
workflow_run = self._handle_workflow_run_failed(
workflow_run=workflow_run,
start_at=graph_runtime_state.start_at,
total_tokens=graph_runtime_state.total_tokens,
total_steps=graph_runtime_state.node_run_steps,
status=WorkflowRunStatus.FAILED
if isinstance(event, QueueWorkflowFailedEvent)
else WorkflowRunStatus.STOPPED,
error=event.error if isinstance(event, QueueWorkflowFailedEvent) else event.get_stop_reason(),
conversation_id=None,
trace_manager=trace_manager,
)
# save workflow app log
self._save_workflow_app_log(workflow_run)
yield self._workflow_finish_to_stream_response(
task_id=self._application_generate_entity.task_id, workflow_run=workflow_run
) )
elif isinstance(event, QueueTextChunkEvent): elif isinstance(event, QueueTextChunkEvent):
delta_text = event.text delta_text = event.text
if delta_text is None: if delta_text is None:
continue continue
if not self._is_stream_out_support( # only publish tts message at text chunk streaming
event=event if tts_publisher:
): tts_publisher.publish(message=queue_message)
continue
self._task_state.answer += delta_text self._task_state.answer += delta_text
yield self._text_chunk_to_stream_response(delta_text) yield self._text_chunk_to_stream_response(
elif isinstance(event, QueueMessageReplaceEvent): delta_text, from_variable_selector=event.from_variable_selector
yield self._text_replace_to_stream_response(event.text) )
elif isinstance(event, QueuePingEvent):
yield self._ping_stream_response()
else: else:
continue continue
if publisher: if tts_publisher:
publisher.publish(None) tts_publisher.publish(None)
def _save_workflow_app_log(self, workflow_run: WorkflowRun) -> None: def _save_workflow_app_log(self, workflow_run: WorkflowRun) -> None:
""" """
@ -329,20 +401,22 @@ class WorkflowAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCycleMa
# not save log for debugging # not save log for debugging
return return
workflow_app_log = WorkflowAppLog( workflow_app_log = WorkflowAppLog()
tenant_id=workflow_run.tenant_id, workflow_app_log.tenant_id = workflow_run.tenant_id
app_id=workflow_run.app_id, workflow_app_log.app_id = workflow_run.app_id
workflow_id=workflow_run.workflow_id, workflow_app_log.workflow_id = workflow_run.workflow_id
workflow_run_id=workflow_run.id, workflow_app_log.workflow_run_id = workflow_run.id
created_from=created_from.value, workflow_app_log.created_from = created_from.value
created_by_role=('account' if isinstance(self._user, Account) else 'end_user'), workflow_app_log.created_by_role = "account" if isinstance(self._user, Account) else "end_user"
created_by=self._user.id, workflow_app_log.created_by = self._user.id
)
db.session.add(workflow_app_log) db.session.add(workflow_app_log)
db.session.commit() db.session.commit()
db.session.close() db.session.close()
def _text_chunk_to_stream_response(self, text: str) -> TextChunkStreamResponse: def _text_chunk_to_stream_response(
self, text: str, from_variable_selector: Optional[list[str]] = None
) -> TextChunkStreamResponse:
""" """
Handle completed event. Handle completed event.
:param text: text :param text: text
@ -350,184 +424,7 @@ class WorkflowAppGenerateTaskPipeline(BasedGenerateTaskPipeline, WorkflowCycleMa
""" """
response = TextChunkStreamResponse( response = TextChunkStreamResponse(
task_id=self._application_generate_entity.task_id, task_id=self._application_generate_entity.task_id,
data=TextChunkStreamResponse.Data(text=text) data=TextChunkStreamResponse.Data(text=text, from_variable_selector=from_variable_selector),
) )
return response return response
def _text_replace_to_stream_response(self, text: str) -> TextReplaceStreamResponse:
"""
Text replace to stream response.
:param text: text
:return:
"""
return TextReplaceStreamResponse(
task_id=self._application_generate_entity.task_id,
text=TextReplaceStreamResponse.Data(text=text)
)
def _get_stream_generate_nodes(self) -> dict[str, WorkflowStreamGenerateNodes]:
"""
Get stream generate nodes.
:return:
"""
# find all answer nodes
graph = self._workflow.graph_dict
end_node_configs = [
node for node in graph['nodes']
if node.get('data', {}).get('type') == NodeType.END.value
]
# parse stream output node value selectors of end nodes
stream_generate_routes = {}
for node_config in end_node_configs:
# get generate route for stream output
end_node_id = node_config['id']
generate_nodes = EndNode.extract_generate_nodes(graph, node_config)
start_node_ids = self._get_end_start_at_node_ids(graph, end_node_id)
if not start_node_ids:
continue
for start_node_id in start_node_ids:
stream_generate_routes[start_node_id] = WorkflowStreamGenerateNodes(
end_node_id=end_node_id,
stream_node_ids=generate_nodes
)
return stream_generate_routes
def _get_end_start_at_node_ids(self, graph: dict, target_node_id: str) \
-> list[str]:
"""
Get end start at node id.
:param graph: graph
:param target_node_id: target node ID
:return:
"""
nodes = graph.get('nodes')
edges = graph.get('edges')
# fetch all ingoing edges from source node
ingoing_edges = []
for edge in edges:
if edge.get('target') == target_node_id:
ingoing_edges.append(edge)
if not ingoing_edges:
return []
start_node_ids = []
for ingoing_edge in ingoing_edges:
source_node_id = ingoing_edge.get('source')
source_node = next((node for node in nodes if node.get('id') == source_node_id), None)
if not source_node:
continue
node_type = source_node.get('data', {}).get('type')
node_iteration_id = source_node.get('data', {}).get('iteration_id')
iteration_start_node_id = None
if node_iteration_id:
iteration_node = next((node for node in nodes if node.get('id') == node_iteration_id), None)
iteration_start_node_id = iteration_node.get('data', {}).get('start_node_id')
if node_type in [
NodeType.IF_ELSE.value,
NodeType.QUESTION_CLASSIFIER.value
]:
start_node_id = target_node_id
start_node_ids.append(start_node_id)
elif node_type == NodeType.START.value or \
node_iteration_id is not None and iteration_start_node_id == source_node.get('id'):
start_node_id = source_node_id
start_node_ids.append(start_node_id)
else:
sub_start_node_ids = self._get_end_start_at_node_ids(graph, source_node_id)
if sub_start_node_ids:
start_node_ids.extend(sub_start_node_ids)
return start_node_ids
def _generate_stream_outputs_when_node_started(self) -> Generator:
"""
Generate stream outputs.
:return:
"""
if self._task_state.current_stream_generate_state:
stream_node_ids = self._task_state.current_stream_generate_state.stream_node_ids
for node_id, node_execution_info in self._task_state.ran_node_execution_infos.items():
if node_id not in stream_node_ids:
continue
node_execution_info = self._task_state.ran_node_execution_infos[node_id]
# get chunk node execution
route_chunk_node_execution = db.session.query(WorkflowNodeExecution).filter(
WorkflowNodeExecution.id == node_execution_info.workflow_node_execution_id).first()
if not route_chunk_node_execution:
continue
outputs = route_chunk_node_execution.outputs_dict
if not outputs:
continue
# get value from outputs
text = outputs.get('text')
if text:
self._task_state.answer += text
yield self._text_chunk_to_stream_response(text)
db.session.close()
def _is_stream_out_support(self, event: QueueTextChunkEvent) -> bool:
"""
Is stream out support
:param event: queue text chunk event
:return:
"""
if not event.metadata:
return False
if 'node_id' not in event.metadata:
return False
node_id = event.metadata.get('node_id')
node_type = event.metadata.get('node_type')
stream_output_value_selector = event.metadata.get('value_selector')
if not stream_output_value_selector:
return False
if not self._task_state.current_stream_generate_state:
return False
if node_id not in self._task_state.current_stream_generate_state.stream_node_ids:
return False
if node_type != NodeType.LLM:
# only LLM support chunk stream output
return False
return True
def _get_iteration_nested_relations(self, graph: dict) -> dict[str, list[str]]:
"""
Get iteration nested relations.
:param graph: graph
:return:
"""
nodes = graph.get('nodes')
iteration_ids = [node.get('id') for node in nodes
if node.get('data', {}).get('type') in [
NodeType.ITERATION.value,
NodeType.LOOP.value,
]]
return {
iteration_id: [
node.get('id') for node in nodes if node.get('data', {}).get('iteration_id') == iteration_id
] for iteration_id in iteration_ids
}

@ -1,200 +0,0 @@
from typing import Any, Optional
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
from core.app.entities.queue_entities import (
AppQueueEvent,
QueueIterationCompletedEvent,
QueueIterationNextEvent,
QueueIterationStartEvent,
QueueNodeFailedEvent,
QueueNodeStartedEvent,
QueueNodeSucceededEvent,
QueueTextChunkEvent,
QueueWorkflowFailedEvent,
QueueWorkflowStartedEvent,
QueueWorkflowSucceededEvent,
)
from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
from core.workflow.entities.base_node_data_entities import BaseNodeData
from core.workflow.entities.node_entities import NodeType
from models.workflow import Workflow
class WorkflowEventTriggerCallback(WorkflowCallback):
def __init__(self, queue_manager: AppQueueManager, workflow: Workflow):
self._queue_manager = queue_manager
def on_workflow_run_started(self) -> None:
"""
Workflow run started
"""
self._queue_manager.publish(
QueueWorkflowStartedEvent(),
PublishFrom.APPLICATION_MANAGER
)
def on_workflow_run_succeeded(self) -> None:
"""
Workflow run succeeded
"""
self._queue_manager.publish(
QueueWorkflowSucceededEvent(),
PublishFrom.APPLICATION_MANAGER
)
def on_workflow_run_failed(self, error: str) -> None:
"""
Workflow run failed
"""
self._queue_manager.publish(
QueueWorkflowFailedEvent(
error=error
),
PublishFrom.APPLICATION_MANAGER
)
def on_workflow_node_execute_started(self, node_id: str,
node_type: NodeType,
node_data: BaseNodeData,
node_run_index: int = 1,
predecessor_node_id: Optional[str] = None) -> None:
"""
Workflow node execute started
"""
self._queue_manager.publish(
QueueNodeStartedEvent(
node_id=node_id,
node_type=node_type,
node_data=node_data,
node_run_index=node_run_index,
predecessor_node_id=predecessor_node_id
),
PublishFrom.APPLICATION_MANAGER
)
def on_workflow_node_execute_succeeded(self, node_id: str,
node_type: NodeType,
node_data: BaseNodeData,
inputs: Optional[dict] = None,
process_data: Optional[dict] = None,
outputs: Optional[dict] = None,
execution_metadata: Optional[dict] = None) -> None:
"""
Workflow node execute succeeded
"""
self._queue_manager.publish(
QueueNodeSucceededEvent(
node_id=node_id,
node_type=node_type,
node_data=node_data,
inputs=inputs,
process_data=process_data,
outputs=outputs,
execution_metadata=execution_metadata
),
PublishFrom.APPLICATION_MANAGER
)
def on_workflow_node_execute_failed(self, node_id: str,
node_type: NodeType,
node_data: BaseNodeData,
error: str,
inputs: Optional[dict] = None,
outputs: Optional[dict] = None,
process_data: Optional[dict] = None) -> None:
"""
Workflow node execute failed
"""
self._queue_manager.publish(
QueueNodeFailedEvent(
node_id=node_id,
node_type=node_type,
node_data=node_data,
inputs=inputs,
outputs=outputs,
process_data=process_data,
error=error
),
PublishFrom.APPLICATION_MANAGER
)
def on_node_text_chunk(self, node_id: str, text: str, metadata: Optional[dict] = None) -> None:
"""
Publish text chunk
"""
self._queue_manager.publish(
QueueTextChunkEvent(
text=text,
metadata={
"node_id": node_id,
**metadata
}
), PublishFrom.APPLICATION_MANAGER
)
def on_workflow_iteration_started(self,
node_id: str,
node_type: NodeType,
node_run_index: int = 1,
node_data: Optional[BaseNodeData] = None,
inputs: dict = None,
predecessor_node_id: Optional[str] = None,
metadata: Optional[dict] = None) -> None:
"""
Publish iteration started
"""
self._queue_manager.publish(
QueueIterationStartEvent(
node_id=node_id,
node_type=node_type,
node_run_index=node_run_index,
node_data=node_data,
inputs=inputs,
predecessor_node_id=predecessor_node_id,
metadata=metadata
),
PublishFrom.APPLICATION_MANAGER
)
def on_workflow_iteration_next(self, node_id: str,
node_type: NodeType,
index: int,
node_run_index: int,
output: Optional[Any]) -> None:
"""
Publish iteration next
"""
self._queue_manager.publish(
QueueIterationNextEvent(
node_id=node_id,
node_type=node_type,
index=index,
node_run_index=node_run_index,
output=output
),
PublishFrom.APPLICATION_MANAGER
)
def on_workflow_iteration_completed(self, node_id: str,
node_type: NodeType,
node_run_index: int,
outputs: dict) -> None:
"""
Publish iteration completed
"""
self._queue_manager.publish(
QueueIterationCompletedEvent(
node_id=node_id,
node_type=node_type,
node_run_index=node_run_index,
outputs=outputs
),
PublishFrom.APPLICATION_MANAGER
)
def on_event(self, event: AppQueueEvent) -> None:
"""
Publish event
"""
pass

@ -0,0 +1,371 @@
from collections.abc import Mapping
from typing import Any, Optional, cast
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
from core.app.apps.base_app_runner import AppRunner
from core.app.entities.queue_entities import (
AppQueueEvent,
QueueIterationCompletedEvent,
QueueIterationNextEvent,
QueueIterationStartEvent,
QueueNodeFailedEvent,
QueueNodeStartedEvent,
QueueNodeSucceededEvent,
QueueParallelBranchRunFailedEvent,
QueueParallelBranchRunStartedEvent,
QueueParallelBranchRunSucceededEvent,
QueueRetrieverResourcesEvent,
QueueTextChunkEvent,
QueueWorkflowFailedEvent,
QueueWorkflowStartedEvent,
QueueWorkflowSucceededEvent,
)
from core.workflow.entities.node_entities import NodeType
from core.workflow.entities.variable_pool import VariablePool
from core.workflow.graph_engine.entities.event import (
GraphEngineEvent,
GraphRunFailedEvent,
GraphRunStartedEvent,
GraphRunSucceededEvent,
IterationRunFailedEvent,
IterationRunNextEvent,
IterationRunStartedEvent,
IterationRunSucceededEvent,
NodeRunFailedEvent,
NodeRunRetrieverResourceEvent,
NodeRunStartedEvent,
NodeRunStreamChunkEvent,
NodeRunSucceededEvent,
ParallelBranchRunFailedEvent,
ParallelBranchRunStartedEvent,
ParallelBranchRunSucceededEvent,
)
from core.workflow.graph_engine.entities.graph import Graph
from core.workflow.nodes.base_node import BaseNode
from core.workflow.nodes.iteration.entities import IterationNodeData
from core.workflow.nodes.node_mapping import node_classes
from core.workflow.workflow_entry import WorkflowEntry
from extensions.ext_database import db
from models.model import App
from models.workflow import Workflow
class WorkflowBasedAppRunner(AppRunner):
def __init__(self, queue_manager: AppQueueManager):
self.queue_manager = queue_manager
def _init_graph(self, graph_config: Mapping[str, Any]) -> Graph:
"""
Init graph
"""
if "nodes" not in graph_config or "edges" not in graph_config:
raise ValueError("nodes or edges not found in workflow graph")
if not isinstance(graph_config.get("nodes"), list):
raise ValueError("nodes in workflow graph must be a list")
if not isinstance(graph_config.get("edges"), list):
raise ValueError("edges in workflow graph must be a list")
# init graph
graph = Graph.init(graph_config=graph_config)
if not graph:
raise ValueError("graph not found in workflow")
return graph
def _get_graph_and_variable_pool_of_single_iteration(
self,
workflow: Workflow,
node_id: str,
user_inputs: dict,
) -> tuple[Graph, VariablePool]:
"""
Get variable pool of single iteration
"""
# fetch workflow graph
graph_config = workflow.graph_dict
if not graph_config:
raise ValueError("workflow graph not found")
graph_config = cast(dict[str, Any], graph_config)
if "nodes" not in graph_config or "edges" not in graph_config:
raise ValueError("nodes or edges not found in workflow graph")
if not isinstance(graph_config.get("nodes"), list):
raise ValueError("nodes in workflow graph must be a list")
if not isinstance(graph_config.get("edges"), list):
raise ValueError("edges in workflow graph must be a list")
# filter nodes only in iteration
node_configs = [
node
for node in graph_config.get("nodes", [])
if node.get("id") == node_id or node.get("data", {}).get("iteration_id", "") == node_id
]
graph_config["nodes"] = node_configs
node_ids = [node.get("id") for node in node_configs]
# filter edges only in iteration
edge_configs = [
edge
for edge in graph_config.get("edges", [])
if (edge.get("source") is None or edge.get("source") in node_ids)
and (edge.get("target") is None or edge.get("target") in node_ids)
]
graph_config["edges"] = edge_configs
# init graph
graph = Graph.init(graph_config=graph_config, root_node_id=node_id)
if not graph:
raise ValueError("graph not found in workflow")
# fetch node config from node id
iteration_node_config = None
for node in node_configs:
if node.get("id") == node_id:
iteration_node_config = node
break
if not iteration_node_config:
raise ValueError("iteration node id not found in workflow graph")
# Get node class
node_type = NodeType.value_of(iteration_node_config.get("data", {}).get("type"))
node_cls = node_classes.get(node_type)
node_cls = cast(type[BaseNode], node_cls)
# init variable pool
variable_pool = VariablePool(
system_variables={},
user_inputs={},
environment_variables=workflow.environment_variables,
)
try:
variable_mapping = node_cls.extract_variable_selector_to_variable_mapping(
graph_config=workflow.graph_dict, config=iteration_node_config
)
except NotImplementedError:
variable_mapping = {}
WorkflowEntry.mapping_user_inputs_to_variable_pool(
variable_mapping=variable_mapping,
user_inputs=user_inputs,
variable_pool=variable_pool,
tenant_id=workflow.tenant_id,
node_type=node_type,
node_data=IterationNodeData(**iteration_node_config.get("data", {})),
)
return graph, variable_pool
def _handle_event(self, workflow_entry: WorkflowEntry, event: GraphEngineEvent) -> None:
"""
Handle event
:param workflow_entry: workflow entry
:param event: event
"""
if isinstance(event, GraphRunStartedEvent):
self._publish_event(
QueueWorkflowStartedEvent(graph_runtime_state=workflow_entry.graph_engine.graph_runtime_state)
)
elif isinstance(event, GraphRunSucceededEvent):
self._publish_event(QueueWorkflowSucceededEvent(outputs=event.outputs))
elif isinstance(event, GraphRunFailedEvent):
self._publish_event(QueueWorkflowFailedEvent(error=event.error))
elif isinstance(event, NodeRunStartedEvent):
self._publish_event(
QueueNodeStartedEvent(
node_execution_id=event.id,
node_id=event.node_id,
node_type=event.node_type,
node_data=event.node_data,
parallel_id=event.parallel_id,
parallel_start_node_id=event.parallel_start_node_id,
parent_parallel_id=event.parent_parallel_id,
parent_parallel_start_node_id=event.parent_parallel_start_node_id,
start_at=event.route_node_state.start_at,
node_run_index=event.route_node_state.index,
predecessor_node_id=event.predecessor_node_id,
in_iteration_id=event.in_iteration_id,
)
)
elif isinstance(event, NodeRunSucceededEvent):
self._publish_event(
QueueNodeSucceededEvent(
node_execution_id=event.id,
node_id=event.node_id,
node_type=event.node_type,
node_data=event.node_data,
parallel_id=event.parallel_id,
parallel_start_node_id=event.parallel_start_node_id,
parent_parallel_id=event.parent_parallel_id,
parent_parallel_start_node_id=event.parent_parallel_start_node_id,
start_at=event.route_node_state.start_at,
inputs=event.route_node_state.node_run_result.inputs
if event.route_node_state.node_run_result
else {},
process_data=event.route_node_state.node_run_result.process_data
if event.route_node_state.node_run_result
else {},
outputs=event.route_node_state.node_run_result.outputs
if event.route_node_state.node_run_result
else {},
execution_metadata=event.route_node_state.node_run_result.metadata
if event.route_node_state.node_run_result
else {},
in_iteration_id=event.in_iteration_id,
)
)
elif isinstance(event, NodeRunFailedEvent):
self._publish_event(
QueueNodeFailedEvent(
node_execution_id=event.id,
node_id=event.node_id,
node_type=event.node_type,
node_data=event.node_data,
parallel_id=event.parallel_id,
parallel_start_node_id=event.parallel_start_node_id,
parent_parallel_id=event.parent_parallel_id,
parent_parallel_start_node_id=event.parent_parallel_start_node_id,
start_at=event.route_node_state.start_at,
inputs=event.route_node_state.node_run_result.inputs
if event.route_node_state.node_run_result
else {},
process_data=event.route_node_state.node_run_result.process_data
if event.route_node_state.node_run_result
else {},
outputs=event.route_node_state.node_run_result.outputs
if event.route_node_state.node_run_result
else {},
error=event.route_node_state.node_run_result.error
if event.route_node_state.node_run_result and event.route_node_state.node_run_result.error
else "Unknown error",
in_iteration_id=event.in_iteration_id,
)
)
elif isinstance(event, NodeRunStreamChunkEvent):
self._publish_event(
QueueTextChunkEvent(
text=event.chunk_content,
from_variable_selector=event.from_variable_selector,
in_iteration_id=event.in_iteration_id,
)
)
elif isinstance(event, NodeRunRetrieverResourceEvent):
self._publish_event(
QueueRetrieverResourcesEvent(
retriever_resources=event.retriever_resources, in_iteration_id=event.in_iteration_id
)
)
elif isinstance(event, ParallelBranchRunStartedEvent):
self._publish_event(
QueueParallelBranchRunStartedEvent(
parallel_id=event.parallel_id,
parallel_start_node_id=event.parallel_start_node_id,
parent_parallel_id=event.parent_parallel_id,
parent_parallel_start_node_id=event.parent_parallel_start_node_id,
in_iteration_id=event.in_iteration_id,
)
)
elif isinstance(event, ParallelBranchRunSucceededEvent):
self._publish_event(
QueueParallelBranchRunSucceededEvent(
parallel_id=event.parallel_id,
parallel_start_node_id=event.parallel_start_node_id,
parent_parallel_id=event.parent_parallel_id,
parent_parallel_start_node_id=event.parent_parallel_start_node_id,
in_iteration_id=event.in_iteration_id,
)
)
elif isinstance(event, ParallelBranchRunFailedEvent):
self._publish_event(
QueueParallelBranchRunFailedEvent(
parallel_id=event.parallel_id,
parallel_start_node_id=event.parallel_start_node_id,
parent_parallel_id=event.parent_parallel_id,
parent_parallel_start_node_id=event.parent_parallel_start_node_id,
in_iteration_id=event.in_iteration_id,
error=event.error,
)
)
elif isinstance(event, IterationRunStartedEvent):
self._publish_event(
QueueIterationStartEvent(
node_execution_id=event.iteration_id,
node_id=event.iteration_node_id,
node_type=event.iteration_node_type,
node_data=event.iteration_node_data,
parallel_id=event.parallel_id,
parallel_start_node_id=event.parallel_start_node_id,
parent_parallel_id=event.parent_parallel_id,
parent_parallel_start_node_id=event.parent_parallel_start_node_id,
start_at=event.start_at,
node_run_index=workflow_entry.graph_engine.graph_runtime_state.node_run_steps,
inputs=event.inputs,
predecessor_node_id=event.predecessor_node_id,
metadata=event.metadata,
)
)
elif isinstance(event, IterationRunNextEvent):
self._publish_event(
QueueIterationNextEvent(
node_execution_id=event.iteration_id,
node_id=event.iteration_node_id,
node_type=event.iteration_node_type,
node_data=event.iteration_node_data,
parallel_id=event.parallel_id,
parallel_start_node_id=event.parallel_start_node_id,
parent_parallel_id=event.parent_parallel_id,
parent_parallel_start_node_id=event.parent_parallel_start_node_id,
index=event.index,
node_run_index=workflow_entry.graph_engine.graph_runtime_state.node_run_steps,
output=event.pre_iteration_output,
)
)
elif isinstance(event, (IterationRunSucceededEvent | IterationRunFailedEvent)):
self._publish_event(
QueueIterationCompletedEvent(
node_execution_id=event.iteration_id,
node_id=event.iteration_node_id,
node_type=event.iteration_node_type,
node_data=event.iteration_node_data,
parallel_id=event.parallel_id,
parallel_start_node_id=event.parallel_start_node_id,
parent_parallel_id=event.parent_parallel_id,
parent_parallel_start_node_id=event.parent_parallel_start_node_id,
start_at=event.start_at,
node_run_index=workflow_entry.graph_engine.graph_runtime_state.node_run_steps,
inputs=event.inputs,
outputs=event.outputs,
metadata=event.metadata,
steps=event.steps,
error=event.error if isinstance(event, IterationRunFailedEvent) else None,
)
)
def get_workflow(self, app_model: App, workflow_id: str) -> Optional[Workflow]:
"""
Get workflow
"""
# fetch 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 == workflow_id
)
.first()
)
# return workflow
return workflow
def _publish_event(self, event: AppQueueEvent) -> None:
self.queue_manager.publish(event, PublishFrom.APPLICATION_MANAGER)

@ -1,10 +1,24 @@
from typing import Optional from typing import Optional
from core.app.entities.queue_entities import AppQueueEvent
from core.model_runtime.utils.encoders import jsonable_encoder from core.model_runtime.utils.encoders import jsonable_encoder
from core.workflow.callbacks.base_workflow_callback import WorkflowCallback from core.workflow.callbacks.base_workflow_callback import WorkflowCallback
from core.workflow.entities.base_node_data_entities import BaseNodeData from core.workflow.graph_engine.entities.event import (
from core.workflow.entities.node_entities import NodeType GraphEngineEvent,
GraphRunFailedEvent,
GraphRunStartedEvent,
GraphRunSucceededEvent,
IterationRunFailedEvent,
IterationRunNextEvent,
IterationRunStartedEvent,
IterationRunSucceededEvent,
NodeRunFailedEvent,
NodeRunStartedEvent,
NodeRunStreamChunkEvent,
NodeRunSucceededEvent,
ParallelBranchRunFailedEvent,
ParallelBranchRunStartedEvent,
ParallelBranchRunSucceededEvent,
)
_TEXT_COLOR_MAPPING = { _TEXT_COLOR_MAPPING = {
"blue": "36;1", "blue": "36;1",
@ -16,138 +30,184 @@ _TEXT_COLOR_MAPPING = {
class WorkflowLoggingCallback(WorkflowCallback): class WorkflowLoggingCallback(WorkflowCallback):
def __init__(self) -> None: def __init__(self) -> None:
self.current_node_id = None self.current_node_id = None
def on_workflow_run_started(self) -> None: def on_event(self, event: GraphEngineEvent) -> None:
if isinstance(event, GraphRunStartedEvent):
self.print_text("\n[GraphRunStartedEvent]", color="pink")
elif isinstance(event, GraphRunSucceededEvent):
self.print_text("\n[GraphRunSucceededEvent]", color="green")
elif isinstance(event, GraphRunFailedEvent):
self.print_text(f"\n[GraphRunFailedEvent] reason: {event.error}", color="red")
elif isinstance(event, NodeRunStartedEvent):
self.on_workflow_node_execute_started(event=event)
elif isinstance(event, NodeRunSucceededEvent):
self.on_workflow_node_execute_succeeded(event=event)
elif isinstance(event, NodeRunFailedEvent):
self.on_workflow_node_execute_failed(event=event)
elif isinstance(event, NodeRunStreamChunkEvent):
self.on_node_text_chunk(event=event)
elif isinstance(event, ParallelBranchRunStartedEvent):
self.on_workflow_parallel_started(event=event)
elif isinstance(event, ParallelBranchRunSucceededEvent | ParallelBranchRunFailedEvent):
self.on_workflow_parallel_completed(event=event)
elif isinstance(event, IterationRunStartedEvent):
self.on_workflow_iteration_started(event=event)
elif isinstance(event, IterationRunNextEvent):
self.on_workflow_iteration_next(event=event)
elif isinstance(event, IterationRunSucceededEvent | IterationRunFailedEvent):
self.on_workflow_iteration_completed(event=event)
else:
self.print_text(f"\n[{event.__class__.__name__}]", color="blue")
def on_workflow_node_execute_started(self, event: NodeRunStartedEvent) -> None:
""" """
Workflow run started Workflow node execute started
""" """
self.print_text("\n[on_workflow_run_started]", color='pink') self.print_text("\n[NodeRunStartedEvent]", color="yellow")
self.print_text(f"Node ID: {event.node_id}", color="yellow")
self.print_text(f"Node Title: {event.node_data.title}", color="yellow")
self.print_text(f"Type: {event.node_type.value}", color="yellow")
def on_workflow_run_succeeded(self) -> None: def on_workflow_node_execute_succeeded(self, event: NodeRunSucceededEvent) -> None:
"""
Workflow run succeeded
""" """
self.print_text("\n[on_workflow_run_succeeded]", color='green') Workflow node execute succeeded
def on_workflow_run_failed(self, error: str) -> None:
""" """
Workflow run failed route_node_state = event.route_node_state
self.print_text("\n[NodeRunSucceededEvent]", color="green")
self.print_text(f"Node ID: {event.node_id}", color="green")
self.print_text(f"Node Title: {event.node_data.title}", color="green")
self.print_text(f"Type: {event.node_type.value}", color="green")
if route_node_state.node_run_result:
node_run_result = route_node_state.node_run_result
self.print_text(
f"Inputs: {jsonable_encoder(node_run_result.inputs) if node_run_result.inputs else ''}", color="green"
)
self.print_text(
f"Process Data: {jsonable_encoder(node_run_result.process_data) if node_run_result.process_data else ''}",
color="green",
)
self.print_text(
f"Outputs: {jsonable_encoder(node_run_result.outputs) if node_run_result.outputs else ''}",
color="green",
)
self.print_text(
f"Metadata: {jsonable_encoder(node_run_result.metadata) if node_run_result.metadata else ''}",
color="green",
)
def on_workflow_node_execute_failed(self, event: NodeRunFailedEvent) -> None:
""" """
self.print_text("\n[on_workflow_run_failed]", color='red') Workflow node execute failed
def on_workflow_node_execute_started(self, node_id: str,
node_type: NodeType,
node_data: BaseNodeData,
node_run_index: int = 1,
predecessor_node_id: Optional[str] = None) -> None:
"""
Workflow node execute started
""" """
self.print_text("\n[on_workflow_node_execute_started]", color='yellow') route_node_state = event.route_node_state
self.print_text(f"Node ID: {node_id}", color='yellow')
self.print_text(f"Type: {node_type.value}", color='yellow') self.print_text("\n[NodeRunFailedEvent]", color="red")
self.print_text(f"Index: {node_run_index}", color='yellow') self.print_text(f"Node ID: {event.node_id}", color="red")
if predecessor_node_id: self.print_text(f"Node Title: {event.node_data.title}", color="red")
self.print_text(f"Predecessor Node ID: {predecessor_node_id}", color='yellow') self.print_text(f"Type: {event.node_type.value}", color="red")
def on_workflow_node_execute_succeeded(self, node_id: str, if route_node_state.node_run_result:
node_type: NodeType, node_run_result = route_node_state.node_run_result
node_data: BaseNodeData, self.print_text(f"Error: {node_run_result.error}", color="red")
inputs: Optional[dict] = None, self.print_text(
process_data: Optional[dict] = None, f"Inputs: {jsonable_encoder(node_run_result.inputs) if node_run_result.inputs else ''}", color="red"
outputs: Optional[dict] = None, )
execution_metadata: Optional[dict] = None) -> None: self.print_text(
f"Process Data: {jsonable_encoder(node_run_result.process_data) if node_run_result.process_data else ''}",
color="red",
)
self.print_text(
f"Outputs: {jsonable_encoder(node_run_result.outputs) if node_run_result.outputs else ''}", color="red"
)
def on_node_text_chunk(self, event: NodeRunStreamChunkEvent) -> None:
""" """
Workflow node execute succeeded Publish text chunk
""" """
self.print_text("\n[on_workflow_node_execute_succeeded]", color='green') route_node_state = event.route_node_state
self.print_text(f"Node ID: {node_id}", color='green') if not self.current_node_id or self.current_node_id != route_node_state.node_id:
self.print_text(f"Type: {node_type.value}", color='green') self.current_node_id = route_node_state.node_id
self.print_text(f"Inputs: {jsonable_encoder(inputs) if inputs else ''}", color='green') self.print_text("\n[NodeRunStreamChunkEvent]")
self.print_text(f"Process Data: {jsonable_encoder(process_data) if process_data else ''}", color='green') self.print_text(f"Node ID: {route_node_state.node_id}")
self.print_text(f"Outputs: {jsonable_encoder(outputs) if outputs else ''}", color='green')
self.print_text(f"Metadata: {jsonable_encoder(execution_metadata) if execution_metadata else ''}", node_run_result = route_node_state.node_run_result
color='green') if node_run_result:
self.print_text(
def on_workflow_node_execute_failed(self, node_id: str, f"Metadata: {jsonable_encoder(node_run_result.metadata) if node_run_result.metadata else ''}"
node_type: NodeType, )
node_data: BaseNodeData,
error: str, self.print_text(event.chunk_content, color="pink", end="")
inputs: Optional[dict] = None,
outputs: Optional[dict] = None, def on_workflow_parallel_started(self, event: ParallelBranchRunStartedEvent) -> None:
process_data: Optional[dict] = None) -> None:
""" """
Workflow node execute failed Publish parallel started
""" """
self.print_text("\n[on_workflow_node_execute_failed]", color='red') self.print_text("\n[ParallelBranchRunStartedEvent]", color="blue")
self.print_text(f"Node ID: {node_id}", color='red') self.print_text(f"Parallel ID: {event.parallel_id}", color="blue")
self.print_text(f"Type: {node_type.value}", color='red') self.print_text(f"Branch ID: {event.parallel_start_node_id}", color="blue")
self.print_text(f"Error: {error}", color='red') if event.in_iteration_id:
self.print_text(f"Inputs: {jsonable_encoder(inputs) if inputs else ''}", color='red') self.print_text(f"Iteration ID: {event.in_iteration_id}", color="blue")
self.print_text(f"Process Data: {jsonable_encoder(process_data) if process_data else ''}", color='red')
self.print_text(f"Outputs: {jsonable_encoder(outputs) if outputs else ''}", color='red')
def on_node_text_chunk(self, node_id: str, text: str, metadata: Optional[dict] = None) -> None: def on_workflow_parallel_completed(
self, event: ParallelBranchRunSucceededEvent | ParallelBranchRunFailedEvent
) -> None:
""" """
Publish text chunk Publish parallel completed
""" """
if not self.current_node_id or self.current_node_id != node_id: if isinstance(event, ParallelBranchRunSucceededEvent):
self.current_node_id = node_id color = "blue"
self.print_text('\n[on_node_text_chunk]') elif isinstance(event, ParallelBranchRunFailedEvent):
self.print_text(f"Node ID: {node_id}") color = "red"
self.print_text(f"Metadata: {jsonable_encoder(metadata) if metadata else ''}")
self.print_text(text, color="pink", end="") self.print_text(
"\n[ParallelBranchRunSucceededEvent]"
if isinstance(event, ParallelBranchRunSucceededEvent)
else "\n[ParallelBranchRunFailedEvent]",
color=color,
)
self.print_text(f"Parallel ID: {event.parallel_id}", color=color)
self.print_text(f"Branch ID: {event.parallel_start_node_id}", color=color)
if event.in_iteration_id:
self.print_text(f"Iteration ID: {event.in_iteration_id}", color=color)
def on_workflow_iteration_started(self, if isinstance(event, ParallelBranchRunFailedEvent):
node_id: str, self.print_text(f"Error: {event.error}", color=color)
node_type: NodeType,
node_run_index: int = 1, def on_workflow_iteration_started(self, event: IterationRunStartedEvent) -> None:
node_data: Optional[BaseNodeData] = None,
inputs: dict = None,
predecessor_node_id: Optional[str] = None,
metadata: Optional[dict] = None) -> None:
""" """
Publish iteration started Publish iteration started
""" """
self.print_text("\n[on_workflow_iteration_started]", color='blue') self.print_text("\n[IterationRunStartedEvent]", color="blue")
self.print_text(f"Node ID: {node_id}", color='blue') self.print_text(f"Iteration Node ID: {event.iteration_id}", color="blue")
def on_workflow_iteration_next(self, node_id: str, def on_workflow_iteration_next(self, event: IterationRunNextEvent) -> None:
node_type: NodeType,
index: int,
node_run_index: int,
output: Optional[dict]) -> None:
""" """
Publish iteration next Publish iteration next
""" """
self.print_text("\n[on_workflow_iteration_next]", color='blue') self.print_text("\n[IterationRunNextEvent]", color="blue")
self.print_text(f"Iteration Node ID: {event.iteration_id}", color="blue")
self.print_text(f"Iteration Index: {event.index}", color="blue")
def on_workflow_iteration_completed(self, node_id: str, def on_workflow_iteration_completed(self, event: IterationRunSucceededEvent | IterationRunFailedEvent) -> None:
node_type: NodeType,
node_run_index: int,
outputs: dict) -> None:
""" """
Publish iteration completed Publish iteration completed
""" """
self.print_text("\n[on_workflow_iteration_completed]", color='blue') self.print_text(
"\n[IterationRunSucceededEvent]"
if isinstance(event, IterationRunSucceededEvent)
else "\n[IterationRunFailedEvent]",
color="blue",
)
self.print_text(f"Node ID: {event.iteration_id}", color="blue")
def on_event(self, event: AppQueueEvent) -> None: def print_text(self, text: str, color: Optional[str] = None, end: str = "\n") -> None:
"""
Publish event
"""
self.print_text("\n[on_workflow_event]", color='blue')
self.print_text(f"Event: {jsonable_encoder(event)}", color='blue')
def print_text(
self, text: str, color: Optional[str] = None, end: str = "\n"
) -> None:
"""Print text with highlighting and no end characters.""" """Print text with highlighting and no end characters."""
text_to_print = self._get_colored_text(text, color) if color else text text_to_print = self._get_colored_text(text, color) if color else text
print(f'{text_to_print}', end=end) print(f"{text_to_print}", end=end)
def _get_colored_text(self, text: str, color: str) -> str: def _get_colored_text(self, text: str, color: str) -> str:
"""Get colored text.""" """Get colored text."""

@ -15,13 +15,14 @@ class InvokeFrom(Enum):
""" """
Invoke From. Invoke From.
""" """
SERVICE_API = 'service-api'
WEB_APP = 'web-app' SERVICE_API = "service-api"
EXPLORE = 'explore' WEB_APP = "web-app"
DEBUGGER = 'debugger' EXPLORE = "explore"
DEBUGGER = "debugger"
@classmethod @classmethod
def value_of(cls, value: str) -> 'InvokeFrom': def value_of(cls, value: str) -> "InvokeFrom":
""" """
Get value of given mode. Get value of given mode.
@ -31,7 +32,7 @@ class InvokeFrom(Enum):
for mode in cls: for mode in cls:
if mode.value == value: if mode.value == value:
return mode return mode
raise ValueError(f'invalid invoke from value {value}') raise ValueError(f"invalid invoke from value {value}")
def to_source(self) -> str: def to_source(self) -> str:
""" """
@ -40,21 +41,22 @@ class InvokeFrom(Enum):
:return: source :return: source
""" """
if self == InvokeFrom.WEB_APP: if self == InvokeFrom.WEB_APP:
return 'web_app' return "web_app"
elif self == InvokeFrom.DEBUGGER: elif self == InvokeFrom.DEBUGGER:
return 'dev' return "dev"
elif self == InvokeFrom.EXPLORE: elif self == InvokeFrom.EXPLORE:
return 'explore_app' return "explore_app"
elif self == InvokeFrom.SERVICE_API: elif self == InvokeFrom.SERVICE_API:
return 'api' return "api"
return 'dev' return "dev"
class ModelConfigWithCredentialsEntity(BaseModel): class ModelConfigWithCredentialsEntity(BaseModel):
""" """
Model Config With Credentials Entity. Model Config With Credentials Entity.
""" """
provider: str provider: str
model: str model: str
model_schema: AIModelEntity model_schema: AIModelEntity
@ -72,6 +74,7 @@ class AppGenerateEntity(BaseModel):
""" """
App Generate Entity. App Generate Entity.
""" """
task_id: str task_id: str
# app config # app config
@ -102,6 +105,7 @@ class EasyUIBasedAppGenerateEntity(AppGenerateEntity):
""" """
Chat Application Generate Entity. Chat Application Generate Entity.
""" """
# app config # app config
app_config: EasyUIBasedAppConfig app_config: EasyUIBasedAppConfig
model_conf: ModelConfigWithCredentialsEntity model_conf: ModelConfigWithCredentialsEntity
@ -116,6 +120,7 @@ class ChatAppGenerateEntity(EasyUIBasedAppGenerateEntity):
""" """
Chat Application Generate Entity. Chat Application Generate Entity.
""" """
conversation_id: Optional[str] = None conversation_id: Optional[str] = None
@ -123,6 +128,7 @@ class CompletionAppGenerateEntity(EasyUIBasedAppGenerateEntity):
""" """
Completion Application Generate Entity. Completion Application Generate Entity.
""" """
pass pass
@ -130,6 +136,7 @@ class AgentChatAppGenerateEntity(EasyUIBasedAppGenerateEntity):
""" """
Agent Chat Application Generate Entity. Agent Chat Application Generate Entity.
""" """
conversation_id: Optional[str] = None conversation_id: Optional[str] = None
@ -137,6 +144,7 @@ class AdvancedChatAppGenerateEntity(AppGenerateEntity):
""" """
Advanced Chat Application Generate Entity. Advanced Chat Application Generate Entity.
""" """
# app config # app config
app_config: WorkflowUIBasedAppConfig app_config: WorkflowUIBasedAppConfig
@ -147,15 +155,18 @@ class AdvancedChatAppGenerateEntity(AppGenerateEntity):
""" """
Single Iteration Run Entity. Single Iteration Run Entity.
""" """
node_id: str node_id: str
inputs: dict inputs: dict
single_iteration_run: Optional[SingleIterationRunEntity] = None single_iteration_run: Optional[SingleIterationRunEntity] = None
class WorkflowAppGenerateEntity(AppGenerateEntity): class WorkflowAppGenerateEntity(AppGenerateEntity):
""" """
Workflow Application Generate Entity. Workflow Application Generate Entity.
""" """
# app config # app config
app_config: WorkflowUIBasedAppConfig app_config: WorkflowUIBasedAppConfig
@ -163,6 +174,7 @@ class WorkflowAppGenerateEntity(AppGenerateEntity):
""" """
Single Iteration Run Entity. Single Iteration Run Entity.
""" """
node_id: str node_id: str
inputs: dict inputs: dict

@ -1,3 +1,4 @@
from datetime import datetime
from enum import Enum from enum import Enum
from typing import Any, Optional from typing import Any, Optional
@ -5,13 +6,15 @@ from pydantic import BaseModel, field_validator
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk
from core.workflow.entities.base_node_data_entities import BaseNodeData from core.workflow.entities.base_node_data_entities import BaseNodeData
from core.workflow.entities.node_entities import NodeType from core.workflow.entities.node_entities import NodeRunMetadataKey, NodeType
from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState
class QueueEvent(str, Enum): class QueueEvent(str, Enum):
""" """
QueueEvent enum QueueEvent enum
""" """
LLM_CHUNK = "llm_chunk" LLM_CHUNK = "llm_chunk"
TEXT_CHUNK = "text_chunk" TEXT_CHUNK = "text_chunk"
AGENT_MESSAGE = "agent_message" AGENT_MESSAGE = "agent_message"
@ -31,6 +34,9 @@ class QueueEvent(str, Enum):
ANNOTATION_REPLY = "annotation_reply" ANNOTATION_REPLY = "annotation_reply"
AGENT_THOUGHT = "agent_thought" AGENT_THOUGHT = "agent_thought"
MESSAGE_FILE = "message_file" MESSAGE_FILE = "message_file"
PARALLEL_BRANCH_RUN_STARTED = "parallel_branch_run_started"
PARALLEL_BRANCH_RUN_SUCCEEDED = "parallel_branch_run_succeeded"
PARALLEL_BRANCH_RUN_FAILED = "parallel_branch_run_failed"
ERROR = "error" ERROR = "error"
PING = "ping" PING = "ping"
STOP = "stop" STOP = "stop"
@ -38,46 +44,73 @@ class QueueEvent(str, Enum):
class AppQueueEvent(BaseModel): class AppQueueEvent(BaseModel):
""" """
QueueEvent entity QueueEvent abstract entity
""" """
event: QueueEvent event: QueueEvent
class QueueLLMChunkEvent(AppQueueEvent): class QueueLLMChunkEvent(AppQueueEvent):
""" """
QueueLLMChunkEvent entity QueueLLMChunkEvent entity
Only for basic mode apps
""" """
event: QueueEvent = QueueEvent.LLM_CHUNK event: QueueEvent = QueueEvent.LLM_CHUNK
chunk: LLMResultChunk chunk: LLMResultChunk
class QueueIterationStartEvent(AppQueueEvent): class QueueIterationStartEvent(AppQueueEvent):
""" """
QueueIterationStartEvent entity QueueIterationStartEvent entity
""" """
event: QueueEvent = QueueEvent.ITERATION_START event: QueueEvent = QueueEvent.ITERATION_START
node_execution_id: str
node_id: str node_id: str
node_type: NodeType node_type: NodeType
node_data: BaseNodeData node_data: BaseNodeData
parallel_id: Optional[str] = None
"""parallel id if node is in parallel"""
parallel_start_node_id: Optional[str] = None
"""parallel start node id if node is in parallel"""
parent_parallel_id: Optional[str] = None
"""parent parallel id if node is in parallel"""
parent_parallel_start_node_id: Optional[str] = None
"""parent parallel start node id if node is in parallel"""
start_at: datetime
node_run_index: int node_run_index: int
inputs: dict = None inputs: Optional[dict[str, Any]] = None
predecessor_node_id: Optional[str] = None predecessor_node_id: Optional[str] = None
metadata: Optional[dict] = None metadata: Optional[dict[str, Any]] = None
class QueueIterationNextEvent(AppQueueEvent): class QueueIterationNextEvent(AppQueueEvent):
""" """
QueueIterationNextEvent entity QueueIterationNextEvent entity
""" """
event: QueueEvent = QueueEvent.ITERATION_NEXT event: QueueEvent = QueueEvent.ITERATION_NEXT
index: int index: int
node_execution_id: str
node_id: str node_id: str
node_type: NodeType node_type: NodeType
node_data: BaseNodeData
parallel_id: Optional[str] = None
"""parallel id if node is in parallel"""
parallel_start_node_id: Optional[str] = None
"""parallel start node id if node is in parallel"""
parent_parallel_id: Optional[str] = None
"""parent parallel id if node is in parallel"""
parent_parallel_start_node_id: Optional[str] = None
"""parent parallel start node id if node is in parallel"""
node_run_index: int node_run_index: int
output: Optional[Any] = None # output for the current iteration output: Optional[Any] = None # output for the current iteration
@field_validator('output', mode='before') @field_validator("output", mode="before")
@classmethod @classmethod
def set_output(cls, v): def set_output(cls, v):
""" """
@ -87,41 +120,66 @@ class QueueIterationNextEvent(AppQueueEvent):
return None return None
if isinstance(v, int | float | str | bool | dict | list): if isinstance(v, int | float | str | bool | dict | list):
return v return v
raise ValueError('output must be a valid type') raise ValueError("output must be a valid type")
class QueueIterationCompletedEvent(AppQueueEvent): class QueueIterationCompletedEvent(AppQueueEvent):
""" """
QueueIterationCompletedEvent entity QueueIterationCompletedEvent entity
""" """
event:QueueEvent = QueueEvent.ITERATION_COMPLETED
event: QueueEvent = QueueEvent.ITERATION_COMPLETED
node_execution_id: str
node_id: str node_id: str
node_type: NodeType node_type: NodeType
node_data: BaseNodeData
parallel_id: Optional[str] = None
"""parallel id if node is in parallel"""
parallel_start_node_id: Optional[str] = None
"""parallel start node id if node is in parallel"""
parent_parallel_id: Optional[str] = None
"""parent parallel id if node is in parallel"""
parent_parallel_start_node_id: Optional[str] = None
"""parent parallel start node id if node is in parallel"""
start_at: datetime
node_run_index: int node_run_index: int
outputs: dict inputs: Optional[dict[str, Any]] = None
outputs: Optional[dict[str, Any]] = None
metadata: Optional[dict[str, Any]] = None
steps: int = 0
error: Optional[str] = None
class QueueTextChunkEvent(AppQueueEvent): class QueueTextChunkEvent(AppQueueEvent):
""" """
QueueTextChunkEvent entity QueueTextChunkEvent entity
""" """
event: QueueEvent = QueueEvent.TEXT_CHUNK event: QueueEvent = QueueEvent.TEXT_CHUNK
text: str text: str
metadata: Optional[dict] = None from_variable_selector: Optional[list[str]] = None
"""from variable selector"""
in_iteration_id: Optional[str] = None
"""iteration id if node is in iteration"""
class QueueAgentMessageEvent(AppQueueEvent): class QueueAgentMessageEvent(AppQueueEvent):
""" """
QueueMessageEvent entity QueueMessageEvent entity
""" """
event: QueueEvent = QueueEvent.AGENT_MESSAGE event: QueueEvent = QueueEvent.AGENT_MESSAGE
chunk: LLMResultChunk chunk: LLMResultChunk
class QueueMessageReplaceEvent(AppQueueEvent): class QueueMessageReplaceEvent(AppQueueEvent):
""" """
QueueMessageReplaceEvent entity QueueMessageReplaceEvent entity
""" """
event: QueueEvent = QueueEvent.MESSAGE_REPLACE event: QueueEvent = QueueEvent.MESSAGE_REPLACE
text: str text: str
@ -130,14 +188,18 @@ class QueueRetrieverResourcesEvent(AppQueueEvent):
""" """
QueueRetrieverResourcesEvent entity QueueRetrieverResourcesEvent entity
""" """
event: QueueEvent = QueueEvent.RETRIEVER_RESOURCES event: QueueEvent = QueueEvent.RETRIEVER_RESOURCES
retriever_resources: list[dict] retriever_resources: list[dict]
in_iteration_id: Optional[str] = None
"""iteration id if node is in iteration"""
class QueueAnnotationReplyEvent(AppQueueEvent): class QueueAnnotationReplyEvent(AppQueueEvent):
""" """
QueueAnnotationReplyEvent entity QueueAnnotationReplyEvent entity
""" """
event: QueueEvent = QueueEvent.ANNOTATION_REPLY event: QueueEvent = QueueEvent.ANNOTATION_REPLY
message_annotation_id: str message_annotation_id: str
@ -146,6 +208,7 @@ class QueueMessageEndEvent(AppQueueEvent):
""" """
QueueMessageEndEvent entity QueueMessageEndEvent entity
""" """
event: QueueEvent = QueueEvent.MESSAGE_END event: QueueEvent = QueueEvent.MESSAGE_END
llm_result: Optional[LLMResult] = None llm_result: Optional[LLMResult] = None
@ -154,6 +217,7 @@ class QueueAdvancedChatMessageEndEvent(AppQueueEvent):
""" """
QueueAdvancedChatMessageEndEvent entity QueueAdvancedChatMessageEndEvent entity
""" """
event: QueueEvent = QueueEvent.ADVANCED_CHAT_MESSAGE_END event: QueueEvent = QueueEvent.ADVANCED_CHAT_MESSAGE_END
@ -161,20 +225,25 @@ class QueueWorkflowStartedEvent(AppQueueEvent):
""" """
QueueWorkflowStartedEvent entity QueueWorkflowStartedEvent entity
""" """
event: QueueEvent = QueueEvent.WORKFLOW_STARTED event: QueueEvent = QueueEvent.WORKFLOW_STARTED
graph_runtime_state: GraphRuntimeState
class QueueWorkflowSucceededEvent(AppQueueEvent): class QueueWorkflowSucceededEvent(AppQueueEvent):
""" """
QueueWorkflowSucceededEvent entity QueueWorkflowSucceededEvent entity
""" """
event: QueueEvent = QueueEvent.WORKFLOW_SUCCEEDED event: QueueEvent = QueueEvent.WORKFLOW_SUCCEEDED
outputs: Optional[dict[str, Any]] = None
class QueueWorkflowFailedEvent(AppQueueEvent): class QueueWorkflowFailedEvent(AppQueueEvent):
""" """
QueueWorkflowFailedEvent entity QueueWorkflowFailedEvent entity
""" """
event: QueueEvent = QueueEvent.WORKFLOW_FAILED event: QueueEvent = QueueEvent.WORKFLOW_FAILED
error: str error: str
@ -183,29 +252,55 @@ class QueueNodeStartedEvent(AppQueueEvent):
""" """
QueueNodeStartedEvent entity QueueNodeStartedEvent entity
""" """
event: QueueEvent = QueueEvent.NODE_STARTED event: QueueEvent = QueueEvent.NODE_STARTED
node_execution_id: str
node_id: str node_id: str
node_type: NodeType node_type: NodeType
node_data: BaseNodeData node_data: BaseNodeData
node_run_index: int = 1 node_run_index: int = 1
predecessor_node_id: Optional[str] = None predecessor_node_id: Optional[str] = None
parallel_id: Optional[str] = None
"""parallel id if node is in parallel"""
parallel_start_node_id: Optional[str] = None
"""parallel start node id if node is in parallel"""
parent_parallel_id: Optional[str] = None
"""parent parallel id if node is in parallel"""
parent_parallel_start_node_id: Optional[str] = None
"""parent parallel start node id if node is in parallel"""
in_iteration_id: Optional[str] = None
"""iteration id if node is in iteration"""
start_at: datetime
class QueueNodeSucceededEvent(AppQueueEvent): class QueueNodeSucceededEvent(AppQueueEvent):
""" """
QueueNodeSucceededEvent entity QueueNodeSucceededEvent entity
""" """
event: QueueEvent = QueueEvent.NODE_SUCCEEDED event: QueueEvent = QueueEvent.NODE_SUCCEEDED
node_execution_id: str
node_id: str node_id: str
node_type: NodeType node_type: NodeType
node_data: BaseNodeData node_data: BaseNodeData
parallel_id: Optional[str] = None
inputs: Optional[dict] = None """parallel id if node is in parallel"""
process_data: Optional[dict] = None parallel_start_node_id: Optional[str] = None
outputs: Optional[dict] = None """parallel start node id if node is in parallel"""
execution_metadata: Optional[dict] = None parent_parallel_id: Optional[str] = None
"""parent parallel id if node is in parallel"""
parent_parallel_start_node_id: Optional[str] = None
"""parent parallel start node id if node is in parallel"""
in_iteration_id: Optional[str] = None
"""iteration id if node is in iteration"""
start_at: datetime
inputs: Optional[dict[str, Any]] = None
process_data: Optional[dict[str, Any]] = None
outputs: Optional[dict[str, Any]] = None
execution_metadata: Optional[dict[NodeRunMetadataKey, Any]] = None
error: Optional[str] = None error: Optional[str] = None
@ -214,15 +309,28 @@ class QueueNodeFailedEvent(AppQueueEvent):
""" """
QueueNodeFailedEvent entity QueueNodeFailedEvent entity
""" """
event: QueueEvent = QueueEvent.NODE_FAILED event: QueueEvent = QueueEvent.NODE_FAILED
node_execution_id: str
node_id: str node_id: str
node_type: NodeType node_type: NodeType
node_data: BaseNodeData node_data: BaseNodeData
parallel_id: Optional[str] = None
inputs: Optional[dict] = None """parallel id if node is in parallel"""
outputs: Optional[dict] = None parallel_start_node_id: Optional[str] = None
process_data: Optional[dict] = None """parallel start node id if node is in parallel"""
parent_parallel_id: Optional[str] = None
"""parent parallel id if node is in parallel"""
parent_parallel_start_node_id: Optional[str] = None
"""parent parallel start node id if node is in parallel"""
in_iteration_id: Optional[str] = None
"""iteration id if node is in iteration"""
start_at: datetime
inputs: Optional[dict[str, Any]] = None
process_data: Optional[dict[str, Any]] = None
outputs: Optional[dict[str, Any]] = None
error: str error: str
@ -231,6 +339,7 @@ class QueueAgentThoughtEvent(AppQueueEvent):
""" """
QueueAgentThoughtEvent entity QueueAgentThoughtEvent entity
""" """
event: QueueEvent = QueueEvent.AGENT_THOUGHT event: QueueEvent = QueueEvent.AGENT_THOUGHT
agent_thought_id: str agent_thought_id: str
@ -239,6 +348,7 @@ class QueueMessageFileEvent(AppQueueEvent):
""" """
QueueAgentThoughtEvent entity QueueAgentThoughtEvent entity
""" """
event: QueueEvent = QueueEvent.MESSAGE_FILE event: QueueEvent = QueueEvent.MESSAGE_FILE
message_file_id: str message_file_id: str
@ -247,6 +357,7 @@ class QueueErrorEvent(AppQueueEvent):
""" """
QueueErrorEvent entity QueueErrorEvent entity
""" """
event: QueueEvent = QueueEvent.ERROR event: QueueEvent = QueueEvent.ERROR
error: Any = None error: Any = None
@ -255,6 +366,7 @@ class QueuePingEvent(AppQueueEvent):
""" """
QueuePingEvent entity QueuePingEvent entity
""" """
event: QueueEvent = QueueEvent.PING event: QueueEvent = QueueEvent.PING
@ -262,10 +374,12 @@ class QueueStopEvent(AppQueueEvent):
""" """
QueueStopEvent entity QueueStopEvent entity
""" """
class StopBy(Enum): class StopBy(Enum):
""" """
Stop by enum Stop by enum
""" """
USER_MANUAL = "user-manual" USER_MANUAL = "user-manual"
ANNOTATION_REPLY = "annotation-reply" ANNOTATION_REPLY = "annotation-reply"
OUTPUT_MODERATION = "output-moderation" OUTPUT_MODERATION = "output-moderation"
@ -274,11 +388,25 @@ class QueueStopEvent(AppQueueEvent):
event: QueueEvent = QueueEvent.STOP event: QueueEvent = QueueEvent.STOP
stopped_by: StopBy stopped_by: StopBy
def get_stop_reason(self) -> str:
"""
To stop reason
"""
reason_mapping = {
QueueStopEvent.StopBy.USER_MANUAL: "Stopped by user.",
QueueStopEvent.StopBy.ANNOTATION_REPLY: "Stopped by annotation reply.",
QueueStopEvent.StopBy.OUTPUT_MODERATION: "Stopped by output moderation.",
QueueStopEvent.StopBy.INPUT_MODERATION: "Stopped by input moderation.",
}
return reason_mapping.get(self.stopped_by, "Stopped by unknown reason.")
class QueueMessage(BaseModel): class QueueMessage(BaseModel):
""" """
QueueMessage entity QueueMessage abstract entity
""" """
task_id: str task_id: str
app_mode: str app_mode: str
event: AppQueueEvent event: AppQueueEvent
@ -288,6 +416,7 @@ class MessageQueueMessage(QueueMessage):
""" """
MessageQueueMessage entity MessageQueueMessage entity
""" """
message_id: str message_id: str
conversation_id: str conversation_id: str
@ -296,4 +425,57 @@ class WorkflowQueueMessage(QueueMessage):
""" """
WorkflowQueueMessage entity WorkflowQueueMessage entity
""" """
pass pass
class QueueParallelBranchRunStartedEvent(AppQueueEvent):
"""
QueueParallelBranchRunStartedEvent entity
"""
event: QueueEvent = QueueEvent.PARALLEL_BRANCH_RUN_STARTED
parallel_id: str
parallel_start_node_id: str
parent_parallel_id: Optional[str] = None
"""parent parallel id if node is in parallel"""
parent_parallel_start_node_id: Optional[str] = None
"""parent parallel start node id if node is in parallel"""
in_iteration_id: Optional[str] = None
"""iteration id if node is in iteration"""
class QueueParallelBranchRunSucceededEvent(AppQueueEvent):
"""
QueueParallelBranchRunSucceededEvent entity
"""
event: QueueEvent = QueueEvent.PARALLEL_BRANCH_RUN_SUCCEEDED
parallel_id: str
parallel_start_node_id: str
parent_parallel_id: Optional[str] = None
"""parent parallel id if node is in parallel"""
parent_parallel_start_node_id: Optional[str] = None
"""parent parallel start node id if node is in parallel"""
in_iteration_id: Optional[str] = None
"""iteration id if node is in iteration"""
class QueueParallelBranchRunFailedEvent(AppQueueEvent):
"""
QueueParallelBranchRunFailedEvent entity
"""
event: QueueEvent = QueueEvent.PARALLEL_BRANCH_RUN_FAILED
parallel_id: str
parallel_start_node_id: str
parent_parallel_id: Optional[str] = None
"""parent parallel id if node is in parallel"""
parent_parallel_start_node_id: Optional[str] = None
"""parent parallel start node id if node is in parallel"""
in_iteration_id: Optional[str] = None
"""iteration id if node is in iteration"""
error: str

@ -3,44 +3,16 @@ from typing import Any, Optional
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from core.model_runtime.entities.llm_entities import LLMResult, LLMUsage from core.model_runtime.entities.llm_entities import LLMResult
from core.model_runtime.utils.encoders import jsonable_encoder from core.model_runtime.utils.encoders import jsonable_encoder
from core.workflow.entities.base_node_data_entities import BaseNodeData
from core.workflow.entities.node_entities import NodeType
from core.workflow.nodes.answer.entities import GenerateRouteChunk
from models.workflow import WorkflowNodeExecutionStatus from models.workflow import WorkflowNodeExecutionStatus
class WorkflowStreamGenerateNodes(BaseModel):
"""
WorkflowStreamGenerateNodes entity
"""
end_node_id: str
stream_node_ids: list[str]
class ChatflowStreamGenerateRoute(BaseModel):
"""
ChatflowStreamGenerateRoute entity
"""
answer_node_id: str
generate_route: list[GenerateRouteChunk]
current_route_position: int = 0
class NodeExecutionInfo(BaseModel):
"""
NodeExecutionInfo entity
"""
workflow_node_execution_id: str
node_type: NodeType
start_at: float
class TaskState(BaseModel): class TaskState(BaseModel):
""" """
TaskState entity TaskState entity
""" """
metadata: dict = {} metadata: dict = {}
@ -48,6 +20,7 @@ class EasyUITaskState(TaskState):
""" """
EasyUITaskState entity EasyUITaskState entity
""" """
llm_result: LLMResult llm_result: LLMResult
@ -55,34 +28,15 @@ class WorkflowTaskState(TaskState):
""" """
WorkflowTaskState entity WorkflowTaskState entity
""" """
answer: str = ""
workflow_run_id: Optional[str] = None
start_at: Optional[float] = None
total_tokens: int = 0
total_steps: int = 0
ran_node_execution_infos: dict[str, NodeExecutionInfo] = {}
latest_node_execution_info: Optional[NodeExecutionInfo] = None
current_stream_generate_state: Optional[WorkflowStreamGenerateNodes] = None
iteration_nested_node_ids: list[str] = None
class AdvancedChatTaskState(WorkflowTaskState):
"""
AdvancedChatTaskState entity
"""
usage: LLMUsage
current_stream_generate_state: Optional[ChatflowStreamGenerateRoute] = None answer: str = ""
class StreamEvent(Enum): class StreamEvent(Enum):
""" """
Stream event Stream event
""" """
PING = "ping" PING = "ping"
ERROR = "error" ERROR = "error"
MESSAGE = "message" MESSAGE = "message"
@ -97,6 +51,8 @@ class StreamEvent(Enum):
WORKFLOW_FINISHED = "workflow_finished" WORKFLOW_FINISHED = "workflow_finished"
NODE_STARTED = "node_started" NODE_STARTED = "node_started"
NODE_FINISHED = "node_finished" NODE_FINISHED = "node_finished"
PARALLEL_BRANCH_STARTED = "parallel_branch_started"
PARALLEL_BRANCH_FINISHED = "parallel_branch_finished"
ITERATION_STARTED = "iteration_started" ITERATION_STARTED = "iteration_started"
ITERATION_NEXT = "iteration_next" ITERATION_NEXT = "iteration_next"
ITERATION_COMPLETED = "iteration_completed" ITERATION_COMPLETED = "iteration_completed"
@ -108,6 +64,7 @@ class StreamResponse(BaseModel):
""" """
StreamResponse entity StreamResponse entity
""" """
event: StreamEvent event: StreamEvent
task_id: str task_id: str
@ -119,6 +76,7 @@ class ErrorStreamResponse(StreamResponse):
""" """
ErrorStreamResponse entity ErrorStreamResponse entity
""" """
event: StreamEvent = StreamEvent.ERROR event: StreamEvent = StreamEvent.ERROR
err: Exception err: Exception
model_config = ConfigDict(arbitrary_types_allowed=True) model_config = ConfigDict(arbitrary_types_allowed=True)
@ -128,15 +86,18 @@ class MessageStreamResponse(StreamResponse):
""" """
MessageStreamResponse entity MessageStreamResponse entity
""" """
event: StreamEvent = StreamEvent.MESSAGE event: StreamEvent = StreamEvent.MESSAGE
id: str id: str
answer: str answer: str
from_variable_selector: Optional[list[str]] = None
class MessageAudioStreamResponse(StreamResponse): class MessageAudioStreamResponse(StreamResponse):
""" """
MessageStreamResponse entity MessageStreamResponse entity
""" """
event: StreamEvent = StreamEvent.TTS_MESSAGE event: StreamEvent = StreamEvent.TTS_MESSAGE
audio: str audio: str
@ -145,6 +106,7 @@ class MessageAudioEndStreamResponse(StreamResponse):
""" """
MessageStreamResponse entity MessageStreamResponse entity
""" """
event: StreamEvent = StreamEvent.TTS_MESSAGE_END event: StreamEvent = StreamEvent.TTS_MESSAGE_END
audio: str audio: str
@ -153,6 +115,7 @@ class MessageEndStreamResponse(StreamResponse):
""" """
MessageEndStreamResponse entity MessageEndStreamResponse entity
""" """
event: StreamEvent = StreamEvent.MESSAGE_END event: StreamEvent = StreamEvent.MESSAGE_END
id: str id: str
metadata: dict = {} metadata: dict = {}
@ -162,6 +125,7 @@ class MessageFileStreamResponse(StreamResponse):
""" """
MessageFileStreamResponse entity MessageFileStreamResponse entity
""" """
event: StreamEvent = StreamEvent.MESSAGE_FILE event: StreamEvent = StreamEvent.MESSAGE_FILE
id: str id: str
type: str type: str
@ -173,6 +137,7 @@ class MessageReplaceStreamResponse(StreamResponse):
""" """
MessageReplaceStreamResponse entity MessageReplaceStreamResponse entity
""" """
event: StreamEvent = StreamEvent.MESSAGE_REPLACE event: StreamEvent = StreamEvent.MESSAGE_REPLACE
answer: str answer: str
@ -181,6 +146,7 @@ class AgentThoughtStreamResponse(StreamResponse):
""" """
AgentThoughtStreamResponse entity AgentThoughtStreamResponse entity
""" """
event: StreamEvent = StreamEvent.AGENT_THOUGHT event: StreamEvent = StreamEvent.AGENT_THOUGHT
id: str id: str
position: int position: int
@ -196,6 +162,7 @@ class AgentMessageStreamResponse(StreamResponse):
""" """
AgentMessageStreamResponse entity AgentMessageStreamResponse entity
""" """
event: StreamEvent = StreamEvent.AGENT_MESSAGE event: StreamEvent = StreamEvent.AGENT_MESSAGE
id: str id: str
answer: str answer: str
@ -210,6 +177,7 @@ class WorkflowStartStreamResponse(StreamResponse):
""" """
Data entity Data entity
""" """
id: str id: str
workflow_id: str workflow_id: str
sequence_number: int sequence_number: int
@ -230,6 +198,7 @@ class WorkflowFinishStreamResponse(StreamResponse):
""" """
Data entity Data entity
""" """
id: str id: str
workflow_id: str workflow_id: str
sequence_number: int sequence_number: int
@ -258,6 +227,7 @@ class NodeStartStreamResponse(StreamResponse):
""" """
Data entity Data entity
""" """
id: str id: str
node_id: str node_id: str
node_type: str node_type: str
@ -267,6 +237,11 @@ class NodeStartStreamResponse(StreamResponse):
inputs: Optional[dict] = None inputs: Optional[dict] = None
created_at: int created_at: int
extras: dict = {} extras: dict = {}
parallel_id: Optional[str] = None
parallel_start_node_id: Optional[str] = None
parent_parallel_id: Optional[str] = None
parent_parallel_start_node_id: Optional[str] = None
iteration_id: Optional[str] = None
event: StreamEvent = StreamEvent.NODE_STARTED event: StreamEvent = StreamEvent.NODE_STARTED
workflow_run_id: str workflow_run_id: str
@ -286,8 +261,13 @@ class NodeStartStreamResponse(StreamResponse):
"predecessor_node_id": self.data.predecessor_node_id, "predecessor_node_id": self.data.predecessor_node_id,
"inputs": None, "inputs": None,
"created_at": self.data.created_at, "created_at": self.data.created_at,
"extras": {} "extras": {},
} "parallel_id": self.data.parallel_id,
"parallel_start_node_id": self.data.parallel_start_node_id,
"parent_parallel_id": self.data.parent_parallel_id,
"parent_parallel_start_node_id": self.data.parent_parallel_start_node_id,
"iteration_id": self.data.iteration_id,
},
} }
@ -300,6 +280,7 @@ class NodeFinishStreamResponse(StreamResponse):
""" """
Data entity Data entity
""" """
id: str id: str
node_id: str node_id: str
node_type: str node_type: str
@ -316,6 +297,11 @@ class NodeFinishStreamResponse(StreamResponse):
created_at: int created_at: int
finished_at: int finished_at: int
files: Optional[list[dict]] = [] files: Optional[list[dict]] = []
parallel_id: Optional[str] = None
parallel_start_node_id: Optional[str] = None
parent_parallel_id: Optional[str] = None
parent_parallel_start_node_id: Optional[str] = None
iteration_id: Optional[str] = None
event: StreamEvent = StreamEvent.NODE_FINISHED event: StreamEvent = StreamEvent.NODE_FINISHED
workflow_run_id: str workflow_run_id: str
@ -342,11 +328,62 @@ class NodeFinishStreamResponse(StreamResponse):
"execution_metadata": None, "execution_metadata": None,
"created_at": self.data.created_at, "created_at": self.data.created_at,
"finished_at": self.data.finished_at, "finished_at": self.data.finished_at,
"files": [] "files": [],
} "parallel_id": self.data.parallel_id,
"parallel_start_node_id": self.data.parallel_start_node_id,
"parent_parallel_id": self.data.parent_parallel_id,
"parent_parallel_start_node_id": self.data.parent_parallel_start_node_id,
"iteration_id": self.data.iteration_id,
},
} }
class ParallelBranchStartStreamResponse(StreamResponse):
"""
ParallelBranchStartStreamResponse entity
"""
class Data(BaseModel):
"""
Data entity
"""
parallel_id: str
parallel_branch_id: str
parent_parallel_id: Optional[str] = None
parent_parallel_start_node_id: Optional[str] = None
iteration_id: Optional[str] = None
created_at: int
event: StreamEvent = StreamEvent.PARALLEL_BRANCH_STARTED
workflow_run_id: str
data: Data
class ParallelBranchFinishedStreamResponse(StreamResponse):
"""
ParallelBranchFinishedStreamResponse entity
"""
class Data(BaseModel):
"""
Data entity
"""
parallel_id: str
parallel_branch_id: str
parent_parallel_id: Optional[str] = None
parent_parallel_start_node_id: Optional[str] = None
iteration_id: Optional[str] = None
status: str
error: Optional[str] = None
created_at: int
event: StreamEvent = StreamEvent.PARALLEL_BRANCH_FINISHED
workflow_run_id: str
data: Data
class IterationNodeStartStreamResponse(StreamResponse): class IterationNodeStartStreamResponse(StreamResponse):
""" """
NodeStartStreamResponse entity NodeStartStreamResponse entity
@ -356,6 +393,7 @@ class IterationNodeStartStreamResponse(StreamResponse):
""" """
Data entity Data entity
""" """
id: str id: str
node_id: str node_id: str
node_type: str node_type: str
@ -364,6 +402,8 @@ class IterationNodeStartStreamResponse(StreamResponse):
extras: dict = {} extras: dict = {}
metadata: dict = {} metadata: dict = {}
inputs: dict = {} inputs: dict = {}
parallel_id: Optional[str] = None
parallel_start_node_id: Optional[str] = None
event: StreamEvent = StreamEvent.ITERATION_STARTED event: StreamEvent = StreamEvent.ITERATION_STARTED
workflow_run_id: str workflow_run_id: str
@ -379,6 +419,7 @@ class IterationNodeNextStreamResponse(StreamResponse):
""" """
Data entity Data entity
""" """
id: str id: str
node_id: str node_id: str
node_type: str node_type: str
@ -387,6 +428,8 @@ class IterationNodeNextStreamResponse(StreamResponse):
created_at: int created_at: int
pre_iteration_output: Optional[Any] = None pre_iteration_output: Optional[Any] = None
extras: dict = {} extras: dict = {}
parallel_id: Optional[str] = None
parallel_start_node_id: Optional[str] = None
event: StreamEvent = StreamEvent.ITERATION_NEXT event: StreamEvent = StreamEvent.ITERATION_NEXT
workflow_run_id: str workflow_run_id: str
@ -402,14 +445,15 @@ class IterationNodeCompletedStreamResponse(StreamResponse):
""" """
Data entity Data entity
""" """
id: str id: str
node_id: str node_id: str
node_type: str node_type: str
title: str title: str
outputs: Optional[dict] = None outputs: Optional[dict] = None
created_at: int created_at: int
extras: dict = None extras: Optional[dict] = None
inputs: dict = None inputs: Optional[dict] = None
status: WorkflowNodeExecutionStatus status: WorkflowNodeExecutionStatus
error: Optional[str] = None error: Optional[str] = None
elapsed_time: float elapsed_time: float
@ -417,6 +461,8 @@ class IterationNodeCompletedStreamResponse(StreamResponse):
execution_metadata: Optional[dict] = None execution_metadata: Optional[dict] = None
finished_at: int finished_at: int
steps: int steps: int
parallel_id: Optional[str] = None
parallel_start_node_id: Optional[str] = None
event: StreamEvent = StreamEvent.ITERATION_COMPLETED event: StreamEvent = StreamEvent.ITERATION_COMPLETED
workflow_run_id: str workflow_run_id: str
@ -432,7 +478,9 @@ class TextChunkStreamResponse(StreamResponse):
""" """
Data entity Data entity
""" """
text: str text: str
from_variable_selector: Optional[list[str]] = None
event: StreamEvent = StreamEvent.TEXT_CHUNK event: StreamEvent = StreamEvent.TEXT_CHUNK
data: Data data: Data
@ -447,6 +495,7 @@ class TextReplaceStreamResponse(StreamResponse):
""" """
Data entity Data entity
""" """
text: str text: str
event: StreamEvent = StreamEvent.TEXT_REPLACE event: StreamEvent = StreamEvent.TEXT_REPLACE
@ -457,6 +506,7 @@ class PingStreamResponse(StreamResponse):
""" """
PingStreamResponse entity PingStreamResponse entity
""" """
event: StreamEvent = StreamEvent.PING event: StreamEvent = StreamEvent.PING
@ -464,6 +514,7 @@ class AppStreamResponse(BaseModel):
""" """
AppStreamResponse entity AppStreamResponse entity
""" """
stream_response: StreamResponse stream_response: StreamResponse
@ -471,6 +522,7 @@ class ChatbotAppStreamResponse(AppStreamResponse):
""" """
ChatbotAppStreamResponse entity ChatbotAppStreamResponse entity
""" """
conversation_id: str conversation_id: str
message_id: str message_id: str
created_at: int created_at: int
@ -480,6 +532,7 @@ class CompletionAppStreamResponse(AppStreamResponse):
""" """
CompletionAppStreamResponse entity CompletionAppStreamResponse entity
""" """
message_id: str message_id: str
created_at: int created_at: int
@ -488,13 +541,15 @@ class WorkflowAppStreamResponse(AppStreamResponse):
""" """
WorkflowAppStreamResponse entity WorkflowAppStreamResponse entity
""" """
workflow_run_id: str
workflow_run_id: Optional[str] = None
class AppBlockingResponse(BaseModel): class AppBlockingResponse(BaseModel):
""" """
AppBlockingResponse entity AppBlockingResponse entity
""" """
task_id: str task_id: str
def to_dict(self) -> dict: def to_dict(self) -> dict:
@ -510,6 +565,7 @@ class ChatbotAppBlockingResponse(AppBlockingResponse):
""" """
Data entity Data entity
""" """
id: str id: str
mode: str mode: str
conversation_id: str conversation_id: str
@ -530,6 +586,7 @@ class CompletionAppBlockingResponse(AppBlockingResponse):
""" """
Data entity Data entity
""" """
id: str id: str
mode: str mode: str
message_id: str message_id: str
@ -549,6 +606,7 @@ class WorkflowAppBlockingResponse(AppBlockingResponse):
""" """
Data entity Data entity
""" """
id: str id: str
workflow_id: str workflow_id: str
status: str status: str
@ -562,25 +620,3 @@ class WorkflowAppBlockingResponse(AppBlockingResponse):
workflow_run_id: str workflow_run_id: str
data: Data data: Data
class WorkflowIterationState(BaseModel):
"""
WorkflowIterationState entity
"""
class Data(BaseModel):
"""
Data entity
"""
parent_iteration_id: Optional[str] = None
iteration_id: str
current_index: int
iteration_steps_boundary: list[int] = None
node_execution_id: str
started_at: float
inputs: dict = None
total_tokens: int = 0
node_data: BaseNodeData
current_iterations: dict[str, Data] = None

@ -13,11 +13,9 @@ logger = logging.getLogger(__name__)
class AnnotationReplyFeature: class AnnotationReplyFeature:
def query(self, app_record: App, def query(
message: Message, self, app_record: App, message: Message, query: str, user_id: str, invoke_from: InvokeFrom
query: str, ) -> Optional[MessageAnnotation]:
user_id: str,
invoke_from: InvokeFrom) -> Optional[MessageAnnotation]:
""" """
Query app annotations to reply Query app annotations to reply
:param app_record: app record :param app_record: app record
@ -27,8 +25,9 @@ class AnnotationReplyFeature:
:param invoke_from: invoke from :param invoke_from: invoke from
:return: :return:
""" """
annotation_setting = db.session.query(AppAnnotationSetting).filter( annotation_setting = (
AppAnnotationSetting.app_id == app_record.id).first() db.session.query(AppAnnotationSetting).filter(AppAnnotationSetting.app_id == app_record.id).first()
)
if not annotation_setting: if not annotation_setting:
return None return None
@ -41,55 +40,50 @@ class AnnotationReplyFeature:
embedding_model_name = collection_binding_detail.model_name embedding_model_name = collection_binding_detail.model_name
dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding( dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding(
embedding_provider_name, embedding_provider_name, embedding_model_name, "annotation"
embedding_model_name,
'annotation'
) )
dataset = Dataset( dataset = Dataset(
id=app_record.id, id=app_record.id,
tenant_id=app_record.tenant_id, tenant_id=app_record.tenant_id,
indexing_technique='high_quality', indexing_technique="high_quality",
embedding_model_provider=embedding_provider_name, embedding_model_provider=embedding_provider_name,
embedding_model=embedding_model_name, embedding_model=embedding_model_name,
collection_binding_id=dataset_collection_binding.id collection_binding_id=dataset_collection_binding.id,
) )
vector = Vector(dataset, attributes=['doc_id', 'annotation_id', 'app_id']) vector = Vector(dataset, attributes=["doc_id", "annotation_id", "app_id"])
documents = vector.search_by_vector( documents = vector.search_by_vector(
query=query, query=query, top_k=1, score_threshold=score_threshold, filter={"group_id": [dataset.id]}
top_k=1,
score_threshold=score_threshold,
filter={
'group_id': [dataset.id]
}
) )
if documents: if documents:
annotation_id = documents[0].metadata['annotation_id'] annotation_id = documents[0].metadata["annotation_id"]
score = documents[0].metadata['score'] score = documents[0].metadata["score"]
annotation = AppAnnotationService.get_annotation_by_id(annotation_id) annotation = AppAnnotationService.get_annotation_by_id(annotation_id)
if annotation: if annotation:
if invoke_from in [InvokeFrom.SERVICE_API, InvokeFrom.WEB_APP]: if invoke_from in [InvokeFrom.SERVICE_API, InvokeFrom.WEB_APP]:
from_source = 'api' from_source = "api"
else: else:
from_source = 'console' from_source = "console"
# insert annotation history # insert annotation history
AppAnnotationService.add_annotation_history(annotation.id, AppAnnotationService.add_annotation_history(
app_record.id, annotation.id,
annotation.question, app_record.id,
annotation.content, annotation.question,
query, annotation.content,
user_id, query,
message.id, user_id,
from_source, message.id,
score) from_source,
score,
)
return annotation return annotation
except Exception as e: except Exception as e:
logger.warning(f'Query annotation failed, exception: {str(e)}.') logger.warning(f"Query annotation failed, exception: {str(e)}.")
return None return None
return None return None

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save