mirror of https://github.com/langgenius/dify.git
Merge branch 'main' into feat/explore
This commit is contained in:
commit
2558a61b28
|
|
@ -5,8 +5,6 @@ on:
|
|||
branches:
|
||||
- 'main'
|
||||
- 'deploy/dev'
|
||||
pull_request:
|
||||
types: [synchronize, opened, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ on:
|
|||
branches:
|
||||
- 'main'
|
||||
- 'deploy/dev'
|
||||
pull_request:
|
||||
types: [synchronize, opened, reopened, ready_for_review]
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ dmypy.json
|
|||
api/.env
|
||||
api/storage/*
|
||||
|
||||
docker/volumes/app/storage/privkeys/*
|
||||
docker/volumes/app/storage/*
|
||||
docker/volumes/db/data/*
|
||||
docker/volumes/redis/data/*
|
||||
docker/volumes/weaviate/*
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ To set up a working development environment, just fork the project git repositor
|
|||
|
||||
### Fork the repository
|
||||
|
||||
you need to fork the [repository](https://github.com/langgenius/langgenius-gateway).
|
||||
you need to fork the [repository](https://github.com/langgenius/dify).
|
||||
|
||||
### Clone the repo
|
||||
|
||||
Clone your GitHub forked repository:
|
||||
|
||||
```
|
||||
git clone git@github.com:<github_username>/langgenius-gateway.git
|
||||
git clone git@github.com:<github_username>/dify.git
|
||||
```
|
||||
|
||||
### Install backend
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<a href="./README_CN.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
[Website](https://dify.ai) • [Docs](https://docs.dify.ai) • [Twitter](https://twitter.com/dify_ai)
|
||||
[Website](https://dify.ai) • [Docs](https://docs.dify.ai) • [Twitter](https://twitter.com/dify_ai) • [Discord](https://discord.gg/FngNHpbcY7)
|
||||
|
||||
**Dify** is an easy-to-use LLMOps platform designed to empower more people to create sustainable, AI-native applications. With visual orchestration for various application types, Dify offers out-of-the-box, ready-to-use applications that can also serve as Backend-as-a-Service APIs. Unify your development process with one API for plugins and datasets integration, and streamline your operations using a single interface for prompt engineering, visual analytics, and continuous improvement.
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ cd docker
|
|||
docker-compose up -d
|
||||
```
|
||||
|
||||
After running, you can access the Dify console in your browser at [http://localhost](http://localhost) and start the initialization operation.
|
||||
After running, you can access the Dify dashboard in your browser at [http://localhost/install](http://localhost/install) and start the initialization installation process.
|
||||
|
||||
### Configuration
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ A: English and Chinese are currently supported, and you can contribute language
|
|||
If you have any questions, suggestions, or partnership inquiries, feel free to contact us through the following channels:
|
||||
|
||||
- Submit an Issue or PR on our GitHub Repo
|
||||
- Join the discussion in our [Discord](https://discord.gg/AhzKf7dNgk) Community
|
||||
- Join the discussion in our [Discord](https://discord.gg/FngNHpbcY7) Community
|
||||
- Send an email to hello@dify.ai
|
||||
|
||||
We're eager to assist you and together create more fun and useful AI applications!
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
</p>
|
||||
|
||||
|
||||
[官方网站](https://dify.ai) • [文档](https://docs.dify.ai/v/zh-hans) • [Twitter](https://twitter.com/dify_ai)
|
||||
[官方网站](https://dify.ai) • [文档](https://docs.dify.ai/v/zh-hans) • [Twitter](https://twitter.com/dify_ai) • [Discord](https://discord.gg/FngNHpbcY7)
|
||||
|
||||
**Dify** 是一个易用的 LLMOps 平台,旨在让更多人可以创建可持续运营的原生 AI 应用。Dify 提供多种类型应用的可视化编排,应用可开箱即用,也能以“后端即服务”的 API 提供服务。
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ cd docker
|
|||
docker-compose up -d
|
||||
```
|
||||
|
||||
运行后,可以在浏览器上访问 [http://localhost](http://localhost) 进入 Dify 控制台,并开始初始化操作。
|
||||
运行后,可以在浏览器上访问 [http://localhost/install](http://localhost/install) 进入 Dify 控制台并开始初始化安装操作。
|
||||
|
||||
### 配置
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ A: 现已支持英文与中文,你可以为我们贡献语言包。
|
|||
如果您有任何问题、建议或合作意向,欢迎通过以下方式联系我们:
|
||||
|
||||
- 在我们的 [GitHub Repo](https://github.com/langgenius/dify) 上提交 Issue 或 PR
|
||||
- 在我们的 [Discord 社区](https://discord.gg/AhzKf7dNgk) 上加入讨论
|
||||
- 在我们的 [Discord 社区](https://discord.gg/FngNHpbcY7) 上加入讨论
|
||||
- 发送邮件至 hello@dify.ai
|
||||
|
||||
## 贡献代码
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ message_detail_fields = {
|
|||
'message_tokens': fields.Integer,
|
||||
'answer': fields.String,
|
||||
'answer_tokens': fields.Integer,
|
||||
'provider_response_latency': fields.Integer,
|
||||
'provider_response_latency': fields.Float,
|
||||
'from_source': fields.String,
|
||||
'from_end_user_id': fields.String,
|
||||
'from_account_id': fields.String,
|
||||
|
|
|
|||
|
|
@ -26,46 +26,46 @@ from services.errors.conversation import ConversationNotExistsError
|
|||
from services.errors.message import MessageNotExistsError
|
||||
from services.message_service import MessageService
|
||||
|
||||
account_fields = {
|
||||
'id': fields.String,
|
||||
'name': fields.String,
|
||||
'email': fields.String
|
||||
}
|
||||
|
||||
class ChatMessageApi(Resource):
|
||||
account_fields = {
|
||||
'id': fields.String,
|
||||
'name': fields.String,
|
||||
'email': fields.String
|
||||
}
|
||||
feedback_fields = {
|
||||
'rating': fields.String,
|
||||
'content': fields.String,
|
||||
'from_source': fields.String,
|
||||
'from_end_user_id': fields.String,
|
||||
'from_account': fields.Nested(account_fields, allow_null=True),
|
||||
}
|
||||
|
||||
feedback_fields = {
|
||||
'rating': fields.String,
|
||||
'content': fields.String,
|
||||
'from_source': fields.String,
|
||||
'from_end_user_id': fields.String,
|
||||
'from_account': fields.Nested(account_fields, allow_null=True),
|
||||
}
|
||||
annotation_fields = {
|
||||
'content': fields.String,
|
||||
'account': fields.Nested(account_fields, allow_null=True),
|
||||
'created_at': TimestampField
|
||||
}
|
||||
|
||||
annotation_fields = {
|
||||
'content': fields.String,
|
||||
'account': fields.Nested(account_fields, allow_null=True),
|
||||
'created_at': TimestampField
|
||||
}
|
||||
message_detail_fields = {
|
||||
'id': fields.String,
|
||||
'conversation_id': fields.String,
|
||||
'inputs': fields.Raw,
|
||||
'query': fields.String,
|
||||
'message': fields.Raw,
|
||||
'message_tokens': fields.Integer,
|
||||
'answer': fields.String,
|
||||
'answer_tokens': fields.Integer,
|
||||
'provider_response_latency': fields.Float,
|
||||
'from_source': fields.String,
|
||||
'from_end_user_id': fields.String,
|
||||
'from_account_id': fields.String,
|
||||
'feedbacks': fields.List(fields.Nested(feedback_fields)),
|
||||
'annotation': fields.Nested(annotation_fields, allow_null=True),
|
||||
'created_at': TimestampField
|
||||
}
|
||||
|
||||
message_detail_fields = {
|
||||
'id': fields.String,
|
||||
'conversation_id': fields.String,
|
||||
'inputs': fields.Raw,
|
||||
'query': fields.String,
|
||||
'message': fields.Raw,
|
||||
'message_tokens': fields.Integer,
|
||||
'answer': fields.String,
|
||||
'answer_tokens': fields.Integer,
|
||||
'provider_response_latency': fields.Integer,
|
||||
'from_source': fields.String,
|
||||
'from_end_user_id': fields.String,
|
||||
'from_account_id': fields.String,
|
||||
'feedbacks': fields.List(fields.Nested(feedback_fields)),
|
||||
'annotation': fields.Nested(annotation_fields, allow_null=True),
|
||||
'created_at': TimestampField
|
||||
}
|
||||
|
||||
class ChatMessageListApi(Resource):
|
||||
message_infinite_scroll_pagination_fields = {
|
||||
'limit': fields.Integer,
|
||||
'has_more': fields.Boolean,
|
||||
|
|
@ -253,7 +253,8 @@ class MessageMoreLikeThisApi(Resource):
|
|||
message_id = str(message_id)
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('response_mode', type=str, required=True, choices=['blocking', 'streaming'], location='args')
|
||||
parser.add_argument('response_mode', type=str, required=True, choices=['blocking', 'streaming'],
|
||||
location='args')
|
||||
args = parser.parse_args()
|
||||
|
||||
streaming = args['response_mode'] == 'streaming'
|
||||
|
|
@ -301,7 +302,8 @@ def compact_response(response: Union[dict | Generator]) -> Response:
|
|||
except QuotaExceededError:
|
||||
yield "data: " + json.dumps(api.handle_error(ProviderQuotaExceededError()).get_json()) + "\n\n"
|
||||
except ModelCurrentlyNotSupportError:
|
||||
yield "data: " + json.dumps(api.handle_error(ProviderModelCurrentlyNotSupportError()).get_json()) + "\n\n"
|
||||
yield "data: " + json.dumps(
|
||||
api.handle_error(ProviderModelCurrentlyNotSupportError()).get_json()) + "\n\n"
|
||||
except (LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError,
|
||||
LLMRateLimitError, LLMAuthorizationError) as e:
|
||||
yield "data: " + json.dumps(api.handle_error(CompletionRequestError(str(e))).get_json()) + "\n\n"
|
||||
|
|
@ -353,9 +355,33 @@ class MessageSuggestedQuestionApi(Resource):
|
|||
return {'data': questions}
|
||||
|
||||
|
||||
class MessageApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@marshal_with(message_detail_fields)
|
||||
def get(self, app_id, message_id):
|
||||
app_id = str(app_id)
|
||||
message_id = str(message_id)
|
||||
|
||||
# get app info
|
||||
app_model = _get_app(app_id, 'chat')
|
||||
|
||||
message = db.session.query(Message).filter(
|
||||
Message.id == message_id,
|
||||
Message.app_id == app_model.id
|
||||
).first()
|
||||
|
||||
if not message:
|
||||
raise NotFound("Message Not Exists.")
|
||||
|
||||
return message
|
||||
|
||||
|
||||
api.add_resource(MessageMoreLikeThisApi, '/apps/<uuid:app_id>/completion-messages/<uuid:message_id>/more-like-this')
|
||||
api.add_resource(MessageSuggestedQuestionApi, '/apps/<uuid:app_id>/chat-messages/<uuid:message_id>/suggested-questions')
|
||||
api.add_resource(ChatMessageApi, '/apps/<uuid:app_id>/chat-messages', endpoint='chat_messages')
|
||||
api.add_resource(ChatMessageListApi, '/apps/<uuid:app_id>/chat-messages', endpoint='console_chat_messages')
|
||||
api.add_resource(MessageFeedbackApi, '/apps/<uuid:app_id>/feedbacks')
|
||||
api.add_resource(MessageAnnotationApi, '/apps/<uuid:app_id>/annotations')
|
||||
api.add_resource(MessageAnnotationCountApi, '/apps/<uuid:app_id>/annotations/count')
|
||||
api.add_resource(MessageApi, '/apps/<uuid:app_id>/messages/<uuid:message_id>', endpoint='console_message')
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import decimal
|
|||
import json
|
||||
from typing import Optional, Union
|
||||
|
||||
from gunicorn.config import User
|
||||
|
||||
from core.callback_handler.entity.agent_loop import AgentLoop
|
||||
from core.callback_handler.entity.dataset_query import DatasetQueryObj
|
||||
from core.callback_handler.entity.llm_message import LLMMessage
|
||||
|
|
@ -269,7 +267,7 @@ class ConversationMessageTask:
|
|||
|
||||
|
||||
class PubHandler:
|
||||
def __init__(self, user: Union[Account | User], task_id: str,
|
||||
def __init__(self, user: Union[Account | EndUser], task_id: str,
|
||||
message: Message, conversation: Conversation,
|
||||
chain_pub: bool = False, agent_thought_pub: bool = False):
|
||||
self._channel = PubHandler.generate_channel_name(user, task_id)
|
||||
|
|
@ -282,12 +280,12 @@ class PubHandler:
|
|||
self._agent_thought_pub = agent_thought_pub
|
||||
|
||||
@classmethod
|
||||
def generate_channel_name(cls, user: Union[Account | User], task_id: str):
|
||||
def generate_channel_name(cls, user: Union[Account | EndUser], task_id: str):
|
||||
user_str = 'account-' + user.id if isinstance(user, Account) else 'end-user-' + user.id
|
||||
return "generate_result:{}-{}".format(user_str, task_id)
|
||||
|
||||
@classmethod
|
||||
def generate_stopped_cache_key(cls, user: Union[Account | User], task_id: str):
|
||||
def generate_stopped_cache_key(cls, user: Union[Account | EndUser], task_id: str):
|
||||
user_str = 'account-' + user.id if isinstance(user, Account) else 'end-user-' + user.id
|
||||
return "generate_result_stopped:{}-{}".format(user_str, task_id)
|
||||
|
||||
|
|
@ -366,7 +364,7 @@ class PubHandler:
|
|||
redis_client.publish(self._channel, json.dumps(content))
|
||||
|
||||
@classmethod
|
||||
def pub_error(cls, user: Union[Account | User], task_id: str, e):
|
||||
def pub_error(cls, user: Union[Account | EndUser], task_id: str, e):
|
||||
content = {
|
||||
'error': type(e).__name__,
|
||||
'description': e.description if getattr(e, 'description', None) is not None else str(e)
|
||||
|
|
@ -379,7 +377,7 @@ class PubHandler:
|
|||
return redis_client.get(self._stopped_cache_key) is not None
|
||||
|
||||
@classmethod
|
||||
def stop(cls, user: Union[Account | User], task_id: str):
|
||||
def stop(cls, user: Union[Account | EndUser], task_id: str):
|
||||
stopped_cache_key = cls.generate_stopped_cache_key(user, task_id)
|
||||
redis_client.setex(stopped_cache_key, 600, 1)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
"""Functionality for splitting text."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
List,
|
||||
Optional,
|
||||
)
|
||||
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
|
||||
class FixedRecursiveCharacterTextSplitter(RecursiveCharacterTextSplitter):
|
||||
def __init__(self, fixed_separator: str = "\n\n", separators: Optional[List[str]] = None, **kwargs: Any):
|
||||
"""Create a new TextSplitter."""
|
||||
super().__init__(**kwargs)
|
||||
self._fixed_separator = fixed_separator
|
||||
self._separators = separators or ["\n\n", "\n", " ", ""]
|
||||
|
||||
def split_text(self, text: str) -> List[str]:
|
||||
"""Split incoming text and return chunks."""
|
||||
if self._fixed_separator:
|
||||
chunks = text.split(self._fixed_separator)
|
||||
else:
|
||||
chunks = list(text)
|
||||
|
||||
final_chunks = []
|
||||
for chunk in chunks:
|
||||
if self._length_function(chunk) > self._chunk_size:
|
||||
final_chunks.extend(self.recursive_split_text(chunk))
|
||||
else:
|
||||
final_chunks.append(chunk)
|
||||
|
||||
return final_chunks
|
||||
|
||||
def recursive_split_text(self, text: str) -> List[str]:
|
||||
"""Split incoming text and return chunks."""
|
||||
final_chunks = []
|
||||
# Get appropriate separator to use
|
||||
separator = self._separators[-1]
|
||||
for _s in self._separators:
|
||||
if _s == "":
|
||||
separator = _s
|
||||
break
|
||||
if _s in text:
|
||||
separator = _s
|
||||
break
|
||||
# Now that we have the separator, split the text
|
||||
if separator:
|
||||
splits = text.split(separator)
|
||||
else:
|
||||
splits = list(text)
|
||||
# Now go merging things, recursively splitting longer texts.
|
||||
_good_splits = []
|
||||
for s in splits:
|
||||
if self._length_function(s) < self._chunk_size:
|
||||
_good_splits.append(s)
|
||||
else:
|
||||
if _good_splits:
|
||||
merged_text = self._merge_splits(_good_splits, separator)
|
||||
final_chunks.extend(merged_text)
|
||||
_good_splits = []
|
||||
other_info = self.recursive_split_text(s)
|
||||
final_chunks.extend(other_info)
|
||||
if _good_splits:
|
||||
merged_text = self._merge_splits(_good_splits, separator)
|
||||
final_chunks.extend(merged_text)
|
||||
return final_chunks
|
||||
|
|
@ -18,6 +18,7 @@ from core.docstore.dataset_docstore import DatesetDocumentStore
|
|||
from core.index.keyword_table_index import KeywordTableIndex
|
||||
from core.index.readers.html_parser import HTMLParser
|
||||
from core.index.readers.pdf_parser import PDFParser
|
||||
from core.index.spiltter.fixed_text_splitter import FixedRecursiveCharacterTextSplitter
|
||||
from core.index.vector_index import VectorIndex
|
||||
from core.llm.token_calculator import TokenCalculator
|
||||
from extensions.ext_database import db
|
||||
|
|
@ -267,16 +268,14 @@ class IndexingRunner:
|
|||
raise ValueError("Custom segment length should be between 50 and 1000.")
|
||||
|
||||
separator = segmentation["separator"]
|
||||
if not separator:
|
||||
separators = ["\n\n", "。", ".", " ", ""]
|
||||
else:
|
||||
if separator:
|
||||
separator = separator.replace('\\n', '\n')
|
||||
separators = [separator, ""]
|
||||
|
||||
character_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
|
||||
character_splitter = FixedRecursiveCharacterTextSplitter.from_tiktoken_encoder(
|
||||
chunk_size=segmentation["max_tokens"],
|
||||
chunk_overlap=0,
|
||||
separators=separators
|
||||
fixed_separator=separator,
|
||||
separators=["\n\n", "。", ".", " ", ""]
|
||||
)
|
||||
else:
|
||||
# Automatic segmentation
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
|
||||
from flask import current_app
|
||||
from flask import current_app, request
|
||||
from flask_login import UserMixin
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ class App(db.Model):
|
|||
|
||||
@property
|
||||
def api_base_url(self):
|
||||
return current_app.config['API_URL'] + '/v1'
|
||||
return (current_app.config['API_URL'] if current_app.config['API_URL'] else request.host_url.rstrip('/')) + '/v1'
|
||||
|
||||
@property
|
||||
def tenant(self):
|
||||
|
|
@ -505,7 +505,7 @@ class Site(db.Model):
|
|||
|
||||
@property
|
||||
def app_base_url(self):
|
||||
return current_app.config['APP_URL']
|
||||
return (current_app.config['APP_URL'] if current_app.config['APP_URL'] else request.host_url.rstrip('/'))
|
||||
|
||||
|
||||
class ApiToken(db.Model):
|
||||
|
|
|
|||
|
|
@ -11,12 +11,18 @@ 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.
|
||||
CONSOLE_URL: http://localhost
|
||||
# The URL for Service API endpoints,refers to the base URL of the current API service.
|
||||
API_URL: http://localhost
|
||||
# The URL for Web APP, refers to the Web App base URL of WEB service.
|
||||
APP_URL: http://localhost
|
||||
# The base URL of console application, 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 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
|
||||
# console or api domain.
|
||||
# example: http://udify.app
|
||||
APP_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.
|
||||
|
|
@ -43,16 +49,25 @@ services:
|
|||
# The configurations of celery broker.
|
||||
# Use redis as the broker, and redis db 1 for celery broker.
|
||||
CELERY_BROKER_URL: redis://:difyai123456@redis:6379/1
|
||||
# Specifies the allowed origins for cross-origin requests to the Web API
|
||||
WEB_API_CORS_ALLOW_ORIGINS: http://localhost,*
|
||||
# Specifies the allowed origins for cross-origin requests to the console API
|
||||
CONSOLE_CORS_ALLOW_ORIGINS: http://localhost,*
|
||||
# Specifies the allowed origins for cross-origin requests to the Web API, e.g. https://dify.app or * for all origins.
|
||||
WEB_API_CORS_ALLOW_ORIGINS: '*'
|
||||
# Specifies the allowed origins for cross-origin requests to the console API, e.g. https://cloud.dify.ai or * for all origins.
|
||||
CONSOLE_CORS_ALLOW_ORIGINS: '*'
|
||||
# CSRF Cookie settings
|
||||
# Controls whether a cookie is sent with cross-site requests,
|
||||
# providing some protection against cross-site request forgery attacks
|
||||
#
|
||||
# Default: `SameSite=Lax, Secure=false, HttpOnly=true`
|
||||
# This default configuration supports same-origin requests using either HTTP or HTTPS,
|
||||
# but does not support cross-origin requests. It is suitable for local debugging purposes.
|
||||
#
|
||||
# If you want to enable cross-origin support,
|
||||
# you must use the HTTPS protocol and set the configuration to `SameSite=None, Secure=true, HttpOnly=true`.
|
||||
#
|
||||
# For **production** purposes, please set `SameSite=Lax, Secure=true, HttpOnly=true`.
|
||||
COOKIE_HTTPONLY: 'true'
|
||||
COOKIE_SAMESITE: 'None'
|
||||
COOKIE_SECURE: 'true'
|
||||
COOKIE_SAMESITE: 'Lax'
|
||||
COOKIE_SECURE: 'false'
|
||||
# The type of storage to use for storing user files. Supported values are `local` and `s3`, Default: `local`
|
||||
STORAGE_TYPE: local
|
||||
# The path to the local storage directory, the directory relative the root path of API service codes or absolute path. Default: `storage` or `/home/john/storage`.
|
||||
|
|
@ -86,7 +101,7 @@ services:
|
|||
- weaviate
|
||||
volumes:
|
||||
# Mount the storage directory to the container, for storing user files.
|
||||
- ./volumes/app/storage:/app/storage
|
||||
- ./volumes/app/storage:/app/api/storage
|
||||
|
||||
# worker service
|
||||
# The Celery worker for processing the queue.
|
||||
|
|
@ -104,12 +119,6 @@ services:
|
|||
# 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`.
|
||||
# same as the API service
|
||||
SECRET_KEY: sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U
|
||||
# The base URL of console application, refers to the Console base URL of WEB service.
|
||||
CONSOLE_URL: http://localhost
|
||||
# The URL for Service API endpoints,refers to the base URL of the current API service.
|
||||
API_URL: http://localhost
|
||||
# The URL for Web APP, refers to the Web App base URL of WEB service.
|
||||
APP_URL: http://localhost
|
||||
# The configurations of postgres database connection.
|
||||
# It is consistent with the configuration in the 'db' service below.
|
||||
DB_USERNAME: postgres
|
||||
|
|
@ -137,7 +146,7 @@ services:
|
|||
- weaviate
|
||||
volumes:
|
||||
# Mount the storage directory to the container, for storing user files.
|
||||
- ./volumes/app/storage:/app/storage
|
||||
- ./volumes/app/storage:/app/api/storage
|
||||
|
||||
# Frontend web application.
|
||||
web:
|
||||
|
|
@ -145,10 +154,14 @@ services:
|
|||
restart: always
|
||||
environment:
|
||||
EDITION: SELF_HOSTED
|
||||
# The base URL of console application, refers to the Console base URL of WEB service.
|
||||
CONSOLE_URL: http://localhost
|
||||
# The URL for Web APP, refers to the Web App base URL of WEB service.
|
||||
APP_URL: http://localhost
|
||||
# The base URL of console application, 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 or api domain.
|
||||
# example: http://udify.app
|
||||
APP_URL: ''
|
||||
|
||||
# The postgres database.
|
||||
db:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
server_name _;
|
||||
|
||||
location /console/api {
|
||||
proxy_pass http://api:5001;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"presets": [
|
||||
"@babel/preset-env"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import axios from 'axios'
|
||||
|
||||
const BASE_URL = 'https://api.dify.ai/v1'
|
||||
export const BASE_URL = 'https://api.dify.ai/v1'
|
||||
|
||||
const routes = {
|
||||
export const routes = {
|
||||
application: {
|
||||
method: 'GET',
|
||||
url: () => `/parameters`
|
||||
|
|
@ -17,7 +17,7 @@ const routes = {
|
|||
},
|
||||
createChatMessage: {
|
||||
method: 'POST',
|
||||
url: () => `/chat-message`,
|
||||
url: () => `/chat-messages`,
|
||||
},
|
||||
getConversationMessages: {
|
||||
method: 'GET',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
import { DifyClient, BASE_URL, routes } from ".";
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
jest.mock('axios')
|
||||
|
||||
describe('Client', () => {
|
||||
let difyClient
|
||||
beforeEach(() => {
|
||||
difyClient = new DifyClient('test')
|
||||
})
|
||||
|
||||
test('should create a client', () => {
|
||||
expect(difyClient).toBeDefined();
|
||||
})
|
||||
// test updateApiKey
|
||||
test('should update the api key', () => {
|
||||
difyClient.updateApiKey('test2');
|
||||
expect(difyClient.apiKey).toBe('test2');
|
||||
})
|
||||
});
|
||||
|
||||
describe('Send Requests', () => {
|
||||
let difyClient
|
||||
|
||||
beforeEach(() => {
|
||||
difyClient = new DifyClient('test')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
it('should make a successful request to the application parameter', async () => {
|
||||
const method = 'GET'
|
||||
const endpoint = routes.application.url
|
||||
const expectedResponse = { data: 'response' }
|
||||
axios.mockResolvedValue(expectedResponse)
|
||||
|
||||
await difyClient.sendRequest(method, endpoint)
|
||||
|
||||
expect(axios).toHaveBeenCalledWith({
|
||||
method,
|
||||
url: `${BASE_URL}${endpoint}`,
|
||||
data: null,
|
||||
params: null,
|
||||
headers: {
|
||||
Authorization: `Bearer ${difyClient.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
responseType: 'json',
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
it('should handle errors from the API', async () => {
|
||||
const method = 'GET'
|
||||
const endpoint = '/test-endpoint'
|
||||
const errorMessage = 'Request failed with status code 404'
|
||||
axios.mockRejectedValue(new Error(errorMessage))
|
||||
|
||||
await expect(difyClient.sendRequest(method, endpoint)).rejects.toThrow(
|
||||
errorMessage
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "dify-client",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"description": "This is the Node.js SDK for the Dify.AI API, which allows you to easily integrate Dify.AI into your Node.js applications.",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
|
@ -14,7 +14,21 @@
|
|||
"<crazywoola> <<427733928@qq.com>> (https://github.com/crazywoola)"
|
||||
],
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "jest"
|
||||
},
|
||||
"jest": {
|
||||
"transform": {
|
||||
"^.+\\.[t|j]sx?$": "babel-jest"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.8",
|
||||
"@babel/preset-env": "^7.21.5",
|
||||
"babel-jest": "^29.5.0",
|
||||
"jest": "^29.5.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
'use client'
|
||||
import React from 'react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { t } from 'i18next'
|
||||
import s from './style.module.css'
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
|
||||
type ICopyBtnProps = {
|
||||
value: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const CopyBtn = ({
|
||||
value,
|
||||
className,
|
||||
}: ICopyBtnProps) => {
|
||||
const [isCopied, setIsCopied] = React.useState(false)
|
||||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<Tooltip
|
||||
selector="copy-btn-tooltip"
|
||||
content={(isCopied ? t('appApi.copied') : t('appApi.copy')) as string}
|
||||
className='z-10'
|
||||
>
|
||||
<div
|
||||
className={`box-border p-0.5 flex items-center justify-center rounded-md bg-white cursor-pointer`}
|
||||
style={{
|
||||
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)'
|
||||
}}
|
||||
onClick={() => {
|
||||
copy(value)
|
||||
setIsCopied(true)
|
||||
}}
|
||||
>
|
||||
<div className={`w-6 h-6 hover:bg-gray-50 ${s.copyIcon} ${isCopied ? s.copied : ''}`}></div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyBtn
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
.copyIcon {
|
||||
background-image: url(~@/app/components/develop/secret-key/assets/copy.svg);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.copyIcon:hover {
|
||||
background-image: url(~@/app/components/develop/secret-key/assets/copy-hover.svg);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.copyIcon.copied {
|
||||
background-image: url(~@/app/components/develop/secret-key/assets/copied.svg);
|
||||
}
|
||||
|
|
@ -16,6 +16,8 @@ import type { Annotation, MessageRating } from '@/models/log'
|
|||
import AppContext from '@/context/app-context'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import LoadingAnim from './loading-anim'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import CopyBtn from './copy-btn'
|
||||
|
||||
const stopIcon = (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
|
@ -105,7 +107,7 @@ const MoreInfo: FC<{ more: MessageMore; isQuestion: boolean }> = ({ more, isQues
|
|||
const { t } = useTranslation()
|
||||
return (<div className={`mt-1 space-x-2 text-xs text-gray-400 ${isQuestion ? 'mr-2 text-right ' : 'ml-2 text-left float-right'}`}>
|
||||
<span>{`${t('appLog.detail.timeConsuming')} ${more.latency}${t('appLog.detail.second')}`}</span>
|
||||
<span>{`${t('appLog.detail.tokenCost')} ${more.tokens}`}</span>
|
||||
<span>{`${t('appLog.detail.tokenCost')} ${formatNumber(more.tokens)}`}</span>
|
||||
<span>· </span>
|
||||
<span>{more.time} </span>
|
||||
</div>)
|
||||
|
|
@ -345,6 +347,10 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedba
|
|||
}
|
||||
</div>
|
||||
<div className='absolute top-[-14px] right-[-14px] flex flex-row justify-end gap-1'>
|
||||
<CopyBtn
|
||||
value={content}
|
||||
className={cn(s.copyBtn, 'mr-1')}
|
||||
/>
|
||||
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation(displayScene !== 'console')}
|
||||
{/* Admin feedback is displayed only in the background. */}
|
||||
{!feedbackDisabled && renderFeedbackRating(localAdminFeedback?.rating, false, false)}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,14 @@
|
|||
background: url(./icons/answer.svg) no-repeat;
|
||||
}
|
||||
|
||||
.copyBtn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.answerWrap:hover .copyBtn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.answerWrap .itemOperation {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -247,8 +247,8 @@ const Debug: FC<IDebug> = ({
|
|||
...draft[index],
|
||||
more: {
|
||||
time: dayjs.unix(newResponseItem.created_at).format('hh:mm A'),
|
||||
tokens: newResponseItem.answer_tokens,
|
||||
latency: (newResponseItem.provider_response_latency / 1000).toFixed(2),
|
||||
tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
|
||||
latency: newResponseItem.provider_response_latency.toFixed(2),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@ const getFormattedChatList = (messages: ChatMessage[]) => {
|
|||
isAnswer: true,
|
||||
more: {
|
||||
time: dayjs.unix(item.created_at).format('hh:mm A'),
|
||||
tokens: item.answer_tokens,
|
||||
latency: (item.provider_response_latency / 1000).toFixed(2),
|
||||
tokens: item.answer_tokens + item.message_tokens,
|
||||
latency: item.provider_response_latency.toFixed(2),
|
||||
},
|
||||
annotation: item.annotation,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({
|
|||
<div className='flex flex-col w-full'>
|
||||
<div className='text-gray-900'>{t(`${prefixCustomize}.way1.step2`)}</div>
|
||||
<div className='text-gray-500 text-xs mt-1 mb-2'>{t(`${prefixCustomize}.way1.step2Tip`)}</div>
|
||||
<pre className='box-border py-3 px-4 bg-gray-100 text-xs font-medium rounded-lg'>
|
||||
<pre className='box-border py-3 px-4 bg-gray-100 text-xs font-medium rounded-lg select-text'>
|
||||
export const APP_ID = '{appId}'<br />
|
||||
export const API_KEY = {`'<Web API Key From Dify>'`}
|
||||
</pre>
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export default function AppSelector({ userProfile, onLogout, langeniusVersionInf
|
|||
<Link
|
||||
className={classNames(itemClassName, 'group justify-between')}
|
||||
href={
|
||||
locale === 'zh-Hans' ? 'https://docs.dify.ai/zh-hans/' : 'https://docs.dify.ai/en/'
|
||||
locale === 'zh-Hans' ? 'https://docs.dify.ai/v/zh-hans/' : 'https://docs.dify.ai/'
|
||||
}
|
||||
target='_blank'>
|
||||
<div>{t('common.userProfile.helpCenter')}</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue