Compare commits

..

66 Commits

Author SHA1 Message Date
jyong
dc3068da2c uncompleted and disabled documents can't create segment with api 2024-09-03 18:02:16 +08:00
jyong
c8da929904 improve the notion table extract 2024-09-03 17:41:11 +08:00
Joel
7fdd964379 fix: frontend handle sometimes server not generate the wrong follow up data struct (#7916) 2024-09-03 14:09:46 +08:00
Joel
0cfcc97e9d feat: support auto generate i18n translate (#6964)
Co-authored-by: crazywoola <427733928@qq.com>
2024-09-03 10:17:05 +08:00
-LAN-
8986be0aab chore: Update versions to 0.7.3 (#7895) 2024-09-03 09:49:32 +08:00
-LAN-
f76bbbf5e6 chore(Dockerfile): Bump expat to 2.6.2-2 (#7904) 2024-09-03 09:48:30 +08:00
kurokobo
fe217da05c fix: correct typo in the setting screen (#7897) 2024-09-02 22:49:56 +08:00
kurokobo
80aa7c4019 feat: allow users to use the app icon as the answer icon (#7888)
Co-authored-by: crazywoola <427733928@qq.com>
2024-09-02 20:00:41 +08:00
Jyong
6f33351eb3 ignore linked images when image id is none (#7890) 2024-09-02 19:37:05 +08:00
Alex
35f13c7327 Add Russian language (#7860)
Co-authored-by: d8rt8v <alex@ydertev.ru>
Co-authored-by: crazywoola <427733928@qq.com>
2024-09-02 19:09:41 +08:00
Fei He
a8b9e01b3e fix: fixed typo on loading reranking_mode (#7887) 2024-09-02 16:18:47 +08:00
Joshua
7193e189f3 Add perplexity search as a new tool (#7861) 2024-09-02 14:48:13 +08:00
orangeclk
3f2a806abe fix: glm models prices and max_tokens correction (#7882) 2024-09-02 14:29:09 +08:00
legao
5e4907e940 fix: layout shift on app card hover (#7872) 2024-09-02 11:05:54 +08:00
omr
bf63c5d1e3 fix typo: langauge -> language (#7875) 2024-09-02 08:41:45 +08:00
Seayon
78989e9049 Add ALIYUN_OSS_PATH configuration for Aliyun OSS (#7864)
Co-authored-by: seayon <zhaoxuyang@shouqianba.com>
2024-09-01 21:30:17 +08:00
Hirotaka Miyagi
1510bdbcf6 refactor: Remove typecasting by any (#7862) 2024-09-01 14:58:12 +08:00
Hirotaka Miyagi
024d688b77 fix(RetrievalConfig): Fix score threshold assignment for zero value (#7865) 2024-09-01 14:57:50 +08:00
zhujinle
ef82a29e23 fix: crash when ECharts accesses undefined objects (#7853) 2024-09-01 14:52:27 +08:00
sino
1f56a20b62 feat: support auth by api key for ark provider (#7845) 2024-08-31 10:56:32 +08:00
Bowen Liang
0c2a62f847 fix: correct http timeout configs‘ default values and ignorance by HttpRequestNode (#7762) 2024-08-30 19:09:10 +08:00
Ethan
ea748b50f2 fix: an issue of keyword search feature in application log list (#7816) 2024-08-30 18:48:05 +08:00
Yi Xiao
62bfc4dba6 fix: tooltip size sets improperly (#7836) 2024-08-30 18:13:54 +08:00
Zhi
ceb2b150ff enhance: include workspace name in create-tenant command (#7834) 2024-08-30 15:53:50 +08:00
非法操作
dc015c380a feat: add zhipu glm_4_plus and glm_4v_plus model (#7824) 2024-08-30 15:08:31 +08:00
Benjamin
c9e0f0bf20 fix: correct typo in environment variable description (#7817) 2024-08-30 00:03:40 +08:00
YidaHu
bd6d4d0553 fix: filter out installed apps without an app (#7799) 2024-08-29 19:03:08 +08:00
hisir
f0273f00e1 Fixed when testing the openai compatible interface model, an error is reported when no object is returned (#7808) 2024-08-29 18:58:19 +08:00
Yeuoly
962cdbbebd chore: add app generator overload (#7792) 2024-08-29 16:04:01 +08:00
NFish
2c51e3a327 fix: webapp sso setting may not the latest value when refresh (#7795) 2024-08-29 15:57:43 +08:00
Jyong
8e311cc45c fixed permission is None (#7788) 2024-08-29 12:46:42 +08:00
crazywoola
c441bea4d1 fix: datasets permission is missing (#7787) 2024-08-29 12:46:33 +08:00
NFish
ad30668eb6 Sync Input component from feat/attachments branch (#7782) 2024-08-29 11:23:16 +08:00
Huang YunKun
62f4801523 Update ssrf_proxy related doc link in docker-compose file (#7778) 2024-08-29 11:22:39 +08:00
kanoshiou
ec1408346e docs: navigate to open issues in contributing documents (#7781) 2024-08-29 11:18:49 +08:00
takatost
0e0a703496 chore: ignore openai error record in sentry (#7770) 2024-08-28 23:26:11 +08:00
Garfield Dai
54b693d5b1 feat: update saas billing hint. (#7760) 2024-08-28 18:55:47 +08:00
Bowen Liang
1262277714 chore: improve http executor configs (#7730) 2024-08-28 17:46:37 +08:00
YidaHu
3a67fc6c5a feat: add support for array types in available variable list (#7715) 2024-08-28 17:30:13 +08:00
zhuhao
26abbe8e5b feat(Tools): add a tool to query the stock price from Alpha Vantage (#7019) (#7752) 2024-08-28 17:27:20 +08:00
Leheng Lu
5d0914daea fix: not able to pass array of string/number/object into variable aggregator groups (#7757) 2024-08-28 17:25:20 +08:00
Joel
7541a492b7 fix: crawl options max length can not set 0 (#7758)
Co-authored-by: Yi <yxiaoisme@gmail.com>
2024-08-28 17:16:07 +08:00
crazywoola
3a071b8db9 fix: datasets permission is missing (#7751) 2024-08-28 15:36:11 +08:00
snickerjp
9342b4b951 Update package "libldap-2.5-0" for docker build. (#7726) 2024-08-28 14:44:05 +08:00
Vimpas
4682e0ac7c fix(storage): 🐛 HeadBucket Operation Permission (#7733)
Co-authored-by: 莫岳恒 <moyueheng@datagrand.com>
2024-08-28 13:57:45 +08:00
sino
7cfebffbb8 chore: update default endpoint for ark provider (#7741) 2024-08-28 13:56:50 +08:00
KVOJJJin
693fe912f2 Fix annotation reply settings (#7696) 2024-08-28 09:42:54 +08:00
kurokobo
bc3a8e0ca2 feat: store created_by and updated_by for apps, modelconfigs, and sites (#7613) 2024-08-28 08:47:30 +08:00
Jiakun Xu
e38334cfd2 fix: doc_language return null when document segment settings (#7719) 2024-08-28 08:45:51 +08:00
走在修行的大街上
92cab33b73 feat(Tools): add feishu document and message plugins (#6435)
Co-authored-by: 黎斌 <libin.23@bytedance.com>
2024-08-27 20:21:42 +08:00
Bowen Liang
3f467613fc feat: support configs for code execution request (#7704) 2024-08-27 19:38:33 +08:00
Bryan
205d33a813 Fix: read properties of undefined issue (#7708)
Co-authored-by: libing <libing@healink.cn>
2024-08-27 19:23:56 +08:00
crazywoola
da326baa5e fix: tongyi Error: 'NoneType' object is not subscriptable (#7705) 2024-08-27 16:56:06 +08:00
crazywoola
d9198b5646 feat: remove unused code (#7702) 2024-08-27 16:47:34 +08:00
Jyong
60001a62c4 fixed chunk_overlap is None (#7703) 2024-08-27 16:38:06 +08:00
sino
ee7d5e7206 feat: support Moonshot and GLM models tool call for volc ark provider (#7666) 2024-08-27 14:43:37 +08:00
呆萌闷油瓶
2726fb3d5d feat:dailymessages (#7603) 2024-08-27 12:53:27 +08:00
kurokobo
d7aa4076c9 feat: display account name on the logs page for the apps (#7668)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
2024-08-27 12:40:44 +08:00
Kenn
122ce41020 feat: rewrite Elasticsearch index and search code to achieve Elasticsearch vector and full-text search (#7641)
Co-authored-by: haokai <haokai@shuwen.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: Bowen Liang <bowenliang@apache.org>
Co-authored-by: wellCh4n <wellCh4n@foxmail.com>
2024-08-27 11:43:44 +08:00
Charlie.Wei
e7afee1176 Langfuse view button (#7684) 2024-08-27 11:25:56 +08:00
zxhlyh
88730906ec fix: empty knowledge add file (#7690) 2024-08-27 11:25:27 +08:00
Bowen Liang
a15080a1d7 bug: (#7586 followup) fix config of CODE_MAX_STRING_LENGTH (#7683) 2024-08-27 10:38:24 +08:00
Jyong
35431bce0d fix dataset_id and index_node_id idx missed in document_segments tabl… (#7681) 2024-08-27 10:25:24 +08:00
Hélio Lúcio
7b7576ad55 Add Azure AI Studio as provider (#7549)
Co-authored-by: Hélio Lúcio <canais.hlucio@voegol.com.br>
2024-08-27 09:52:59 +08:00
Qin Liu
162faee4f2 fix: set score_threshold to zero if it is None for MyScale vectordb (#7640)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
2024-08-27 09:47:16 +08:00
Zhi
b7ff98d7ff fix: Remove useless debug information. (#7647) 2024-08-26 20:40:26 +08:00
226 changed files with 7298 additions and 496 deletions

View File

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

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

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

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,6 +1,6 @@
from typing import Optional
from typing import Annotated, Optional
from pydantic import AliasChoices, Field, NegativeInt, NonNegativeInt, PositiveInt, computed_field
from pydantic import AliasChoices, Field, HttpUrl, NegativeInt, NonNegativeInt, PositiveInt, computed_field
from pydantic_settings import BaseSettings
from configs.feature.hosted_service import HostedServiceConfig
@@ -45,7 +45,7 @@ class CodeExecutionSandboxConfig(BaseSettings):
Code Execution Sandbox configs
"""
CODE_EXECUTION_ENDPOINT: str = Field(
CODE_EXECUTION_ENDPOINT: HttpUrl = Field(
description="endpoint URL of code execution servcie",
default="http://sandbox:8194",
)
@@ -55,6 +55,21 @@ class CodeExecutionSandboxConfig(BaseSettings):
default="dify-sandbox",
)
CODE_EXECUTION_CONNECT_TIMEOUT: Optional[float] = Field(
description="connect timeout in seconds for code execution request",
default=10.0,
)
CODE_EXECUTION_READ_TIMEOUT: Optional[float] = Field(
description="read timeout in seconds for code execution request",
default=60.0,
)
CODE_EXECUTION_WRITE_TIMEOUT: Optional[float] = Field(
description="write timeout in seconds for code execution request",
default=10.0,
)
CODE_MAX_NUMBER: PositiveInt = Field(
description="max depth for code execution",
default=9223372036854775807,
@@ -202,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

@@ -13,6 +13,7 @@ from configs.middleware.storage.oci_storage_config import OCIStorageConfig
from configs.middleware.storage.tencent_cos_storage_config import TencentCloudCOSStorageConfig
from configs.middleware.vdb.analyticdb_config import AnalyticdbConfig
from configs.middleware.vdb.chroma_config import ChromaConfig
from configs.middleware.vdb.elasticsearch_config import ElasticsearchConfig
from configs.middleware.vdb.milvus_config import MilvusConfig
from configs.middleware.vdb.myscale_config import MyScaleConfig
from configs.middleware.vdb.opensearch_config import OpenSearchConfig
@@ -200,5 +201,6 @@ class MiddlewareConfig(
TencentVectorDBConfig,
TiDBVectorConfig,
WeaviateConfig,
ElasticsearchConfig,
):
pass

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

@@ -0,0 +1,30 @@
from typing import Optional
from pydantic import Field, PositiveInt
from pydantic_settings import BaseSettings
class ElasticsearchConfig(BaseSettings):
"""
Elasticsearch configs
"""
ELASTICSEARCH_HOST: Optional[str] = Field(
description="Elasticsearch host",
default="127.0.0.1",
)
ELASTICSEARCH_PORT: PositiveInt = Field(
description="Elasticsearch port",
default=9200,
)
ELASTICSEARCH_USERNAME: Optional[str] = Field(
description="Elasticsearch username",
default="elastic",
)
ELASTICSEARCH_PASSWORD: Optional[str] = Field(
description="Elasticsearch password",
default="elastic",
)

View File

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

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

@@ -32,6 +32,8 @@ class ModelConfigResource(Resource):
new_app_model_config = AppModelConfig(
app_id=app_model.id,
created_by=current_user.id,
updated_by=current_user.id,
)
new_app_model_config = new_app_model_config.from_model_config_dict(model_configuration)

View File

@@ -1,3 +1,5 @@
from datetime import datetime, timezone
from flask_login import current_user
from flask_restful import Resource, marshal_with, reqparse
from werkzeug.exceptions import Forbidden, NotFound
@@ -32,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()
@@ -66,11 +69,14 @@ 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:
setattr(site, attr_name, value)
site.updated_by = current_user.id
site.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
db.session.commit()
return site
@@ -93,6 +99,8 @@ class AppSiteAccessTokenReset(Resource):
raise NotFound
site.code = Site.generate_code(16)
site.updated_by = current_user.id
site.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
db.session.commit()
return site

View File

@@ -16,6 +16,60 @@ from libs.login import login_required
from models.model import AppMode
class DailyMessageStatistic(Resource):
@setup_required
@login_required
@account_initialization_required
@get_app_model
def get(self, app_model):
account = current_user
parser = reqparse.RequestParser()
parser.add_argument("start", type=datetime_string("%Y-%m-%d %H:%M"), location="args")
parser.add_argument("end", type=datetime_string("%Y-%m-%d %H:%M"), location="args")
args = parser.parse_args()
sql_query = """
SELECT date(DATE_TRUNC('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date, count(*) AS message_count
FROM messages where app_id = :app_id
"""
arg_dict = {"tz": account.timezone, "app_id": app_model.id}
timezone = pytz.timezone(account.timezone)
utc_timezone = pytz.utc
if args["start"]:
start_datetime = datetime.strptime(args["start"], "%Y-%m-%d %H:%M")
start_datetime = start_datetime.replace(second=0)
start_datetime_timezone = timezone.localize(start_datetime)
start_datetime_utc = start_datetime_timezone.astimezone(utc_timezone)
sql_query += " and created_at >= :start"
arg_dict["start"] = start_datetime_utc
if args["end"]:
end_datetime = datetime.strptime(args["end"], "%Y-%m-%d %H:%M")
end_datetime = end_datetime.replace(second=0)
end_datetime_timezone = timezone.localize(end_datetime)
end_datetime_utc = end_datetime_timezone.astimezone(utc_timezone)
sql_query += " and created_at < :end"
arg_dict["end"] = end_datetime_utc
sql_query += " GROUP BY date order by date"
response_data = []
with db.engine.begin() as conn:
rs = conn.execute(db.text(sql_query), arg_dict)
for i in rs:
response_data.append({"date": str(i.date), "message_count": i.message_count})
return jsonify({"data": response_data})
class DailyConversationStatistic(Resource):
@setup_required
@login_required
@@ -419,6 +473,7 @@ WHERE app_id = :app_id"""
return jsonify({"data": response_data})
api.add_resource(DailyMessageStatistic, "/apps/<uuid:app_id>/statistics/daily-messages")
api.add_resource(DailyConversationStatistic, "/apps/<uuid:app_id>/statistics/daily-conversations")
api.add_resource(DailyTerminalsStatistic, "/apps/<uuid:app_id>/statistics/daily-end-users")
api.add_resource(DailyTokenCostStatistic, "/apps/<uuid:app_id>/statistics/token-costs")

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

@@ -599,6 +599,7 @@ class DocumentDetailApi(DocumentResource):
"hit_count": document.hit_count,
"display_status": document.display_status,
"doc_form": document.doc_form,
"doc_language": document.doc_language,
}
else:
process_rules = DatasetService.get_process_rules(dataset_id)
@@ -631,6 +632,7 @@ class DocumentDetailApi(DocumentResource):
"hit_count": document.hit_count,
"display_status": document.display_status,
"doc_form": document.doc_form,
"doc_language": document.doc_language,
}
return response, 200

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

@@ -36,6 +36,10 @@ class SegmentApi(DatasetApiResource):
document = DocumentService.get_document(dataset.id, document_id)
if not document:
raise NotFound("Document not found.")
if document.indexing_status != "completed":
raise NotFound("Document is already completed.")
if not document.enabled:
raise NotFound("Document is disabled.")
# check embedding model setting
if dataset.indexing_technique == "high_quality":
try:

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 Union
from typing import Literal, Union, overload
from flask import Flask, current_app
from pydantic import ValidationError
@@ -39,6 +39,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,
workflow: Workflow,

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 Union
from typing import Literal, Union, overload
from flask import Flask, current_app
from pydantic import ValidationError
@@ -32,6 +32,26 @@ logger = logging.getLogger(__name__)
class WorkflowAppGenerator(BaseAppGenerator):
@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,
workflow: Workflow,
@@ -107,7 +127,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
application_generate_entity: WorkflowAppGenerateEntity,
invoke_from: InvokeFrom,
stream: bool = True,
) -> Union[dict, Generator[dict, None, None]]:
) -> Union[dict, Generator[str, None, None]]:
"""
Generate App response.

View File

@@ -15,12 +15,6 @@ from core.helper.code_executor.template_transformer import TemplateTransformer
logger = logging.getLogger(__name__)
# Code Executor
CODE_EXECUTION_ENDPOINT = dify_config.CODE_EXECUTION_ENDPOINT
CODE_EXECUTION_API_KEY = dify_config.CODE_EXECUTION_API_KEY
CODE_EXECUTION_TIMEOUT = Timeout(connect=10, write=10, read=60, pool=None)
class CodeExecutionException(Exception):
pass
@@ -71,10 +65,10 @@ class CodeExecutor:
:param code: code
:return:
"""
url = URL(CODE_EXECUTION_ENDPOINT) / 'v1' / 'sandbox' / 'run'
url = URL(str(dify_config.CODE_EXECUTION_ENDPOINT)) / 'v1' / 'sandbox' / 'run'
headers = {
'X-Api-Key': CODE_EXECUTION_API_KEY
'X-Api-Key': dify_config.CODE_EXECUTION_API_KEY
}
data = {
@@ -85,7 +79,12 @@ class CodeExecutor:
}
try:
response = post(str(url), json=data, headers=headers, timeout=CODE_EXECUTION_TIMEOUT)
response = post(str(url), json=data, headers=headers,
timeout=Timeout(
connect=dify_config.CODE_EXECUTION_CONNECT_TIMEOUT,
read=dify_config.CODE_EXECUTION_READ_TIMEOUT,
write=dify_config.CODE_EXECUTION_WRITE_TIMEOUT,
pool=None))
if response.status_code == 503:
raise CodeExecutionException('Code execution service is unavailable')
elif response.status_code != 200:
@@ -96,7 +95,7 @@ class CodeExecutor:
raise CodeExecutionException('Failed to execute code, which is likely a network issue,'
' please check if the sandbox service is running.'
f' ( Error: {str(e)} )')
try:
response = response.json()
except:
@@ -104,12 +103,12 @@ class CodeExecutor:
if (code := response.get('code')) != 0:
raise CodeExecutionException(f"Got error code: {code}. Got error msg: {response.get('message')}")
response = CodeExecutionResponse(**response)
if response.data.error:
raise CodeExecutionException(response.data.error)
return response.data.stdout or ''
@classmethod
@@ -133,4 +132,3 @@ class CodeExecutor:
raise e
return template_transformer.transform_response(response)

View File

@@ -720,6 +720,7 @@ class IndexingRunner:
document_ids = [document.metadata['doc_id'] for document in documents]
db.session.query(DocumentSegment).filter(
DocumentSegment.document_id == document_id,
DocumentSegment.dataset_id == dataset_id,
DocumentSegment.index_node_id.in_(document_ids),
DocumentSegment.status == "indexing"
).update({
@@ -751,6 +752,7 @@ class IndexingRunner:
document_ids = [document.metadata['doc_id'] for document in chunk_documents]
db.session.query(DocumentSegment).filter(
DocumentSegment.document_id == dataset_document.id,
DocumentSegment.dataset_id == dataset.id,
DocumentSegment.index_node_id.in_(document_ids),
DocumentSegment.status == "indexing"
).update({

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,17 @@
import logging
from core.model_runtime.model_providers.__base.model_provider import ModelProvider
logger = logging.getLogger(__name__)
class AzureAIStudioProvider(ModelProvider):
def validate_provider_credentials(self, credentials: dict) -> None:
"""
Validate provider credentials
if validate failed, raise exception
:param credentials: provider credentials, credentials form defined in `provider_credential_schema`.
"""
pass

View File

@@ -0,0 +1,65 @@
provider: azure_ai_studio
label:
zh_Hans: Azure AI Studio
en_US: Azure AI Studio
icon_small:
en_US: icon_s_en.png
icon_large:
en_US: icon_l_en.png
description:
en_US: Azure AI Studio
zh_Hans: Azure AI Studio
background: "#93c5fd"
help:
title:
en_US: How to deploy customized model on Azure AI Studio
zh_Hans: 如何在Azure AI Studio上的私有化部署的模型
url:
en_US: https://learn.microsoft.com/en-us/azure/ai-studio/how-to/deploy-models
zh_Hans: https://learn.microsoft.com/zh-cn/azure/ai-studio/how-to/deploy-models
supported_model_types:
- llm
- rerank
configurate_methods:
- customizable-model
model_credential_schema:
model:
label:
en_US: Model Name
zh_Hans: 模型名称
placeholder:
en_US: Enter your model name
zh_Hans: 输入模型名称
credential_form_schemas:
- variable: endpoint
label:
en_US: Azure AI Studio Endpoint
type: text-input
required: true
placeholder:
zh_Hans: 请输入你的Azure AI Studio推理端点
en_US: 'Enter your API Endpoint, eg: https://example.com'
- variable: api_key
required: true
label:
en_US: API Key
zh_Hans: API Key
type: secret-input
placeholder:
en_US: Enter your Azure AI Studio API Key
zh_Hans: 在此输入您的 Azure AI Studio API Key
show_on:
- variable: __model_type
value: llm
- variable: jwt_token
required: true
label:
en_US: JWT Token
zh_Hans: JWT令牌
type: secret-input
placeholder:
en_US: Enter your Azure AI Studio JWT Token
zh_Hans: 在此输入您的 Azure AI Studio 推理 API Key
show_on:
- variable: __model_type
value: rerank

View File

@@ -0,0 +1,334 @@
import logging
from collections.abc import Generator
from typing import Any, Optional, Union
from azure.ai.inference import ChatCompletionsClient
from azure.ai.inference.models import StreamingChatCompletionsUpdate
from azure.core.credentials import AzureKeyCredential
from azure.core.exceptions import (
ClientAuthenticationError,
DecodeError,
DeserializationError,
HttpResponseError,
ResourceExistsError,
ResourceModifiedError,
ResourceNotFoundError,
ResourceNotModifiedError,
SerializationError,
ServiceRequestError,
ServiceResponseError,
)
from core.model_runtime.callbacks.base_callback import Callback
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage
from core.model_runtime.entities.message_entities import (
AssistantPromptMessage,
PromptMessage,
PromptMessageTool,
)
from core.model_runtime.entities.model_entities import (
AIModelEntity,
FetchFrom,
I18nObject,
ModelType,
ParameterRule,
ParameterType,
)
from core.model_runtime.errors.invoke import (
InvokeAuthorizationError,
InvokeBadRequestError,
InvokeConnectionError,
InvokeError,
InvokeServerUnavailableError,
)
from core.model_runtime.errors.validate import CredentialsValidateFailedError
from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
logger = logging.getLogger(__name__)
class AzureAIStudioLargeLanguageModel(LargeLanguageModel):
"""
Model class for Azure AI Studio large language model.
"""
client: Any = None
from azure.ai.inference.models import StreamingChatCompletionsUpdate
def _invoke(
self,
model: str,
credentials: dict,
prompt_messages: list[PromptMessage],
model_parameters: dict,
tools: Optional[list[PromptMessageTool]] = None,
stop: Optional[list[str]] = None,
stream: bool = True,
user: Optional[str] = None,
) -> Union[LLMResult, Generator]:
"""
Invoke large language model
:param model: model name
:param credentials: model credentials
:param prompt_messages: prompt messages
:param model_parameters: model parameters
:param tools: tools for tool calling
:param stop: stop words
:param stream: is stream response
:param user: unique user id
:return: full response or stream response chunk generator result
"""
if not self.client:
endpoint = credentials.get("endpoint")
api_key = credentials.get("api_key")
self.client = ChatCompletionsClient(endpoint=endpoint, credential=AzureKeyCredential(api_key))
messages = [{"role": msg.role.value, "content": msg.content} for msg in prompt_messages]
payload = {
"messages": messages,
"max_tokens": model_parameters.get("max_tokens", 4096),
"temperature": model_parameters.get("temperature", 0),
"top_p": model_parameters.get("top_p", 1),
"stream": stream,
}
if stop:
payload["stop"] = stop
if tools:
payload["tools"] = [tool.model_dump() for tool in tools]
try:
response = self.client.complete(**payload)
if stream:
return self._handle_stream_response(response, model, prompt_messages)
else:
return self._handle_non_stream_response(response, model, prompt_messages, credentials)
except Exception as e:
raise self._transform_invoke_error(e)
def _handle_stream_response(self, response, model: str, prompt_messages: list[PromptMessage]) -> Generator:
for chunk in response:
if isinstance(chunk, StreamingChatCompletionsUpdate):
if chunk.choices:
delta = chunk.choices[0].delta
if delta.content:
yield LLMResultChunk(
model=model,
prompt_messages=prompt_messages,
delta=LLMResultChunkDelta(
index=0,
message=AssistantPromptMessage(content=delta.content, tool_calls=[]),
),
)
def _handle_non_stream_response(
self, response, model: str, prompt_messages: list[PromptMessage], credentials: dict
) -> LLMResult:
assistant_text = response.choices[0].message.content
assistant_prompt_message = AssistantPromptMessage(content=assistant_text)
usage = self._calc_response_usage(
model, credentials, response.usage.prompt_tokens, response.usage.completion_tokens
)
result = LLMResult(model=model, prompt_messages=prompt_messages, message=assistant_prompt_message, usage=usage)
if hasattr(response, "system_fingerprint"):
result.system_fingerprint = response.system_fingerprint
return result
def _invoke_result_generator(
self,
model: str,
result: Generator,
credentials: dict,
prompt_messages: list[PromptMessage],
model_parameters: dict,
tools: Optional[list[PromptMessageTool]] = None,
stop: Optional[list[str]] = None,
stream: bool = True,
user: Optional[str] = None,
callbacks: Optional[list[Callback]] = None,
) -> Generator:
"""
Invoke result generator
:param result: result generator
:return: result generator
"""
callbacks = callbacks or []
prompt_message = AssistantPromptMessage(content="")
usage = None
system_fingerprint = None
real_model = model
try:
for chunk in result:
if isinstance(chunk, dict):
content = chunk["choices"][0]["message"]["content"]
usage = chunk["usage"]
chunk = LLMResultChunk(
model=model,
prompt_messages=prompt_messages,
delta=LLMResultChunkDelta(
index=0,
message=AssistantPromptMessage(content=content, tool_calls=[]),
),
system_fingerprint=chunk.get("system_fingerprint"),
)
yield chunk
self._trigger_new_chunk_callbacks(
chunk=chunk,
model=model,
credentials=credentials,
prompt_messages=prompt_messages,
model_parameters=model_parameters,
tools=tools,
stop=stop,
stream=stream,
user=user,
callbacks=callbacks,
)
prompt_message.content += chunk.delta.message.content
real_model = chunk.model
if hasattr(chunk.delta, "usage"):
usage = chunk.delta.usage
if chunk.system_fingerprint:
system_fingerprint = chunk.system_fingerprint
except Exception as e:
raise self._transform_invoke_error(e)
self._trigger_after_invoke_callbacks(
model=model,
result=LLMResult(
model=real_model,
prompt_messages=prompt_messages,
message=prompt_message,
usage=usage if usage else LLMUsage.empty_usage(),
system_fingerprint=system_fingerprint,
),
credentials=credentials,
prompt_messages=prompt_messages,
model_parameters=model_parameters,
tools=tools,
stop=stop,
stream=stream,
user=user,
callbacks=callbacks,
)
def get_num_tokens(
self,
model: str,
credentials: dict,
prompt_messages: list[PromptMessage],
tools: Optional[list[PromptMessageTool]] = None,
) -> int:
"""
Get number of tokens for given prompt messages
:param model: model name
:param credentials: model credentials
:param prompt_messages: prompt messages
:param tools: tools for tool calling
:return:
"""
# Implement token counting logic here
# Might need to use a tokenizer specific to the Azure AI Studio model
return 0
def validate_credentials(self, model: str, credentials: dict) -> None:
"""
Validate model credentials
:param model: model name
:param credentials: model credentials
:return:
"""
try:
endpoint = credentials.get("endpoint")
api_key = credentials.get("api_key")
client = ChatCompletionsClient(endpoint=endpoint, credential=AzureKeyCredential(api_key))
client.get_model_info()
except Exception as ex:
raise CredentialsValidateFailedError(str(ex))
@property
def _invoke_error_mapping(self) -> dict[type[InvokeError], list[type[Exception]]]:
"""
Map model invoke error to unified error
The key is the error type thrown to the caller
The value is the error type thrown by the model,
which needs to be converted into a unified error type for the caller.
:return: Invoke error mapping
"""
return {
InvokeConnectionError: [
ServiceRequestError,
],
InvokeServerUnavailableError: [
ServiceResponseError,
],
InvokeAuthorizationError: [
ClientAuthenticationError,
],
InvokeBadRequestError: [
HttpResponseError,
DecodeError,
ResourceExistsError,
ResourceNotFoundError,
ResourceModifiedError,
ResourceNotModifiedError,
SerializationError,
DeserializationError,
],
}
def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None:
"""
Used to define customizable model schema
"""
rules = [
ParameterRule(
name="temperature",
type=ParameterType.FLOAT,
use_template="temperature",
label=I18nObject(zh_Hans="温度", en_US="Temperature"),
),
ParameterRule(
name="top_p",
type=ParameterType.FLOAT,
use_template="top_p",
label=I18nObject(zh_Hans="Top P", en_US="Top P"),
),
ParameterRule(
name="max_tokens",
type=ParameterType.INT,
use_template="max_tokens",
min=1,
default=512,
label=I18nObject(zh_Hans="最大生成长度", en_US="Max Tokens"),
),
]
entity = AIModelEntity(
model=model,
label=I18nObject(en_US=model),
fetch_from=FetchFrom.CUSTOMIZABLE_MODEL,
model_type=ModelType.LLM,
features=[],
model_properties={},
parameter_rules=rules,
)
return entity

View File

@@ -0,0 +1,164 @@
import json
import logging
import os
import ssl
import urllib.request
from typing import Optional
from core.model_runtime.entities.common_entities import I18nObject
from core.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType
from core.model_runtime.entities.rerank_entities import RerankDocument, RerankResult
from core.model_runtime.errors.invoke import (
InvokeAuthorizationError,
InvokeBadRequestError,
InvokeConnectionError,
InvokeError,
InvokeRateLimitError,
InvokeServerUnavailableError,
)
from core.model_runtime.errors.validate import CredentialsValidateFailedError
from core.model_runtime.model_providers.__base.rerank_model import RerankModel
logger = logging.getLogger(__name__)
class AzureRerankModel(RerankModel):
"""
Model class for Azure AI Studio rerank model.
"""
def _allow_self_signed_https(self, allowed):
# bypass the server certificate verification on client side
if allowed and not os.environ.get("PYTHONHTTPSVERIFY", "") and getattr(ssl, "_create_unverified_context", None):
ssl._create_default_https_context = ssl._create_unverified_context
def _azure_rerank(self, query_input: str, docs: list[str], endpoint: str, api_key: str):
# self._allow_self_signed_https(True) # Enable if using self-signed certificate
data = {"inputs": query_input, "docs": docs}
body = json.dumps(data).encode("utf-8")
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
req = urllib.request.Request(endpoint, body, headers)
try:
with urllib.request.urlopen(req) as response:
result = response.read()
return json.loads(result)
except urllib.error.HTTPError as error:
logger.error(f"The request failed with status code: {error.code}")
logger.error(error.info())
logger.error(error.read().decode("utf8", "ignore"))
raise
def _invoke(
self,
model: str,
credentials: dict,
query: str,
docs: list[str],
score_threshold: Optional[float] = None,
top_n: Optional[int] = None,
user: Optional[str] = None,
) -> RerankResult:
"""
Invoke rerank model
:param model: model name
:param credentials: model credentials
:param query: search query
:param docs: docs for reranking
:param score_threshold: score threshold
:param top_n: top n
:param user: unique user id
:return: rerank result
"""
try:
if len(docs) == 0:
return RerankResult(model=model, docs=[])
endpoint = credentials.get("endpoint")
api_key = credentials.get("jwt_token")
if not endpoint or not api_key:
raise ValueError("Azure endpoint and API key must be provided in credentials")
result = self._azure_rerank(query, docs, endpoint, api_key)
logger.info(f"Azure rerank result: {result}")
rerank_documents = []
for idx, (doc, score_dict) in enumerate(zip(docs, result)):
score = score_dict["score"]
rerank_document = RerankDocument(index=idx, text=doc, score=score)
if score_threshold is None or score >= score_threshold:
rerank_documents.append(rerank_document)
rerank_documents.sort(key=lambda x: x.score, reverse=True)
if top_n:
rerank_documents = rerank_documents[:top_n]
return RerankResult(model=model, docs=rerank_documents)
except Exception as e:
logger.exception(f"Exception in Azure rerank: {e}")
raise
def validate_credentials(self, model: str, credentials: dict) -> None:
"""
Validate model credentials
:param model: model name
:param credentials: model credentials
:return:
"""
try:
self._invoke(
model=model,
credentials=credentials,
query="What is the capital of the United States?",
docs=[
"Carson City is the capital city of the American state of Nevada. At the 2010 United States "
"Census, Carson City had a population of 55,274.",
"The Commonwealth of the Northern Mariana Islands is a group of islands in the Pacific Ocean that "
"are a political division controlled by the United States. Its capital is Saipan.",
],
score_threshold=0.8,
)
except Exception as ex:
raise CredentialsValidateFailedError(str(ex))
@property
def _invoke_error_mapping(self) -> dict[type[InvokeError], list[type[Exception]]]:
"""
Map model invoke error to unified error
The key is the error type thrown to the caller
The value is the error type thrown by the model,
which needs to be converted into a unified error type for the caller.
:return: Invoke error mapping
"""
return {
InvokeConnectionError: [urllib.error.URLError],
InvokeServerUnavailableError: [urllib.error.HTTPError],
InvokeRateLimitError: [InvokeRateLimitError],
InvokeAuthorizationError: [InvokeAuthorizationError],
InvokeBadRequestError: [InvokeBadRequestError, KeyError, ValueError, json.JSONDecodeError],
}
def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity | None:
"""
used to define customizable model schema
"""
entity = AIModelEntity(
model=model,
label=I18nObject(en_US=model),
fetch_from=FetchFrom.CUSTOMIZABLE_MODEL,
model_type=ModelType.RERANK,
model_properties={},
parameter_rules=[],
)
return entity

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

@@ -137,9 +137,19 @@ class TongyiTextEmbeddingModel(_CommonTongyi, TextEmbeddingModel):
input=text,
text_type="document",
)
data = response.output["embeddings"][0]
embeddings.append(data["embedding"])
embedding_used_tokens += response.usage["total_tokens"]
if response.output and "embeddings" in response.output and response.output["embeddings"]:
data = response.output["embeddings"][0]
if "embedding" in data:
embeddings.append(data["embedding"])
else:
raise ValueError("Embedding data is missing in the response.")
else:
raise ValueError("Response output is missing or does not contain embeddings.")
if response.usage and "total_tokens" in response.usage:
embedding_used_tokens += response.usage["total_tokens"]
else:
raise ValueError("Response usage is missing or does not contain total tokens.")
return [list(map(float, e)) for e in embeddings], embedding_used_tokens

View File

@@ -32,6 +32,9 @@ from core.model_runtime.entities.message_entities import (
UserPromptMessage,
)
DEFAULT_V2_ENDPOINT = "maas-api.ml-platform-cn-beijing.volces.com"
DEFAULT_V3_ENDPOINT = "https://ark.cn-beijing.volces.com/api/v3"
class ArkClientV3:
endpoint_id: Optional[str] = None
@@ -43,16 +46,24 @@ class ArkClientV3:
@staticmethod
def is_legacy(credentials: dict) -> bool:
# match default v2 endpoint
if ArkClientV3.is_compatible_with_legacy(credentials):
return False
sdk_version = credentials.get("sdk_version", "v2")
return sdk_version != "v3"
# match default v3 endpoint
if credentials.get("api_endpoint_host") == DEFAULT_V3_ENDPOINT:
return False
# only v3 support api_key
if credentials.get("auth_method") == "api_key":
return False
# these cases are considered as sdk v2
# - modified default v2 endpoint
# - modified default v3 endpoint and auth without api_key
return True
@staticmethod
def is_compatible_with_legacy(credentials: dict) -> bool:
sdk_version = credentials.get("sdk_version")
endpoint = credentials.get("api_endpoint_host")
return sdk_version is None and endpoint == "maas-api.ml-platform-cn-beijing.volces.com"
return endpoint == DEFAULT_V2_ENDPOINT
@classmethod
def from_credentials(cls, credentials):
@@ -60,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"] = "https://ark.cn-beijing.volces.com/api/v3"
args = {
**args,
"base_url": DEFAULT_V3_ENDPOINT
}
client = ArkClientV3(
**args

View File

@@ -38,7 +38,7 @@ configs: dict[str, ModelConfig] = {
),
'Doubao-lite-128k': ModelConfig(
properties=ModelProperties(context_size=131072, max_tokens=4096, mode=LLMMode.CHAT),
features=[ModelFeature.TOOL_CALL]
features=[]
),
'Skylark2-pro-4k': ModelConfig(
properties=ModelProperties(context_size=4096, max_tokens=4096, mode=LLMMode.CHAT),
@@ -54,23 +54,23 @@ configs: dict[str, ModelConfig] = {
),
'Moonshot-v1-8k': ModelConfig(
properties=ModelProperties(context_size=8192, max_tokens=4096, mode=LLMMode.CHAT),
features=[]
features=[ModelFeature.TOOL_CALL]
),
'Moonshot-v1-32k': ModelConfig(
properties=ModelProperties(context_size=32768, max_tokens=16384, mode=LLMMode.CHAT),
features=[]
features=[ModelFeature.TOOL_CALL]
),
'Moonshot-v1-128k': ModelConfig(
properties=ModelProperties(context_size=131072, max_tokens=65536, mode=LLMMode.CHAT),
features=[]
features=[ModelFeature.TOOL_CALL]
),
'GLM3-130B': ModelConfig(
properties=ModelProperties(context_size=8192, max_tokens=4096, mode=LLMMode.CHAT),
features=[]
features=[ModelFeature.TOOL_CALL]
),
'GLM3-130B-Fin': ModelConfig(
properties=ModelProperties(context_size=8192, max_tokens=4096, mode=LLMMode.CHAT),
features=[]
features=[ModelFeature.TOOL_CALL]
),
'Mistral-7B': ModelConfig(
properties=ModelProperties(context_size=8192, max_tokens=2048, mode=LLMMode.CHAT),

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:
@@ -64,7 +98,7 @@ model_credential_schema:
en_US: API Endpoint Host
zh_Hans: API Endpoint Host
type: text-input
default: maas-api.ml-platform-cn-beijing.volces.com
default: https://ark.cn-beijing.volces.com/api/v3
placeholder:
en_US: Enter your API Endpoint Host
zh_Hans: 输入 API Endpoint Host

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

@@ -21,7 +21,6 @@ class LangfuseConfig(BaseTracingConfig):
"""
public_key: str
secret_key: str
project_key: str
host: str = 'https://api.langfuse.com'
@field_validator("host")

View File

@@ -1,5 +1,7 @@
import json
from typing import Any
import logging
from typing import Any, Optional
from urllib.parse import urlparse
import requests
from elasticsearch import Elasticsearch
@@ -7,16 +9,20 @@ from flask import current_app
from pydantic import BaseModel, model_validator
from core.rag.datasource.entity.embedding import Embeddings
from core.rag.datasource.vdb.field import Field
from core.rag.datasource.vdb.vector_base import BaseVector
from core.rag.datasource.vdb.vector_factory import AbstractVectorFactory
from core.rag.datasource.vdb.vector_type import VectorType
from core.rag.models.document import Document
from extensions.ext_redis import redis_client
from models.dataset import Dataset
logger = logging.getLogger(__name__)
class ElasticSearchConfig(BaseModel):
host: str
port: str
port: int
username: str
password: str
@@ -37,12 +43,19 @@ class ElasticSearchVector(BaseVector):
def __init__(self, index_name: str, config: ElasticSearchConfig, attributes: list):
super().__init__(index_name.lower())
self._client = self._init_client(config)
self._version = self._get_version()
self._check_version()
self._attributes = attributes
def _init_client(self, config: ElasticSearchConfig) -> Elasticsearch:
try:
parsed_url = urlparse(config.host)
if parsed_url.scheme in ['http', 'https']:
hosts = f'{config.host}:{config.port}'
else:
hosts = f'http://{config.host}:{config.port}'
client = Elasticsearch(
hosts=f'{config.host}:{config.port}',
hosts=hosts,
basic_auth=(config.username, config.password),
request_timeout=100000,
retry_on_timeout=True,
@@ -53,42 +66,27 @@ class ElasticSearchVector(BaseVector):
return client
def _get_version(self) -> str:
info = self._client.info()
return info['version']['number']
def _check_version(self):
if self._version < '8.0.0':
raise ValueError("Elasticsearch vector database version must be greater than 8.0.0")
def get_type(self) -> str:
return 'elasticsearch'
def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs):
uuids = self._get_uuids(documents)
texts = [d.page_content for d in documents]
metadatas = [d.metadata for d in documents]
if not self._client.indices.exists(index=self._collection_name):
dim = len(embeddings[0])
mapping = {
"properties": {
"text": {
"type": "text"
},
"vector": {
"type": "dense_vector",
"index": True,
"dims": dim,
"similarity": "l2_norm"
},
}
}
self._client.indices.create(index=self._collection_name, mappings=mapping)
added_ids = []
for i, text in enumerate(texts):
for i in range(len(documents)):
self._client.index(index=self._collection_name,
id=uuids[i],
document={
"text": text,
"vector": embeddings[i] if embeddings[i] else None,
"metadata": metadatas[i] if metadatas[i] else {},
Field.CONTENT_KEY.value: documents[i].page_content,
Field.VECTOR.value: embeddings[i] if embeddings[i] else None,
Field.METADATA_KEY.value: documents[i].metadata if documents[i].metadata else {}
})
added_ids.append(uuids[i])
self._client.indices.refresh(index=self._collection_name)
return uuids
@@ -116,28 +114,21 @@ class ElasticSearchVector(BaseVector):
self._client.indices.delete(index=self._collection_name)
def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
query_str = {
"query": {
"script_score": {
"query": {
"match_all": {}
},
"script": {
"source": "cosineSimilarity(params.query_vector, 'vector') + 1.0",
"params": {
"query_vector": query_vector
}
}
}
}
top_k = kwargs.get("top_k", 10)
knn = {
"field": Field.VECTOR.value,
"query_vector": query_vector,
"k": top_k
}
results = self._client.search(index=self._collection_name, body=query_str)
results = self._client.search(index=self._collection_name, knn=knn, size=top_k)
docs_and_scores = []
for hit in results['hits']['hits']:
docs_and_scores.append(
(Document(page_content=hit['_source']['text'], metadata=hit['_source']['metadata']), hit['_score']))
(Document(page_content=hit['_source'][Field.CONTENT_KEY.value],
vector=hit['_source'][Field.VECTOR.value],
metadata=hit['_source'][Field.METADATA_KEY.value]), hit['_score']))
docs = []
for doc, score in docs_and_scores:
@@ -146,25 +137,61 @@ class ElasticSearchVector(BaseVector):
doc.metadata['score'] = score
docs.append(doc)
# Sort the documents by score in descending order
docs = sorted(docs, key=lambda x: x.metadata['score'], reverse=True)
return docs
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
query_str = {
"match": {
"text": query
Field.CONTENT_KEY.value: query
}
}
results = self._client.search(index=self._collection_name, query=query_str)
docs = []
for hit in results['hits']['hits']:
docs.append(Document(page_content=hit['_source']['text'], metadata=hit['_source']['metadata']))
docs.append(Document(
page_content=hit['_source'][Field.CONTENT_KEY.value],
vector=hit['_source'][Field.VECTOR.value],
metadata=hit['_source'][Field.METADATA_KEY.value],
))
return docs
def create(self, texts: list[Document], embeddings: list[list[float]], **kwargs):
return self.add_texts(texts, embeddings, **kwargs)
metadatas = [d.metadata for d in texts]
self.create_collection(embeddings, metadatas)
self.add_texts(texts, embeddings, **kwargs)
def create_collection(
self, embeddings: list, metadatas: Optional[list[dict]] = None, index_params: Optional[dict] = None
):
lock_name = f'vector_indexing_lock_{self._collection_name}'
with redis_client.lock(lock_name, timeout=20):
collection_exist_cache_key = f'vector_indexing_{self._collection_name}'
if redis_client.get(collection_exist_cache_key):
logger.info(f"Collection {self._collection_name} already exists.")
return
if not self._client.indices.exists(index=self._collection_name):
dim = len(embeddings[0])
mappings = {
"properties": {
Field.CONTENT_KEY.value: {"type": "text"},
Field.VECTOR.value: { # Make sure the dimension is correct here
"type": "dense_vector",
"dims": dim,
"similarity": "cosine"
},
Field.METADATA_KEY.value: {
"type": "object",
"properties": {
"doc_id": {"type": "keyword"} # Map doc_id to keyword type
}
}
}
}
self._client.indices.create(index=self._collection_name, mappings=mappings)
redis_client.set(collection_exist_cache_key, 1, ex=3600)
class ElasticSearchVectorFactory(AbstractVectorFactory):

View File

@@ -122,7 +122,7 @@ class MyScaleVector(BaseVector):
def _search(self, dist: str, order: SortOrder, **kwargs: Any) -> list[Document]:
top_k = kwargs.get("top_k", 5)
score_threshold = kwargs.get("score_threshold", 0.0)
score_threshold = kwargs.get('score_threshold') or 0.0
where_str = f"WHERE dist < {1 - score_threshold}" if \
self._metric.upper() == "COSINE" and order == SortOrder.ASC and score_threshold > 0.0 else ""
sql = f"""

View File

@@ -281,20 +281,25 @@ class NotionExtractor(BaseExtractor):
for table_header_cell_text in tabel_header_cell:
text = table_header_cell_text["text"]["content"]
table_header_cell_texts.append(text)
# get table columns text and format
else:
table_header_cell_texts.append('')
# Initialize Markdown table with headers
markdown_table = "| " + " | ".join(table_header_cell_texts) + " |\n"
markdown_table += "| " + " | ".join(['---'] * len(table_header_cell_texts)) + " |\n"
# Process data to format each row in Markdown table format
results = data["results"]
for i in range(len(results) - 1):
column_texts = []
tabel_column_cells = data["results"][i + 1]['table_row']['cells']
for j in range(len(tabel_column_cells)):
if tabel_column_cells[j]:
for table_column_cell_text in tabel_column_cells[j]:
table_column_cells = data["results"][i + 1]['table_row']['cells']
for j in range(len(table_column_cells)):
if table_column_cells[j]:
for table_column_cell_text in table_column_cells[j]:
column_text = table_column_cell_text["text"]["content"]
column_texts.append(f'{table_header_cell_texts[j]}:{column_text}')
cur_result_text = "\n".join(column_texts)
result_lines_arr.append(cur_result_text)
column_texts.append(column_text)
# Add row to Markdown table
markdown_table += "| " + " | ".join(column_texts) + " |\n"
result_lines_arr.append(markdown_table)
if data["next_cursor"] is None:
done = True
break

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

@@ -57,7 +57,7 @@ class BaseIndexProcessor(ABC):
character_splitter = FixedRecursiveCharacterTextSplitter.from_encoder(
chunk_size=segmentation["max_tokens"],
chunk_overlap=segmentation.get('chunk_overlap', 0),
chunk_overlap=segmentation.get('chunk_overlap', 0) or 0,
fixed_separator=separator,
separators=["\n\n", "", ". ", " ", ""],
embedding_model_instance=embedding_model_instance

View File

@@ -1,5 +1,6 @@
- google
- bing
- perplexity
- duckduckgo
- searchapi
- serper
@@ -10,6 +11,7 @@
- wikipedia
- nominatim
- yahoo
- alphavantage
- arxiv
- pubmed
- stablediffusion
@@ -30,5 +32,7 @@
- dingtalk
- feishu
- feishu_base
- feishu_document
- feishu_message
- slack
- tianditu

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="56px" height="56px" viewBox="0 0 56 56" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>形状结合</title>
<g id="设计规范" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M56,0 L56,56 L0,56 L0,0 L56,0 Z M31.6063018,12 L24.3936982,12 L24.1061064,12.7425499 L12.6071308,42.4324141 L12,44 L19.7849972,44 L20.0648488,43.2391815 L22.5196173,36.5567427 L33.4780427,36.5567427 L35.9351512,43.2391815 L36.2150028,44 L44,44 L43.3928692,42.4324141 L31.8938936,12.7425499 L31.6063018,12 Z M28.0163803,21.5755126 L31.1613993,30.2523823 L24.8432808,30.2523823 L28.0163803,21.5755126 Z" id="形状结合" fill="#2F4F4F"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 780 B

View File

@@ -0,0 +1,22 @@
from typing import Any
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.provider.builtin.alphavantage.tools.query_stock import QueryStockTool
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
class AlphaVantageProvider(BuiltinToolProviderController):
def _validate_credentials(self, credentials: dict[str, Any]) -> None:
try:
QueryStockTool().fork_tool_runtime(
runtime={
"credentials": credentials,
}
).invoke(
user_id='',
tool_parameters={
"code": "AAPL", # Apple Inc.
},
)
except Exception as e:
raise ToolProviderCredentialValidationError(str(e))

View File

@@ -0,0 +1,31 @@
identity:
author: zhuhao
name: alphavantage
label:
en_US: AlphaVantage
zh_Hans: AlphaVantage
pt_BR: AlphaVantage
description:
en_US: AlphaVantage is an online platform that provides financial market data and APIs, making it convenient for individual investors and developers to access stock quotes, technical indicators, and stock analysis.
zh_Hans: AlphaVantage是一个在线平台它提供金融市场数据和API便于个人投资者和开发者获取股票报价、技术指标和股票分析。
pt_BR: AlphaVantage is an online platform that provides financial market data and APIs, making it convenient for individual investors and developers to access stock quotes, technical indicators, and stock analysis.
icon: icon.svg
tags:
- finance
credentials_for_provider:
api_key:
type: secret-input
required: true
label:
en_US: AlphaVantage API key
zh_Hans: AlphaVantage API key
pt_BR: AlphaVantage API key
placeholder:
en_US: Please input your AlphaVantage API key
zh_Hans: 请输入你的 AlphaVantage API key
pt_BR: Please input your AlphaVantage API key
help:
en_US: Get your AlphaVantage API key from AlphaVantage
zh_Hans: 从 AlphaVantage 获取您的 AlphaVantage API key
pt_BR: Get your AlphaVantage API key from AlphaVantage
url: https://www.alphavantage.co/support/#api-key

View File

@@ -0,0 +1,49 @@
from typing import Any, Union
import requests
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
ALPHAVANTAGE_API_URL = "https://www.alphavantage.co/query"
class QueryStockTool(BuiltinTool):
def _invoke(self,
user_id: str,
tool_parameters: dict[str, Any],
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
stock_code = tool_parameters.get('code', '')
if not stock_code:
return self.create_text_message('Please tell me your stock code')
if 'api_key' not in self.runtime.credentials or not self.runtime.credentials.get('api_key'):
return self.create_text_message("Alpha Vantage API key is required.")
params = {
"function": "TIME_SERIES_DAILY",
"symbol": stock_code,
"outputsize": "compact",
"datatype": "json",
"apikey": self.runtime.credentials['api_key']
}
response = requests.get(url=ALPHAVANTAGE_API_URL, params=params)
response.raise_for_status()
result = self._handle_response(response.json())
return self.create_json_message(result)
def _handle_response(self, response: dict[str, Any]) -> dict[str, Any]:
result = response.get('Time Series (Daily)', {})
if not result:
return {}
stock_result = {}
for k, v in result.items():
stock_result[k] = {}
stock_result[k]['open'] = v.get('1. open')
stock_result[k]['high'] = v.get('2. high')
stock_result[k]['low'] = v.get('3. low')
stock_result[k]['close'] = v.get('4. close')
stock_result[k]['volume'] = v.get('5. volume')
return stock_result

View File

@@ -0,0 +1,27 @@
identity:
name: query_stock
author: zhuhao
label:
en_US: query_stock
zh_Hans: query_stock
pt_BR: query_stock
description:
human:
en_US: Retrieve information such as daily opening price, daily highest price, daily lowest price, daily closing price, and daily trading volume for a specified stock symbol.
zh_Hans: 获取指定股票代码的每日开盘价、每日最高价、每日最低价、每日收盘价和每日交易量等信息。
pt_BR: Retrieve information such as daily opening price, daily highest price, daily lowest price, daily closing price, and daily trading volume for a specified stock symbol
llm: Retrieve information such as daily opening price, daily highest price, daily lowest price, daily closing price, and daily trading volume for a specified stock symbol
parameters:
- name: code
type: string
required: true
label:
en_US: stock code
zh_Hans: 股票代码
pt_BR: stock code
human_description:
en_US: stock code
zh_Hans: 股票代码
pt_BR: stock code
llm_description: stock code for query from alphavantage
form: llm

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="64px" height="64px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
<g><path style="opacity:1" fill="#fefefe" d="M -0.5,-0.5 C 20.8333,-0.5 42.1667,-0.5 63.5,-0.5C 63.5,20.8333 63.5,42.1667 63.5,63.5C 42.1667,63.5 20.8333,63.5 -0.5,63.5C -0.5,42.1667 -0.5,20.8333 -0.5,-0.5 Z"/></g>
<g><path style="opacity:1" fill="#346df3" d="M 47.5,33.5 C 43.3272,29.8779 38.9939,29.7112 34.5,33C 32.682,35.4897 30.3487,37.3231 27.5,38.5C 23.5003,43.5136 24.167,47.847 29.5,51.5C 24.1563,51.666 18.8229,51.4994 13.5,51C 13,50.5 12.5,50 12,49.5C 11.3333,36.8333 11.3333,24.1667 12,11.5C 12.5,11 13,10.5 13.5,10C 24.1667,9.33333 34.8333,9.33333 45.5,10C 46,10.5 46.5,11 47,11.5C 47.4997,18.8258 47.6663,26.1591 47.5,33.5 Z"/></g>
<g><path style="opacity:1" fill="#f9fafe" d="M 20.5,19.5 C 25.1785,19.3342 29.8452,19.5008 34.5,20C 35.8333,21 35.8333,22 34.5,23C 29.8333,23.6667 25.1667,23.6667 20.5,23C 19.3157,21.8545 19.3157,20.6879 20.5,19.5 Z"/></g>
<g><path style="opacity:1" fill="#f3f6fe" d="M 20.5,27.5 C 22.5273,27.3379 24.5273,27.5045 26.5,28C 27.8333,29 27.8333,30 26.5,31C 24.5,31.6667 22.5,31.6667 20.5,31C 19.3157,29.8545 19.3157,28.6879 20.5,27.5 Z"/></g>
<g><path style="opacity:1" fill="#36d4c1" d="M 47.5,33.5 C 48.7298,35.2972 49.3964,37.2972 49.5,39.5C 51.3904,39.2965 52.8904,39.9632 54,41.5C 55.1825,45.2739 54.3492,48.4406 51.5,51C 44.1742,51.4997 36.8409,51.6663 29.5,51.5C 24.167,47.847 23.5003,43.5136 27.5,38.5C 30.3487,37.3231 32.682,35.4897 34.5,33C 38.9939,29.7112 43.3272,29.8779 47.5,33.5 Z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,15 @@
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
from core.tools.utils.feishu_api_utils import FeishuRequest
class FeishuDocumentProvider(BuiltinToolProviderController):
def _validate_credentials(self, credentials: dict) -> None:
app_id = credentials.get('app_id')
app_secret = credentials.get('app_secret')
if not app_id or not app_secret:
raise ToolProviderCredentialValidationError("app_id and app_secret is required")
try:
assert FeishuRequest(app_id, app_secret).tenant_access_token is not None
except Exception as e:
raise ToolProviderCredentialValidationError(str(e))

View File

@@ -0,0 +1,34 @@
identity:
author: Doug Lea
name: feishu_document
label:
en_US: Lark Cloud Document
zh_Hans: 飞书云文档
description:
en_US: Lark Cloud Document
zh_Hans: 飞书云文档
icon: icon.svg
tags:
- social
- productivity
credentials_for_provider:
app_id:
type: text-input
required: true
label:
en_US: APP ID
placeholder:
en_US: Please input your feishu app id
zh_Hans: 请输入你的飞书 app id
help:
en_US: Get your app_id and app_secret from Feishu
zh_Hans: 从飞书获取您的 app_id 和 app_secret
url: https://open.feishu.cn
app_secret:
type: secret-input
required: true
label:
en_US: APP Secret
placeholder:
en_US: Please input your app secret
zh_Hans: 请输入你的飞书 app secret

View File

@@ -0,0 +1,19 @@
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.utils.feishu_api_utils import FeishuRequest
class CreateDocumentTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
app_id = self.runtime.credentials.get('app_id')
app_secret = self.runtime.credentials.get('app_secret')
client = FeishuRequest(app_id, app_secret)
title = tool_parameters.get('title')
content = tool_parameters.get('content')
folder_token = tool_parameters.get('folder_token')
res = client.create_document(title, content, folder_token)
return self.create_json_message(res)

View File

@@ -0,0 +1,47 @@
identity:
name: create_document
author: Doug Lea
label:
en_US: Create Lark document
zh_Hans: 创建飞书文档
description:
human:
en_US: Create Lark document
zh_Hans: 创建飞书文档,支持创建空文档和带内容的文档,支持 markdown 语法创建。
llm: A tool for creating Feishu documents.
parameters:
- name: title
type: string
required: false
label:
en_US: Document title
zh_Hans: 文档标题
human_description:
en_US: Document title, only supports plain text content.
zh_Hans: 文档标题,只支持纯文本内容。
llm_description: 文档标题,只支持纯文本内容,可以为空。
form: llm
- name: content
type: string
required: false
label:
en_US: Document content
zh_Hans: 文档内容
human_description:
en_US: Document content, supports markdown syntax, can be empty.
zh_Hans: 文档内容,支持 markdown 语法,可以为空。
llm_description: 文档内容,支持 markdown 语法,可以为空。
form: llm
- name: folder_token
type: string
required: false
label:
en_US: folder_token
zh_Hans: 文档所在文件夹的 Token
human_description:
en_US: The token of the folder where the document is located. If it is not passed or is empty, it means the root directory.
zh_Hans: 文档所在文件夹的 Token不传或传空表示根目录。
llm_description: 文档所在文件夹的 Token不传或传空表示根目录。
form: llm

View File

@@ -0,0 +1,17 @@
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.utils.feishu_api_utils import FeishuRequest
class GetDocumentRawContentTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
app_id = self.runtime.credentials.get('app_id')
app_secret = self.runtime.credentials.get('app_secret')
client = FeishuRequest(app_id, app_secret)
document_id = tool_parameters.get('document_id')
res = client.get_document_raw_content(document_id)
return self.create_json_message(res)

View File

@@ -0,0 +1,23 @@
identity:
name: get_document_raw_content
author: Doug Lea
label:
en_US: Get Document Raw Content
zh_Hans: 获取文档纯文本内容
description:
human:
en_US: Get document raw content
zh_Hans: 获取文档纯文本内容
llm: A tool for getting the plain text content of Feishu documents
parameters:
- name: document_id
type: string
required: true
label:
en_US: document_id
zh_Hans: 飞书文档的唯一标识
human_description:
en_US: Unique ID of Feishu document document_id
zh_Hans: 飞书文档的唯一标识 document_id
llm_description: 飞书文档的唯一标识 document_id
form: llm

View File

@@ -0,0 +1,19 @@
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.utils.feishu_api_utils import FeishuRequest
class ListDocumentBlockTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
app_id = self.runtime.credentials.get('app_id')
app_secret = self.runtime.credentials.get('app_secret')
client = FeishuRequest(app_id, app_secret)
document_id = tool_parameters.get('document_id')
page_size = tool_parameters.get('page_size', 500)
page_token = tool_parameters.get('page_token', '')
res = client.list_document_block(document_id, page_token, page_size)
return self.create_json_message(res)

View File

@@ -0,0 +1,48 @@
identity:
name: list_document_block
author: Doug Lea
label:
en_US: List Document Block
zh_Hans: 获取飞书文档所有块
description:
human:
en_US: List document block
zh_Hans: 获取飞书文档所有块的富文本内容并分页返回。
llm: A tool to get all blocks of Feishu documents
parameters:
- name: document_id
type: string
required: true
label:
en_US: document_id
zh_Hans: 飞书文档的唯一标识
human_description:
en_US: Unique ID of Feishu document document_id
zh_Hans: 飞书文档的唯一标识 document_id
llm_description: 飞书文档的唯一标识 document_id
form: llm
- name: page_size
type: number
required: false
default: 500
label:
en_US: page_size
zh_Hans: 分页大小
human_description:
en_US: Paging size, the default and maximum value is 500.
zh_Hans: 分页大小, 默认值和最大值为 500。
llm_description: 分页大小, 表示一次请求最多返回多少条数据,默认值和最大值为 500。
form: llm
- name: page_token
type: string
required: false
label:
en_US: page_token
zh_Hans: 分页标记
human_description:
en_US: Pagination tag, used to paginate query results so that more items can be obtained in the next traversal.
zh_Hans: 分页标记,用于分页查询结果,以便下次遍历时获取更多项。
llm_description: 分页标记,第一次请求不填,表示从头开始遍历;分页查询结果还有更多项时会同时返回新的 page_token下次遍历可采用该 page_token 获取查询结果。
form: llm

View File

@@ -0,0 +1,19 @@
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.utils.feishu_api_utils import FeishuRequest
class CreateDocumentTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
app_id = self.runtime.credentials.get('app_id')
app_secret = self.runtime.credentials.get('app_secret')
client = FeishuRequest(app_id, app_secret)
document_id = tool_parameters.get('document_id')
content = tool_parameters.get('content')
position = tool_parameters.get('position')
res = client.write_document(document_id, content, position)
return self.create_json_message(res)

View File

@@ -0,0 +1,56 @@
identity:
name: write_document
author: Doug Lea
label:
en_US: Write Document
zh_Hans: 在飞书文档中新增内容
description:
human:
en_US: Adding new content to Lark documents
zh_Hans: 在飞书文档中新增内容
llm: A tool for adding new content to Lark documents.
parameters:
- name: document_id
type: string
required: true
label:
en_US: document_id
zh_Hans: 飞书文档的唯一标识
human_description:
en_US: Unique ID of Feishu document document_id
zh_Hans: 飞书文档的唯一标识 document_id
llm_description: 飞书文档的唯一标识 document_id
form: llm
- name: content
type: string
required: true
label:
en_US: document content
zh_Hans: 文档内容
human_description:
en_US: Document content, supports markdown syntax, can be empty.
zh_Hans: 文档内容,支持 markdown 语法,可以为空。
llm_description:
form: llm
- name: position
type: select
required: true
default: start
label:
en_US: Choose where to add content
zh_Hans: 选择添加内容的位置
human_description:
en_US: Please fill in start or end to add content at the beginning or end of the document respectively.
zh_Hans: 请填入 start 或 end, 分别表示在文档开头(start)或结尾(end)添加内容。
form: llm
options:
- value: start
label:
en_US: start
zh_Hans: 在文档开头添加内容
- value: end
label:
en_US: end
zh_Hans: 在文档结尾添加内容

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="64px" height="64px" viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve"> <image id="image0" width="64" height="64" x="0" y="0"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAIGNIUk0AAHomAACAhAAA+gAAAIDo
AAB1MAAA6mAAADqYAAAXcJy6UTwAAAC9UExURf///////+bs/vL2/qa/+n+j+E1/9TNt9FmI9nOa
+Obt/sza/GaR97PI+9nk/aa/+5m2+oCk+Iyt+Yys+eXt/oCj+L/R+4yt+HOb+Ex/9TOA6jOi2jO8
zTPJxzPWwDOa3eb69zN67X/l2DOb3TPPw0DZxLPv55nq4LPw6DOB6vL9+0B29TOo16bt4zPCynPj
00zbyDN08WbgzzOH50DYxFmI9bLI+5nr34zn3OX699n384zo21ndyzTWwJnq37nAcdIAAAABdFJO
U/4a4wd9AAAAAWJLR0QAiAUdSAAAAAlwSFlzAAAWJQAAFiUBSVIk8AAAAAd0SU1FB+gHEggfEk4D
XiUAAAFOSURBVFjD7dVZU8IwFAXgpq2NtFFRUVTKtYC4gCvu6///WcCMI9Cc3CR2fLLn/XyT3KRp
IComqIEa+GMgDMNfA1G8lsh51htx6g9kSi5HbfgBm6v1eZLUA9iSKE1nYFviqMgNMPVn44xcgB1p
jnIAmpLLrhVoST6ZDdizAMoCZNKWjAdsC8BLWACRtS9lygH7DkDMAW0H4IADlANwyAEJUzzq5F2i
bn5cMIC53svpJ/3CHxic0FKGp75Ah0o585uB1ic69zmFnt6nYQEBfA9yAFDf/SZeEMwIfgtjAFxi
4AoBcA/XGLiBAHoPcJ9uISAaWv/OABAGWuOKgIgrbgHM0TDEiQnQHnavY0Tfwz0GCgMA/kweVxm/
y2gJD4UJQJd5wE6gfIxlIXlsPz1rwIsRwNGFkR8gXicVASHe3j++u5+zfHlugU8N1MD/AQI2U2Cm
Yux2lsz2AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDI0LTA3LTE4VDA4OjMxOjE4KzAwOjAwPdC6HgAA
ACV0RVh0ZGF0ZTptb2RpZnkAMjAyNC0wNy0xOFQwODozMToxOCswMDowMEyNAqIAAAAodEVYdGRh
dGU6dGltZXN0YW1wADIwMjQtMDctMThUMDg6MzE6MTgrMDA6MDAbmCN9AAAAAElFTkSuQmCC" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,15 @@
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
from core.tools.utils.feishu_api_utils import FeishuRequest
class FeishuMessageProvider(BuiltinToolProviderController):
def _validate_credentials(self, credentials: dict) -> None:
app_id = credentials.get('app_id')
app_secret = credentials.get('app_secret')
if not app_id or not app_secret:
raise ToolProviderCredentialValidationError("app_id and app_secret is required")
try:
assert FeishuRequest(app_id, app_secret).tenant_access_token is not None
except Exception as e:
raise ToolProviderCredentialValidationError(str(e))

View File

@@ -0,0 +1,34 @@
identity:
author: Doug Lea
name: feishu_message
label:
en_US: Lark Message
zh_Hans: 飞书消息
description:
en_US: Lark Message
zh_Hans: 飞书消息
icon: icon.svg
tags:
- social
- productivity
credentials_for_provider:
app_id:
type: text-input
required: true
label:
en_US: APP ID
placeholder:
en_US: Please input your feishu app id
zh_Hans: 请输入你的飞书 app id
help:
en_US: Get your app_id and app_secret from Feishu
zh_Hans: 从飞书获取您的 app_id 和 app_secret
url: https://open.feishu.cn
app_secret:
type: secret-input
required: true
label:
en_US: APP Secret
placeholder:
en_US: Please input your app secret
zh_Hans: 请输入你的飞书 app secret

View File

@@ -0,0 +1,20 @@
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.utils.feishu_api_utils import FeishuRequest
class SendBotMessageTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> ToolInvokeMessage:
app_id = self.runtime.credentials.get('app_id')
app_secret = self.runtime.credentials.get('app_secret')
client = FeishuRequest(app_id, app_secret)
receive_id_type = tool_parameters.get('receive_id_type')
receive_id = tool_parameters.get('receive_id')
msg_type = tool_parameters.get('msg_type')
content = tool_parameters.get('content')
res = client.send_bot_message(receive_id_type, receive_id, msg_type, content)
return self.create_json_message(res)

View File

@@ -0,0 +1,91 @@
identity:
name: send_bot_message
author: Doug Lea
label:
en_US: Send Bot Message
zh_Hans: 发送飞书应用消息
description:
human:
en_US: Send bot message
zh_Hans: 发送飞书应用消息
llm: A tool for sending Feishu application messages.
parameters:
- name: receive_id_type
type: select
required: true
options:
- value: open_id
label:
en_US: open id
zh_Hans: open id
- value: union_id
label:
en_US: union id
zh_Hans: union id
- value: user_id
label:
en_US: user id
zh_Hans: user id
- value: email
label:
en_US: email
zh_Hans: email
- value: chat_id
label:
en_US: chat id
zh_Hans: chat id
label:
en_US: User ID Type
zh_Hans: 用户 ID 类型
human_description:
en_US: User ID Type
zh_Hans: 用户 ID 类型,可选值有 open_id、union_id、user_id、email、chat_id。
llm_description: 用户 ID 类型,可选值有 open_id、union_id、user_id、email、chat_id。
form: llm
- name: receive_id
type: string
required: true
label:
en_US: Receive Id
zh_Hans: 消息接收者的 ID
human_description:
en_US: The ID of the message receiver. The ID type should correspond to the query parameter receive_id_type.
zh_Hans: 消息接收者的 IDID 类型应与查询参数 receive_id_type 对应。
llm_description: 消息接收者的 IDID 类型应与查询参数 receive_id_type 对应。
form: llm
- name: msg_type
type: string
required: true
options:
- value: text
label:
en_US: text
zh_Hans: 文本
- value: interactive
label:
en_US: message card
zh_Hans: 消息卡片
label:
en_US: Message type
zh_Hans: 消息类型
human_description:
en_US: Message type, optional values are, text (text), interactive (message card).
zh_Hans: 消息类型可选值有text(文本)、interactive(消息卡片)。
llm_description: 消息类型可选值有text(文本)、interactive(消息卡片)。
form: llm
- name: content
type: string
required: true
label:
en_US: Message content
zh_Hans: 消息内容
human_description:
en_US: Message content
zh_Hans: |
消息内容JSON 结构序列化后的字符串。不同 msg_type 对应不同内容,
具体格式说明参考https://open.larkoffice.com/document/server-docs/im-v1/message-content-description/create_json
llm_description: 消息内容JSON 结构序列化后的字符串。不同 msg_type 对应不同内容。
form: llm

View File

@@ -0,0 +1,19 @@
from typing import Any
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.tool.builtin_tool import BuiltinTool
from core.tools.utils.feishu_api_utils import FeishuRequest
class SendWebhookMessageTool(BuiltinTool):
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) ->ToolInvokeMessage:
app_id = self.runtime.credentials.get('app_id')
app_secret = self.runtime.credentials.get('app_secret')
client = FeishuRequest(app_id, app_secret)
webhook = tool_parameters.get('webhook')
msg_type = tool_parameters.get('msg_type')
content = tool_parameters.get('content')
res = client.send_webhook_message(webhook, msg_type, content)
return self.create_json_message(res)

View File

@@ -0,0 +1,58 @@
identity:
name: send_webhook_message
author: Doug Lea
label:
en_US: Send Webhook Message
zh_Hans: 使用自定义机器人发送飞书消息
description:
human:
en_US: Send webhook message
zh_Hans: 使用自定义机器人发送飞书消息
llm: A tool for sending Lark messages using a custom robot.
parameters:
- name: webhook
type: string
required: true
label:
en_US: webhook
zh_Hans: webhook 的地址
human_description:
en_US: The address of the webhook
zh_Hans: webhook 的地址
llm_description: webhook 的地址
form: llm
- name: msg_type
type: string
required: true
options:
- value: text
label:
en_US: text
zh_Hans: 文本
- value: interactive
label:
en_US: message card
zh_Hans: 消息卡片
label:
en_US: Message type
zh_Hans: 消息类型
human_description:
en_US: Message type, optional values are, text (text), interactive (message card).
zh_Hans: 消息类型可选值有text(文本)、interactive(消息卡片)。
llm_description: 消息类型可选值有text(文本)、interactive(消息卡片)。
form: llm
- name: content
type: string
required: true
label:
en_US: Message content
zh_Hans: 消息内容
human_description:
en_US: Message content
zh_Hans: |
消息内容JSON 结构序列化后的字符串。不同 msg_type 对应不同内容,
具体格式说明参考https://open.larkoffice.com/document/server-docs/im-v1/message-content-description/create_json
llm_description: 消息内容JSON 结构序列化后的字符串。不同 msg_type 对应不同内容。
form: llm

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

@@ -0,0 +1,143 @@
import httpx
from extensions.ext_redis import redis_client
class FeishuRequest:
def __init__(self, app_id: str, app_secret: str):
self.app_id = app_id
self.app_secret = app_secret
@property
def tenant_access_token(self):
feishu_tenant_access_token = f"tools:{self.app_id}:feishu_tenant_access_token"
if redis_client.exists(feishu_tenant_access_token):
return redis_client.get(feishu_tenant_access_token).decode()
res = self.get_tenant_access_token(self.app_id, self.app_secret)
redis_client.setex(feishu_tenant_access_token, res.get("expire"), res.get("tenant_access_token"))
return res.get("tenant_access_token")
def _send_request(self, url: str, method: str = "post", require_token: bool = True, payload: dict = None,
params: dict = None):
headers = {
"Content-Type": "application/json",
"user-agent": "Dify",
}
if require_token:
headers["tenant-access-token"] = f"{self.tenant_access_token}"
res = httpx.request(method=method, url=url, headers=headers, json=payload, params=params, timeout=30).json()
if res.get("code") != 0:
raise Exception(res)
return res
def get_tenant_access_token(self, app_id: str, app_secret: str) -> dict:
"""
API url: https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal
Example Response:
{
"code": 0,
"msg": "ok",
"tenant_access_token": "t-caecc734c2e3328a62489fe0648c4b98779515d3",
"expire": 7200
}
"""
url = "https://lark-plugin-api.solutionsuite.cn/lark-plugin/access_token/get_tenant_access_token"
payload = {
"app_id": app_id,
"app_secret": app_secret
}
res = self._send_request(url, require_token=False, payload=payload)
return res
def create_document(self, title: str, content: str, folder_token: str) -> dict:
"""
API url: https://open.larkoffice.com/document/server-docs/docs/docs/docx-v1/document/create
Example Response:
{
"data": {
"title": "title",
"url": "https://svi136aogf123.feishu.cn/docx/VWbvd4fEdoW0WSxaY1McQTz8n7d",
"type": "docx",
"token": "VWbvd4fEdoW0WSxaY1McQTz8n7d"
},
"log_id": "021721281231575fdbddc0200ff00060a9258ec0000103df61b5d",
"code": 0,
"msg": "创建飞书文档成功,请查看"
}
"""
url = "https://lark-plugin-api.solutionsuite.cn/lark-plugin/document/create_document"
payload = {
"title": title,
"content": content,
"folder_token": folder_token,
}
res = self._send_request(url, payload=payload)
return res.get("data")
def write_document(self, document_id: str, content: str, position: str = "start") -> dict:
url = "https://lark-plugin-api.solutionsuite.cn/lark-plugin/document/write_document"
payload = {
"document_id": document_id,
"content": content,
"position": position
}
res = self._send_request(url, payload=payload)
return res.get("data")
def get_document_raw_content(self, document_id: str) -> dict:
"""
API url: https://open.larkoffice.com/document/server-docs/docs/docs/docx-v1/document/raw_content
Example Response:
{
"code": 0,
"msg": "success",
"data": {
"content": "云文档\n多人实时协同,插入一切元素。不仅是在线文档,更是强大的创作和互动工具\n云文档:专为协作而生\n"
}
}
"""
params = {
"document_id": document_id,
}
url = "https://lark-plugin-api.solutionsuite.cn/lark-plugin/document/get_document_raw_content"
res = self._send_request(url, method="get", params=params)
return res.get("data").get("content")
def list_document_block(self, document_id: str, page_token: str, page_size: int = 500) -> dict:
"""
API url: https://open.larkoffice.com/document/server-docs/docs/docs/docx-v1/document/list
"""
url = "https://lark-plugin-api.solutionsuite.cn/lark-plugin/document/list_document_block"
params = {
"document_id": document_id,
"page_size": page_size,
"page_token": page_token,
}
res = self._send_request(url, method="get", params=params)
return res.get("data")
def send_bot_message(self, receive_id_type: str, receive_id: str, msg_type: str, content: str) -> dict:
"""
API url: https://open.larkoffice.com/document/server-docs/im-v1/message/create
"""
url = "https://lark-plugin-api.solutionsuite.cn/lark-plugin/message/send_bot_message"
params = {
"receive_id_type": receive_id_type,
}
payload = {
"receive_id": receive_id,
"msg_type": msg_type,
"content": content,
}
res = self._send_request(url, params=params, payload=payload)
return res.get("data")
def send_webhook_message(self, webhook: str, msg_type: str, content: str) -> dict:
url = "https://lark-plugin-api.solutionsuite.cn/lark-plugin/message/send_webhook_message"
payload = {
"webhook": webhook,
"msg_type": msg_type,
"content": content,
}
res = self._send_request(url, require_token=False, payload=payload)
return res

View File

@@ -26,7 +26,6 @@ def load_yaml_file(file_path: str, ignore_error: bool = True, default_value: Any
raise YAMLError(f'Failed to load YAML file {file_path}: {e}')
except Exception as e:
if ignore_error:
logger.debug(f'Failed to load YAML file {file_path}: {e}')
return default_value
else:
raise e

View File

@@ -88,9 +88,9 @@ class CodeNode(BaseNode):
else:
raise ValueError(f"Output variable `{variable}` must be a string")
if len(value) > dify_config.CODE_MAX_STRING_ARRAY_LENGTH:
if len(value) > dify_config.CODE_MAX_STRING_LENGTH:
raise ValueError(f'The length of output variable `{variable}` must be'
f' less than {dify_config.CODE_MAX_STRING_ARRAY_LENGTH} characters')
f' less than {dify_config.CODE_MAX_STRING_LENGTH} characters')
return value.replace('\x00', '')

View File

@@ -5,10 +5,6 @@ from pydantic import BaseModel, ValidationInfo, field_validator
from configs import dify_config
from core.workflow.entities.base_node_data_entities import BaseNodeData
MAX_CONNECT_TIMEOUT = dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT
MAX_READ_TIMEOUT = dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT
MAX_WRITE_TIMEOUT = dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT
class HttpRequestNodeAuthorizationConfig(BaseModel):
type: Literal[None, 'basic', 'bearer', 'custom']
@@ -41,9 +37,9 @@ class HttpRequestNodeBody(BaseModel):
class HttpRequestNodeTimeout(BaseModel):
connect: int = MAX_CONNECT_TIMEOUT
read: int = MAX_READ_TIMEOUT
write: int = MAX_WRITE_TIMEOUT
connect: int = dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT
read: int = dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT
write: int = dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT
class HttpRequestNodeData(BaseNodeData):

View File

@@ -3,6 +3,7 @@ from mimetypes import guess_extension
from os import path
from typing import cast
from configs import dify_config
from core.app.segments import parser
from core.file.file_obj import FileTransferMethod, FileType, FileVar
from core.tools.tool_file_manager import ToolFileManager
@@ -11,9 +12,6 @@ from core.workflow.entities.node_entities import NodeRunResult, NodeType
from core.workflow.entities.variable_pool import VariablePool
from core.workflow.nodes.base_node import BaseNode
from core.workflow.nodes.http_request.entities import (
MAX_CONNECT_TIMEOUT,
MAX_READ_TIMEOUT,
MAX_WRITE_TIMEOUT,
HttpRequestNodeData,
HttpRequestNodeTimeout,
)
@@ -21,9 +19,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, MAX_CONNECT_TIMEOUT),
read=min(60, MAX_READ_TIMEOUT),
write=min(20, 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,
)
@@ -43,9 +41,9 @@ class HttpRequestNode(BaseNode):
'body': {'type': 'none'},
'timeout': {
**HTTP_REQUEST_DEFAULT_TIMEOUT.model_dump(),
'max_connect_timeout': MAX_CONNECT_TIMEOUT,
'max_read_timeout': MAX_READ_TIMEOUT,
'max_write_timeout': MAX_WRITE_TIMEOUT,
'max_connect_timeout': dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT,
'max_read_timeout': dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT,
'max_write_timeout': dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT,
},
},
}
@@ -92,17 +90,15 @@ class HttpRequestNode(BaseNode):
},
)
def _get_request_timeout(self, node_data: HttpRequestNodeData) -> HttpRequestNodeTimeout:
@staticmethod
def _get_request_timeout(node_data: HttpRequestNodeData) -> HttpRequestNodeTimeout:
timeout = node_data.timeout
if timeout is None:
return HTTP_REQUEST_DEFAULT_TIMEOUT
timeout.connect = timeout.connect or HTTP_REQUEST_DEFAULT_TIMEOUT.connect
timeout.connect = min(timeout.connect, MAX_CONNECT_TIMEOUT)
timeout.read = timeout.read or HTTP_REQUEST_DEFAULT_TIMEOUT.read
timeout.read = min(timeout.read, MAX_READ_TIMEOUT)
timeout.write = timeout.write or HTTP_REQUEST_DEFAULT_TIMEOUT.write
timeout.write = min(timeout.write, MAX_WRITE_TIMEOUT)
return timeout
@classmethod

View File

@@ -17,7 +17,7 @@ class AdvancedSettings(BaseModel):
"""
Group.
"""
output_type: Literal['string', 'number', 'array', 'object']
output_type: Literal['string', 'number', 'object', 'array[string]', 'array[number]', 'array[object]']
variables: list[list[str]]
group_name: str
@@ -30,4 +30,4 @@ class VariableAssignerNodeData(BaseNodeData):
type: str = 'variable-assigner'
output_type: str
variables: list[list[str]]
advanced_settings: Optional[AdvancedSettings] = None
advanced_settings: Optional[AdvancedSettings] = None

View File

@@ -17,6 +17,8 @@ def handle(sender, **kwargs):
default_language=account.interface_language,
customize_token_strategy="not_allow",
code=Site.generate_code(16),
created_by=app.created_by,
updated_by=app.updated_by,
)
db.session.add(site)

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)

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