Merge branch 'feat/workflow-parallel-support' of github.com:langgenius/dify into feat/workflow-parallel-support

This commit is contained in:
Yi 2024-09-03 10:35:15 +08:00
commit b28c7b1cda
122 changed files with 4511 additions and 239 deletions

View File

@ -8,7 +8,7 @@ In terms of licensing, please take a minute to read our short [License and Contr
## 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:

View File

@ -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) 一个新问题。我们将问题分为两类:
### 功能请求:

View File

@ -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 つのタイプに分類しています。
### 機能リクエスト

View File

@ -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
[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:

View File

@ -60,7 +60,8 @@ ALIYUN_OSS_SECRET_KEY=your-secret-key
ALIYUN_OSS_ENDPOINT=your-endpoint
ALIYUN_OSS_AUTH_VERSION=v1
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_BUCKET_NAME=yout-bucket-name
GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64=your-google-service-account-json-base64-string

View File

@ -559,8 +559,9 @@ def add_qdrant_doc_id_index(field: str):
@click.command("create-tenant", help="Create account and tenant.")
@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.")
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
"""
@ -580,13 +581,15 @@ def create_tenant(email: str, language: Optional[str] = None):
if language not in languages:
language = "en-US"
name = name.strip()
# generate random password
new_password = secrets.token_urlsafe(16)
# register account
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.style(

View File

@ -1,4 +1,4 @@
from typing import Optional
from typing import Annotated, Optional
from pydantic import AliasChoices, Field, HttpUrl, NegativeInt, NonNegativeInt, PositiveInt, computed_field
from pydantic_settings import BaseSettings
@ -217,20 +217,17 @@ class HttpConfig(BaseSettings):
def WEB_API_CORS_ALLOW_ORIGINS(self) -> list[str]:
return self.inner_WEB_API_CORS_ALLOW_ORIGINS.split(",")
HTTP_REQUEST_MAX_CONNECT_TIMEOUT: NonNegativeInt = Field(
description="",
default=300,
)
HTTP_REQUEST_MAX_CONNECT_TIMEOUT: Annotated[
PositiveInt, Field(ge=10, description="connect timeout in seconds for HTTP request")
] = 10
HTTP_REQUEST_MAX_READ_TIMEOUT: NonNegativeInt = Field(
description="",
default=600,
)
HTTP_REQUEST_MAX_READ_TIMEOUT: Annotated[
PositiveInt, Field(ge=60, description="read timeout in seconds for HTTP request")
] = 60
HTTP_REQUEST_MAX_WRITE_TIMEOUT: NonNegativeInt = Field(
description="",
default=600,
)
HTTP_REQUEST_MAX_WRITE_TIMEOUT: Annotated[
PositiveInt, Field(ge=10, description="read timeout in seconds for HTTP request")
] = 20
HTTP_REQUEST_NODE_MAX_BINARY_SIZE: PositiveInt = Field(
description="",

View File

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

View File

@ -174,6 +174,7 @@ class AppApi(Resource):
parser.add_argument("icon", 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("use_icon_as_answer_icon", type=bool, location="json")
args = parser.parse_args()
app_service = AppService()

View File

@ -173,21 +173,18 @@ class ChatConversationApi(Resource):
if args["keyword"]:
keyword_filter = "%{}%".format(args["keyword"])
query = (
query.join(
Message,
Message.conversation_id == Conversation.id,
)
.join(subquery, subquery.c.conversation_id == Conversation.id)
.filter(
or_(
Message.query.ilike(keyword_filter),
Message.answer.ilike(keyword_filter),
Conversation.name.ilike(keyword_filter),
Conversation.introduction.ilike(keyword_filter),
subquery.c.from_end_user_session_id.ilike(keyword_filter),
),
)
message_subquery = (
db.session.query(Message.conversation_id)
.filter(or_(Message.query.ilike(keyword_filter), Message.answer.ilike(keyword_filter)))
.subquery()
)
query = query.join(subquery, subquery.c.conversation_id == Conversation.id).filter(
or_(
Conversation.id.in_(message_subquery),
Conversation.name.ilike(keyword_filter),
Conversation.introduction.ilike(keyword_filter),
subquery.c.from_end_user_session_id.ilike(keyword_filter),
),
)
account = current_user

View File

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

View File

@ -122,6 +122,7 @@ class DatasetListApi(Resource):
name=args["name"],
indexing_technique=args["indexing_technique"],
account=current_user,
permission=DatasetPermissionEnum.ONLY_ME,
)
except services.errors.dataset.DatasetNameDuplicateError:
raise DatasetNameDuplicateError()

View File

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

View File

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

View File

@ -46,9 +46,7 @@ def only_edition_self_hosted(view):
return decorated
def cloud_edition_billing_resource_check(
resource: str, error_msg: str = "You have reached the limit of your subscription."
):
def cloud_edition_billing_resource_check(resource: str):
def interceptor(view):
@wraps(view)
def decorated(*args, **kwargs):
@ -60,22 +58,22 @@ def cloud_edition_billing_resource_check(
documents_upload_quota = features.documents_upload_quota
annotation_quota_limit = features.annotation_quota_limit
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:
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:
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:
# 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")
if source == "datasets":
abort(403, error_msg)
abort(403, "The number of documents has reached the limit of your subscription.")
else:
return view(*args, **kwargs)
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:
abort(403, error_msg)
abort(403, "The annotation quota has reached the limit of your subscription.")
else:
return view(*args, **kwargs)
@ -86,10 +84,7 @@ def cloud_edition_billing_resource_check(
return interceptor
def cloud_edition_billing_knowledge_limit_check(
resource: str,
error_msg: str = "To unlock this feature and elevate your Dify experience, please upgrade to a paid plan.",
):
def cloud_edition_billing_knowledge_limit_check(resource: str):
def interceptor(view):
@wraps(view)
def decorated(*args, **kwargs):
@ -97,7 +92,10 @@ def cloud_edition_billing_knowledge_limit_check(
if features.billing.enabled:
if resource == "add_segment":
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:
return view(*args, **kwargs)

View File

@ -83,9 +83,7 @@ def validate_app_token(view: Optional[Callable] = None, *, fetch_user_arg: Optio
return decorator(view)
def cloud_edition_billing_resource_check(
resource: str, api_token_type: str, error_msg: str = "You have reached the limit of your subscription."
):
def cloud_edition_billing_resource_check(resource: str, api_token_type: str):
def interceptor(view):
def decorated(*args, **kwargs):
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
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:
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:
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:
raise Forbidden(error_msg)
raise Forbidden("The number of documents has reached the limit of your subscription.")
else:
return view(*args, **kwargs)
@ -115,11 +113,7 @@ def cloud_edition_billing_resource_check(
return interceptor
def cloud_edition_billing_knowledge_limit_check(
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 cloud_edition_billing_knowledge_limit_check(resource: str, api_token_type: str):
def interceptor(view):
@wraps(view)
def decorated(*args, **kwargs):
@ -128,7 +122,9 @@ def cloud_edition_billing_knowledge_limit_check(
if features.billing.enabled:
if resource == "add_segment":
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:
return view(*args, **kwargs)

View File

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

View File

@ -93,7 +93,7 @@ class DatasetConfigManager:
reranking_model=dataset_configs.get('reranking_model'),
weights=dataset_configs.get('weights'),
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'),
)
)

View File

@ -4,7 +4,7 @@ import os
import threading
import uuid
from collections.abc import Generator
from typing import Any, Optional, Union
from typing import Any, Literal, Optional, Union, overload
from flask import Flask, current_app
from pydantic import ValidationError
@ -32,6 +32,26 @@ logger = logging.getLogger(__name__)
class AdvancedChatAppGenerator(MessageBasedAppGenerator):
@overload
def generate(
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,

View File

@ -3,7 +3,7 @@ import os
import threading
import uuid
from collections.abc import Generator
from typing import Any, Union
from typing import Any, Literal, Union, overload
from flask import Flask, current_app
from pydantic import ValidationError
@ -28,6 +28,24 @@ logger = logging.getLogger(__name__)
class AgentChatAppGenerator(MessageBasedAppGenerator):
@overload
def generate(
self, app_model: App,
user: Union[Account, EndUser],
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,

View File

@ -3,7 +3,7 @@ import os
import threading
import uuid
from collections.abc import Generator
from typing import Any, Union
from typing import Any, Literal, Union, overload
from flask import Flask, current_app
from pydantic import ValidationError
@ -28,13 +28,31 @@ logger = logging.getLogger(__name__)
class ChatAppGenerator(MessageBasedAppGenerator):
@overload
def generate(
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],
args: Any,
invoke_from: InvokeFrom,
stream: bool = True,
) -> Union[dict, Generator[dict, None, None]]:
) -> Union[dict, Generator[str, None, None]]:
"""
Generate App response.

View File

@ -3,7 +3,7 @@ import os
import threading
import uuid
from collections.abc import Generator
from typing import Any, Union
from typing import Any, Literal, Union, overload
from flask import Flask, current_app
from pydantic import ValidationError
@ -30,12 +30,30 @@ logger = logging.getLogger(__name__)
class CompletionAppGenerator(MessageBasedAppGenerator):
@overload
def generate(
self, app_model: App,
user: Union[Account, EndUser],
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[dict, None, None]]:
-> Union[dict, Generator[str, None, None]]:
"""
Generate App response.
@ -203,7 +221,7 @@ class CompletionAppGenerator(MessageBasedAppGenerator):
user: Union[Account, EndUser],
invoke_from: InvokeFrom,
stream: bool = True) \
-> Union[dict, Generator[dict, None, None]]:
-> Union[dict, Generator[str, None, None]]:
"""
Generate App response.

View File

@ -4,7 +4,7 @@ import os
import threading
import uuid
from collections.abc import Generator
from typing import Any, Union
from typing import Any, Literal, Optional, Union, overload
from flask import Flask, current_app
from pydantic import ValidationError
@ -32,8 +32,32 @@ logger = logging.getLogger(__name__)
class WorkflowAppGenerator(BaseAppGenerator):
@overload
def generate(
self,
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,
user: Union[Account, EndUser],
@ -41,6 +65,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
invoke_from: InvokeFrom,
stream: bool = True,
call_depth: int = 0,
workflow_thread_pool_id: Optional[str] = None
):
"""
Generate App response.
@ -52,6 +77,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
:param invoke_from: invoke from source
:param stream: is stream
:param call_depth: call depth
:param workflow_thread_pool_id: workflow thread pool id
"""
inputs = args['inputs']
@ -99,6 +125,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
application_generate_entity=application_generate_entity,
invoke_from=invoke_from,
stream=stream,
workflow_thread_pool_id=workflow_thread_pool_id
)
def _generate(
@ -109,7 +136,8 @@ class WorkflowAppGenerator(BaseAppGenerator):
application_generate_entity: WorkflowAppGenerateEntity,
invoke_from: InvokeFrom,
stream: bool = True,
) -> dict[str, Any] | Generator[str, Any, None]:
workflow_thread_pool_id: Optional[str] = None
) -> dict[str, Any] | Generator[str, None, None]:
"""
Generate App response.
@ -119,6 +147,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
:param application_generate_entity: application generate entity
:param invoke_from: invoke from source
:param stream: is stream
:param workflow_thread_pool_id: workflow thread pool id
"""
# init queue manager
queue_manager = WorkflowAppQueueManager(
@ -133,7 +162,8 @@ class WorkflowAppGenerator(BaseAppGenerator):
'flask_app': current_app._get_current_object(), # type: ignore
'application_generate_entity': application_generate_entity,
'queue_manager': queue_manager,
'context': contextvars.copy_context()
'context': contextvars.copy_context(),
'workflow_thread_pool_id': workflow_thread_pool_id
})
worker_thread.start()
@ -211,12 +241,14 @@ class WorkflowAppGenerator(BaseAppGenerator):
def _generate_worker(self, flask_app: Flask,
application_generate_entity: WorkflowAppGenerateEntity,
queue_manager: AppQueueManager,
context: contextvars.Context) -> None:
context: contextvars.Context,
workflow_thread_pool_id: Optional[str] = None) -> None:
"""
Generate worker in a new thread.
:param flask_app: Flask app
:param application_generate_entity: application generate entity
:param queue_manager: queue manager
:param workflow_thread_pool_id: workflow thread pool id
:return:
"""
for var, val in context.items():
@ -226,9 +258,10 @@ class WorkflowAppGenerator(BaseAppGenerator):
# workflow app
runner = WorkflowAppRunner(
application_generate_entity=application_generate_entity,
queue_manager=queue_manager
queue_manager=queue_manager,
workflow_thread_pool_id=workflow_thread_pool_id
)
runner.run()
except GenerateTaskStoppedException:
pass

View File

@ -1,6 +1,6 @@
import logging
import os
from typing import cast
from typing import Optional, cast
from core.app.apps.base_app_queue_manager import AppQueueManager
from core.app.apps.workflow.app_config_manager import WorkflowAppConfig
@ -29,14 +29,17 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
def __init__(
self,
application_generate_entity: WorkflowAppGenerateEntity,
queue_manager: AppQueueManager
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:
"""
@ -116,6 +119,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
invoke_from=self.application_generate_entity.invoke_from,
call_depth=self.application_generate_entity.call_depth,
variable_pool=variable_pool,
thread_pool_id=self.workflow_thread_pool_id
)
generator = workflow_entry.run(

View File

@ -150,9 +150,9 @@ class OAIAPICompatLargeLanguageModel(_CommonOAI_API_Compat, LargeLanguageModel):
except json.JSONDecodeError as e:
raise CredentialsValidateFailedError('Credentials validation failed: JSON decode error')
if (completion_type is LLMMode.CHAT and json_result['object'] == ''):
if (completion_type is LLMMode.CHAT and json_result.get('object','') == ''):
json_result['object'] = 'chat.completion'
elif (completion_type is LLMMode.COMPLETION and json_result['object'] == ''):
elif (completion_type is LLMMode.COMPLETION and json_result.get('object','') == ''):
json_result['object'] = 'text_completion'
if (completion_type is LLMMode.CHAT

View File

@ -71,11 +71,24 @@ class ArkClientV3:
args = {
"base_url": credentials['api_endpoint_host'],
"region": credentials['volc_region'],
"ak": credentials['volc_access_key_id'],
"sk": credentials['volc_secret_access_key'],
}
if credentials.get("auth_method") == "api_key":
args = {
**args,
"api_key": credentials['volc_api_key'],
}
else:
args = {
**args,
"ak": credentials['volc_access_key_id'],
"sk": credentials['volc_secret_access_key'],
}
if cls.is_compatible_with_legacy(credentials):
args["base_url"] = DEFAULT_V3_ENDPOINT
args = {
**args,
"base_url": DEFAULT_V3_ENDPOINT
}
client = ArkClientV3(
**args

View File

@ -30,8 +30,28 @@ model_credential_schema:
en_US: Enter your Model Name
zh_Hans: 输入模型名称
credential_form_schemas:
- variable: auth_method
required: true
label:
en_US: Authentication Method
zh_Hans: 鉴权方式
type: select
default: aksk
options:
- label:
en_US: API Key
value: api_key
- label:
en_US: Access Key / Secret Access Key
value: aksk
placeholder:
en_US: Enter your Authentication Method
zh_Hans: 选择鉴权方式
- variable: volc_access_key_id
required: true
show_on:
- variable: auth_method
value: aksk
label:
en_US: Access Key
zh_Hans: Access Key
@ -41,6 +61,9 @@ model_credential_schema:
zh_Hans: 输入您的 Access Key
- variable: volc_secret_access_key
required: true
show_on:
- variable: auth_method
value: aksk
label:
en_US: Secret Access Key
zh_Hans: Secret Access Key
@ -48,6 +71,17 @@ model_credential_schema:
placeholder:
en_US: Enter your Secret Access Key
zh_Hans: 输入您的 Secret Access Key
- variable: volc_api_key
required: true
show_on:
- variable: auth_method
value: api_key
label:
en_US: API Key
type: secret-input
placeholder:
en_US: Enter your API Key
zh_Hans: 输入您的 API Key
- variable: volc_region
required: true
label:

View File

@ -38,7 +38,7 @@ parameter_rules:
min: 1
max: 8192
pricing:
input: '0.0001'
output: '0.0001'
input: '0'
output: '0'
unit: '0.001'
currency: RMB

View File

@ -37,3 +37,8 @@ parameter_rules:
default: 1024
min: 1
max: 8192
pricing:
input: '0.001'
output: '0.001'
unit: '0.001'
currency: RMB

View File

@ -37,3 +37,8 @@ parameter_rules:
default: 1024
min: 1
max: 8192
pricing:
input: '0.1'
output: '0.1'
unit: '0.001'
currency: RMB

View File

@ -30,4 +30,9 @@ parameter_rules:
use_template: max_tokens
default: 1024
min: 1
max: 4096
max: 8192
pricing:
input: '0.001'
output: '0.001'
unit: '0.001'
currency: RMB

View File

@ -0,0 +1,44 @@
model: glm-4-plus
label:
en_US: glm-4-plus
model_type: llm
features:
- multi-tool-call
- agent-thought
- stream-tool-call
model_properties:
mode: chat
parameter_rules:
- name: temperature
use_template: temperature
default: 0.95
min: 0.0
max: 1.0
help:
zh_Hans: 采样温度,控制输出的随机性,必须为正数取值范围是:(0.0,1.0],不能等于 0,默认值为 0.95 值越大,会使输出更随机,更具创造性;值越小,输出会更加稳定或确定建议您根据应用场景调整 top_p 或 temperature 参数,但不要同时调整两个参数。
en_US: Sampling temperature, controls the randomness of the output, must be a positive number. The value range is (0.0,1.0], which cannot be equal to 0. The default value is 0.95. The larger the value, the more random and creative the output will be; the smaller the value, The output will be more stable or certain. It is recommended that you adjust the top_p or temperature parameters according to the application scenario, but do not adjust both parameters at the same time.
- name: top_p
use_template: top_p
default: 0.7
help:
zh_Hans: 用温度取样的另一种方法,称为核取样取值范围是:(0.0, 1.0) 开区间,不能等于 0 或 1默认值为 0.7 模型考虑具有 top_p 概率质量tokens的结果例如0.1 意味着模型解码器只考虑从前 10% 的概率的候选集中取 tokens 建议您根据应用场景调整 top_p 或 temperature 参数,但不要同时调整两个参数。
en_US: Another method of temperature sampling is called kernel sampling. The value range is (0.0, 1.0) open interval, which cannot be equal to 0 or 1. The default value is 0.7. The model considers the results with top_p probability mass tokens. For example 0.1 means The model decoder only considers tokens from the candidate set with the top 10% probability. It is recommended that you adjust the top_p or temperature parameters according to the application scenario, but do not adjust both parameters at the same time.
- name: incremental
label:
zh_Hans: 增量返回
en_US: Incremental
type: boolean
help:
zh_Hans: SSE接口调用时用于控制每次返回内容方式是增量还是全量不提供此参数时默认为增量返回true 为增量返回false 为全量返回。
en_US: When the SSE interface is called, it is used to control whether the content is returned incrementally or in full. If this parameter is not provided, the default is incremental return. true means incremental return, false means full return.
required: false
- name: max_tokens
use_template: max_tokens
default: 1024
min: 1
max: 8192
pricing:
input: '0.05'
output: '0.05'
unit: '0.001'
currency: RMB

View File

@ -34,4 +34,9 @@ parameter_rules:
use_template: max_tokens
default: 1024
min: 1
max: 8192
max: 1024
pricing:
input: '0.05'
output: '0.05'
unit: '0.001'
currency: RMB

View File

@ -0,0 +1,42 @@
model: glm-4v-plus
label:
en_US: glm-4v-plus
model_type: llm
model_properties:
mode: chat
features:
- vision
parameter_rules:
- name: temperature
use_template: temperature
default: 0.95
min: 0.0
max: 1.0
help:
zh_Hans: 采样温度,控制输出的随机性,必须为正数取值范围是:(0.0,1.0],不能等于 0,默认值为 0.95 值越大,会使输出更随机,更具创造性;值越小,输出会更加稳定或确定建议您根据应用场景调整 top_p 或 temperature 参数,但不要同时调整两个参数。
en_US: Sampling temperature, controls the randomness of the output, must be a positive number. The value range is (0.0,1.0], which cannot be equal to 0. The default value is 0.95. The larger the value, the more random and creative the output will be; the smaller the value, The output will be more stable or certain. It is recommended that you adjust the top_p or temperature parameters according to the application scenario, but do not adjust both parameters at the same time.
- name: top_p
use_template: top_p
default: 0.7
help:
zh_Hans: 用温度取样的另一种方法,称为核取样取值范围是:(0.0, 1.0) 开区间,不能等于 0 或 1默认值为 0.7 模型考虑具有 top_p 概率质量tokens的结果例如0.1 意味着模型解码器只考虑从前 10% 的概率的候选集中取 tokens 建议您根据应用场景调整 top_p 或 temperature 参数,但不要同时调整两个参数。
en_US: Another method of temperature sampling is called kernel sampling. The value range is (0.0, 1.0) open interval, which cannot be equal to 0 or 1. The default value is 0.7. The model considers the results with top_p probability mass tokens. For example 0.1 means The model decoder only considers tokens from the candidate set with the top 10% probability. It is recommended that you adjust the top_p or temperature parameters according to the application scenario, but do not adjust both parameters at the same time.
- name: incremental
label:
zh_Hans: 增量返回
en_US: Incremental
type: boolean
help:
zh_Hans: SSE接口调用时用于控制每次返回内容方式是增量还是全量不提供此参数时默认为增量返回true 为增量返回false 为全量返回。
en_US: When the SSE interface is called, it is used to control whether the content is returned incrementally or in full. If this parameter is not provided, the default is incremental return. true means incremental return, false means full return.
required: false
- name: max_tokens
use_template: max_tokens
default: 1024
min: 1
max: 1024
pricing:
input: '0.01'
output: '0.01'
unit: '0.001'
currency: RMB

View File

@ -153,7 +153,8 @@ class ZhipuAILargeLanguageModel(_CommonZhipuaiAI, LargeLanguageModel):
:return: full response or stream response chunk generator result
"""
extra_model_kwargs = {}
if stop:
# request to glm-4v-plus with stop words will always response "finish_reason":"network_error"
if stop and model!= 'glm-4v-plus':
extra_model_kwargs['stop'] = stop
client = ZhipuAI(
@ -174,7 +175,7 @@ class ZhipuAILargeLanguageModel(_CommonZhipuaiAI, LargeLanguageModel):
if copy_prompt_message.role in [PromptMessageRole.USER, PromptMessageRole.SYSTEM, PromptMessageRole.TOOL]:
if isinstance(copy_prompt_message.content, list):
# check if model is 'glm-4v'
if model != 'glm-4v':
if model not in ('glm-4v', 'glm-4v-plus'):
# not support list message
continue
# get image and
@ -207,7 +208,7 @@ class ZhipuAILargeLanguageModel(_CommonZhipuaiAI, LargeLanguageModel):
else:
new_prompt_messages.append(copy_prompt_message)
if model == 'glm-4v':
if model == 'glm-4v' or model == 'glm-4v-plus':
params = self._construct_glm_4v_parameter(model, new_prompt_messages, model_parameters)
else:
params = {
@ -304,7 +305,7 @@ class ZhipuAILargeLanguageModel(_CommonZhipuaiAI, LargeLanguageModel):
return params
def _construct_glm_4v_messages(self, prompt_message: Union[str | list[PromptMessageContent]]) -> list[dict]:
def _construct_glm_4v_messages(self, prompt_message: Union[str, list[PromptMessageContent]]) -> list[dict]:
if isinstance(prompt_message, str):
return [{'type': 'text', 'text': prompt_message}]

View File

@ -170,6 +170,8 @@ class WordExtractor(BaseExtractor):
if run.element.xpath('.//a:blip'):
for blip in run.element.xpath('.//a:blip'):
image_id = blip.get("{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed")
if not image_id:
continue
image_part = paragraph.part.rels[image_id].target_part
if image_part in image_map:
@ -256,6 +258,6 @@ class WordExtractor(BaseExtractor):
content.append(parsed_paragraph)
elif isinstance(element.tag, str) and element.tag.endswith('tbl'): # table
table = tables.pop(0)
content.append(self._table_to_markdown(table,image_map))
content.append(self._table_to_markdown(table, image_map))
return '\n'.join(content)

View File

@ -1,5 +1,6 @@
- google
- bing
- perplexity
- duckduckgo
- searchapi
- serper

View File

@ -0,0 +1,3 @@
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M101.008 42L190.99 124.905L190.99 124.886L190.99 42.1913H208.506L208.506 125.276L298.891 42V136.524L336 136.524V272.866H299.005V357.035L208.506 277.525L208.506 357.948H190.99L190.99 278.836L101.11 358V272.866H64V136.524H101.008V42ZM177.785 153.826H81.5159V255.564H101.088V223.472L177.785 153.826ZM118.625 231.149V319.392L190.99 255.655L190.99 165.421L118.625 231.149ZM209.01 254.812V165.336L281.396 231.068V272.866H281.489V318.491L209.01 254.812ZM299.005 255.564H318.484V153.826L222.932 153.826L299.005 222.751V255.564ZM281.375 136.524V81.7983L221.977 136.524L281.375 136.524ZM177.921 136.524H118.524V81.7983L177.921 136.524Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 798 B

View File

@ -0,0 +1,46 @@
from typing import Any
import requests
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.provider.builtin.perplexity.tools.perplexity_search import PERPLEXITY_API_URL
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
class PerplexityProvider(BuiltinToolProviderController):
def _validate_credentials(self, credentials: dict[str, Any]) -> None:
headers = {
"Authorization": f"Bearer {credentials.get('perplexity_api_key')}",
"Content-Type": "application/json"
}
payload = {
"model": "llama-3.1-sonar-small-128k-online",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "Hello"
}
],
"max_tokens": 5,
"temperature": 0.1,
"top_p": 0.9,
"stream": False
}
try:
response = requests.post(PERPLEXITY_API_URL, json=payload, headers=headers)
response.raise_for_status()
except requests.RequestException as e:
raise ToolProviderCredentialValidationError(
f"Failed to validate Perplexity API key: {str(e)}"
)
if response.status_code != 200:
raise ToolProviderCredentialValidationError(
f"Perplexity API key is invalid. Status code: {response.status_code}"
)

View File

@ -0,0 +1,26 @@
identity:
author: Dify
name: perplexity
label:
en_US: Perplexity
zh_Hans: Perplexity
description:
en_US: Perplexity.AI
zh_Hans: Perplexity.AI
icon: icon.svg
tags:
- search
credentials_for_provider:
perplexity_api_key:
type: secret-input
required: true
label:
en_US: Perplexity API key
zh_Hans: Perplexity API key
placeholder:
en_US: Please input your Perplexity API key
zh_Hans: 请输入你的 Perplexity API key
help:
en_US: Get your Perplexity API key from Perplexity
zh_Hans: 从 Perplexity 获取您的 Perplexity API key
url: https://www.perplexity.ai/settings/api

View File

@ -0,0 +1,72 @@
import json
from typing import Any, Union
import requests
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions"
class PerplexityAITool(BuiltinTool):
def _parse_response(self, response: dict) -> dict:
"""Parse the response from Perplexity AI API"""
if 'choices' in response and len(response['choices']) > 0:
message = response['choices'][0]['message']
return {
'content': message.get('content', ''),
'role': message.get('role', ''),
'citations': response.get('citations', [])
}
else:
return {'content': 'Unable to get a valid response', 'role': 'assistant', 'citations': []}
def _invoke(self,
user_id: str,
tool_parameters: dict[str, Any],
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
headers = {
"Authorization": f"Bearer {self.runtime.credentials['perplexity_api_key']}",
"Content-Type": "application/json"
}
payload = {
"model": tool_parameters.get('model', 'llama-3.1-sonar-small-128k-online'),
"messages": [
{
"role": "system",
"content": "Be precise and concise."
},
{
"role": "user",
"content": tool_parameters['query']
}
],
"max_tokens": tool_parameters.get('max_tokens', 4096),
"temperature": tool_parameters.get('temperature', 0.7),
"top_p": tool_parameters.get('top_p', 1),
"top_k": tool_parameters.get('top_k', 5),
"presence_penalty": tool_parameters.get('presence_penalty', 0),
"frequency_penalty": tool_parameters.get('frequency_penalty', 1),
"stream": False
}
if 'search_recency_filter' in tool_parameters:
payload['search_recency_filter'] = tool_parameters['search_recency_filter']
if 'return_citations' in tool_parameters:
payload['return_citations'] = tool_parameters['return_citations']
if 'search_domain_filter' in tool_parameters:
if isinstance(tool_parameters['search_domain_filter'], str):
payload['search_domain_filter'] = [tool_parameters['search_domain_filter']]
elif isinstance(tool_parameters['search_domain_filter'], list):
payload['search_domain_filter'] = tool_parameters['search_domain_filter']
response = requests.post(url=PERPLEXITY_API_URL, json=payload, headers=headers)
response.raise_for_status()
valuable_res = self._parse_response(response.json())
return [
self.create_json_message(valuable_res),
self.create_text_message(json.dumps(valuable_res, ensure_ascii=False, indent=2))
]

View File

@ -0,0 +1,178 @@
identity:
name: perplexity
author: Dify
label:
en_US: Perplexity Search
description:
human:
en_US: Search information using Perplexity AI's language models.
llm: This tool is used to search information using Perplexity AI's language models.
parameters:
- name: query
type: string
required: true
label:
en_US: Query
zh_Hans: 查询
human_description:
en_US: The text query to be processed by the AI model.
zh_Hans: 要由 AI 模型处理的文本查询。
form: llm
- name: model
type: select
required: false
label:
en_US: Model Name
zh_Hans: 模型名称
human_description:
en_US: The Perplexity AI model to use for generating the response.
zh_Hans: 用于生成响应的 Perplexity AI 模型。
form: form
default: "llama-3.1-sonar-small-128k-online"
options:
- value: llama-3.1-sonar-small-128k-online
label:
en_US: llama-3.1-sonar-small-128k-online
zh_Hans: llama-3.1-sonar-small-128k-online
- value: llama-3.1-sonar-large-128k-online
label:
en_US: llama-3.1-sonar-large-128k-online
zh_Hans: llama-3.1-sonar-large-128k-online
- value: llama-3.1-sonar-huge-128k-online
label:
en_US: llama-3.1-sonar-huge-128k-online
zh_Hans: llama-3.1-sonar-huge-128k-online
- name: max_tokens
type: number
required: false
label:
en_US: Max Tokens
zh_Hans: 最大令牌数
pt_BR: Máximo de Tokens
human_description:
en_US: The maximum number of tokens to generate in the response.
zh_Hans: 在响应中生成的最大令牌数。
pt_BR: O número máximo de tokens a serem gerados na resposta.
form: form
default: 4096
min: 1
max: 4096
- name: temperature
type: number
required: false
label:
en_US: Temperature
zh_Hans: 温度
pt_BR: Temperatura
human_description:
en_US: Controls randomness in the output. Lower values make the output more focused and deterministic.
zh_Hans: 控制输出的随机性。较低的值使输出更加集中和确定。
form: form
default: 0.7
min: 0
max: 1
- name: top_k
type: number
required: false
label:
en_US: Top K
zh_Hans: 取样数量
human_description:
en_US: The number of top results to consider for response generation.
zh_Hans: 用于生成响应的顶部结果数量。
form: form
default: 5
min: 1
max: 100
- name: top_p
type: number
required: false
label:
en_US: Top P
zh_Hans: Top P
human_description:
en_US: Controls diversity via nucleus sampling.
zh_Hans: 通过核心采样控制多样性。
form: form
default: 1
min: 0.1
max: 1
step: 0.1
- name: presence_penalty
type: number
required: false
label:
en_US: Presence Penalty
zh_Hans: 存在惩罚
human_description:
en_US: Positive values penalize new tokens based on whether they appear in the text so far.
zh_Hans: 正值会根据新词元是否已经出现在文本中来对其进行惩罚。
form: form
default: 0
min: -1.0
max: 1.0
step: 0.1
- name: frequency_penalty
type: number
required: false
label:
en_US: Frequency Penalty
zh_Hans: 频率惩罚
human_description:
en_US: Positive values penalize new tokens based on their existing frequency in the text so far.
zh_Hans: 正值会根据新词元在文本中已经出现的频率来对其进行惩罚。
form: form
default: 1
min: 0.1
max: 1.0
step: 0.1
- name: return_citations
type: boolean
required: false
label:
en_US: Return Citations
zh_Hans: 返回引用
human_description:
en_US: Whether to return citations in the response.
zh_Hans: 是否在响应中返回引用。
form: form
default: true
- name: search_domain_filter
type: string
required: false
label:
en_US: Search Domain Filter
zh_Hans: 搜索域过滤器
human_description:
en_US: Domain to filter the search results.
zh_Hans: 用于过滤搜索结果的域名。
form: form
default: ""
- name: search_recency_filter
type: select
required: false
label:
en_US: Search Recency Filter
zh_Hans: 搜索时间过滤器
human_description:
en_US: Filter for search results based on recency.
zh_Hans: 基于时间筛选搜索结果。
form: form
default: "month"
options:
- value: day
label:
en_US: Day
zh_Hans:
- value: week
label:
en_US: Week
zh_Hans:
- value: month
label:
en_US: Month
zh_Hans:
- value: year
label:
en_US: Year
zh_Hans:

View File

@ -1,7 +1,7 @@
import json
import logging
from copy import deepcopy
from typing import Any, Union
from typing import Any, Optional, Union
from core.file.file_obj import FileTransferMethod, FileVar
from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParameter, ToolProviderType
@ -18,6 +18,7 @@ class WorkflowTool(Tool):
version: str
workflow_entities: dict[str, Any]
workflow_call_depth: int
thread_pool_id: Optional[str] = None
label: str
@ -57,6 +58,7 @@ class WorkflowTool(Tool):
invoke_from=self.runtime.invoke_from,
stream=False,
call_depth=self.workflow_call_depth + 1,
workflow_thread_pool_id=self.thread_pool_id
)
data = result.get('data', {})

View File

@ -128,6 +128,7 @@ class ToolEngine:
user_id: str,
workflow_tool_callback: DifyWorkflowCallbackHandler,
workflow_call_depth: int,
thread_pool_id: Optional[str] = None
) -> list[ToolInvokeMessage]:
"""
Workflow invokes the tool with the given arguments.
@ -141,6 +142,7 @@ class ToolEngine:
if isinstance(tool, WorkflowTool):
tool.workflow_call_depth = workflow_call_depth + 1
tool.thread_pool_id = thread_pool_id
if tool.runtime and tool.runtime.runtime_parameters:
tool_parameters = {**tool.runtime.runtime_parameters, **tool_parameters}

View File

@ -1,12 +1,12 @@
import logging
import queue
import threading
import time
import uuid
from collections.abc import Generator, Mapping
from concurrent.futures import ThreadPoolExecutor, wait
from typing import Any, Optional
from flask import Flask, current_app
from uritemplate.variable import VariableValue
from core.app.apps.base_app_queue_manager import GenerateTaskStoppedException
from core.app.entities.app_invoke_entities import InvokeFrom
@ -15,7 +15,7 @@ from core.workflow.entities.node_entities import (
NodeType,
UserFrom,
)
from core.workflow.entities.variable_pool import VariablePool
from core.workflow.entities.variable_pool import VariablePool, VariableValue
from core.workflow.graph_engine.condition_handlers.condition_manager import ConditionManager
from core.workflow.graph_engine.entities.event import (
BaseIterationEvent,
@ -47,7 +47,28 @@ from models.workflow import WorkflowNodeExecutionStatus, WorkflowType
logger = logging.getLogger(__name__)
class GraphEngineThreadPool(ThreadPoolExecutor):
def __init__(self, max_workers=None, thread_name_prefix='',
initializer=None, initargs=(), max_submit_count=100) -> None:
super().__init__(max_workers, thread_name_prefix, initializer, initargs)
self.max_submit_count = max_submit_count
self.submit_count = 0
def submit(self, fn, *args, **kwargs):
self.submit_count += 1
self.check_is_full()
return super().submit(fn, *args, **kwargs)
def check_is_full(self) -> None:
print(f"submit_count: {self.submit_count}, max_submit_count: {self.max_submit_count}")
if self.submit_count > self.max_submit_count:
raise ValueError(f"Max submit count {self.max_submit_count} of workflow thread pool reached.")
class GraphEngine:
workflow_thread_pool_mapping: dict[str, GraphEngineThreadPool] = {}
def __init__(
self,
tenant_id: str,
@ -62,8 +83,26 @@ class GraphEngine:
graph_config: Mapping[str, Any],
variable_pool: VariablePool,
max_execution_steps: int,
max_execution_time: int
max_execution_time: int,
thread_pool_id: Optional[str] = None
) -> None:
thread_pool_max_submit_count = 100
thread_pool_max_workers = 10
## init thread pool
if thread_pool_id:
if not thread_pool_id in GraphEngine.workflow_thread_pool_mapping:
raise ValueError(f"Max submit count {thread_pool_max_submit_count} of workflow thread pool reached.")
self.thread_pool_id = thread_pool_id
self.thread_pool = GraphEngine.workflow_thread_pool_mapping[thread_pool_id]
self.is_main_thread_pool = False
else:
self.thread_pool = GraphEngineThreadPool(max_workers=thread_pool_max_workers, max_submit_count=thread_pool_max_submit_count)
self.thread_pool_id = str(uuid.uuid4())
self.is_main_thread_pool = True
GraphEngine.workflow_thread_pool_mapping[self.thread_pool_id] = self.thread_pool
self.graph = graph
self.init_params = GraphInitParams(
tenant_id=tenant_id,
@ -142,6 +181,9 @@ class GraphEngine:
logger.exception("Unknown Error when graph running")
yield GraphRunFailedEvent(error=str(e))
raise e
finally:
if self.is_main_thread_pool and self.thread_pool_id in GraphEngine.workflow_thread_pool_mapping:
del GraphEngine.workflow_thread_pool_mapping[self.thread_pool_id]
def _run(
self,
@ -194,7 +236,8 @@ class GraphEngine:
graph_init_params=self.init_params,
graph=self.graph,
graph_runtime_state=self.graph_runtime_state,
previous_node_id=previous_node_id
previous_node_id=previous_node_id,
thread_pool_id=self.thread_pool_id
)
try:
@ -355,10 +398,10 @@ class GraphEngine:
node_id = edge_mappings[0].target_node_id
node_config = self.graph.node_id_config_mapping.get(node_id)
if not node_config:
raise GraphRunFailedError(f'Node {node_id} related parallel not found.')
raise GraphRunFailedError(f'Node {node_id} related parallel not found or incorrectly connected to multiple parallel branches.')
node_title = node_config.get('data', {}).get('title')
raise GraphRunFailedError(f'Node {node_title} related parallel not found.')
raise GraphRunFailedError(f'Node {node_title} related parallel not found or incorrectly connected to multiple parallel branches.')
parallel = self.graph.parallel_mapping.get(parallel_id)
if not parallel:
@ -368,7 +411,7 @@ class GraphEngine:
q: queue.Queue = queue.Queue()
# Create a list to store the threads
threads = []
futures = []
# new thread
for edge in edge_mappings:
@ -378,17 +421,16 @@ class GraphEngine:
):
continue
thread = threading.Thread(target=self._run_parallel_node, kwargs={
'flask_app': current_app._get_current_object(), # type: ignore[attr-defined]
'q': q,
'parallel_id': parallel_id,
'parallel_start_node_id': edge.target_node_id,
'parent_parallel_id': in_parallel_id,
'parent_parallel_start_node_id': parallel_start_node_id,
})
threads.append(thread)
thread.start()
futures.append(
self.thread_pool.submit(self._run_parallel_node, **{
'flask_app': current_app._get_current_object(), # type: ignore[attr-defined]
'q': q,
'parallel_id': parallel_id,
'parallel_start_node_id': edge.target_node_id,
'parent_parallel_id': in_parallel_id,
'parent_parallel_start_node_id': parallel_start_node_id,
})
)
succeeded_count = 0
while True:
@ -401,7 +443,7 @@ class GraphEngine:
if event.parallel_id == parallel_id:
if isinstance(event, ParallelBranchRunSucceededEvent):
succeeded_count += 1
if succeeded_count == len(threads):
if succeeded_count == len(futures):
q.put(None)
continue
@ -410,9 +452,8 @@ class GraphEngine:
except queue.Empty:
continue
# Join all threads
for thread in threads:
thread.join()
# wait all threads
wait(futures)
# get final node id
final_node_id = parallel.end_to_node_id

View File

@ -21,7 +21,8 @@ class BaseNode(ABC):
graph_init_params: GraphInitParams,
graph: Graph,
graph_runtime_state: GraphRuntimeState,
previous_node_id: Optional[str] = None) -> None:
previous_node_id: Optional[str] = None,
thread_pool_id: Optional[str] = None) -> None:
self.id = id
self.tenant_id = graph_init_params.tenant_id
self.app_id = graph_init_params.app_id
@ -35,6 +36,7 @@ class BaseNode(ABC):
self.graph = graph
self.graph_runtime_state = graph_runtime_state
self.previous_node_id = previous_node_id
self.thread_pool_id = thread_pool_id
node_id = config.get("id")
if not node_id:

View File

@ -18,9 +18,9 @@ from core.workflow.nodes.http_request.http_executor import HttpExecutor, HttpExe
from models.workflow import WorkflowNodeExecutionStatus
HTTP_REQUEST_DEFAULT_TIMEOUT = HttpRequestNodeTimeout(
connect=min(10, dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT),
read=min(60, dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT),
write=min(20, dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT),
connect=dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT,
read=dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT,
write=dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT,
)
@ -100,12 +100,9 @@ class HttpRequestNode(BaseNode):
if timeout is None:
return HTTP_REQUEST_DEFAULT_TIMEOUT
timeout.connect = min(timeout.connect or HTTP_REQUEST_DEFAULT_TIMEOUT.connect,
dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT)
timeout.read = min(timeout.read or HTTP_REQUEST_DEFAULT_TIMEOUT.read,
dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT)
timeout.write = min(timeout.write or HTTP_REQUEST_DEFAULT_TIMEOUT.write,
dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT)
timeout.connect = timeout.connect or HTTP_REQUEST_DEFAULT_TIMEOUT.connect
timeout.read = timeout.read or HTTP_REQUEST_DEFAULT_TIMEOUT.read
timeout.write = timeout.write or HTTP_REQUEST_DEFAULT_TIMEOUT.write
return timeout
@classmethod

View File

@ -66,6 +66,7 @@ class ToolNode(BaseNode):
user_id=self.user_id,
workflow_tool_callback=DifyWorkflowCallbackHandler(),
workflow_call_depth=self.workflow_call_depth,
thread_pool_id=self.thread_pool_id,
)
except Exception as e:
return NodeRunResult(

View File

@ -44,7 +44,8 @@ class WorkflowEntry:
user_from: UserFrom,
invoke_from: InvokeFrom,
call_depth: int,
variable_pool: VariablePool
variable_pool: VariablePool,
thread_pool_id: Optional[str] = None
) -> None:
"""
Init workflow entry
@ -59,7 +60,9 @@ class WorkflowEntry:
:param invoke_from: invoke from
:param call_depth: call depth
:param variable_pool: variable pool
:param thread_pool_id: thread pool id
"""
# check call depth
workflow_call_max_depth = dify_config.WORKFLOW_CALL_MAX_DEPTH
if call_depth > workflow_call_max_depth:
raise ValueError('Max workflow call depth {} reached.'.format(workflow_call_max_depth))
@ -78,7 +81,8 @@ class WorkflowEntry:
graph_config=graph_config,
variable_pool=variable_pool,
max_execution_steps=dify_config.WORKFLOW_MAX_EXECUTION_STEPS,
max_execution_time=dify_config.WORKFLOW_MAX_EXECUTION_TIME
max_execution_time=dify_config.WORKFLOW_MAX_EXECUTION_TIME,
thread_pool_id=thread_pool_id
)
def run(

View File

@ -1,3 +1,4 @@
import openai
import sentry_sdk
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.flask import FlaskIntegration
@ -9,7 +10,7 @@ def init_app(app):
sentry_sdk.init(
dsn=app.config.get("SENTRY_DSN"),
integrations=[FlaskIntegration(), CeleryIntegration()],
ignore_errors=[HTTPException, ValueError],
ignore_errors=[HTTPException, ValueError, openai.APIStatusError],
traces_sample_rate=app.config.get("SENTRY_TRACES_SAMPLE_RATE", 1.0),
profiles_sample_rate=app.config.get("SENTRY_PROFILES_SAMPLE_RATE", 1.0),
environment=app.config.get("DEPLOY_ENV"),

View File

@ -15,6 +15,7 @@ class AliyunStorage(BaseStorage):
app_config = self.app.config
self.bucket_name = app_config.get("ALIYUN_OSS_BUCKET_NAME")
self.folder = app.config.get("ALIYUN_OSS_PATH")
oss_auth_method = aliyun_s3.Auth
region = None
if app_config.get("ALIYUN_OSS_AUTH_VERSION") == "v4":
@ -30,15 +31,29 @@ class AliyunStorage(BaseStorage):
)
def save(self, filename, data):
if not self.folder or self.folder.endswith("/"):
filename = self.folder + filename
else:
filename = self.folder + "/" + filename
self.client.put_object(filename, data)
def load_once(self, filename: str) -> bytes:
if not self.folder or self.folder.endswith("/"):
filename = self.folder + filename
else:
filename = self.folder + "/" + filename
with closing(self.client.get_object(filename)) as obj:
data = obj.read()
return data
def load_stream(self, filename: str) -> Generator:
def generate(filename: str = filename) -> Generator:
if not self.folder or self.folder.endswith("/"):
filename = self.folder + filename
else:
filename = self.folder + "/" + filename
with closing(self.client.get_object(filename)) as obj:
while chunk := obj.read(4096):
yield chunk
@ -46,10 +61,24 @@ class AliyunStorage(BaseStorage):
return generate()
def download(self, filename, target_filepath):
if not self.folder or self.folder.endswith("/"):
filename = self.folder + filename
else:
filename = self.folder + "/" + filename
self.client.get_object_to_file(filename, target_filepath)
def exists(self, filename):
if not self.folder or self.folder.endswith("/"):
filename = self.folder + filename
else:
filename = self.folder + "/" + filename
return self.client.object_exists(filename)
def delete(self, filename):
if not self.folder or self.folder.endswith("/"):
filename = self.folder + filename
else:
filename = self.folder + "/" + filename
self.client.delete_object(filename)

View File

@ -58,6 +58,7 @@ app_detail_fields = {
"model_config": fields.Nested(model_config_fields, attribute="app_model_config", allow_null=True),
"workflow": fields.Nested(workflow_partial_fields, allow_null=True),
"tracing": fields.Raw,
"use_icon_as_answer_icon": fields.Boolean,
"created_by": fields.String,
"created_at": TimestampField,
"updated_by": fields.String,
@ -91,6 +92,7 @@ app_partial_fields = {
"icon_url": AppIconUrlField,
"model_config": fields.Nested(model_config_partial_fields, attribute="app_model_config", allow_null=True),
"workflow": fields.Nested(workflow_partial_fields, allow_null=True),
"use_icon_as_answer_icon": fields.Boolean,
"created_by": fields.String,
"created_at": TimestampField,
"updated_by": fields.String,
@ -140,6 +142,7 @@ site_fields = {
"prompt_public": fields.Boolean,
"app_base_url": fields.String,
"show_workflow_steps": fields.Boolean,
"use_icon_as_answer_icon": fields.Boolean,
"created_by": fields.String,
"created_at": TimestampField,
"updated_by": fields.String,
@ -161,6 +164,7 @@ app_detail_fields_with_site = {
"workflow": fields.Nested(workflow_partial_fields, allow_null=True),
"site": fields.Nested(site_fields),
"api_base_url": fields.String,
"use_icon_as_answer_icon": fields.Boolean,
"created_by": fields.String,
"created_at": TimestampField,
"updated_by": fields.String,
@ -184,4 +188,5 @@ app_site_fields = {
"customize_token_strategy": fields.String,
"prompt_public": fields.Boolean,
"show_workflow_steps": fields.Boolean,
"use_icon_as_answer_icon": fields.Boolean,
}

View File

@ -10,6 +10,7 @@ app_fields = {
"icon": fields.String,
"icon_background": fields.String,
"icon_url": AppIconUrlField,
"use_icon_as_answer_icon": fields.Boolean,
}
installed_app_fields = {

View File

@ -0,0 +1,45 @@
"""add use_icon_as_answer_icon fields for app and site
Revision ID: 030f4915f36a
Revises: d0187d6a88dd
Create Date: 2024-09-01 12:55:45.129687
"""
import sqlalchemy as sa
from alembic import op
import models as models
# revision identifiers, used by Alembic.
revision = "030f4915f36a"
down_revision = "d0187d6a88dd"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("apps", schema=None) as batch_op:
batch_op.add_column(
sa.Column("use_icon_as_answer_icon", sa.Boolean(), server_default=sa.text("false"), nullable=False)
)
with op.batch_alter_table("sites", schema=None) as batch_op:
batch_op.add_column(
sa.Column("use_icon_as_answer_icon", sa.Boolean(), server_default=sa.text("false"), nullable=False)
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("sites", schema=None) as batch_op:
batch_op.drop_column("use_icon_as_answer_icon")
with op.batch_alter_table("apps", schema=None) as batch_op:
batch_op.drop_column("use_icon_as_answer_icon")
# ### end Alembic commands ###

View File

@ -1,7 +1,7 @@
"""add node_execution_id into node_executions
Revision ID: 675b5321501b
Revises: d0187d6a88dd
Revises: 030f4915f36a
Create Date: 2024-08-12 10:54:02.259331
"""
@ -12,7 +12,7 @@ import models as models
# revision identifiers, used by Alembic.
revision = '675b5321501b'
down_revision = 'd0187d6a88dd'
down_revision = '030f4915f36a'
branch_labels = None
depends_on = None

View File

@ -86,6 +86,7 @@ class App(db.Model):
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
updated_by = db.Column(StringUUID, nullable=True)
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
use_icon_as_answer_icon = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
@property
def desc_or_prompt(self):
@ -1114,6 +1115,7 @@ class Site(db.Model):
copyright = db.Column(db.String(255))
privacy_policy = db.Column(db.String(255))
show_workflow_steps = db.Column(db.Boolean, nullable=False, server_default=db.text('true'))
use_icon_as_answer_icon = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
custom_disclaimer = db.Column(db.String(255), nullable=True)
customize_domain = db.Column(db.String(255))
customize_token_strategy = db.Column(db.String(255), nullable=False)

View File

@ -265,7 +265,7 @@ class TenantService:
return tenant
@staticmethod
def create_owner_tenant_if_not_exist(account: Account):
def create_owner_tenant_if_not_exist(account: Account, name: Optional[str] = None):
"""Create owner tenant if not exist"""
available_ta = (
TenantAccountJoin.query.filter_by(account_id=account.id).order_by(TenantAccountJoin.id.asc()).first()
@ -274,7 +274,10 @@ class TenantService:
if available_ta:
return
tenant = TenantService.create_tenant(f"{account.name}'s Workspace")
if name:
tenant = TenantService.create_tenant(name)
else:
tenant = TenantService.create_tenant(f"{account.name}'s Workspace")
TenantService.create_tenant_member(tenant, account, role="owner")
account.current_tenant = tenant
db.session.commit()

View File

@ -87,6 +87,7 @@ class AppDslService:
icon_background = (
args.get("icon_background") if args.get("icon_background") else app_data.get("icon_background")
)
use_icon_as_answer_icon = app_data.get("use_icon_as_answer_icon", False)
# import dsl and create app
app_mode = AppMode.value_of(app_data.get("mode"))
@ -101,6 +102,7 @@ class AppDslService:
icon_type=icon_type,
icon=icon,
icon_background=icon_background,
use_icon_as_answer_icon=use_icon_as_answer_icon,
)
elif app_mode in [AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.COMPLETION]:
app = cls._import_and_create_new_model_config_based_app(
@ -113,6 +115,7 @@ class AppDslService:
icon_type=icon_type,
icon=icon,
icon_background=icon_background,
use_icon_as_answer_icon=use_icon_as_answer_icon,
)
else:
raise ValueError("Invalid app mode")
@ -171,6 +174,7 @@ class AppDslService:
"icon": "🤖" if app_model.icon_type == "image" else app_model.icon,
"icon_background": "#FFEAD5" if app_model.icon_type == "image" else app_model.icon_background,
"description": app_model.description,
"use_icon_as_answer_icon": app_model.use_icon_as_answer_icon,
},
}
@ -218,6 +222,7 @@ class AppDslService:
icon_type: str,
icon: str,
icon_background: str,
use_icon_as_answer_icon: bool,
) -> App:
"""
Import app dsl and create new workflow based app
@ -231,6 +236,7 @@ class AppDslService:
:param icon_type: app icon type, "emoji" or "image"
:param icon: app icon
:param icon_background: app icon background
:param use_icon_as_answer_icon: use app icon as answer icon
"""
if not workflow_data:
raise ValueError("Missing workflow in data argument " "when app mode is advanced-chat or workflow")
@ -244,6 +250,7 @@ class AppDslService:
icon_type=icon_type,
icon=icon,
icon_background=icon_background,
use_icon_as_answer_icon=use_icon_as_answer_icon,
)
# init draft workflow
@ -316,6 +323,7 @@ class AppDslService:
icon_type: str,
icon: str,
icon_background: str,
use_icon_as_answer_icon: bool,
) -> App:
"""
Import app dsl and create new model config based app
@ -341,6 +349,7 @@ class AppDslService:
icon_type=icon_type,
icon=icon,
icon_background=icon_background,
use_icon_as_answer_icon=use_icon_as_answer_icon,
)
app_model_config = AppModelConfig()
@ -369,6 +378,7 @@ class AppDslService:
icon_type: str,
icon: str,
icon_background: str,
use_icon_as_answer_icon: bool,
) -> App:
"""
Create new app
@ -381,6 +391,7 @@ class AppDslService:
:param icon_type: app icon type, "emoji" or "image"
:param icon: app icon
:param icon_background: app icon background
:param use_icon_as_answer_icon: use app icon as answer icon
"""
app = App(
tenant_id=tenant_id,
@ -392,6 +403,7 @@ class AppDslService:
icon_background=icon_background,
enable_site=True,
enable_api=True,
use_icon_as_answer_icon=use_icon_as_answer_icon,
created_by=account.id,
updated_by=account.id,
)

View File

@ -221,6 +221,7 @@ class AppService:
app.icon_type = args.get("icon_type", "emoji")
app.icon = args.get("icon")
app.icon_background = args.get("icon_background")
app.use_icon_as_answer_icon = args.get("use_icon_as_answer_icon", False)
app.updated_by = current_user.id
app.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
db.session.commit()

View File

@ -137,7 +137,7 @@ class DatasetService:
@staticmethod
def create_empty_dataset(
tenant_id: str, name: str, indexing_technique: Optional[str], account: Account, permission: Optional[str]
tenant_id: str, name: str, indexing_technique: Optional[str], account: Account, permission: Optional[str] = None
):
# check if dataset name already exists
if Dataset.query.filter_by(name=name, tenant_id=tenant_id).first():

View File

@ -19,7 +19,7 @@ def send_invite_member_mail_task(language: str, to: str, token: str, inviter_nam
:param inviter_name
:param workspace_name
Usage: send_invite_member_mail_task.delay(langauge, to, token, inviter_name, workspace_name)
Usage: send_invite_member_mail_task.delay(language, to, token, inviter_name, workspace_name)
"""
if not mail.is_inited():
return

View File

@ -19,6 +19,7 @@ def example_env_file(tmp_path, monkeypatch) -> str:
"""
CONSOLE_API_URL=https://example.com
CONSOLE_WEB_URL=https://example.com
HTTP_REQUEST_MAX_WRITE_TIMEOUT=30
"""
)
)
@ -48,6 +49,12 @@ def test_dify_config(example_env_file):
assert config.API_COMPRESSION_ENABLED is False
assert config.SENTRY_TRACES_SAMPLE_RATE == 1.0
# annotated field with default value
assert config.HTTP_REQUEST_MAX_READ_TIMEOUT == 60
# annotated field with configured value
assert config.HTTP_REQUEST_MAX_WRITE_TIMEOUT == 30
# NOTE: If there is a `.env` file in your Workspace, this test might not succeed as expected.
# This is due to `pymilvus` loading all the variables from the `.env` file into `os.environ`.

View File

@ -285,6 +285,8 @@ ALIYUN_OSS_SECRET_KEY=your-secret-key
ALIYUN_OSS_ENDPOINT=https://oss-ap-southeast-1-internal.aliyuncs.com
ALIYUN_OSS_REGION=ap-southeast-1
ALIYUN_OSS_AUTH_VERSION=v4
# Don't start with '/'. OSS doesn't support leading slash in object names.
ALIYUN_OSS_PATH=your-path
# Tencent COS Configuration
# The name of the Tencent COS bucket to use for storing files.

View File

@ -66,6 +66,7 @@ x-shared-env: &shared-api-worker-env
ALIYUN_OSS_ENDPOINT: ${ALIYUN_OSS_ENDPOINT:-}
ALIYUN_OSS_REGION: ${ALIYUN_OSS_REGION:-}
ALIYUN_OSS_AUTH_VERSION: ${ALIYUN_OSS_AUTH_VERSION:-v4}
ALIYUN_OSS_PATHS: ${ALIYUN_OSS_PATH:-}
TENCENT_COS_BUCKET_NAME: ${TENCENT_COS_BUCKET_NAME:-}
TENCENT_COS_SECRET_KEY: ${TENCENT_COS_SECRET_KEY:-}
TENCENT_COS_SECRET_ID: ${TENCENT_COS_SECRET_ID:-}
@ -294,7 +295,7 @@ services:
# ssrf_proxy server
# for more information, please refer to
# https://docs.dify.ai/learn-more/faq/self-host-faq#id-18.-why-is-ssrf_proxy-needed
# https://docs.dify.ai/learn-more/faq/install-faq#id-18.-why-is-ssrf_proxy-needed
ssrf_proxy:
image: ubuntu/squid:latest
restart: always

View File

@ -128,7 +128,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
if (e.status === 404)
router.replace('/apps')
})
}, [appId, isCurrentWorkspaceEditor])
}, [appId, isCurrentWorkspaceEditor, systemFeatures])
useUnmount(() => {
setAppDetail()

View File

@ -95,7 +95,7 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
if (systemFeatures.enable_web_sso_switch_component) {
const [sso_err] = await asyncRunSafe<AppSSO>(
updateAppSSO({ id: appId, enabled: params.enable_sso }) as Promise<AppSSO>,
updateAppSSO({ id: appId, enabled: Boolean(params.enable_sso) }) as Promise<AppSSO>,
)
if (sso_err) {
handleCallbackResult(sso_err)

View File

@ -79,6 +79,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
icon,
icon_background,
description,
use_icon_as_answer_icon,
}) => {
try {
await updateAppInfo({
@ -88,6 +89,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
icon,
icon_background,
description,
use_icon_as_answer_icon,
})
setShowEditModal(false)
notify({
@ -255,7 +257,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
e.preventDefault()
getRedirection(isCurrentWorkspaceEditor, app, push)
}}
className='group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
className='relative group col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
>
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
<div className='relative shrink-0'>
@ -297,17 +299,16 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
</div>
</div>
</div>
<div
className={cn(
'grow mb-2 px-[14px] max-h-[72px] text-xs leading-normal text-gray-500 group-hover:line-clamp-2 group-hover:max-h-[36px]',
tags.length ? 'line-clamp-2' : 'line-clamp-4',
)}
title={app.description}
>
{app.description}
<div className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-gray-500'>
<div
className={cn(tags.length ? 'line-clamp-2' : 'line-clamp-4', 'group-hover:line-clamp-2')}
title={app.description}
>
{app.description}
</div>
</div>
<div className={cn(
'items-center shrink-0 mt-1 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]',
'absolute bottom-1 left-0 right-0 items-center shrink-0 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]',
tags.length ? 'flex' : '!hidden group-hover:!flex',
)}>
{isCurrentWorkspaceEditor && (
@ -371,6 +372,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
appIconBackground={app.icon_background}
appIconUrl={app.icon_url}
appDescription={app.description}
appMode={app.mode}
appUseIconAsAnswerIcon={app.use_icon_as_answer_icon}
show={showEditModal}
onConfirm={onEdit}
onHide={() => setShowEditModal(false)}

View File

@ -139,7 +139,7 @@ const Apps = () => {
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
{isCurrentWorkspaceEditor
&& <NewAppCard onSuccess={mutate} />}
{data?.map(({ data: apps }: any) => apps.map((app: any) => (
{data?.map(({ data: apps }) => apps.map(app => (
<AppCard key={app.id} app={app} onRefresh={mutate} />
)))}
<CheckModal />

View File

@ -63,6 +63,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
icon,
icon_background,
description,
use_icon_as_answer_icon,
}) => {
if (!appDetail)
return
@ -74,6 +75,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
icon,
icon_background,
description,
use_icon_as_answer_icon,
})
setShowEditModal(false)
notify({
@ -423,6 +425,8 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
appIconBackground={appDetail.icon_background}
appIconUrl={appDetail.icon_url}
appDescription={appDetail.description}
appMode={appDetail.mode}
appUseIconAsAnswerIcon={appDetail.use_icon_as_answer_icon}
show={showEditModal}
onConfirm={onEdit}
onHide={() => setShowEditModal(false)}

View File

@ -134,8 +134,8 @@ function AppCard({
return (
<div
className={`shadow-xs border-[0.5px] rounded-lg border-gray-200 ${className ?? ''
}`}
className={
`shadow-xs border-[0.5px] rounded-lg border-gray-200 ${className ?? ''}`}
>
<div className={`px-6 py-5 ${customBgColor ?? bgColor} rounded-lg`}>
<div className="mb-2.5 flex flex-row items-start justify-between">
@ -176,7 +176,6 @@ function AppCard({
{isApp && <ShareQRCode content={isApp ? appUrl : apiUrl} selectorId={randomString(8)} className={'hover:bg-gray-200'} />}
<CopyFeedback
content={isApp ? appUrl : apiUrl}
selectorId={randomString(8)}
className={'hover:bg-gray-200'}
/>
{/* button copy link/ button regenerate */}
@ -202,8 +201,8 @@ function AppCard({
onClick={() => setShowConfirmDelete(true)}
>
<div
className={`w-full h-full ${style.refreshIcon} ${genLoading ? style.generateLogo : ''
}`}
className={
`w-full h-full ${style.refreshIcon} ${genLoading ? style.generateLogo : ''}`}
></div>
</div>
</Tooltip>

View File

@ -43,6 +43,7 @@ export type ConfigParams = {
icon: string
icon_background?: string
show_workflow_steps: boolean
use_icon_as_answer_icon: boolean
enable_sso?: boolean
}
@ -72,6 +73,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
custom_disclaimer,
default_language,
show_workflow_steps,
use_icon_as_answer_icon,
} = appInfo.site
const [inputInfo, setInputInfo] = useState({
title,
@ -82,6 +84,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
privacyPolicy: privacy_policy,
customDisclaimer: custom_disclaimer,
show_workflow_steps,
use_icon_as_answer_icon,
enable_sso: appInfo.enable_sso,
})
const [language, setLanguage] = useState(default_language)
@ -94,6 +97,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
? { type: 'image', url: icon_url!, fileId: icon }
: { type: 'emoji', icon, background: icon_background! },
)
const isChatBot = appInfo.mode === 'chat' || appInfo.mode === 'advanced-chat' || appInfo.mode === 'agent-chat'
useEffect(() => {
setInputInfo({
@ -105,6 +109,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
privacyPolicy: privacy_policy,
customDisclaimer: custom_disclaimer,
show_workflow_steps,
use_icon_as_answer_icon,
enable_sso: appInfo.enable_sso,
})
setLanguage(default_language)
@ -157,6 +162,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId,
icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
show_workflow_steps: inputInfo.show_workflow_steps,
use_icon_as_answer_icon: inputInfo.use_icon_as_answer_icon,
enable_sso: inputInfo.enable_sso,
}
await onSave?.(params)
@ -209,6 +215,18 @@ const SettingsModal: FC<ISettingsModalProps> = ({
onChange={onChange('desc')}
placeholder={t(`${prefixSettings}.webDescPlaceholder`) as string}
/>
{isChatBot && (
<div className='w-full mt-4'>
<div className='flex justify-between items-center'>
<div className={`font-medium ${s.settingTitle} text-gray-900 `}>{t('app.answerIcon.title')}</div>
<Switch
defaultValue={inputInfo.use_icon_as_answer_icon}
onChange={v => setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })}
/>
</div>
<p className='body-xs-regular text-gray-500'>{t('app.answerIcon.description')}</p>
</div>
)}
<div className={`mt-6 mb-2 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.language`)}</div>
<SimpleSelect
items={languages.filter(item => item.supported)}

View File

@ -0,0 +1,47 @@
'use client'
import type { FC } from 'react'
import { init } from 'emoji-mart'
import data from '@emoji-mart/data'
import classNames from '@/utils/classnames'
import type { AppIconType } from '@/types/app'
init({ data })
export type AnswerIconProps = {
iconType?: AppIconType | null
icon?: string | null
background?: string | null
imageUrl?: string | null
}
const AnswerIcon: FC<AnswerIconProps> = ({
iconType,
icon,
background,
imageUrl,
}) => {
const wrapperClassName = classNames(
'flex',
'items-center',
'justify-center',
'w-full',
'h-full',
'rounded-full',
'border-[0.5px]',
'border-black/5',
'text-xl',
)
const isValidImageIcon = iconType === 'image' && imageUrl
return <div
className={wrapperClassName}
style={{ background: background || '#D5F5F6' }}
>
{isValidImageIcon
? <img src={imageUrl} className="w-full h-full rounded-full" alt="answer icon" />
: (icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />
}
</div>
}
export default AnswerIcon

View File

@ -13,6 +13,7 @@ import {
getUrl,
stopChatMessageResponding,
} from '@/service/share'
import AnswerIcon from '@/app/components/base/answer-icon'
const ChatWrapper = () => {
const {
@ -128,6 +129,15 @@ const ChatWrapper = () => {
isMobile,
])
const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon)
? <AnswerIcon
iconType={appData.site.icon_type}
icon={appData.site.icon}
background={appData.site.icon_background}
imageUrl={appData.site.icon_url}
/>
: null
return (
<Chat
appData={appData}
@ -143,6 +153,7 @@ const ChatWrapper = () => {
allToolIcons={appMeta?.tool_icons || {}}
onFeedback={handleFeedback}
suggestedQuestions={suggestedQuestions}
answerIcon={answerIcon}
hideProcessDetail
themeBuilder={themeBuilder}
/>

View File

@ -65,6 +65,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
prompt_public: false,
copyright: '',
show_workflow_steps: true,
use_icon_as_answer_icon: app.use_icon_as_answer_icon,
},
plan: 'basic',
} as AppData

View File

@ -22,6 +22,7 @@ import Citation from '@/app/components/base/chat/chat/citation'
import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
import type { Emoji } from '@/app/components/tools/types'
import type { AppData } from '@/models/share'
import AnswerIcon from '@/app/components/base/answer-icon'
type AnswerProps = {
item: ChatItem
@ -89,11 +90,7 @@ const Answer: FC<AnswerProps> = ({
<div className='flex mb-2 last:mb-0'>
<div className='shrink-0 relative w-10 h-10'>
{
answerIcon || (
<div className='flex items-center justify-center w-full h-full rounded-full bg-[#d5f5f6] border-[0.5px] border-black/5 text-xl'>
🤖
</div>
)
answerIcon || <AnswerIcon />
}
{
responding && (

View File

@ -15,6 +15,7 @@ import {
stopChatMessageResponding,
} from '@/service/share'
import LogoAvatar from '@/app/components/base/logo/logo-embeded-chat-avatar'
import AnswerIcon from '@/app/components/base/answer-icon'
const ChatWrapper = () => {
const {
@ -114,6 +115,17 @@ const ChatWrapper = () => {
return null
}, [currentConversationId, inputsForms, isMobile])
const answerIcon = isDify()
? <LogoAvatar className='relative shrink-0' />
: (appData?.site && appData.site.use_icon_as_answer_icon)
? <AnswerIcon
iconType={appData.site.icon_type}
icon={appData.site.icon}
background={appData.site.icon_background}
imageUrl={appData.site.icon_url}
/>
: null
return (
<Chat
appData={appData}
@ -129,7 +141,7 @@ const ChatWrapper = () => {
allToolIcons={appMeta?.tool_icons || {}}
onFeedback={handleFeedback}
suggestedQuestions={suggestedQuestions}
answerIcon={isDify() ? <LogoAvatar className='relative shrink-0' /> : null}
answerIcon={answerIcon}
hideProcessDetail
themeBuilder={themeBuilder}
/>

View File

@ -1,43 +1,83 @@
'use client'
import type { SVGProps } from 'react'
import React, { useState } from 'react'
import type { CSSProperties } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import { RiCloseCircleFill, RiErrorWarningLine, RiSearchLine } from '@remixicon/react'
import { type VariantProps, cva } from 'class-variance-authority'
import cn from '@/utils/classnames'
type InputProps = {
placeholder?: string
value?: string
defaultValue?: string
onChange?: (v: string) => void
className?: string
wrapperClassName?: string
type?: string
showPrefix?: React.ReactNode
prefixIcon?: React.ReactNode
}
const GlassIcon = ({ className }: SVGProps<SVGElement>) => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
<path d="M12.25 12.25L10.2084 10.2083M11.6667 6.70833C11.6667 9.44675 9.44675 11.6667 6.70833 11.6667C3.96992 11.6667 1.75 9.44675 1.75 6.70833C1.75 3.96992 3.96992 1.75 6.70833 1.75C9.44675 1.75 11.6667 3.96992 11.6667 6.70833Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
</svg>
export const inputVariants = cva(
'',
{
variants: {
size: {
regular: 'px-3 radius-md system-sm-regular',
large: 'px-4 radius-lg system-md-regular',
},
},
defaultVariants: {
size: 'regular',
},
},
)
const Input = ({ value, defaultValue, onChange, className = '', wrapperClassName = '', placeholder, type, showPrefix, prefixIcon }: InputProps) => {
const [localValue, setLocalValue] = useState(value ?? defaultValue)
export type InputProps = {
showLeftIcon?: boolean
showClearIcon?: boolean
onClear?: () => void
disabled?: boolean
destructive?: boolean
wrapperClassName?: string
styleCss?: CSSProperties
} & React.InputHTMLAttributes<HTMLInputElement> & VariantProps<typeof inputVariants>
const Input = ({
size,
disabled,
destructive,
showLeftIcon,
showClearIcon,
onClear,
wrapperClassName,
className,
styleCss,
value,
placeholder,
onChange,
...props
}: InputProps) => {
const { t } = useTranslation()
return (
<div className={`relative inline-flex w-full ${wrapperClassName}`}>
{showPrefix && <span className='whitespace-nowrap absolute left-2 self-center'>{prefixIcon ?? <GlassIcon className='h-3.5 w-3.5 stroke-current text-gray-700 stroke-2' />}</span>}
<div className={cn('relative w-full', wrapperClassName)}>
{showLeftIcon && <RiSearchLine className={cn('absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-components-input-text-placeholder')} />}
<input
type={type ?? 'text'}
className={cn('inline-flex h-7 w-full py-1 px-2 rounded-lg text-xs leading-normal bg-gray-100 caret-primary-600 hover:bg-gray-100 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none focus:bg-white placeholder:text-gray-400', showPrefix ? '!pl-7' : '', className)}
placeholder={placeholder ?? (showPrefix ? t('common.operation.search') ?? '' : 'please input')}
value={localValue}
onChange={(e) => {
setLocalValue(e.target.value)
onChange && onChange(e.target.value)
}}
style={styleCss}
className={cn(
'w-full py-[7px] bg-components-input-bg-normal border border-transparent text-components-input-text-filled hover:bg-components-input-bg-hover hover:border-components-input-border-hover focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs placeholder:text-components-input-text-placeholder appearance-none outline-none caret-primary-600',
inputVariants({ size }),
showLeftIcon && 'pl-[26px]',
showLeftIcon && size === 'large' && 'pl-7',
showClearIcon && value && 'pr-[26px]',
showClearIcon && value && size === 'large' && 'pr-7',
destructive && 'pr-[26px]',
destructive && size === 'large' && 'pr-7',
disabled && 'bg-components-input-bg-disabled border-transparent text-components-input-text-filled-disabled cursor-not-allowed hover:bg-components-input-bg-disabled hover:border-transparent',
destructive && 'bg-components-input-bg-destructive border-components-input-border-destructive text-components-input-text-filled hover:bg-components-input-bg-destructive hover:border-components-input-border-destructive focus:bg-components-input-bg-destructive focus:border-components-input-border-destructive',
className,
)}
placeholder={placeholder ?? (showLeftIcon ? t('common.operation.search') ?? '' : 'please input')}
value={value}
onChange={onChange}
disabled={disabled}
{...props}
/>
{showClearIcon && value && !disabled && !destructive && (
<div className={cn('absolute right-2 top-1/2 -translate-y-1/2 group p-[1px] cursor-pointer')} onClick={onClear}>
<RiCloseCircleFill className='w-3.5 h-3.5 text-text-quaternary cursor-pointer group-hover:text-text-tertiary' />
</div>
)}
{destructive && (
<RiErrorWarningLine className='absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-text-destructive-secondary' />
)}
</div>
)
}

View File

@ -8,7 +8,7 @@ import RemarkGfm from 'remark-gfm'
import SyntaxHighlighter from 'react-syntax-highlighter'
import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs'
import type { RefObject } from 'react'
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import { Component, memo, useEffect, useMemo, useRef, useState } from 'react'
import type { CodeComponent } from 'react-markdown/lib/ast-to-react'
import cn from '@/utils/classnames'
import CopyBtn from '@/app/components/base/copy-btn'
@ -104,7 +104,7 @@ const CodeBlock: CodeComponent = memo(({ inline, className, children, ...props }
const match = /language-(\w+)/.exec(className || '')
const language = match?.[1]
const languageShowName = getCorrectCapitalizationLanguageName(language || '')
let chartData = JSON.parse(String('{"title":{"text":"Something went wrong."}}').replace(/\n$/, ''))
let chartData = JSON.parse(String('{"title":{"text":"ECharts error - Wrong JSON format."}}').replace(/\n$/, ''))
if (language === 'echarts') {
try {
chartData = JSON.parse(String(children).replace(/\n$/, ''))
@ -143,10 +143,10 @@ const CodeBlock: CodeComponent = memo(({ inline, className, children, ...props }
? (<Flowchart PrimitiveCode={String(children).replace(/\n$/, '')} />)
: (
(language === 'echarts')
? (<div style={{ minHeight: '250px', minWidth: '250px' }}><ReactEcharts
? (<div style={{ minHeight: '250px', minWidth: '250px' }}><ErrorBoundary><ReactEcharts
option={chartData}
>
</ReactEcharts></div>)
</ReactEcharts></ErrorBoundary></div>)
: (<SyntaxHighlighter
{...props}
style={atelierHeathLight}
@ -211,3 +211,25 @@ export function Markdown(props: { content: string; className?: string }) {
</div>
)
}
// **Add an ECharts runtime error handler
// Avoid error #7832 (Crash when ECharts accesses undefined objects)
// This can happen when a component attempts to access an undefined object that references an unregistered map, causing the program to crash.
export default class ErrorBoundary extends Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
componentDidCatch(error, errorInfo) {
this.setState({ hasError: true })
console.error(error, errorInfo)
}
render() {
if (this.state.hasError)
return <div>Oops! ECharts reported a runtime error. <br />(see the browser console for more information)</div>
return this.props.children
}
}

View File

@ -191,7 +191,7 @@ const RetrievalParamConfig: FC<Props> = ({
<div className='truncate'>{option.label}</div>
<Tooltip
popupContent={<div className='w-[200px]'>{option.tips}</div>}
triggerClassName='ml-0.5 w-3.5 h-4.5'
triggerClassName='ml-0.5 w-3.5 h-3.5'
/>
</div>
))

View File

@ -58,7 +58,7 @@ const EmptyDatasetCreationModal = ({
<div className={s.tip}>{t('datasetCreation.stepOne.modal.tip')}</div>
<div className={s.form}>
<div className={s.label}>{t('datasetCreation.stepOne.modal.input')}</div>
<Input className='!h-8' value={inputValue} placeholder={t('datasetCreation.stepOne.modal.placeholder') || ''} onChange={setInputValue} />
<Input className='!h-8' value={inputValue} placeholder={t('datasetCreation.stepOne.modal.placeholder') || ''} onChange={e => setInputValue(e.target.value)} />
</div>
<div className='flex flex-row-reverse'>
<Button className='w-24 ml-2' variant='primary' onClick={submit}>{t('datasetCreation.stepOne.modal.confirmButton')}</Button>

View File

@ -391,7 +391,7 @@ const Completed: FC<ICompletedProps> = ({
defaultValue={'all'}
className={s.select}
wrapperClassName='h-fit w-[120px] mr-2' />
<Input showPrefix wrapperClassName='!w-52' className='!h-8' onChange={debounce(setSearchValue, 500)} />
<Input showLeftIcon wrapperClassName='!w-52' className='!h-8' onChange={debounce(e => setSearchValue(e.target.value), 500)} />
</div>
<InfiniteVirtualList
embeddingAvailable={embeddingAvailable}

View File

@ -79,7 +79,7 @@ export const FieldInfo: FC<IFieldInfoProps> = ({
/>
: <Input
className={s.input}
onChange={onUpdate}
onChange={e => onUpdate?.(e.target.value)}
value={value}
defaultValue={defaultValue}
placeholder={`${t('datasetDocuments.metadata.placeholder.add')}${label}`}

View File

@ -201,10 +201,10 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
<div className='flex flex-col px-6 py-4 flex-1'>
<div className='flex items-center justify-between flex-wrap'>
<Input
showPrefix
showLeftIcon
wrapperClassName='!w-[200px]'
className='!h-8 !text-[13px]'
onChange={debounce(setSearchValue, 500)}
onChange={debounce(e => setSearchValue(e.target.value), 500)}
value={searchValue}
/>
<div className='flex gap-2 justify-center items-center !h-8'>

View File

@ -23,7 +23,7 @@ const AppCard = ({
const { t } = useTranslation()
const { app: appBasicInfo } = app
return (
<div className={cn('group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-lg shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg')}>
<div className={cn('relative overflow-hidden pb-2 group col-span-1 bg-white border-2 border-solid border-transparent rounded-lg shadow-sm flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg')}>
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
<div className='relative shrink-0'>
<AppIcon
@ -64,9 +64,13 @@ const AppCard = ({
</div>
</div>
</div>
<div className='mb-1 px-[14px] text-xs leading-normal text-gray-500 line-clamp-4 group-hover:line-clamp-2 group-hover:h-9'>{app.description}</div>
<div className="description-wrapper h-[90px] px-[14px] text-xs leading-normal text-gray-500 ">
<div className='line-clamp-4 group-hover:line-clamp-2'>
{app.description}
</div>
</div>
{isExplore && canCreate && (
<div className={cn('hidden items-center flex-wrap min-h-[42px] px-[14px] pt-2 pb-[10px] group-hover:flex')}>
<div className={cn('hidden items-center flex-wrap min-h-[42px] px-[14px] pt-2 pb-[10px] bg-white group-hover:flex absolute bottom-0 left-0 right-0')}>
<div className={cn('flex items-center w-full space-x-2')}>
<Button variant='primary' className='grow h-7' onClick={() => onCreate()}>
<PlusIcon className='w-4 h-4 mr-1' />
@ -76,7 +80,7 @@ const AppCard = ({
</div>
)}
{!isExplore && (
<div className={cn('hidden items-center flex-wrap min-h-[42px] px-[14px] pt-2 pb-[10px] group-hover:flex')}>
<div className={cn('hidden items-center flex-wrap min-h-[42px] px-[14px] pt-2 pb-[10px] bg-white group-hover:flex absolute bottom-0 left-0 right-0')}>
<div className={cn('flex items-center w-full space-x-2')}>
<Button variant='primary' className='grow h-7' onClick={() => onCreate()}>
<PlusIcon className='w-4 h-4 mr-1' />

View File

@ -5,6 +5,7 @@ import { RiCloseLine } from '@remixicon/react'
import AppIconPicker from '../../base/app-icon-picker'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon'
import { useProviderContext } from '@/context/provider-context'
@ -20,12 +21,15 @@ export type CreateAppModalProps = {
appIcon: string
appIconBackground?: string | null
appIconUrl?: string | null
appMode?: string
appUseIconAsAnswerIcon?: boolean
onConfirm: (info: {
name: string
icon_type: AppIconType
icon: string
icon_background?: string
description: string
use_icon_as_answer_icon?: boolean
}) => Promise<void>
onHide: () => void
}
@ -39,6 +43,8 @@ const CreateAppModal = ({
appIconUrl,
appName,
appDescription,
appMode,
appUseIconAsAnswerIcon,
onConfirm,
onHide,
}: CreateAppModalProps) => {
@ -52,6 +58,7 @@ const CreateAppModal = ({
)
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
const [description, setDescription] = useState(appDescription || '')
const [useIconAsAnswerIcon, setUseIconAsAnswerIcon] = useState(appUseIconAsAnswerIcon || false)
const { plan, enableBilling } = useProviderContext()
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
@ -67,6 +74,7 @@ const CreateAppModal = ({
icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId,
icon_background: appIcon.type === 'emoji' ? appIcon.background! : undefined,
description,
use_icon_as_answer_icon: useIconAsAnswerIcon,
})
onHide()
}
@ -119,6 +127,19 @@ const CreateAppModal = ({
onChange={e => setDescription(e.target.value)}
/>
</div>
{/* answer icon */}
{isEditModal && (appMode === 'chat' || appMode === 'advanced-chat' || appMode === 'agent-chat') && (
<div className='pt-2'>
<div className='flex justify-between items-center'>
<div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.answerIcon.title')}</div>
<Switch
defaultValue={useIconAsAnswerIcon}
onChange={v => setUseIconAsAnswerIcon(v)}
/>
</div>
<p className='body-xs-regular text-gray-500'>{t('app.answerIcon.descriptionInExplore')}</p>
</div>
)}
{!isEditModal && isAppsFull && <AppsFull loc='app-explore-create' />}
</div>
<div className='flex flex-row-reverse'>

View File

@ -148,7 +148,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
{t('common.modelProvider.systemReasoningModel.tip')}
</div>
}
triggerClassName='ml-0.5'
triggerClassName='ml-0.5 w-4 h-4 shrink-0'
/>
</div>
<div>
@ -168,8 +168,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
{t('common.modelProvider.embeddingModel.tip')}
</div>
}
needsDelay={false}
triggerClassName='ml-0.5'
triggerClassName='ml-0.5 w-4 h-4 shrink-0'
/>
</div>
<div>
@ -189,8 +188,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
{t('common.modelProvider.rerankModel.tip')}
</div>
}
needsDelay={false}
triggerClassName='ml-0.5'
triggerClassName='ml-0.5 w-4 h-4 shrink-0'
/>
</div>
<div>
@ -210,8 +208,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
{t('common.modelProvider.speechToTextModel.tip')}
</div>
}
needsDelay={false}
triggerClassName='ml-0.5'
triggerClassName='ml-0.5 w-4 h-4 shrink-0'
/>
</div>
<div>
@ -231,7 +228,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
{t('common.modelProvider.ttsModel.tip')}
</div>
}
triggerClassName='ml-0.5'
triggerClassName='ml-0.5 w-4 h-4 shrink-0'
/>
</div>
<div>

View File

@ -114,7 +114,7 @@ const ConfigCredential: FC<Props> = ({
{t('tools.createTool.authMethod.keyTooltip')}
</div>
}
triggerClassName='ml-0.5'
triggerClassName='ml-0.5 w-4 h-4'
/>
</div>
<input

View File

@ -12,12 +12,10 @@ import type {
} from 'reactflow'
import {
getConnectedEdges,
getIncomers,
getOutgoers,
useReactFlow,
useStoreApi,
} from 'reactflow'
import { uniq } from 'lodash-es'
import type { ToolDefaultValue } from '../block-selector/types'
import type {
Edge,
@ -212,19 +210,22 @@ export const useNodesInteractions = () => {
})
})
setEdges(newEdges)
const incomesNodes = getIncomers(node, nodes, edges)
if (incomesNodes.length) {
const incomesNodesOutgoersId = uniq(incomesNodes.map(incomeNode => getOutgoers(incomeNode, nodes, edges)).flat().map(outgoer => outgoer.id))
const connectedEdges = getConnectedEdges([node], edges).filter(edge => edge.target === node.id)
if (incomesNodesOutgoersId.length > 1) {
const newNodes = produce(nodes, (draft) => {
draft.forEach((n) => {
if (incomesNodesOutgoersId.includes(n.id))
n.data._inParallelHovering = true
})
const targetNodes: Node[] = []
for (let i = 0; i < connectedEdges.length; i++) {
const sourceConnectedEdges = getConnectedEdges([{ id: connectedEdges[i].source } as Node], edges).filter(edge => edge.source === connectedEdges[i].source && edge.sourceHandle === connectedEdges[i].sourceHandle)
targetNodes.push(...sourceConnectedEdges.map(edge => nodes.find(n => n.id === edge.target)!))
}
if (targetNodes.length > 1) {
const newNodes = produce(nodes, (draft) => {
draft.forEach((n) => {
if (targetNodes.some(targetNode => n.id === targetNode.id))
n.data._inParallelHovering = true
})
setNodes(newNodes)
}
})
setNodes(newNodes)
}
}, [store, workflowStore, getNodesReadOnly])

View File

@ -73,7 +73,7 @@ const KeyValueItem: FC<Props> = ({
<Input
className='rounded-none bg-white border-none system-sm-regular focus:ring-0 focus:bg-gray-100! hover:bg-gray-50'
value={payload.key}
onChange={handleChange('key')}
onChange={e => handleChange('key')(e.target.value)}
/>
)}
</div>

View File

@ -70,7 +70,7 @@ const RetrievalConfig: FC<Props> = ({
}
onMultipleRetrievalConfigChange({
top_k: configs.top_k,
score_threshold: configs.score_threshold_enabled ? (configs.score_threshold || DATASET_DEFAULT.score_threshold) : null,
score_threshold: configs.score_threshold_enabled ? (configs.score_threshold ?? DATASET_DEFAULT.score_threshold) : null,
reranking_model: payload.retrieval_mode === RETRIEVE_TYPE.oneWay
? undefined
: (!configs.reranking_model?.reranking_provider_name

View File

@ -682,9 +682,7 @@ export const getParallelInfo = (nodes: Node[], edges: Edge[], parentNodeId?: str
const outgoerConnectedEdges = getConnectedEdges([outgoer], edges).filter(edge => edge.source === outgoer.id)
const sourceEdgesGroup = groupBy(outgoerConnectedEdges, 'sourceHandle')
Object.keys(sourceEdgesGroup).sort((a, b) => {
return sourceEdgesGroup[b].length - sourceEdgesGroup[a].length
}).forEach((sourceHandle) => {
Object.keys(sourceEdgesGroup).forEach((sourceHandle) => {
nextHandles.push({ node: outgoer, handle: sourceHandle })
})
if (!outgoerConnectedEdges.length)

View File

@ -77,6 +77,11 @@ const translation = {
emoji: 'Emoji',
image: 'Image',
},
answerIcon: {
title: 'Use WebApp icon to replace 🤖',
description: 'Wether to use the WebApp icon to replace 🤖 in the shared application',
descriptionInExplore: 'Whether to use the WebApp icon to replace 🤖 in Explore',
},
switch: 'Switch to Workflow Orchestrate',
switchTipStart: 'A new app copy will be created for you, and the new copy will switch to Workflow Orchestrate. The new copy will ',
switchTip: 'not allow',

View File

@ -49,6 +49,7 @@ export const NOTICE_I18N = {
ko_KR: '중요 공지',
pl_PL: 'Ważne ogłoszenie',
uk_UA: 'Важливе повідомлення',
ru_RU: 'Важное Уведомление',
vi_VN: 'Thông báo quan trọng',
it_IT: 'Avviso Importante',
fa_IR: 'هشدار مهم',
@ -74,6 +75,8 @@ export const NOTICE_I18N = {
'Nasz system będzie niedostępny od 19:00 do 24:00 UTC 28 sierpnia w celu aktualizacji. W przypadku pytań prosimy o kontakt z naszym zespołem wsparcia (support@dify.ai). Doceniamy Twoją cierpliwość.',
uk_UA:
'Наша система буде недоступна з 19:00 до 24:00 UTC 28 серпня для оновлення. Якщо у вас виникнуть запитання, будь ласка, зв’яжіться з нашою службою підтримки (support@dify.ai). Дякуємо за терпіння.',
ru_RU:
'Наша система будет недоступна с 19:00 до 24:00 UTC 28 августа для обновления. По вопросам, пожалуйста, обращайтесь в нашу службу поддержки (support@dify.ai). Спасибо за ваше терпение',
vi_VN:
'Hệ thống của chúng tôi sẽ ngừng hoạt động từ 19:00 đến 24:00 UTC vào ngày 28 tháng 8 để nâng cấp. Nếu có thắc mắc, vui lòng liên hệ với nhóm hỗ trợ của chúng tôi (support@dify.ai). Chúng tôi đánh giá cao sự kiên nhẫn của bạn.',
tr_TR:

View File

@ -68,7 +68,7 @@
"name": "Русский (Россия)",
"prompt_name": "Russian",
"example": " Привет, Dify!",
"supported": false
"supported": true
},
{
"value": "it-IT",

View File

@ -0,0 +1,87 @@
const translation = {
title: 'Аннотации',
name: 'Ответить на аннотацию',
editBy: 'Ответ отредактирован {{author}}',
noData: {
title: 'Нет аннотаций',
description: 'Вы можете редактировать аннотации во время отладки приложения или импортировать их массово здесь для получения качественного ответа.',
},
table: {
header: {
question: 'вопрос',
answer: 'ответ',
createdAt: 'создано',
hits: 'попаданий',
actions: 'действия',
addAnnotation: 'Добавить аннотацию',
bulkImport: 'Массовый импорт',
bulkExport: 'Массовый экспорт',
clearAll: 'Очистить все аннотации',
},
},
editModal: {
title: 'Редактировать ответ аннотации',
queryName: 'Запрос пользователя',
answerName: 'Storyteller Bot',
yourAnswer: 'Ваш ответ',
answerPlaceholder: 'Введите ваш ответ здесь',
yourQuery: 'Ваш запрос',
queryPlaceholder: 'Введите ваш запрос здесь',
removeThisCache: 'Удалить эту аннотацию',
createdAt: 'Создано',
},
addModal: {
title: 'Добавить ответ аннотации',
queryName: 'Вопрос',
answerName: 'Ответ',
answerPlaceholder: 'Введите ответ здесь',
queryPlaceholder: 'Введите вопрос здесь',
createNext: 'Добавить еще один аннотированный ответ',
},
batchModal: {
title: 'Массовый импорт',
csvUploadTitle: 'Перетащите сюда ваш CSV-файл или ',
browse: 'выберите файл',
tip: 'CSV-файл должен соответствовать следующей структуре:',
question: 'вопрос',
answer: 'ответ',
contentTitle: 'содержимое фрагмента',
content: 'содержимое',
template: 'Скачать шаблон здесь',
cancel: 'Отмена',
run: 'Запустить пакет',
runError: 'Ошибка запуска пакета',
processing: 'В процессе пакетной обработки',
completed: 'Импорт завершен',
error: 'Ошибка импорта',
ok: 'ОК',
},
errorMessage: {
answerRequired: 'Ответ обязателен',
queryRequired: 'Вопрос обязателен',
},
viewModal: {
annotatedResponse: 'Ответ аннотации',
hitHistory: 'История попаданий',
hit: 'Попадание',
hits: 'Попадания',
noHitHistory: 'Нет истории попаданий',
},
hitHistoryTable: {
query: 'Запрос',
match: 'Совпадение',
response: 'Ответ',
source: 'Источник',
score: 'Оценка',
time: 'Время',
},
initSetup: {
title: 'Начальная настройка ответа аннотации',
configTitle: 'Настройка ответа аннотации',
confirmBtn: 'Сохранить и включить',
configConfirmBtn: 'Сохранить',
},
embeddingModelSwitchTip: 'Модель векторизации текста аннотаций, переключение между моделями будет осуществлено повторно, что приведет к дополнительным затратам.',
}
export default translation

83
web/i18n/ru-RU/app-api.ts Normal file
View File

@ -0,0 +1,83 @@
const translation = {
apiServer: 'API Сервер',
apiKey: 'API Ключ',
status: 'Статус',
disabled: 'Отключено',
ok: 'В работе',
copy: 'Копировать',
copied: 'Скопировано',
play: 'Запустить',
pause: 'Приостановить',
playing: 'Запущено',
loading: 'Загрузка',
merMaind: {
rerender: 'Перезапустить рендеринг',
},
never: 'Никогда',
apiKeyModal: {
apiSecretKey: 'Секретный ключ API',
apiSecretKeyTips: 'Чтобы предотвратить злоупотребление API, защитите свой API ключ. Избегайте использования его в виде plain-текста во фронтенд-коде. :)',
createNewSecretKey: 'Создать новый секретный ключ',
secretKey: 'Секретный ключ',
created: 'СОЗДАН',
lastUsed: 'ПОСЛЕДНЕЕ ИСПОЛЬЗОВАНИЕ',
generateTips: 'Храните этот ключ в безопасном и доступном месте.',
},
actionMsg: {
deleteConfirmTitle: 'Удалить этот секретный ключ?',
deleteConfirmTips: 'Это действие необратимо.',
ok: 'ОК',
},
completionMode: {
title: 'API приложения',
info: 'Для высококачественной генерации текста, такой как статьи, резюме и переводы, используйте API completion-messages с пользовательским вводом. Генерация текста основана на параметрах модели и шаблонах подсказок, установленных в Dify Prompt Engineering.',
createCompletionApi: 'Создать completion-message',
createCompletionApiTip: 'Создайте completion-message для поддержки режима вопросов и ответов.',
inputsTips: '(Необязательно) Укажите поля пользовательского ввода в виде пар ключ-значение, соответствующих переменным в Prompt Eng. Ключ - это имя переменной, Значение - это значение параметра. Если тип поля - Выбор, отправленное Значение должно быть одним из предустановленных вариантов.',
queryTips: 'Текстовое содержимое пользовательского ввода.',
blocking: 'Блокирующий тип, ожидает завершения выполнения и возвращает результаты. (Запросы могут быть прерваны, если процесс длительный)',
streaming: ' Ответ в рамках потока. Реализация потоковой передачи ответов на основе SSE (Server-Sent Events).',
messageFeedbackApi: 'Обратная связь по сообщению (лайк)',
messageFeedbackApiTip: 'Оцените полученные сообщения от имени конечных пользователей с помощью лайков или дизлайков. Эти данные видны на странице Журналы и аннотации и используются для будущей тонкой настройки модели.',
messageIDTip: 'Идентификатор сообщения',
ratingTip: 'лайк или дизлайк, null - отмена',
parametersApi: 'Получить информацию о параметрах приложения',
parametersApiTip: 'Получить настроенные входные параметры, включая имена переменных, имена полей, типы и значения по умолчанию. Обычно используется для отображения этих полей в форме или заполнения значений по умолчанию после загрузки клиента.',
},
chatMode: {
title: 'API приложения чата',
info: 'Для универсальных диалоговых приложений, использующих формат вопросов и ответов, вызовите API chat-messages, чтобы начать диалог. Поддерживайте текущие разговоры, передавая возвращенный conversation_id. Параметры ответа и шаблоны зависят от настроек Dify Prompt Eng.',
createChatApi: 'Создать сообщение чата',
createChatApiTip: 'Создайте новое сообщение разговора или продолжите существующий диалог.',
inputsTips: '(Необязательно) Укажите поля пользовательского ввода в виде пар ключ-значение, соответствующих переменным в Prompt Eng. Ключ - это имя переменной, Значение - это значение параметра. Если тип поля - Выбор, отправленное Значение должно быть одним из предустановленных вариантов.',
queryTips: 'Содержимое пользовательского ввода/вопроса',
blocking: 'Блокирующий тип, ожидает завершения выполнения и возвращает результаты. (Запросы могут быть прерваны, если процесс длительный)',
streaming: 'потоковая передача возвращает. Реализация потоковой передачи возврата на основе SSE (Server-Sent Events).',
conversationIdTip: '(Необязательно) Идентификатор разговора: оставьте пустым для первого разговора; передайте conversation_id из контекста, чтобы продолжить диалог.',
messageFeedbackApi: 'Обратная связь конечного пользователя по сообщению, лайк',
messageFeedbackApiTip: 'Оцените полученные сообщения от имени конечных пользователей с помощью лайков или дизлайков. Эти данные видны на странице Журналы и аннотации и используются для будущей тонкой настройки модели.',
messageIDTip: 'Идентификатор сообщения',
ratingTip: 'лайк или дизлайк, null - отмена',
chatMsgHistoryApi: 'Получить историю сообщений чата',
chatMsgHistoryApiTip: 'Первая страница возвращает последние `limit` строк, которые находятся в обратном порядке.',
chatMsgHistoryConversationIdTip: 'Идентификатор разговора',
chatMsgHistoryFirstId: 'Идентификатор первой записи чата на текущей странице. По умолчанию - нет.',
chatMsgHistoryLimit: 'Сколько чатов возвращается за один запрос',
conversationsListApi: 'Получить список разговоров',
conversationsListApiTip: 'Получает список сеансов текущего пользователя. По умолчанию возвращаются последние 20 сеансов.',
conversationsListFirstIdTip: 'Идентификатор последней записи на текущей странице, по умолчанию - нет.',
conversationsListLimitTip: 'Сколько чатов возвращается за один запрос',
conversationRenamingApi: 'Переименование разговора',
conversationRenamingApiTip: 'Переименовать разговоры; имя отображается в многосессионных клиентских интерфейсах.',
conversationRenamingNameTip: 'Новое имя',
parametersApi: 'Получить информацию о параметрах приложения',
parametersApiTip: 'Получить настроенные входные параметры, включая имена переменных, имена полей, типы и значения по умолчанию. Обычно используется для отображения этих полей в форме или заполнения значений по умолчанию после загрузки клиента.',
},
develop: {
requestBody: 'Тело запроса',
pathParams: 'Параметры пути',
query: 'Запрос',
},
}
export default translation

463
web/i18n/ru-RU/app-debug.ts Normal file
View File

@ -0,0 +1,463 @@
const translation = {
pageTitle: {
line1: 'PROMPT',
line2: 'Engineering',
},
orchestrate: 'Оркестрация',
promptMode: {
simple: 'Переключиться в экспертный режим для редактирования всего ПРОМПТА',
advanced: 'Экспертный режим',
switchBack: 'Переключиться обратно',
advancedWarning: {
title: 'Вы переключились в экспертный режим, и после изменения ПРОМПТА вы НЕ СМОЖЕТЕ вернуться в базовый режим.',
description: 'В экспертном режиме вы можете редактировать весь ПРОМПТ.',
learnMore: 'Узнать больше',
ok: 'ОК',
},
operation: {
addMessage: 'Добавить сообщение',
},
contextMissing: 'Отсутствует компонент контекста, эффективность промпта может быть невысокой.',
},
operation: {
applyConfig: 'Опубликовать',
resetConfig: 'Сбросить',
debugConfig: 'Отладка',
addFeature: 'Добавить функцию',
automatic: 'Сгенерировать',
stopResponding: 'Остановить ответ',
agree: 'лайк',
disagree: 'дизлайк',
cancelAgree: 'Отменить лайк',
cancelDisagree: 'Отменить дизлайк',
userAction: 'Пользователь ',
},
notSetAPIKey: {
title: 'Ключ поставщика LLM не установлен',
trailFinished: 'Пробный период закончен',
description: 'Ключ поставщика LLM не установлен, его необходимо установить перед отладкой.',
settingBtn: 'Перейти к настройкам',
},
trailUseGPT4Info: {
title: 'В настоящее время не поддерживается gpt-4',
description: 'Чтобы использовать gpt-4, пожалуйста, установите API ключ.',
},
feature: {
groupChat: {
title: 'Улучшение чата',
description: 'Добавление настроек предварительного разговора для приложений может улучшить пользовательский опыт.',
},
groupExperience: {
title: 'Улучшение опыта',
},
conversationOpener: {
title: 'Начальное сообщение',
description: 'В чат-приложении первое предложение, которое ИИ активно говорит пользователю, обычно используется в качестве приветствия.',
},
suggestedQuestionsAfterAnswer: {
title: 'Последующие вопросы',
description: 'Настройка предложения следующих вопросов может улучшить чат для пользователей.',
resDes: '3 предложения для следующего вопроса пользователя.',
tryToAsk: 'Попробуйте спросить',
},
moreLikeThis: {
title: 'Больше похожего',
description: 'Сгенерируйте несколько текстов одновременно, а затем отредактируйте и продолжайте генерировать',
generateNumTip: 'Количество генерируемых каждый раз',
tip: 'Использование этой функции приведет к дополнительным расходам токенов',
},
speechToText: {
title: 'Преобразование речи в текст',
description: 'После включения вы можете использовать голосовой ввод.',
resDes: 'Голосовой ввод включен',
},
textToSpeech: {
title: 'Преобразование текста в речь',
description: 'После включения текст можно преобразовать в речь.',
resDes: 'Преобразование текста в аудио включено',
},
citation: {
title: 'Цитаты и ссылки',
description: 'После включения отображается исходный документ и атрибутированная часть сгенерированного контента.',
resDes: 'Цитаты и ссылки включены',
},
annotation: {
title: 'Ответ аннотации',
description: 'Вы можете вручную добавить высококачественный ответ в кэш для приоритетного сопоставления с похожими вопросами пользователей.',
resDes: 'Ответ аннотации включен',
scoreThreshold: {
title: 'Порог оценки',
description: 'Используется для установки порога сходства для ответа аннотации.',
easyMatch: 'Простое совпадение',
accurateMatch: 'Точное совпадение',
},
matchVariable: {
title: 'Переменная соответствия',
choosePlaceholder: 'Выберите переменную соответствия',
},
cacheManagement: 'Аннотации',
cached: 'Аннотировано',
remove: 'Удалить',
removeConfirm: 'Удалить эту аннотацию?',
add: 'Добавить аннотацию',
edit: 'Редактировать аннотацию',
},
dataSet: {
title: 'Контекст',
noData: 'Вы можете импортировать знания в качестве контекста',
words: 'Слова',
textBlocks: 'Текстовые блоки',
selectTitle: 'Выберите справочные знания',
selected: 'Знания выбраны',
noDataSet: 'Знания не найдены',
toCreate: 'Перейти к созданию',
notSupportSelectMulti: 'В настоящее время поддерживаются только одни знания',
queryVariable: {
title: 'Переменная запроса',
tip: 'Эта переменная будет использоваться в качестве входных данных запроса для поиска контекста, получая информацию о контексте, связанную с вводом этой переменной.',
choosePlaceholder: 'Выберите переменную запроса',
noVar: 'Нет переменных',
noVarTip: 'пожалуйста, создайте переменную в разделе Переменные',
unableToQueryDataSet: 'Невозможно запросить знания',
unableToQueryDataSetTip: 'Не удалось успешно запросить знания, пожалуйста, выберите переменную запроса контекста в разделе контекста.',
ok: 'ОК',
contextVarNotEmpty: 'переменная запроса контекста не может быть пустой',
deleteContextVarTitle: 'Удалить переменную "{{varName}}"?',
deleteContextVarTip: 'Эта переменная была установлена в качестве переменной запроса контекста, и ее удаление повлияет на нормальное использование знаний. Если вам все еще нужно удалить ее, пожалуйста, выберите ее заново в разделе контекста.',
},
},
tools: {
title: 'Инструменты',
tips: 'Инструменты предоставляют стандартный метод вызова API, принимая пользовательский ввод или переменные в качестве параметров запроса для запроса внешних данных в качестве контекста.',
toolsInUse: '{{count}} инструментов используется',
modal: {
title: 'Инструмент',
toolType: {
title: 'Тип инструмента',
placeholder: 'Пожалуйста, выберите тип инструмента',
},
name: {
title: 'Имя',
placeholder: 'Пожалуйста, введите имя',
},
variableName: {
title: 'Имя переменной',
placeholder: 'Пожалуйста, введите имя переменной',
},
},
},
conversationHistory: {
title: 'История разговоров',
description: 'Установить префиксы имен для ролей разговора',
tip: 'История разговоров не включена, пожалуйста, добавьте <histories> в промпт выше.',
learnMore: 'Узнать больше',
editModal: {
title: 'Редактировать имена ролей разговора',
userPrefix: 'Префикс пользователя',
assistantPrefix: 'Префикс помощника',
},
},
toolbox: {
title: 'НАБОР ИНСТРУМЕНТОВ',
},
moderation: {
title: 'Модерация контента',
description: 'Обеспечьте безопасность выходных данных модели, используя API модерации или поддерживая список чувствительных слов.',
allEnabled: 'ВХОДНОЙ/ВЫХОДНОЙ контент включен',
inputEnabled: 'ВХОДНОЙ контент включен',
outputEnabled: 'ВЫХОДНОЙ контент включен',
modal: {
title: 'Настройки модерации контента',
provider: {
title: 'Поставщик',
openai: 'Модерация OpenAI',
openaiTip: {
prefix: 'Для модерации OpenAI требуется ключ API OpenAI, настроенный в ',
suffix: '.',
},
keywords: 'Ключевые слова',
},
keywords: {
tip: 'По одному на строку, разделенные разрывами строк. До 100 символов на строку.',
placeholder: 'По одному на строку, разделенные разрывами строк',
line: 'Строка',
},
content: {
input: 'Модерировать ВХОДНОЙ контент',
output: 'Модерировать ВЫХОДНОЙ контент',
preset: 'Предустановленные ответы',
placeholder: 'Здесь содержимое предустановленных ответов',
condition: 'Модерация ВХОДНОГО и ВЫХОДНОГО контента включена хотя бы одна',
fromApi: 'Предустановленные ответы возвращаются API',
errorMessage: 'Предустановленные ответы не могут быть пустыми',
supportMarkdown: 'Markdown поддерживается',
},
openaiNotConfig: {
before: 'Для модерации OpenAI требуется ключ API OpenAI, настроенный в',
after: '',
},
},
},
},
generate: {
title: 'Генератор промпта',
description: 'Генератор промпта использует настроенную модель для оптимизации промпта для повышения качества и улучшения структуры. Пожалуйста, напишите четкие и подробные инструкции.',
tryIt: 'Попробуйте',
instruction: 'Инструкции',
instructionPlaceHolder: 'Напишите четкие и конкретные инструкции.',
generate: 'Сгенерировать',
resTitle: 'Сгенерированный промпт',
noDataLine1: 'Опишите свой случай использования слева,',
noDataLine2: 'предварительный просмотр оркестрации будет показан здесь.',
apply: 'Применить',
loading: 'Оркестрация приложения для вас...',
overwriteTitle: 'Перезаписать существующую конфигурацию?',
overwriteMessage: 'Применение этого промпта перезапишет существующую конфигурацию.',
template: {
pythonDebugger: {
name: 'Отладчик Python',
instruction: 'Бот, который может генерировать и отлаживать ваш код на основе ваших инструкций',
},
translation: {
name: 'Переводчик',
instruction: 'Переводчик, который может переводить на несколько языков',
},
professionalAnalyst: {
name: 'Профессиональный аналитик',
instruction: 'Извлекайте информацию, выявляйте риски и извлекайте ключевую информацию из длинных отчетов в одну записку',
},
excelFormulaExpert: {
name: 'Эксперт по формулам Excel',
instruction: 'Чат-бот, который может помочь начинающим пользователям понять, использовать и создавать формулы Excel на основе инструкций пользователя',
},
travelPlanning: {
name: 'Планировщик путешествий',
instruction: 'Помощник по планированию путешествий - это интеллектуальный инструмент, разработанный, чтобы помочь пользователям без труда планировать свои поездки',
},
SQLSorcerer: {
name: 'SQL-ассистент',
instruction: 'Преобразуйте повседневный язык в SQL-запросы',
},
GitGud: {
name: 'Git gud',
instruction: 'Генерируйте соответствующие команды Git на основе описанных пользователем действий по управлению версиями',
},
meetingTakeaways: {
name: 'Итоги совещания',
instruction: 'Извлекайте из совещаний краткие резюме, включая темы обсуждения, ключевые выводы и элементы действий',
},
writingsPolisher: {
name: 'Редактор',
instruction: 'Используйте LLM, чтобы улучшить свои письменные работы',
},
},
},
resetConfig: {
title: 'Подтвердить сброс?',
message:
'Сброс отменяет изменения, восстанавливая последнюю опубликованную конфигурацию.',
},
errorMessage: {
nameOfKeyRequired: 'имя ключа: {{key}} обязательно',
valueOfVarRequired: 'значение {{key}} не может быть пустым',
queryRequired: 'Требуется текст запроса.',
waitForResponse:
'Пожалуйста, дождитесь завершения ответа на предыдущее сообщение.',
waitForBatchResponse:
'Пожалуйста, дождитесь завершения ответа на пакетное задание.',
notSelectModel: 'Пожалуйста, выберите модель',
waitForImgUpload: 'Пожалуйста, дождитесь загрузки изображения',
},
chatSubTitle: 'Инструкции',
completionSubTitle: 'Префикс Промпта',
promptTip:
'Промпт направляют ответы ИИ с помощью инструкций и ограничений. Вставьте переменные, такие как {{input}}. Этот Промпт не будет видна пользователям.',
formattingChangedTitle: 'Форматирование изменено',
formattingChangedText:
'Изменение форматирования приведет к сбросу области отладки, вы уверены?',
variableTitle: 'Переменные',
variableTip:
'Пользователи заполняют переменные в форме, автоматически заменяя переменные в промпте.',
notSetVar: 'Переменные позволяют пользователям вводить промпты или вступительные замечания при заполнении форм. Вы можете попробовать ввести "{{input}}" в промптах.',
autoAddVar: 'В предварительной промпте упоминаются неопределенные переменные, хотите ли вы добавить их в форму пользовательского ввода?',
variableTable: {
key: 'Ключ переменной',
name: 'Имя поля пользовательского ввода',
optional: 'Необязательно',
type: 'Тип ввода',
action: 'Действия',
typeString: 'Строка',
typeSelect: 'Выбор',
},
varKeyError: {
canNoBeEmpty: '{{key}} обязательно',
tooLong: '{{key}} слишком длинное. Не может быть длиннее 30 символов',
notValid: '{{key}} недействительно. Может содержать только буквы, цифры и подчеркивания',
notStartWithNumber: '{{key}} не может начинаться с цифры',
keyAlreadyExists: '{{key}} уже существует',
},
otherError: {
promptNoBeEmpty: 'Промпт не может быть пустой',
historyNoBeEmpty: 'История разговоров должна быть установлена в промпте',
queryNoBeEmpty: 'Запрос должен быть установлен в промпте',
},
variableConig: {
'addModalTitle': 'Добавить поле ввода',
'editModalTitle': 'Редактировать поле ввода',
'description': 'Настройка для переменной {{varName}}',
'fieldType': 'Тип поля',
'string': 'Короткий текст',
'text-input': 'Короткий текст',
'paragraph': 'Абзац',
'select': 'Выбор',
'number': 'Число',
'notSet': 'Не задано, попробуйте ввести {{input}} в префикс промпта',
'stringTitle': 'Параметры текстового поля формы',
'maxLength': 'Максимальная длина',
'options': 'Варианты',
'addOption': 'Добавить вариант',
'apiBasedVar': 'Переменная на основе API',
'varName': 'Имя переменной',
'labelName': 'Имя метки',
'inputPlaceholder': 'Пожалуйста, введите',
'content': 'Содержимое',
'required': 'Обязательно',
'errorMsg': {
labelNameRequired: 'Имя метки обязательно',
varNameCanBeRepeat: 'Имя переменной не может повторяться',
atLeastOneOption: 'Требуется хотя бы один вариант',
optionRepeat: 'Есть повторяющиеся варианты',
},
},
vision: {
name: 'Зрение',
description: 'Включение зрения позволит модели принимать изображения и отвечать на вопросы о них.',
settings: 'Настройки',
visionSettings: {
title: 'Настройки зрения',
resolution: 'Разрешение',
resolutionTooltip: `Низкое разрешение позволит модели получать версию изображения с низким разрешением 512 x 512 и представлять изображение с бюджетом 65 токенов. Это позволяет API возвращать ответы быстрее и потреблять меньше входных токенов для случаев использования, не требующих высокой детализации.
\n
Высокое разрешение сначала позволит модели увидеть изображение с низким разрешением, а затем создаст детальные фрагменты входных изображений в виде квадратов 512 пикселей на основе размера входного изображения. Каждый из детальных фрагментов использует вдвое больший бюджет токенов, в общей сложности 129 токенов.`,
high: 'Высокое',
low: 'Низкое',
uploadMethod: 'Метод загрузки',
both: 'Оба',
localUpload: 'Локальная загрузка',
url: 'URL',
uploadLimit: 'Лимит загрузки',
},
},
voice: {
name: 'Голос',
defaultDisplay: 'Голос по умолчанию',
description: 'Настройки преобразования текста в речь',
settings: 'Настройки',
voiceSettings: {
title: 'Настройки голоса',
language: 'Язык',
resolutionTooltip: 'Язык, поддерживаемый преобразованием текста в речь.',
voice: 'Голос',
autoPlay: 'Автовоспроизведение',
autoPlayEnabled: 'Включить',
autoPlayDisabled: 'Выключить',
},
},
openingStatement: {
title: 'Начальное сообщение',
add: 'Добавить',
writeOpener: 'Написать начальное сообщение',
placeholder: 'Напишите здесь свое начальное сообщение, вы можете использовать переменные, попробуйте ввести {{variable}}.',
openingQuestion: 'Начальные вопросы',
noDataPlaceHolder:
'Начало разговора с пользователем может помочь ИИ установить более тесную связь с ним в диалоговых приложениях.',
varTip: 'Вы можете использовать переменные, попробуйте ввести {{variable}}',
tooShort: 'Для генерации вступительного замечания к разговору требуется не менее 20 слов начального промпта.',
notIncludeKey: 'Начальный промпт не включает переменную: {{key}}. Пожалуйста, добавьте её в начальную промпт.',
},
modelConfig: {
model: 'Модель',
setTone: 'Установить тон ответов',
title: 'Модель и параметры',
modeType: {
chat: 'Чат',
completion: 'Завершение',
},
},
inputs: {
title: 'Отладка и предварительный просмотр',
noPrompt: 'Попробуйте написать промпт во входных данных предварительного промпта',
userInputField: 'Поле пользовательского ввода',
noVar: 'Заполните значение переменной, которое будет автоматически заменяться в промпте каждый раз при запуске нового сеанса.',
chatVarTip:
'Заполните значение переменной, которое будет автоматически заменяться в промпте каждый раз при запуске нового сеанса',
completionVarTip:
'Заполните значение переменной, которое будет автоматически заменяться в промпте каждый раз при отправке вопроса.',
previewTitle: 'Предварительный просмотр промпта',
queryTitle: 'Содержимое запроса',
queryPlaceholder: 'Пожалуйста, введите текст запроса.',
run: 'ЗАПУСТИТЬ',
},
result: 'Выходной текст',
datasetConfig: {
settingTitle: 'Настройки поиска',
knowledgeTip: 'Нажмите кнопку "+", чтобы добавить знания',
retrieveOneWay: {
title: 'Поиск N-к-1',
description: 'На основе намерения пользователя и описаний знаний агент автономно выбирает наилучшие знания для запроса. Лучше всего подходит для приложений с различными, ограниченными знаниями.',
},
retrieveMultiWay: {
title: 'Многопутный поиск',
description: 'На основе намерения пользователя выполняет запросы по всем знаниям, извлекает соответствующий текст из нескольких источников и выбирает наилучшие результаты, соответствующие запросу пользователя, после повторного ранжирования.',
},
rerankModelRequired: 'Требуется rerank-модель ',
params: 'Параметры',
top_k: 'Top K',
top_kTip: 'Используется для фильтрации фрагментов, наиболее похожих на вопросы пользователей. Система также будет динамически корректировать значение Top K в зависимости от max_tokens выбранной модели.',
score_threshold: 'Порог оценки',
score_thresholdTip: 'Используется для установки порога сходства для фильтрации фрагментов.',
retrieveChangeTip: 'Изменение режима индексации и режима поиска может повлиять на приложения, связанные с этими знаниями.',
},
debugAsSingleModel: 'Отладка как одной модели',
debugAsMultipleModel: 'Отладка как нескольких моделей',
duplicateModel: 'Дублировать',
publishAs: 'Опубликовать как',
assistantType: {
name: 'Тип помощника',
chatAssistant: {
name: 'Базовый помощник',
description: 'Создайте помощника на основе чата, используя большую языковую модель',
},
agentAssistant: {
name: 'Агент-помощник',
description: 'Создайте интеллектуального агента, который может автономно выбирать инструменты для выполнения задач',
},
},
agent: {
agentMode: 'Режим агента',
agentModeDes: 'Установите тип режима вывода для агента',
agentModeType: {
ReACT: 'ReAct',
functionCall: 'Вызов функции',
},
setting: {
name: 'Настройки агента',
description: 'Настройки агента-помощника позволяют установить режим агента и расширенные функции, такие как встроенные промпты, доступные только в типе агента.',
maximumIterations: {
name: 'Максимальное количество итераций',
description: 'Ограничьте количество итераций, которые может выполнить агент-помощник',
},
},
buildInPrompt: 'Встроенный промпт',
firstPrompt: 'Первый промпт',
nextIteration: 'Следующая итерация',
promptPlaceholder: 'Напишите здесь свой первый промпт',
tools: {
name: 'Инструменты',
description: 'Использование инструментов может расширить возможности LLM, такие как поиск в Интернете или выполнение научных расчетов',
enabled: 'Включено',
},
},
}
export default translation

95
web/i18n/ru-RU/app-log.ts Normal file
View File

@ -0,0 +1,95 @@
const translation = {
title: 'Логирование',
description: 'В логах записывается состояние работы приложения, включая пользовательский ввод и ответы ИИ.',
dateTimeFormat: 'DD.MM.YYYY HH:mm',
table: {
header: {
updatedTime: 'Время обновления',
time: 'Время создания',
endUser: 'Конечный пользователь или аккаунт',
input: 'Ввод',
output: 'Вывод',
summary: 'Заголовок',
messageCount: 'Количество сообщений',
userRate: 'Оценка пользователя',
adminRate: 'Оценка оп.',
startTime: 'ВРЕМЯ НАЧАЛА',
status: 'СТАТУС',
runtime: 'ВРЕМЯ ВЫПОЛНЕНИЯ',
tokens: 'ТОКЕНЫ',
user: 'Конечный пользователь или аккаунт',
version: 'ВЕРСИЯ',
},
pagination: {
previous: 'Предыдущий',
next: 'Следующий',
},
empty: {
noChat: 'Еще нет чатов',
noOutput: 'Нет вывода',
element: {
title: 'Есть кто-нибудь?',
content: 'Наблюдайте и аннотируйте взаимодействия между конечными пользователями и приложениями ИИ здесь, чтобы постоянно повышать точность ИИ. Вы можете попробовать <shareLink>поделиться</shareLink> или <testLink>протестировать</testLink> веб-приложение самостоятельно, а затем вернуться на эту страницу.',
},
},
},
detail: {
time: 'Время',
conversationId: 'Идентификатор разговора',
promptTemplate: 'Шаблон подсказки',
promptTemplateBeforeChat: 'Шаблон подсказки перед чатом · Как системное сообщение',
annotationTip: 'Улучшения, отмеченные {{user}}',
timeConsuming: '',
second: 'с',
tokenCost: 'Потрачено токенов',
loading: 'загрузка',
operation: {
like: 'лайк',
dislike: 'дизлайк',
addAnnotation: 'Добавить улучшение',
editAnnotation: 'Редактировать улучшение',
annotationPlaceholder: 'Введите ожидаемый ответ, который вы хотите получить от ИИ, который может быть использован для тонкой настройки модели и постоянного улучшения качества генерации текста в будущем.',
},
variables: 'Переменные',
uploadImages: 'Загруженные изображения',
},
filter: {
period: {
today: 'Сегодня',
last7days: 'Последние 7 дней',
last4weeks: 'Последние 4 недели',
last3months: 'Последние 3 месяца',
last12months: 'Последние 12 месяцев',
monthToDate: 'С начала месяца',
quarterToDate: 'С начала квартала',
yearToDate: 'С начала года',
allTime: 'Все время',
},
annotation: {
all: 'Все',
annotated: 'Аннотированные улучшения ({{count}} элементов)',
not_annotated: 'Не аннотировано',
},
sortBy: 'Сортировать по:',
descending: 'по убыванию',
ascending: 'по возрастанию',
},
workflowTitle: 'Журналы рабочих процессов',
workflowSubtitle: 'Журнал записал работу Automate.',
runDetail: {
title: 'Журнал разговоров',
workflowTitle: 'Подробная информация о журнале',
},
promptLog: 'Журнал подсказок',
agentLog: 'Журнал агента',
viewLog: 'Просмотреть журнал',
agentLogDetail: {
agentMode: 'Режим агента',
toolUsed: 'Использованный инструмент',
iterations: 'Итерации',
iteration: 'Итерация',
finalProcessing: 'Окончательная обработка',
},
}
export default translation

View File

@ -0,0 +1,168 @@
const translation = {
welcome: {
firstStepTip: 'Чтобы начать,',
enterKeyTip: 'введите свой ключ API OpenAI ниже',
getKeyTip: 'Получите свой ключ API на панели инструментов OpenAI',
placeholder: 'Ваш ключ API OpenAI (например, sk-xxxx)',
},
apiKeyInfo: {
cloud: {
trial: {
title: 'Вы используете пробную квоту {{providerName}}.',
description: 'Пробная квота предоставляется для тестирования. Прежде чем пробная квота будет исчерпана, пожалуйста, настройте своего собственного поставщика модели или приобретите дополнительную квоту.',
},
exhausted: {
title: 'Ваша пробная квота была исчерпана, пожалуйста, настройте свой APIKey.',
description: 'Вы исчерпали свою пробную квоту. Пожалуйста, настройте своего собственного поставщика модели или приобретите дополнительную квоту.',
},
},
selfHost: {
title: {
row1: 'Чтобы начать,',
row2: 'сначала настройте своего поставщика модели.',
},
},
callTimes: 'Количество вызовов',
usedToken: 'Использованные токены',
setAPIBtn: 'Перейти к настройке поставщика модели',
tryCloud: 'Или попробуйте облачную версию Dify с бесплатной квотой',
},
overview: {
title: 'Обзор',
appInfo: {
explanation: 'Готовое к использованию веб-приложение ИИ',
accessibleAddress: 'Публичный URL',
preview: 'Предварительный просмотр',
regenerate: 'Перегенерировать',
regenerateNotice: 'Вы хотите перегенерировать публичный URL?',
preUseReminder: 'Пожалуйста, включите веб-приложение перед продолжением.',
settings: {
entry: 'Настройки',
title: 'Настройки веб-приложения',
webName: 'Название веб-приложения',
webDesc: 'Описание веб-приложения',
webDescTip: 'Этот текст будет отображаться на стороне клиента, предоставляя базовые инструкции по использованию приложения',
webDescPlaceholder: 'Введите описание веб-приложения',
language: 'Язык',
workflow: {
title: 'Рабочий процесс',
subTitle: 'Подробности рабочего процесса',
show: 'Показать',
hide: 'Скрыть',
showDesc: 'Показать или скрыть подробности рабочего процесса в веб-приложении',
},
chatColorTheme: 'Цветовая тема чата',
chatColorThemeDesc: 'Установите цветовую тему чат-бота',
chatColorThemeInverted: 'Инвертированные цвета',
invalidHexMessage: 'Неверное HEX-значение',
sso: {
label: 'SSO аутентификация',
title: 'WebApp SSO',
description: 'Все пользователи должны войти в систему с помощью SSO перед использованием WebApp',
tooltip: 'Обратитесь к администратору, чтобы включить WebApp SSO',
},
more: {
entry: 'Показать больше настроек',
copyright: 'Авторские права',
copyRightPlaceholder: 'Введите имя автора или организации',
privacyPolicy: 'Политика конфиденциальности',
privacyPolicyPlaceholder: 'Введите ссылку на политику конфиденциальности',
privacyPolicyTip: 'Помогает посетителям понять, какие данные собирает приложение, см. <privacyPolicyLink>Политику конфиденциальности</privacyPolicyLink> Dify.',
customDisclaimer: 'Пользовательский отказ от ответственности',
customDisclaimerPlaceholder: 'Введите текст пользовательского отказа от ответственности',
customDisclaimerTip: 'Текст пользовательского отказа от ответственности будет отображаться на стороне клиента, предоставляя дополнительную информацию о приложении',
},
},
embedded: {
entry: 'Встраивание',
title: 'Встроить на веб-сайт',
explanation: 'Выберите способ встраивания чат-приложения на свой веб-сайт',
iframe: 'Чтобы добавить чат-приложение в любое место на вашем веб-сайте, добавьте этот iframe в свой HTML-код.',
scripts: 'Чтобы добавить чат-приложение в правый нижний угол вашего веб-сайта, добавьте этот код в свой HTML.',
chromePlugin: 'Установите расширение Dify Chatbot для Chrome',
copied: 'Скопировано',
copy: 'Копировать',
},
qrcode: {
title: 'QR-код ссылки',
scan: 'Сканировать, чтобы поделиться',
download: 'Скачать QR-код',
},
customize: {
way: 'способ',
entry: 'Настроить',
title: 'Настроить веб-приложение ИИ',
explanation: 'Вы можете настроить внешний интерфейс веб-приложения в соответствии со своими потребностями.',
way1: {
name: 'Создайте форк клиентского кода, измените его и разверните на Vercel (рекомендуется)',
step1: 'Создайте форк клиентского кода и измените его',
step1Tip: 'Нажмите здесь, чтобы создать форк исходного кода в своей учетной записи GitHub и изменить код',
step1Operation: 'Dify-WebClient',
step2: 'Развернуть на Vercel',
step2Tip: 'Нажмите здесь, чтобы импортировать репозиторий в Vercel и развернуть',
step2Operation: 'Импортировать репозиторий',
step3: 'Настроить переменные среды',
step3Tip: 'Добавьте следующие переменные среды в Vercel',
},
way2: {
name: 'Напишите клиентский код для вызова API и разверните его на сервере',
operation: 'Документация',
},
},
},
apiInfo: {
title: 'API серверной части',
explanation: 'Легко интегрируется в ваше приложение',
accessibleAddress: 'Конечная точка API сервиса',
doc: 'Справочник по API',
},
status: {
running: 'В работе',
disable: 'Отключено',
},
},
analysis: {
title: 'Анализ',
ms: 'мс',
tokenPS: 'Токен/с',
totalMessages: {
title: 'Всего сообщений',
explanation: 'Ежедневное количество взаимодействий с ИИ.',
},
totalConversations: {
title: 'Всего чатов',
explanation: 'Ежедневное количество чатов с LLM; проектирование/отладка не учитываются.',
},
activeUsers: {
title: 'Активные пользователи',
explanation: 'Уникальные пользователи, участвующие в вопросах и ответах с LLM; проектирование/отладка не учитываются.',
},
tokenUsage: {
title: 'Использование токенов',
explanation: 'Отражает ежедневное использование токенов языковой модели для приложения, полезно для целей контроля затрат.',
consumed: 'Потрачено',
},
avgSessionInteractions: {
title: 'Среднее количество взаимодействий за сеанс',
explanation: 'Количество непрерывных взаимодействий пользователя с LLM; для приложений на основе чатов.',
},
avgUserInteractions: {
title: 'Среднее количество взаимодействий пользователя',
explanation: 'Отражает ежедневную частоту использования пользователями. Эта метрика отражает активность пользователей.',
},
userSatisfactionRate: {
title: 'Уровень удовлетворенности пользователей',
explanation: 'Количество лайков на 1000 сообщений. Это указывает на долю ответов, которыми пользователи довольны.',
},
avgResponseTime: {
title: 'Среднее время ответа',
explanation: 'Время (мс) для обработки/ответа LLM; для текстовых приложений.',
},
tps: {
title: 'Скорость вывода токенов',
explanation: 'Измерьте производительность LLM. Подсчитайте скорость вывода токенов LLM от начала запроса до завершения вывода.',
},
},
}
export default translation

133
web/i18n/ru-RU/app.ts Normal file
View File

@ -0,0 +1,133 @@
const translation = {
createApp: 'СОЗДАТЬ ПРИЛОЖЕНИЕ',
types: {
all: 'Все',
chatbot: 'Чат-бот',
agent: 'Агент',
workflow: 'Рабочий процесс',
completion: 'Завершение',
},
duplicate: 'Дублировать',
duplicateTitle: 'Дублировать приложение',
export: 'Экспортировать DSL',
exportFailed: 'Ошибка экспорта DSL.',
importDSL: 'Импортировать файл DSL',
createFromConfigFile: 'Создать из файла DSL',
importFromDSL: 'Импортировать из DSL',
importFromDSLFile: 'Из файла DSL',
importFromDSLUrl: 'Из URL',
importFromDSLUrlPlaceholder: 'Вставьте ссылку DSL сюда',
deleteAppConfirmTitle: 'Удалить это приложение?',
deleteAppConfirmContent:
'Удаление приложения необратимо. Пользователи больше не смогут получить доступ к вашему приложению, и все настройки подсказок и журналы будут безвозвратно удалены.',
appDeleted: 'Приложение удалено',
appDeleteFailed: 'Не удалось удалить приложение',
join: 'Присоединяйтесь к сообществу',
communityIntro:
'Общайтесь с членами команды, участниками и разработчиками на разных каналах.',
roadmap: 'Посмотреть наш roadmap',
newApp: {
startFromBlank: 'Создать с нуля',
startFromTemplate: 'Создать из шаблона',
captionAppType: 'Какой тип приложения вы хотите создать?',
chatbotDescription: 'Создайте приложение на основе чата. Это приложение использует формат вопросов и ответов, позволяя общаться непрерывно.',
completionDescription: 'Создайте приложение, которое генерирует высококачественный текст на основе подсказок, например, генерирует статьи, резюме, переводы и многое другое.',
completionWarning: 'Этот тип приложения больше не будет поддерживаться.',
agentDescription: 'Создайте интеллектуального агента, который может автономно выбирать инструменты для выполнения задач',
workflowDescription: 'Создайте приложение, которое генерирует высококачественный текст на основе рабочего процесса, организованного с высокой степенью настройки. Подходит для опытных пользователей.',
workflowWarning: 'В настоящее время находится в бета-версии',
chatbotType: 'Метод организации чат-бота',
basic: 'Базовый',
basicTip: 'Для начинающих, можно переключиться на Chatflow позже',
basicFor: 'ДЛЯ НАЧИНАЮЩИХ',
basicDescription: 'Базовый конструктор позволяет создать приложение чат-бота с помощью простых настроек, без возможности изменять встроенные подсказки. Подходит для начинающих.',
advanced: 'Chatflow',
advancedFor: 'Для продвинутых пользователей',
advancedDescription: 'Организация рабочего процесса организует чат-ботов в виде рабочих процессов, предлагая высокую степень настройки, включая возможность редактирования встроенных подсказок. Подходит для опытных пользователей.',
captionName: 'Значок и название приложения',
appNamePlaceholder: 'Дайте вашему приложению имя',
captionDescription: 'Описание',
appDescriptionPlaceholder: 'Введите описание приложения',
useTemplate: 'Использовать этот шаблон',
previewDemo: 'Предварительный просмотр',
chatApp: 'Ассистент',
chatAppIntro:
'Я хочу создать приложение на основе чата. Это приложение использует формат вопросов и ответов, позволяя общаться непрерывно.',
agentAssistant: 'Новый Ассистент Агента',
completeApp: 'Генератор текста',
completeAppIntro:
'Я хочу создать приложение, которое генерирует высококачественный текст на основе подсказок, например, генерирует статьи, резюме, переводы и многое другое.',
showTemplates: 'Я хочу выбрать из шаблона',
hideTemplates: 'Вернуться к выбору режима',
Create: 'Создать',
Cancel: 'Отмена',
nameNotEmpty: 'Имя не может быть пустым',
appTemplateNotSelected: 'Пожалуйста, выберите шаблон',
appTypeRequired: 'Пожалуйста, выберите тип приложения',
appCreated: 'Приложение создано',
appCreateFailed: 'Не удалось создать приложение',
},
editApp: 'Редактировать информацию',
editAppTitle: 'Редактировать информацию о приложении',
editDone: 'Информация о приложении обновлена',
editFailed: 'Не удалось обновить информацию о приложении',
iconPicker: {
ok: 'ОК',
cancel: 'Отмена',
emoji: 'Эмодзи',
image: 'Изображение',
},
switch: 'Переключиться на Workflow',
switchTipStart: 'Для вас будет создана новая копия Workflow. Новая копия ',
switchTip: 'не позволит',
switchTipEnd: ' переключиться обратно на базовую организацию.',
switchLabel: 'Копия приложения, которая будет создана',
removeOriginal: 'Удалить исходное приложение',
switchStart: 'Переключиться',
typeSelector: {
all: 'ВСЕ типы',
chatbot: 'Чат-бот',
agent: 'Агент',
workflow: 'Рабочий процесс',
completion: 'Завершение',
},
tracing: {
title: 'Отслеживание производительности приложения',
description: 'Настройка стороннего поставщика LLMOps и отслеживание производительности приложения.',
config: 'Настройка',
view: 'Просмотр',
collapse: 'Свернуть',
expand: 'Развернуть',
tracing: 'Отслеживание',
disabled: 'Отключено',
disabledTip: 'Пожалуйста, сначала настройте провайдера LLM',
enabled: 'В работе',
tracingDescription: 'Запись полного контекста выполнения приложения, включая вызовы LLM, контекст, подсказки, HTTP-запросы и многое другое, на стороннюю платформу трассировки.',
configProviderTitle: {
configured: 'Настроено',
notConfigured: 'Настройте провайдера, чтобы включить трассировку',
moreProvider: 'Больше провайдеров',
},
langsmith: {
title: 'LangSmith',
description: 'Универсальная платформа для разработчиков для каждого этапа жизненного цикла приложения на базе LLM.',
},
langfuse: {
title: 'Langfuse',
description: 'Трассировка, оценка, управление подсказками и метрики для отладки и улучшения вашего приложения LLM.',
},
inUse: 'Используется',
configProvider: {
title: 'Настройка ',
placeholder: 'Введите ваш {{key}}',
project: 'Проект',
publicKey: 'Публичный ключ',
secretKey: 'Секретный ключ',
viewDocsLink: 'Посмотреть документацию {{key}}',
removeConfirmTitle: 'Удалить конфигурацию {{key}}?',
removeConfirmContent: 'Текущая конфигурация используется, ее удаление отключит функцию трассировки.',
},
},
}
export default translation

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