Compare commits

...

47 Commits
0.6.3 ... 0.6.4

Author SHA1 Message Date
b64080be1b version to 0.6.4 (#3670) 2024-04-22 12:13:31 +08:00
aadebd6d23 python 3.12 support (#3652) 2024-04-22 11:41:13 +08:00
71cc0074ef fix: delete tool parameters cache when sync draft workflow for run workflow use new parameter change in draft workflow (#3637) 2024-04-22 11:12:00 +08:00
d77f52bf85 Optimize README_CN (#3660) 2024-04-21 17:59:53 +08:00
b71163706b fix: workflow_run_id not log_id in workflow api doc (#3658) 2024-04-21 14:48:07 +08:00
1fb7df12d7 fix: in alembic's offline mode (db migrate with --sql option), skip data operations (#3533) 2024-04-21 09:44:35 +08:00
b3996b3221 Fix problem with scroll inside chat window (#3578) 2024-04-21 09:39:24 +08:00
7251748d59 fix: validate languages (#3638) 2024-04-20 10:50:10 +08:00
73e9f35ab1 feat: add file log (#3612)
Co-authored-by: liuzhenghua-jk <liuzhenghua-jk@360shuke.com>
2024-04-20 08:59:49 +08:00
d7f0056e2d Fix error in [Update yaml and py file in Tavily Tool] (#3465)
Co-authored-by: Yeuoly <admin@srmxy.cn>
2024-04-19 16:51:51 +08:00
9b7b133cbc content fix to continue (#3633)
Co-authored-by: xiaohan <fuck@qq.com>
2024-04-19 16:51:38 +08:00
7545e5de6c add-llama3-for-nvidia-api-catalog (#3631) 2024-04-19 14:51:22 +08:00
a0c30702c1 feat: moonshot fc (#3629) 2024-04-19 14:04:30 +08:00
03c988388e fix: chat rename (#3627) 2024-04-19 13:29:25 +08:00
0a56c522eb get dict key indexing_technique in DocumentAddByFileApi (#3615)
Co-authored-by: songqijun <songqijun@qipeng.com>
2024-04-19 09:37:11 +08:00
646858ea08 feat: Vision switch functionality is provided on OpenRouter (#3564) 2024-04-19 09:13:25 +08:00
d9b821cecc chore: apply ruff rules on tests and app.py (#3605) 2024-04-18 20:24:05 +08:00
d5448e07ab seucirty: http smuggling (#3609) 2024-04-18 18:18:42 +08:00
3aa182e26a fix: copy invite link has duplicated origin (#3608) 2024-04-18 17:56:07 +08:00
de3b490f8e Add mixtral 8x22b (#3606) 2024-04-18 17:44:22 +08:00
4481906be2 Feat/enterprise sso (#3602) 2024-04-18 17:33:32 +08:00
d9f1a8ce9f feat: stable diffusion 3 (#3599) 2024-04-18 16:54:37 +08:00
aa6d2e3035 fix(openai_api_compatible): fixing the error when converting chunk to json (#3570) 2024-04-18 16:54:16 +08:00
4365843c20 enhance:speedup xinference embedding & rerank (#3587) 2024-04-18 16:54:00 +08:00
b4d2d635f7 docs: Update README.md (#3577) 2024-04-18 13:55:42 +08:00
b9b28900b1 add-open-mixtral-8x22b (#3591) 2024-04-18 13:48:32 +08:00
d463b82aba test: add scripts for running tests on api module both locally and CI jobs (#3497) 2024-04-18 13:43:15 +08:00
ed861ff782 fix: json in raw text sometimes changed back to key value in HTTP node (#3586) 2024-04-18 12:08:18 +08:00
8cc1944160 Fix: use debounce for switch (#3585) 2024-04-18 11:54:54 +08:00
80e390b906 feat: add workflow api in Node.js sdk (#3584) 2024-04-18 11:23:18 +08:00
c2acb2be60 feat: code (#3557) 2024-04-18 08:00:02 +08:00
8ba95c08a1 added claude 3 opus (#3545) 2024-04-17 20:53:59 +08:00
c7de51ca9a enhance: preload general packages (#3567) 2024-04-17 19:49:53 +08:00
e02ee3bb2e fix event/stream ping (#3553) 2024-04-17 18:28:24 +08:00
394ceee141 optimize question classifier prompt and support keyword hit test (#3565) 2024-04-17 17:40:40 +08:00
40b48510f4 feat: economical index support retrieval testing (#3563) 2024-04-17 17:40:28 +08:00
be3b37114c fix: tool node show output text variable type error (#3556) 2024-04-17 15:26:18 +08:00
e212a87b86 fix: json-reader-json-output (#3552) 2024-04-17 14:09:42 +08:00
b890c11c14 feat: filter empty content messages in llm node (#3547) 2024-04-17 13:30:33 +08:00
2e27425e93 fix: workflow delete edge (#3541) 2024-04-17 11:09:43 +08:00
6269e011db fix: typo of PublishConfig (#3540) 2024-04-17 10:45:26 +08:00
e70482dfc0 feat: agent log (#3537)
Co-authored-by: jyong <718720800@qq.com>
2024-04-17 10:30:52 +08:00
9b8861e3e1 feat: increase read timeout of OpenAI Compatible API, Ollama, Nvidia LLM (#3538) 2024-04-17 09:25:50 +08:00
38ca3b29b5 add support for swagger object type (#3426)
Co-authored-by: lipeikui <lipeikui@3vjia.com>
2024-04-16 19:54:17 +08:00
066076b157 chore: lint .env file templates (#3507) 2024-04-16 19:53:54 +08:00
buu
be27ac0e69 fix: the hover style of the card-item operation button container (#3520) 2024-04-16 18:09:06 +08:00
9e6d4eeb92 fix the return with wrong datatype of segment (#3525) 2024-04-16 17:09:15 +08:00
236 changed files with 2864 additions and 427 deletions

View File

@ -8,6 +8,9 @@ on:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
env:
OPENAI_API_KEY: sk-IamNotARealKeyJustForMockTestKawaiiiiiiiiii
@ -37,10 +40,10 @@ jobs:
with:
packages: ffmpeg
- name: Set up Python
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: |
./api/requirements.txt
@ -50,10 +53,10 @@ jobs:
run: pip install -r ./api/requirements.txt -r ./api/requirements-dev.txt
- name: Run ModelRuntime
run: pytest api/tests/integration_tests/model_runtime/anthropic api/tests/integration_tests/model_runtime/azure_openai api/tests/integration_tests/model_runtime/openai api/tests/integration_tests/model_runtime/chatglm api/tests/integration_tests/model_runtime/google api/tests/integration_tests/model_runtime/xinference api/tests/integration_tests/model_runtime/huggingface_hub/test_llm.py
run: dev/pytest/pytest_model_runtime.sh
- name: Run Tool
run: pytest api/tests/integration_tests/tools/test_all_provider.py
run: dev/pytest/pytest_tools.sh
- name: Run Workflow
run: pytest api/tests/integration_tests/workflow
run: dev/pytest/pytest_workflow.sh

View File

@ -24,11 +24,14 @@ jobs:
python-version: '3.10'
- name: Python dependencies
run: pip install ruff
run: pip install ruff dotenv-linter
- name: Ruff check
run: ruff check ./api
- name: Dotenv check
run: dotenv-linter ./api/.env.example ./web/.env.example
- name: Lint hints
if: failure()
run: echo "Please run 'dev/reformat' to fix the fixable linting errors."

View File

@ -33,8 +33,8 @@
<a href="./README_CN.md"><img alt="Commits last month" src="https://img.shields.io/badge/简体中文-d9d9d9"></a>
<a href="./README_JA.md"><img alt="Commits last month" src="https://img.shields.io/badge/日本語-d9d9d9"></a>
<a href="./README_ES.md"><img alt="Commits last month" src="https://img.shields.io/badge/Español-d9d9d9"></a>
<a href="./README_KL.md"><img alt="Commits last month" src="https://img.shields.io/badge/Français-d9d9d9"></a>
<a href="./README_FR.md"><img alt="Commits last month" src="https://img.shields.io/badge/Klingon-d9d9d9"></a>
<a href="./README_FR.md"><img alt="Commits last month" src="https://img.shields.io/badge/Français-d9d9d9"></a>
<a href="./README_KL.md"><img alt="Commits last month" src="https://img.shields.io/badge/Klingon-d9d9d9"></a>
</p>
#

View File

@ -44,11 +44,11 @@
<a href="https://trendshift.io/repositories/2152" target="_blank"><img src="https://trendshift.io/api/badge/repositories/2152" alt="langgenius%2Fdify | 趋势转变" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
Dify 是一个开源的LLM应用开发平台。其直观的界面结合了AI工作流、RAG管道、代理功能、模型管理、可观性功能等,让您可以快速从原型到生产。以下是其核心功能列表:
Dify 是一个开源的 LLM 应用开发平台。其直观的界面结合了 AI 工作流、RAG 管道、Agent、模型管理、可观性功能等,让您可以快速从原型到生产。以下是其核心功能列表:
</br> </br>
**1. 工作流**:
视觉画布上构建和测试功能强大的AI工作流程利用以下所有功能以及更多功能。
在画布上构建和测试功能强大的 AI 工作流程,利用以下所有功能以及更多功能。
https://github.com/langgenius/dify/assets/13230914/356df23e-1604-483d-80a6-9517ece318aa
@ -56,7 +56,7 @@ Dify 是一个开源的LLM应用开发平台。其直观的界面结合了AI工
**2. 全面的模型支持**:
与数百种专有/开源LLMs以及数十种推理提供商和自托管解决方案无缝集成涵盖GPT、Mistral、Llama2以及任何与OpenAI API兼容的模型。完整的支持模型提供商列表可在[此处](https://docs.dify.ai/getting-started/readme/model-providers)找到。
与数百种专有/开源 LLMs 以及数十种推理提供商和自托管解决方案无缝集成,涵盖 GPT、Mistral、Llama3 以及任何与 OpenAI API 兼容的模型。完整的支持模型提供商列表可在[此处](https://docs.dify.ai/getting-started/readme/model-providers)找到。
![providers-v5](https://github.com/langgenius/dify/assets/13230914/5a17bdbe-097a-4100-8363-40255b70f6e3)
@ -65,16 +65,16 @@ Dify 是一个开源的LLM应用开发平台。其直观的界面结合了AI工
用于制作提示、比较模型性能以及向基于聊天的应用程序添加其他功能(如文本转语音)的直观界面。
**4. RAG Pipeline**:
广泛的RAG功能涵盖从文档摄入到检索的所有内容支持从PDF、PPT和其他常见文档格式中提取文本的开箱即用的支持。
广泛的 RAG 功能,涵盖从文档摄入到检索的所有内容,支持从 PDF、PPT 和其他常见文档格式中提取文本的开箱即用的支持。
**5. Agent 智能体**:
您可以基于LLM函数调用或ReAct定义代理,并为代理添加预构建或自定义工具。Dify为AI代理提供了50多种内置工具如谷歌搜索、DELL·E、稳定扩散和WolframAlpha等。
您可以基于 LLM 函数调用或 ReAct 定义 Agent并为 Agent 添加预构建或自定义工具。Dify 为 AI Agent 提供了50多种内置工具如谷歌搜索、DELL·E、Stable Diffusion 和 WolframAlpha 等。
**6. LLMOps**:
随时间监视和分析应用程序日志和性能。您可以根据生产数据和注持续改进提示、数据集和模型。
随时间监视和分析应用程序日志和性能。您可以根据生产数据和注持续改进提示、数据集和模型。
**7. 后端即服务**:
所有Dify的功能都带有相应的API因此您可以轻松地将Dify集成到自己的业务逻辑中。
所有 Dify 的功能都带有相应的 API因此您可以轻松地将 Dify 集成到自己的业务逻辑中。
## 功能比较
@ -84,21 +84,21 @@ Dify 是一个开源的LLM应用开发平台。其直观的界面结合了AI工
<th align="center">Dify.AI</th>
<th align="center">LangChain</th>
<th align="center">Flowise</th>
<th align="center">OpenAI助理API</th>
<th align="center">OpenAI Assistant API</th>
</tr>
<tr>
<td align="center">编程方法</td>
<td align="center">API + 应用程序导向</td>
<td align="center">Python代码</td>
<td align="center">Python 代码</td>
<td align="center">应用程序导向</td>
<td align="center">API导向</td>
<td align="center">API 导向</td>
</tr>
<tr>
<td align="center">支持的LLMs</td>
<td align="center">支持的 LLMs</td>
<td align="center">丰富多样</td>
<td align="center">丰富多样</td>
<td align="center">丰富多样</td>
<td align="center">仅限OpenAI</td>
<td align="center">仅限 OpenAI</td>
</tr>
<tr>
<td align="center">RAG引擎</td>
@ -108,21 +108,21 @@ Dify 是一个开源的LLM应用开发平台。其直观的界面结合了AI工
<td align="center"></td>
</tr>
<tr>
<td align="center">代理</td>
<td align="center">Agent</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">工作流</td>
<td align="center">工作流</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="center">可观</td>
<td align="center">可观</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
@ -202,7 +202,7 @@ docker compose up -d
## Contributing
对于那些想要贡献代码的人,请参阅我们的[贡献指南](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)。
同时请考虑通过社交媒体、活动和会议来支持Dify的分享。
同时,请考虑通过社交媒体、活动和会议来支持 Dify 的分享。
> 我们正在寻找贡献者来帮助将Dify翻译成除了中文和英文之外的其他语言。如果您有兴趣帮助请参阅我们的[i18n README](https://github.com/langgenius/dify/blob/main/web/i18n/README.md)获取更多信息,并在我们的[Discord社区服务器](https://discord.gg/8Tpq4AcN9c)的`global-users`频道中留言。

View File

@ -55,3 +55,16 @@
9. If you need to debug local async processing, please start the worker service by running
`celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail`.
The started celery app handles the async tasks, e.g. dataset importing and documents indexing.
## Testing
1. Install dependencies for both the backend and the test environment
```bash
pip install -r requirements.txt -r requirements-dev.txt
```
2. Run the tests locally with mocked system environment variables in `tool.pytest_env` section in `pyproject.toml`
```bash
dev/pytest/pytest_all_tests.sh
```

View File

@ -1,4 +1,6 @@
import os
import sys
from logging.handlers import RotatingFileHandler
if not os.environ.get("DEBUG") or os.environ.get("DEBUG").lower() != 'true':
from gevent import monkey
@ -17,10 +19,13 @@ import warnings
from flask import Flask, Response, request
from flask_cors import CORS
from werkzeug.exceptions import Unauthorized
from commands import register_commands
from config import CloudEditionConfig, Config
# DO NOT REMOVE BELOW
from events import event_handlers
from extensions import (
ext_celery,
ext_code_based_extension,
@ -37,11 +42,8 @@ from extensions import (
from extensions.ext_database import db
from extensions.ext_login import login_manager
from libs.passport import PassportService
from services.account_service import AccountService
# DO NOT REMOVE BELOW
from events import event_handlers
from models import account, dataset, model, source, task, tool, tools, web
from services.account_service import AccountService
# DO NOT REMOVE ABOVE
@ -86,7 +88,25 @@ def create_app(test_config=None) -> Flask:
app.secret_key = app.config['SECRET_KEY']
logging.basicConfig(level=app.config.get('LOG_LEVEL', 'INFO'))
log_handlers = None
log_file = app.config.get('LOG_FILE')
if log_file:
log_dir = os.path.dirname(log_file)
os.makedirs(log_dir, exist_ok=True)
log_handlers = [
RotatingFileHandler(
filename=log_file,
maxBytes=1024 * 1024 * 1024,
backupCount=5
),
logging.StreamHandler(sys.stdout)
]
logging.basicConfig(
level=app.config.get('LOG_LEVEL'),
format=app.config.get('LOG_FORMAT'),
datefmt=app.config.get('LOG_DATEFORMAT'),
handlers=log_handlers
)
initialize_extensions(app)
register_blueprints(app)
@ -115,7 +135,7 @@ def initialize_extensions(app):
@login_manager.request_loader
def load_user_from_request(request_from_flask_login):
"""Load user based on the request."""
if request.blueprint == 'console':
if request.blueprint in ['console', 'inner_api']:
# Check if the user_id contains a dot, indicating the old format
auth_header = request.headers.get('Authorization', '')
if not auth_header:
@ -151,6 +171,7 @@ def unauthorized_handler():
def register_blueprints(app):
from controllers.console import bp as console_app_bp
from controllers.files import bp as files_bp
from controllers.inner_api import bp as inner_api_bp
from controllers.service_api import bp as service_api_bp
from controllers.web import bp as web_bp
@ -188,6 +209,8 @@ def register_blueprints(app):
)
app.register_blueprint(files_bp)
app.register_blueprint(inner_api_bp)
# create app
app = create_app()

View File

@ -38,6 +38,9 @@ DEFAULTS = {
'QDRANT_CLIENT_TIMEOUT': 20,
'CELERY_BACKEND': 'database',
'LOG_LEVEL': 'INFO',
'LOG_FILE': '',
'LOG_FORMAT': '%(asctime)s.%(msecs)03d %(levelname)s [%(threadName)s] [%(filename)s:%(lineno)d] - %(message)s',
'LOG_DATEFORMAT': '%Y-%m-%d %H:%M:%S',
'HOSTED_OPENAI_QUOTA_LIMIT': 200,
'HOSTED_OPENAI_TRIAL_ENABLED': 'False',
'HOSTED_OPENAI_TRIAL_MODELS': 'gpt-3.5-turbo,gpt-3.5-turbo-1106,gpt-3.5-turbo-instruct,gpt-3.5-turbo-16k,gpt-3.5-turbo-16k-0613,gpt-3.5-turbo-0613,gpt-3.5-turbo-0125,text-davinci-003',
@ -69,6 +72,8 @@ DEFAULTS = {
'TOOL_ICON_CACHE_MAX_AGE': 3600,
'MILVUS_DATABASE': 'default',
'KEYWORD_DATA_SOURCE_TYPE': 'database',
'INNER_API': 'False',
'ENTERPRISE_ENABLED': 'False',
}
@ -99,12 +104,15 @@ class Config:
# ------------------------
# General Configurations.
# ------------------------
self.CURRENT_VERSION = "0.6.3"
self.CURRENT_VERSION = "0.6.4"
self.COMMIT_SHA = get_env('COMMIT_SHA')
self.EDITION = "SELF_HOSTED"
self.DEPLOY_ENV = get_env('DEPLOY_ENV')
self.TESTING = False
self.LOG_LEVEL = get_env('LOG_LEVEL')
self.LOG_FILE = get_env('LOG_FILE')
self.LOG_FORMAT = get_env('LOG_FORMAT')
self.LOG_DATEFORMAT = get_env('LOG_DATEFORMAT')
# The backend URL prefix of the console API.
# used to concatenate the login authorization callback or notion integration callback.
@ -133,6 +141,11 @@ class Config:
# Alternatively you can set it with `SECRET_KEY` environment variable.
self.SECRET_KEY = get_env('SECRET_KEY')
# Enable or disable the inner API.
self.INNER_API = get_bool_env('INNER_API')
# The inner API key is used to authenticate the inner API.
self.INNER_API_KEY = get_env('INNER_API_KEY')
# cors settings
self.CONSOLE_CORS_ALLOW_ORIGINS = get_cors_allow_origins(
'CONSOLE_CORS_ALLOW_ORIGINS', self.CONSOLE_WEB_URL)
@ -327,6 +340,8 @@ class Config:
self.TOOL_ICON_CACHE_MAX_AGE = get_env('TOOL_ICON_CACHE_MAX_AGE')
self.KEYWORD_DATA_SOURCE_TYPE = get_env('KEYWORD_DATA_SOURCE_TYPE')
self.ENTERPRISE_ENABLED = get_bool_env('ENTERPRISE_ENABLED')
class CloudEditionConfig(Config):

View File

@ -1,4 +1,3 @@
# -*- coding:utf-8 -*-

View File

@ -1,22 +1,57 @@
from flask import Blueprint
from libs.external_api import ExternalApi
bp = Blueprint('console', __name__, url_prefix='/console/api')
api = ExternalApi(bp)
# Import other controllers
from . import admin, apikey, extension, feature, setup, version, ping
from . import admin, apikey, extension, feature, ping, setup, version
# Import app controllers
from .app import (advanced_prompt_template, annotation, app, audio, completion, conversation, generator, message,
model_config, site, statistic, workflow, workflow_run, workflow_app_log, workflow_statistic, agent)
from .app import (
advanced_prompt_template,
agent,
annotation,
app,
audio,
completion,
conversation,
generator,
message,
model_config,
site,
statistic,
workflow,
workflow_app_log,
workflow_run,
workflow_statistic,
)
# Import auth controllers
from .auth import activate, data_source_oauth, login, oauth
# Import billing controllers
from .billing import billing
# Import datasets controllers
from .datasets import data_source, datasets, datasets_document, datasets_segments, file, hit_testing
# Import enterprise controllers
from .enterprise import enterprise_sso
# Import explore controllers
from .explore import (audio, completion, conversation, installed_app, message, parameter, recommended_app,
saved_message, workflow)
from .explore import (
audio,
completion,
conversation,
installed_app,
message,
parameter,
recommended_app,
saved_message,
workflow,
)
# Import workspace controllers
from .workspace import account, members, model_providers, models, tool_providers, workspace
from .workspace import account, members, model_providers, models, tool_providers, workspace

View File

@ -2,13 +2,15 @@ import json
from flask_login import current_user
from flask_restful import Resource, inputs, marshal_with, reqparse
from werkzeug.exceptions import Forbidden, BadRequest
from werkzeug.exceptions import BadRequest, Forbidden
from controllers.console import api
from controllers.console.app.wraps import get_app_model
from controllers.console.setup import setup_required
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
from core.agent.entities import AgentToolEntity
from core.tools.tool_manager import ToolManager
from core.tools.utils.configuration import ToolParameterConfigurationManager
from extensions.ext_database import db
from fields.app_fields import (
app_detail_fields,
@ -16,11 +18,8 @@ from fields.app_fields import (
app_pagination_fields,
)
from libs.login import login_required
from models.model import App, AppMode, AppModelConfig
from services.app_service import AppService
from models.model import App, AppModelConfig, AppMode
from core.tools.utils.configuration import ToolParameterConfigurationManager
from core.tools.tool_manager import ToolManager
ALLOW_CREATE_APP_MODES = ['chat', 'agent-chat', 'advanced-chat', 'workflow', 'completion']

View File

@ -26,10 +26,13 @@ class LoginApi(Resource):
try:
account = AccountService.authenticate(args['email'], args['password'])
except services.errors.account.AccountLoginError:
return {'code': 'unauthorized', 'message': 'Invalid email or password'}, 401
except services.errors.account.AccountLoginError as e:
return {'code': 'unauthorized', 'message': str(e)}, 401
TenantService.create_owner_tenant_if_not_exist(account)
# SELF_HOSTED only have one workspace
tenants = TenantService.get_join_tenants(account)
if len(tenants) == 0:
return {'result': 'fail', 'data': 'workspace not found, please contact system admin to invite you to join in a workspace'}
AccountService.update_last_login(account, request)

View File

@ -12,7 +12,7 @@ from controllers.console.app.error import (
ProviderNotInitializeError,
ProviderQuotaExceededError,
)
from controllers.console.datasets.error import DatasetNotInitializedError, HighQualityDatasetOnlyError
from controllers.console.datasets.error import DatasetNotInitializedError
from controllers.console.setup import setup_required
from controllers.console.wraps import account_initialization_required
from core.errors.error import (
@ -45,10 +45,6 @@ class HitTestingApi(Resource):
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
# only high quality dataset can be used for hit testing
if dataset.indexing_technique != 'high_quality':
raise HighQualityDatasetOnlyError()
parser = reqparse.RequestParser()
parser.add_argument('query', type=str, location='json')
parser.add_argument('retrieval_model', type=dict, required=False, location='json')

View File

@ -0,0 +1,59 @@
from flask import current_app, redirect
from flask_restful import Resource, reqparse
from controllers.console import api
from controllers.console.setup import setup_required
from services.enterprise.enterprise_sso_service import EnterpriseSSOService
class EnterpriseSSOSamlLogin(Resource):
@setup_required
def get(self):
return EnterpriseSSOService.get_sso_saml_login()
class EnterpriseSSOSamlAcs(Resource):
@setup_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('SAMLResponse', type=str, required=True, location='form')
args = parser.parse_args()
saml_response = args['SAMLResponse']
try:
token = EnterpriseSSOService.post_sso_saml_acs(saml_response)
return redirect(f'{current_app.config.get("CONSOLE_WEB_URL")}/signin?console_token={token}')
except Exception as e:
return redirect(f'{current_app.config.get("CONSOLE_WEB_URL")}/signin?message={str(e)}')
class EnterpriseSSOOidcLogin(Resource):
@setup_required
def get(self):
return EnterpriseSSOService.get_sso_oidc_login()
class EnterpriseSSOOidcCallback(Resource):
@setup_required
def get(self):
parser = reqparse.RequestParser()
parser.add_argument('state', type=str, required=True, location='args')
parser.add_argument('code', type=str, required=True, location='args')
parser.add_argument('oidc-state', type=str, required=True, location='cookies')
args = parser.parse_args()
try:
token = EnterpriseSSOService.get_sso_oidc_callback(args)
return redirect(f'{current_app.config.get("CONSOLE_WEB_URL")}/signin?console_token={token}')
except Exception as e:
return redirect(f'{current_app.config.get("CONSOLE_WEB_URL")}/signin?message={str(e)}')
api.add_resource(EnterpriseSSOSamlLogin, '/enterprise/sso/saml/login')
api.add_resource(EnterpriseSSOSamlAcs, '/enterprise/sso/saml/acs')
api.add_resource(EnterpriseSSOOidcLogin, '/enterprise/sso/oidc/login')
api.add_resource(EnterpriseSSOOidcCallback, '/enterprise/sso/oidc/callback')

View File

@ -1,6 +1,7 @@
from flask_login import current_user
from flask_restful import Resource
from services.enterprise.enterprise_feature_service import EnterpriseFeatureService
from services.feature_service import FeatureService
from . import api
@ -14,4 +15,10 @@ class FeatureApi(Resource):
return FeatureService.get_features(current_user.current_tenant_id).dict()
class EnterpriseFeatureApi(Resource):
def get(self):
return EnterpriseFeatureService.get_enterprise_features().dict()
api.add_resource(FeatureApi, '/features')
api.add_resource(EnterpriseFeatureApi, '/enterprise-features')

View File

@ -58,6 +58,8 @@ class SetupApi(Resource):
password=args['password']
)
TenantService.create_owner_tenant_if_not_exist(account)
setup()
AccountService.update_last_login(account, request)

View File

@ -3,6 +3,7 @@ import logging
from flask import request
from flask_login import current_user
from flask_restful import Resource, fields, inputs, marshal, marshal_with, reqparse
from werkzeug.exceptions import Unauthorized
import services
from controllers.console import api
@ -19,7 +20,7 @@ from controllers.console.wraps import account_initialization_required, cloud_edi
from extensions.ext_database import db
from libs.helper import TimestampField
from libs.login import login_required
from models.account import Tenant
from models.account import Tenant, TenantStatus
from services.account_service import TenantService
from services.file_service import FileService
from services.workspace_service import WorkspaceService
@ -116,6 +117,16 @@ class TenantApi(Resource):
tenant = current_user.current_tenant
if tenant.status == TenantStatus.ARCHIVE:
tenants = TenantService.get_join_tenants(current_user)
# if there is any tenant, switch to the first one
if len(tenants) > 0:
TenantService.switch_tenant(current_user, tenants[0].id)
tenant = tenants[0]
# else, raise Unauthorized
else:
raise Unauthorized('workspace is archived')
return WorkspaceService.get_tenant_info(tenant), 200

View File

@ -1,5 +1,5 @@
# -*- coding:utf-8 -*-
from flask import Blueprint
from libs.external_api import ExternalApi
bp = Blueprint('files', __name__)

View File

@ -0,0 +1,9 @@
from flask import Blueprint
from libs.external_api import ExternalApi
bp = Blueprint('inner_api', __name__, url_prefix='/inner/api')
api = ExternalApi(bp)
from .workspace import workspace

View File

@ -0,0 +1,37 @@
from flask_restful import Resource, reqparse
from controllers.console.setup import setup_required
from controllers.inner_api import api
from controllers.inner_api.wraps import inner_api_only
from events.tenant_event import tenant_was_created
from models.account import Account
from services.account_service import TenantService
class EnterpriseWorkspace(Resource):
@setup_required
@inner_api_only
def post(self):
parser = reqparse.RequestParser()
parser.add_argument('name', type=str, required=True, location='json')
parser.add_argument('owner_email', type=str, required=True, location='json')
args = parser.parse_args()
account = Account.query.filter_by(email=args['owner_email']).first()
if account is None:
return {
'message': 'owner account not found.'
}, 404
tenant = TenantService.create_tenant(args['name'])
TenantService.create_tenant_member(tenant, account, role='owner')
tenant_was_created.send(tenant)
return {
'message': 'enterprise workspace created.'
}
api.add_resource(EnterpriseWorkspace, '/enterprise/workspace')

View File

@ -0,0 +1,61 @@
from base64 import b64encode
from functools import wraps
from hashlib import sha1
from hmac import new as hmac_new
from flask import abort, current_app, request
from extensions.ext_database import db
from models.model import EndUser
def inner_api_only(view):
@wraps(view)
def decorated(*args, **kwargs):
if not current_app.config['INNER_API']:
abort(404)
# get header 'X-Inner-Api-Key'
inner_api_key = request.headers.get('X-Inner-Api-Key')
if not inner_api_key or inner_api_key != current_app.config['INNER_API_KEY']:
abort(404)
return view(*args, **kwargs)
return decorated
def inner_api_user_auth(view):
@wraps(view)
def decorated(*args, **kwargs):
if not current_app.config['INNER_API']:
return view(*args, **kwargs)
# get header 'X-Inner-Api-Key'
authorization = request.headers.get('Authorization')
if not authorization:
return view(*args, **kwargs)
parts = authorization.split(':')
if len(parts) != 2:
return view(*args, **kwargs)
user_id, token = parts
if ' ' in user_id:
user_id = user_id.split(' ')[1]
inner_api_key = request.headers.get('X-Inner-Api-Key')
data_to_sign = f'DIFY {user_id}'
signature = hmac_new(inner_api_key.encode('utf-8'), data_to_sign.encode('utf-8'), sha1)
signature = b64encode(signature.digest()).decode('utf-8')
if signature != token:
return view(*args, **kwargs)
kwargs['user'] = db.session.query(EndUser).filter(EndUser.id == user_id).first()
return view(*args, **kwargs)
return decorated

View File

@ -1,5 +1,5 @@
# -*- coding:utf-8 -*-
from flask import Blueprint
from libs.external_api import ExternalApi
bp = Blueprint('service_api', __name__, url_prefix='/v1')

View File

@ -174,7 +174,7 @@ class DocumentAddByFileApi(DatasetApiResource):
if not dataset:
raise ValueError('Dataset is not exist.')
if not dataset.indexing_technique and not args['indexing_technique']:
if not dataset.indexing_technique and not args.get('indexing_technique'):
raise ValueError('indexing_technique is required.')
# save file info

View File

@ -12,7 +12,7 @@ from werkzeug.exceptions import Forbidden, NotFound, Unauthorized
from extensions.ext_database import db
from libs.login import _get_user
from models.account import Account, Tenant, TenantAccountJoin
from models.account import Account, Tenant, TenantAccountJoin, TenantStatus
from models.model import ApiToken, App, EndUser
from services.feature_service import FeatureService
@ -47,6 +47,10 @@ def validate_app_token(view: Optional[Callable] = None, *, fetch_user_arg: Optio
if not app_model.enable_api:
raise NotFound()
tenant = db.session.query(Tenant).filter(Tenant.id == app_model.tenant_id).first()
if tenant.status == TenantStatus.ARCHIVE:
raise NotFound()
kwargs['app_model'] = app_model
if fetch_user_arg:
@ -137,6 +141,7 @@ def validate_dataset_token(view=None):
.filter(Tenant.id == api_token.tenant_id) \
.filter(TenantAccountJoin.tenant_id == Tenant.id) \
.filter(TenantAccountJoin.role.in_(['owner'])) \
.filter(Tenant.status == TenantStatus.NORMAL) \
.one_or_none() # TODO: only owner information is required, so only one is returned.
if tenant_account_join:
tenant, ta = tenant_account_join

View File

@ -1,5 +1,5 @@
# -*- coding:utf-8 -*-
from flask import Blueprint
from libs.external_api import ExternalApi
bp = Blueprint('web', __name__, url_prefix='/api')

View File

@ -7,7 +7,7 @@ from controllers.web import api
from controllers.web.error import AppUnavailableError
from controllers.web.wraps import WebApiResource
from extensions.ext_database import db
from models.model import App, AppModelConfig, AppMode
from models.model import App, AppMode, AppModelConfig
from models.tools import ApiToolProvider
from services.app_service import AppService

View File

@ -6,6 +6,7 @@ from werkzeug.exceptions import Forbidden
from controllers.web import api
from controllers.web.wraps import WebApiResource
from extensions.ext_database import db
from models.account import TenantStatus
from models.model import Site
from services.feature_service import FeatureService
@ -54,6 +55,9 @@ class AppSiteApi(WebApiResource):
if not site:
raise Forbidden()
if app_model.tenant.status == TenantStatus.ARCHIVE:
raise Forbidden()
can_replace_logo = FeatureService.get_features(app_model.tenant_id).can_replace_logo
return AppSiteInfo(app_model.tenant, app_model, site, end_user.id, can_replace_logo)

View File

@ -26,7 +26,10 @@ class AppGenerateResponseConverter(ABC):
else:
def _generate():
for chunk in cls.convert_stream_full_response(response):
yield f'data: {chunk}\n\n'
if chunk == 'ping':
yield f'event: {chunk}\n\n'
else:
yield f'data: {chunk}\n\n'
return _generate()
else:
@ -35,7 +38,10 @@ class AppGenerateResponseConverter(ABC):
else:
def _generate():
for chunk in cls.convert_stream_simple_response(response):
yield f'data: {chunk}\n\n'
if chunk == 'ping':
yield f'event: {chunk}\n\n'
else:
yield f'data: {chunk}\n\n'
return _generate()

View File

@ -84,7 +84,7 @@ class DatasetDocumentStore:
if not isinstance(doc, Document):
raise ValueError("doc must be a Document")
segment_document = self.get_document(doc_id=doc.metadata['doc_id'], raise_error=False)
segment_document = self.get_document_segment(doc_id=doc.metadata['doc_id'])
# NOTE: doc could already exist in the store, but we overwrite it
if not allow_update and segment_document:

View File

@ -30,34 +30,24 @@ class CodeExecutionResponse(BaseModel):
class CodeExecutor:
@classmethod
def execute_code(cls, language: Literal['python3', 'javascript', 'jinja2'], code: str, inputs: dict) -> dict:
def execute_code(cls, language: Literal['python3', 'javascript', 'jinja2'], preload: str, code: str) -> str:
"""
Execute code
:param language: code language
:param code: code
:param inputs: inputs
:return:
"""
template_transformer = None
if language == 'python3':
template_transformer = PythonTemplateTransformer
elif language == 'jinja2':
template_transformer = Jinja2TemplateTransformer
elif language == 'javascript':
template_transformer = NodeJsTemplateTransformer
else:
raise CodeExecutionException('Unsupported language')
runner, preload = template_transformer.transform_caller(code, inputs)
url = URL(CODE_EXECUTION_ENDPOINT) / 'v1' / 'sandbox' / 'run'
headers = {
'X-Api-Key': CODE_EXECUTION_API_KEY
}
data = {
'language': 'python3' if language == 'jinja2' else
'nodejs' if language == 'javascript' else
'python3' if language == 'python3' else None,
'code': runner,
'code': code,
'preload': preload
}
@ -85,4 +75,32 @@ class CodeExecutor:
if response.data.error:
raise CodeExecutionException(response.data.error)
return template_transformer.transform_response(response.data.stdout)
return response.data.stdout
@classmethod
def execute_workflow_code_template(cls, language: Literal['python3', 'javascript', 'jinja2'], code: str, inputs: dict) -> dict:
"""
Execute code
:param language: code language
:param code: code
:param inputs: inputs
:return:
"""
template_transformer = None
if language == 'python3':
template_transformer = PythonTemplateTransformer
elif language == 'jinja2':
template_transformer = Jinja2TemplateTransformer
elif language == 'javascript':
template_transformer = NodeJsTemplateTransformer
else:
raise CodeExecutionException('Unsupported language')
runner, preload = template_transformer.transform_caller(code, inputs)
try:
response = cls.execute_code(language, preload, runner)
except CodeExecutionException as e:
raise e
return template_transformer.transform_response(response)

View File

@ -20,8 +20,28 @@ result = f'''<<RESULT>>
print(result)
"""
PYTHON_PRELOAD = """"""
PYTHON_PRELOAD = """
# prepare general imports
import json
import datetime
import math
import random
import re
import string
import sys
import time
import traceback
import uuid
import os
import base64
import hashlib
import hmac
import binascii
import collections
import functools
import operator
import itertools
"""
class PythonTemplateTransformer(TemplateTransformer):
@classmethod

View File

@ -88,6 +88,14 @@ class PromptMessage(ABC, BaseModel):
content: Optional[str | list[PromptMessageContent]] = None
name: Optional[str] = None
def is_empty(self) -> bool:
"""
Check if prompt message is empty.
:return: True if prompt message is empty, False otherwise
"""
return not self.content
class UserPromptMessage(PromptMessage):
"""
@ -118,6 +126,16 @@ class AssistantPromptMessage(PromptMessage):
role: PromptMessageRole = PromptMessageRole.ASSISTANT
tool_calls: list[ToolCall] = []
def is_empty(self) -> bool:
"""
Check if prompt message is empty.
:return: True if prompt message is empty, False otherwise
"""
if not super().is_empty() and not self.tool_calls:
return False
return True
class SystemPromptMessage(PromptMessage):
"""
@ -132,3 +150,14 @@ class ToolPromptMessage(PromptMessage):
"""
role: PromptMessageRole = PromptMessageRole.TOOL
tool_call_id: str
def is_empty(self) -> bool:
"""
Check if prompt message is empty.
:return: True if prompt message is empty, False otherwise
"""
if not super().is_empty() and not self.tool_call_id:
return False
return True

View File

@ -0,0 +1,57 @@
model: anthropic.claude-3-opus-20240229-v1:0
label:
en_US: Claude 3 Opus
model_type: llm
features:
- agent-thought
- vision
model_properties:
mode: chat
context_size: 200000
# docs: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html
parameter_rules:
- name: max_tokens
use_template: max_tokens
required: true
type: int
default: 4096
min: 1
max: 4096
help:
zh_Hans: 停止前生成的最大令牌数。请注意Anthropic Claude 模型可能会在达到 max_tokens 的值之前停止生成令牌。不同的 Anthropic Claude 模型对此参数具有不同的最大值。
en_US: The maximum number of tokens to generate before stopping. Note that Anthropic Claude models might stop generating tokens before reaching the value of max_tokens. Different Anthropic Claude models have different maximum values for this parameter.
# docs: https://docs.anthropic.com/claude/docs/system-prompts
- name: temperature
use_template: temperature
required: false
type: float
default: 1
min: 0.0
max: 1.0
help:
zh_Hans: 生成内容的随机性。
en_US: The amount of randomness injected into the response.
- name: top_p
required: false
type: float
default: 0.999
min: 0.000
max: 1.000
help:
zh_Hans: 在核采样中Anthropic Claude 按概率递减顺序计算每个后续标记的所有选项的累积分布,并在达到 top_p 指定的特定概率时将其切断。您应该更改温度或top_p但不能同时更改两者。
en_US: In nucleus sampling, Anthropic Claude computes the cumulative distribution over all the options for each subsequent token in decreasing probability order and cuts it off once it reaches a particular probability specified by top_p. You should alter either temperature or top_p, but not both.
- name: top_k
required: false
type: int
default: 0
min: 0
# tip docs from aws has error, max value is 500
max: 500
help:
zh_Hans: 对于每个后续标记,仅从前 K 个选项中进行采样。使用 top_k 删除长尾低概率响应。
en_US: Only sample from the top K options for each subsequent token. Use top_k to remove long tail low probability responses.
pricing:
input: '0.015'
output: '0.075'
unit: '0.001'
currency: USD

View File

@ -1,5 +1,6 @@
- open-mistral-7b
- open-mixtral-8x7b
- open-mixtral-8x22b
- mistral-small-latest
- mistral-medium-latest
- mistral-large-latest

View File

@ -6,6 +6,7 @@ model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 32000
parameter_rules:
- name: temperature

View File

@ -6,6 +6,7 @@ model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 32000
parameter_rules:
- name: temperature

View File

@ -6,6 +6,7 @@ model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 32000
parameter_rules:
- name: temperature

View File

@ -6,6 +6,7 @@ model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 8000
parameter_rules:
- name: temperature

View File

@ -0,0 +1,51 @@
model: open-mixtral-8x22b
label:
zh_Hans: open-mixtral-8x22b
en_US: open-mixtral-8x22b
model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 64000
parameter_rules:
- name: temperature
use_template: temperature
default: 0.7
min: 0
max: 1
- name: top_p
use_template: top_p
default: 1
min: 0
max: 1
- name: max_tokens
use_template: max_tokens
default: 1024
min: 1
max: 8000
- name: safe_prompt
default: false
type: boolean
help:
en_US: Whether to inject a safety prompt before all conversations.
zh_Hans: 是否开启提示词审查
label:
en_US: SafePrompt
zh_Hans: 提示词审查
- name: random_seed
type: int
help:
en_US: The seed to use for random sampling. If set, different calls will generate deterministic results.
zh_Hans: 当开启随机数种子以后,你可以通过指定一个固定的种子来使得回答结果更加稳定
label:
en_US: RandomSeed
zh_Hans: 随机数种子
default: 0
min: 0
max: 2147483647
pricing:
input: '0.002'
output: '0.006'
unit: '0.001'
currency: USD

View File

@ -6,6 +6,7 @@ model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 32000
parameter_rules:
- name: temperature

View File

@ -5,6 +5,9 @@ label:
model_type: llm
features:
- agent-thought
- tool-call
- multi-tool-call
- stream-tool-call
model_properties:
mode: chat
context_size: 128000

View File

@ -5,6 +5,9 @@ label:
model_type: llm
features:
- agent-thought
- tool-call
- multi-tool-call
- stream-tool-call
model_properties:
mode: chat
context_size: 32000

View File

@ -5,6 +5,9 @@ label:
model_type: llm
features:
- agent-thought
- tool-call
- multi-tool-call
- stream-tool-call
model_properties:
mode: chat
context_size: 8192

View File

@ -1,5 +1,7 @@
- google/gemma-7b
- google/codegemma-7b
- meta/llama2-70b
- meta/llama3-8b
- meta/llama3-70b
- mistralai/mixtral-8x7b-instruct-v0.1
- fuyu-8b

View File

@ -11,13 +11,19 @@ model_properties:
parameter_rules:
- name: temperature
use_template: temperature
min: 0
max: 1
default: 0.5
- name: top_p
use_template: top_p
min: 0
max: 1
default: 1
- name: max_tokens
use_template: max_tokens
default: 1024
min: 1
max: 1024
default: 1024
- name: frequency_penalty
use_template: frequency_penalty
min: -2

View File

@ -22,6 +22,6 @@ parameter_rules:
max: 1
- name: max_tokens
use_template: max_tokens
default: 512
default: 1024
min: 1
max: 1024

View File

@ -11,13 +11,19 @@ model_properties:
parameter_rules:
- name: temperature
use_template: temperature
min: 0
max: 1
default: 0.5
- name: top_p
use_template: top_p
min: 0
max: 1
default: 1
- name: max_tokens
use_template: max_tokens
default: 512
min: 1
max: 1024
default: 1024
- name: frequency_penalty
use_template: frequency_penalty
min: -2

View File

@ -7,17 +7,23 @@ features:
- agent-thought
model_properties:
mode: chat
context_size: 32768
context_size: 4096
parameter_rules:
- name: temperature
use_template: temperature
min: 0
max: 1
default: 0.5
- name: top_p
use_template: top_p
min: 0
max: 1
default: 1
- name: max_tokens
use_template: max_tokens
default: 512
min: 1
max: 1024
default: 1024
- name: frequency_penalty
use_template: frequency_penalty
min: -2

View File

@ -0,0 +1,36 @@
model: meta/llama3-70b
label:
zh_Hans: meta/llama3-70b
en_US: meta/llama3-70b
model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 8192
parameter_rules:
- name: temperature
use_template: temperature
min: 0
max: 1
default: 0.5
- name: top_p
use_template: top_p
min: 0
max: 1
default: 1
- name: max_tokens
use_template: max_tokens
min: 1
max: 1024
default: 1024
- name: frequency_penalty
use_template: frequency_penalty
min: -2
max: 2
default: 0
- name: presence_penalty
use_template: presence_penalty
min: -2
max: 2
default: 0

View File

@ -0,0 +1,36 @@
model: meta/llama3-8b
label:
zh_Hans: meta/llama3-8b
en_US: meta/llama3-8b
model_type: llm
features:
- agent-thought
model_properties:
mode: chat
context_size: 8192
parameter_rules:
- name: temperature
use_template: temperature
min: 0
max: 1
default: 0.5
- name: top_p
use_template: top_p
min: 0
max: 1
default: 1
- name: max_tokens
use_template: max_tokens
min: 1
max: 1024
default: 1024
- name: frequency_penalty
use_template: frequency_penalty
min: -2
max: 2
default: 0
- name: presence_penalty
use_template: presence_penalty
min: -2
max: 2
default: 0

View File

@ -25,7 +25,10 @@ class NVIDIALargeLanguageModel(OAIAPICompatLargeLanguageModel):
'mistralai/mixtral-8x7b-instruct-v0.1': '',
'google/gemma-7b': '',
'google/codegemma-7b': '',
'meta/llama2-70b': ''
'meta/llama2-70b': '',
'meta/llama3-8b': '',
'meta/llama3-70b': ''
}
def _invoke(self, model: str, credentials: dict,
@ -131,7 +134,7 @@ class NVIDIALargeLanguageModel(OAIAPICompatLargeLanguageModel):
endpoint_url,
headers=headers,
json=data,
timeout=(10, 60)
timeout=(10, 300)
)
if response.status_code != 200:
@ -232,7 +235,7 @@ class NVIDIALargeLanguageModel(OAIAPICompatLargeLanguageModel):
endpoint_url,
headers=headers,
json=data,
timeout=(10, 60),
timeout=(10, 300),
stream=stream
)

View File

@ -11,13 +11,19 @@ model_properties:
parameter_rules:
- name: temperature
use_template: temperature
min: 0
max: 1
default: 0.5
- name: top_p
use_template: top_p
min: 0
max: 1
default: 1
- name: max_tokens
use_template: max_tokens
default: 512
min: 1
max: 1024
default: 1024
- name: frequency_penalty
use_template: frequency_penalty
min: -2

View File

@ -1,6 +1,9 @@
provider: nvidia
label:
en_US: API Catalog
description:
en_US: API Catalog
zh_Hans: API Catalog
icon_small:
en_US: icon_s_en.svg
icon_large:

View File

@ -201,7 +201,7 @@ class OllamaLargeLanguageModel(LargeLanguageModel):
endpoint_url,
headers=headers,
json=data,
timeout=(10, 60),
timeout=(10, 300),
stream=stream
)

View File

@ -138,7 +138,7 @@ class OAIAPICompatLargeLanguageModel(_CommonOAI_API_Compat, LargeLanguageModel):
endpoint_url,
headers=headers,
json=data,
timeout=(10, 60)
timeout=(10, 300)
)
if response.status_code != 200:
@ -154,7 +154,7 @@ class OAIAPICompatLargeLanguageModel(_CommonOAI_API_Compat, LargeLanguageModel):
json_result['object'] = 'chat.completion'
elif (completion_type is LLMMode.COMPLETION and json_result['object'] == ''):
json_result['object'] = 'text_completion'
if (completion_type is LLMMode.CHAT
and ('object' not in json_result or json_result['object'] != 'chat.completion')):
raise CredentialsValidateFailedError(
@ -334,7 +334,7 @@ class OAIAPICompatLargeLanguageModel(_CommonOAI_API_Compat, LargeLanguageModel):
endpoint_url,
headers=headers,
json=data,
timeout=(10, 60),
timeout=(10, 300),
stream=stream
)
@ -425,6 +425,7 @@ class OAIAPICompatLargeLanguageModel(_CommonOAI_API_Compat, LargeLanguageModel):
finish_reason = 'Unknown'
for chunk in response.iter_lines(decode_unicode=True, delimiter=delimiter):
chunk = chunk.strip()
if chunk:
# ignore sse comments
if chunk.startswith(':'):

View File

@ -73,3 +73,22 @@ model_credential_schema:
value: llm
default: "4096"
type: text-input
- variable: vision_support
show_on:
- variable: __model_type
value: llm
label:
zh_Hans: 是否支持 Vision
en_US: Vision Support
type: radio
required: false
default: 'no_support'
options:
- value: 'support'
label:
en_US: 'Yes'
zh_Hans:
- value: 'no_support'
label:
en_US: 'No'
zh_Hans:

View File

@ -47,17 +47,8 @@ class XinferenceRerankModel(RerankModel):
if credentials['server_url'].endswith('/'):
credentials['server_url'] = credentials['server_url'][:-1]
# initialize client
client = Client(
base_url=credentials['server_url']
)
xinference_client = client.get_model(model_uid=credentials['model_uid'])
if not isinstance(xinference_client, RESTfulRerankModelHandle):
raise InvokeBadRequestError('please check model type, the model you want to invoke is not a rerank model')
response = xinference_client.rerank(
handle = RESTfulRerankModelHandle(credentials['model_uid'], credentials['server_url'],auth_headers={})
response = handle.rerank(
documents=docs,
query=query,
top_n=top_n,
@ -97,6 +88,20 @@ class XinferenceRerankModel(RerankModel):
try:
if "/" in credentials['model_uid'] or "?" in credentials['model_uid'] or "#" in credentials['model_uid']:
raise CredentialsValidateFailedError("model_uid should not contain /, ?, or #")
if credentials['server_url'].endswith('/'):
credentials['server_url'] = credentials['server_url'][:-1]
# initialize client
client = Client(
base_url=credentials['server_url']
)
xinference_client = client.get_model(model_uid=credentials['model_uid'])
if not isinstance(xinference_client, RESTfulRerankModelHandle):
raise InvokeBadRequestError(
'please check model type, the model you want to invoke is not a rerank model')
self.invoke(
model=model,
@ -157,4 +162,4 @@ class XinferenceRerankModel(RerankModel):
parameter_rules=[]
)
return entity
return entity

View File

@ -47,17 +47,8 @@ class XinferenceTextEmbeddingModel(TextEmbeddingModel):
if server_url.endswith('/'):
server_url = server_url[:-1]
client = Client(base_url=server_url)
try:
handle = client.get_model(model_uid=model_uid)
except RuntimeError as e:
raise InvokeAuthorizationError(e)
if not isinstance(handle, RESTfulEmbeddingModelHandle):
raise InvokeBadRequestError('please check model type, the model you want to invoke is not a text embedding model')
try:
handle = RESTfulEmbeddingModelHandle(model_uid, server_url, auth_headers={})
embeddings = handle.create_embedding(input=texts)
except RuntimeError as e:
raise InvokeServerUnavailableError(e)
@ -122,6 +113,18 @@ class XinferenceTextEmbeddingModel(TextEmbeddingModel):
if extra_args.max_tokens:
credentials['max_tokens'] = extra_args.max_tokens
if server_url.endswith('/'):
server_url = server_url[:-1]
client = Client(base_url=server_url)
try:
handle = client.get_model(model_uid=model_uid)
except RuntimeError as e:
raise InvokeAuthorizationError(e)
if not isinstance(handle, RESTfulEmbeddingModelHandle):
raise InvokeBadRequestError('please check model type, the model you want to invoke is not a text embedding model')
self._invoke(model=model, credentials=credentials, texts=['ping'])
except InvokeAuthorizationError as e:
@ -198,4 +201,4 @@ class XinferenceTextEmbeddingModel(TextEmbeddingModel):
parameter_rules=[]
)
return entity
return entity

View File

@ -1,6 +1,15 @@
from .__version__ import __version__
from ._client import ZhipuAI
from .core._errors import (APIAuthenticationError, APIInternalError, APIReachLimitError, APIRequestFailedError,
APIResponseError, APIResponseValidationError, APIServerFlowExceedError, APIStatusError,
APITimeoutError, ZhipuAIError)
from .core._errors import (
APIAuthenticationError,
APIInternalError,
APIReachLimitError,
APIRequestFailedError,
APIResponseError,
APIResponseValidationError,
APIServerFlowExceedError,
APIStatusError,
APITimeoutError,
ZhipuAIError,
)

View File

@ -4,6 +4,7 @@
- searxng
- dalle
- azuredalle
- stability
- wikipedia
- model.openai
- model.google
@ -17,6 +18,7 @@
- model.zhipuai
- aippt
- youtube
- code
- wolframalpha
- maths
- github

View File

@ -0,0 +1 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" data-icon="Code" aria-hidden="true"><g id="icons/code"><path id="Vector (Stroke)" fill-rule="evenodd" clip-rule="evenodd" d="M8.32593 1.69675C8.67754 1.78466 8.89132 2.14096 8.80342 2.49257L6.47009 11.8259C6.38218 12.1775 6.02588 12.3913 5.67427 12.3034C5.32265 12.2155 5.10887 11.8592 5.19678 11.5076L7.53011 2.17424C7.61801 1.82263 7.97431 1.60885 8.32593 1.69675ZM3.96414 4.20273C4.22042 4.45901 4.22042 4.87453 3.96413 5.13081L2.45578 6.63914C2.45577 6.63915 2.45578 6.63914 2.45578 6.63914C2.25645 6.83851 2.25643 7.16168 2.45575 7.36103C2.45574 7.36103 2.45576 7.36104 2.45575 7.36103L3.96413 8.86936C4.22041 9.12564 4.22042 9.54115 3.96414 9.79744C3.70787 10.0537 3.29235 10.0537 3.03607 9.79745L1.52769 8.28913C0.815811 7.57721 0.815803 6.42302 1.52766 5.7111L3.03606 4.20272C3.29234 3.94644 3.70786 3.94644 3.96414 4.20273ZM10.0361 4.20273C10.2923 3.94644 10.7078 3.94644 10.9641 4.20272L12.4725 5.71108C13.1843 6.423 13.1844 7.57717 12.4725 8.28909L10.9641 9.79745C10.7078 10.0537 10.2923 10.0537 10.036 9.79744C9.77977 9.54115 9.77978 9.12564 10.0361 8.86936L11.5444 7.36107C11.7437 7.16172 11.7438 6.83854 11.5444 6.63917C11.5444 6.63915 11.5445 6.63918 11.5444 6.63917L10.0361 5.13081C9.77978 4.87453 9.77978 4.45901 10.0361 4.20273Z" fill="currentColor"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,8 @@
from typing import Any
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
class CodeToolProvider(BuiltinToolProviderController):
def _validate_credentials(self, credentials: dict[str, Any]) -> None:
pass

View File

@ -0,0 +1,13 @@
identity:
author: Dify
name: code
label:
en_US: Code Interpreter
zh_Hans: 代码解释器
pt_BR: Interpretador de Código
description:
en_US: Run a piece of code and get the result back.
zh_Hans: 运行一段代码并返回结果。
pt_BR: Execute um trecho de código e obtenha o resultado de volta.
icon: icon.svg
credentials_for_provider:

View File

@ -0,0 +1,22 @@
from typing import Any
from core.helper.code_executor.code_executor import CodeExecutor
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
class SimpleCode(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage | list[ToolInvokeMessage]:
"""
invoke simple code
"""
language = tool_parameters.get('language', 'python3')
code = tool_parameters.get('code', '')
if language not in ['python3', 'javascript']:
raise ValueError(f'Only python3 and javascript are supported, not {language}')
result = CodeExecutor.execute_code(language, '', code)
return self.create_text_message(result)

View File

@ -0,0 +1,51 @@
identity:
name: simple_code
author: Dify
label:
en_US: Code Interpreter
zh_Hans: 代码解释器
pt_BR: Interpretador de Código
description:
human:
en_US: Run code and get the result back, when you're using a lower quality model, please make sure there are some tips help LLM to understand how to write the code.
zh_Hans: 运行一段代码并返回结果当您使用较低质量的模型时请确保有一些提示帮助LLM理解如何编写代码。
pt_BR: Execute um trecho de código e obtenha o resultado de volta, quando você estiver usando um modelo de qualidade inferior, certifique-se de que existam algumas dicas para ajudar o LLM a entender como escrever o código.
llm: A tool for running code and getting the result back, but only native packages are allowed, network/IO operations are disabled. and you must use print() or console.log() to output the result or result will be empty.
parameters:
- name: language
type: string
required: true
label:
en_US: Language
zh_Hans: 语言
pt_BR: Idioma
human_description:
en_US: The programming language of the code
zh_Hans: 代码的编程语言
pt_BR: A linguagem de programação do código
llm_description: language of the code, only "python3" and "javascript" are supported
form: llm
options:
- value: python3
label:
en_US: Python3
zh_Hans: Python3
pt_BR: Python3
- value: javascript
label:
en_US: JavaScript
zh_Hans: JavaScript
pt_BR: JavaScript
- name: code
type: string
required: true
label:
en_US: Code
zh_Hans: 代码
pt_BR: Código
human_description:
en_US: The code to be executed
zh_Hans: 要执行的代码
pt_BR: O código a ser executado
llm_description: code to be executed, only native packages are allowed, network/IO operations are disabled.
form: llm

View File

@ -20,7 +20,7 @@ class JinaReaderTool(BuiltinTool):
url = tool_parameters['url']
headers = {
'Accept': 'text/event-stream'
'Accept': 'application/json'
}
response = ssrf_proxy.get(

View File

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none">
<path d="M12.0377 35C19.1243 35 23.7343 31.3 23.7343 25.7333C23.7343 21.4167 20.931 18.6733 15.9177 17.5367L12.701 16.585C9.87768 15.96 8.22935 15.21 8.61768 13.2933C8.94102 11.6983 9.90602 10.7983 12.1543 10.7983C19.296 10.7983 21.9427 13.2933 21.9427 13.2933V7.29333C21.9427 7.29333 19.366 5 12.1543 5C5.35435 5 1.66602 8.45 1.66602 13.7883C1.66602 18.105 4.22268 20.6167 9.40768 21.8083L9.96435 21.9467C10.7527 22.1867 11.8177 22.505 13.1577 22.9C15.8077 23.525 16.4893 24.1883 16.4893 26.1767C16.4893 27.9933 14.5727 29.0267 12.0393 29.0267C4.73435 29.0267 1.66602 25.385 1.66602 25.385V32.0333C1.66602 32.0333 3.58602 35 12.0377 35Z" fill="url(#paint0_linear_17756_15767)"/>
<path d="M33.9561 34.55C36.4645 34.55 38.3328 32.7617 38.3328 30.34C38.3328 27.8667 36.5178 26.13 33.9561 26.13C31.4478 26.13 29.6328 27.8667 29.6328 30.34C29.6328 32.8133 31.4478 34.55 33.9561 34.55Z" fill="#E80000"/>
<defs>
<linearGradient id="paint0_linear_17756_15767" x1="1105.08" y1="5" x2="1105.08" y2="3005" gradientUnits="userSpaceOnUse">
<stop stop-color="#9D39FF"/>
<stop offset="1" stop-color="#A380FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,15 @@
from typing import Any
from core.tools.provider.builtin.stability.tools.base import BaseStabilityAuthorization
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
class StabilityToolProvider(BuiltinToolProviderController, BaseStabilityAuthorization):
"""
This class is responsible for providing the stability tool.
"""
def _validate_credentials(self, credentials: dict[str, Any]) -> None:
"""
This method is responsible for validating the credentials.
"""
self.sd_validate_credentials(credentials)

View File

@ -0,0 +1,29 @@
identity:
author: Dify
name: stability
label:
en_US: Stability
zh_Hans: Stability
pt_BR: Stability
description:
en_US: Activating humanity's potential through generative AI
zh_Hans: 通过生成式 AI 激活人类的潜力
pt_BR: Activating humanity's potential through generative AI
icon: icon.svg
credentials_for_provider:
api_key:
type: secret-input
required: true
label:
en_US: API key
zh_Hans: API key
pt_BR: API key
placeholder:
en_US: Please input your API key
zh_Hans: 请输入你的 API key
pt_BR: Please input your API key
help:
en_US: Get your API key from Stability
zh_Hans: 从 Stability 获取你的 API key
pt_BR: Get your API key from Stability
url: https://platform.stability.ai/account/keys

View File

@ -0,0 +1,34 @@
import requests
from yarl import URL
from core.tools.errors import ToolProviderCredentialValidationError
class BaseStabilityAuthorization:
def sd_validate_credentials(self, credentials: dict):
"""
This method is responsible for validating the credentials.
"""
api_key = credentials.get('api_key', '')
if not api_key:
raise ToolProviderCredentialValidationError('API key is required.')
response = requests.get(
URL('https://api.stability.ai') / 'v1' / 'user' / 'account',
headers=self.generate_authorization_headers(credentials),
timeout=(5, 30)
)
if not response.ok:
raise ToolProviderCredentialValidationError('Invalid API key.')
return True
def generate_authorization_headers(self, credentials: dict) -> dict[str, str]:
"""
This method is responsible for generating the authorization headers.
"""
return {
'Authorization': f'Bearer {credentials.get("api_key", "")}'
}

View File

@ -0,0 +1,60 @@
from typing import Any
from httpx import post
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.provider.builtin.stability.tools.base import BaseStabilityAuthorization
from core.tools.tool.builtin_tool import BuiltinTool
class StableDiffusionTool(BuiltinTool, BaseStabilityAuthorization):
"""
This class is responsible for providing the stable diffusion tool.
"""
model_endpoint_map = {
'sd3': 'https://api.stability.ai/v2beta/stable-image/generate/sd3',
'sd3-turbo': 'https://api.stability.ai/v2beta/stable-image/generate/sd3',
'core': 'https://api.stability.ai/v2beta/stable-image/generate/core',
}
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage | list[ToolInvokeMessage]:
"""
Invoke the tool.
"""
payload = {
'prompt': tool_parameters.get('prompt', ''),
'aspect_radio': tool_parameters.get('aspect_radio', '16:9'),
'mode': 'text-to-image',
'seed': tool_parameters.get('seed', 0),
'output_format': 'png',
}
model = tool_parameters.get('model', 'core')
if model in ['sd3', 'sd3-turbo']:
payload['model'] = tool_parameters.get('model')
if not model == 'sd3-turbo':
payload['negative_prompt'] = tool_parameters.get('negative_prompt', '')
response = post(
self.model_endpoint_map[tool_parameters.get('model', 'core')],
headers={
'accept': 'image/*',
**self.generate_authorization_headers(self.runtime.credentials),
},
files={
key: (None, str(value)) for key, value in payload.items()
},
timeout=(5, 30)
)
if not response.status_code == 200:
raise Exception(response.text)
return self.create_blob_message(
blob=response.content, meta={
'mime_type': 'image/png'
},
save_as=self.VARIABLE_KEY.IMAGE.value
)

View File

@ -0,0 +1,142 @@
identity:
name: stability_text2image
author: Dify
label:
en_US: StableDiffusion
zh_Hans: 稳定扩散
pt_BR: StableDiffusion
description:
human:
en_US: A tool for generate images based on the text input
zh_Hans: 一个基于文本输入生成图像的工具
pt_BR: A tool for generate images based on the text input
llm: A tool for generate images based on the text input
parameters:
- name: prompt
type: string
required: true
label:
en_US: Prompt
zh_Hans: 提示词
pt_BR: Prompt
human_description:
en_US: used for generating images
zh_Hans: 用于生成图像
pt_BR: used for generating images
llm_description: key words for generating images
form: llm
- name: model
type: select
default: sd3-turbo
required: true
label:
en_US: Model
zh_Hans: 模型
pt_BR: Model
options:
- value: core
label:
en_US: Core
zh_Hans: Core
pt_BR: Core
- value: sd3
label:
en_US: Stable Diffusion 3
zh_Hans: Stable Diffusion 3
pt_BR: Stable Diffusion 3
- value: sd3-turbo
label:
en_US: Stable Diffusion 3 Turbo
zh_Hans: Stable Diffusion 3 Turbo
pt_BR: Stable Diffusion 3 Turbo
human_description:
en_US: Model for generating images
zh_Hans: 用于生成图像的模型
pt_BR: Model for generating images
llm_description: Model for generating images
form: form
- name: negative_prompt
type: string
default: bad art, ugly, deformed, watermark, duplicated, discontinuous lines
required: false
label:
en_US: Negative Prompt
zh_Hans: 负面提示
pt_BR: Negative Prompt
human_description:
en_US: Negative Prompt
zh_Hans: 负面提示
pt_BR: Negative Prompt
llm_description: Negative Prompt
form: form
- name: seeds
type: number
default: 0
required: false
label:
en_US: Seeds
zh_Hans: 种子
pt_BR: Seeds
human_description:
en_US: Seeds
zh_Hans: 种子
pt_BR: Seeds
llm_description: Seeds
min: 0
max: 4294967294
form: form
- name: aspect_radio
type: select
default: '16:9'
options:
- value: '16:9'
label:
en_US: '16:9'
zh_Hans: '16:9'
pt_BR: '16:9'
- value: '1:1'
label:
en_US: '1:1'
zh_Hans: '1:1'
pt_BR: '1:1'
- value: '21:9'
label:
en_US: '21:9'
zh_Hans: '21:9'
pt_BR: '21:9'
- value: '2:3'
label:
en_US: '2:3'
zh_Hans: '2:3'
pt_BR: '2:3'
- value: '4:5'
label:
en_US: '4:5'
zh_Hans: '4:5'
pt_BR: '4:5'
- value: '5:4'
label:
en_US: '5:4'
zh_Hans: '5:4'
pt_BR: '5:4'
- value: '9:16'
label:
en_US: '9:16'
zh_Hans: '9:16'
pt_BR: '9:16'
- value: '9:21'
label:
en_US: '9:21'
zh_Hans: '9:21'
pt_BR: '9:21'
required: false
label:
en_US: Aspect Radio
zh_Hans: 长宽比
pt_BR: Aspect Radio
human_description:
en_US: Aspect Radio
zh_Hans: 长宽比
pt_BR: Aspect Radio
llm_description: Aspect Radio
form: form

View File

@ -16,6 +16,13 @@ class TavilyProvider(BuiltinToolProviderController):
user_id='',
tool_parameters={
"query": "Sachin Tendulkar",
"search_depth": "basic",
"include_answer": True,
"include_images": False,
"include_raw_content": False,
"max_results": 5,
"include_domains": "",
"exclude_domains": ""
},
)
except Exception as e:

View File

@ -1,4 +1,4 @@
from typing import Any, Optional
from typing import Any
import requests
@ -24,87 +24,43 @@ class TavilySearch:
def __init__(self, api_key: str) -> None:
self.api_key = api_key
def raw_results(
self,
query: str,
max_results: Optional[int] = 3,
search_depth: Optional[str] = "advanced",
include_domains: Optional[list[str]] = [],
exclude_domains: Optional[list[str]] = [],
include_answer: Optional[bool] = False,
include_raw_content: Optional[bool] = False,
include_images: Optional[bool] = False,
) -> dict:
def raw_results(self, params: dict[str, Any]) -> dict:
"""
Retrieves raw search results from the Tavily Search API.
Args:
query (str): The search query.
max_results (int, optional): The maximum number of results to retrieve. Defaults to 3.
search_depth (str, optional): The search depth. Defaults to "advanced".
include_domains (List[str], optional): The domains to include in the search. Defaults to [].
exclude_domains (List[str], optional): The domains to exclude from the search. Defaults to [].
include_answer (bool, optional): Whether to include answer in the search results. Defaults to False.
include_raw_content (bool, optional): Whether to include raw content in the search results. Defaults to False.
include_images (bool, optional): Whether to include images in the search results. Defaults to False.
params (Dict[str, Any]): The search parameters.
Returns:
dict: The raw search results.
"""
params = {
"api_key": self.api_key,
"query": query,
"max_results": max_results,
"search_depth": search_depth,
"include_domains": include_domains,
"exclude_domains": exclude_domains,
"include_answer": include_answer,
"include_raw_content": include_raw_content,
"include_images": include_images,
}
params["api_key"] = self.api_key
if 'exclude_domains' in params and isinstance(params['exclude_domains'], str) and params['exclude_domains'] != 'None':
params['exclude_domains'] = params['exclude_domains'].split()
else:
params['exclude_domains'] = []
if 'include_domains' in params and isinstance(params['include_domains'], str) and params['include_domains'] != 'None':
params['include_domains'] = params['include_domains'].split()
else:
params['include_domains'] = []
response = requests.post(f"{TAVILY_API_URL}/search", json=params)
response.raise_for_status()
return response.json()
def results(
self,
query: str,
max_results: Optional[int] = 3,
search_depth: Optional[str] = "advanced",
include_domains: Optional[list[str]] = [],
exclude_domains: Optional[list[str]] = [],
include_answer: Optional[bool] = False,
include_raw_content: Optional[bool] = False,
include_images: Optional[bool] = False,
) -> list[dict]:
def results(self, params: dict[str, Any]) -> list[dict]:
"""
Retrieves cleaned search results from the Tavily Search API.
Args:
query (str): The search query.
max_results (int, optional): The maximum number of results to retrieve. Defaults to 3.
search_depth (str, optional): The search depth. Defaults to "advanced".
include_domains (List[str], optional): The domains to include in the search. Defaults to [].
exclude_domains (List[str], optional): The domains to exclude from the search. Defaults to [].
include_answer (bool, optional): Whether to include answer in the search results. Defaults to False.
include_raw_content (bool, optional): Whether to include raw content in the search results. Defaults to False.
include_images (bool, optional): Whether to include images in the search results. Defaults to False.
params (Dict[str, Any]): The search parameters.
Returns:
list: The cleaned search results.
"""
raw_search_results = self.raw_results(
query,
max_results=max_results,
search_depth=search_depth,
include_domains=include_domains,
exclude_domains=exclude_domains,
include_answer=include_answer,
include_raw_content=include_raw_content,
include_images=include_images,
)
raw_search_results = self.raw_results(params)
return self.clean_results(raw_search_results["results"])
def clean_results(self, results: list[dict]) -> list[dict]:
@ -149,13 +105,14 @@ class TavilySearchTool(BuiltinTool):
ToolInvokeMessage | list[ToolInvokeMessage]: The result of the Tavily search tool invocation.
"""
query = tool_parameters.get("query", "")
api_key = self.runtime.credentials["tavily_api_key"]
if not query:
return self.create_text_message("Please input query")
tavily_search = TavilySearch(api_key)
results = tavily_search.results(query)
results = tavily_search.results(tool_parameters)
print(results)
if not results:
return self.create_text_message(f"No results found for '{query}' in Tavily")
else:
return self.create_text_message(text=results)
return self.create_text_message(text=results)

View File

@ -25,3 +25,138 @@ parameters:
pt_BR: used for searching
llm_description: key words for searching
form: llm
- name: search_depth
type: select
required: false
label:
en_US: Search Depth
zh_Hans: 搜索深度
pt_BR: Search Depth
human_description:
en_US: The depth of search results
zh_Hans: 搜索结果的深度
pt_BR: The depth of search results
form: form
options:
- value: basic
label:
en_US: Basic
zh_Hans: 基本
pt_BR: Basic
- value: advanced
label:
en_US: Advanced
zh_Hans: 高级
pt_BR: Advanced
default: basic
- name: include_images
type: boolean
required: false
label:
en_US: Include Images
zh_Hans: 包含图片
pt_BR: Include Images
human_description:
en_US: Include images in the search results
zh_Hans: 在搜索结果中包含图片
pt_BR: Include images in the search results
form: form
options:
- value: true
label:
en_US: Yes
zh_Hans:
pt_BR: Yes
- value: false
label:
en_US: No
zh_Hans:
pt_BR: No
default: false
- name: include_answer
type: boolean
required: false
label:
en_US: Include Answer
zh_Hans: 包含答案
pt_BR: Include Answer
human_description:
en_US: Include answers in the search results
zh_Hans: 在搜索结果中包含答案
pt_BR: Include answers in the search results
form: form
options:
- value: true
label:
en_US: Yes
zh_Hans:
pt_BR: Yes
- value: false
label:
en_US: No
zh_Hans:
pt_BR: No
default: false
- name: include_raw_content
type: boolean
required: false
label:
en_US: Include Raw Content
zh_Hans: 包含原始内容
pt_BR: Include Raw Content
human_description:
en_US: Include raw content in the search results
zh_Hans: 在搜索结果中包含原始内容
pt_BR: Include raw content in the search results
form: form
options:
- value: true
label:
en_US: Yes
zh_Hans:
pt_BR: Yes
- value: false
label:
en_US: No
zh_Hans:
pt_BR: No
default: false
- name: max_results
type: number
required: false
label:
en_US: Max Results
zh_Hans: 最大结果
pt_BR: Max Results
human_description:
en_US: The number of maximum search results to return
zh_Hans: 返回的最大搜索结果数
pt_BR: The number of maximum search results to return
form: form
min: 1
max: 20
default: 5
- name: include_domains
type: string
required: false
label:
en_US: Include Domains
zh_Hans: 包含域
pt_BR: Include Domains
human_description:
en_US: A list of domains to specifically include in the search results
zh_Hans: 在搜索结果中特别包含的域名列表
pt_BR: A list of domains to specifically include in the search results
form: form
- name: exclude_domains
type: string
required: false
label:
en_US: Exclude Domains
zh_Hans: 排除域
pt_BR: Exclude Domains
human_description:
en_US: A list of domains to specifically exclude from the search results
zh_Hans: 从搜索结果中特别排除的域名列表
pt_BR: A list of domains to specifically exclude from the search results
form: form

View File

@ -291,6 +291,16 @@ class ApiTool(Tool):
elif property['type'] == 'null':
if value is None:
return None
elif property['type'] == 'object':
if isinstance(value, str):
try:
return json.loads(value)
except ValueError:
return value
elif isinstance(value, dict):
return value
else:
return value
else:
raise ValueError(f"Invalid type {property['type']} for property {property}")
elif 'anyOf' in property and isinstance(property['anyOf'], list):

View File

@ -81,7 +81,7 @@ class ApiBasedToolSchemaParser:
for content_type, content in request_body['content'].items():
# if there is a reference, get the reference and overwrite the content
if 'schema' not in content:
content
continue
if '$ref' in content['schema']:
# get the reference

View File

@ -112,7 +112,7 @@ class CodeNode(BaseNode):
variables[variable] = value
# Run code
try:
result = CodeExecutor.execute_code(
result = CodeExecutor.execute_workflow_code_template(
language=code_language,
code=code,
inputs=variables

View File

@ -438,7 +438,11 @@ class LLMNode(BaseNode):
stop = model_config.stop
vision_enabled = node_data.vision.enabled
filtered_prompt_messages = []
for prompt_message in prompt_messages:
if prompt_message.is_empty():
continue
if not isinstance(prompt_message.content, str):
prompt_message_content = []
for content_item in prompt_message.content:
@ -453,7 +457,13 @@ class LLMNode(BaseNode):
and prompt_message_content[0].type == PromptMessageContentType.TEXT):
prompt_message.content = prompt_message_content[0].data
return prompt_messages, stop
filtered_prompt_messages.append(prompt_message)
if not filtered_prompt_messages:
raise ValueError("No prompt found in the LLM configuration. "
"Please ensure a prompt is properly configured before proceeding.")
return filtered_prompt_messages, stop
@classmethod
def deduct_llm_quota(cls, tenant_id: str, model_instance: ModelInstance, usage: LLMUsage) -> None:

View File

@ -1,4 +1,3 @@
import json
import logging
from typing import Optional, Union, cast
@ -26,6 +25,7 @@ from core.workflow.nodes.question_classifier.template_prompts import (
QUESTION_CLASSIFIER_USER_PROMPT_2,
QUESTION_CLASSIFIER_USER_PROMPT_3,
)
from libs.json_in_md_parser import parse_and_check_json_markdown
from models.workflow import WorkflowNodeExecutionStatus
@ -64,7 +64,8 @@ class QuestionClassifierNode(LLMNode):
)
categories = [_class.name for _class in node_data.classes]
try:
result_text_json = json.loads(result_text.strip('```JSON\n'))
result_text_json = parse_and_check_json_markdown(result_text, [])
#result_text_json = json.loads(result_text.strip('```JSON\n'))
categories_result = result_text_json.get('categories', [])
if categories_result:
categories = categories_result

View File

@ -19,29 +19,33 @@ QUESTION_CLASSIFIER_SYSTEM_PROMPT = """
QUESTION_CLASSIFIER_USER_PROMPT_1 = """
{ "input_text": ["I recently had a great experience with your company. The service was prompt and the staff was very friendly."],
"categories": ["Customer Service", "Satisfaction", "Sales", "Product"],
"classification_instructions": ["classify the text based on the feedback provided by customer"]}```JSON
"classification_instructions": ["classify the text based on the feedback provided by customer"]}
"""
QUESTION_CLASSIFIER_ASSISTANT_PROMPT_1 = """
```json
{"keywords": ["recently", "great experience", "company", "service", "prompt", "staff", "friendly"],
"categories": ["Customer Service"]}```
"categories": ["Customer Service"]}
```
"""
QUESTION_CLASSIFIER_USER_PROMPT_2 = """
{"input_text": ["bad service, slow to bring the food"],
"categories": ["Food Quality", "Experience", "Price" ],
"classification_instructions": []}```JSON
"classification_instructions": []}
"""
QUESTION_CLASSIFIER_ASSISTANT_PROMPT_2 = """
```json
{"keywords": ["bad service", "slow", "food", "tip", "terrible", "waitresses"],
"categories": ["Experience"]}```
"categories": ["Experience"]}
```
"""
QUESTION_CLASSIFIER_USER_PROMPT_3 = """
'{{"input_text": ["{input_text}"],',
'"categories": ["{categories}" ], ',
'"classification_instructions": ["{classification_instructions}"]}}```JSON'
'"classification_instructions": ["{classification_instructions}"]}}'
"""
QUESTION_CLASSIFIER_COMPLETION_PROMPT = """

View File

@ -52,7 +52,7 @@ class TemplateTransformNode(BaseNode):
variables[variable] = value
# Run code
try:
result = CodeExecutor.execute_code(
result = CodeExecutor.execute_workflow_code_template(
language='jinja2',
code=node_data.template,
inputs=variables

View File

@ -11,3 +11,6 @@ app_model_config_was_updated = signal('app-model-config-was-updated')
# sender: app, kwargs: published_workflow
app_published_workflow_was_updated = signal('app-published-workflow-was-updated')
# sender: app, kwargs: synced_draft_workflow
app_draft_workflow_was_synced = signal('app-draft-workflow-was-synced')

View File

@ -5,6 +5,7 @@ from .create_installed_app_when_app_created import handle
from .create_site_record_when_app_created import handle
from .deduct_quota_when_messaeg_created import handle
from .delete_installed_app_when_app_deleted import handle
from .delete_tool_parameters_cache_when_sync_draft_workflow import handle
from .update_app_dataset_join_when_app_model_config_updated import handle
from .update_provider_last_used_at_when_messaeg_created import handle
from .update_app_dataset_join_when_app_published_workflow_updated import handle
from .update_provider_last_used_at_when_messaeg_created import handle

View File

@ -0,0 +1,26 @@
from core.tools.tool_manager import ToolManager
from core.tools.utils.configuration import ToolParameterConfigurationManager
from core.workflow.entities.node_entities import NodeType
from core.workflow.nodes.tool.entities import ToolEntity
from events.app_event import app_draft_workflow_was_synced
@app_draft_workflow_was_synced.connect
def handle(sender, **kwargs):
app = sender
for node_data in kwargs.get('synced_draft_workflow').graph_dict.get('nodes', []):
if node_data.get('data', {}).get('type') == NodeType.TOOL.value:
tool_entity = ToolEntity(**node_data["data"])
tool_runtime = ToolManager.get_tool_runtime(
provider_type=tool_entity.provider_type,
provider_name=tool_entity.provider_id,
tool_name=tool_entity.tool_name,
tenant_id=app.tenant_id,
)
manager = ToolParameterConfigurationManager(
tenant_id=app.tenant_id,
tool_runtime=tool_runtime,
provider_name=tool_entity.provider_name,
provider_type=tool_entity.provider_type,
)
manager.delete_tool_parameters_cache()

View File

@ -1 +0,0 @@
# -*- coding:utf-8 -*-

View File

@ -8,7 +8,7 @@ Create Date: 2024-01-21 12:09:04.651394
from json import dumps, loads
import sqlalchemy as sa
from alembic import op
from alembic import context, op
# revision identifiers, used by Alembic.
revision = 'de95f5c77138'
@ -40,8 +40,13 @@ def upgrade():
{"serpapi_api_key": "$KEY"}
- created_at <- tool_providers.created_at
- updated_at <- tool_providers.updated_at
"""
# in alembic's offline mode (with --sql option), skip data operations and output comments describing the migration to raw sql
if context.is_offline_mode():
print(f" /*{upgrade.__doc__}*/\n")
return
# select all tool_providers
tool_providers = op.get_bind().execute(
sa.text(

View File

@ -105,6 +105,12 @@ class Account(UserMixin, db.Model):
def is_admin_or_owner(self):
return self._current_tenant.current_role in ['admin', 'owner']
class TenantStatus(str, enum.Enum):
NORMAL = 'normal'
ARCHIVE = 'archive'
class Tenant(db.Model):
__tablename__ = 'tenants'
__table_args__ = (

View File

@ -3,9 +3,6 @@ requires-python = ">=3.10"
[tool.ruff]
exclude = [
"app.py",
"__init__.py",
"tests/",
]
line-length = 120
@ -25,3 +22,37 @@ ignore = [
"UP007", # non-pep604-annotation
"UP032", # f-string
]
[tool.ruff.lint.per-file-ignores]
"app.py" = [
"F401", # unused-import
"F811", # redefined-while-unused
]
"__init__.py" = [
"F401", # unused-import
"F811", # redefined-while-unused
]
"tests/*" = [
"F401", # unused-import
"F811", # redefined-while-unused
]
[tool.pytest_env]
OPENAI_API_KEY = "sk-IamNotARealKeyJustForMockTestKawaiiiiiiiiii"
AZURE_OPENAI_API_BASE = "https://difyai-openai.openai.azure.com"
AZURE_OPENAI_API_KEY = "xxxxb1707exxxxxxxxxxaaxxxxxf94"
ANTHROPIC_API_KEY = "sk-ant-api11-IamNotARealKeyJustForMockTestKawaiiiiiiiiii-NotBaka-ASkksz"
CHATGLM_API_BASE = "http://a.abc.com:11451"
XINFERENCE_SERVER_URL = "http://a.abc.com:11451"
XINFERENCE_GENERATION_MODEL_UID = "generate"
XINFERENCE_CHAT_MODEL_UID = "chat"
XINFERENCE_EMBEDDINGS_MODEL_UID = "embedding"
XINFERENCE_RERANK_MODEL_UID = "rerank"
GOOGLE_API_KEY = "abcdefghijklmnopqrstuvwxyz"
HUGGINGFACE_API_KEY = "hf-awuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwuwu"
HUGGINGFACE_TEXT_GEN_ENDPOINT_URL = "a"
HUGGINGFACE_TEXT2TEXT_GEN_ENDPOINT_URL = "b"
HUGGINGFACE_EMBEDDINGS_ENDPOINT_URL = "c"
MOCK_SWITCH = "true"
CODE_MAX_STRING_LENGTH = "80000"

View File

@ -1,4 +1,5 @@
coverage~=7.2.4
pytest~=7.3.1
pytest-mock~=3.11.1
pytest~=8.1.1
pytest-benchmark~=4.0.0
pytest-env~=1.1.3
pytest-mock~=3.14.0

View File

@ -7,7 +7,7 @@ flask-login~=0.6.3
flask-migrate~=4.0.5
flask-restful~=0.3.10
flask-cors~=4.0.0
gunicorn~=21.2.0
gunicorn~=22.0.0
gevent~=23.9.1
openai~=1.13.3
tiktoken~=0.6.0
@ -52,14 +52,14 @@ transformers~=4.35.0
tokenizers~=0.15.0
pandas==1.5.3
xinference-client==0.9.4
safetensors==0.3.2
safetensors~=0.4.3
zhipuai==1.0.7
werkzeug~=3.0.1
pymilvus==2.3.0
pymilvus~=2.3.0
qdrant-client==1.7.3
cohere~=5.2.4
pyyaml~=6.0.1
numpy~=1.25.2
numpy~=1.26.4
unstructured[docx,pptx,msg,md,ppt,epub]~=0.10.27
bs4~=0.0.1
markdown~=3.5.1
@ -67,7 +67,7 @@ httpx[socks]~=0.24.1
matplotlib~=3.8.2
yfinance~=0.2.35
pydub~=0.25.1
gmpy2~=2.1.5
gmpy2~=2.2.0a1
numexpr~=2.9.0
duckduckgo-search==5.2.2
arxiv==2.1.0

View File

@ -1,2 +1 @@
# -*- coding:utf-8 -*-
import services.errors

View File

@ -8,7 +8,7 @@ from typing import Any, Optional
from flask import current_app
from sqlalchemy import func
from werkzeug.exceptions import Forbidden
from werkzeug.exceptions import Unauthorized
from constants.languages import language_timezone_mapping, languages
from events.tenant_event import tenant_was_created
@ -44,7 +44,7 @@ class AccountService:
return None
if account.status in [AccountStatus.BANNED.value, AccountStatus.CLOSED.value]:
raise Forbidden('Account is banned or closed.')
raise Unauthorized("Account is banned or closed.")
current_tenant = TenantAccountJoin.query.filter_by(account_id=account.id, current=True).first()
if current_tenant:
@ -255,7 +255,7 @@ class TenantService:
"""Get account join tenants"""
return db.session.query(Tenant).join(
TenantAccountJoin, Tenant.id == TenantAccountJoin.tenant_id
).filter(TenantAccountJoin.account_id == account.id).all()
).filter(TenantAccountJoin.account_id == account.id, Tenant.status == TenantStatus.NORMAL).all()
@staticmethod
def get_current_tenant_by_account(account: Account):
@ -279,7 +279,12 @@ class TenantService:
if tenant_id is None:
raise ValueError("Tenant ID must be provided.")
tenant_account_join = TenantAccountJoin.query.filter_by(account_id=account.id, tenant_id=tenant_id).first()
tenant_account_join = db.session.query(TenantAccountJoin).join(Tenant, TenantAccountJoin.tenant_id == Tenant.id).filter(
TenantAccountJoin.account_id == account.id,
TenantAccountJoin.tenant_id == tenant_id,
Tenant.status == TenantStatus.NORMAL,
).first()
if not tenant_account_join:
raise AccountNotLinkTenantError("Tenant not found or account is not a member of the tenant.")
else:

View File

View File

@ -0,0 +1,20 @@
import os
import requests
class EnterpriseRequest:
base_url = os.environ.get('ENTERPRISE_API_URL', 'ENTERPRISE_API_URL')
secret_key = os.environ.get('ENTERPRISE_API_SECRET_KEY', 'ENTERPRISE_API_SECRET_KEY')
@classmethod
def send_request(cls, method, endpoint, json=None, params=None):
headers = {
"Content-Type": "application/json",
"Enterprise-Api-Secret-Key": cls.secret_key
}
url = f"{cls.base_url}{endpoint}"
response = requests.request(method, url, json=json, params=params, headers=headers)
return response.json()

View File

@ -0,0 +1,28 @@
from flask import current_app
from pydantic import BaseModel
from services.enterprise.enterprise_service import EnterpriseService
class EnterpriseFeatureModel(BaseModel):
sso_enforced_for_signin: bool = False
sso_enforced_for_signin_protocol: str = ''
class EnterpriseFeatureService:
@classmethod
def get_enterprise_features(cls) -> EnterpriseFeatureModel:
features = EnterpriseFeatureModel()
if current_app.config['ENTERPRISE_ENABLED']:
cls._fulfill_params_from_enterprise(features)
return features
@classmethod
def _fulfill_params_from_enterprise(cls, features):
enterprise_info = EnterpriseService.get_info()
features.sso_enforced_for_signin = enterprise_info['sso_enforced_for_signin']
features.sso_enforced_for_signin_protocol = enterprise_info['sso_enforced_for_signin_protocol']

View File

@ -0,0 +1,8 @@
from services.enterprise.base import EnterpriseRequest
class EnterpriseService:
@classmethod
def get_info(cls):
return EnterpriseRequest.send_request('GET', '/info')

View File

@ -0,0 +1,60 @@
import logging
from models.account import Account, AccountStatus
from services.account_service import AccountService, TenantService
from services.enterprise.base import EnterpriseRequest
logger = logging.getLogger(__name__)
class EnterpriseSSOService:
@classmethod
def get_sso_saml_login(cls) -> str:
return EnterpriseRequest.send_request('GET', '/sso/saml/login')
@classmethod
def post_sso_saml_acs(cls, saml_response: str) -> str:
response = EnterpriseRequest.send_request('POST', '/sso/saml/acs', json={'SAMLResponse': saml_response})
if 'email' not in response or response['email'] is None:
logger.exception(response)
raise Exception('Saml response is invalid')
return cls.login_with_email(response.get('email'))
@classmethod
def get_sso_oidc_login(cls):
return EnterpriseRequest.send_request('GET', '/sso/oidc/login')
@classmethod
def get_sso_oidc_callback(cls, args: dict):
state_from_query = args['state']
code_from_query = args['code']
state_from_cookies = args['oidc-state']
if state_from_cookies != state_from_query:
raise Exception('invalid state or code')
response = EnterpriseRequest.send_request('GET', '/sso/oidc/callback', params={'code': code_from_query})
if 'email' not in response or response['email'] is None:
logger.exception(response)
raise Exception('OIDC response is invalid')
return cls.login_with_email(response.get('email'))
@classmethod
def login_with_email(cls, email: str) -> str:
account = Account.query.filter_by(email=email).first()
if account is None:
raise Exception('account not found, please contact system admin to invite you to join in a workspace')
if account.status == AccountStatus.BANNED:
raise Exception('account is banned, please contact system admin')
tenants = TenantService.get_join_tenants(account)
if len(tenants) == 0:
raise Exception("workspace not found, please contact system admin to invite you to join in a workspace")
token = AccountService.get_account_jwt_token(account)
return token

View File

@ -1,4 +1,3 @@
# -*- coding:utf-8 -*-
__all__ = [
'base', 'conversation', 'message', 'index', 'app_model_config', 'account', 'document', 'dataset',
'app', 'completion', 'audio', 'file'

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