Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
D
dify
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tech
dify
Commits
9098d099
Commit
9098d099
authored
Jul 18, 2023
by
Joel
Browse files
Options
Browse Files
Download
Plain Diff
feat: merge main
parents
ca8939ab
ecd6cbae
Changes
133
Show whitespace changes
Inline
Side-by-side
Showing
133 changed files
with
2587 additions
and
751 deletions
+2587
-751
README.md
README.md
+9
-3
README_CN.md
README_CN.md
+9
-4
.env.example
api/.env.example
+15
-4
Dockerfile
api/Dockerfile
+5
-3
app.py
api/app.py
+12
-2
commands.py
api/commands.py
+33
-1
config.py
api/config.py
+27
-6
__init__.py
api/controllers/console/__init__.py
+1
-1
audio.py
api/controllers/console/app/audio.py
+2
-2
completion.py
api/controllers/console/app/completion.py
+6
-6
error.py
api/controllers/console/app/error.py
+1
-1
generator.py
api/controllers/console/app/generator.py
+4
-4
message.py
api/controllers/console/app/message.py
+6
-6
activate.py
api/controllers/console/auth/activate.py
+75
-0
data_source_oauth.py
api/controllers/console/auth/data_source_oauth.py
+5
-5
oauth.py
api/controllers/console/auth/oauth.py
+3
-3
datasets_document.py
api/controllers/console/datasets/datasets_document.py
+4
-4
hit_testing.py
api/controllers/console/datasets/hit_testing.py
+2
-2
error.py
api/controllers/console/error.py
+6
-0
audio.py
api/controllers/console/explore/audio.py
+2
-2
completion.py
api/controllers/console/explore/completion.py
+6
-6
message.py
api/controllers/console/explore/message.py
+6
-6
account.py
api/controllers/console/workspace/account.py
+8
-4
error.py
api/controllers/console/workspace/error.py
+6
-0
members.py
api/controllers/console/workspace/members.py
+14
-4
providers.py
api/controllers/console/workspace/providers.py
+21
-6
audio.py
api/controllers/service_api/app/audio.py
+2
-2
completion.py
api/controllers/service_api/app/completion.py
+6
-6
conversation.py
api/controllers/service_api/app/conversation.py
+6
-6
document.py
api/controllers/service_api/dataset/document.py
+2
-2
audio.py
api/controllers/web/audio.py
+2
-2
completion.py
api/controllers/web/completion.py
+6
-6
message.py
api/controllers/web/message.py
+6
-6
__init__.py
api/core/__init__.py
+8
-0
llm_callback_handler.py
api/core/callback_handler/llm_callback_handler.py
+1
-1
completion.py
api/core/completion.py
+36
-20
llm_constant.py
api/core/constant/llm_constant.py
+19
-2
conversation_message_task.py
api/core/conversation_message_task.py
+3
-2
cached_embedding.py
api/core/embedding/cached_embedding.py
+2
-0
llm_generator.py
api/core/generator/llm_generator.py
+15
-1
index.py
api/core/index/index.py
+1
-1
error.py
api/core/llm/error.py
+3
-0
llm_builder.py
api/core/llm/llm_builder.py
+44
-28
anthropic_provider.py
api/core/llm/provider/anthropic_provider.py
+127
-12
azure_provider.py
api/core/llm/provider/azure_provider.py
+2
-3
base.py
api/core/llm/provider/base.py
+25
-29
llm_provider_service.py
api/core/llm/provider/llm_provider_service.py
+4
-4
openai_provider.py
api/core/llm/provider/openai_provider.py
+11
-0
streamable_azure_chat_open_ai.py
api/core/llm/streamable_azure_chat_open_ai.py
+19
-36
streamable_azure_open_ai.py
api/core/llm/streamable_azure_open_ai.py
+5
-11
streamable_chat_anthropic.py
api/core/llm/streamable_chat_anthropic.py
+39
-0
streamable_chat_open_ai.py
api/core/llm/streamable_chat_open_ai.py
+17
-34
streamable_open_ai.py
api/core/llm/streamable_open_ai.py
+5
-11
whisper.py
api/core/llm/whisper.py
+3
-2
anthropic_wrapper.py
api/core/llm/wrappers/anthropic_wrapper.py
+27
-0
openai_wrapper.py
api/core/llm/wrappers/openai_wrapper.py
+1
-25
read_only_conversation_token_db_buffer_shared_memory.py
...y/read_only_conversation_token_db_buffer_shared_memory.py
+5
-5
dataset_index_tool.py
api/core/tool/dataset_index_tool.py
+2
-2
create_provider_when_tenant_created.py
...nts/event_handlers/create_provider_when_tenant_created.py
+16
-1
create_provider_when_tenant_updated.py
...nts/event_handlers/create_provider_when_tenant_updated.py
+16
-1
ext_mail.py
api/extensions/ext_mail.py
+61
-0
account.py
api/models/account.py
+4
-0
model.py
api/models/model.py
+3
-2
requirements.txt
api/requirements.txt
+5
-3
account_service.py
api/services/account_service.py
+91
-11
app_model_config_service.py
api/services/app_model_config_service.py
+28
-4
audio_service.py
api/services/audio_service.py
+1
-6
dataset_service.py
api/services/dataset_service.py
+23
-0
hit_testing_service.py
api/services/hit_testing_service.py
+1
-1
provider_service.py
api/services/provider_service.py
+24
-34
workspace_service.py
api/services/workspace_service.py
+2
-2
mail_invite_member_task.py
api/tasks/mail_invite_member_task.py
+52
-0
docker-compose.yaml
docker/docker-compose.yaml
+32
-12
Dockerfile
web/Dockerfile
+2
-2
activateForm.tsx
web/app/activate/activateForm.tsx
+233
-0
page.tsx
web/app/activate/page.tsx
+32
-0
style.module.css
web/app/activate/style.module.css
+4
-0
team-28x28.png
web/app/activate/team-28x28.png
+0
-0
index.tsx
web/app/components/app/chat/index.tsx
+6
-2
index.tsx
web/app/components/app/configuration/config-model/index.tsx
+53
-19
index.tsx
web/app/components/app/configuration/debug/index.tsx
+1
-1
index.tsx
web/app/components/app/configuration/index.tsx
+4
-2
index.tsx
web/app/components/app/text-generate/item/index.tsx
+14
-1
index.tsx
web/app/components/base/auto-height-textarea/index.tsx
+10
-2
locale.tsx
web/app/components/base/select/locale.tsx
+13
-12
browser-initor.tsx
web/app/components/browser-initor.tsx
+52
-0
template_chat.en.mdx
web/app/components/develop/template/template_chat.en.mdx
+47
-0
template_chat.zh.mdx
web/app/components/develop/template/template_chat.zh.mdx
+46
-0
index.module.css
web/app/components/header/account-about/index.module.css
+1
-1
index.tsx
web/app/components/header/account-about/index.tsx
+1
-1
index.tsx
.../components/header/account-setting/account-page/index.tsx
+158
-45
index.tsx
web/app/components/header/account-setting/index.tsx
+33
-24
index.tsx
.../components/header/account-setting/members-page/index.tsx
+4
-1
index.tsx
...eader/account-setting/members-page/invite-modal/index.tsx
+12
-12
copied.svg
...ount-setting/members-page/invited-modal/assets/copied.svg
+3
-0
copy-hover.svg
...-setting/members-page/invited-modal/assets/copy-hover.svg
+3
-0
copy.svg
...ccount-setting/members-page/invited-modal/assets/copy.svg
+3
-0
index.module.css
...count-setting/members-page/invited-modal/index.module.css
+16
-0
index.tsx
...ader/account-setting/members-page/invited-modal/index.tsx
+23
-8
invitation-link.tsx
...nt-setting/members-page/invited-modal/invitation-link.tsx
+63
-0
index.module.css
.../provider-page/anthropic-hosted-provider/index.module.css
+24
-0
index.tsx
...setting/provider-page/anthropic-hosted-provider/index.tsx
+65
-0
index.module.css
...setting/provider-page/anthropic-provider/index.module.css
+0
-0
index.tsx
...ccount-setting/provider-page/anthropic-provider/index.tsx
+90
-0
index.tsx
...components/header/account-setting/provider-page/index.tsx
+19
-1
index.tsx
...der/account-setting/provider-page/provider-item/index.tsx
+88
-5
index.tsx
web/app/components/share/chat/index.tsx
+1
-1
dify-header.svg
web/app/components/share/chatbot/icons/dify-header.svg
+22
-0
dify.svg
web/app/components/share/chatbot/icons/dify.svg
+11
-0
index.tsx
web/app/components/share/chatbot/index.tsx
+8
-2
style.module.css
web/app/components/share/chatbot/style.module.css
+11
-0
index.tsx
web/app/components/share/chatbot/welcome/index.tsx
+1
-1
header.tsx
web/app/components/share/header.tsx
+3
-1
installForm.tsx
web/app/install/installForm.tsx
+33
-37
layout.tsx
web/app/layout.tsx
+7
-4
_header.tsx
web/app/signin/_header.tsx
+1
-7
forms.tsx
web/app/signin/forms.tsx
+2
-3
normalForm.tsx
web/app/signin/normalForm.tsx
+23
-23
oneMoreStep.tsx
web/app/signin/oneMoreStep.tsx
+22
-12
page.tsx
web/app/signin/page.tsx
+5
-6
app-context.tsx
web/context/app-context.tsx
+2
-0
entrypoint.sh
web/docker/entrypoint.sh
+13
-2
common.en.ts
web/i18n/lang/common.en.ts
+30
-4
common.zh.ts
web/i18n/lang/common.zh.ts
+29
-4
login.en.ts
web/i18n/lang/login.en.ts
+53
-37
login.zh.ts
web/i18n/lang/login.zh.ts
+53
-37
common.ts
web/models/common.ts
+7
-0
package.json
web/package.json
+2
-2
embed.js
web/public/embed.js
+1
-1
embed.min.js
web/public/embed.min.js
+1
-1
base.ts
web/service/base.ts
+5
-2
common.ts
web/service/common.ts
+12
-4
app.ts
web/types/app.ts
+5
-0
No files found.
README.md
View file @
9098d099
...
...
@@ -17,9 +17,15 @@ A single API encompassing plugin capabilities, context enhancement, and more, sa
Visual data analysis, log review, and annotation for applications
Dify is compatible with Langchain, meaning we'll gradually support multiple LLMs, currently supported:
-
GPT 3 (text-davinci-003)
-
GPT 3.5 Turbo(ChatGPT)
-
GPT-4
*
**OpenAI**
:GPT4、GPT3.5-turbo、GPT3.5-turbo-16k、text-davinci-003
*
**Azure OpenAI**
*
**Antropic**
:Claude2、Claude-instant
> We've got 1000 free trial credits available for all cloud service users to try out the Claude model.Visit [Dify.ai](https://dify.ai) and
try it now.
*
**hugging face Hub**
:Coming soon.
## Use Cloud Services
...
...
README_CN.md
View file @
9098d099
...
...
@@ -17,11 +17,16 @@
-
一套 API 即可包含插件、上下文增强等能力,替你省下了后端代码的编写工作
-
可视化的对应用进行数据分析,查阅日志或进行标注
Dify 兼容 Langchain,这意味着我们将逐步支持多种 LLMs ,目前
已支持
:
Dify 兼容 Langchain,这意味着我们将逐步支持多种 LLMs ,目前
支持的模型供应商
:
-
GPT 3 (text-davinci-003)
-
GPT 3.5 Turbo(ChatGPT)
-
GPT-4
*
**OpenAI**
:GPT4、GPT3.5-turbo、GPT3.5-turbo-16k、text-davinci-003
*
**Azure OpenAI Service**
*
**Anthropic**
:Claude2、Claude-instant
> 我们为所有注册云端版的用户免费提供了 1000 次 Claude 模型的消息调用额度,登录 [dify.ai](https://cloud.dify.ai) 即可使用。
*
**Hugging Face Hub**
(即将推出)
## 使用云服务
...
...
api/.env.example
View file @
9098d099
...
...
@@ -8,13 +8,19 @@ EDITION=SELF_HOSTED
SECRET_KEY=
# Console API base URL
CONSOLE_URL=http://127.0.0.1:5001
CONSOLE_API_URL=http://127.0.0.1:5001
# Console frontend web base URL
CONSOLE_WEB_URL=http://127.0.0.1:3000
# Service API base URL
API_URL=http://127.0.0.1:5001
SERVICE_API_URL=http://127.0.0.1:5001
# Web APP API base URL
APP_API_URL=http://127.0.0.1:5001
# Web APP base URL
APP_URL=http://127.0.0.1:3000
# Web APP
frontend web
base URL
APP_
WEB_
URL=http://127.0.0.1:3000
# celery configuration
CELERY_BROKER_URL=redis://:difyai123456@localhost:6379/1
...
...
@@ -79,6 +85,11 @@ WEAVIATE_BATCH_SIZE=100
QDRANT_URL=path:storage/qdrant
QDRANT_API_KEY=your-qdrant-api-key
# Mail configuration, support: resend
MAIL_TYPE=
MAIL_DEFAULT_SEND_FROM=no-reply <no-reply@dify.ai>
RESEND_API_KEY=
# Sentry configuration
SENTRY_DSN=
...
...
api/Dockerfile
View file @
9098d099
...
...
@@ -5,9 +5,11 @@ LABEL maintainer="takatost@gmail.com"
ENV
FLASK_APP app.py
ENV
EDITION SELF_HOSTED
ENV
DEPLOY_ENV PRODUCTION
ENV
CONSOLE_URL http://127.0.0.1:5001
ENV
API_URL http://127.0.0.1:5001
ENV
APP_URL http://127.0.0.1:5001
ENV
CONSOLE_API_URL http://127.0.0.1:5001
ENV
CONSOLE_WEB_URL http://127.0.0.1:3000
ENV
SERVICE_API_URL http://127.0.0.1:5001
ENV
APP_API_URL http://127.0.0.1:5001
ENV
APP_WEB_URL http://127.0.0.1:3000
EXPOSE
5001
...
...
api/app.py
View file @
9098d099
...
...
@@ -2,6 +2,8 @@
import
os
from
datetime
import
datetime
from
werkzeug.exceptions
import
Forbidden
if
not
os
.
environ
.
get
(
"DEBUG"
)
or
os
.
environ
.
get
(
"DEBUG"
)
.
lower
()
!=
'true'
:
from
gevent
import
monkey
monkey
.
patch_all
()
...
...
@@ -15,7 +17,7 @@ import flask_login
from
flask_cors
import
CORS
from
extensions
import
ext_session
,
ext_celery
,
ext_sentry
,
ext_redis
,
ext_login
,
ext_migrate
,
\
ext_database
,
ext_storage
ext_database
,
ext_storage
,
ext_mail
from
extensions.ext_database
import
db
from
extensions.ext_login
import
login_manager
...
...
@@ -27,7 +29,7 @@ from events import event_handlers
import
core
from
config
import
Config
,
CloudEditionConfig
from
commands
import
register_commands
from
models.account
import
TenantAccountJoin
from
models.account
import
TenantAccountJoin
,
AccountStatus
from
models.model
import
Account
,
EndUser
,
App
import
warnings
...
...
@@ -83,6 +85,7 @@ def initialize_extensions(app):
ext_celery
.
init_app
(
app
)
ext_session
.
init_app
(
app
)
ext_login
.
init_app
(
app
)
ext_mail
.
init_app
(
app
)
ext_sentry
.
init_app
(
app
)
...
...
@@ -100,6 +103,9 @@ def load_user(user_id):
account
=
db
.
session
.
query
(
Account
)
.
filter
(
Account
.
id
==
account_id
)
.
first
()
if
account
:
if
account
.
status
==
AccountStatus
.
BANNED
.
value
or
account
.
status
==
AccountStatus
.
CLOSED
.
value
:
raise
Forbidden
(
'Account is banned or closed.'
)
workspace_id
=
session
.
get
(
'workspace_id'
)
if
workspace_id
:
tenant_account_join
=
db
.
session
.
query
(
TenantAccountJoin
)
.
filter
(
...
...
@@ -149,6 +155,10 @@ def register_blueprints(app):
from
controllers.web
import
bp
as
web_bp
from
controllers.console
import
bp
as
console_app_bp
CORS
(
service_api_bp
,
allow_headers
=
[
'Content-Type'
,
'Authorization'
,
'X-App-Code'
],
methods
=
[
'GET'
,
'PUT'
,
'POST'
,
'DELETE'
,
'OPTIONS'
,
'PATCH'
]
)
app
.
register_blueprint
(
service_api_bp
)
CORS
(
web_bp
,
...
...
api/commands.py
View file @
9098d099
...
...
@@ -18,7 +18,8 @@ from models.model import Account
import
secrets
import
base64
from
models.provider
import
Provider
from
models.provider
import
Provider
,
ProviderName
from
services.provider_service
import
ProviderService
@
click
.
command
(
'reset-password'
,
help
=
'Reset the account password.'
)
...
...
@@ -193,9 +194,40 @@ def recreate_all_dataset_indexes():
click
.
echo
(
click
.
style
(
'Congratulations! Recreate {} dataset indexes.'
.
format
(
recreate_count
),
fg
=
'green'
))
@
click
.
command
(
'sync-anthropic-hosted-providers'
,
help
=
'Sync anthropic hosted providers.'
)
def
sync_anthropic_hosted_providers
():
click
.
echo
(
click
.
style
(
'Start sync anthropic hosted providers.'
,
fg
=
'green'
))
count
=
0
page
=
1
while
True
:
try
:
tenants
=
db
.
session
.
query
(
Tenant
)
.
order_by
(
Tenant
.
created_at
.
desc
())
.
paginate
(
page
=
page
,
per_page
=
50
)
except
NotFound
:
break
page
+=
1
for
tenant
in
tenants
:
try
:
click
.
echo
(
'Syncing tenant anthropic hosted provider: {}'
.
format
(
tenant
.
id
))
ProviderService
.
create_system_provider
(
tenant
,
ProviderName
.
ANTHROPIC
.
value
,
current_app
.
config
[
'ANTHROPIC_HOSTED_QUOTA_LIMIT'
],
True
)
count
+=
1
except
Exception
as
e
:
click
.
echo
(
click
.
style
(
'Sync tenant anthropic hosted provider error: {} {}'
.
format
(
e
.
__class__
.
__name__
,
str
(
e
)),
fg
=
'red'
))
continue
click
.
echo
(
click
.
style
(
'Congratulations! Synced {} anthropic hosted providers.'
.
format
(
count
),
fg
=
'green'
))
def
register_commands
(
app
):
app
.
cli
.
add_command
(
reset_password
)
app
.
cli
.
add_command
(
reset_email
)
app
.
cli
.
add_command
(
generate_invitation_codes
)
app
.
cli
.
add_command
(
reset_encrypt_key_pair
)
app
.
cli
.
add_command
(
recreate_all_dataset_indexes
)
app
.
cli
.
add_command
(
sync_anthropic_hosted_providers
)
api/config.py
View file @
9098d099
...
...
@@ -28,9 +28,11 @@ DEFAULTS = {
'SESSION_REDIS_USE_SSL'
:
'False'
,
'OAUTH_REDIRECT_PATH'
:
'/console/api/oauth/authorize'
,
'OAUTH_REDIRECT_INDEX_PATH'
:
'/'
,
'CONSOLE_URL'
:
'https://cloud.dify.ai'
,
'API_URL'
:
'https://api.dify.ai'
,
'APP_URL'
:
'https://udify.app'
,
'CONSOLE_WEB_URL'
:
'https://cloud.dify.ai'
,
'CONSOLE_API_URL'
:
'https://cloud.dify.ai'
,
'SERVICE_API_URL'
:
'https://api.dify.ai'
,
'APP_WEB_URL'
:
'https://udify.app'
,
'APP_API_URL'
:
'https://udify.app'
,
'STORAGE_TYPE'
:
'local'
,
'STORAGE_LOCAL_PATH'
:
'storage'
,
'CHECK_UPDATE_URL'
:
'https://updates.dify.ai'
,
...
...
@@ -48,7 +50,10 @@ DEFAULTS = {
'PDF_PREVIEW'
:
'True'
,
'LOG_LEVEL'
:
'INFO'
,
'DISABLE_PROVIDER_CONFIG_VALIDATION'
:
'False'
,
'DEFAULT_LLM_PROVIDER'
:
'openai'
'DEFAULT_LLM_PROVIDER'
:
'openai'
,
'OPENAI_HOSTED_QUOTA_LIMIT'
:
200
,
'ANTHROPIC_HOSTED_QUOTA_LIMIT'
:
1000
,
'TENANT_DOCUMENT_COUNT'
:
100
}
...
...
@@ -76,10 +81,15 @@ class Config:
def
__init__
(
self
):
# app settings
self
.
CONSOLE_API_URL
=
get_env
(
'CONSOLE_URL'
)
if
get_env
(
'CONSOLE_URL'
)
else
get_env
(
'CONSOLE_API_URL'
)
self
.
CONSOLE_WEB_URL
=
get_env
(
'CONSOLE_URL'
)
if
get_env
(
'CONSOLE_URL'
)
else
get_env
(
'CONSOLE_WEB_URL'
)
self
.
SERVICE_API_URL
=
get_env
(
'API_URL'
)
if
get_env
(
'API_URL'
)
else
get_env
(
'SERVICE_API_URL'
)
self
.
APP_WEB_URL
=
get_env
(
'APP_URL'
)
if
get_env
(
'APP_URL'
)
else
get_env
(
'APP_WEB_URL'
)
self
.
APP_API_URL
=
get_env
(
'APP_URL'
)
if
get_env
(
'APP_URL'
)
else
get_env
(
'APP_API_URL'
)
self
.
CONSOLE_URL
=
get_env
(
'CONSOLE_URL'
)
self
.
API_URL
=
get_env
(
'API_URL'
)
self
.
APP_URL
=
get_env
(
'APP_URL'
)
self
.
CURRENT_VERSION
=
"0.3.
7
"
self
.
CURRENT_VERSION
=
"0.3.
9
"
self
.
COMMIT_SHA
=
get_env
(
'COMMIT_SHA'
)
self
.
EDITION
=
"SELF_HOSTED"
self
.
DEPLOY_ENV
=
get_env
(
'DEPLOY_ENV'
)
...
...
@@ -147,10 +157,15 @@ class Config:
# cors settings
self
.
CONSOLE_CORS_ALLOW_ORIGINS
=
get_cors_allow_origins
(
'CONSOLE_CORS_ALLOW_ORIGINS'
,
self
.
CONSOLE_URL
)
'CONSOLE_CORS_ALLOW_ORIGINS'
,
self
.
CONSOLE_
WEB_
URL
)
self
.
WEB_API_CORS_ALLOW_ORIGINS
=
get_cors_allow_origins
(
'WEB_API_CORS_ALLOW_ORIGINS'
,
'*'
)
# mail settings
self
.
MAIL_TYPE
=
get_env
(
'MAIL_TYPE'
)
self
.
MAIL_DEFAULT_SEND_FROM
=
get_env
(
'MAIL_DEFAULT_SEND_FROM'
)
self
.
RESEND_API_KEY
=
get_env
(
'RESEND_API_KEY'
)
# sentry settings
self
.
SENTRY_DSN
=
get_env
(
'SENTRY_DSN'
)
self
.
SENTRY_TRACES_SAMPLE_RATE
=
float
(
get_env
(
'SENTRY_TRACES_SAMPLE_RATE'
))
...
...
@@ -179,6 +194,10 @@ class Config:
# hosted provider credentials
self
.
OPENAI_API_KEY
=
get_env
(
'OPENAI_API_KEY'
)
self
.
ANTHROPIC_API_KEY
=
get_env
(
'ANTHROPIC_API_KEY'
)
self
.
OPENAI_HOSTED_QUOTA_LIMIT
=
get_env
(
'OPENAI_HOSTED_QUOTA_LIMIT'
)
self
.
ANTHROPIC_HOSTED_QUOTA_LIMIT
=
get_env
(
'ANTHROPIC_HOSTED_QUOTA_LIMIT'
)
# By default it is False
# You could disable it for compatibility with certain OpenAPI providers
...
...
@@ -195,6 +214,8 @@ class Config:
self
.
NOTION_INTERNAL_SECRET
=
get_env
(
'NOTION_INTERNAL_SECRET'
)
self
.
NOTION_INTEGRATION_TOKEN
=
get_env
(
'NOTION_INTEGRATION_TOKEN'
)
self
.
TENANT_DOCUMENT_COUNT
=
get_env
(
'TENANT_DOCUMENT_COUNT'
)
class
CloudEditionConfig
(
Config
):
...
...
api/controllers/console/__init__.py
View file @
9098d099
...
...
@@ -12,7 +12,7 @@ from . import setup, version, apikey, admin
from
.app
import
app
,
site
,
completion
,
model_config
,
statistic
,
conversation
,
message
,
generator
,
audio
# Import auth controllers
from
.auth
import
login
,
oauth
,
data_source_oauth
from
.auth
import
login
,
oauth
,
data_source_oauth
,
activate
# Import datasets controllers
from
.datasets
import
datasets
,
datasets_document
,
datasets_segments
,
file
,
hit_testing
,
data_source
...
...
api/controllers/console/app/audio.py
View file @
9098d099
...
...
@@ -50,8 +50,8 @@ class ChatMessageAudioApi(Resource):
raise
UnsupportedAudioTypeError
()
except
ProviderNotSupportSpeechToTextServiceError
:
raise
ProviderNotSupportSpeechToTextError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
api/controllers/console/app/completion.py
View file @
9098d099
...
...
@@ -63,8 +63,8 @@ class CompletionMessageApi(Resource):
except
services
.
errors
.
app_model_config
.
AppModelConfigBrokenError
:
logging
.
exception
(
"App model config broken."
)
raise
AppUnavailableError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
@@ -133,8 +133,8 @@ class ChatMessageApi(Resource):
except
services
.
errors
.
app_model_config
.
AppModelConfigBrokenError
:
logging
.
exception
(
"App model config broken."
)
raise
AppUnavailableError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
@@ -164,8 +164,8 @@ def compact_response(response: Union[dict | Generator]) -> Response:
except
services
.
errors
.
app_model_config
.
AppModelConfigBrokenError
:
logging
.
exception
(
"App model config broken."
)
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
AppUnavailableError
())
.
get_json
())
+
"
\n\n
"
except
ProviderTokenNotInitError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderNotInitializeError
())
.
get_json
())
+
"
\n\n
"
except
ProviderTokenNotInitError
as
ex
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderNotInitializeError
(
ex
.
description
))
.
get_json
())
+
"
\n\n
"
except
QuotaExceededError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderQuotaExceededError
())
.
get_json
())
+
"
\n\n
"
except
ModelCurrentlyNotSupportError
:
...
...
api/controllers/console/app/error.py
View file @
9098d099
...
...
@@ -16,7 +16,7 @@ class ProviderNotInitializeError(BaseHTTPException):
class
ProviderQuotaExceededError
(
BaseHTTPException
):
error_code
=
'provider_quota_exceeded'
description
=
"Your quota for Dify Hosted
OpenAI
has been exhausted. "
\
description
=
"Your quota for Dify Hosted
Model Provider
has been exhausted. "
\
"Please go to Settings -> Model Provider to complete your own provider credentials."
code
=
400
...
...
api/controllers/console/app/generator.py
View file @
9098d099
...
...
@@ -27,8 +27,8 @@ class IntroductionGenerateApi(Resource):
account
.
current_tenant_id
,
args
[
'prompt_template'
]
)
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
@@ -58,8 +58,8 @@ class RuleGenerateApi(Resource):
args
[
'audiences'
],
args
[
'hoping_to_solve'
]
)
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
api/controllers/console/app/message.py
View file @
9098d099
...
...
@@ -269,8 +269,8 @@ class MessageMoreLikeThisApi(Resource):
raise
NotFound
(
"Message Not Exists."
)
except
MoreLikeThisDisabledError
:
raise
AppMoreLikeThisDisabledError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
@@ -297,8 +297,8 @@ def compact_response(response: Union[dict | Generator]) -> Response:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
NotFound
(
"Message Not Exists."
))
.
get_json
())
+
"
\n\n
"
except
MoreLikeThisDisabledError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
AppMoreLikeThisDisabledError
())
.
get_json
())
+
"
\n\n
"
except
ProviderTokenNotInitError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderNotInitializeError
())
.
get_json
())
+
"
\n\n
"
except
ProviderTokenNotInitError
as
ex
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderNotInitializeError
(
ex
.
description
))
.
get_json
())
+
"
\n\n
"
except
QuotaExceededError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderQuotaExceededError
())
.
get_json
())
+
"
\n\n
"
except
ModelCurrentlyNotSupportError
:
...
...
@@ -339,8 +339,8 @@ class MessageSuggestedQuestionApi(Resource):
raise
NotFound
(
"Message not found"
)
except
ConversationNotExistsError
:
raise
NotFound
(
"Conversation not found"
)
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
api/controllers/console/auth/activate.py
0 → 100644
View file @
9098d099
import
base64
import
secrets
from
datetime
import
datetime
from
flask_restful
import
Resource
,
reqparse
from
controllers.console
import
api
from
controllers.console.error
import
AlreadyActivateError
from
extensions.ext_database
import
db
from
libs.helper
import
email
,
str_len
,
supported_language
,
timezone
from
libs.password
import
valid_password
,
hash_password
from
models.account
import
AccountStatus
,
Tenant
from
services.account_service
import
RegisterService
class
ActivateCheckApi
(
Resource
):
def
get
(
self
):
parser
=
reqparse
.
RequestParser
()
parser
.
add_argument
(
'workspace_id'
,
type
=
str
,
required
=
True
,
nullable
=
False
,
location
=
'args'
)
parser
.
add_argument
(
'email'
,
type
=
email
,
required
=
True
,
nullable
=
False
,
location
=
'args'
)
parser
.
add_argument
(
'token'
,
type
=
str
,
required
=
True
,
nullable
=
False
,
location
=
'args'
)
args
=
parser
.
parse_args
()
account
=
RegisterService
.
get_account_if_token_valid
(
args
[
'workspace_id'
],
args
[
'email'
],
args
[
'token'
])
tenant
=
db
.
session
.
query
(
Tenant
)
.
filter
(
Tenant
.
id
==
args
[
'workspace_id'
],
Tenant
.
status
==
'normal'
)
.
first
()
return
{
'is_valid'
:
account
is
not
None
,
'workspace_name'
:
tenant
.
name
}
class
ActivateApi
(
Resource
):
def
post
(
self
):
parser
=
reqparse
.
RequestParser
()
parser
.
add_argument
(
'workspace_id'
,
type
=
str
,
required
=
True
,
nullable
=
False
,
location
=
'json'
)
parser
.
add_argument
(
'email'
,
type
=
email
,
required
=
True
,
nullable
=
False
,
location
=
'json'
)
parser
.
add_argument
(
'token'
,
type
=
str
,
required
=
True
,
nullable
=
False
,
location
=
'json'
)
parser
.
add_argument
(
'name'
,
type
=
str_len
(
30
),
required
=
True
,
nullable
=
False
,
location
=
'json'
)
parser
.
add_argument
(
'password'
,
type
=
valid_password
,
required
=
True
,
nullable
=
False
,
location
=
'json'
)
parser
.
add_argument
(
'interface_language'
,
type
=
supported_language
,
required
=
True
,
nullable
=
False
,
location
=
'json'
)
parser
.
add_argument
(
'timezone'
,
type
=
timezone
,
required
=
True
,
nullable
=
False
,
location
=
'json'
)
args
=
parser
.
parse_args
()
account
=
RegisterService
.
get_account_if_token_valid
(
args
[
'workspace_id'
],
args
[
'email'
],
args
[
'token'
])
if
account
is
None
:
raise
AlreadyActivateError
()
RegisterService
.
revoke_token
(
args
[
'workspace_id'
],
args
[
'email'
],
args
[
'token'
])
account
.
name
=
args
[
'name'
]
# generate password salt
salt
=
secrets
.
token_bytes
(
16
)
base64_salt
=
base64
.
b64encode
(
salt
)
.
decode
()
# encrypt password with salt
password_hashed
=
hash_password
(
args
[
'password'
],
salt
)
base64_password_hashed
=
base64
.
b64encode
(
password_hashed
)
.
decode
()
account
.
password
=
base64_password_hashed
account
.
password_salt
=
base64_salt
account
.
interface_language
=
args
[
'interface_language'
]
account
.
timezone
=
args
[
'timezone'
]
account
.
interface_theme
=
'light'
account
.
status
=
AccountStatus
.
ACTIVE
.
value
account
.
initialized_at
=
datetime
.
utcnow
()
db
.
session
.
commit
()
return
{
'result'
:
'success'
}
api
.
add_resource
(
ActivateCheckApi
,
'/activate/check'
)
api
.
add_resource
(
ActivateApi
,
'/activate'
)
api/controllers/console/auth/data_source_oauth.py
View file @
9098d099
...
...
@@ -20,7 +20,7 @@ def get_oauth_providers():
client_secret
=
current_app
.
config
.
get
(
'NOTION_CLIENT_SECRET'
),
redirect_uri
=
current_app
.
config
.
get
(
'CONSOLE_URL'
)
+
'/console/api/oauth/data-source/callback/notion'
)
'CONSOLE_
API_
URL'
)
+
'/console/api/oauth/data-source/callback/notion'
)
OAUTH_PROVIDERS
=
{
'notion'
:
notion_oauth
...
...
@@ -42,7 +42,7 @@ class OAuthDataSource(Resource):
if
current_app
.
config
.
get
(
'NOTION_INTEGRATION_TYPE'
)
==
'internal'
:
internal_secret
=
current_app
.
config
.
get
(
'NOTION_INTERNAL_SECRET'
)
oauth_provider
.
save_internal_access_token
(
internal_secret
)
return
redirect
(
f
'{current_app.config.get("CONSOLE_URL")}?oauth_data_source=success'
)
return
redirect
(
f
'{current_app.config.get("CONSOLE_
WEB_
URL")}?oauth_data_source=success'
)
else
:
auth_url
=
oauth_provider
.
get_authorization_url
()
return
redirect
(
auth_url
)
...
...
@@ -66,12 +66,12 @@ class OAuthDataSourceCallback(Resource):
f
"An error occurred during the OAuthCallback process with {provider}: {e.response.text}"
)
return
{
'error'
:
'OAuth data source process failed'
},
400
return
redirect
(
f
'{current_app.config.get("CONSOLE_URL")}?oauth_data_source=success'
)
return
redirect
(
f
'{current_app.config.get("CONSOLE_
WEB_
URL")}?oauth_data_source=success'
)
elif
'error'
in
request
.
args
:
error
=
request
.
args
.
get
(
'error'
)
return
redirect
(
f
'{current_app.config.get("CONSOLE_URL")}?oauth_data_source={error}'
)
return
redirect
(
f
'{current_app.config.get("CONSOLE_
WEB_
URL")}?oauth_data_source={error}'
)
else
:
return
redirect
(
f
'{current_app.config.get("CONSOLE_URL")}?oauth_data_source=access_denied'
)
return
redirect
(
f
'{current_app.config.get("CONSOLE_
WEB_
URL")}?oauth_data_source=access_denied'
)
class
OAuthDataSourceSync
(
Resource
):
...
...
api/controllers/console/auth/oauth.py
View file @
9098d099
...
...
@@ -20,13 +20,13 @@ def get_oauth_providers():
client_secret
=
current_app
.
config
.
get
(
'GITHUB_CLIENT_SECRET'
),
redirect_uri
=
current_app
.
config
.
get
(
'CONSOLE_URL'
)
+
'/console/api/oauth/authorize/github'
)
'CONSOLE_
API_
URL'
)
+
'/console/api/oauth/authorize/github'
)
google_oauth
=
GoogleOAuth
(
client_id
=
current_app
.
config
.
get
(
'GOOGLE_CLIENT_ID'
),
client_secret
=
current_app
.
config
.
get
(
'GOOGLE_CLIENT_SECRET'
),
redirect_uri
=
current_app
.
config
.
get
(
'CONSOLE_URL'
)
+
'/console/api/oauth/authorize/google'
)
'CONSOLE_
API_
URL'
)
+
'/console/api/oauth/authorize/google'
)
OAUTH_PROVIDERS
=
{
'github'
:
github_oauth
,
...
...
@@ -80,7 +80,7 @@ class OAuthCallback(Resource):
flask_login
.
login_user
(
account
,
remember
=
True
)
AccountService
.
update_last_login
(
account
,
request
)
return
redirect
(
f
'{current_app.config.get("CONSOLE_URL")}?oauth_login=success'
)
return
redirect
(
f
'{current_app.config.get("CONSOLE_
WEB_
URL")}?oauth_login=success'
)
def
_get_account_by_openid_or_email
(
provider
:
str
,
user_info
:
OAuthUserInfo
)
->
Optional
[
Account
]:
...
...
api/controllers/console/datasets/datasets_document.py
View file @
9098d099
...
...
@@ -279,8 +279,8 @@ class DatasetDocumentListApi(Resource):
try
:
documents
,
batch
=
DocumentService
.
save_document_with_dataset_id
(
dataset
,
args
,
current_user
)
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
@@ -324,8 +324,8 @@ class DatasetInitApi(Resource):
document_data
=
args
,
account
=
current_user
)
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
api/controllers/console/datasets/hit_testing.py
View file @
9098d099
...
...
@@ -95,8 +95,8 @@ class HitTestingApi(Resource):
return
{
"query"
:
response
[
'query'
],
'records'
:
marshal
(
response
[
'records'
],
hit_testing_record_fields
)}
except
services
.
errors
.
index
.
IndexNotInitializedError
:
raise
DatasetNotInitializedError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
api/controllers/console/error.py
View file @
9098d099
...
...
@@ -18,3 +18,9 @@ class AccountNotLinkTenantError(BaseHTTPException):
error_code
=
'account_not_link_tenant'
description
=
"Account not link tenant."
code
=
403
class
AlreadyActivateError
(
BaseHTTPException
):
error_code
=
'already_activate'
description
=
"Auth Token is invalid or account already activated, please check again."
code
=
403
api/controllers/console/explore/audio.py
View file @
9098d099
...
...
@@ -47,8 +47,8 @@ class ChatAudioApi(InstalledAppResource):
raise
UnsupportedAudioTypeError
()
except
ProviderNotSupportSpeechToTextServiceError
:
raise
ProviderNotSupportSpeechToTextError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
api/controllers/console/explore/completion.py
View file @
9098d099
...
...
@@ -54,8 +54,8 @@ class CompletionApi(InstalledAppResource):
except
services
.
errors
.
app_model_config
.
AppModelConfigBrokenError
:
logging
.
exception
(
"App model config broken."
)
raise
AppUnavailableError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
@@ -113,8 +113,8 @@ class ChatApi(InstalledAppResource):
except
services
.
errors
.
app_model_config
.
AppModelConfigBrokenError
:
logging
.
exception
(
"App model config broken."
)
raise
AppUnavailableError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
@@ -155,8 +155,8 @@ def compact_response(response: Union[dict | Generator]) -> Response:
except
services
.
errors
.
app_model_config
.
AppModelConfigBrokenError
:
logging
.
exception
(
"App model config broken."
)
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
AppUnavailableError
())
.
get_json
())
+
"
\n\n
"
except
ProviderTokenNotInitError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderNotInitializeError
())
.
get_json
())
+
"
\n\n
"
except
ProviderTokenNotInitError
as
ex
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderNotInitializeError
(
ex
.
description
))
.
get_json
())
+
"
\n\n
"
except
QuotaExceededError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderQuotaExceededError
())
.
get_json
())
+
"
\n\n
"
except
ModelCurrentlyNotSupportError
:
...
...
api/controllers/console/explore/message.py
View file @
9098d099
...
...
@@ -107,8 +107,8 @@ class MessageMoreLikeThisApi(InstalledAppResource):
raise
NotFound
(
"Message Not Exists."
)
except
MoreLikeThisDisabledError
:
raise
AppMoreLikeThisDisabledError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
@@ -135,8 +135,8 @@ def compact_response(response: Union[dict | Generator]) -> Response:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
NotFound
(
"Message Not Exists."
))
.
get_json
())
+
"
\n\n
"
except
MoreLikeThisDisabledError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
AppMoreLikeThisDisabledError
())
.
get_json
())
+
"
\n\n
"
except
ProviderTokenNotInitError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderNotInitializeError
())
.
get_json
())
+
"
\n\n
"
except
ProviderTokenNotInitError
as
ex
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderNotInitializeError
(
ex
.
description
))
.
get_json
())
+
"
\n\n
"
except
QuotaExceededError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderQuotaExceededError
())
.
get_json
())
+
"
\n\n
"
except
ModelCurrentlyNotSupportError
:
...
...
@@ -174,8 +174,8 @@ class MessageSuggestedQuestionApi(InstalledAppResource):
raise
NotFound
(
"Conversation not found"
)
except
SuggestedQuestionsAfterAnswerDisabledError
:
raise
AppSuggestedQuestionsAfterAnswerDisabledError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
api/controllers/console/workspace/account.py
View file @
9098d099
...
...
@@ -6,22 +6,23 @@ from flask import current_app, request
from
flask_login
import
login_required
,
current_user
from
flask_restful
import
Resource
,
reqparse
,
fields
,
marshal_with
from
services.errors.account
import
CurrentPasswordIncorrectError
as
ServiceCurrentPasswordIncorrectError
from
controllers.console
import
api
from
controllers.console.setup
import
setup_required
from
controllers.console.workspace.error
import
AccountAlreadyInitedError
,
InvalidInvitationCodeError
,
\
RepeatPasswordNotMatchError
RepeatPasswordNotMatchError
,
CurrentPasswordIncorrectError
from
controllers.console.wraps
import
account_initialization_required
from
libs.helper
import
TimestampField
,
supported_language
,
timezone
from
extensions.ext_database
import
db
from
models.account
import
InvitationCode
,
AccountIntegrate
from
services.account_service
import
AccountService
account_fields
=
{
'id'
:
fields
.
String
,
'name'
:
fields
.
String
,
'avatar'
:
fields
.
String
,
'email'
:
fields
.
String
,
'is_password_set'
:
fields
.
Boolean
,
'interface_language'
:
fields
.
String
,
'interface_theme'
:
fields
.
String
,
'timezone'
:
fields
.
String
,
...
...
@@ -194,8 +195,11 @@ class AccountPasswordApi(Resource):
if
args
[
'new_password'
]
!=
args
[
'repeat_new_password'
]:
raise
RepeatPasswordNotMatchError
()
try
:
AccountService
.
update_account_password
(
current_user
,
args
[
'password'
],
args
[
'new_password'
])
except
ServiceCurrentPasswordIncorrectError
:
raise
CurrentPasswordIncorrectError
()
return
{
"result"
:
"success"
}
...
...
api/controllers/console/workspace/error.py
View file @
9098d099
...
...
@@ -7,6 +7,12 @@ class RepeatPasswordNotMatchError(BaseHTTPException):
code
=
400
class
CurrentPasswordIncorrectError
(
BaseHTTPException
):
error_code
=
'current_password_incorrect'
description
=
"Current password is incorrect."
code
=
400
class
ProviderRequestFailedError
(
BaseHTTPException
):
error_code
=
'provider_request_failed'
description
=
None
...
...
api/controllers/console/workspace/members.py
View file @
9098d099
# -*- coding:utf-8 -*-
from
flask
import
current_app
from
flask_login
import
login_required
,
current_user
from
flask_restful
import
Resource
,
reqparse
,
marshal_with
,
abort
,
fields
,
marshal
...
...
@@ -60,7 +60,8 @@ class MemberInviteEmailApi(Resource):
inviter
=
current_user
try
:
RegisterService
.
invite_new_member
(
inviter
.
current_tenant
,
invitee_email
,
role
=
invitee_role
,
inviter
=
inviter
)
token
=
RegisterService
.
invite_new_member
(
inviter
.
current_tenant
,
invitee_email
,
role
=
invitee_role
,
inviter
=
inviter
)
account
=
db
.
session
.
query
(
Account
,
TenantAccountJoin
.
role
)
.
join
(
TenantAccountJoin
,
Account
.
id
==
TenantAccountJoin
.
account_id
)
.
filter
(
Account
.
email
==
args
[
'email'
])
.
first
()
...
...
@@ -78,7 +79,16 @@ class MemberInviteEmailApi(Resource):
# todo:413
return
{
'result'
:
'success'
,
'account'
:
account
},
201
return
{
'result'
:
'success'
,
'account'
:
account
,
'invite_url'
:
'{}/activate?workspace_id={}&email={}&token={}'
.
format
(
current_app
.
config
.
get
(
"CONSOLE_WEB_URL"
),
str
(
current_user
.
current_tenant_id
),
invitee_email
,
token
)
},
201
class
MemberCancelInviteApi
(
Resource
):
...
...
@@ -88,7 +98,7 @@ class MemberCancelInviteApi(Resource):
@
login_required
@
account_initialization_required
def
delete
(
self
,
member_id
):
member
=
Account
.
query
.
get
(
str
(
member_id
)
)
member
=
db
.
session
.
query
(
Account
)
.
filter
(
Account
.
id
==
str
(
member_id
))
.
first
(
)
if
not
member
:
abort
(
404
)
...
...
api/controllers/console/workspace/providers.py
View file @
9098d099
...
...
@@ -3,6 +3,7 @@ import base64
import
json
import
logging
from
flask
import
current_app
from
flask_login
import
login_required
,
current_user
from
flask_restful
import
Resource
,
reqparse
,
abort
from
werkzeug.exceptions
import
Forbidden
...
...
@@ -34,7 +35,7 @@ class ProviderListApi(Resource):
plaintext, the rest is replaced by * and the last two bits are displayed in plaintext
"""
ProviderService
.
init_supported_provider
(
current_user
.
current_tenant
,
"cloud"
)
ProviderService
.
init_supported_provider
(
current_user
.
current_tenant
)
providers
=
Provider
.
query
.
filter_by
(
tenant_id
=
tenant_id
)
.
all
()
provider_list
=
[
...
...
@@ -50,7 +51,8 @@ class ProviderListApi(Resource):
'quota_used'
:
p
.
quota_used
}
if
p
.
provider_type
==
ProviderType
.
SYSTEM
.
value
else
{}),
'token'
:
ProviderService
.
get_obfuscated_api_key
(
current_user
.
current_tenant
,
ProviderName
(
p
.
provider_name
))
ProviderName
(
p
.
provider_name
),
only_custom
=
True
)
if
p
.
provider_type
==
ProviderType
.
CUSTOM
.
value
else
None
}
for
p
in
providers
]
...
...
@@ -121,9 +123,10 @@ class ProviderTokenApi(Resource):
is_valid
=
token_is_valid
)
db
.
session
.
add
(
provider_model
)
if
provider_model
.
is_valid
:
if
provider
in
[
ProviderName
.
OPENAI
.
value
,
ProviderName
.
AZURE_OPENAI
.
value
]
and
provider
_model
.
is_valid
:
other_providers
=
db
.
session
.
query
(
Provider
)
.
filter
(
Provider
.
tenant_id
==
tenant
.
id
,
Provider
.
provider_name
.
in_
([
ProviderName
.
OPENAI
.
value
,
ProviderName
.
AZURE_OPENAI
.
value
]),
Provider
.
provider_name
!=
provider
,
Provider
.
provider_type
==
ProviderType
.
CUSTOM
.
value
)
.
all
()
...
...
@@ -133,7 +136,7 @@ class ProviderTokenApi(Resource):
db
.
session
.
commit
()
if
provider
in
[
ProviderName
.
A
NTHROPIC
.
value
,
ProviderName
.
A
ZURE_OPENAI
.
value
,
ProviderName
.
COHERE
.
value
,
if
provider
in
[
ProviderName
.
AZURE_OPENAI
.
value
,
ProviderName
.
COHERE
.
value
,
ProviderName
.
HUGGINGFACEHUB
.
value
]:
return
{
'result'
:
'success'
,
'warning'
:
'MOCK: This provider is not supported yet.'
},
201
...
...
@@ -157,7 +160,7 @@ class ProviderTokenValidateApi(Resource):
args
=
parser
.
parse_args
()
# todo: remove this when the provider is supported
if
provider
in
[
ProviderName
.
ANTHROPIC
.
value
,
ProviderName
.
COHERE
.
value
,
if
provider
in
[
ProviderName
.
COHERE
.
value
,
ProviderName
.
HUGGINGFACEHUB
.
value
]:
return
{
'result'
:
'success'
,
'warning'
:
'MOCK: This provider is not supported yet.'
}
...
...
@@ -203,7 +206,19 @@ class ProviderSystemApi(Resource):
provider_model
.
is_valid
=
args
[
'is_enabled'
]
db
.
session
.
commit
()
elif
not
provider_model
:
ProviderService
.
create_system_provider
(
tenant
,
provider
,
args
[
'is_enabled'
])
if
provider
==
ProviderName
.
OPENAI
.
value
:
quota_limit
=
current_app
.
config
[
'OPENAI_HOSTED_QUOTA_LIMIT'
]
elif
provider
==
ProviderName
.
ANTHROPIC
.
value
:
quota_limit
=
current_app
.
config
[
'ANTHROPIC_HOSTED_QUOTA_LIMIT'
]
else
:
quota_limit
=
0
ProviderService
.
create_system_provider
(
tenant
,
provider
,
quota_limit
,
args
[
'is_enabled'
]
)
else
:
abort
(
403
)
...
...
api/controllers/service_api/app/audio.py
View file @
9098d099
...
...
@@ -43,8 +43,8 @@ class AudioApi(AppApiResource):
raise
UnsupportedAudioTypeError
()
except
ProviderNotSupportSpeechToTextServiceError
:
raise
ProviderNotSupportSpeechToTextError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
api/controllers/service_api/app/completion.py
View file @
9098d099
...
...
@@ -54,8 +54,8 @@ class CompletionApi(AppApiResource):
except
services
.
errors
.
app_model_config
.
AppModelConfigBrokenError
:
logging
.
exception
(
"App model config broken."
)
raise
AppUnavailableError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
@@ -115,8 +115,8 @@ class ChatApi(AppApiResource):
except
services
.
errors
.
app_model_config
.
AppModelConfigBrokenError
:
logging
.
exception
(
"App model config broken."
)
raise
AppUnavailableError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
@@ -156,8 +156,8 @@ def compact_response(response: Union[dict | Generator]) -> Response:
except
services
.
errors
.
app_model_config
.
AppModelConfigBrokenError
:
logging
.
exception
(
"App model config broken."
)
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
AppUnavailableError
())
.
get_json
())
+
"
\n\n
"
except
ProviderTokenNotInitError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderNotInitializeError
())
.
get_json
())
+
"
\n\n
"
except
ProviderTokenNotInitError
as
ex
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderNotInitializeError
(
ex
.
description
))
.
get_json
())
+
"
\n\n
"
except
QuotaExceededError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderQuotaExceededError
())
.
get_json
())
+
"
\n\n
"
except
ModelCurrentlyNotSupportError
:
...
...
api/controllers/service_api/app/conversation.py
View file @
9098d099
# -*- coding:utf-8 -*-
from
flask
import
request
from
flask_restful
import
fields
,
marshal_with
,
reqparse
from
flask_restful.inputs
import
int_range
from
werkzeug.exceptions
import
NotFound
...
...
@@ -56,16 +57,14 @@ class ConversationDetailApi(AppApiResource):
conversation_id
=
str
(
c_id
)
parser
=
reqparse
.
RequestParser
()
parser
.
add_argument
(
'user'
,
type
=
str
,
location
=
'args'
)
args
=
parser
.
parse_args
()
user
=
request
.
get_json
()
.
get
(
'user'
)
if
end_user
is
None
and
args
[
'user'
]
is
not
None
:
end_user
=
create_or_update_end_user_for_user_id
(
app_model
,
args
[
'user'
]
)
if
end_user
is
None
and
user
is
not
None
:
end_user
=
create_or_update_end_user_for_user_id
(
app_model
,
user
)
try
:
ConversationService
.
delete
(
app_model
,
conversation_id
,
end_user
)
return
{
"result"
:
"success"
}
,
204
return
{
"result"
:
"success"
}
except
services
.
errors
.
conversation
.
ConversationNotExistsError
:
raise
NotFound
(
"Conversation Not Exists."
)
...
...
@@ -95,3 +94,4 @@ class ConversationRenameApi(AppApiResource):
api
.
add_resource
(
ConversationRenameApi
,
'/conversations/<uuid:c_id>/name'
,
endpoint
=
'conversation_name'
)
api
.
add_resource
(
ConversationApi
,
'/conversations'
)
api
.
add_resource
(
ConversationApi
,
'/conversations/<uuid:c_id>'
,
endpoint
=
'conversation'
)
api
.
add_resource
(
ConversationDetailApi
,
'/conversations/<uuid:c_id>'
,
endpoint
=
'conversation_detail'
)
api/controllers/service_api/dataset/document.py
View file @
9098d099
...
...
@@ -85,8 +85,8 @@ class DocumentListApi(DatasetApiResource):
dataset_process_rule
=
dataset
.
latest_process_rule
,
created_from
=
'api'
)
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
document
=
documents
[
0
]
if
doc_type
and
doc_metadata
:
metadata_schema
=
DocumentService
.
DOCUMENT_METADATA_SCHEMA
[
doc_type
]
...
...
api/controllers/web/audio.py
View file @
9098d099
...
...
@@ -45,8 +45,8 @@ class AudioApi(WebApiResource):
raise
UnsupportedAudioTypeError
()
except
ProviderNotSupportSpeechToTextServiceError
:
raise
ProviderNotSupportSpeechToTextError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
api/controllers/web/completion.py
View file @
9098d099
...
...
@@ -52,8 +52,8 @@ class CompletionApi(WebApiResource):
except
services
.
errors
.
app_model_config
.
AppModelConfigBrokenError
:
logging
.
exception
(
"App model config broken."
)
raise
AppUnavailableError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
@@ -109,8 +109,8 @@ class ChatApi(WebApiResource):
except
services
.
errors
.
app_model_config
.
AppModelConfigBrokenError
:
logging
.
exception
(
"App model config broken."
)
raise
AppUnavailableError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
@@ -150,8 +150,8 @@ def compact_response(response: Union[dict | Generator]) -> Response:
except
services
.
errors
.
app_model_config
.
AppModelConfigBrokenError
:
logging
.
exception
(
"App model config broken."
)
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
AppUnavailableError
())
.
get_json
())
+
"
\n\n
"
except
ProviderTokenNotInitError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderNotInitializeError
())
.
get_json
())
+
"
\n\n
"
except
ProviderTokenNotInitError
as
ex
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderNotInitializeError
(
ex
.
description
))
.
get_json
())
+
"
\n\n
"
except
QuotaExceededError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderQuotaExceededError
())
.
get_json
())
+
"
\n\n
"
except
ModelCurrentlyNotSupportError
:
...
...
api/controllers/web/message.py
View file @
9098d099
...
...
@@ -101,8 +101,8 @@ class MessageMoreLikeThisApi(WebApiResource):
raise
NotFound
(
"Message Not Exists."
)
except
MoreLikeThisDisabledError
:
raise
AppMoreLikeThisDisabledError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
@@ -129,8 +129,8 @@ def compact_response(response: Union[dict | Generator]) -> Response:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
NotFound
(
"Message Not Exists."
))
.
get_json
())
+
"
\n\n
"
except
MoreLikeThisDisabledError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
AppMoreLikeThisDisabledError
())
.
get_json
())
+
"
\n\n
"
except
ProviderTokenNotInitError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderNotInitializeError
())
.
get_json
())
+
"
\n\n
"
except
ProviderTokenNotInitError
as
ex
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderNotInitializeError
(
ex
.
description
))
.
get_json
())
+
"
\n\n
"
except
QuotaExceededError
:
yield
"data: "
+
json
.
dumps
(
api
.
handle_error
(
ProviderQuotaExceededError
())
.
get_json
())
+
"
\n\n
"
except
ModelCurrentlyNotSupportError
:
...
...
@@ -167,8 +167,8 @@ class MessageSuggestedQuestionApi(WebApiResource):
raise
NotFound
(
"Conversation not found"
)
except
SuggestedQuestionsAfterAnswerDisabledError
:
raise
AppSuggestedQuestionsAfterAnswerDisabledError
()
except
ProviderTokenNotInitError
:
raise
ProviderNotInitializeError
()
except
ProviderTokenNotInitError
as
ex
:
raise
ProviderNotInitializeError
(
ex
.
description
)
except
QuotaExceededError
:
raise
ProviderQuotaExceededError
()
except
ModelCurrentlyNotSupportError
:
...
...
api/core/__init__.py
View file @
9098d099
...
...
@@ -13,8 +13,13 @@ class HostedOpenAICredential(BaseModel):
api_key
:
str
class
HostedAnthropicCredential
(
BaseModel
):
api_key
:
str
class
HostedLLMCredentials
(
BaseModel
):
openai
:
Optional
[
HostedOpenAICredential
]
=
None
anthropic
:
Optional
[
HostedAnthropicCredential
]
=
None
hosted_llm_credentials
=
HostedLLMCredentials
()
...
...
@@ -26,3 +31,6 @@ def init_app(app: Flask):
if
app
.
config
.
get
(
"OPENAI_API_KEY"
):
hosted_llm_credentials
.
openai
=
HostedOpenAICredential
(
api_key
=
app
.
config
.
get
(
"OPENAI_API_KEY"
))
if
app
.
config
.
get
(
"ANTHROPIC_API_KEY"
):
hosted_llm_credentials
.
anthropic
=
HostedAnthropicCredential
(
api_key
=
app
.
config
.
get
(
"ANTHROPIC_API_KEY"
))
api/core/callback_handler/llm_callback_handler.py
View file @
9098d099
...
...
@@ -48,7 +48,7 @@ class LLMCallbackHandler(BaseCallbackHandler):
})
self
.
llm_message
.
prompt
=
real_prompts
self
.
llm_message
.
prompt_tokens
=
self
.
llm
.
get_
messages_token
s
(
messages
[
0
])
self
.
llm_message
.
prompt_tokens
=
self
.
llm
.
get_
num_tokens_from_message
s
(
messages
[
0
])
def
on_llm_start
(
self
,
serialized
:
Dict
[
str
,
Any
],
prompts
:
List
[
str
],
**
kwargs
:
Any
...
...
api/core/completion.py
View file @
9098d099
...
...
@@ -118,6 +118,7 @@ class Completion:
prompt
,
stop_words
=
cls
.
get_main_llm_prompt
(
mode
=
mode
,
llm
=
final_llm
,
model
=
app_model_config
.
model_dict
,
pre_prompt
=
app_model_config
.
pre_prompt
,
query
=
query
,
inputs
=
inputs
,
...
...
@@ -129,6 +130,7 @@ class Completion:
cls
.
recale_llm_max_tokens
(
final_llm
=
final_llm
,
model
=
app_model_config
.
model_dict
,
prompt
=
prompt
,
mode
=
mode
)
...
...
@@ -138,7 +140,8 @@ class Completion:
return
response
@
classmethod
def
get_main_llm_prompt
(
cls
,
mode
:
str
,
llm
:
BaseLanguageModel
,
pre_prompt
:
str
,
query
:
str
,
inputs
:
dict
,
def
get_main_llm_prompt
(
cls
,
mode
:
str
,
llm
:
BaseLanguageModel
,
model
:
dict
,
pre_prompt
:
str
,
query
:
str
,
inputs
:
dict
,
chain_output
:
Optional
[
str
],
memory
:
Optional
[
ReadOnlyConversationTokenDBBufferSharedMemory
])
->
\
Tuple
[
Union
[
str
|
List
[
BaseMessage
]],
Optional
[
List
[
str
]]]:
...
...
@@ -151,10 +154,11 @@ class Completion:
if
mode
==
'completion'
:
prompt_template
=
JinjaPromptTemplate
.
from_template
(
template
=
(
"""Use the following CONTEXT as your learned knowledge:
[CONTEXT]
template
=
(
"""Use the following context as your learned knowledge, inside <context></context> XML tags.
<context>
{{context}}
[END CONTEXT]
</context>
When answer to user:
- If you don't know, just say that you don't know.
...
...
@@ -204,10 +208,11 @@ And answer according to the language of the user's question.
if
chain_output
:
human_inputs
[
'context'
]
=
chain_output
human_message_prompt
+=
"""Use the following CONTEXT as your learned knowledge.
[CONTEXT]
human_message_prompt
+=
"""Use the following context as your learned knowledge, inside <context></context> XML tags.
<context>
{{context}}
[END CONTEXT]
</context>
When answer to user:
- If you don't know, just say that you don't know.
...
...
@@ -219,7 +224,7 @@ And answer according to the language of the user's question.
if
pre_prompt
:
human_message_prompt
+=
pre_prompt
query_prompt
=
"
\n
Human: {{query}}
\n
AI
: "
query_prompt
=
"
\n
\n
Human: {{query}}
\n\n
Assistant
: "
if
memory
:
# append chat histories
...
...
@@ -228,9 +233,11 @@ And answer according to the language of the user's question.
inputs
=
human_inputs
)
curr_message_tokens
=
memory
.
llm
.
get_messages_tokens
([
tmp_human_message
])
rest_tokens
=
llm_constant
.
max_context_token_length
[
memory
.
llm
.
model_name
]
\
-
memory
.
llm
.
max_tokens
-
curr_message_tokens
curr_message_tokens
=
memory
.
llm
.
get_num_tokens_from_messages
([
tmp_human_message
])
model_name
=
model
[
'name'
]
max_tokens
=
model
.
get
(
"completion_params"
)
.
get
(
'max_tokens'
)
rest_tokens
=
llm_constant
.
max_context_token_length
[
model_name
]
\
-
max_tokens
-
curr_message_tokens
rest_tokens
=
max
(
rest_tokens
,
0
)
histories
=
cls
.
get_history_messages_from_memory
(
memory
,
rest_tokens
)
...
...
@@ -241,7 +248,10 @@ And answer according to the language of the user's question.
# if histories_param not in human_inputs:
# human_inputs[histories_param] = '{{' + histories_param + '}}'
human_message_prompt
+=
"
\n\n
"
+
histories
human_message_prompt
+=
"
\n\n
"
if
human_message_prompt
else
""
human_message_prompt
+=
"Here is the chat histories between human and assistant, "
\
"inside <histories></histories> XML tags.
\n\n
<histories>"
human_message_prompt
+=
histories
+
"</histories>"
human_message_prompt
+=
query_prompt
...
...
@@ -307,13 +317,15 @@ And answer according to the language of the user's question.
model
=
app_model_config
.
model_dict
)
model_limited_tokens
=
llm_constant
.
max_context_token_length
[
llm
.
model_name
]
max_tokens
=
llm
.
max_tokens
model_name
=
app_model_config
.
model_dict
.
get
(
"name"
)
model_limited_tokens
=
llm_constant
.
max_context_token_length
[
model_name
]
max_tokens
=
app_model_config
.
model_dict
.
get
(
"completion_params"
)
.
get
(
'max_tokens'
)
# get prompt without memory and context
prompt
,
_
=
cls
.
get_main_llm_prompt
(
mode
=
mode
,
llm
=
llm
,
model
=
app_model_config
.
model_dict
,
pre_prompt
=
app_model_config
.
pre_prompt
,
query
=
query
,
inputs
=
inputs
,
...
...
@@ -332,16 +344,17 @@ And answer according to the language of the user's question.
return
rest_tokens
@
classmethod
def
recale_llm_max_tokens
(
cls
,
final_llm
:
Union
[
StreamableOpenAI
,
StreamableChatOpenAI
]
,
def
recale_llm_max_tokens
(
cls
,
final_llm
:
BaseLanguageModel
,
model
:
dict
,
prompt
:
Union
[
str
,
List
[
BaseMessage
]],
mode
:
str
):
# recalc max_tokens if sum(prompt_token + max_tokens) over model token limit
model_limited_tokens
=
llm_constant
.
max_context_token_length
[
final_llm
.
model_name
]
max_tokens
=
final_llm
.
max_tokens
model_name
=
model
.
get
(
"name"
)
model_limited_tokens
=
llm_constant
.
max_context_token_length
[
model_name
]
max_tokens
=
model
.
get
(
"completion_params"
)
.
get
(
'max_tokens'
)
if
mode
==
'completion'
and
isinstance
(
final_llm
,
BaseLLM
):
prompt_tokens
=
final_llm
.
get_num_tokens
(
prompt
)
else
:
prompt_tokens
=
final_llm
.
get_
messages_token
s
(
prompt
)
prompt_tokens
=
final_llm
.
get_
num_tokens_from_message
s
(
prompt
)
if
prompt_tokens
+
max_tokens
>
model_limited_tokens
:
max_tokens
=
max
(
model_limited_tokens
-
prompt_tokens
,
16
)
...
...
@@ -350,9 +363,10 @@ And answer according to the language of the user's question.
@
classmethod
def
generate_more_like_this
(
cls
,
task_id
:
str
,
app
:
App
,
message
:
Message
,
pre_prompt
:
str
,
app_model_config
:
AppModelConfig
,
user
:
Account
,
streaming
:
bool
):
llm
:
StreamableOpenAI
=
LLMBuilder
.
to_llm
(
llm
=
LLMBuilder
.
to_llm_from_model
(
tenant_id
=
app
.
tenant_id
,
model
_name
=
'gpt-3.5-turbo'
,
model
=
app_model_config
.
model_dict
,
streaming
=
streaming
)
...
...
@@ -360,6 +374,7 @@ And answer according to the language of the user's question.
original_prompt
,
_
=
cls
.
get_main_llm_prompt
(
mode
=
"completion"
,
llm
=
llm
,
model
=
app_model_config
.
model_dict
,
pre_prompt
=
pre_prompt
,
query
=
message
.
query
,
inputs
=
message
.
inputs
,
...
...
@@ -390,6 +405,7 @@ And answer according to the language of the user's question.
cls
.
recale_llm_max_tokens
(
final_llm
=
llm
,
model
=
app_model_config
.
model_dict
,
prompt
=
prompt
,
mode
=
'completion'
)
...
...
api/core/constant/llm_constant.py
View file @
9098d099
from
_decimal
import
Decimal
models
=
{
'claude-instant-1'
:
'anthropic'
,
# 100,000 tokens
'claude-2'
:
'anthropic'
,
# 100,000 tokens
'gpt-4'
:
'openai'
,
# 8,192 tokens
'gpt-4-32k'
:
'openai'
,
# 32,768 tokens
'gpt-3.5-turbo'
:
'openai'
,
# 4,096 tokens
...
...
@@ -10,10 +12,13 @@ models = {
'text-curie-001'
:
'openai'
,
# 2,049 tokens
'text-babbage-001'
:
'openai'
,
# 2,049 tokens
'text-ada-001'
:
'openai'
,
# 2,049 tokens
'text-embedding-ada-002'
:
'openai'
# 8191 tokens, 1536 dimensions
'text-embedding-ada-002'
:
'openai'
,
# 8191 tokens, 1536 dimensions
'whisper-1'
:
'openai'
}
max_context_token_length
=
{
'claude-instant-1'
:
100000
,
'claude-2'
:
100000
,
'gpt-4'
:
8192
,
'gpt-4-32k'
:
32768
,
'gpt-3.5-turbo'
:
4096
,
...
...
@@ -23,17 +28,21 @@ max_context_token_length = {
'text-curie-001'
:
2049
,
'text-babbage-001'
:
2049
,
'text-ada-001'
:
2049
,
'text-embedding-ada-002'
:
8191
'text-embedding-ada-002'
:
8191
,
}
models_by_mode
=
{
'chat'
:
[
'claude-instant-1'
,
# 100,000 tokens
'claude-2'
,
# 100,000 tokens
'gpt-4'
,
# 8,192 tokens
'gpt-4-32k'
,
# 32,768 tokens
'gpt-3.5-turbo'
,
# 4,096 tokens
'gpt-3.5-turbo-16k'
,
# 16,384 tokens
],
'completion'
:
[
'claude-instant-1'
,
# 100,000 tokens
'claude-2'
,
# 100,000 tokens
'gpt-4'
,
# 8,192 tokens
'gpt-4-32k'
,
# 32,768 tokens
'gpt-3.5-turbo'
,
# 4,096 tokens
...
...
@@ -52,6 +61,14 @@ models_by_mode = {
model_currency
=
'USD'
model_prices
=
{
'claude-instant-1'
:
{
'prompt'
:
Decimal
(
'0.00163'
),
'completion'
:
Decimal
(
'0.00551'
),
},
'claude-2'
:
{
'prompt'
:
Decimal
(
'0.01102'
),
'completion'
:
Decimal
(
'0.03268'
),
},
'gpt-4'
:
{
'prompt'
:
Decimal
(
'0.03'
),
'completion'
:
Decimal
(
'0.06'
),
...
...
api/core/conversation_message_task.py
View file @
9098d099
...
...
@@ -56,7 +56,7 @@ class ConversationMessageTask:
)
def
init
(
self
):
provider_name
=
LLMBuilder
.
get_default_provider
(
self
.
app
.
tenant_id
)
provider_name
=
LLMBuilder
.
get_default_provider
(
self
.
app
.
tenant_id
,
self
.
model_name
)
self
.
model_dict
[
'provider'
]
=
provider_name
override_model_configs
=
None
...
...
@@ -89,7 +89,7 @@ class ConversationMessageTask:
system_message
=
PromptBuilder
.
to_system_message
(
self
.
app_model_config
.
pre_prompt
,
self
.
inputs
)
system_instruction
=
system_message
.
content
llm
=
LLMBuilder
.
to_llm
(
self
.
tenant_id
,
self
.
model_name
)
system_instruction_tokens
=
llm
.
get_
messages_token
s
([
system_message
])
system_instruction_tokens
=
llm
.
get_
num_tokens_from_message
s
([
system_message
])
if
not
self
.
conversation
:
self
.
is_new_conversation
=
True
...
...
@@ -185,6 +185,7 @@ class ConversationMessageTask:
if
provider
and
provider
.
provider_type
==
ProviderType
.
SYSTEM
.
value
:
db
.
session
.
query
(
Provider
)
.
filter
(
Provider
.
tenant_id
==
self
.
app
.
tenant_id
,
Provider
.
provider_name
==
provider
.
provider_name
,
Provider
.
quota_limit
>
Provider
.
quota_used
)
.
update
({
'quota_used'
:
Provider
.
quota_used
+
1
})
...
...
api/core/embedding/cached_embedding.py
View file @
9098d099
...
...
@@ -4,6 +4,7 @@ from typing import List
from
langchain.embeddings.base
import
Embeddings
from
sqlalchemy.exc
import
IntegrityError
from
core.llm.wrappers.openai_wrapper
import
handle_openai_exceptions
from
extensions.ext_database
import
db
from
libs
import
helper
from
models.dataset
import
Embedding
...
...
@@ -49,6 +50,7 @@ class CacheEmbedding(Embeddings):
text_embeddings
.
extend
(
embedding_results
)
return
text_embeddings
@
handle_openai_exceptions
def
embed_query
(
self
,
text
:
str
)
->
List
[
float
]:
"""Embed query text."""
# use doc embedding cache or store if not exists
...
...
api/core/generator/llm_generator.py
View file @
9098d099
...
...
@@ -23,6 +23,10 @@ class LLMGenerator:
@
classmethod
def
generate_conversation_name
(
cls
,
tenant_id
:
str
,
query
,
answer
):
prompt
=
CONVERSATION_TITLE_PROMPT
if
len
(
query
)
>
2000
:
query
=
query
[:
300
]
+
"...[TRUNCATED]..."
+
query
[
-
300
:]
prompt
=
prompt
.
format
(
query
=
query
)
llm
:
StreamableOpenAI
=
LLMBuilder
.
to_llm
(
tenant_id
=
tenant_id
,
...
...
@@ -52,7 +56,17 @@ class LLMGenerator:
if
not
message
.
answer
:
continue
message_qa_text
=
"Human:"
+
message
.
query
+
"
\n
AI:"
+
message
.
answer
+
"
\n
"
if
len
(
message
.
query
)
>
2000
:
query
=
message
.
query
[:
300
]
+
"...[TRUNCATED]..."
+
message
.
query
[
-
300
:]
else
:
query
=
message
.
query
if
len
(
message
.
answer
)
>
2000
:
answer
=
message
.
answer
[:
300
]
+
"...[TRUNCATED]..."
+
message
.
answer
[
-
300
:]
else
:
answer
=
message
.
answer
message_qa_text
=
"
\n\n
Human:"
+
query
+
"
\n\n
Assistant:"
+
answer
if
rest_tokens
-
TokenCalculator
.
get_num_tokens
(
model
,
context
+
message_qa_text
)
>
0
:
context
+=
message_qa_text
...
...
api/core/index/index.py
View file @
9098d099
...
...
@@ -17,7 +17,7 @@ class IndexBuilder:
model_credentials
=
LLMBuilder
.
get_model_credentials
(
tenant_id
=
dataset
.
tenant_id
,
model_provider
=
LLMBuilder
.
get_default_provider
(
dataset
.
tenant_id
),
model_provider
=
LLMBuilder
.
get_default_provider
(
dataset
.
tenant_id
,
'text-embedding-ada-002'
),
model_name
=
'text-embedding-ada-002'
)
...
...
api/core/llm/error.py
View file @
9098d099
...
...
@@ -40,6 +40,9 @@ class ProviderTokenNotInitError(Exception):
"""
description
=
"Provider Token Not Init"
def
__init__
(
self
,
*
args
,
**
kwargs
):
self
.
description
=
args
[
0
]
if
args
else
self
.
description
class
QuotaExceededError
(
Exception
):
"""
...
...
api/core/llm/llm_builder.py
View file @
9098d099
...
...
@@ -8,9 +8,10 @@ from core.llm.provider.base import BaseProvider
from
core.llm.provider.llm_provider_service
import
LLMProviderService
from
core.llm.streamable_azure_chat_open_ai
import
StreamableAzureChatOpenAI
from
core.llm.streamable_azure_open_ai
import
StreamableAzureOpenAI
from
core.llm.streamable_chat_anthropic
import
StreamableChatAnthropic
from
core.llm.streamable_chat_open_ai
import
StreamableChatOpenAI
from
core.llm.streamable_open_ai
import
StreamableOpenAI
from
models.provider
import
ProviderType
from
models.provider
import
ProviderType
,
ProviderName
class
LLMBuilder
:
...
...
@@ -32,43 +33,43 @@ class LLMBuilder:
@
classmethod
def
to_llm
(
cls
,
tenant_id
:
str
,
model_name
:
str
,
**
kwargs
)
->
Union
[
StreamableOpenAI
,
StreamableChatOpenAI
]:
provider
=
cls
.
get_default_provider
(
tenant_id
)
provider
=
cls
.
get_default_provider
(
tenant_id
,
model_name
)
model_credentials
=
cls
.
get_model_credentials
(
tenant_id
,
provider
,
model_name
)
llm_cls
=
None
mode
=
cls
.
get_mode_by_model
(
model_name
)
if
mode
==
'chat'
:
if
provider
==
'openai'
:
if
provider
==
ProviderName
.
OPENAI
.
value
:
llm_cls
=
StreamableChatOpenAI
el
s
e
:
el
if
provider
==
ProviderName
.
AZURE_OPENAI
.
valu
e
:
llm_cls
=
StreamableAzureChatOpenAI
elif
provider
==
ProviderName
.
ANTHROPIC
.
value
:
llm_cls
=
StreamableChatAnthropic
elif
mode
==
'completion'
:
if
provider
==
'openai'
:
if
provider
==
ProviderName
.
OPENAI
.
value
:
llm_cls
=
StreamableOpenAI
el
s
e
:
el
if
provider
==
ProviderName
.
AZURE_OPENAI
.
valu
e
:
llm_cls
=
StreamableAzureOpenAI
else
:
raise
ValueError
(
f
"model name {model_name} is not supported."
)
if
not
llm_cls
:
raise
ValueError
(
f
"model name {model_name} is not supported."
)
model_kwargs
=
{
'model_name'
:
model_name
,
'temperature'
:
kwargs
.
get
(
'temperature'
,
0
),
'max_tokens'
:
kwargs
.
get
(
'max_tokens'
,
256
),
'top_p'
:
kwargs
.
get
(
'top_p'
,
1
),
'frequency_penalty'
:
kwargs
.
get
(
'frequency_penalty'
,
0
),
'presence_penalty'
:
kwargs
.
get
(
'presence_penalty'
,
0
),
'callbacks'
:
kwargs
.
get
(
'callbacks'
,
None
),
'streaming'
:
kwargs
.
get
(
'streaming'
,
False
),
}
model_extras_kwargs
=
model_kwargs
if
mode
==
'completion'
else
{
'model_kwargs'
:
model_kwargs
}
model_kwargs
.
update
(
model_credentials
)
model_kwargs
=
llm_cls
.
get_kwargs_from_model_params
(
model_kwargs
)
return
llm_cls
(
model_name
=
model_name
,
temperature
=
kwargs
.
get
(
'temperature'
,
0
),
max_tokens
=
kwargs
.
get
(
'max_tokens'
,
256
),
**
model_extras_kwargs
,
callbacks
=
kwargs
.
get
(
'callbacks'
,
None
),
streaming
=
kwargs
.
get
(
'streaming'
,
False
),
# request_timeout=None
**
model_credentials
)
return
llm_cls
(
**
model_kwargs
)
@
classmethod
def
to_llm_from_model
(
cls
,
tenant_id
:
str
,
model
:
dict
,
streaming
:
bool
=
False
,
...
...
@@ -118,10 +119,25 @@ class LLMBuilder:
return
provider_service
.
get_credentials
(
model_name
)
@
classmethod
def
get_default_provider
(
cls
,
tenant_id
:
str
)
->
str
:
provider
=
BaseProvider
.
get_valid_provider
(
tenant_id
)
def
get_default_provider
(
cls
,
tenant_id
:
str
,
model_name
:
str
)
->
str
:
provider_name
=
llm_constant
.
models
[
model_name
]
if
provider_name
==
'openai'
:
# get the default provider (openai / azure_openai) for the tenant
openai_provider
=
BaseProvider
.
get_valid_provider
(
tenant_id
,
ProviderName
.
OPENAI
.
value
)
azure_openai_provider
=
BaseProvider
.
get_valid_provider
(
tenant_id
,
ProviderName
.
AZURE_OPENAI
.
value
)
provider
=
None
if
openai_provider
:
provider
=
openai_provider
elif
azure_openai_provider
:
provider
=
azure_openai_provider
if
not
provider
:
raise
ProviderTokenNotInitError
()
raise
ProviderTokenNotInitError
(
f
"No valid {provider_name} model provider credentials found. "
f
"Please go to Settings -> Model Provider to complete your provider credentials."
)
if
provider
.
provider_type
==
ProviderType
.
SYSTEM
.
value
:
provider_name
=
'openai'
...
...
api/core/llm/provider/anthropic_provider.py
View file @
9098d099
from
typing
import
Optional
import
json
import
logging
from
typing
import
Optional
,
Union
import
anthropic
from
langchain.chat_models
import
ChatAnthropic
from
langchain.schema
import
HumanMessage
from
core
import
hosted_llm_credentials
from
core.llm.error
import
ProviderTokenNotInitError
from
core.llm.provider.base
import
BaseProvider
from
models.provider
import
ProviderName
from
core.llm.provider.errors
import
ValidateFailedError
from
models.provider
import
ProviderName
,
ProviderType
class
AnthropicProvider
(
BaseProvider
):
def
get_models
(
self
,
model_id
:
Optional
[
str
]
=
None
)
->
list
[
dict
]:
credentials
=
self
.
get_credentials
(
model_id
)
# todo
return
[]
return
[
{
'id'
:
'claude-instant-1'
,
'name'
:
'claude-instant-1'
,
},
{
'id'
:
'claude-2'
,
'name'
:
'claude-2'
,
},
]
def
get_credentials
(
self
,
model_id
:
Optional
[
str
]
=
None
)
->
dict
:
return
self
.
get_provider_api_key
(
model_id
=
model_id
)
def
get_provider_name
(
self
):
return
ProviderName
.
ANTHROPIC
def
get_provider_configs
(
self
,
obfuscated
:
bool
=
False
,
only_custom
:
bool
=
False
)
->
Union
[
str
|
dict
]:
"""
Returns the API credentials for Azure OpenAI as a dictionary, for the given tenant_id.
The dictionary contains keys: azure_api_type, azure_api_version, azure_api_base, and azure_api_key.
Returns the provider configs.
"""
return
{
'anthropic_api_key'
:
self
.
get_provider_api_key
(
model_id
=
model_id
)
try
:
config
=
self
.
get_provider_api_key
(
only_custom
=
only_custom
)
except
:
config
=
{
'anthropic_api_key'
:
''
}
def
get_provider_name
(
self
):
return
ProviderName
.
ANTHROPIC
\ No newline at end of file
if
obfuscated
:
if
not
config
.
get
(
'anthropic_api_key'
):
config
=
{
'anthropic_api_key'
:
''
}
config
[
'anthropic_api_key'
]
=
self
.
obfuscated_token
(
config
.
get
(
'anthropic_api_key'
))
return
config
return
config
def
get_encrypted_token
(
self
,
config
:
Union
[
dict
|
str
]):
"""
Returns the encrypted token.
"""
return
json
.
dumps
({
'anthropic_api_key'
:
self
.
encrypt_token
(
config
[
'anthropic_api_key'
])
})
def
get_decrypted_token
(
self
,
token
:
str
):
"""
Returns the decrypted token.
"""
config
=
json
.
loads
(
token
)
config
[
'anthropic_api_key'
]
=
self
.
decrypt_token
(
config
[
'anthropic_api_key'
])
return
config
def
get_token_type
(
self
):
return
dict
def
config_validate
(
self
,
config
:
Union
[
dict
|
str
]):
"""
Validates the given config.
"""
# check OpenAI / Azure OpenAI credential is valid
openai_provider
=
BaseProvider
.
get_valid_provider
(
self
.
tenant_id
,
ProviderName
.
OPENAI
.
value
)
azure_openai_provider
=
BaseProvider
.
get_valid_provider
(
self
.
tenant_id
,
ProviderName
.
AZURE_OPENAI
.
value
)
provider
=
None
if
openai_provider
:
provider
=
openai_provider
elif
azure_openai_provider
:
provider
=
azure_openai_provider
if
not
provider
:
raise
ValidateFailedError
(
f
"OpenAI or Azure OpenAI provider must be configured first."
)
if
provider
.
provider_type
==
ProviderType
.
SYSTEM
.
value
:
quota_used
=
provider
.
quota_used
if
provider
.
quota_used
is
not
None
else
0
quota_limit
=
provider
.
quota_limit
if
provider
.
quota_limit
is
not
None
else
0
if
quota_used
>=
quota_limit
:
raise
ValidateFailedError
(
f
"Your quota for Dify Hosted OpenAI has been exhausted, "
f
"please configure OpenAI or Azure OpenAI provider first."
)
try
:
if
not
isinstance
(
config
,
dict
):
raise
ValueError
(
'Config must be a object.'
)
if
'anthropic_api_key'
not
in
config
:
raise
ValueError
(
'anthropic_api_key must be provided.'
)
chat_llm
=
ChatAnthropic
(
model
=
'claude-instant-1'
,
anthropic_api_key
=
config
[
'anthropic_api_key'
],
max_tokens_to_sample
=
10
,
temperature
=
0
,
default_request_timeout
=
60
)
messages
=
[
HumanMessage
(
content
=
"ping"
)
]
chat_llm
(
messages
)
except
anthropic
.
APIConnectionError
as
ex
:
raise
ValidateFailedError
(
f
"Anthropic: Connection error, cause: {ex.__cause__}"
)
except
(
anthropic
.
APIStatusError
,
anthropic
.
RateLimitError
)
as
ex
:
raise
ValidateFailedError
(
f
"Anthropic: Error code: {ex.status_code} - "
f
"{ex.body['error']['type']}: {ex.body['error']['message']}"
)
except
Exception
as
ex
:
logging
.
exception
(
'Anthropic config validation failed'
)
raise
ex
def
get_hosted_credentials
(
self
)
->
Union
[
str
|
dict
]:
if
not
hosted_llm_credentials
.
anthropic
or
not
hosted_llm_credentials
.
anthropic
.
api_key
:
raise
ProviderTokenNotInitError
(
f
"No valid {self.get_provider_name().value} model provider credentials found. "
f
"Please go to Settings -> Model Provider to complete your provider credentials."
)
return
{
'anthropic_api_key'
:
hosted_llm_credentials
.
anthropic
.
api_key
}
api/core/llm/provider/azure_provider.py
View file @
9098d099
...
...
@@ -52,12 +52,12 @@ class AzureProvider(BaseProvider):
def
get_provider_name
(
self
):
return
ProviderName
.
AZURE_OPENAI
def
get_provider_configs
(
self
,
obfuscated
:
bool
=
False
)
->
Union
[
str
|
dict
]:
def
get_provider_configs
(
self
,
obfuscated
:
bool
=
False
,
only_custom
:
bool
=
False
)
->
Union
[
str
|
dict
]:
"""
Returns the provider configs.
"""
try
:
config
=
self
.
get_provider_api_key
()
config
=
self
.
get_provider_api_key
(
only_custom
=
only_custom
)
except
:
config
=
{
'openai_api_type'
:
'azure'
,
...
...
@@ -81,7 +81,6 @@ class AzureProvider(BaseProvider):
return
config
def
get_token_type
(
self
):
# TODO: change to dict when implemented
return
dict
def
config_validate
(
self
,
config
:
Union
[
dict
|
str
]):
...
...
api/core/llm/provider/base.py
View file @
9098d099
...
...
@@ -2,7 +2,7 @@ import base64
from
abc
import
ABC
,
abstractmethod
from
typing
import
Optional
,
Union
from
core
import
hosted_llm_credentials
from
core
.constant
import
llm_constant
from
core.llm.error
import
QuotaExceededError
,
ModelCurrentlyNotSupportError
,
ProviderTokenNotInitError
from
extensions.ext_database
import
db
from
libs
import
rsa
...
...
@@ -14,15 +14,18 @@ class BaseProvider(ABC):
def
__init__
(
self
,
tenant_id
:
str
):
self
.
tenant_id
=
tenant_id
def
get_provider_api_key
(
self
,
model_id
:
Optional
[
str
]
=
None
,
prefer_custom
:
bool
=
Tru
e
)
->
Union
[
str
|
dict
]:
def
get_provider_api_key
(
self
,
model_id
:
Optional
[
str
]
=
None
,
only_custom
:
bool
=
Fals
e
)
->
Union
[
str
|
dict
]:
"""
Returns the decrypted API key for the given tenant_id and provider_name.
If the provider is of type SYSTEM and the quota is exceeded, raises a QuotaExceededError.
If the provider is not found or not valid, raises a ProviderTokenNotInitError.
"""
provider
=
self
.
get_provider
(
prefer
_custom
)
provider
=
self
.
get_provider
(
only
_custom
)
if
not
provider
:
raise
ProviderTokenNotInitError
()
raise
ProviderTokenNotInitError
(
f
"No valid {llm_constant.models[model_id]} model provider credentials found. "
f
"Please go to Settings -> Model Provider to complete your provider credentials."
)
if
provider
.
provider_type
==
ProviderType
.
SYSTEM
.
value
:
quota_used
=
provider
.
quota_used
if
provider
.
quota_used
is
not
None
else
0
...
...
@@ -38,18 +41,19 @@ class BaseProvider(ABC):
else
:
return
self
.
get_decrypted_token
(
provider
.
encrypted_config
)
def
get_provider
(
self
,
prefer_custom
:
bool
)
->
Optional
[
Provider
]:
def
get_provider
(
self
,
only_custom
:
bool
=
False
)
->
Optional
[
Provider
]:
"""
Returns the Provider instance for the given tenant_id and provider_name.
If both CUSTOM and System providers exist, the preferred provider will be returned based on the prefer_custom flag.
"""
return
BaseProvider
.
get_valid_provider
(
self
.
tenant_id
,
self
.
get_provider_name
()
.
value
,
prefer
_custom
)
return
BaseProvider
.
get_valid_provider
(
self
.
tenant_id
,
self
.
get_provider_name
()
.
value
,
only
_custom
)
@
classmethod
def
get_valid_provider
(
cls
,
tenant_id
:
str
,
provider_name
:
str
=
None
,
prefer_custom
:
bool
=
False
)
->
Optional
[
Provider
]:
def
get_valid_provider
(
cls
,
tenant_id
:
str
,
provider_name
:
str
=
None
,
only_custom
:
bool
=
False
)
->
Optional
[
Provider
]:
"""
Returns the Provider instance for the given tenant_id and provider_name.
If both CUSTOM and System providers exist
, the preferred provider will be returned based on the prefer_custom flag
.
If both CUSTOM and System providers exist.
"""
query
=
db
.
session
.
query
(
Provider
)
.
filter
(
Provider
.
tenant_id
==
tenant_id
...
...
@@ -58,39 +62,31 @@ class BaseProvider(ABC):
if
provider_name
:
query
=
query
.
filter
(
Provider
.
provider_name
==
provider_name
)
providers
=
query
.
order_by
(
Provider
.
provider_type
.
desc
()
if
prefer_custom
else
Provider
.
provider_type
)
.
all
()
if
only_custom
:
query
=
query
.
filter
(
Provider
.
provider_type
==
ProviderType
.
CUSTOM
.
value
)
custom_provider
=
None
system_provider
=
None
providers
=
query
.
order_by
(
Provider
.
provider_type
.
asc
())
.
all
()
for
provider
in
providers
:
if
provider
.
provider_type
==
ProviderType
.
CUSTOM
.
value
and
provider
.
is_valid
and
provider
.
encrypted_config
:
custom_provider
=
provider
return
provider
elif
provider
.
provider_type
==
ProviderType
.
SYSTEM
.
value
and
provider
.
is_valid
:
system_provider
=
provider
return
provider
if
custom_provider
:
return
custom_provider
elif
system_provider
:
return
system_provider
else
:
return
None
def
get_hosted_credentials
(
self
)
->
str
:
if
self
.
get_provider_name
()
!=
ProviderName
.
OPENAI
:
raise
ProviderTokenNotInitError
()
if
not
hosted_llm_credentials
.
openai
or
not
hosted_llm_credentials
.
openai
.
api_key
:
raise
ProviderTokenNotInitError
()
return
hosted_llm_credentials
.
openai
.
api_key
def
get_hosted_credentials
(
self
)
->
Union
[
str
|
dict
]:
raise
ProviderTokenNotInitError
(
f
"No valid {self.get_provider_name().value} model provider credentials found. "
f
"Please go to Settings -> Model Provider to complete your provider credentials."
)
def
get_provider_configs
(
self
,
obfuscated
:
bool
=
False
)
->
Union
[
str
|
dict
]:
def
get_provider_configs
(
self
,
obfuscated
:
bool
=
False
,
only_custom
:
bool
=
False
)
->
Union
[
str
|
dict
]:
"""
Returns the provider configs.
"""
try
:
config
=
self
.
get_provider_api_key
()
config
=
self
.
get_provider_api_key
(
only_custom
=
only_custom
)
except
:
config
=
''
...
...
api/core/llm/provider/llm_provider_service.py
View file @
9098d099
...
...
@@ -31,11 +31,11 @@ class LLMProviderService:
def
get_credentials
(
self
,
model_id
:
Optional
[
str
]
=
None
)
->
dict
:
return
self
.
provider
.
get_credentials
(
model_id
)
def
get_provider_configs
(
self
,
obfuscated
:
bool
=
False
)
->
Union
[
str
|
dict
]:
return
self
.
provider
.
get_provider_configs
(
obfuscated
)
def
get_provider_configs
(
self
,
obfuscated
:
bool
=
False
,
only_custom
:
bool
=
False
)
->
Union
[
str
|
dict
]:
return
self
.
provider
.
get_provider_configs
(
obfuscated
=
obfuscated
,
only_custom
=
only_custom
)
def
get_provider_db_record
(
self
,
prefer_custom
:
bool
=
False
)
->
Optional
[
Provider
]:
return
self
.
provider
.
get_provider
(
prefer_custom
)
def
get_provider_db_record
(
self
)
->
Optional
[
Provider
]:
return
self
.
provider
.
get_provider
()
def
config_validate
(
self
,
config
:
Union
[
dict
|
str
]):
"""
...
...
api/core/llm/provider/openai_provider.py
View file @
9098d099
...
...
@@ -4,6 +4,8 @@ from typing import Optional, Union
import
openai
from
openai.error
import
AuthenticationError
,
OpenAIError
from
core
import
hosted_llm_credentials
from
core.llm.error
import
ProviderTokenNotInitError
from
core.llm.moderation
import
Moderation
from
core.llm.provider.base
import
BaseProvider
from
core.llm.provider.errors
import
ValidateFailedError
...
...
@@ -42,3 +44,12 @@ class OpenAIProvider(BaseProvider):
except
Exception
as
ex
:
logging
.
exception
(
'OpenAI config validation failed'
)
raise
ex
def
get_hosted_credentials
(
self
)
->
Union
[
str
|
dict
]:
if
not
hosted_llm_credentials
.
openai
or
not
hosted_llm_credentials
.
openai
.
api_key
:
raise
ProviderTokenNotInitError
(
f
"No valid {self.get_provider_name().value} model provider credentials found. "
f
"Please go to Settings -> Model Provider to complete your provider credentials."
)
return
hosted_llm_credentials
.
openai
.
api_key
api/core/llm/streamable_azure_chat_open_ai.py
View file @
9098d099
from
langchain.callbacks.manager
import
Callback
ManagerForLLMRun
,
AsyncCallbackManagerForLLMRun
,
Callback
s
from
langchain.schema
import
BaseMessage
,
ChatResult
,
LLMResult
from
langchain.callbacks.manager
import
Callbacks
from
langchain.schema
import
BaseMessage
,
LLMResult
from
langchain.chat_models
import
AzureChatOpenAI
from
typing
import
Optional
,
List
,
Dict
,
Any
from
pydantic
import
root_validator
from
core.llm.
error_handle_wraps
import
handle_llm_exceptions
,
handle_llm_exceptions_async
from
core.llm.
wrappers.openai_wrapper
import
handle_openai_exceptions
class
StreamableAzureChatOpenAI
(
AzureChatOpenAI
):
...
...
@@ -46,30 +46,7 @@ class StreamableAzureChatOpenAI(AzureChatOpenAI):
"organization"
:
self
.
openai_organization
if
self
.
openai_organization
else
None
,
}
def
get_messages_tokens
(
self
,
messages
:
List
[
BaseMessage
])
->
int
:
"""Get the number of tokens in a list of messages.
Args:
messages: The messages to count the tokens of.
Returns:
The number of tokens in the messages.
"""
tokens_per_message
=
5
tokens_per_request
=
3
message_tokens
=
tokens_per_request
message_strs
=
''
for
message
in
messages
:
message_strs
+=
message
.
content
message_tokens
+=
tokens_per_message
# calc once
message_tokens
+=
self
.
get_num_tokens
(
message_strs
)
return
message_tokens
@
handle_llm_exceptions
@
handle_openai_exceptions
def
generate
(
self
,
messages
:
List
[
List
[
BaseMessage
]],
...
...
@@ -79,12 +56,18 @@ class StreamableAzureChatOpenAI(AzureChatOpenAI):
)
->
LLMResult
:
return
super
()
.
generate
(
messages
,
stop
,
callbacks
,
**
kwargs
)
@
handle_llm_exceptions_async
async
def
agenerate
(
self
,
messages
:
List
[
List
[
BaseMessage
]],
stop
:
Optional
[
List
[
str
]]
=
None
,
callbacks
:
Callbacks
=
None
,
**
kwargs
:
Any
,
)
->
LLMResult
:
return
await
super
()
.
agenerate
(
messages
,
stop
,
callbacks
,
**
kwargs
)
@
classmethod
def
get_kwargs_from_model_params
(
cls
,
params
:
dict
):
model_kwargs
=
{
'top_p'
:
params
.
get
(
'top_p'
,
1
),
'frequency_penalty'
:
params
.
get
(
'frequency_penalty'
,
0
),
'presence_penalty'
:
params
.
get
(
'presence_penalty'
,
0
),
}
del
params
[
'top_p'
]
del
params
[
'frequency_penalty'
]
del
params
[
'presence_penalty'
]
params
[
'model_kwargs'
]
=
model_kwargs
return
params
api/core/llm/streamable_azure_open_ai.py
View file @
9098d099
...
...
@@ -5,7 +5,7 @@ from typing import Optional, List, Dict, Mapping, Any
from
pydantic
import
root_validator
from
core.llm.
error_handle_wraps
import
handle_llm_exceptions
,
handle_llm_exceptions_async
from
core.llm.
wrappers.openai_wrapper
import
handle_openai_exceptions
class
StreamableAzureOpenAI
(
AzureOpenAI
):
...
...
@@ -50,7 +50,7 @@ class StreamableAzureOpenAI(AzureOpenAI):
"organization"
:
self
.
openai_organization
if
self
.
openai_organization
else
None
,
}}
@
handle_
llm
_exceptions
@
handle_
openai
_exceptions
def
generate
(
self
,
prompts
:
List
[
str
],
...
...
@@ -60,12 +60,6 @@ class StreamableAzureOpenAI(AzureOpenAI):
)
->
LLMResult
:
return
super
()
.
generate
(
prompts
,
stop
,
callbacks
,
**
kwargs
)
@
handle_llm_exceptions_async
async
def
agenerate
(
self
,
prompts
:
List
[
str
],
stop
:
Optional
[
List
[
str
]]
=
None
,
callbacks
:
Callbacks
=
None
,
**
kwargs
:
Any
,
)
->
LLMResult
:
return
await
super
()
.
agenerate
(
prompts
,
stop
,
callbacks
,
**
kwargs
)
@
classmethod
def
get_kwargs_from_model_params
(
cls
,
params
:
dict
):
return
params
api/core/llm/streamable_chat_anthropic.py
0 → 100644
View file @
9098d099
from
typing
import
List
,
Optional
,
Any
,
Dict
from
langchain.callbacks.manager
import
Callbacks
from
langchain.chat_models
import
ChatAnthropic
from
langchain.schema
import
BaseMessage
,
LLMResult
from
core.llm.wrappers.anthropic_wrapper
import
handle_anthropic_exceptions
class
StreamableChatAnthropic
(
ChatAnthropic
):
"""
Wrapper around Anthropic's large language model.
"""
@
handle_anthropic_exceptions
def
generate
(
self
,
messages
:
List
[
List
[
BaseMessage
]],
stop
:
Optional
[
List
[
str
]]
=
None
,
callbacks
:
Callbacks
=
None
,
*
,
tags
:
Optional
[
List
[
str
]]
=
None
,
metadata
:
Optional
[
Dict
[
str
,
Any
]]
=
None
,
**
kwargs
:
Any
,
)
->
LLMResult
:
return
super
()
.
generate
(
messages
,
stop
,
callbacks
,
tags
=
tags
,
metadata
=
metadata
,
**
kwargs
)
@
classmethod
def
get_kwargs_from_model_params
(
cls
,
params
:
dict
):
params
[
'model'
]
=
params
.
get
(
'model_name'
)
del
params
[
'model_name'
]
params
[
'max_tokens_to_sample'
]
=
params
.
get
(
'max_tokens'
)
del
params
[
'max_tokens'
]
del
params
[
'frequency_penalty'
]
del
params
[
'presence_penalty'
]
return
params
api/core/llm/streamable_chat_open_ai.py
View file @
9098d099
...
...
@@ -7,7 +7,7 @@ from typing import Optional, List, Dict, Any
from
pydantic
import
root_validator
from
core.llm.
error_handle_wraps
import
handle_llm_exceptions
,
handle_llm_exceptions_async
from
core.llm.
wrappers.openai_wrapper
import
handle_openai_exceptions
class
StreamableChatOpenAI
(
ChatOpenAI
):
...
...
@@ -48,30 +48,7 @@ class StreamableChatOpenAI(ChatOpenAI):
"organization"
:
self
.
openai_organization
if
self
.
openai_organization
else
None
,
}
def
get_messages_tokens
(
self
,
messages
:
List
[
BaseMessage
])
->
int
:
"""Get the number of tokens in a list of messages.
Args:
messages: The messages to count the tokens of.
Returns:
The number of tokens in the messages.
"""
tokens_per_message
=
5
tokens_per_request
=
3
message_tokens
=
tokens_per_request
message_strs
=
''
for
message
in
messages
:
message_strs
+=
message
.
content
message_tokens
+=
tokens_per_message
# calc once
message_tokens
+=
self
.
get_num_tokens
(
message_strs
)
return
message_tokens
@
handle_llm_exceptions
@
handle_openai_exceptions
def
generate
(
self
,
messages
:
List
[
List
[
BaseMessage
]],
...
...
@@ -81,12 +58,18 @@ class StreamableChatOpenAI(ChatOpenAI):
)
->
LLMResult
:
return
super
()
.
generate
(
messages
,
stop
,
callbacks
,
**
kwargs
)
@
handle_llm_exceptions_async
async
def
agenerate
(
self
,
messages
:
List
[
List
[
BaseMessage
]],
stop
:
Optional
[
List
[
str
]]
=
None
,
callbacks
:
Callbacks
=
None
,
**
kwargs
:
Any
,
)
->
LLMResult
:
return
await
super
()
.
agenerate
(
messages
,
stop
,
callbacks
,
**
kwargs
)
@
classmethod
def
get_kwargs_from_model_params
(
cls
,
params
:
dict
):
model_kwargs
=
{
'top_p'
:
params
.
get
(
'top_p'
,
1
),
'frequency_penalty'
:
params
.
get
(
'frequency_penalty'
,
0
),
'presence_penalty'
:
params
.
get
(
'presence_penalty'
,
0
),
}
del
params
[
'top_p'
]
del
params
[
'frequency_penalty'
]
del
params
[
'presence_penalty'
]
params
[
'model_kwargs'
]
=
model_kwargs
return
params
api/core/llm/streamable_open_ai.py
View file @
9098d099
...
...
@@ -6,7 +6,7 @@ from typing import Optional, List, Dict, Any, Mapping
from
langchain
import
OpenAI
from
pydantic
import
root_validator
from
core.llm.
error_handle_wraps
import
handle_llm_exceptions
,
handle_llm_exceptions_async
from
core.llm.
wrappers.openai_wrapper
import
handle_openai_exceptions
class
StreamableOpenAI
(
OpenAI
):
...
...
@@ -49,7 +49,7 @@ class StreamableOpenAI(OpenAI):
"organization"
:
self
.
openai_organization
if
self
.
openai_organization
else
None
,
}}
@
handle_
llm
_exceptions
@
handle_
openai
_exceptions
def
generate
(
self
,
prompts
:
List
[
str
],
...
...
@@ -59,12 +59,6 @@ class StreamableOpenAI(OpenAI):
)
->
LLMResult
:
return
super
()
.
generate
(
prompts
,
stop
,
callbacks
,
**
kwargs
)
@
handle_llm_exceptions_async
async
def
agenerate
(
self
,
prompts
:
List
[
str
],
stop
:
Optional
[
List
[
str
]]
=
None
,
callbacks
:
Callbacks
=
None
,
**
kwargs
:
Any
,
)
->
LLMResult
:
return
await
super
()
.
agenerate
(
prompts
,
stop
,
callbacks
,
**
kwargs
)
@
classmethod
def
get_kwargs_from_model_params
(
cls
,
params
:
dict
):
return
params
api/core/llm/whisper.py
View file @
9098d099
import
openai
from
core.llm.wrappers.openai_wrapper
import
handle_openai_exceptions
from
models.provider
import
ProviderName
from
core.llm.error_handle_wraps
import
handle_llm_exceptions
from
core.llm.provider.base
import
BaseProvider
...
...
@@ -13,7 +14,7 @@ class Whisper:
self
.
client
=
openai
.
Audio
self
.
credentials
=
provider
.
get_credentials
()
@
handle_
llm
_exceptions
@
handle_
openai
_exceptions
def
transcribe
(
self
,
file
):
return
self
.
client
.
transcribe
(
model
=
'whisper-1'
,
...
...
api/core/llm/wrappers/anthropic_wrapper.py
0 → 100644
View file @
9098d099
import
logging
from
functools
import
wraps
import
anthropic
from
core.llm.error
import
LLMAPIConnectionError
,
LLMAPIUnavailableError
,
LLMRateLimitError
,
LLMAuthorizationError
,
\
LLMBadRequestError
def
handle_anthropic_exceptions
(
func
):
@
wraps
(
func
)
def
wrapper
(
*
args
,
**
kwargs
):
try
:
return
func
(
*
args
,
**
kwargs
)
except
anthropic
.
APIConnectionError
as
e
:
logging
.
exception
(
"Failed to connect to Anthropic API."
)
raise
LLMAPIConnectionError
(
f
"Anthropic: The server could not be reached, cause: {e.__cause__}"
)
except
anthropic
.
RateLimitError
:
raise
LLMRateLimitError
(
"Anthropic: A 429 status code was received; we should back off a bit."
)
except
anthropic
.
AuthenticationError
as
e
:
raise
LLMAuthorizationError
(
f
"Anthropic: {e.message}"
)
except
anthropic
.
BadRequestError
as
e
:
raise
LLMBadRequestError
(
f
"Anthropic: {e.message}"
)
except
anthropic
.
APIStatusError
as
e
:
raise
LLMAPIUnavailableError
(
f
"Anthropic: code: {e.status_code}, cause: {e.message}"
)
return
wrapper
api/core/llm/
error_handle_wraps
.py
→
api/core/llm/
wrappers/openai_wrapper
.py
View file @
9098d099
...
...
@@ -7,7 +7,7 @@ from core.llm.error import LLMAPIConnectionError, LLMAPIUnavailableError, LLMRat
LLMBadRequestError
def
handle_
llm
_exceptions
(
func
):
def
handle_
openai
_exceptions
(
func
):
@
wraps
(
func
)
def
wrapper
(
*
args
,
**
kwargs
):
try
:
...
...
@@ -29,27 +29,3 @@ def handle_llm_exceptions(func):
raise
LLMBadRequestError
(
e
.
__class__
.
__name__
+
":"
+
str
(
e
))
return
wrapper
def
handle_llm_exceptions_async
(
func
):
@
wraps
(
func
)
async
def
wrapper
(
*
args
,
**
kwargs
):
try
:
return
await
func
(
*
args
,
**
kwargs
)
except
openai
.
error
.
InvalidRequestError
as
e
:
logging
.
exception
(
"Invalid request to OpenAI API."
)
raise
LLMBadRequestError
(
str
(
e
))
except
openai
.
error
.
APIConnectionError
as
e
:
logging
.
exception
(
"Failed to connect to OpenAI API."
)
raise
LLMAPIConnectionError
(
e
.
__class__
.
__name__
+
":"
+
str
(
e
))
except
(
openai
.
error
.
APIError
,
openai
.
error
.
ServiceUnavailableError
,
openai
.
error
.
Timeout
)
as
e
:
logging
.
exception
(
"OpenAI service unavailable."
)
raise
LLMAPIUnavailableError
(
e
.
__class__
.
__name__
+
":"
+
str
(
e
))
except
openai
.
error
.
RateLimitError
as
e
:
raise
LLMRateLimitError
(
str
(
e
))
except
openai
.
error
.
AuthenticationError
as
e
:
raise
LLMAuthorizationError
(
str
(
e
))
except
openai
.
error
.
OpenAIError
as
e
:
raise
LLMBadRequestError
(
e
.
__class__
.
__name__
+
":"
+
str
(
e
))
return
wrapper
api/core/memory/read_only_conversation_token_db_buffer_shared_memory.py
View file @
9098d099
from
typing
import
Any
,
List
,
Dict
,
Union
from
langchain.memory.chat_memory
import
BaseChatMemory
from
langchain.schema
import
get_buffer_string
,
BaseMessage
,
HumanMessage
,
AIMessage
from
langchain.schema
import
get_buffer_string
,
BaseMessage
,
HumanMessage
,
AIMessage
,
BaseLanguageModel
from
core.llm.streamable_chat_open_ai
import
StreamableChatOpenAI
from
core.llm.streamable_open_ai
import
StreamableOpenAI
...
...
@@ -12,8 +12,8 @@ from models.model import Conversation, Message
class
ReadOnlyConversationTokenDBBufferSharedMemory
(
BaseChatMemory
):
conversation
:
Conversation
human_prefix
:
str
=
"Human"
ai_prefix
:
str
=
"A
I
"
llm
:
Union
[
StreamableChatOpenAI
|
StreamableOpenAI
]
ai_prefix
:
str
=
"A
ssistant
"
llm
:
BaseLanguageModel
memory_key
:
str
=
"chat_history"
max_token_limit
:
int
=
2000
message_limit
:
int
=
10
...
...
@@ -38,12 +38,12 @@ class ReadOnlyConversationTokenDBBufferSharedMemory(BaseChatMemory):
return
chat_messages
# prune the chat message if it exceeds the max token limit
curr_buffer_length
=
self
.
llm
.
get_
messages_token
s
(
chat_messages
)
curr_buffer_length
=
self
.
llm
.
get_
num_tokens_from_message
s
(
chat_messages
)
if
curr_buffer_length
>
self
.
max_token_limit
:
pruned_memory
=
[]
while
curr_buffer_length
>
self
.
max_token_limit
and
chat_messages
:
pruned_memory
.
append
(
chat_messages
.
pop
(
0
))
curr_buffer_length
=
self
.
llm
.
get_
messages_token
s
(
chat_messages
)
curr_buffer_length
=
self
.
llm
.
get_
num_tokens_from_message
s
(
chat_messages
)
return
chat_messages
...
...
api/core/tool/dataset_index_tool.py
View file @
9098d099
...
...
@@ -30,7 +30,7 @@ class DatasetTool(BaseTool):
else
:
model_credentials
=
LLMBuilder
.
get_model_credentials
(
tenant_id
=
self
.
dataset
.
tenant_id
,
model_provider
=
LLMBuilder
.
get_default_provider
(
self
.
dataset
.
tenant_id
),
model_provider
=
LLMBuilder
.
get_default_provider
(
self
.
dataset
.
tenant_id
,
'text-embedding-ada-002'
),
model_name
=
'text-embedding-ada-002'
)
...
...
@@ -60,7 +60,7 @@ class DatasetTool(BaseTool):
async
def
_arun
(
self
,
tool_input
:
str
)
->
str
:
model_credentials
=
LLMBuilder
.
get_model_credentials
(
tenant_id
=
self
.
dataset
.
tenant_id
,
model_provider
=
LLMBuilder
.
get_default_provider
(
self
.
dataset
.
tenant_id
),
model_provider
=
LLMBuilder
.
get_default_provider
(
self
.
dataset
.
tenant_id
,
'text-embedding-ada-002'
),
model_name
=
'text-embedding-ada-002'
)
...
...
api/events/event_handlers/create_provider_when_tenant_created.py
View file @
9098d099
from
flask
import
current_app
from
events.tenant_event
import
tenant_was_updated
from
models.provider
import
ProviderName
from
services.provider_service
import
ProviderService
...
...
@@ -6,4 +9,16 @@ from services.provider_service import ProviderService
def
handle
(
sender
,
**
kwargs
):
tenant
=
sender
if
tenant
.
status
==
'normal'
:
ProviderService
.
create_system_provider
(
tenant
)
ProviderService
.
create_system_provider
(
tenant
,
ProviderName
.
OPENAI
.
value
,
current_app
.
config
[
'OPENAI_HOSTED_QUOTA_LIMIT'
],
True
)
ProviderService
.
create_system_provider
(
tenant
,
ProviderName
.
ANTHROPIC
.
value
,
current_app
.
config
[
'ANTHROPIC_HOSTED_QUOTA_LIMIT'
],
True
)
api/events/event_handlers/create_provider_when_tenant_updated.py
View file @
9098d099
from
flask
import
current_app
from
events.tenant_event
import
tenant_was_created
from
models.provider
import
ProviderName
from
services.provider_service
import
ProviderService
...
...
@@ -6,4 +9,16 @@ from services.provider_service import ProviderService
def
handle
(
sender
,
**
kwargs
):
tenant
=
sender
if
tenant
.
status
==
'normal'
:
ProviderService
.
create_system_provider
(
tenant
)
ProviderService
.
create_system_provider
(
tenant
,
ProviderName
.
OPENAI
.
value
,
current_app
.
config
[
'OPENAI_HOSTED_QUOTA_LIMIT'
],
True
)
ProviderService
.
create_system_provider
(
tenant
,
ProviderName
.
ANTHROPIC
.
value
,
current_app
.
config
[
'ANTHROPIC_HOSTED_QUOTA_LIMIT'
],
True
)
api/extensions/ext_mail.py
0 → 100644
View file @
9098d099
from
typing
import
Optional
import
resend
from
flask
import
Flask
class
Mail
:
def
__init__
(
self
):
self
.
_client
=
None
self
.
_default_send_from
=
None
def
is_inited
(
self
)
->
bool
:
return
self
.
_client
is
not
None
def
init_app
(
self
,
app
:
Flask
):
if
app
.
config
.
get
(
'MAIL_TYPE'
):
if
app
.
config
.
get
(
'MAIL_DEFAULT_SEND_FROM'
):
self
.
_default_send_from
=
app
.
config
.
get
(
'MAIL_DEFAULT_SEND_FROM'
)
if
app
.
config
.
get
(
'MAIL_TYPE'
)
==
'resend'
:
api_key
=
app
.
config
.
get
(
'RESEND_API_KEY'
)
if
not
api_key
:
raise
ValueError
(
'RESEND_API_KEY is not set'
)
resend
.
api_key
=
api_key
self
.
_client
=
resend
.
Emails
else
:
raise
ValueError
(
'Unsupported mail type {}'
.
format
(
app
.
config
.
get
(
'MAIL_TYPE'
)))
def
send
(
self
,
to
:
str
,
subject
:
str
,
html
:
str
,
from_
:
Optional
[
str
]
=
None
):
if
not
self
.
_client
:
raise
ValueError
(
'Mail client is not initialized'
)
if
not
from_
and
self
.
_default_send_from
:
from_
=
self
.
_default_send_from
if
not
from_
:
raise
ValueError
(
'mail from is not set'
)
if
not
to
:
raise
ValueError
(
'mail to is not set'
)
if
not
subject
:
raise
ValueError
(
'mail subject is not set'
)
if
not
html
:
raise
ValueError
(
'mail html is not set'
)
self
.
_client
.
send
({
"from"
:
from_
,
"to"
:
to
,
"subject"
:
subject
,
"html"
:
html
})
def
init_app
(
app
:
Flask
):
mail
.
init_app
(
app
)
mail
=
Mail
()
api/models/account.py
View file @
9098d099
...
...
@@ -38,6 +38,10 @@ class Account(UserMixin, db.Model):
created_at
=
db
.
Column
(
db
.
DateTime
,
nullable
=
False
,
server_default
=
db
.
text
(
'CURRENT_TIMESTAMP(0)'
))
updated_at
=
db
.
Column
(
db
.
DateTime
,
nullable
=
False
,
server_default
=
db
.
text
(
'CURRENT_TIMESTAMP(0)'
))
@
property
def
is_password_set
(
self
):
return
self
.
password
is
not
None
@
property
def
current_tenant
(
self
):
return
self
.
_current_tenant
...
...
api/models/model.py
View file @
9098d099
...
...
@@ -56,7 +56,8 @@ class App(db.Model):
@
property
def
api_base_url
(
self
):
return
(
current_app
.
config
[
'API_URL'
]
if
current_app
.
config
[
'API_URL'
]
else
request
.
host_url
.
rstrip
(
'/'
))
+
'/v1'
return
(
current_app
.
config
[
'SERVICE_API_URL'
]
if
current_app
.
config
[
'SERVICE_API_URL'
]
else
request
.
host_url
.
rstrip
(
'/'
))
+
'/v1'
@
property
def
tenant
(
self
):
...
...
@@ -515,7 +516,7 @@ class Site(db.Model):
@
property
def
app_base_url
(
self
):
return
(
current_app
.
config
[
'APP_
URL'
]
if
current_app
.
config
[
'APP
_URL'
]
else
request
.
host_url
.
rstrip
(
'/'
))
return
(
current_app
.
config
[
'APP_
WEB_URL'
]
if
current_app
.
config
[
'APP_WEB
_URL'
]
else
request
.
host_url
.
rstrip
(
'/'
))
class
ApiToken
(
db
.
Model
):
...
...
api/requirements.txt
View file @
9098d099
...
...
@@ -10,7 +10,7 @@ flask-session2==1.3.1
flask-cors==3.0.10
gunicorn~=20.1.0
gevent~=22.10.2
langchain==0.0.2
09
langchain==0.0.2
30
openai~=0.27.5
psycopg2-binary~=2.9.6
pycryptodome==3.17
...
...
@@ -21,7 +21,7 @@ Authlib==1.2.0
boto3~=1.26.123
tenacity==8.2.2
cachetools~=5.3.0
weaviate-client~=3.
16.2
weaviate-client~=3.
21.0
qdrant_client~=1.1.6
mailchimp-transactional~=1.0.50
scikit-learn==1.2.2
...
...
@@ -33,4 +33,6 @@ openpyxl==3.1.2
chardet~=5.1.0
docx2txt==0.8
pypdfium2==4.16.0
resend~=0.5.1
pyjwt~=2.6.0
anthropic~=0.3.4
api/services/account_service.py
View file @
9098d099
...
...
@@ -2,13 +2,16 @@
import
base64
import
logging
import
secrets
import
uuid
from
datetime
import
datetime
from
hashlib
import
sha256
from
typing
import
Optional
from
flask
import
session
from
sqlalchemy
import
func
from
events.tenant_event
import
tenant_was_created
from
extensions.ext_redis
import
redis_client
from
services.errors.account
import
AccountLoginError
,
CurrentPasswordIncorrectError
,
LinkAccountIntegrateError
,
\
TenantNotFound
,
AccountNotLinkTenantError
,
InvalidActionError
,
CannotOperateSelfError
,
MemberNotInTenantError
,
\
RoleAlreadyAssignedError
,
NoPermissionError
,
AccountRegisterError
,
AccountAlreadyInTenantError
...
...
@@ -16,6 +19,7 @@ from libs.helper import get_remote_ip
from
libs.password
import
compare_password
,
hash_password
from
libs.rsa
import
generate_key_pair
from
models.account
import
*
from
tasks.mail_invite_member_task
import
send_invite_member_mail_task
class
AccountService
:
...
...
@@ -48,12 +52,18 @@ class AccountService:
@
staticmethod
def
update_account_password
(
account
,
password
,
new_password
):
"""update account password"""
# todo: split validation and update
if
account
.
password
and
not
compare_password
(
password
,
account
.
password
,
account
.
password_salt
):
raise
CurrentPasswordIncorrectError
(
"Current password is incorrect."
)
password_hashed
=
hash_password
(
new_password
,
account
.
password_salt
)
# generate password salt
salt
=
secrets
.
token_bytes
(
16
)
base64_salt
=
base64
.
b64encode
(
salt
)
.
decode
()
# encrypt password with salt
password_hashed
=
hash_password
(
new_password
,
salt
)
base64_password_hashed
=
base64
.
b64encode
(
password_hashed
)
.
decode
()
account
.
password
=
base64_password_hashed
account
.
password_salt
=
base64_salt
db
.
session
.
commit
()
return
account
...
...
@@ -283,8 +293,6 @@ class TenantService:
@
staticmethod
def
remove_member_from_tenant
(
tenant
:
Tenant
,
account
:
Account
,
operator
:
Account
)
->
None
:
"""Remove member from tenant"""
# todo: check permission
if
operator
.
id
==
account
.
id
and
TenantService
.
check_member_permission
(
tenant
,
operator
,
account
,
'remove'
):
raise
CannotOperateSelfError
(
"Cannot operate self."
)
...
...
@@ -293,6 +301,12 @@ class TenantService:
raise
MemberNotInTenantError
(
"Member not in tenant."
)
db
.
session
.
delete
(
ta
)
account
.
initialized_at
=
None
account
.
status
=
AccountStatus
.
PENDING
.
value
account
.
password
=
None
account
.
password_salt
=
None
db
.
session
.
commit
()
@
staticmethod
...
...
@@ -332,8 +346,8 @@ class TenantService:
class
RegisterService
:
@
static
method
def
register
(
email
,
name
,
password
:
str
=
None
,
open_id
:
str
=
None
,
provider
:
str
=
None
)
->
Account
:
@
class
method
def
register
(
cls
,
email
,
name
,
password
:
str
=
None
,
open_id
:
str
=
None
,
provider
:
str
=
None
)
->
Account
:
db
.
session
.
begin_nested
()
"""Register account"""
try
:
...
...
@@ -359,9 +373,9 @@ class RegisterService:
return
account
@
static
method
def
invite_new_member
(
tenant
:
Tenant
,
email
:
str
,
role
:
str
=
'normal'
,
inviter
:
Account
=
None
)
->
TenantAccountJoin
:
@
class
method
def
invite_new_member
(
cls
,
tenant
:
Tenant
,
email
:
str
,
role
:
str
=
'normal'
,
inviter
:
Account
=
None
)
->
str
:
"""Invite new member"""
account
=
Account
.
query
.
filter_by
(
email
=
email
)
.
first
()
...
...
@@ -380,5 +394,71 @@ class RegisterService:
if
ta
:
raise
AccountAlreadyInTenantError
(
"Account already in tenant."
)
ta
=
TenantService
.
create_tenant_member
(
tenant
,
account
,
role
)
return
ta
TenantService
.
create_tenant_member
(
tenant
,
account
,
role
)
token
=
cls
.
generate_invite_token
(
tenant
,
account
)
# send email
send_invite_member_mail_task
.
delay
(
to
=
email
,
token
=
cls
.
generate_invite_token
(
tenant
,
account
),
inviter_name
=
inviter
.
name
if
inviter
else
'Dify'
,
workspace_id
=
tenant
.
id
,
workspace_name
=
tenant
.
name
,
)
return
token
@
classmethod
def
generate_invite_token
(
cls
,
tenant
:
Tenant
,
account
:
Account
)
->
str
:
token
=
str
(
uuid
.
uuid4
())
email_hash
=
sha256
(
account
.
email
.
encode
())
.
hexdigest
()
cache_key
=
'member_invite_token:{}, {}:{}'
.
format
(
str
(
tenant
.
id
),
email_hash
,
token
)
redis_client
.
setex
(
cache_key
,
3600
,
str
(
account
.
id
))
return
token
@
classmethod
def
revoke_token
(
cls
,
workspace_id
:
str
,
email
:
str
,
token
:
str
):
email_hash
=
sha256
(
email
.
encode
())
.
hexdigest
()
cache_key
=
'member_invite_token:{}, {}:{}'
.
format
(
workspace_id
,
email_hash
,
token
)
redis_client
.
delete
(
cache_key
)
@
classmethod
def
get_account_if_token_valid
(
cls
,
workspace_id
:
str
,
email
:
str
,
token
:
str
)
->
Optional
[
Account
]:
tenant
=
db
.
session
.
query
(
Tenant
)
.
filter
(
Tenant
.
id
==
workspace_id
,
Tenant
.
status
==
'normal'
)
.
first
()
if
not
tenant
:
return
None
tenant_account
=
db
.
session
.
query
(
Account
,
TenantAccountJoin
.
role
)
.
join
(
TenantAccountJoin
,
Account
.
id
==
TenantAccountJoin
.
account_id
)
.
filter
(
Account
.
email
==
email
,
TenantAccountJoin
.
tenant_id
==
tenant
.
id
)
.
first
()
if
not
tenant_account
:
return
None
account_id
=
cls
.
_get_account_id_by_invite_token
(
workspace_id
,
email
,
token
)
if
not
account_id
:
return
None
account
=
tenant_account
[
0
]
if
not
account
:
return
None
if
account_id
!=
str
(
account
.
id
):
return
None
return
account
@
classmethod
def
_get_account_id_by_invite_token
(
cls
,
workspace_id
:
str
,
email
:
str
,
token
:
str
)
->
Optional
[
str
]:
email_hash
=
sha256
(
email
.
encode
())
.
hexdigest
()
cache_key
=
'member_invite_token:{}, {}:{}'
.
format
(
workspace_id
,
email_hash
,
token
)
account_id
=
redis_client
.
get
(
cache_key
)
if
not
account_id
:
return
None
return
account_id
.
decode
(
'utf-8'
)
api/services/app_model_config_service.py
View file @
9098d099
...
...
@@ -6,6 +6,30 @@ from models.account import Account
from
services.dataset_service
import
DatasetService
from
core.llm.llm_builder
import
LLMBuilder
MODEL_PROVIDERS
=
[
'openai'
,
'anthropic'
,
]
MODELS_BY_APP_MODE
=
{
'chat'
:
[
'claude-instant-1'
,
'claude-2'
,
'gpt-4'
,
'gpt-4-32k'
,
'gpt-3.5-turbo'
,
'gpt-3.5-turbo-16k'
,
],
'completion'
:
[
'claude-instant-1'
,
'claude-2'
,
'gpt-4'
,
'gpt-4-32k'
,
'gpt-3.5-turbo'
,
'gpt-3.5-turbo-16k'
,
'text-davinci-003'
,
]
}
class
AppModelConfigService
:
@
staticmethod
...
...
@@ -125,7 +149,7 @@ class AppModelConfigService:
if
not
isinstance
(
config
[
"speech_to_text"
][
"enabled"
],
bool
):
raise
ValueError
(
"enabled in speech_to_text must be of boolean type"
)
provider_name
=
LLMBuilder
.
get_default_provider
(
account
.
current_tenant_id
)
provider_name
=
LLMBuilder
.
get_default_provider
(
account
.
current_tenant_id
,
'whisper-1'
)
if
config
[
"speech_to_text"
][
"enabled"
]
and
provider_name
!=
'openai'
:
raise
ValueError
(
"provider not support speech to text"
)
...
...
@@ -153,14 +177,14 @@ class AppModelConfigService:
raise
ValueError
(
"model must be of object type"
)
# model.provider
if
'provider'
not
in
config
[
"model"
]
or
config
[
"model"
][
"provider"
]
!=
"openai"
:
raise
ValueError
(
"model.provider must be 'openai'
"
)
if
'provider'
not
in
config
[
"model"
]
or
config
[
"model"
][
"provider"
]
not
in
MODEL_PROVIDERS
:
raise
ValueError
(
f
"model.provider is required and must be in {str(MODEL_PROVIDERS)}
"
)
# model.name
if
'name'
not
in
config
[
"model"
]:
raise
ValueError
(
"model.name is required"
)
if
config
[
"model"
][
"name"
]
not
in
llm_constant
.
models_by_mode
[
mode
]:
if
config
[
"model"
][
"name"
]
not
in
MODELS_BY_APP_MODE
[
mode
]:
raise
ValueError
(
"model.name must be in the specified model list"
)
# model.completion_params
...
...
api/services/audio_service.py
View file @
9098d099
...
...
@@ -27,7 +27,7 @@ class AudioService:
message
=
f
"Audio size larger than {FILE_SIZE} mb"
raise
AudioTooLargeServiceError
(
message
)
provider_name
=
LLMBuilder
.
get_default_provider
(
tenant_id
)
provider_name
=
LLMBuilder
.
get_default_provider
(
tenant_id
,
'whisper-1'
)
if
provider_name
!=
ProviderName
.
OPENAI
.
value
:
raise
ProviderNotSupportSpeechToTextServiceError
()
...
...
@@ -37,8 +37,3 @@ class AudioService:
buffer
.
name
=
'temp.mp3'
return
Whisper
(
provider_service
.
provider
)
.
transcribe
(
buffer
)
\ No newline at end of file
api/services/dataset_service.py
View file @
9098d099
...
...
@@ -4,6 +4,9 @@ import datetime
import
time
import
random
from
typing
import
Optional
,
List
from
flask
import
current_app
from
extensions.ext_redis
import
redis_client
from
flask_login
import
current_user
...
...
@@ -374,6 +377,12 @@ class DocumentService:
def
save_document_with_dataset_id
(
dataset
:
Dataset
,
document_data
:
dict
,
account
:
Account
,
dataset_process_rule
:
Optional
[
DatasetProcessRule
]
=
None
,
created_from
:
str
=
'web'
):
# check document limit
if
current_app
.
config
[
'EDITION'
]
==
'CLOUD'
:
documents_count
=
DocumentService
.
get_tenant_documents_count
()
tenant_document_count
=
int
(
current_app
.
config
[
'TENANT_DOCUMENT_COUNT'
])
if
documents_count
>
tenant_document_count
:
raise
ValueError
(
f
"over document limit {tenant_document_count}."
)
# if dataset is empty, update dataset data_source_type
if
not
dataset
.
data_source_type
:
dataset
.
data_source_type
=
document_data
[
"data_source"
][
"type"
]
...
...
@@ -521,6 +530,14 @@ class DocumentService:
)
return
document
@
staticmethod
def
get_tenant_documents_count
():
documents_count
=
Document
.
query
.
filter
(
Document
.
completed_at
.
isnot
(
None
),
Document
.
enabled
==
True
,
Document
.
archived
==
False
,
Document
.
tenant_id
==
current_user
.
current_tenant_id
)
.
count
()
return
documents_count
@
staticmethod
def
update_document_with_dataset_id
(
dataset
:
Dataset
,
document_data
:
dict
,
account
:
Account
,
dataset_process_rule
:
Optional
[
DatasetProcessRule
]
=
None
,
...
...
@@ -616,6 +633,12 @@ class DocumentService:
@
staticmethod
def
save_document_without_dataset_id
(
tenant_id
:
str
,
document_data
:
dict
,
account
:
Account
):
# check document limit
if
current_app
.
config
[
'EDITION'
]
==
'CLOUD'
:
documents_count
=
DocumentService
.
get_tenant_documents_count
()
tenant_document_count
=
int
(
current_app
.
config
[
'TENANT_DOCUMENT_COUNT'
])
if
documents_count
>
tenant_document_count
:
raise
ValueError
(
f
"over document limit {tenant_document_count}."
)
# save dataset
dataset
=
Dataset
(
tenant_id
=
tenant_id
,
...
...
api/services/hit_testing_service.py
View file @
9098d099
...
...
@@ -31,7 +31,7 @@ class HitTestingService:
model_credentials
=
LLMBuilder
.
get_model_credentials
(
tenant_id
=
dataset
.
tenant_id
,
model_provider
=
LLMBuilder
.
get_default_provider
(
dataset
.
tenant_id
),
model_provider
=
LLMBuilder
.
get_default_provider
(
dataset
.
tenant_id
,
'text-embedding-ada-002'
),
model_name
=
'text-embedding-ada-002'
)
...
...
api/services/provider_service.py
View file @
9098d099
...
...
@@ -10,50 +10,40 @@ from models.provider import *
class
ProviderService
:
@
staticmethod
def
init_supported_provider
(
tenant
,
edition
):
def
init_supported_provider
(
tenant
):
"""Initialize the model provider, check whether the supported provider has a record"""
providers
=
Provider
.
query
.
filter_by
(
tenant_id
=
tenant
.
id
)
.
all
()
need_init_provider_names
=
[
ProviderName
.
OPENAI
.
value
,
ProviderName
.
AZURE_OPENAI
.
value
,
ProviderName
.
ANTHROPIC
.
value
]
openai_provider_exists
=
False
azure_openai_provider_exists
=
False
# TODO: The cloud version needs to construct the data of the SYSTEM type
providers
=
db
.
session
.
query
(
Provider
)
.
filter
(
Provider
.
tenant_id
==
tenant
.
id
,
Provider
.
provider_type
==
ProviderType
.
CUSTOM
.
value
,
Provider
.
provider_name
.
in_
(
need_init_provider_names
)
)
.
all
()
exists_provider_names
=
[]
for
provider
in
providers
:
if
provider
.
provider_name
==
ProviderName
.
OPENAI
.
value
and
provider
.
provider_type
==
ProviderType
.
CUSTOM
.
value
:
openai_provider_exists
=
True
if
provider
.
provider_name
==
ProviderName
.
AZURE_OPENAI
.
value
and
provider
.
provider_type
==
ProviderType
.
CUSTOM
.
value
:
azure_openai_provider_exists
=
True
# Initialize the model provider, check whether the supported provider has a record
exists_provider_names
.
append
(
provider
.
provider_name
)
# Create default providers if they don't exist
if
not
openai_provider_exists
:
openai_provider
=
Provider
(
tenant_id
=
tenant
.
id
,
provider_name
=
ProviderName
.
OPENAI
.
value
,
provider_type
=
ProviderType
.
CUSTOM
.
value
,
is_valid
=
False
)
db
.
session
.
add
(
openai_provider
)
not_exists_provider_names
=
list
(
set
(
need_init_provider_names
)
-
set
(
exists_provider_names
))
if
not
azure_openai_provider_exists
:
azure_openai_provider
=
Provider
(
if
not_exists_provider_names
:
# Initialize the model provider, check whether the supported provider has a record
for
provider_name
in
not_exists_provider_names
:
provider
=
Provider
(
tenant_id
=
tenant
.
id
,
provider_name
=
ProviderName
.
AZURE_OPENAI
.
valu
e
,
provider_name
=
provider_nam
e
,
provider_type
=
ProviderType
.
CUSTOM
.
value
,
is_valid
=
False
)
db
.
session
.
add
(
azure_openai_
provider
)
db
.
session
.
add
(
provider
)
if
not
openai_provider_exists
or
not
azure_openai_provider_exists
:
db
.
session
.
commit
()
@
staticmethod
def
get_obfuscated_api_key
(
tenant
,
provider_name
:
ProviderName
):
def
get_obfuscated_api_key
(
tenant
,
provider_name
:
ProviderName
,
only_custom
:
bool
=
False
):
llm_provider_service
=
LLMProviderService
(
tenant
.
id
,
provider_name
.
value
)
return
llm_provider_service
.
get_provider_configs
(
obfuscated
=
True
)
return
llm_provider_service
.
get_provider_configs
(
obfuscated
=
True
,
only_custom
=
only_custom
)
@
staticmethod
def
get_token_type
(
tenant
,
provider_name
:
ProviderName
):
...
...
@@ -73,7 +63,7 @@ class ProviderService:
return
llm_provider_service
.
get_encrypted_token
(
configs
)
@
staticmethod
def
create_system_provider
(
tenant
:
Tenant
,
provider_name
:
str
=
ProviderName
.
OPENAI
.
value
,
def
create_system_provider
(
tenant
:
Tenant
,
provider_name
:
str
=
ProviderName
.
OPENAI
.
value
,
quota_limit
:
int
=
200
,
is_valid
:
bool
=
True
):
if
current_app
.
config
[
'EDITION'
]
!=
'CLOUD'
:
return
...
...
@@ -90,7 +80,7 @@ class ProviderService:
provider_name
=
provider_name
,
provider_type
=
ProviderType
.
SYSTEM
.
value
,
quota_type
=
ProviderQuotaType
.
TRIAL
.
value
,
quota_limit
=
200
,
quota_limit
=
quota_limit
,
encrypted_config
=
''
,
is_valid
=
is_valid
,
)
...
...
api/services/workspace_service.py
View file @
9098d099
from
extensions.ext_database
import
db
from
models.account
import
Tenant
from
models.provider
import
Provider
,
ProviderType
from
models.provider
import
Provider
,
ProviderType
,
ProviderName
class
WorkspaceService
:
...
...
@@ -33,7 +33,7 @@ class WorkspaceService:
if
provider
.
is_valid
and
provider
.
encrypted_config
:
custom_provider
=
provider
elif
provider
.
provider_type
==
ProviderType
.
SYSTEM
.
value
:
if
provider
.
is_valid
:
if
provider
.
provider_name
==
ProviderName
.
OPENAI
.
value
and
provider
.
is_valid
:
system_provider
=
provider
if
system_provider
and
not
custom_provider
:
...
...
api/tasks/mail_invite_member_task.py
0 → 100644
View file @
9098d099
import
logging
import
time
import
click
from
celery
import
shared_task
from
flask
import
current_app
from
extensions.ext_mail
import
mail
@
shared_task
def
send_invite_member_mail_task
(
to
:
str
,
token
:
str
,
inviter_name
:
str
,
workspace_id
:
str
,
workspace_name
:
str
):
"""
Async Send invite member mail
:param to
:param token
:param inviter_name
:param workspace_id
:param workspace_name
Usage: send_invite_member_mail_task.delay(to, token, inviter_name, workspace_id, workspace_name)
"""
if
not
mail
.
is_inited
():
return
logging
.
info
(
click
.
style
(
'Start send invite member mail to {} in workspace {}'
.
format
(
to
,
workspace_name
),
fg
=
'green'
))
start_at
=
time
.
perf_counter
()
try
:
mail
.
send
(
to
=
to
,
subject
=
"{} invited you to join {}"
.
format
(
inviter_name
,
workspace_name
),
html
=
"""<p>Hi there,</p>
<p>{inviter_name} invited you to join {workspace_name}.</p>
<p>Click <a href="{url}">here</a> to join.</p>
<p>Thanks,</p>
<p>Dify Team</p>"""
.
format
(
inviter_name
=
inviter_name
,
workspace_name
=
workspace_name
,
url
=
'{}/activate?workspace_id={}&email={}&token={}'
.
format
(
current_app
.
config
.
get
(
"CONSOLE_WEB_URL"
),
workspace_id
,
to
,
token
)
)
)
end_at
=
time
.
perf_counter
()
logging
.
info
(
click
.
style
(
'Send invite member mail to {} succeeded: latency: {}'
.
format
(
to
,
end_at
-
start_at
),
fg
=
'green'
))
except
Exception
:
logging
.
exception
(
"Send invite member mail to {} failed"
.
format
(
to
))
docker/docker-compose.yaml
View file @
9098d099
...
...
@@ -2,7 +2,7 @@ version: '3.1'
services
:
# API service
api
:
image
:
langgenius/dify-api:0.3.
7
image
:
langgenius/dify-api:0.3.
9
restart
:
always
environment
:
# Startup mode, 'api' starts the API server.
...
...
@@ -11,18 +11,26 @@ services:
LOG_LEVEL
:
INFO
# A secret key that is used for securely signing the session cookie and encrypting sensitive information on the database. You can generate a strong key using `openssl rand -base64 42`.
SECRET_KEY
:
sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U
# The base URL of console application, refers to the Console base URL of WEB service if console domain is
# The base URL of console application
web frontend
, refers to the Console base URL of WEB service if console domain is
# different from api or web app domain.
# example: http://cloud.dify.ai
CONSOLE_URL
:
'
'
CONSOLE_WEB_URL
:
'
'
# The base URL of console application api server, refers to the Console base URL of WEB service if console domain is
# different from api or web app domain.
# example: http://cloud.dify.ai
CONSOLE_API_URL
:
'
'
# The URL for Service API endpoints,refers to the base URL of the current API service if api domain is
# different from console domain.
# example: http://api.dify.ai
API_URL
:
'
'
# The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from
SERVICE_API_URL
:
'
'
# The URL for Web APP api server, refers to the Web App base URL of WEB service if web app domain is different from
# console or api domain.
# example: http://udify.app
APP_API_URL
:
'
'
# The URL for Web APP frontend, refers to the Web App base URL of WEB service if web app domain is different from
# console or api domain.
# example: http://udify.app
APP_URL
:
'
'
APP_
WEB_
URL
:
'
'
# When enabled, migrations will be executed prior to application startup and the application will start after the migrations have completed.
MIGRATION_ENABLED
:
'
true'
# The configurations of postgres database connection.
...
...
@@ -93,6 +101,12 @@ services:
QDRANT_URL
:
'
https://your-qdrant-cluster-url.qdrant.tech/'
# The Qdrant API key.
QDRANT_API_KEY
:
'
ak-difyai'
# Mail configuration, support: resend
MAIL_TYPE
:
'
'
# default send from email address, if not specified
MAIL_DEFAULT_SEND_FROM
:
'
YOUR
EMAIL
FROM
(eg:
no-reply
<no-reply@dify.ai>)'
# the api-key for resend (https://resend.com)
RESEND_API_KEY
:
'
'
# The DSN for Sentry error reporting. If not set, Sentry error reporting will be disabled.
SENTRY_DSN
:
'
'
# The sample rate for Sentry events. Default: `1.0`
...
...
@@ -110,7 +124,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker
:
image
:
langgenius/dify-api:0.3.
7
image
:
langgenius/dify-api:0.3.
9
restart
:
always
environment
:
# Startup mode, 'worker' starts the Celery worker for processing the queue.
...
...
@@ -146,6 +160,12 @@ services:
VECTOR_STORE
:
weaviate
WEAVIATE_ENDPOINT
:
http://weaviate:8080
WEAVIATE_API_KEY
:
WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih
# Mail configuration, support: resend
MAIL_TYPE
:
'
'
# default send from email address, if not specified
MAIL_DEFAULT_SEND_FROM
:
'
YOUR
EMAIL
FROM
(eg:
no-reply
<no-reply@dify.ai>)'
# the api-key for resend (https://resend.com)
RESEND_API_KEY
:
'
'
depends_on
:
-
db
-
redis
...
...
@@ -156,18 +176,18 @@ services:
# Frontend web application.
web
:
image
:
langgenius/dify-web:0.3.
7
image
:
langgenius/dify-web:0.3.
9
restart
:
always
environment
:
EDITION
:
SELF_HOSTED
# The base URL of console application, refers to the Console base URL of WEB service if console domain is
# The base URL of console application
api server
, refers to the Console base URL of WEB service if console domain is
# different from api or web app domain.
# example: http://cloud.dify.ai
CONSOLE_URL
:
'
'
# The URL for Web APP, refers to the Web App base URL of WEB service if web app domain is different from
CONSOLE_
API_
URL
:
'
'
# The URL for Web APP
api server
, refers to the Web App base URL of WEB service if web app domain is different from
# console or api domain.
# example: http://udify.app
APP_URL
:
'
'
APP_
API_
URL
:
'
'
# The DSN for Sentry error reporting. If not set, Sentry error reporting will be disabled.
SENTRY_DSN
:
'
'
...
...
web/Dockerfile
View file @
9098d099
...
...
@@ -4,8 +4,8 @@ LABEL maintainer="takatost@gmail.com"
ENV
EDITION SELF_HOSTED
ENV
DEPLOY_ENV PRODUCTION
ENV
CONSOLE_URL http://127.0.0.1:5001
ENV
APP_URL http://127.0.0.1:5001
ENV
CONSOLE_
API_
URL http://127.0.0.1:5001
ENV
APP_
API_
URL http://127.0.0.1:5001
EXPOSE
3000
...
...
web/app/activate/activateForm.tsx
0 → 100644
View file @
9098d099
'use client'
import
{
useState
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
useSWR
from
'swr'
import
{
useSearchParams
}
from
'next/navigation'
import
cn
from
'classnames'
import
Link
from
'next/link'
import
{
CheckCircleIcon
}
from
'@heroicons/react/24/solid'
import
style
from
'./style.module.css'
import
Button
from
'@/app/components/base/button'
import
{
SimpleSelect
}
from
'@/app/components/base/select'
import
{
timezones
}
from
'@/utils/timezone'
import
{
languageMaps
,
languages
}
from
'@/utils/language'
import
{
activateMember
,
invitationCheck
}
from
'@/service/common'
import
Toast
from
'@/app/components/base/toast'
import
Loading
from
'@/app/components/base/loading'
const
validPassword
=
/^
(?=
.*
[
a-zA-Z
])(?=
.*
\d)
.
{8,}
$/
const
ActivateForm
=
()
=>
{
const
{
t
}
=
useTranslation
()
const
searchParams
=
useSearchParams
()
const
workspaceID
=
searchParams
.
get
(
'workspace_id'
)
const
email
=
searchParams
.
get
(
'email'
)
const
token
=
searchParams
.
get
(
'token'
)
const
checkParams
=
{
url
:
'/activate/check'
,
params
:
{
workspace_id
:
workspaceID
,
email
,
token
,
},
}
const
{
data
:
checkRes
,
mutate
:
recheck
}
=
useSWR
(
checkParams
,
invitationCheck
,
{
revalidateOnFocus
:
false
,
})
const
[
name
,
setName
]
=
useState
(
''
)
const
[
password
,
setPassword
]
=
useState
(
''
)
const
[
timezone
,
setTimezone
]
=
useState
(
'Asia/Shanghai'
)
const
[
language
,
setLanguage
]
=
useState
(
'en-US'
)
const
[
showSuccess
,
setShowSuccess
]
=
useState
(
false
)
const
showErrorMessage
=
(
message
:
string
)
=>
{
Toast
.
notify
({
type
:
'error'
,
message
,
})
}
const
valid
=
()
=>
{
if
(
!
name
.
trim
())
{
showErrorMessage
(
t
(
'login.error.nameEmpty'
))
return
false
}
if
(
!
password
.
trim
())
{
showErrorMessage
(
t
(
'login.error.passwordEmpty'
))
return
false
}
if
(
!
validPassword
.
test
(
password
))
showErrorMessage
(
t
(
'login.error.passwordInvalid'
))
return
true
}
const
handleActivate
=
async
()
=>
{
if
(
!
valid
())
return
try
{
await
activateMember
({
url
:
'/activate'
,
body
:
{
workspace_id
:
workspaceID
,
email
,
token
,
name
,
password
,
interface_language
:
language
,
timezone
,
},
})
setShowSuccess
(
true
)
}
catch
{
recheck
()
}
}
return
(
<
div
className=
{
cn
(
'flex flex-col items-center w-full grow items-center justify-center'
,
'px-6'
,
'md:px-[108px]'
,
)
}
>
{
!
checkRes
&&
<
Loading
/>
}
{
checkRes
&&
!
checkRes
.
is_valid
&&
(
<
div
className=
"flex flex-col md:w-[400px]"
>
<
div
className=
"w-full mx-auto"
>
<
div
className=
"mb-3 flex justify-center items-center w-20 h-20 p-5 rounded-[20px] border border-gray-100 shadow-lg text-[40px] font-bold"
>
🤷♂️
</
div
>
<
h2
className=
"text-[32px] font-bold text-gray-900"
>
{
t
(
'login.invalid'
)
}
</
h2
>
</
div
>
<
div
className=
"w-full mx-auto mt-6"
>
<
Button
type=
'primary'
className=
'w-full !fone-medium !text-sm'
>
<
a
href=
"https://dify.ai"
>
{
t
(
'login.explore'
)
}
</
a
>
</
Button
>
</
div
>
</
div
>
)
}
{
checkRes
&&
checkRes
.
is_valid
&&
!
showSuccess
&&
(
<
div
className=
'flex flex-col md:w-[400px]'
>
<
div
className=
"w-full mx-auto"
>
<
div
className=
{
`mb-3 flex justify-center items-center w-20 h-20 p-5 rounded-[20px] border border-gray-100 shadow-lg text-[40px] font-bold ${style.logo}`
}
>
</
div
>
<
h2
className=
"text-[32px] font-bold text-gray-900"
>
{
`${t('login.join')} ${checkRes.workspace_name}`
}
</
h2
>
<
p
className=
'mt-1 text-sm text-gray-600 '
>
{
`${t('login.joinTipStart')} ${checkRes.workspace_name} ${t('login.joinTipEnd')}`
}
</
p
>
</
div
>
<
div
className=
"w-full mx-auto mt-6"
>
<
div
className=
"bg-white"
>
{
/* username */
}
<
div
className=
'mb-5'
>
<
label
htmlFor=
"name"
className=
"my-2 flex items-center justify-between text-sm font-medium text-gray-900"
>
{
t
(
'login.name'
)
}
</
label
>
<
div
className=
"mt-1 relative rounded-md shadow-sm"
>
<
input
id=
"name"
type=
"text"
value=
{
name
}
onChange=
{
e
=>
setName
(
e
.
target
.
value
)
}
placeholder=
{
t
(
'login.namePlaceholder'
)
||
''
}
className=
{
'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'
}
/>
</
div
>
</
div
>
{
/* password */
}
<
div
className=
'mb-5'
>
<
label
htmlFor=
"password"
className=
"my-2 flex items-center justify-between text-sm font-medium text-gray-900"
>
{
t
(
'login.password'
)
}
</
label
>
<
div
className=
"mt-1 relative rounded-md shadow-sm"
>
<
input
id=
"password"
type=
'password'
value=
{
password
}
onChange=
{
e
=>
setPassword
(
e
.
target
.
value
)
}
placeholder=
{
t
(
'login.passwordPlaceholder'
)
||
''
}
className=
{
'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'
}
/>
</
div
>
<
div
className=
'mt-1 text-xs text-gray-500'
>
{
t
(
'login.error.passwordInvalid'
)
}
</
div
>
</
div
>
{
/* language */
}
<
div
className=
'mb-5'
>
<
label
htmlFor=
"name"
className=
"my-2 flex items-center justify-between text-sm font-medium text-gray-900"
>
{
t
(
'login.interfaceLanguage'
)
}
</
label
>
<
div
className=
"relative mt-1 rounded-md shadow-sm"
>
<
SimpleSelect
defaultValue=
{
languageMaps
.
en
}
items=
{
languages
}
onSelect=
{
(
item
)
=>
{
setLanguage
(
item
.
value
as
string
)
}
}
/>
</
div
>
</
div
>
{
/* timezone */
}
<
div
className=
'mb-4'
>
<
label
htmlFor=
"timezone"
className=
"block text-sm font-medium text-gray-700"
>
{
t
(
'login.timezone'
)
}
</
label
>
<
div
className=
"relative mt-1 rounded-md shadow-sm"
>
<
SimpleSelect
defaultValue=
{
timezone
}
items=
{
timezones
}
onSelect=
{
(
item
)
=>
{
setTimezone
(
item
.
value
as
string
)
}
}
/>
</
div
>
</
div
>
<
div
>
<
Button
type=
'primary'
className=
'w-full !fone-medium !text-sm'
onClick=
{
handleActivate
}
>
{
`${t('login.join')} ${checkRes.workspace_name}`
}
</
Button
>
</
div
>
<
div
className=
"block w-hull mt-2 text-xs text-gray-600"
>
{
t
(
'login.license.tip'
)
}
<
Link
className=
'text-primary-600'
target=
{
'_blank'
}
href=
'https://docs.dify.ai/community/open-source'
>
{
t
(
'login.license.link'
)
}
</
Link
>
</
div
>
</
div
>
</
div
>
</
div
>
)
}
{
checkRes
&&
checkRes
.
is_valid
&&
showSuccess
&&
(
<
div
className=
"flex flex-col md:w-[400px]"
>
<
div
className=
"w-full mx-auto"
>
<
div
className=
"mb-3 flex justify-center items-center w-20 h-20 p-5 rounded-[20px] border border-gray-100 shadow-lg text-[40px] font-bold"
>
<
CheckCircleIcon
className=
'w-10 h-10 text-[#039855]'
/>
</
div
>
<
h2
className=
"text-[32px] font-bold text-gray-900"
>
{
`${t('login.activatedTipStart')} ${checkRes.workspace_name} ${t('login.activatedTipEnd')}`
}
</
h2
>
</
div
>
<
div
className=
"w-full mx-auto mt-6"
>
<
Button
type=
'primary'
className=
'w-full !fone-medium !text-sm'
>
<
a
href=
"/signin"
>
{
t
(
'login.activated'
)
}
</
a
>
</
Button
>
</
div
>
</
div
>
)
}
</
div
>
)
}
export
default
ActivateForm
web/app/activate/page.tsx
0 → 100644
View file @
9098d099
import
React
from
'react'
import
cn
from
'classnames'
import
Header
from
'../signin/_header'
import
style
from
'../signin/page.module.css'
import
ActivateForm
from
'./activateForm'
const
Activate
=
()
=>
{
return
(
<
div
className=
{
cn
(
style
.
background
,
'flex w-full min-h-screen'
,
'sm:p-4 lg:p-8'
,
'gap-x-20'
,
'justify-center lg:justify-start'
,
)
}
>
<
div
className=
{
cn
(
'flex w-full flex-col bg-white shadow rounded-2xl shrink-0'
,
'space-between'
,
)
}
>
<
Header
/>
<
ActivateForm
/>
<
div
className=
'px-8 py-6 text-sm font-normal text-gray-500'
>
©
{
new
Date
().
getFullYear
()
}
Dify, Inc. All rights reserved.
</
div
>
</
div
>
</
div
>
)
}
export
default
Activate
web/app/activate/style.module.css
0 → 100644
View file @
9098d099
.logo
{
background
:
#fff
center
no-repeat
url(./team-28x28.png)
;
background-size
:
56px
;
}
\ No newline at end of file
web/app/activate/team-28x28.png
0 → 100644
View file @
9098d099
7.18 KB
web/app/components/app/chat/index.tsx
View file @
9098d099
...
...
@@ -65,6 +65,7 @@ export type IChatProps = {
isShowSuggestion
?:
boolean
suggestionList
?:
string
[]
isShowSpeechToText
?:
boolean
answerIconClassName
?:
string
}
export
type
MessageMore
=
{
...
...
@@ -174,10 +175,11 @@ type IAnswerProps = {
onSubmitAnnotation
?:
SubmitAnnotationFunc
displayScene
:
DisplayScene
isResponsing
?:
boolean
answerIconClassName
?:
string
}
// The component needs to maintain its own state to control whether to display input component
const
Answer
:
FC
<
IAnswerProps
>
=
({
item
,
feedbackDisabled
=
false
,
isHideFeedbackEdit
=
false
,
onFeedback
,
onSubmitAnnotation
,
displayScene
=
'web'
,
isResponsing
})
=>
{
const
Answer
:
FC
<
IAnswerProps
>
=
({
item
,
feedbackDisabled
=
false
,
isHideFeedbackEdit
=
false
,
onFeedback
,
onSubmitAnnotation
,
displayScene
=
'web'
,
isResponsing
,
answerIconClassName
})
=>
{
const
{
id
,
content
,
more
,
feedback
,
adminFeedback
,
annotation
:
initAnnotation
}
=
item
const
[
showEdit
,
setShowEdit
]
=
useState
(
false
)
const
[
loading
,
setLoading
]
=
useState
(
false
)
...
...
@@ -292,7 +294,7 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedba
return
(
<
div
key=
{
id
}
>
<
div
className=
'flex items-start'
>
<
div
className=
{
`${s.answerIcon} w-10 h-10 shrink-0`
}
>
<
div
className=
{
`${s.answerIcon}
${answerIconClassName}
w-10 h-10 shrink-0`
}
>
{
isResponsing
&&
<
div
className=
{
s
.
typeingIcon
}
>
<
LoadingAnim
type=
'avatar'
/>
...
...
@@ -428,6 +430,7 @@ const Chat: FC<IChatProps> = ({
isShowSuggestion
,
suggestionList
,
isShowSpeechToText
,
answerIconClassName
,
})
=>
{
const
{
t
}
=
useTranslation
()
const
{
notify
}
=
useContext
(
ToastContext
)
...
...
@@ -520,6 +523,7 @@ const Chat: FC<IChatProps> = ({
onSubmitAnnotation=
{
onSubmitAnnotation
}
displayScene=
{
displayScene
??
'web'
}
isResponsing=
{
isResponsing
&&
isLast
}
answerIconClassName=
{
answerIconClassName
}
/>
}
return
<
Question
key=
{
item
.
id
}
id=
{
item
.
id
}
content=
{
item
.
content
}
more=
{
item
.
more
}
useCurrentUserAvatar=
{
useCurrentUserAvatar
}
/>
...
...
web/app/components/app/configuration/config-model/index.tsx
View file @
9098d099
...
...
@@ -9,15 +9,16 @@ import ParamItem from './param-item'
import
Radio
from
'@/app/components/base/radio'
import
Panel
from
'@/app/components/base/panel'
import
type
{
CompletionParams
}
from
'@/models/debug'
import
{
AppType
}
from
'@/types/app'
import
{
AppType
,
ProviderType
}
from
'@/types/app'
import
{
TONE_LIST
}
from
'@/config'
import
Toast
from
'@/app/components/base/toast'
import
{
AlertTriangle
}
from
'@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import
{
formatNumber
}
from
'@/utils/format'
export
type
IConifgModelProps
=
{
mode
:
string
modelId
:
string
setModelId
:
(
id
:
string
)
=>
void
setModelId
:
(
id
:
string
,
provider
:
ProviderType
)
=>
void
completionParams
:
CompletionParams
onCompletionParamsChange
:
(
newParams
:
CompletionParams
)
=>
void
disabled
:
boolean
...
...
@@ -29,18 +30,49 @@ const options = [
{
id
:
'gpt-3.5-turbo'
,
name
:
'gpt-3.5-turbo'
,
type
:
AppType
.
chat
},
{
id
:
'gpt-3.5-turbo-16k'
,
name
:
'gpt-3.5-turbo-16k'
,
type
:
AppType
.
chat
},
{
id
:
'gpt-4'
,
name
:
'gpt-4'
,
type
:
AppType
.
chat
},
// 8k version
{
id
:
'claude-instant-1'
,
name
:
'claude-instant-1'
,
type
:
AppType
.
chat
,
provider
:
ProviderType
.
anthropic
},
// set 30k
{
id
:
'claude-2'
,
name
:
'claude-2'
,
type
:
AppType
.
chat
,
provider
:
ProviderType
.
anthropic
},
// set 30k
{
id
:
'gpt-3.5-turbo'
,
name
:
'gpt-3.5-turbo'
,
type
:
AppType
.
completion
},
{
id
:
'gpt-3.5-turbo-16k'
,
name
:
'gpt-3.5-turbo-16k'
,
type
:
AppType
.
completion
},
{
id
:
'text-davinci-003'
,
name
:
'text-davinci-003'
,
type
:
AppType
.
completion
},
{
id
:
'gpt-4'
,
name
:
'gpt-4'
,
type
:
AppType
.
completion
},
// 8k version
{
id
:
'claude-instant-1'
,
name
:
'claude-instant-1'
,
type
:
AppType
.
completion
,
provider
:
ProviderType
.
anthropic
},
// set 30k
{
id
:
'claude-2'
,
name
:
'claude-2'
,
type
:
AppType
.
completion
,
provider
:
ProviderType
.
anthropic
},
// set 30k
]
const
ModelIcon
=
({
className
}:
{
className
?:
string
})
=>
(
const
getMaxToken
=
(
modelId
:
string
)
=>
{
if
([
'claude-instant-1'
,
'claude-2'
].
includes
(
modelId
))
return
30
*
1000
if
([
'gpt-4'
,
'gpt-3.5-turbo-16k'
].
includes
(
modelId
))
return
8000
return
4000
}
const
ModelIcon
=
({
provider
,
className
}:
{
provider
?:
ProviderType
;
className
?:
string
})
=>
{
if
(
provider
===
ProviderType
.
anthropic
)
{
return
(
<
svg
className=
{
`w-4 h-4 ${className}`
}
width=
"24"
height=
"24"
viewBox=
"0 0 24 24"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<
rect
width=
"24"
height=
"24"
rx=
"6"
fill=
"#CA9F7B"
/>
<
g
clip
-
path=
"url(#clip0_3907_39360)"
>
<
path
d=
"M14.9613 7.13043H12.8476L16.7022 16.8696H18.8159L14.9613 7.13043ZM8.85457 7.13043L5 16.8696H7.15539L7.94365 14.8243H11.9763L12.7645 16.8696H14.9199L11.0653 7.13043H8.85457ZM8.64091 13.0156L9.95996 9.59291L11.279 13.0156H8.64091Z"
fill=
"#191918"
/>
</
g
>
<
defs
>
<
clipPath
id=
"clip0_3907_39360"
>
<
rect
width=
"14"
height=
"9.73913"
fill=
"white"
transform=
"translate(5 7.13043)"
/>
</
clipPath
>
</
defs
>
</
svg
>
)
}
return
(
<
svg
className=
{
`w-4 h-4 ${className}`
}
width=
"20"
height=
"20"
viewBox=
"0 0 20 20"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<
rect
width=
"20"
height=
"20"
rx=
"6"
fill=
"black"
/>
<
path
d=
"M16.5963 9.65729C16.748 9.99569 16.8443 10.3574 16.8836 10.7265C16.9216 11.0955 16.9026 11.4689 16.8238 11.8321C16.7465 12.1953 16.6123 12.5439 16.4256 12.8648C16.3031 13.0793 16.1587 13.2805 15.9924 13.4658C15.8276 13.6496 15.6438 13.8159 15.444 13.9617C15.2427 14.1076 15.0283 14.2301 14.8007 14.3308C14.5746 14.4299 14.3383 14.5058 14.0962 14.5554C13.9824 14.9084 13.8132 15.2424 13.5944 15.5429C13.3771 15.8434 13.1131 16.1074 12.8126 16.3247C12.5121 16.5435 12.1795 16.7127 11.8266 16.8265C11.4736 16.9417 11.1045 16.9986 10.7326 16.9986C10.4861 17.0001 10.2381 16.9738 9.99596 16.9242C9.75529 16.8732 9.51899 16.7959 9.2929 16.6952C9.06681 16.5946 8.85239 16.4691 8.65256 16.3233C8.45418 16.1774 8.2704 16.0097 8.10703 15.8244C7.74237 15.9032 7.36896 15.9221 6.99992 15.8842C6.63089 15.8448 6.26914 15.7486 5.92928 15.5969C5.59088 15.4466 5.27727 15.2424 5.00159 14.993C4.72591 14.7436 4.49107 14.4518 4.30582 14.1309C4.18184 13.9165 4.07973 13.6904 4.00242 13.4556C3.92511 13.2207 3.87406 12.9786 3.84781 12.7321C3.82155 12.487 3.82301 12.2391 3.84927 11.9926C3.87552 11.7475 3.92949 11.5054 4.0068 11.2705C3.75883 10.9949 3.55462 10.6813 3.40292 10.3428C3.25268 10.003 3.15495 9.6427 3.11703 9.27367C3.07765 8.90463 3.09807 8.53122 3.17538 8.16802C3.25268 7.80482 3.38688 7.4562 3.57358 7.1353C3.69611 6.92088 3.84051 6.71813 4.00534 6.53434C4.17017 6.35056 4.35541 6.18427 4.55525 6.03841C4.75508 5.89254 4.97096 5.76856 5.19705 5.66937C5.42459 5.56873 5.66089 5.49434 5.90303 5.44474C6.0168 5.09029 6.186 4.75772 6.40334 4.45725C6.62213 4.15677 6.88615 3.89275 7.18663 3.67396C7.48711 3.45662 7.81968 3.28742 8.17267 3.17219C8.52566 3.05841 8.89469 3.00007 9.26664 3.00153C9.51315 3.00007 9.76112 3.02486 10.0033 3.07592C10.2454 3.12697 10.4817 3.20282 10.7078 3.30346C10.9339 3.40557 11.1483 3.52955 11.3481 3.67542C11.548 3.82274 11.7317 3.98902 11.8951 4.17427C12.2583 4.09696 12.6317 4.078 13.0008 4.11592C13.3698 4.15385 13.7301 4.25158 14.0699 4.40182C14.4083 4.55352 14.7219 4.75627 14.9976 5.00569C15.2733 5.25366 15.5082 5.54393 15.6934 5.86629C15.8174 6.07925 15.9195 6.30534 15.9968 6.54164C16.0741 6.77648 16.1266 7.01861 16.1514 7.26512C16.1777 7.51163 16.1777 7.7596 16.15 8.00611C16.1237 8.25262 16.0697 8.49475 15.9924 8.72959C16.2418 9.00528 16.4446 9.31742 16.5963 9.65729ZM11.7361 15.8842C12.0541 15.7529 12.3429 15.5589 12.5865 15.3153C12.8301 15.0717 13.0241 14.7829 13.1554 14.4635C13.2866 14.1455 13.3552 13.8042 13.3552 13.46V10.2072C13.3542 10.2043 13.3533 10.2009 13.3523 10.197C13.3513 10.1941 13.3499 10.1911 13.3479 10.1882C13.346 10.1853 13.3435 10.1829 13.3406 10.1809C13.3377 10.178 13.3348 10.1761 13.3319 10.1751L12.1547 9.49538V13.4249C12.1547 13.4643 12.1489 13.5052 12.1387 13.5431C12.1285 13.5825 12.1139 13.6189 12.0935 13.654C12.0731 13.689 12.0497 13.7211 12.0206 13.7488C11.9922 13.777 11.9603 13.8015 11.9257 13.8217L9.13828 15.4306C9.11495 15.4452 9.07556 15.4656 9.05514 15.4772C9.17037 15.575 9.29582 15.661 9.42709 15.7369C9.55983 15.8127 9.69694 15.8769 9.83989 15.9294C9.98284 15.9805 10.1302 16.0199 10.2789 16.0461C10.4292 16.0724 10.5809 16.0855 10.7326 16.0855C11.0768 16.0855 11.4181 16.0169 11.7361 15.8842ZM5.09786 13.6758C5.27144 13.9749 5.50044 14.2345 5.77321 14.4445C6.04743 14.6546 6.35812 14.8077 6.69069 14.8967C7.02326 14.9857 7.37042 15.009 7.71174 14.9638C8.05306 14.9186 8.38125 14.8077 8.68027 14.6356L11.4984 13.0092L11.5057 13.0019C11.5076 13 11.5091 12.9971 11.51 12.9932C11.512 12.9903 11.5134 12.9874 11.5144 12.9844V11.6133L8.11286 13.581C8.07786 13.6014 8.04139 13.616 8.00346 13.6277C7.96408 13.6379 7.9247 13.6423 7.88386 13.6423C7.84447 13.6423 7.80509 13.6379 7.76571 13.6277C7.72778 13.616 7.68986 13.6014 7.65485 13.581L4.86739 11.9707C4.8426 11.9561 4.80613 11.9342 4.78571 11.9211C4.75946 12.0713 4.74633 12.223 4.74633 12.3747C4.74633 12.5264 4.76091 12.6781 4.78717 12.8284C4.81342 12.9771 4.85427 13.1245 4.90532 13.2674C4.95783 13.4104 5.02201 13.5475 5.09786 13.6788V13.6758ZM4.36562 7.59332C4.1935 7.89234 4.08265 8.22199 4.03743 8.56331C3.99221 8.90463 4.01555 9.25033 4.10453 9.58436C4.1935 9.91692 4.34666 10.2276 4.5567 10.5018C4.76675 10.7746 5.02784 11.0036 5.32541 11.1757L8.14204 12.8036C8.14495 12.8045 8.14836 12.8055 8.15225 12.8065H8.16246C8.16635 12.8065 8.16975 12.8055 8.17267 12.8036C8.17558 12.8026 8.1785 12.8011 8.18142 12.7992L9.36291 12.1165L5.96137 10.1532C5.92782 10.1328 5.89573 10.108 5.86656 10.0803C5.8383 10.0519 5.81379 10.0201 5.79363 9.98548C5.77467 9.95047 5.75862 9.91401 5.74841 9.87462C5.7382 9.8367 5.73237 9.79732 5.73383 9.75647V6.44391C5.59088 6.49642 5.45231 6.5606 5.32103 6.63645C5.18975 6.71376 5.06577 6.80128 4.94908 6.899C4.83385 6.99673 4.72591 7.10467 4.62818 7.22136C4.53045 7.3366 4.44439 7.46204 4.36854 7.59332H4.36562ZM14.0408 9.84545C14.0758 9.86587 14.1079 9.88921 14.137 9.91838C14.1647 9.9461 14.1895 9.97819 14.21 10.0132C14.2289 10.0482 14.245 10.0861 14.2552 10.1241C14.2639 10.1634 14.2698 10.2028 14.2683 10.2437V13.5562C14.7365 13.3841 15.145 13.0822 15.4469 12.6854C15.7503 12.2887 15.9326 11.8146 15.9749 11.3187C16.0172 10.8227 15.918 10.3239 15.6876 9.88192C15.4571 9.43995 15.1056 9.07237 14.6738 8.82441L11.8572 7.19657C11.8543 7.19559 11.8509 7.19462 11.847 7.19365H11.8368C11.8338 7.19462 11.8304 7.19559 11.8266 7.19657C11.8236 7.19754 11.8207 7.199 11.8178 7.20094L10.6421 7.88067L14.0437 9.84545H14.0408ZM15.215 8.0805H15.2135V8.08196L15.215 8.0805ZM15.2135 8.07904C15.2981 7.58894 15.2412 7.08425 15.0487 6.62478C14.8576 6.16531 14.5382 5.77002 14.1297 5.48413C13.7213 5.19969 13.24 5.03632 12.7426 5.01445C12.2437 4.99402 11.7507 5.11509 11.3189 5.36306L8.50232 6.98944C8.4994 6.99138 8.49697 6.99382 8.49503 6.99673L8.48919 7.00549C8.48822 7.0084 8.48725 7.01181 8.48627 7.0157C8.4853 7.01861 8.48482 7.02202 8.48482 7.02591V8.38536L11.8864 6.42057C11.9214 6.40015 11.9593 6.38556 11.9972 6.37389C12.0366 6.36368 12.076 6.35931 12.1154 6.35931C12.1562 6.35931 12.1956 6.36368 12.235 6.37389C12.2729 6.38556 12.3094 6.40015 12.3444 6.42057L15.1318 8.03091C15.1566 8.04549 15.1931 8.06591 15.2135 8.07904ZM7.84301 6.57373C7.84301 6.53434 7.84885 6.49496 7.85906 6.45558C7.86927 6.41765 7.88386 6.37973 7.90428 6.34472C7.9247 6.31117 7.94804 6.27908 7.97721 6.24991C8.00492 6.2222 8.03701 6.1974 8.07202 6.17844L10.8595 4.56956C10.8857 4.55352 10.9222 4.53309 10.9426 4.52288C10.5605 4.20344 10.0937 3.99923 9.59921 3.93651C9.10474 3.87233 8.60296 3.9511 8.15225 4.1626C7.70007 4.3741 7.3179 4.71105 7.05097 5.13114C6.78404 5.55268 6.64256 6.03987 6.64256 6.53872V9.79148C6.64353 9.79537 6.6445 9.79878 6.64547 9.80169C6.64645 9.80461 6.6479 9.80753 6.64985 9.81044C6.65179 9.81336 6.65422 9.81628 6.65714 9.8192C6.65909 9.82114 6.662 9.82309 6.66589 9.82503L7.84301 10.5048V6.57373ZM8.4819 10.8723L9.99742 11.7475L11.5129 10.8723V9.12343L9.99888 8.24824L8.48336 9.12343L8.4819 10.8723Z"
fill=
"white"
/>
</
svg
>
)
)
}
const
ConifgModel
:
FC
<
IConifgModelProps
>
=
({
mode
,
...
...
@@ -58,6 +90,7 @@ const ConifgModel: FC<IConifgModelProps> = ({
const
[
isShowConfig
,
{
setFalse
:
hideConfig
,
toggle
:
toogleShowConfig
}]
=
useBoolean
(
false
)
const
[
maxTokenSettingTipVisible
,
setMaxTokenSettingTipVisible
]
=
useState
(
false
)
const
configContentRef
=
React
.
useRef
(
null
)
const
currModel
=
options
.
find
(
item
=>
item
.
id
===
modelId
)
useClickAway
(()
=>
{
hideConfig
()
},
configContentRef
)
...
...
@@ -99,7 +132,7 @@ const ConifgModel: FC<IConifgModelProps> = ({
key
:
'max_tokens'
,
tip
:
t
(
'common.model.params.maxTokenTip'
),
step
:
100
,
max
:
(
modelId
===
'gpt-4'
||
modelId
===
'gpt-3.5-turbo-16k'
)
?
8000
:
4000
,
max
:
getMaxToken
(
modelId
)
,
},
]
...
...
@@ -112,7 +145,7 @@ const ConifgModel: FC<IConifgModelProps> = ({
hideOption
()
},
triggerRef
)
const
handleSelectModel
=
(
id
:
string
)
=>
{
const
handleSelectModel
=
(
id
:
string
,
provider
=
ProviderType
.
openai
)
=>
{
return
()
=>
{
if
(
id
===
'gpt-4'
&&
!
canUseGPT4
)
{
hideConfig
()
...
...
@@ -120,17 +153,18 @@ const ConifgModel: FC<IConifgModelProps> = ({
onShowUseGPT4Confirm
()
return
}
if
(
id
!==
'gpt-4'
&&
completionParams
.
max_tokens
>
4000
)
{
const
nextSelectModelMaxToken
=
getMaxToken
(
id
)
if
(
completionParams
.
max_tokens
>
nextSelectModelMaxToken
)
{
Toast
.
notify
({
type
:
'warning'
,
message
:
t
(
'common.model.params.setToCurrentModelMaxTokenTip'
),
message
:
t
(
'common.model.params.setToCurrentModelMaxTokenTip'
,
{
maxToken
:
formatNumber
(
nextSelectModelMaxToken
)
}
),
})
onCompletionParamsChange
({
...
completionParams
,
max_tokens
:
4000
,
max_tokens
:
nextSelectModelMaxToken
,
})
}
setModelId
(
id
)
setModelId
(
id
,
provider
)
}
}
...
...
@@ -181,7 +215,7 @@ const ConifgModel: FC<IConifgModelProps> = ({
useEffect
(()
=>
{
const
max
=
params
[
4
].
max
if
(
completionParams
.
max_tokens
>
max
*
2
/
3
)
if
(
c
urrModel
?.
provider
!==
ProviderType
.
anthropic
&&
c
ompletionParams
.
max_tokens
>
max
*
2
/
3
)
setMaxTokenSettingTipVisible
(
true
)
else
setMaxTokenSettingTipVisible
(
false
)
...
...
@@ -193,7 +227,7 @@ const ConifgModel: FC<IConifgModelProps> = ({
className=
{
cn
(
'flex items-center border h-8 px-2.5 space-x-2 rounded-lg'
,
disabled
?
diabledStyle
:
ableStyle
)
}
onClick=
{
()
=>
!
disabled
&&
toogleShowConfig
()
}
>
<
ModelIcon
/>
<
ModelIcon
provider=
{
currModel
?.
provider
}
/>
<
div
className=
'text-[13px] text-gray-900 font-medium'
>
{
selectedModel
.
name
}
</
div
>
{
disabled
?
<
InformationCircleIcon
className=
'w-3.5 h-3.5 text-[#F79009]'
/>
:
<
Cog8ToothIcon
className=
'w-3.5 h-3.5 text-gray-500'
/>
}
</
div
>
...
...
@@ -220,15 +254,15 @@ const ConifgModel: FC<IConifgModelProps> = ({
{
/* model selector */
}
<
div
className=
"relative"
style=
{
{
zIndex
:
30
}
}
>
<
div
ref=
{
triggerRef
}
onClick=
{
()
=>
!
selectModelDisabled
&&
toogleOption
()
}
className=
{
cn
(
selectModelDisabled
?
'cursor-not-allowed'
:
'cursor-pointer'
,
'flex items-center h-9 px-3 space-x-2 rounded-lg bg-gray-50 '
)
}
>
<
ModelIcon
/>
<
ModelIcon
provider=
{
currModel
?.
provider
}
/>
<
div
className=
"text-sm gray-900"
>
{
selectedModel
?.
name
}
</
div
>
{
!
selectModelDisabled
&&
<
ChevronDownIcon
className=
{
cn
(
isShowOption
&&
'rotate-180'
,
'w-[14px] h-[14px] text-gray-500'
)
}
/>
}
</
div
>
{
isShowOption
&&
(
<
div
className=
{
cn
(
isChatApp
?
'min-w-[159px]'
:
'w-[179px]'
,
'absolute right-0 bg-gray-50 rounded-lg shadow'
)
}
>
{
availableModels
.
map
(
item
=>
(
<
div
key=
{
item
.
id
}
onClick=
{
handleSelectModel
(
item
.
id
)
}
className=
"flex items-center h-9 px-3 rounded-lg cursor-pointer hover:bg-gray-100"
>
<
ModelIcon
className=
'shrink-0 mr-2'
/>
<
div
key=
{
item
.
id
}
onClick=
{
handleSelectModel
(
item
.
id
,
item
.
provider
)
}
className=
"flex items-center h-9 px-3 rounded-lg cursor-pointer hover:bg-gray-100"
>
<
ModelIcon
className=
'shrink-0 mr-2'
provider=
{
item
?.
provider
}
/>
<
div
className=
"text-sm gray-900 whitespace-nowrap"
>
{
item
.
name
}
</
div
>
</
div
>
))
}
...
...
web/app/components/app/configuration/debug/index.tsx
View file @
9098d099
...
...
@@ -372,7 +372,7 @@ const Debug: FC<IDebug> = ({
{
/* Chat */
}
{
mode
===
AppType
.
chat
&&
(
<
div
className=
"mt-[34px] h-full flex flex-col"
>
<
div
className=
{
cn
(
doShowSuggestion
?
'pb-[140px]'
:
(
isResponsing
?
'pb-[113px]'
:
'pb-[
6
6px]'
),
'relative mt-1.5 grow h-[200px] overflow-hidden'
)
}
>
<
div
className=
{
cn
(
doShowSuggestion
?
'pb-[140px]'
:
(
isResponsing
?
'pb-[113px]'
:
'pb-[
7
6px]'
),
'relative mt-1.5 grow h-[200px] overflow-hidden'
)
}
>
<
div
className=
"h-full overflow-y-auto overflow-x-hidden"
ref=
{
chatListDomRef
}
>
<
Chat
chatList=
{
chatList
}
...
...
web/app/components/app/configuration/index.tsx
View file @
9098d099
...
...
@@ -16,6 +16,7 @@ import ConfigModel from '@/app/components/app/configuration/config-model'
import
Config
from
'@/app/components/app/configuration/config'
import
Debug
from
'@/app/components/app/configuration/debug'
import
Confirm
from
'@/app/components/base/confirm'
import
{
ProviderType
}
from
'@/types/app'
import
type
{
AppDetailResponse
}
from
'@/models/app'
import
{
ToastContext
}
from
'@/app/components/base/toast'
import
{
fetchTenantInfo
}
from
'@/service/common'
...
...
@@ -67,7 +68,7 @@ const Configuration: FC = () => {
frequency_penalty
:
1
,
// -2-2
})
const
[
modelConfig
,
doSetModelConfig
]
=
useState
<
ModelConfig
>
({
provider
:
'openai'
,
provider
:
ProviderType
.
openai
,
model_id
:
'gpt-3.5-turbo'
,
configs
:
{
prompt_template
:
''
,
...
...
@@ -84,8 +85,9 @@ const Configuration: FC = () => {
doSetModelConfig
(
newModelConfig
)
}
const
setModelId
=
(
modelId
:
string
)
=>
{
const
setModelId
=
(
modelId
:
string
,
provider
:
ProviderType
)
=>
{
const
newModelConfig
=
produce
(
modelConfig
,
(
draft
:
any
)
=>
{
draft
.
provider
=
provider
draft
.
model_id
=
modelId
})
setModelConfig
(
newModelConfig
)
...
...
web/app/components/app/text-generate/item/index.tsx
View file @
9098d099
...
...
@@ -128,6 +128,9 @@ const GenerationItem: FC<IGenerationItemProps> = ({
startQuerying
()
const
res
:
any
=
await
fetchMoreLikeThis
(
messageId
as
string
,
isInstalledApp
,
installedAppId
)
setCompletionRes
(
res
.
answer
)
setChildFeedback
({
rating
:
null
,
})
setChildMessageId
(
res
.
id
)
stopQuerying
()
}
...
...
@@ -152,6 +155,12 @@ const GenerationItem: FC<IGenerationItemProps> = ({
}
},
[
controlClearMoreLikeThis
])
// regeneration clear child
useEffect
(()
=>
{
if
(
isLoading
)
setChildMessageId
(
null
)
},
[
isLoading
])
return
(
<
div
className=
{
cn
(
className
,
isTop
?
'rounded-xl border border-gray-200 bg-white'
:
'rounded-br-xl !mt-0'
)
}
style=
{
isTop
...
...
@@ -175,7 +184,11 @@ const GenerationItem: FC<IGenerationItemProps> = ({
{
taskId
}
</
div
>)
}
<
div
className=
'flex'
>
<
div
className=
'grow w-0'
>
<
Markdown
content=
{
content
}
/>
</
div
>
</
div
>
{
messageId
&&
(
<
div
className=
'flex items-center justify-between mt-3'
>
<
div
className=
'flex items-center'
>
...
...
web/app/components/base/auto-height-textarea/index.tsx
View file @
9098d099
...
...
@@ -19,6 +19,7 @@ const AutoHeightTextarea = forwardRef(
{
value
,
onChange
,
placeholder
,
className
,
minHeight
=
36
,
maxHeight
=
96
,
autoFocus
,
controlFocus
,
onKeyDown
,
onKeyUp
}:
IProps
,
outerRef
:
any
,
)
=>
{
// eslint-disable-next-line react-hooks/rules-of-hooks
const
ref
=
outerRef
||
useRef
<
HTMLTextAreaElement
>
(
null
)
const
doFocus
=
()
=>
{
...
...
@@ -54,13 +55,20 @@ const AutoHeightTextarea = forwardRef(
return
(
<
div
className=
'relative'
>
<
div
className=
{
cn
(
className
,
'invisible whitespace-pre-wrap break-all overflow-y-auto'
)
}
style=
{
{
minHeight
,
maxHeight
}
}
>
<
div
className=
{
cn
(
className
,
'invisible whitespace-pre-wrap break-all overflow-y-auto'
)
}
style=
{
{
minHeight
,
maxHeight
,
paddingRight
:
(
value
&&
value
.
trim
().
length
>
10000
)
?
140
:
130
,
}
}
>
{
!
value
?
placeholder
:
value
.
replace
(
/
\n
$/
,
'
\
n '
)
}
</
div
>
<
textarea
ref=
{
ref
}
autoFocus=
{
autoFocus
}
className=
{
cn
(
className
,
'absolute inset-0 resize-none overflow-hidden'
)
}
className=
{
cn
(
className
,
'absolute inset-0 resize-none overflow-auto'
)
}
style=
{
{
paddingRight
:
(
value
&&
value
.
trim
().
length
>
10000
)
?
140
:
130
,
}
}
placeholder=
{
placeholder
}
onChange=
{
onChange
}
onKeyDown=
{
onKeyDown
}
...
...
web/app/components/base/select/locale.tsx
View file @
9098d099
...
...
@@ -11,7 +11,7 @@ export const RFC_LOCALES = [
{
value
:
'en-US'
,
name
:
'EN'
},
{
value
:
'zh-Hans'
,
name
:
'简体中文'
},
]
interface
ISelectProps
{
type
ISelectProps
=
{
items
:
Array
<
{
value
:
string
;
name
:
string
}
>
value
?:
string
className
?:
string
...
...
@@ -21,7 +21,7 @@ interface ISelectProps {
export
default
function
Select
({
items
,
value
,
onChange
onChange
,
}:
ISelectProps
)
{
const
item
=
items
.
filter
(
item
=>
item
.
value
===
value
)[
0
]
...
...
@@ -29,11 +29,12 @@ export default function Select({
<
div
className=
"w-56 text-right"
>
<
Menu
as=
"div"
className=
"relative inline-block text-left"
>
<
div
>
<
Menu
.
Button
className=
"inline-flex w-full justify-center items-center
rounded-lg px-2 py-1
text-gray-600 text-xs font-medium
border border-gray-200"
>
<
GlobeAltIcon
className=
"w-5 h-5 mr-2 "
aria
-
hidden=
"true"
/>
<
Menu
.
Button
className=
"inline-flex w-full h-[44px]justify-center items-center
rounded-lg px-[10px] py-[6px]
text-gray-900 text-[13px] font-medium
border border-gray-200
hover:bg-gray-100"
>
<
GlobeAltIcon
className=
"w-5 h-5 mr-1"
aria
-
hidden=
"true"
/>
{
item
?.
name
}
</
Menu
.
Button
>
</
div
>
...
...
@@ -46,14 +47,14 @@ export default function Select({
leaveFrom=
"transform opacity-100 scale-100"
leaveTo=
"transform opacity-0 scale-95"
>
<
Menu
.
Items
className=
"absolute right-0 mt-2 w-
28
origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<
Menu
.
Items
className=
"absolute right-0 mt-2 w-
[120px]
origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<
div
className=
"px-1 py-1 "
>
{
items
.
map
((
item
)
=>
{
return
<
Menu
.
Item
key=
{
item
.
value
}
>
{
({
active
})
=>
(
<
button
className=
{
`${active ? 'bg-gray-100' : ''
} group flex w-full items-center rounded-md px-2 py-2 text-sm
`
}
} group flex w-full items-center rounded-lg px-3 py-2 text-sm text-gray-700
`
}
onClick=
{
(
evt
)
=>
{
evt
.
preventDefault
()
onChange
&&
onChange
(
item
.
value
)
...
...
@@ -77,7 +78,7 @@ export default function Select({
export
function
InputSelect
({
items
,
value
,
onChange
onChange
,
}:
ISelectProps
)
{
const
item
=
items
.
filter
(
item
=>
item
.
value
===
value
)[
0
]
return
(
...
...
web/app/components/browser-initor.tsx
0 → 100644
View file @
9098d099
'use client'
class
StorageMock
{
data
:
Record
<
string
,
string
>
constructor
()
{
this
.
data
=
{}
as
Record
<
string
,
string
>
}
setItem
(
name
:
string
,
value
:
string
)
{
this
.
data
[
name
]
=
value
}
getItem
(
name
:
string
)
{
return
this
.
data
[
name
]
||
null
}
removeItem
(
name
:
string
)
{
delete
this
.
data
[
name
]
}
clear
()
{
this
.
data
=
{}
}
}
let
localStorage
,
sessionStorage
try
{
localStorage
=
globalThis
.
localStorage
sessionStorage
=
globalThis
.
sessionStorage
}
catch
(
e
)
{
localStorage
=
new
StorageMock
()
sessionStorage
=
new
StorageMock
()
}
Object
.
defineProperty
(
globalThis
,
'localStorage'
,
{
value
:
localStorage
,
})
Object
.
defineProperty
(
globalThis
,
'sessionStorage'
,
{
value
:
sessionStorage
,
})
const
BrowerInitor
=
({
children
,
}:
{
children
:
React
.
ReactElement
})
=>
{
return
children
}
export
default
BrowerInitor
web/app/components/develop/template/template_chat.en.mdx
View file @
9098d099
...
...
@@ -334,6 +334,53 @@ For versatile conversational apps using a Q&A format, call the chat-messages API
</Row>
---
<Heading
url='/conversations/{converation_id}'
method='DELETE'
title='Conversation deletion'
name='#delete'
/>
<Row>
<Col>
Delete conversation.
### Request Body
<Properties>
<Property name='user' type='string' key='user'>
The user identifier, defined by the developer, must ensure uniqueness within the app.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/conversations/{converation_id}" targetCode={`curl --location --request DELETE '${props.appDetail.api_base_url}/conversations/{conversation_id}' \\\n--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{ \n "user": "abc-123"\n}'`}>
```bash {{ title: 'cURL' }}
curl --location --request DELETE 'https://cloud.langgenius.dev/api/conversations/{convsation_id}' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \
--data '{
"user": "abc-123"
}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"result": "success"
}
```
</CodeGroup>
</Col>
</Row>
---
<Heading
...
...
web/app/components/develop/template/template_chat.zh.mdx
View file @
9098d099
...
...
@@ -333,6 +333,52 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
</Col>
</Row>
---
<Heading
url='/conversations/{converation_id}'
method='DELETE'
title='删除会话'
name='#delete'
/>
<Row>
<Col>
删除会话。
### Request Body
<Properties>
<Property name='user' type='string' key='user'>
用户标识,由开发者定义规则,需保证用户标识在应用内唯一。
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/conversations/{converation_id}" targetCode={`curl --location --request DELETE '${props.appDetail.api_base_url}/conversations/{conversation_id}' \\\n--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{ \n "user": "abc-123"\n}'`}>
```bash {{ title: 'cURL' }}
curl --location --request DELETE 'https://cloud.langgenius.dev/api/conversations/{convsation_id}' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \
--data '{
"user": "abc-123"
}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"result": "success"
}
```
</CodeGroup>
</Col>
</Row>
---
...
...
web/app/components/header/account-about/index.module.css
View file @
9098d099
.logo-icon
{
background
:
url(../assets/logo-icon.png)
center
center
no-repeat
;
background-size
:
contain
;
background-size
:
32px
;
box-shadow
:
0px
4px
6px
-1px
rgba
(
0
,
0
,
0
,
0.05
),
0px
2px
4px
-2px
rgba
(
0
,
0
,
0
,
0.05
);
}
...
...
web/app/components/header/account-about/index.tsx
View file @
9098d099
...
...
@@ -34,7 +34,7 @@ export default function AccountAbout({
<
div
>
<
div
className=
{
classNames
(
s
[
'logo-icon'
],
'mx-auto mb-3 w-12 h-12 bg-white rounded border border-gray-200'
,
'mx-auto mb-3 w-12 h-12 bg-white rounded
-xl
border border-gray-200'
,
)
}
/>
<
div
className=
{
classNames
(
s
[
'logo-text'
],
...
...
web/app/components/header/account-setting/account-page/index.tsx
View file @
9098d099
...
...
@@ -13,6 +13,7 @@ import { useAppContext } from '@/context/app-context'
import
{
ToastContext
}
from
'@/app/components/base/toast'
import
AppIcon
from
'@/app/components/base/app-icon'
import
Avatar
from
'@/app/components/base/avatar'
import
{
IS_CE_EDITION
}
from
'@/config'
const
titleClassName
=
`
text-sm font-medium text-gray-900
...
...
@@ -25,13 +26,19 @@ const inputClassName = `
text-sm font-normal text-gray-800
`
const
validPassword
=
/^
(?=
.*
[
a-zA-Z
])(?=
.*
\d)
.
{8,}
$/
export
default
function
AccountPage
()
{
const
{
t
}
=
useTranslation
()
const
{
mutateUserProfile
,
userProfile
,
apps
}
=
useAppContext
()
const
{
notify
}
=
useContext
(
ToastContext
)
const
[
editNameModalVisible
,
setEditNameModalVisible
]
=
useState
(
false
)
const
[
editName
,
setEditName
]
=
useState
(
''
)
const
[
editing
,
setEditing
]
=
useState
(
false
)
const
{
t
}
=
useTranslation
()
const
[
editPasswordModalVisible
,
setEditPasswordModalVisible
]
=
useState
(
false
)
const
[
currentPassword
,
setCurrentPassword
]
=
useState
(
''
)
const
[
password
,
setPassword
]
=
useState
(
''
)
const
[
confirmPassword
,
setConfirmPassword
]
=
useState
(
''
)
const
handleEditName
=
()
=>
{
setEditNameModalVisible
(
true
)
...
...
@@ -52,6 +59,56 @@ export default function AccountPage() {
setEditing
(
false
)
}
}
const
showErrorMessage
=
(
message
:
string
)
=>
{
notify
({
type
:
'error'
,
message
,
})
}
const
valid
=
()
=>
{
if
(
!
password
.
trim
())
{
showErrorMessage
(
t
(
'login.error.passwordEmpty'
))
return
false
}
if
(
!
validPassword
.
test
(
password
))
showErrorMessage
(
t
(
'login.error.passwordInvalid'
))
if
(
password
!==
confirmPassword
)
showErrorMessage
(
t
(
'common.account.notEqual'
))
return
true
}
const
resetPasswordForm
=
()
=>
{
setCurrentPassword
(
''
)
setPassword
(
''
)
setConfirmPassword
(
''
)
}
const
handleSavePassowrd
=
async
()
=>
{
if
(
!
valid
())
return
try
{
setEditing
(
true
)
await
updateUserProfile
({
url
:
'account/password'
,
body
:
{
password
:
currentPassword
,
new_password
:
password
,
repeat_new_password
:
confirmPassword
,
},
})
notify
({
type
:
'success'
,
message
:
t
(
'common.actionMsg.modifiedSuccessfully'
)
})
mutateUserProfile
()
setEditPasswordModalVisible
(
false
)
resetPasswordForm
()
setEditing
(
false
)
}
catch
(
e
)
{
notify
({
type
:
'error'
,
message
:
(
e
as
Error
).
message
})
setEditPasswordModalVisible
(
false
)
setEditing
(
false
)
}
}
const
renderAppItem
=
(
item
:
IItem
)
=>
{
return
(
<
div
className=
'flex px-3 py-1'
>
...
...
@@ -80,8 +137,14 @@ export default function AccountPage() {
<
div
className=
{
titleClassName
}
>
{
t
(
'common.account.email'
)
}
</
div
>
<
div
className=
{
classNames
(
inputClassName
,
'cursor-pointer'
)
}
>
{
userProfile
.
email
}
</
div
>
</
div
>
{
!!
apps
.
length
&&
(
{
IS_CE_EDITION
&&
(
<
div
className=
'mb-8'
>
<
div
className=
'mb-1 text-sm font-medium text-gray-900'
>
{
t
(
'common.account.password'
)
}
</
div
>
<
div
className=
'mb-2 text-xs text-gray-500'
>
{
t
(
'common.account.passwordTip'
)
}
</
div
>
<
Button
className=
'font-medium !text-gray-700 !px-3 !py-[7px] !text-[13px]'
onClick=
{
()
=>
setEditPasswordModalVisible
(
true
)
}
>
{
userProfile
.
is_password_set
?
t
(
'common.account.resetPassword'
)
:
t
(
'common.account.setPassword'
)
}
</
Button
>
</
div
>
)
}
{
!!
apps
.
length
&&
(
<>
<
div
className=
'mb-6 border-[0.5px] border-gray-100'
/>
<
div
className=
'mb-8'
>
...
...
@@ -95,10 +158,8 @@ export default function AccountPage() {
/>
</
div
>
</>
)
}
{
editNameModalVisible
&&
(
)
}
{
editNameModalVisible
&&
(
<
Modal
isShow
onClose=
{
()
=>
setEditNameModalVisible
(
false
)
}
...
...
@@ -123,8 +184,60 @@ export default function AccountPage() {
</
Button
>
</
div
>
</
Modal
>
)
}
)
}
{
editPasswordModalVisible
&&
(
<
Modal
isShow
onClose=
{
()
=>
{
setEditPasswordModalVisible
(
false
)
resetPasswordForm
()
}
}
className=
{
s
.
modal
}
>
<
div
className=
'mb-6 text-lg font-medium text-gray-900'
>
{
userProfile
.
is_password_set
?
t
(
'common.account.resetPassword'
)
:
t
(
'common.account.setPassword'
)
}
</
div
>
{
userProfile
.
is_password_set
&&
(
<>
<
div
className=
{
titleClassName
}
>
{
t
(
'common.account.currentPassword'
)
}
</
div
>
<
input
type=
"password"
className=
{
inputClassName
}
value=
{
currentPassword
}
onChange=
{
e
=>
setCurrentPassword
(
e
.
target
.
value
)
}
/>
</>
)
}
<
div
className=
'mt-8 text-sm font-medium text-gray-900'
>
{
userProfile
.
is_password_set
?
t
(
'common.account.newPassword'
)
:
t
(
'common.account.password'
)
}
</
div
>
<
input
type=
"password"
className=
{
inputClassName
}
value=
{
password
}
onChange=
{
e
=>
setPassword
(
e
.
target
.
value
)
}
/>
<
div
className=
'mt-8 text-sm font-medium text-gray-900'
>
{
t
(
'common.account.confirmPassword'
)
}
</
div
>
<
input
type=
"password"
className=
{
inputClassName
}
value=
{
confirmPassword
}
onChange=
{
e
=>
setConfirmPassword
(
e
.
target
.
value
)
}
/>
<
div
className=
'flex justify-end mt-10'
>
<
Button
className=
'mr-2 text-sm font-medium'
onClick=
{
()
=>
{
setEditPasswordModalVisible
(
false
)
resetPasswordForm
()
}
}
>
{
t
(
'common.operation.cancel'
)
}
</
Button
>
<
Button
disabled=
{
editing
}
type=
'primary'
className=
'text-sm font-medium'
onClick=
{
handleSavePassowrd
}
>
{
userProfile
.
is_password_set
?
t
(
'common.operation.reset'
)
:
t
(
'common.operation.save'
)
}
</
Button
>
</
div
>
</
Modal
>
)
}
</>
)
}
web/app/components/header/account-setting/index.tsx
View file @
9098d099
'use client'
import
{
useTranslation
}
from
'react-i18next'
import
{
useState
}
from
'react'
import
{
useEffect
,
useRef
,
useState
}
from
'react'
import
cn
from
'classnames'
import
{
AtSymbolIcon
,
CubeTransparentIcon
,
GlobeAltIcon
,
UserIcon
,
UsersIcon
,
XMarkIcon
}
from
'@heroicons/react/24/outline'
import
{
GlobeAltIcon
as
GlobalAltIconSolid
,
UserIcon
as
UserIconSolid
,
UsersIcon
as
UsersIconSolid
}
from
'@heroicons/react/24/solid'
import
AccountPage
from
'./account-page'
...
...
@@ -19,6 +20,10 @@ const iconClassName = `
w-4 h-4 ml-3 mr-2
`
const
scrolledClassName
=
`
border-b shadow-xs bg-white/[.98]
`
type
IAccountSettingProps
=
{
onCancel
:
()
=>
void
activeTab
?:
string
...
...
@@ -85,6 +90,22 @@ export default function AccountSetting({
],
},
]
const
scrollRef
=
useRef
<
HTMLDivElement
>
(
null
)
const
[
scrolled
,
setScrolled
]
=
useState
(
false
)
const
scrollHandle
=
(
e
:
any
)
=>
{
if
(
e
.
target
.
scrollTop
>
0
)
setScrolled
(
true
)
else
setScrolled
(
false
)
}
useEffect
(()
=>
{
const
targetElement
=
scrollRef
.
current
targetElement
?.
addEventListener
(
'scroll'
,
scrollHandle
)
return
()
=>
{
targetElement
?.
removeEventListener
(
'scroll'
,
scrollHandle
)
}
},
[])
return
(
<
Modal
...
...
@@ -122,32 +143,20 @@ export default function AccountSetting({
}
</
div
>
</
div
>
<
div
className=
'w-[520px] h-[580px] px-6 py
-4 overflow-y-auto'
>
<
div
className=
'flex items-center justify-between h-6 mb-8 text-base font-medium text-gray-900 '
>
<
div
ref=
{
scrollRef
}
className=
'relative w-[520px] h-[580px] pb
-4 overflow-y-auto'
>
<
div
className=
{
cn
(
'sticky top-0 px-6 py-4 flex items-center justify-between h-14 mb-4 bg-white text-base font-medium text-gray-900'
,
scrolled
&&
scrolledClassName
)
}
>
{
[...
menuItems
[
0
].
items
,
...
menuItems
[
1
].
items
].
find
(
item
=>
item
.
key
===
activeMenu
)?.
name
}
<
XMarkIcon
className=
'w-4 h-4 cursor-pointer'
onClick=
{
onCancel
}
/>
</
div
>
{
activeMenu
===
'account'
&&
<
AccountPage
/>
}
{
activeMenu
===
'members'
&&
<
MembersPage
/>
}
{
activeMenu
===
'integrations'
&&
<
IntegrationsPage
/>
}
{
activeMenu
===
'language'
&&
<
LanguagePage
/>
}
{
activeMenu
===
'provider'
&&
<
ProviderPage
/>
}
{
activeMenu
===
'data-source'
&&
<
DataSourcePage
/>
}
{
activeMenu
===
'plugin'
&&
<
PluginPage
/>
}
<
div
className=
'px-6'
>
{
activeMenu
===
'account'
&&
<
AccountPage
/>
}
{
activeMenu
===
'members'
&&
<
MembersPage
/>
}
{
activeMenu
===
'integrations'
&&
<
IntegrationsPage
/>
}
{
activeMenu
===
'language'
&&
<
LanguagePage
/>
}
{
activeMenu
===
'provider'
&&
<
ProviderPage
/>
}
{
activeMenu
===
'data-source'
&&
<
DataSourcePage
/>
}
{
activeMenu
===
'data-source'
&&
<
PluginPage
/>
}
</
div
>
</
div
>
</
div
>
</
Modal
>
...
...
web/app/components/header/account-setting/members-page/index.tsx
View file @
9098d099
...
...
@@ -30,6 +30,7 @@ const MembersPage = () => {
const
{
userProfile
}
=
useAppContext
()
const
{
data
,
mutate
}
=
useSWR
({
url
:
'/workspaces/current/members'
},
fetchMembers
)
const
[
inviteModalVisible
,
setInviteModalVisible
]
=
useState
(
false
)
const
[
invitationLink
,
setInvitationLink
]
=
useState
(
''
)
const
[
invitedModalVisible
,
setInvitedModalVisible
]
=
useState
(
false
)
const
accounts
=
data
?.
accounts
||
[]
const
owner
=
accounts
.
filter
(
account
=>
account
.
role
===
'owner'
)?.[
0
]?.
email
===
userProfile
.
email
...
...
@@ -93,8 +94,9 @@ const MembersPage = () => {
inviteModalVisible
&&
(
<
InviteModal
onCancel=
{
()
=>
setInviteModalVisible
(
false
)
}
onSend=
{
()
=>
{
onSend=
{
(
url
)
=>
{
setInvitedModalVisible
(
true
)
setInvitationLink
(
url
)
mutate
()
}
}
/>
...
...
@@ -103,6 +105,7 @@ const MembersPage = () => {
{
invitedModalVisible
&&
(
<
InvitedModal
invitationLink=
{
invitationLink
}
onCancel=
{
()
=>
setInvitedModalVisible
(
false
)
}
/>
)
...
...
web/app/components/header/account-setting/members-page/invite-modal/index.tsx
View file @
9098d099
...
...
@@ -3,16 +3,16 @@ import { useState } from 'react'
import
{
useContext
}
from
'use-context-selector'
import
{
XMarkIcon
}
from
'@heroicons/react/24/outline'
import
{
useTranslation
}
from
'react-i18next'
import
s
from
'./index.module.css'
import
Modal
from
'@/app/components/base/modal'
import
Button
from
'@/app/components/base/button'
import
s
from
'./index.module.css'
import
{
inviteMember
}
from
'@/service/common'
import
{
emailRegex
}
from
'@/config'
import
{
ToastContext
}
from
'@/app/components/base/toast'
interface
IInviteModalProps
{
onCancel
:
()
=>
void
,
onSend
:
(
)
=>
void
,
type
IInviteModalProps
=
{
onCancel
:
()
=>
void
onSend
:
(
url
:
string
)
=>
void
}
const
InviteModal
=
({
onCancel
,
...
...
@@ -25,16 +25,16 @@ const InviteModal = ({
const
handleSend
=
async
()
=>
{
if
(
emailRegex
.
test
(
email
))
{
try
{
const
res
=
await
inviteMember
({
url
:
'/workspaces/current/members/invite-email'
,
body
:
{
email
,
role
:
'admin'
}
})
const
res
=
await
inviteMember
({
url
:
'/workspaces/current/members/invite-email'
,
body
:
{
email
,
role
:
'admin'
}
})
if
(
res
.
result
===
'success'
)
{
onCancel
()
onSend
()
onSend
(
res
.
invite_url
)
}
}
catch
(
e
)
{
}
}
else
{
catch
(
e
)
{}
}
else
{
notify
({
type
:
'error'
,
message
:
t
(
'common.members.emailInvalid'
)
})
}
}
...
...
web/app/components/header/account-setting/members-page/invited-modal/assets/copied.svg
0 → 100644
View file @
9098d099
<svg
width=
"16"
height=
"16"
viewBox=
"0 0 16 16"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<path
d=
"M10.6665 2.66683C11.2865 2.66683 11.5965 2.66683 11.8508 2.73498C12.541 2.91991 13.0801 3.45901 13.265 4.14919C13.3332 4.40352 13.3332 4.71352 13.3332 5.3335V11.4668C13.3332 12.5869 13.3332 13.147 13.1152 13.5748C12.9234 13.9511 12.6175 14.2571 12.2412 14.4488C11.8133 14.6668 11.2533 14.6668 10.1332 14.6668H5.8665C4.7464 14.6668 4.18635 14.6668 3.75852 14.4488C3.3822 14.2571 3.07624 13.9511 2.88449 13.5748C2.6665 13.147 2.6665 12.5869 2.6665 11.4668V5.3335C2.6665 4.71352 2.6665 4.40352 2.73465 4.14919C2.91959 3.45901 3.45868 2.91991 4.14887 2.73498C4.4032 2.66683 4.71319 2.66683 5.33317 2.66683M5.99984 10.0002L7.33317 11.3335L10.3332 8.3335M6.39984 4.00016H9.59984C9.9732 4.00016 10.1599 4.00016 10.3025 3.9275C10.4279 3.86359 10.5299 3.7616 10.5938 3.63616C10.6665 3.49355 10.6665 3.30686 10.6665 2.9335V2.40016C10.6665 2.02679 10.6665 1.84011 10.5938 1.6975C10.5299 1.57206 10.4279 1.47007 10.3025 1.40616C10.1599 1.3335 9.97321 1.3335 9.59984 1.3335H6.39984C6.02647 1.3335 5.83978 1.3335 5.69718 1.40616C5.57174 1.47007 5.46975 1.57206 5.40583 1.6975C5.33317 1.84011 5.33317 2.02679 5.33317 2.40016V2.9335C5.33317 3.30686 5.33317 3.49355 5.40583 3.63616C5.46975 3.7616 5.57174 3.86359 5.69718 3.9275C5.83978 4.00016 6.02647 4.00016 6.39984 4.00016Z"
stroke=
"#1D2939"
stroke-width=
"1.5"
stroke-linecap=
"round"
stroke-linejoin=
"round"
/>
</svg>
web/app/components/header/account-setting/members-page/invited-modal/assets/copy-hover.svg
0 → 100644
View file @
9098d099
<svg
width=
"16"
height=
"16"
viewBox=
"0 0 16 16"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<path
d=
"M10.6665 2.66634H11.9998C12.3535 2.66634 12.6926 2.80682 12.9426 3.05687C13.1927 3.30691 13.3332 3.64605 13.3332 3.99967V13.333C13.3332 13.6866 13.1927 14.0258 12.9426 14.2758C12.6926 14.5259 12.3535 14.6663 11.9998 14.6663H3.99984C3.64622 14.6663 3.30708 14.5259 3.05703 14.2758C2.80698 14.0258 2.6665 13.6866 2.6665 13.333V3.99967C2.6665 3.64605 2.80698 3.30691 3.05703 3.05687C3.30708 2.80682 3.64622 2.66634 3.99984 2.66634H5.33317M5.99984 1.33301H9.99984C10.368 1.33301 10.6665 1.63148 10.6665 1.99967V3.33301C10.6665 3.7012 10.368 3.99967 9.99984 3.99967H5.99984C5.63165 3.99967 5.33317 3.7012 5.33317 3.33301V1.99967C5.33317 1.63148 5.63165 1.33301 5.99984 1.33301Z"
stroke=
"#1D2939"
stroke-width=
"1.5"
stroke-linecap=
"round"
stroke-linejoin=
"round"
/>
</svg>
web/app/components/header/account-setting/members-page/invited-modal/assets/copy.svg
0 → 100644
View file @
9098d099
<svg
width=
"16"
height=
"16"
viewBox=
"0 0 16 16"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
>
<path
d=
"M10.6665 2.66634H11.9998C12.3535 2.66634 12.6926 2.80682 12.9426 3.05687C13.1927 3.30691 13.3332 3.64605 13.3332 3.99967V13.333C13.3332 13.6866 13.1927 14.0258 12.9426 14.2758C12.6926 14.5259 12.3535 14.6663 11.9998 14.6663H3.99984C3.64622 14.6663 3.30708 14.5259 3.05703 14.2758C2.80698 14.0258 2.6665 13.6866 2.6665 13.333V3.99967C2.6665 3.64605 2.80698 3.30691 3.05703 3.05687C3.30708 2.80682 3.64622 2.66634 3.99984 2.66634H5.33317M5.99984 1.33301H9.99984C10.368 1.33301 10.6665 1.63148 10.6665 1.99967V3.33301C10.6665 3.7012 10.368 3.99967 9.99984 3.99967H5.99984C5.63165 3.99967 5.33317 3.7012 5.33317 3.33301V1.99967C5.33317 1.63148 5.63165 1.33301 5.99984 1.33301Z"
stroke=
"#667085"
stroke-width=
"1.5"
stroke-linecap=
"round"
stroke-linejoin=
"round"
/>
</svg>
web/app/components/header/account-setting/members-page/invited-modal/index.module.css
View file @
9098d099
...
...
@@ -3,3 +3,19 @@
width
:
480px
!important
;
background
:
linear-gradient
(
180deg
,
rgba
(
3
,
152
,
85
,
0.05
)
0%
,
rgba
(
3
,
152
,
85
,
0
)
22.44%
),
#F9FAFB
!important
;
}
.copyIcon
{
background-image
:
url(./assets/copy.svg)
;
background-position
:
center
;
background-repeat
:
no-repeat
;
}
.copyIcon
:hover
{
background-image
:
url(./assets/copy-hover.svg)
;
background-position
:
center
;
background-repeat
:
no-repeat
;
}
.copyIcon.copied
{
background-image
:
url(./assets/copied.svg)
;
}
\ No newline at end of file
web/app/components/header/account-setting/members-page/invited-modal/index.tsx
View file @
9098d099
import
{
CheckCircleIcon
}
from
'@heroicons/react/24/solid'
import
{
XMarkIcon
}
from
'@heroicons/react/24/outline'
import
{
useTranslation
}
from
'react-i18next'
import
InvitationLink
from
'./invitation-link'
import
s
from
'./index.module.css'
import
Modal
from
'@/app/components/base/modal'
import
Button
from
'@/app/components/base/button'
import
s
from
'./index.module.css
'
import
{
IS_CE_EDITION
}
from
'@/config
'
interface
IInvitedModalProps
{
onCancel
:
()
=>
void
,
type
IInvitedModalProps
=
{
invitationLink
:
string
onCancel
:
()
=>
void
}
const
InvitedModal
=
({
onCancel
invitationLink
,
onCancel
,
}:
IInvitedModalProps
)
=>
{
const
{
t
}
=
useTranslation
()
...
...
@@ -27,7 +31,18 @@ const InvitedModal = ({
<
XMarkIcon
className=
'w-4 h-4 cursor-pointer'
onClick=
{
onCancel
}
/>
</
div
>
<
div
className=
'mb-1 text-xl font-semibold text-gray-900'
>
{
t
(
'common.members.invitationSent'
)
}
</
div
>
{
!
IS_CE_EDITION
&&
(
<
div
className=
'mb-10 text-sm text-gray-500'
>
{
t
(
'common.members.invitationSentTip'
)
}
</
div
>
)
}
{
IS_CE_EDITION
&&
(
<>
<
div
className=
'mb-5 text-sm text-gray-500'
>
{
t
(
'common.members.invitationSentTip'
)
}
</
div
>
<
div
className=
'mb-9'
>
<
div
className=
'py-2 text-sm font-Medium text-gray-900'
>
{
t
(
'common.members.invitationLink'
)
}
</
div
>
<
InvitationLink
value=
{
invitationLink
}
/>
</
div
>
</>
)
}
<
div
className=
'flex justify-end'
>
<
Button
className=
'w-[96px] text-sm font-medium'
...
...
web/app/components/header/account-setting/members-page/invited-modal/invitation-link.tsx
0 → 100644
View file @
9098d099
'use client'
import
React
,
{
useCallback
,
useEffect
,
useState
}
from
'react'
import
{
t
}
from
'i18next'
import
s
from
'./index.module.css'
import
Tooltip
from
'@/app/components/base/tooltip'
import
useCopyToClipboard
from
'@/hooks/use-copy-to-clipboard'
type
IInvitationLinkProps
=
{
value
?:
string
}
const
InvitationLink
=
({
value
=
''
,
}:
IInvitationLinkProps
)
=>
{
const
[
isCopied
,
setIsCopied
]
=
useState
(
false
)
const
[
_
,
copy
]
=
useCopyToClipboard
()
const
copyHandle
=
useCallback
(()
=>
{
copy
(
value
)
setIsCopied
(
true
)
},
[
value
,
copy
])
useEffect
(()
=>
{
if
(
isCopied
)
{
const
timeout
=
setTimeout
(()
=>
{
setIsCopied
(
false
)
},
1000
)
return
()
=>
{
clearTimeout
(
timeout
)
}
}
},
[
isCopied
])
return
(
<
div
className=
'flex rounded-lg bg-gray-100 hover:bg-gray-100 border border-gray-200 py-2 items-center'
>
<
div
className=
"flex items-center flex-grow h-5"
>
<
div
className=
'flex-grow bg-gray-100 text-[13px] relative h-full'
>
<
Tooltip
selector=
"top-uniq"
content=
{
isCopied
?
`${t('appApi.copied')}`
:
`${t('appApi.copy')}`
}
className=
'z-10'
>
<
div
className=
'absolute top-0 left-0 w-full pl-2 pr-2 truncate cursor-pointer r-0'
onClick=
{
copyHandle
}
>
{
value
}
</
div
>
</
Tooltip
>
</
div
>
<
div
className=
"flex-shrink-0 h-4 bg-gray-200 border"
/>
<
Tooltip
selector=
"top-uniq"
content=
{
isCopied
?
`${t('appApi.copied')}`
:
`${t('appApi.copy')}`
}
className=
'z-10'
>
<
div
className=
"px-0.5 flex-shrink-0"
>
<
div
className=
{
`box-border w-[30px] h-[30px] flex items-center justify-center rounded-lg hover:bg-gray-100 cursor-pointer ${s.copyIcon} ${isCopied ? s.copied : ''}`
}
onClick=
{
copyHandle
}
>
</
div
>
</
div
>
</
Tooltip
>
</
div
>
</
div
>
)
}
export
default
InvitationLink
web/app/components/header/account-setting/provider-page/anthropic-hosted-provider/index.module.css
0 → 100644
View file @
9098d099
.icon
{
width
:
24px
;
height
:
24px
;
margin-right
:
12px
;
background
:
url(../../../assets/anthropic.svg)
center
center
no-repeat
;
background-size
:
contain
;
}
.bar
{
background
:
linear-gradient
(
90deg
,
rgba
(
41
,
112
,
255
,
0.9
)
0%
,
rgba
(
21
,
94
,
239
,
0.9
)
100%
);
}
.bar-error
{
background
:
linear-gradient
(
90deg
,
rgba
(
240
,
68
,
56
,
0.72
)
0%
,
rgba
(
217
,
45
,
32
,
0.9
)
100%
);
}
.bar-item
{
width
:
10%
;
border-right
:
1px
solid
rgba
(
255
,
255
,
255
,
0.5
);
}
.bar-item
:last-of-type
{
border-right
:
0
;
}
\ No newline at end of file
web/app/components/header/account-setting/provider-page/anthropic-hosted-provider/index.tsx
0 → 100644
View file @
9098d099
import
{
useTranslation
}
from
'react-i18next'
import
cn
from
'classnames'
import
s
from
'./index.module.css'
import
type
{
ProviderHosted
}
from
'@/models/common'
type
AnthropicHostedProviderProps
=
{
provider
:
ProviderHosted
}
const
AnthropicHostedProvider
=
({
provider
,
}:
AnthropicHostedProviderProps
)
=>
{
const
{
t
}
=
useTranslation
()
const
exhausted
=
provider
.
quota_used
>
provider
.
quota_limit
return
(
<
div
className=
{
`
border-[0.5px] border-gray-200 rounded-xl
${exhausted ? 'bg-[#FFFBFA]' : 'bg-gray-50'}
`
}
>
<
div
className=
'pt-4 px-4 pb-3'
>
<
div
className=
'flex items-center mb-3'
>
<
div
className=
{
s
.
icon
}
/>
<
div
className=
'grow text-sm font-medium text-gray-800'
>
{
t
(
'common.provider.anthropicHosted.anthropicHosted'
)
}
</
div
>
<
div
className=
{
`
px-2 h-[22px] flex items-center rounded-md border
text-xs font-semibold
${exhausted ? 'border-[#D92D20] text-[#D92D20]' : 'border-primary-600 text-primary-600'}
`
}
>
{
exhausted
?
t
(
'common.provider.anthropicHosted.exhausted'
)
:
t
(
'common.provider.anthropicHosted.onTrial'
)
}
</
div
>
</
div
>
<
div
className=
'text-[13px] text-gray-500'
>
{
t
(
'common.provider.anthropicHosted.desc'
)
}
</
div
>
</
div
>
<
div
className=
'flex items-center h-[42px] px-4 border-t-[0.5px] border-t-[rgba(0, 0, 0, 0.05)]'
>
<
div
className=
'text-[13px] text-gray-700'
>
{
t
(
'common.provider.anthropicHosted.callTimes'
)
}
</
div
>
<
div
className=
'relative grow h-2 flex bg-gray-200 rounded-md mx-2 overflow-hidden'
>
<
div
className=
{
cn
(
s
.
bar
,
exhausted
&&
s
[
'bar-error'
],
'absolute top-0 left-0 right-0 bottom-0'
)
}
style=
{
{
width
:
`${(provider.quota_used / provider.quota_limit * 100).toFixed(2)}%`
}
}
/>
{
Array
(
10
).
fill
(
0
).
map
((
i
,
k
)
=>
(
<
div
key=
{
k
}
className=
{
s
[
'bar-item'
]
}
/>
))
}
</
div
>
<
div
className=
{
`
text-[13px] font-medium ${exhausted ? 'text-[#D92D20]' : 'text-gray-700'}
`
}
>
{
provider
.
quota_used
}
/
{
provider
.
quota_limit
}
</
div
>
</
div
>
{
exhausted
&&
(
<
div
className=
'
px-4 py-3 leading-[18px] flex items-center text-[13px] text-gray-700 font-medium
bg-[#FFFAEB] border-t border-t-[rgba(0, 0, 0, 0.05)] rounded-b-xl
'
>
{
t
(
'common.provider.anthropicHosted.usedUp'
)
}
</
div
>
)
}
</
div
>
)
}
export
default
AnthropicHostedProvider
web/app/components/header/account-setting/provider-page/anthropic-provider/index.module.css
0 → 100644
View file @
9098d099
web/app/components/header/account-setting/provider-page/anthropic-provider/index.tsx
0 → 100644
View file @
9098d099
import
{
useEffect
,
useState
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
Link
from
'next/link'
import
{
ArrowTopRightOnSquareIcon
}
from
'@heroicons/react/24/outline'
import
ProviderInput
from
'../provider-input'
import
type
{
ValidatedStatusState
}
from
'../provider-input/useValidateToken'
import
useValidateToken
,
{
ValidatedStatus
}
from
'../provider-input/useValidateToken'
import
{
ValidatedErrorIcon
,
ValidatedErrorOnOpenaiTip
,
ValidatedSuccessIcon
,
ValidatingTip
,
}
from
'../provider-input/Validate'
import
type
{
Provider
,
ProviderAnthropicToken
}
from
'@/models/common'
type
AnthropicProviderProps
=
{
provider
:
Provider
onValidatedStatus
:
(
status
?:
ValidatedStatusState
)
=>
void
onTokenChange
:
(
token
:
ProviderAnthropicToken
)
=>
void
}
const
AnthropicProvider
=
({
provider
,
onValidatedStatus
,
onTokenChange
,
}:
AnthropicProviderProps
)
=>
{
const
{
t
}
=
useTranslation
()
const
[
token
,
setToken
]
=
useState
<
ProviderAnthropicToken
>
((
provider
.
token
as
ProviderAnthropicToken
)
||
{
anthropic_api_key
:
''
})
const
[
validating
,
validatedStatus
,
setValidatedStatus
,
validate
]
=
useValidateToken
(
provider
.
provider_name
)
const
handleFocus
=
()
=>
{
if
(
token
.
anthropic_api_key
===
(
provider
.
token
as
ProviderAnthropicToken
).
anthropic_api_key
)
{
setToken
({
anthropic_api_key
:
''
})
onTokenChange
({
anthropic_api_key
:
''
})
setValidatedStatus
({})
}
}
const
handleChange
=
(
v
:
string
)
=>
{
const
apiKey
=
{
anthropic_api_key
:
v
}
setToken
(
apiKey
)
onTokenChange
(
apiKey
)
validate
(
apiKey
,
{
beforeValidating
:
()
=>
{
if
(
!
v
)
{
setValidatedStatus
({})
return
false
}
return
true
},
})
}
useEffect
(()
=>
{
if
(
typeof
onValidatedStatus
===
'function'
)
onValidatedStatus
(
validatedStatus
)
},
[
validatedStatus
])
const
getValidatedIcon
=
()
=>
{
if
(
validatedStatus
?.
status
===
ValidatedStatus
.
Error
||
validatedStatus
.
status
===
ValidatedStatus
.
Exceed
)
return
<
ValidatedErrorIcon
/>
if
(
validatedStatus
.
status
===
ValidatedStatus
.
Success
)
return
<
ValidatedSuccessIcon
/>
}
const
getValidatedTip
=
()
=>
{
if
(
validating
)
return
<
ValidatingTip
/>
if
(
validatedStatus
?.
status
===
ValidatedStatus
.
Error
)
return
<
ValidatedErrorOnOpenaiTip
errorMessage=
{
validatedStatus
.
message
??
''
}
/>
}
return
(
<
div
className=
'px-4 pt-3 pb-4'
>
<
ProviderInput
value=
{
token
.
anthropic_api_key
}
name=
{
t
(
'common.provider.apiKey'
)
}
placeholder=
{
t
(
'common.provider.enterYourKey'
)
}
onChange=
{
handleChange
}
onFocus=
{
handleFocus
}
validatedIcon=
{
getValidatedIcon
()
}
validatedTip=
{
getValidatedTip
()
}
/>
<
Link
className=
"inline-flex items-center mt-3 text-xs font-normal cursor-pointer text-primary-600 w-fit"
href=
"https://docs.anthropic.com/claude/reference/getting-started-with-the-api"
target=
{
'_blank'
}
>
{
t
(
'common.provider.anthropic.keyFrom'
)
}
<
ArrowTopRightOnSquareIcon
className=
'w-3 h-3 ml-1 text-primary-600'
aria
-
hidden=
"true"
/>
</
Link
>
</
div
>
)
}
export
default
AnthropicProvider
web/app/components/header/account-setting/provider-page/index.tsx
View file @
9098d099
...
...
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import
Link
from
'next/link'
import
ProviderItem
from
'./provider-item'
import
OpenaiHostedProvider
from
'./openai-hosted-provider'
import
AnthropicHostedProvider
from
'./anthropic-hosted-provider'
import
type
{
ProviderHosted
}
from
'@/models/common'
import
{
fetchProviders
}
from
'@/service/common'
import
{
IS_CE_EDITION
}
from
'@/config'
...
...
@@ -18,6 +19,10 @@ const providersMap: { [k: string]: any } = {
icon
:
'azure'
,
name
:
'Azure OpenAI Service'
,
},
'anthropic-custom'
:
{
icon
:
'anthropic'
,
name
:
'Anthropic'
,
},
}
// const providersList = [
...
...
@@ -65,6 +70,8 @@ const ProviderPage = () => {
}
})
const
providerHosted
=
data
?.
filter
(
provider
=>
provider
.
provider_name
===
'openai'
&&
provider
.
provider_type
===
'system'
)?.[
0
]
const
anthropicHosted
=
data
?.
filter
(
provider
=>
provider
.
provider_name
===
'anthropic'
&&
provider
.
provider_type
===
'system'
)?.[
0
]
const
providedOpenaiProvider
=
data
?.
find
(
provider
=>
provider
.
is_enabled
&&
(
provider
.
provider_name
===
'openai'
||
provider
.
provider_name
===
'azure_openai'
))
return
(
<
div
className=
'pb-7'
>
...
...
@@ -78,6 +85,16 @@ const ProviderPage = () => {
</>
)
}
{
anthropicHosted
&&
!
IS_CE_EDITION
&&
(
<>
<
div
>
<
AnthropicHostedProvider
provider=
{
anthropicHosted
as
ProviderHosted
}
/>
</
div
>
<
div
className=
'my-5 w-full h-0 border-[0.5px] border-gray-100'
/>
</>
)
}
<
div
>
{
providers
?.
map
(
providerItem
=>
(
...
...
@@ -89,11 +106,12 @@ const ProviderPage = () => {
activeId=
{
activeProviderId
}
onActive=
{
aid
=>
setActiveProviderId
(
aid
)
}
onSave=
{
()
=>
mutate
()
}
providedOpenaiProvider=
{
providedOpenaiProvider
}
/>
))
}
</
div
>
<
div
className=
'
absolute bottom-0 w-full
h-[42px] flex items-center bg-white text-xs text-gray-500'
>
<
div
className=
'
fixed bottom-0 w-[472px]
h-[42px] flex items-center bg-white text-xs text-gray-500'
>
<
LockClosedIcon
className=
'w-3 h-3 mr-1'
/>
{
t
(
'common.provider.encrypted.front'
)
}
<
Link
...
...
web/app/components/header/account-setting/provider-page/provider-item/index.tsx
View file @
9098d099
...
...
@@ -5,14 +5,20 @@ import { useTranslation } from 'react-i18next'
import
Indicator
from
'../../../indicator'
import
OpenaiProvider
from
'../openai-provider'
import
AzureProvider
from
'../azure-provider'
import
AnthropicProvider
from
'../anthropic-provider'
import
type
{
ValidatedStatusState
}
from
'../provider-input/useValidateToken'
import
{
ValidatedStatus
}
from
'../provider-input/useValidateToken'
import
s
from
'./index.module.css'
import
type
{
Provider
,
ProviderAzureToken
}
from
'@/models/common'
import
type
{
Provider
,
ProviderA
nthropicToken
,
ProviderA
zureToken
}
from
'@/models/common'
import
{
ProviderName
}
from
'@/models/common'
import
{
updateProviderAIKey
}
from
'@/service/common'
import
{
ToastContext
}
from
'@/app/components/base/toast'
import
Tooltip
from
'@/app/components/base/tooltip'
const
providerNameMap
:
Record
<
string
,
string
>
=
{
openai
:
'OpenAI'
,
azure_openai
:
'Azure OpenAI Service'
,
}
type
IProviderItemProps
=
{
icon
:
string
name
:
string
...
...
@@ -20,6 +26,7 @@ type IProviderItemProps = {
activeId
:
string
onActive
:
(
v
:
string
)
=>
void
onSave
:
()
=>
void
providedOpenaiProvider
?:
Provider
}
const
ProviderItem
=
({
activeId
,
...
...
@@ -28,14 +35,17 @@ const ProviderItem = ({
provider
,
onActive
,
onSave
,
providedOpenaiProvider
,
}:
IProviderItemProps
)
=>
{
const
{
t
}
=
useTranslation
()
const
[
validatedStatus
,
setValidatedStatus
]
=
useState
<
ValidatedStatusState
>
()
const
[
loading
,
setLoading
]
=
useState
(
false
)
const
{
notify
}
=
useContext
(
ToastContext
)
const
[
token
,
setToken
]
=
useState
<
ProviderAzureToken
|
string
>
(
const
[
token
,
setToken
]
=
useState
<
ProviderAzureToken
|
string
|
ProviderAnthropicToken
>
(
provider
.
provider_name
===
'azure_openai'
?
{
openai_api_base
:
''
,
openai_api_key
:
''
}
:
provider
.
provider_name
===
'anthropic'
?
{
anthropic_api_key
:
''
}
:
''
,
)
const
id
=
`
${
provider
.
provider_name
}
-
${
provider
.
provider_type
}
`
...
...
@@ -54,6 +64,8 @@ const ProviderItem = ({
}
if
(
provider
.
provider_name
===
ProviderName
.
OPENAI
)
return
provider
.
token
if
(
provider
.
provider_name
===
ProviderName
.
ANTHROPIC
)
return
provider
.
token
?.
anthropic_api_key
}
const
handleUpdateToken
=
async
()
=>
{
if
(
loading
)
...
...
@@ -81,7 +93,7 @@ const ProviderItem = ({
<
div
className=
{
cn
(
s
[
`icon-${icon}`
],
'mr-3 w-6 h-6 rounded-md'
)
}
/>
<
div
className=
'grow text-sm font-medium text-gray-800'
>
{
name
}
</
div
>
{
providerTokenHasSetted
()
&&
!
comingSoon
&&
!
isOpen
&&
(
providerTokenHasSetted
()
&&
!
comingSoon
&&
!
isOpen
&&
provider
.
provider_name
!==
ProviderName
.
ANTHROPIC
&&
(
<
div
className=
'flex items-center mr-4'
>
{
!
isValid
&&
<
div
className=
'text-xs text-[#D92D20]'
>
{
t
(
'common.provider.invalidApiKey'
)
}
</
div
>
}
<
Indicator
color=
{
!
isValid
?
'red'
:
'green'
}
className=
'ml-2'
/>
...
...
@@ -89,7 +101,27 @@ const ProviderItem = ({
)
}
{
!
comingSoon
&&
!
isOpen
&&
(
(
providerTokenHasSetted
()
&&
!
comingSoon
&&
!
isOpen
&&
provider
.
provider_name
===
ProviderName
.
ANTHROPIC
)
&&
(
<
div
className=
'flex items-center mr-4'
>
{
providedOpenaiProvider
?.
is_valid
?
!
isValid
?
<
div
className=
'text-xs text-[#D92D20]'
>
{
t
(
'common.provider.invalidApiKey'
)
}
</
div
>
:
null
:
<
div
className=
'text-xs text-[#DC6803]'
>
{
t
(
'common.provider.anthropic.notEnabled'
)
}
</
div
>
}
<
Indicator
color=
{
providedOpenaiProvider
?.
is_valid
?
isValid
?
'green'
:
'red'
:
'yellow'
}
className=
'ml-2'
/>
</
div
>
)
}
{
!
comingSoon
&&
!
isOpen
&&
provider
.
provider_name
!==
ProviderName
.
ANTHROPIC
&&
(
<
div
className=
'
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
text-xs font-medium text-gray-700 flex items-center
...
...
@@ -98,6 +130,34 @@ const ProviderItem = ({
</
div
>
)
}
{
(
!
comingSoon
&&
!
isOpen
&&
provider
.
provider_name
===
ProviderName
.
ANTHROPIC
)
?
providedOpenaiProvider
?.
is_enabled
?
(
<
div
className=
'
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
text-xs font-medium text-gray-700 flex items-center
'
onClick=
{
()
=>
providedOpenaiProvider
.
is_valid
&&
onActive
(
id
)
}
>
{
providerTokenHasSetted
()
?
t
(
'common.provider.editKey'
)
:
t
(
'common.provider.addKey'
)
}
</
div
>
)
:
(
<
Tooltip
htmlContent=
{
<
div
className=
'w-[320px]'
>
{
t
(
'common.provider.anthropic.enableTip'
)
}
</
div
>
}
position=
'bottom'
selector=
'anthropic-provider-enable-top-tooltip'
>
<
div
className=
'
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-not-allowed
text-xs font-medium text-gray-700 flex items-center opacity-50
'
>
{
t
(
'common.provider.addKey'
)
}
</
div
>
</
Tooltip
>
)
:
null
}
{
comingSoon
&&
!
isOpen
&&
(
<
div
className=
'
...
...
@@ -147,6 +207,29 @@ const ProviderItem = ({
/>
)
}
{
provider
.
provider_name
===
ProviderName
.
ANTHROPIC
&&
isOpen
&&
(
<
AnthropicProvider
provider=
{
provider
}
onValidatedStatus=
{
v
=>
setValidatedStatus
(
v
)
}
onTokenChange=
{
v
=>
setToken
(
v
)
}
/>
)
}
{
provider
.
provider_name
===
ProviderName
.
ANTHROPIC
&&
!
isOpen
&&
providerTokenHasSetted
()
&&
providedOpenaiProvider
?.
is_valid
&&
(
<
div
className=
'px-4 py-3 text-[13px] font-medium text-gray-700'
>
{
t
(
'common.provider.anthropic.using'
)
}
{
providerNameMap
[
providedOpenaiProvider
.
provider_name
as
string
]
}
</
div
>
)
}
{
provider
.
provider_name
===
ProviderName
.
ANTHROPIC
&&
!
isOpen
&&
providerTokenHasSetted
()
&&
!
providedOpenaiProvider
?.
is_valid
&&
(
<
div
className=
'px-4 py-3 bg-[#FFFAEB] text-[13px] font-medium text-gray-700'
>
{
t
(
'common.provider.anthropic.enableTip'
)
}
</
div
>
)
}
</
div
>
)
}
...
...
web/app/components/share/chat/index.tsx
View file @
9098d099
...
...
@@ -622,7 +622,7 @@ const Main: FC<IMainProps> = ({
{
hasSetInputs
&&
(
<
div
className=
{
cn
(
doShowSuggestion
?
'pb-[140px]'
:
(
isResponsing
?
'pb-[113px]'
:
'pb-[
6
6px]'
),
'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden'
)
}
>
<
div
className=
{
cn
(
doShowSuggestion
?
'pb-[140px]'
:
(
isResponsing
?
'pb-[113px]'
:
'pb-[
7
6px]'
),
'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden'
)
}
>
<
div
className=
'h-full overflow-y-auto'
ref=
{
chatListDomRef
}
>
<
Chat
chatList=
{
chatList
}
...
...
web/app/components/share/chatbot/icons/dify-header.svg
0 → 100644
View file @
9098d099
<svg
width=
"29"
height=
"28"
viewBox=
"0 0 29 28"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
xmlns:xlink=
"http://www.w3.org/1999/xlink"
>
<g
filter=
"url(#filter0_d_613_54650)"
>
<rect
x=
"2.5"
y=
"1"
width=
"24"
height=
"24"
rx=
"6"
fill=
"white"
/>
<rect
x=
"2.5"
y=
"1"
width=
"24"
height=
"24"
rx=
"6"
fill=
"url(#pattern0)"
/>
<rect
x=
"2.75"
y=
"1.25"
width=
"23.5"
height=
"23.5"
rx=
"5.75"
stroke=
"black"
stroke-opacity=
"0.05"
stroke-width=
"0.5"
/>
</g>
<defs>
<filter
id=
"filter0_d_613_54650"
x=
"0.5"
y=
"0"
width=
"28"
height=
"28"
filterUnits=
"userSpaceOnUse"
color-interpolation-filters=
"sRGB"
>
<feFlood
flood-opacity=
"0"
result=
"BackgroundImageFix"
/>
<feColorMatrix
in=
"SourceAlpha"
type=
"matrix"
values=
"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
result=
"hardAlpha"
/>
<feOffset
dy=
"1"
/>
<feGaussianBlur
stdDeviation=
"1"
/>
<feColorMatrix
type=
"matrix"
values=
"0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"
/>
<feBlend
mode=
"normal"
in2=
"BackgroundImageFix"
result=
"effect1_dropShadow_613_54650"
/>
<feBlend
mode=
"normal"
in=
"SourceGraphic"
in2=
"effect1_dropShadow_613_54650"
result=
"shape"
/>
</filter>
<pattern
id=
"pattern0"
patternContentUnits=
"objectBoundingBox"
width=
"1"
height=
"1"
>
<use
xlink:href=
"#image0_613_54650"
transform=
"scale(0.00195312)"
/>
</pattern>
<image
id=
"image0_613_54650"
width=
"512"
height=
"512"
xlink:href=
"data:image/png;base64,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BX2WCziF/gjHGD0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgy8L/Hz2FvoWT6p6fAAAAAElFTkSuQmCC"
/>
</defs>
</svg>
web/app/components/share/chatbot/icons/dify.svg
0 → 100644
View file @
9098d099
<svg
width=
"40"
height=
"40"
viewBox=
"0 0 40 40"
fill=
"none"
xmlns=
"http://www.w3.org/2000/svg"
xmlns:xlink=
"http://www.w3.org/1999/xlink"
>
<rect
width=
"40"
height=
"40"
rx=
"20"
fill=
"white"
/>
<rect
width=
"40"
height=
"40"
rx=
"20"
fill=
"url(#pattern0)"
/>
<rect
x=
"0.5"
y=
"0.5"
width=
"39"
height=
"39"
rx=
"19.5"
stroke=
"black"
stroke-opacity=
"0.05"
/>
<defs>
<pattern
id=
"pattern0"
patternContentUnits=
"objectBoundingBox"
width=
"1"
height=
"1"
>
<use
xlink:href=
"#image0_871_11434"
transform=
"scale(0.00195312)"
/>
</pattern>
<image
id=
"image0_871_11434"
width=
"512"
height=
"512"
xlink:href=
"data:image/png;base64,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BX2WCziF/gjHGD0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgy8L/Hz2FvoWT6p6fAAAAAElFTkSuQmCC"
/>
</defs>
</svg>
web/app/components/share/chatbot/index.tsx
View file @
9098d099
...
...
@@ -552,6 +552,10 @@ const Main: FC<IMainProps> = ({
)
}
const
difyIcon
=
(
<
div
className=
{
s
.
difyHeader
}
></
div
>
)
if
(
appUnavailable
)
return
<
AppUnavailable
isUnknwonReason=
{
isUnknwonReason
}
/>
...
...
@@ -562,7 +566,8 @@ const Main: FC<IMainProps> = ({
<
div
>
<
Header
title=
{
siteInfo
.
title
}
icon=
{
siteInfo
.
icon
||
''
}
icon=
''
customerIcon=
{
difyIcon
}
icon_background=
{
siteInfo
.
icon_background
}
isEmbedScene=
{
true
}
isMobile=
{
isMobile
}
...
...
@@ -604,7 +609,7 @@ const Main: FC<IMainProps> = ({
{
hasSetInputs
&&
(
<
div
className=
{
cn
(
doShowSuggestion
?
'pb-[140px]'
:
(
isResponsing
?
'pb-[113px]'
:
'pb-[
6
6px]'
),
'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden'
)
}
>
<
div
className=
{
cn
(
doShowSuggestion
?
'pb-[140px]'
:
(
isResponsing
?
'pb-[113px]'
:
'pb-[
7
6px]'
),
'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden'
)
}
>
<
div
className=
'h-full overflow-y-auto'
ref=
{
chatListDomRef
}
>
<
Chat
chatList=
{
chatList
}
...
...
@@ -624,6 +629,7 @@ const Main: FC<IMainProps> = ({
suggestionList=
{
suggestQuestions
}
displayScene=
'web'
isShowSpeechToText=
{
speechToTextConfig
?.
enabled
}
answerIconClassName=
{
s
.
difyIcon
}
/>
</
div
>
</
div
>)
...
...
web/app/components/share/chatbot/style.module.css
View file @
9098d099
.installedApp
{
height
:
calc
(
100vh
-
74px
);
}
.difyIcon
{
background-image
:
url(./icons/dify.svg)
;
}
.difyHeader
{
width
:
24px
;
height
:
24px
;
background
:
url(./icons/dify-header.svg)
center
center
no-repeat
;
background-size
:
contain
;
}
\ No newline at end of file
web/app/components/share/chatbot/welcome/index.tsx
View file @
9098d099
...
...
@@ -307,7 +307,7 @@ const Welcome: FC<IWelcomeProps> = ({
}
return (
<
div
className=
'relative
mobile:min-h-[48px]
tablet:min-h-[64px]'
>
<
div
className=
'relative tablet:min-h-[64px]'
>
{
/* {hasSetInputs && renderHeader()} */
}
<
div
className=
'mx-auto pc:w-[794px] max-w-full mobile:w-full px-3.5'
>
{
/* Has't set inputs */
}
...
...
web/app/components/share/header.tsx
View file @
9098d099
...
...
@@ -3,6 +3,7 @@ import React from 'react'
import
AppIcon
from
'@/app/components/base/app-icon'
export
type
IHeaderProps
=
{
title
:
string
customerIcon
?:
React
.
ReactNode
icon
:
string
icon_background
:
string
isMobile
?:
boolean
...
...
@@ -11,6 +12,7 @@ export type IHeaderProps = {
const
Header
:
FC
<
IHeaderProps
>
=
({
title
,
isMobile
,
customerIcon
,
icon
,
icon_background
,
isEmbedScene
=
false
,
...
...
@@ -25,7 +27,7 @@ const Header: FC<IHeaderProps> = ({
>
<
div
></
div
>
<
div
className=
"flex items-center space-x-2"
>
<
AppIcon
size=
"small"
icon=
{
icon
}
background=
{
icon_background
}
/>
{
customerIcon
||
<
AppIcon
size=
"small"
icon=
{
icon
}
background=
{
icon_background
}
/>
}
<
div
className=
{
`text-sm text-gray-800 font-bold ${
isEmbedScene ? 'text-white' : ''
...
...
web/app/install/installForm.tsx
View file @
9098d099
'use client'
import
React
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
Button
from
'@/app/components/base/button'
import
Link
from
'next/link'
import
{
useRouter
}
from
'next/navigation'
import
Toast
from
'../components/base/toast'
import
Button
from
'@/app/components/base/button'
import
{
setup
}
from
'@/service/common'
const
validEmailReg
=
/^
[\w\.
-
]
+@
([\w
-
]
+
\.)
+
[\w
-
]{2,}
$/
...
...
@@ -40,36 +40,37 @@ const InstallForm = () => {
showErrorMessage
(
t
(
'login.error.passwordEmpty'
))
return
false
}
if
(
!
validPassword
.
test
(
password
))
{
if
(
!
validPassword
.
test
(
password
))
showErrorMessage
(
t
(
'login.error.passwordInvalid'
))
}
return
true
}
const
handleSetting
=
async
()
=>
{
if
(
!
valid
())
return
if
(
!
valid
())
return
await
setup
({
body
:
{
email
,
name
,
password
}
password
,
}
,
})
router
.
push
(
'/signin'
)
}
return
(
<>
<
div
className=
"sm:mx-auto sm:w-full sm:max-w-md"
>
<
h2
className=
"text-
3xl font-normal
text-gray-900"
>
{
t
(
'login.setAdminAccount'
)
}
</
h2
>
<
h2
className=
"text-
[32px] font-bold
text-gray-900"
>
{
t
(
'login.setAdminAccount'
)
}
</
h2
>
<
p
className=
'
mt-
2
text-sm text-gray-600
mt-
1
text-sm text-gray-600
'
>
{
t
(
'login.setAdminAccountDesc'
)
}
</
p
>
</
div
>
<
div
className=
"grow mt-8 sm:mx-auto sm:w-full sm:max-w-md"
>
<
div
className=
"bg-white "
>
<
form
className=
"space-y-6"
onSubmit=
{
()
=>
{
}
}
>
<
div
>
<
label
htmlFor=
"email"
className=
"
block text-sm font-medium text-gray-7
00"
>
<
form
onSubmit=
{
()
=>
{
}
}
>
<
div
className=
'mb-5'
>
<
label
htmlFor=
"email"
className=
"
my-2 flex items-center justify-between text-sm font-medium text-gray-9
00"
>
{
t
(
'login.email'
)
}
</
label
>
<
div
className=
"mt-1"
>
...
...
@@ -78,13 +79,14 @@ const InstallForm = () => {
type=
"email"
value=
{
email
}
onChange=
{
e
=>
setEmail
(
e
.
target
.
value
)
}
className=
{
'appearance-none block w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md shadow-sm placeholder-gray-400 sm:text-sm'
}
placeholder=
{
t
(
'login.emailPlaceholder'
)
||
''
}
className=
{
'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm'
}
/>
</
div
>
</
div
>
<
div
>
<
label
htmlFor=
"name"
className=
"
block text-sm font-medium text-gray-7
00"
>
<
div
className=
'mb-5'
>
<
label
htmlFor=
"name"
className=
"
my-2 flex items-center justify-between text-sm font-medium text-gray-9
00"
>
{
t
(
'login.name'
)
}
</
label
>
<
div
className=
"mt-1 relative rounded-md shadow-sm"
>
...
...
@@ -93,13 +95,14 @@ const InstallForm = () => {
type=
"text"
value=
{
name
}
onChange=
{
e
=>
setName
(
e
.
target
.
value
)
}
className=
{
'appearance-none block w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md shadow-sm placeholder-gray-400 sm:text-sm pr-10'
}
placeholder=
{
t
(
'login.namePlaceholder'
)
||
''
}
className=
{
'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'
}
/>
</
div
>
</
div
>
<
div
>
<
label
htmlFor=
"password"
className=
"block text-sm font-medium text-gray-700"
>
<
div
className=
'mb-5'
>
<
label
htmlFor=
"password"
className=
"my-2 flex items-center justify-between text-sm font-medium text-gray-900"
>
{
t
(
'login.password'
)
}
</
label
>
<
div
className=
"mt-1 relative rounded-md shadow-sm"
>
...
...
@@ -108,7 +111,8 @@ const InstallForm = () => {
type=
'password'
value=
{
password
}
onChange=
{
e
=>
setPassword
(
e
.
target
.
value
)
}
className=
{
'appearance-none block w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md shadow-sm placeholder-gray-400 sm:text-sm pr-10'
}
placeholder=
{
t
(
'login.passwordPlaceholder'
)
||
''
}
className=
{
'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'
}
/>
</
div
>
<
div
className=
'mt-1 text-xs text-gray-500'
>
{
t
(
'login.error.passwordInvalid'
)
}
</
div
>
...
...
@@ -123,29 +127,21 @@ const InstallForm = () => {
</div>
</div>
</div> */
}
{
/* agree to our Terms and Privacy Policy. */
}
<
div
className=
"block mt-6 text-xs text-gray-600"
>
{
t
(
'login.tosDesc'
)
}
<
Link
className=
'text-primary-600'
target=
{
'_blank'
}
href=
'https://docs.dify.ai/user-agreement/terms-of-service'
>
{
t
(
'login.tos'
)
}
</
Link
>
&
<
Link
className=
'text-primary-600'
target=
{
'_blank'
}
href=
'https://langgenius.ai/privacy-policy'
>
{
t
(
'login.pp'
)
}
</
Link
>
</
div
>
<
div
>
<
Button
type=
'primary'
onClick=
{
handleSetting
}
>
<
Button
type=
'primary'
className=
'w-full !fone-medium !text-sm'
onClick=
{
handleSetting
}
>
{
t
(
'login.installBtn'
)
}
</
Button
>
</
div
>
</
form
>
<
div
className=
"block w-hull mt-2 text-xs text-gray-600"
>
{
t
(
'login.license.tip'
)
}
<
Link
className=
'text-primary-600'
target=
{
'_blank'
}
href=
'https://docs.dify.ai/community/open-source'
>
{
t
(
'login.license.link'
)
}
</
Link
>
</
div
>
</
div
>
</
div
>
</>
...
...
web/app/layout.tsx
View file @
9098d099
import
I18nServer
from
'./components/i18n-server'
import
BrowerInitor
from
'./components/browser-initor'
import
SentryInitor
from
'./components/sentry-initor'
import
{
getLocaleOnServer
}
from
'@/i18n/server'
...
...
@@ -25,10 +26,12 @@ const LocaleLayout = ({
data
-
public
-
edition=
{
process
.
env
.
NEXT_PUBLIC_EDITION
}
data
-
public
-
sentry
-
dsn=
{
process
.
env
.
NEXT_PUBLIC_SENTRY_DSN
}
>
<
BrowerInitor
>
<
SentryInitor
>
{
/* @ts-expect-error Async Server Component */
}
<
I18nServer
locale=
{
locale
}
>
{
children
}
</
I18nServer
>
</
SentryInitor
>
</
BrowerInitor
>
</
body
>
</
html
>
)
...
...
web/app/signin/_header.tsx
View file @
9098d099
'use client'
import
React
from
'react'
import
{
useContext
}
from
'use-context-selector'
import
style
from
'./page.module.css'
import
Select
,
{
LOCALES
}
from
'@/app/components/base/select/locale'
import
{
type
Locale
}
from
'@/i18n'
import
I18n
from
'@/context/i18n'
import
{
setLocaleOnClient
}
from
'@/i18n/client'
import
{
useContext
}
from
'use-context-selector'
type
IHeaderProps
=
{
locale
:
string
}
const
Header
=
()
=>
{
const
{
locale
,
setLocaleOnClient
}
=
useContext
(
I18n
)
...
...
web/app/signin/forms.tsx
View file @
9098d099
...
...
@@ -2,9 +2,9 @@
import
React
from
'react'
import
{
useSearchParams
}
from
'next/navigation'
import
cn
from
'classnames'
import
NormalForm
from
'./normalForm'
import
OneMoreStep
from
'./oneMoreStep'
import
classNames
from
'classnames'
const
Forms
=
()
=>
{
const
searchParams
=
useSearchParams
()
...
...
@@ -19,7 +19,7 @@ const Forms = () => {
}
}
return
<
div
className=
{
c
lassNames
(
c
n
(
'flex flex-col items-center w-full grow items-center justify-center'
,
'px-6'
,
'md:px-[108px]'
,
...
...
@@ -28,7 +28,6 @@ const Forms = () => {
<
div
className=
'flex flex-col md:w-[400px]'
>
{
getForm
()
}
</
div
>
</
div
>
}
...
...
web/app/signin/normalForm.tsx
View file @
9098d099
...
...
@@ -2,16 +2,15 @@
import
React
,
{
useEffect
,
useReducer
,
useState
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
{
useRouter
}
from
'next/navigation'
import
{
IS_CE_EDITION
}
from
'@/config'
import
classNames
from
'classnames'
import
useSWR
from
'swr'
import
Link
from
'next/link'
import
Toast
from
'../components/base/toast'
import
style
from
'./page.module.css'
// import Tooltip from '@/app/components/base/tooltip/index'
import
Toast
from
'../components/base/toast
'
import
{
IS_CE_EDITION
,
apiPrefix
}
from
'@/config
'
import
Button
from
'@/app/components/base/button'
import
{
login
,
oauth
}
from
'@/service/common'
import
{
apiPrefix
}
from
'@/config'
const
validEmailReg
=
/^
[\w\.
-
]
+@
([\w
-
]
+
\.)
+
[\w
-
]{2,}
$/
...
...
@@ -91,8 +90,9 @@ const NormalForm = () => {
remember_me
:
true
,
},
})
router
.
push
(
'/'
)
}
finally
{
router
.
push
(
'/apps'
)
}
finally
{
setIsLoading
(
false
)
}
}
...
...
@@ -132,8 +132,8 @@ const NormalForm = () => {
return
(
<>
<
div
className=
"w-full mx-auto"
>
<
h2
className=
"text-
3xl font-normal
text-gray-900"
>
{
t
(
'login.pageTitle'
)
}
</
h2
>
<
p
className=
'mt-
2 text-sm text-gray-600
'
>
{
t
(
'login.welcome'
)
}
</
p
>
<
h2
className=
"text-
[32px] font-bold
text-gray-900"
>
{
t
(
'login.pageTitle'
)
}
</
h2
>
<
p
className=
'mt-
1 text-sm text-gray-600
'
>
{
t
(
'login.welcome'
)
}
</
p
>
</
div
>
<
div
className=
"w-full mx-auto mt-8"
>
...
...
@@ -145,7 +145,7 @@ const NormalForm = () => {
<
Button
type=
'default'
disabled=
{
isLoading
}
className=
'w-full'
className=
'w-full
hover:!bg-gray-50 !text-sm !font-medium
'
>
<>
<
span
className=
{
...
...
@@ -154,7 +154,7 @@ const NormalForm = () => {
'w-5 h-5 mr-2'
,
)
}
/>
<
span
className=
"truncate"
>
{
t
(
'login.withGitHub'
)
}
</
span
>
<
span
className=
"truncate
text-gray-800
"
>
{
t
(
'login.withGitHub'
)
}
</
span
>
</>
</
Button
>
</
a
>
...
...
@@ -164,7 +164,7 @@ const NormalForm = () => {
<
Button
type=
'default'
disabled=
{
isLoading
}
className=
'w-full'
className=
'w-full
hover:!bg-gray-50 !text-sm !font-medium
'
>
<>
<
span
className=
{
...
...
@@ -173,7 +173,7 @@ const NormalForm = () => {
'w-5 h-5 mr-2'
,
)
}
/>
<
span
className=
"truncate"
>
{
t
(
'login.withGoogle'
)
}
</
span
>
<
span
className=
"truncate
text-gray-800
"
>
{
t
(
'login.withGoogle'
)
}
</
span
>
</>
</
Button
>
</
a
>
...
...
@@ -192,9 +192,9 @@ const NormalForm = () => {
</div>
</div> */
}
<
form
className=
"space-y-6"
onSubmit=
{
()
=>
{
}
}
>
<
div
>
<
label
htmlFor=
"email"
className=
"
block text-sm font-medium text-gray-7
00"
>
<
form
onSubmit=
{
()
=>
{
}
}
>
<
div
className=
'mb-5'
>
<
label
htmlFor=
"email"
className=
"
my-2 block text-sm font-medium text-gray-9
00"
>
{
t
(
'login.email'
)
}
</
label
>
<
div
className=
"mt-1"
>
...
...
@@ -204,13 +204,14 @@ const NormalForm = () => {
id=
"email"
type=
"email"
autoComplete=
"email"
className=
{
'appearance-none block w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-primary-500 focus:border-primary-500 rounded-md shadow-sm placeholder-gray-400 sm:text-sm'
}
placeholder=
{
t
(
'login.emailPlaceholder'
)
||
''
}
className=
{
'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm'
}
/>
</
div
>
</
div
>
<
div
>
<
label
htmlFor=
"password"
className=
"
flex items-center justify-between text-sm font-medium text-gray-7
00"
>
<
div
className=
'mb-4'
>
<
label
htmlFor=
"password"
className=
"
my-2 flex items-center justify-between text-sm font-medium text-gray-9
00"
>
<
span
>
{
t
(
'login.password'
)
}
</
span
>
{
/* <Tooltip
selector='forget-password'
...
...
@@ -235,10 +236,8 @@ const NormalForm = () => {
onChange=
{
e
=>
setPassword
(
e
.
target
.
value
)
}
type=
{
showPassword
?
'text'
:
'password'
}
autoComplete=
"current-password"
className=
{
`appearance-none block w-full px-3 py-2
border border-gray-300
focus:outline-none focus:ring-indigo-500 focus:border-indigo-500
rounded-md shadow-sm placeholder-gray-400 sm:text-sm pr-10`
}
placeholder=
{
t
(
'login.passwordPlaceholder'
)
||
''
}
className=
{
'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'
}
/>
<
div
className=
"absolute inset-y-0 right-0 flex items-center pr-3"
>
<
button
...
...
@@ -252,18 +251,19 @@ const NormalForm = () => {
</
div
>
</
div
>
<
div
>
<
div
className=
'mb-2'
>
<
Button
type=
'primary'
onClick=
{
handleEmailPasswordLogin
}
disabled=
{
isLoading
}
className=
"w-full !fone-medium !text-sm"
>
{
t
(
'login.signBtn'
)
}
</
Button
>
</
div
>
</
form
>
</>
}
{
/* agree to our Terms and Privacy Policy. */
}
<
div
className=
"
block mt-6
text-xs text-gray-600"
>
<
div
className=
"
w-hull text-center block mt-2
text-xs text-gray-600"
>
{
t
(
'login.tosDesc'
)
}
<
Link
...
...
web/app/signin/oneMoreStep.tsx
View file @
9098d099
'use client'
import
React
,
{
useEffect
,
useReducer
}
from
'react'
import
{
useTranslation
}
from
'react-i18next'
import
Link
from
'next/link'
import
useSWR
from
'swr'
import
{
useRouter
}
from
'next/navigation'
import
Button
from
'@/app/components/base/button'
...
...
@@ -74,14 +75,14 @@ const OneMoreStep = () => {
return
(
<>
<
div
className=
"w-full mx-auto"
>
<
h2
className=
"text-
3xl font-normal
text-gray-900"
>
{
t
(
'login.oneMoreStep'
)
}
</
h2
>
<
p
className=
'mt-
2
text-sm text-gray-600 '
>
{
t
(
'login.createSample'
)
}
</
p
>
<
h2
className=
"text-
[32px] font-bold
text-gray-900"
>
{
t
(
'login.oneMoreStep'
)
}
</
h2
>
<
p
className=
'mt-
1
text-sm text-gray-600 '
>
{
t
(
'login.createSample'
)
}
</
p
>
</
div
>
<
div
className=
"w-full mx-auto mt-
8
"
>
<
div
className=
"
space-y-6
bg-white"
>
<
div
className=
""
>
<
label
className=
"flex items-center justify-between text-sm font-medium text-gray-900"
>
<
div
className=
"w-full mx-auto mt-
6
"
>
<
div
className=
"bg-white"
>
<
div
className=
"
mb-5
"
>
<
label
className=
"
my-2
flex items-center justify-between text-sm font-medium text-gray-900"
>
{
t
(
'login.invitationCode'
)
}
<
Tooltip
clickable
...
...
@@ -103,16 +104,16 @@ const OneMoreStep = () => {
id=
"invitation_code"
value=
{
state
.
invitation_code
}
type=
"text"
className=
{
'appearance-none block w-full px-3 py-2 border border-gray-300 focus:outline-none focus:ring-primary-600 focus:border-primary-600 rounded-md shadow-sm placeholder-gray-400 sm:text-sm'
}
placeholder=
{
t
(
'login.invitationCodePlaceholder'
)
||
''
}
className=
{
'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm'
}
onChange=
{
(
e
)
=>
{
dispatch
({
type
:
'invitation_code'
,
value
:
e
.
target
.
value
.
trim
()
})
}
}
/>
</
div
>
</
div
>
<
div
>
<
label
htmlFor=
"name"
className=
"block text-sm font-medium text-gray-700"
>
<
div
className=
'mb-5'
>
<
label
htmlFor=
"name"
className=
"my-2 flex items-center justify-between text-sm font-medium text-gray-900"
>
{
t
(
'login.interfaceLanguage'
)
}
</
label
>
<
div
className=
"relative mt-1 rounded-md shadow-sm"
>
...
...
@@ -125,8 +126,7 @@ const OneMoreStep = () => {
/>
</
div
>
</
div
>
<
div
>
<
div
className=
'mb-4'
>
<
label
htmlFor=
"timezone"
className=
"block text-sm font-medium text-gray-700"
>
{
t
(
'login.timezone'
)
}
</
label
>
...
...
@@ -143,6 +143,7 @@ const OneMoreStep = () => {
<
div
>
<
Button
type=
'primary'
className=
'w-full !fone-medium !text-sm'
disabled=
{
state
.
formState
===
'processing'
}
onClick=
{
()
=>
{
dispatch
({
type
:
'formState'
,
value
:
'processing'
})
...
...
@@ -151,6 +152,15 @@ const OneMoreStep = () => {
{
t
(
'login.go'
)
}
</
Button
>
</
div
>
<
div
className=
"block w-hull mt-2 text-xs text-gray-600"
>
{
t
(
'login.license.tip'
)
}
<
Link
className=
'text-primary-600'
target=
{
'_blank'
}
href=
'https://docs.dify.ai/community/open-source'
>
{
t
(
'login.license.link'
)
}
</
Link
>
</
div
>
</
div
>
</
div
>
</>
...
...
web/app/signin/page.tsx
View file @
9098d099
import
React
from
'react'
import
cn
from
'classnames'
import
Forms
from
'./forms'
import
Header
from
'./_header'
import
style
from
'./page.module.css'
import
classNames
from
'classnames'
const
SignIn
=
()
=>
{
return
(
<>
<
div
className=
{
c
lassNames
(
<
div
className=
{
c
n
(
style
.
background
,
'flex w-full min-h-screen'
,
'sm:p-4 lg:p-8'
,
'gap-x-20'
,
'justify-center lg:justify-start'
'justify-center lg:justify-start'
,
)
}
>
<
div
className=
{
c
lassNames
(
c
n
(
'flex w-full flex-col bg-white shadow rounded-2xl shrink-0'
,
'space-between'
'space-between'
,
)
}
>
<
Header
/>
...
...
web/context/app-context.tsx
View file @
9098d099
...
...
@@ -37,6 +37,8 @@ const AppContext = createContext<AppContextValue>({
id
:
''
,
name
:
''
,
email
:
''
,
avatar
:
''
,
is_password_set
:
false
,
},
mutateUserProfile
:
()
=>
{
},
pageContainerRef
:
createRef
(),
...
...
web/docker/entrypoint.sh
View file @
9098d099
...
...
@@ -4,8 +4,19 @@ set -e
export
NEXT_PUBLIC_DEPLOY_ENV
=
${
DEPLOY_ENV
}
export
NEXT_PUBLIC_EDITION
=
${
EDITION
}
export
NEXT_PUBLIC_API_PREFIX
=
${
CONSOLE_URL
}
/console/api
export
NEXT_PUBLIC_PUBLIC_API_PREFIX
=
${
APP_URL
}
/api
if
[[
-z
"
$CONSOLE_URL
"
]]
;
then
export
NEXT_PUBLIC_API_PREFIX
=
${
CONSOLE_API_URL
}
/console/api
else
export
NEXT_PUBLIC_API_PREFIX
=
${
CONSOLE_URL
}
/console/api
fi
if
[[
-z
"
$APP_URL
"
]]
;
then
export
NEXT_PUBLIC_PUBLIC_API_PREFIX
=
${
APP_API_URL
}
/api
else
export
NEXT_PUBLIC_PUBLIC_API_PREFIX
=
${
APP_URL
}
/api
fi
export
NEXT_PUBLIC_SENTRY_DSN
=
${
SENTRY_DSN
}
/usr/local/bin/pm2
-v
...
...
web/i18n/lang/common.en.ts
View file @
9098d099
...
...
@@ -14,6 +14,7 @@ const translation = {
edit
:
'Edit'
,
add
:
'Add'
,
refresh
:
'Restart'
,
reset
:
'Reset'
,
search
:
'Search'
,
change
:
'Change'
,
remove
:
'Remove'
,
...
...
@@ -53,7 +54,7 @@ const translation = {
maxTokenTip
:
'Max tokens depending on the model. Prompt and completion share this limit. One token is roughly 1 English character.'
,
maxTokenSettingTip
:
'Your max token setting is high, potentially limiting space for prompts, queries, and data. Consider setting it below 2/3.'
,
setToCurrentModelMaxTokenTip
:
'Max token is updated to the maximum token of the current model
4,000
.'
,
setToCurrentModelMaxTokenTip
:
'Max token is updated to the maximum token of the current model
{{maxToken}}
.'
,
},
tone
:
{
Creative
:
'Creative'
,
...
...
@@ -96,6 +97,14 @@ const translation = {
avatar
:
'Avatar'
,
name
:
'Name'
,
email
:
'Email'
,
password
:
'Password'
,
passwordTip
:
'You can set a permanent password if you don’t want to use temporary login codes'
,
setPassword
:
'Set a password'
,
resetPassword
:
'Reset password'
,
currentPassword
:
'Current password'
,
newPassword
:
'New password'
,
confirmPassword
:
'Confirm password'
,
notEqual
:
'Two passwords are different.'
,
langGeniusAccount
:
'Dify account'
,
langGeniusAccountTip
:
'Your Dify account and associated user data.'
,
editName
:
'Edit Name'
,
...
...
@@ -112,15 +121,16 @@ const translation = {
admin
:
'Admin'
,
adminTip
:
'Can build apps & manage team settings'
,
normal
:
'Normal'
,
normalTip
:
'Only can use apps
,
can not build apps'
,
normalTip
:
'Only can use apps
,
can not build apps'
,
inviteTeamMember
:
'Add team member'
,
inviteTeamMemberTip
:
'They can access your team data directly after signing in.'
,
email
:
'Email'
,
emailInvalid
:
'Invalid Email Format'
,
emailPlaceholder
:
'Input Email'
,
sendInvite
:
'Add'
,
invitationSent
:
'Added'
,
invitationSentTip
:
'Added, and they can sign in to Dify to access your team data.'
,
invitationSent
:
'Invitation sent'
,
invitationSentTip
:
'Invitation sent, and they can sign in to Dify to access your team data.'
,
invitationLink
:
'Invitation Link'
,
ok
:
'OK'
,
removeFromTeam
:
'Remove from team'
,
removeFromTeamTip
:
'Will remove team access'
,
...
...
@@ -171,6 +181,22 @@ const translation = {
useYourModel
:
'Currently using own Model Provider.'
,
close
:
'Close'
,
},
anthropicHosted
:
{
anthropicHosted
:
'Anthropic Claude'
,
onTrial
:
'ON TRIAL'
,
exhausted
:
'QUOTA EXHAUSTED'
,
desc
:
'Powerful model, which excels at a wide range of tasks from sophisticated dialogue and creative content generation to detailed instruction.'
,
callTimes
:
'Call times'
,
usedUp
:
'Trial quota used up. Add own Model Provider.'
,
useYourModel
:
'Currently using own Model Provider.'
,
close
:
'Close'
,
},
anthropic
:
{
using
:
'The embedding capability is using'
,
enableTip
:
'To enable the Anthropic model, you need to bind to OpenAI or Azure OpenAI Service first.'
,
notEnabled
:
'Not enabled'
,
keyFrom
:
'Get your API key from Anthropic'
,
},
encrypted
:
{
front
:
'Your API KEY will be encrypted and stored using'
,
back
:
' technology.'
,
...
...
web/i18n/lang/common.zh.ts
View file @
9098d099
...
...
@@ -14,6 +14,7 @@ const translation = {
edit
:
'编辑'
,
add
:
'添加'
,
refresh
:
'重新开始'
,
reset
:
'重置'
,
search
:
'搜索'
,
change
:
'更改'
,
remove
:
'移除'
,
...
...
@@ -53,7 +54,7 @@ const translation = {
maxTokenTip
:
'生成的最大令牌数取决于模型。提示和完成共享令牌数限制。一个令牌约等于 1 个英文或 半个中文字符。'
,
maxTokenSettingTip
:
'您设置的最大 tokens 数较大,可能会导致 prompt、用户问题、数据集内容没有 token 空间进行处理,建议设置到 2/3 以下。'
,
setToCurrentModelMaxTokenTip
:
'最大令牌数更新为当前模型最大的令牌数
4,000
。'
,
setToCurrentModelMaxTokenTip
:
'最大令牌数更新为当前模型最大的令牌数
{{maxToken}}
。'
,
},
tone
:
{
Creative
:
'创意'
,
...
...
@@ -96,7 +97,14 @@ const translation = {
avatar
:
'头像'
,
name
:
'用户名'
,
email
:
'邮箱'
,
edit
:
'编辑'
,
password
:
'密码'
,
passwordTip
:
'如果您不想使用验证码登录,可以设置永久密码'
,
setPassword
:
'设置密码'
,
resetPassword
:
'重置密码'
,
currentPassword
:
'原密码'
,
newPassword
:
'新密码'
,
notEqual
:
'两个密码不相同'
,
confirmPassword
:
'确认密码'
,
langGeniusAccount
:
'Dify 账号'
,
langGeniusAccountTip
:
'您的 Dify 账号和相关的用户数据。'
,
editName
:
'编辑名字'
,
...
...
@@ -120,8 +128,9 @@ const translation = {
emailInvalid
:
'邮箱格式无效'
,
emailPlaceholder
:
'输入邮箱'
,
sendInvite
:
'添加'
,
invitationSent
:
'已添加'
,
invitationSentTip
:
'已添加,对方登录 Dify 后即可访问你的团队数据。'
,
invitationSent
:
'邀请已发送'
,
invitationSentTip
:
'邀请已发送,对方登录 Dify 后即可访问你的团队数据。'
,
invitationLink
:
'邀请链接'
,
ok
:
'好的'
,
removeFromTeam
:
'移除团队'
,
removeFromTeamTip
:
'将取消团队访问'
,
...
...
@@ -172,6 +181,22 @@ const translation = {
useYourModel
:
'当前正在使用你自己的模型供应商。'
,
close
:
'关闭'
,
},
anthropicHosted
:
{
anthropicHosted
:
'Anthropic Claude'
,
onTrial
:
'体验'
,
exhausted
:
'超出限额'
,
desc
:
'功能强大的模型,擅长执行从复杂对话和创意内容生成到详细指导的各种任务。'
,
callTimes
:
'调用次数'
,
usedUp
:
'试用额度已用完,请在下方添加自己的模型供应商'
,
useYourModel
:
'当前正在使用你自己的模型供应商。'
,
close
:
'关闭'
,
},
anthropic
:
{
using
:
'嵌入能力正在使用'
,
enableTip
:
'要启用 Anthropic 模型,您需要先绑定 OpenAI 或 Azure OpenAI 服务。'
,
notEnabled
:
'未启用'
,
keyFrom
:
'从 Anthropic 获取您的 API 密钥'
,
},
encrypted
:
{
front
:
'密钥将使用 '
,
back
:
' 技术进行加密和存储。'
,
...
...
web/i18n/lang/login.en.ts
View file @
9098d099
const
translation
=
{
"pageTitle"
:
"Hey, let's get started!👋"
,
"welcome"
:
"Welcome to Dify, please log in to continue."
,
"email"
:
"Email address"
,
"password"
:
"Password"
,
"name"
:
"Name"
,
"forget"
:
"Forgot your password?"
,
"signBtn"
:
"Sign in"
,
"installBtn"
:
"Setting"
,
"setAdminAccount"
:
"Setting up an admin account"
,
"setAdminAccountDesc"
:
"Maximum privileges for admin account, which can be used to create applications and manage LLM providers, etc."
,
"createAndSignIn"
:
"Create and sign in"
,
"oneMoreStep"
:
"One more step"
,
"createSample"
:
"Based on this information, we’ll create sample application for you"
,
"invitationCode"
:
"Invitation Code"
,
"interfaceLanguage"
:
"Interface Dify"
,
"timezone"
:
"Time zone"
,
"go"
:
"Go to Dify"
,
"sendUsMail"
:
"Email us your introduction, and we'll handle the invitation request."
,
"acceptPP"
:
"I have read and accept the privacy policy"
,
"reset"
:
"Please run following command to reset your password"
,
"withGitHub"
:
"Continue with GitHub"
,
"withGoogle"
:
"Continue with Google"
,
"rightTitle"
:
"Unlock the full potential of LLM"
,
"rightDesc"
:
"Effortlessly build visually captivating, operable, and improvable AI applications."
,
"tos"
:
"Terms of Service"
,
"pp"
:
"Privacy Policy"
,
"tosDesc"
:
"By signing up, you agree to our"
,
"donthave"
:
"Don't have?"
,
"invalidInvitationCode"
:
"Invalid invitation code"
,
"accountAlreadyInited"
:
"Account already inited"
,
"error"
:
{
"emailEmpty"
:
"Email address is required"
,
"emailInValid"
:
"Please enter a valid email address"
,
"nameEmpty"
:
"Name is required"
,
"passwordEmpty"
:
"Password is required"
,
"passwordInvalid"
:
"Password must contain letters and numbers, and the length must be greater than 8"
,
}
pageTitle
:
'Hey, let
\'
s get started!👋'
,
welcome
:
'Welcome to Dify, please log in to continue.'
,
email
:
'Email address'
,
emailPlaceholder
:
'Your email'
,
password
:
'Password'
,
passwordPlaceholder
:
'Your password'
,
name
:
'Username'
,
namePlaceholder
:
'Your username'
,
forget
:
'Forgot your password?'
,
signBtn
:
'Sign in'
,
installBtn
:
'Setting'
,
setAdminAccount
:
'Setting up an admin account'
,
setAdminAccountDesc
:
'Maximum privileges for admin account, which can be used to create applications and manage LLM providers, etc.'
,
createAndSignIn
:
'Create and sign in'
,
oneMoreStep
:
'One more step'
,
createSample
:
'Based on this information, we’ll create sample application for you'
,
invitationCode
:
'Invitation Code'
,
invitationCodePlaceholder
:
'Your invitation code'
,
interfaceLanguage
:
'Interface Language'
,
timezone
:
'Time zone'
,
go
:
'Go to Dify'
,
sendUsMail
:
'Email us your introduction, and we
\'
ll handle the invitation request.'
,
acceptPP
:
'I have read and accept the privacy policy'
,
reset
:
'Please run following command to reset your password'
,
withGitHub
:
'Continue with GitHub'
,
withGoogle
:
'Continue with Google'
,
rightTitle
:
'Unlock the full potential of LLM'
,
rightDesc
:
'Effortlessly build visually captivating, operable, and improvable AI applications.'
,
tos
:
'Terms of Service'
,
pp
:
'Privacy Policy'
,
tosDesc
:
'By signing up, you agree to our'
,
donthave
:
'Don
\'
t have?'
,
invalidInvitationCode
:
'Invalid invitation code'
,
accountAlreadyInited
:
'Account already inited'
,
error
:
{
emailEmpty
:
'Email address is required'
,
emailInValid
:
'Please enter a valid email address'
,
nameEmpty
:
'Name is required'
,
passwordEmpty
:
'Password is required'
,
passwordInvalid
:
'Password must contain letters and numbers, and the length must be greater than 8'
,
},
license
:
{
tip
:
'Before starting Dify Community Edition, read the GitHub'
,
link
:
'Open-source License'
,
},
join
:
'Join'
,
joinTipStart
:
'Invite you join'
,
joinTipEnd
:
'team on Dify'
,
invalid
:
'The link has expired'
,
explore
:
'Explore Dify'
,
activatedTipStart
:
'You have joined the'
,
activatedTipEnd
:
'team'
,
activated
:
'Sign In Now'
,
}
export
default
translation
web/i18n/lang/login.zh.ts
View file @
9098d099
const
translation
=
{
"pageTitle"
:
"嗨,近来可好 👋"
,
"welcome"
:
"欢迎来到 Dify, 登录以继续"
,
"email"
:
"邮箱"
,
"password"
:
"密码"
,
"name"
:
"用户名"
,
"forget"
:
"忘记密码?"
,
"signBtn"
:
"登录"
,
"installBtn"
:
"设置"
,
"setAdminAccount"
:
"设置管理员账户"
,
"setAdminAccountDesc"
:
"管理员拥有的最大权限,可用于创建应用和管理 LLM 供应商等。"
,
"createAndSignIn"
:
"创建账户"
,
"oneMoreStep"
:
"还差一步"
,
"createSample"
:
"基于这些信息,我们将为您创建一个示例应用"
,
"invitationCode"
:
"邀请码"
,
"interfaceLanguage"
:
"界面语言"
,
"timezone"
:
"时区"
,
"go"
:
"跳转至 Dify"
,
"sendUsMail"
:
"发封邮件介绍你自己,我们会尽快处理。"
,
"acceptPP"
:
"我已阅读并接受隐私政策"
,
"reset"
:
"请运行以下命令重置密码"
,
"withGitHub"
:
"使用 GitHub 登录"
,
"withGoogle"
:
"使用 Google 登录"
,
"rightTitle"
:
"释放大型语言模型的全部潜能"
,
"rightDesc"
:
"简单构建可视化、可运营、可改进的 AI 应用"
,
"tos"
:
"使用协议"
,
"pp"
:
"隐私政策"
,
"tosDesc"
:
"使用即代表你并同意我们的"
,
"donthave"
:
"还没有邀请码?"
,
"invalidInvitationCode"
:
"无效的邀请码"
,
"accountAlreadyInited"
:
"账户已经初始化"
,
"error"
:
{
"emailEmpty"
:
"邮箱不能为空"
,
"emailInValid"
:
"请输入有效的邮箱地址"
,
"nameEmpty"
:
"用户名不能为空"
,
"passwordEmpty"
:
"密码不能为空"
,
"passwordInvalid"
:
"密码必须包含字母和数字,且长度不小于8位"
,
}
pageTitle
:
'嗨,近来可好 👋'
,
welcome
:
'欢迎来到 Dify, 登录以继续'
,
email
:
'邮箱'
,
emailPlaceholder
:
'输入邮箱地址'
,
password
:
'密码'
,
passwordPlaceholder
:
'输入密码'
,
name
:
'用户名'
,
namePlaceholder
:
'输入用户名'
,
forget
:
'忘记密码?'
,
signBtn
:
'登录'
,
installBtn
:
'设置'
,
setAdminAccount
:
'设置管理员账户'
,
setAdminAccountDesc
:
'管理员拥有的最大权限,可用于创建应用和管理 LLM 供应商等。'
,
createAndSignIn
:
'创建账户'
,
oneMoreStep
:
'还差一步'
,
createSample
:
'基于这些信息,我们将为您创建一个示例应用'
,
invitationCode
:
'邀请码'
,
invitationCodePlaceholder
:
'输入邀请码'
,
interfaceLanguage
:
'界面语言'
,
timezone
:
'时区'
,
go
:
'跳转至 Dify'
,
sendUsMail
:
'发封邮件介绍你自己,我们会尽快处理。'
,
acceptPP
:
'我已阅读并接受隐私政策'
,
reset
:
'请运行以下命令重置密码'
,
withGitHub
:
'使用 GitHub 登录'
,
withGoogle
:
'使用 Google 登录'
,
rightTitle
:
'释放大型语言模型的全部潜能'
,
rightDesc
:
'简单构建可视化、可运营、可改进的 AI 应用'
,
tos
:
'使用协议'
,
pp
:
'隐私政策'
,
tosDesc
:
'使用即代表你并同意我们的'
,
donthave
:
'还没有邀请码?'
,
invalidInvitationCode
:
'无效的邀请码'
,
accountAlreadyInited
:
'账户已经初始化'
,
error
:
{
emailEmpty
:
'邮箱不能为空'
,
emailInValid
:
'请输入有效的邮箱地址'
,
nameEmpty
:
'用户名不能为空'
,
passwordEmpty
:
'密码不能为空'
,
passwordInvalid
:
'密码必须包含字母和数字,且长度不小于8位'
,
},
license
:
{
tip
:
'启动 Dify 社区版之前, 请阅读 GitHub 上的'
,
link
:
'开源协议'
,
},
join
:
'加入'
,
joinTipStart
:
'邀请你加入'
,
joinTipEnd
:
'团队'
,
invalid
:
'链接已失效'
,
explore
:
'探索 Dify'
,
activatedTipStart
:
'您已加入'
,
activatedTipEnd
:
'团队'
,
activated
:
'现在登录'
,
}
export
default
translation
web/models/common.ts
View file @
9098d099
...
...
@@ -10,6 +10,8 @@ export type UserProfileResponse = {
id
:
string
name
:
string
email
:
string
avatar
:
string
is_password_set
:
boolean
interface_language
?:
string
interface_theme
?:
string
timezone
?:
string
...
...
@@ -57,14 +59,19 @@ export type Member = Pick<UserProfileResponse, 'id' | 'name' | 'email' | 'last_l
export
enum
ProviderName
{
OPENAI
=
'openai'
,
AZURE_OPENAI
=
'azure_openai'
,
ANTHROPIC
=
'anthropic'
,
}
export
type
ProviderAzureToken
=
{
openai_api_base
?:
string
openai_api_key
?:
string
}
export
type
ProviderAnthropicToken
=
{
anthropic_api_key
?:
string
}
export
type
ProviderTokenType
=
{
[
ProviderName
.
OPENAI
]:
string
[
ProviderName
.
AZURE_OPENAI
]:
ProviderAzureToken
[
ProviderName
.
ANTHROPIC
]:
ProviderAnthropicToken
}
export
type
Provider
=
{
[
Name
in
ProviderName
]:
{
...
...
web/package.json
View file @
9098d099
{
"name"
:
"dify-web"
,
"version"
:
"0.3.
7
"
,
"version"
:
"0.3.
9
"
,
"private"
:
true
,
"scripts"
:
{
"dev"
:
"next dev"
,
...
...
web/public/embed.js
View file @
9098d099
...
...
@@ -54,7 +54,7 @@ async function embedChatbot () {
iframe
.
title
=
"dify chatbot bubble window"
iframe
.
id
=
'dify-chatbot-bubble-window'
iframe
.
src
=
`https://
${
isDev
?
'dev.'
:
''
}
udify.app/chatbot/
${
difyChatbotConfig
.
token
}
`
;
iframe
.
style
.
cssText
=
'border: none; position: fixed; flex-direction: column; justify-content: space-between; box-shadow: rgba(150, 150, 150, 0.2) 0px 10px 30px 0px, rgba(150, 150, 150, 0.2) 0px 0px 0px 1px; bottom: 5rem; right: 1rem; width: 24rem; height: 40rem; border-radius: 0.75rem; display: flex; z-index: 2147483647; overflow: hidden; left: unset;'
iframe
.
style
.
cssText
=
'border: none; position: fixed; flex-direction: column; justify-content: space-between; box-shadow: rgba(150, 150, 150, 0.2) 0px 10px 30px 0px, rgba(150, 150, 150, 0.2) 0px 0px 0px 1px; bottom: 5rem; right: 1rem; width: 24rem; height: 40rem; border-radius: 0.75rem; display: flex; z-index: 2147483647; overflow: hidden; left: unset;
background-color: #F3F4F6;
'
document
.
body
.
appendChild
(
iframe
);
}
...
...
web/public/embed.min.js
View file @
9098d099
...
...
@@ -27,4 +27,4 @@ async function embedChatbot(){const t=window.difyChatbotConfig;if(t&&t.token){co
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>`
;
if
(
!
document
.
getElementById
(
"dify-chatbot-bubble-button"
)){
var
e
=
document
.
createElement
(
"div"
);
e
.
id
=
"dify-chatbot-bubble-button"
,
e
.
style
.
cssText
=
"position: fixed; bottom: 1rem; right: 1rem; width: 50px; height: 50px; border-radius: 25px; background-color: #155EEF; box-shadow: rgba(0, 0, 0, 0.2) 0px 4px 8px 0px; cursor: pointer; z-index: 2147483647; transition: all 0.2s ease-in-out 0s; left: unset; transform: scale(1); :hover {transform: scale(1.1);}"
;
const
d
=
document
.
createElement
(
"div"
);
d
.
style
.
cssText
=
"display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;"
,
d
.
innerHTML
=
n
,
e
.
appendChild
(
d
),
document
.
body
.
appendChild
(
e
),
e
.
addEventListener
(
"click"
,
function
(){
var
e
=
document
.
getElementById
(
"dify-chatbot-bubble-window"
);
e
?
"none"
===
e
.
style
.
display
?(
e
.
style
.
display
=
"block"
,
d
.
innerHTML
=
i
):(
e
.
style
.
display
=
"none"
,
d
.
innerHTML
=
n
):((
e
=
document
.
createElement
(
"iframe"
)).
allow
=
"fullscreen;microphone"
,
e
.
title
=
"dify chatbot bubble window"
,
e
.
id
=
"dify-chatbot-bubble-window"
,
e
.
src
=
`https://
${
o
?
"dev."
:
""
}
udify.app/chatbot/`
+
t
.
token
,
e
.
style
.
cssText
=
"border: none; position: fixed; flex-direction: column; justify-content: space-between; box-shadow: rgba(150, 150, 150, 0.2) 0px 10px 30px 0px, rgba(150, 150, 150, 0.2) 0px 0px 0px 1px; bottom: 5rem; right: 1rem; width: 24rem; height: 40rem; border-radius: 0.75rem; display: flex; z-index: 2147483647; overflow: hidden; left: unset;"
,
document
.
body
.
appendChild
(
e
),
d
.
innerHTML
=
i
)})}}
else
console
.
error
(
"difyChatbotConfig is empty or token is not provided"
)}
document
.
body
.
onload
=
embedChatbot
;
\ No newline at end of file
</svg>`
;
if
(
!
document
.
getElementById
(
"dify-chatbot-bubble-button"
)){
var
e
=
document
.
createElement
(
"div"
);
e
.
id
=
"dify-chatbot-bubble-button"
,
e
.
style
.
cssText
=
"position: fixed; bottom: 1rem; right: 1rem; width: 50px; height: 50px; border-radius: 25px; background-color: #155EEF; box-shadow: rgba(0, 0, 0, 0.2) 0px 4px 8px 0px; cursor: pointer; z-index: 2147483647; transition: all 0.2s ease-in-out 0s; left: unset; transform: scale(1); :hover {transform: scale(1.1);}"
;
const
d
=
document
.
createElement
(
"div"
);
d
.
style
.
cssText
=
"display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;"
,
d
.
innerHTML
=
n
,
e
.
appendChild
(
d
),
document
.
body
.
appendChild
(
e
),
e
.
addEventListener
(
"click"
,
function
(){
var
e
=
document
.
getElementById
(
"dify-chatbot-bubble-window"
);
e
?
"none"
===
e
.
style
.
display
?(
e
.
style
.
display
=
"block"
,
d
.
innerHTML
=
i
):(
e
.
style
.
display
=
"none"
,
d
.
innerHTML
=
n
):((
e
=
document
.
createElement
(
"iframe"
)).
allow
=
"fullscreen;microphone"
,
e
.
title
=
"dify chatbot bubble window"
,
e
.
id
=
"dify-chatbot-bubble-window"
,
e
.
src
=
`https://
${
o
?
"dev."
:
""
}
udify.app/chatbot/`
+
t
.
token
,
e
.
style
.
cssText
=
"border: none; position: fixed; flex-direction: column; justify-content: space-between; box-shadow: rgba(150, 150, 150, 0.2) 0px 10px 30px 0px, rgba(150, 150, 150, 0.2) 0px 0px 0px 1px; bottom: 5rem; right: 1rem; width: 24rem; height: 40rem; border-radius: 0.75rem; display: flex; z-index: 2147483647; overflow: hidden; left: unset; background-color: #F3F4F6;"
,
document
.
body
.
appendChild
(
e
),
d
.
innerHTML
=
i
)})}}
else
console
.
error
(
"difyChatbotConfig is empty or token is not provided"
)}
document
.
body
.
onload
=
embedChatbot
;
\ No newline at end of file
web/service/base.ts
View file @
9098d099
...
...
@@ -334,13 +334,16 @@ export const ssePost = (url: string, fetchOptions: any, { isPublicAPI = false, o
return
handleStream
(
res
,
(
str
:
string
,
isFirstMessage
:
boolean
,
moreInfo
:
IOnDataMoreInfo
)
=>
{
if
(
moreInfo
.
errorMessage
)
{
onError
?.(
moreInfo
.
errorMessage
)
if
(
moreInfo
.
errorMessage
!==
'AbortError: The user aborted a request.'
)
Toast
.
notify
({
type
:
'error'
,
message
:
moreInfo
.
errorMessage
})
return
}
onData
?.(
str
,
isFirstMessage
,
moreInfo
)
},
onCompleted
)
}).
catch
((
e
)
=>
{
if
(
e
.
toString
()
!==
'AbortError: The user aborted a request.'
)
Toast
.
notify
({
type
:
'error'
,
message
:
e
})
onError
?.(
e
)
})
}
...
...
web/service/common.ts
View file @
9098d099
...
...
@@ -3,7 +3,7 @@ import { del, get, patch, post, put } from './base'
import
type
{
AccountIntegrate
,
CommonResponse
,
DataSourceNotion
,
IWorkspace
,
LangGeniusVersionResponse
,
Member
,
OauthResponse
,
PluginProvider
,
Provider
,
ProviderAzureToken
,
TenantInfoResponse
,
OauthResponse
,
PluginProvider
,
Provider
,
ProviderA
nthropicToken
,
ProviderA
zureToken
,
TenantInfoResponse
,
UserProfileOriginResponse
,
}
from
'@/models/common'
import
type
{
...
...
@@ -58,7 +58,7 @@ export const fetchProviders: Fetcher<Provider[] | null, { url: string; params: R
export
const
validateProviderKey
:
Fetcher
<
ValidateOpenAIKeyResponse
,
{
url
:
string
;
body
:
{
token
:
string
}
}
>
=
({
url
,
body
})
=>
{
return
post
(
url
,
{
body
})
as
Promise
<
ValidateOpenAIKeyResponse
>
}
export
const
updateProviderAIKey
:
Fetcher
<
UpdateOpenAIKeyResponse
,
{
url
:
string
;
body
:
{
token
:
string
|
ProviderAzureToken
}
}
>
=
({
url
,
body
})
=>
{
export
const
updateProviderAIKey
:
Fetcher
<
UpdateOpenAIKeyResponse
,
{
url
:
string
;
body
:
{
token
:
string
|
ProviderAzureToken
|
ProviderAnthropicToken
}
}
>
=
({
url
,
body
})
=>
{
return
post
(
url
,
{
body
})
as
Promise
<
UpdateOpenAIKeyResponse
>
}
...
...
@@ -66,8 +66,8 @@ export const fetchAccountIntegrates: Fetcher<{ data: AccountIntegrate[] | null }
return
get
(
url
,
{
params
})
as
Promise
<
{
data
:
AccountIntegrate
[]
|
null
}
>
}
export
const
inviteMember
:
Fetcher
<
CommonResponse
&
{
account
:
Member
},
{
url
:
string
;
body
:
Record
<
string
,
any
>
}
>
=
({
url
,
body
})
=>
{
return
post
(
url
,
{
body
})
as
Promise
<
CommonResponse
&
{
account
:
Member
}
>
export
const
inviteMember
:
Fetcher
<
CommonResponse
&
{
account
:
Member
;
invite_url
:
string
},
{
url
:
string
;
body
:
Record
<
string
,
any
>
}
>
=
({
url
,
body
})
=>
{
return
post
(
url
,
{
body
})
as
Promise
<
CommonResponse
&
{
account
:
Member
;
invite_url
:
string
}
>
}
export
const
updateMemberRole
:
Fetcher
<
CommonResponse
,
{
url
:
string
;
body
:
Record
<
string
,
any
>
}
>
=
({
url
,
body
})
=>
{
...
...
@@ -112,3 +112,11 @@ export const validatePluginProviderKey: Fetcher<ValidateOpenAIKeyResponse, { url
export
const
updatePluginProviderAIKey
:
Fetcher
<
UpdateOpenAIKeyResponse
,
{
url
:
string
;
body
:
{
credentials
:
any
}
}
>
=
({
url
,
body
})
=>
{
return
post
(
url
,
{
body
})
as
Promise
<
UpdateOpenAIKeyResponse
>
}
export
const
invitationCheck
:
Fetcher
<
CommonResponse
&
{
is_valid
:
boolean
;
workspace_name
:
string
},
{
url
:
string
;
params
:
{
workspace_id
:
string
;
email
:
string
;
token
:
string
}
}
>
=
({
url
,
params
})
=>
{
return
get
(
url
,
{
params
})
as
Promise
<
CommonResponse
&
{
is_valid
:
boolean
;
workspace_name
:
string
}
>
}
export
const
activateMember
:
Fetcher
<
CommonResponse
,
{
url
:
string
;
body
:
any
}
>
=
({
url
,
body
})
=>
{
return
post
(
url
,
{
body
})
as
Promise
<
CommonResponse
>
}
web/types/app.ts
View file @
9098d099
export
enum
ProviderType
{
openai
=
'openai'
,
anthropic
=
'anthropic'
,
}
export
enum
AppType
{
'chat'
=
'chat'
,
'completion'
=
'completion'
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment