diff --git a/.github/workflows/build-api-image.yml b/.github/workflows/build-api-image.yml index 006a8d98ab..4aaebf0863 100644 --- a/.github/workflows/build-api-image.yml +++ b/.github/workflows/build-api-image.yml @@ -5,8 +5,6 @@ on: branches: - 'main' - 'deploy/dev' - pull_request: - types: [synchronize, opened, reopened, ready_for_review] jobs: build-and-push: diff --git a/.github/workflows/build-web-image.yml b/.github/workflows/build-web-image.yml index 69a44f97de..6f097ee7fa 100644 --- a/.github/workflows/build-web-image.yml +++ b/.github/workflows/build-web-image.yml @@ -5,8 +5,6 @@ on: branches: - 'main' - 'deploy/dev' - pull_request: - types: [synchronize, opened, reopened, ready_for_review] jobs: build-and-push: diff --git a/.gitignore b/.gitignore index a3e3d66fed..50f682499e 100644 --- a/.gitignore +++ b/.gitignore @@ -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/* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e96a51072f..de764d6a56 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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:/langgenius-gateway.git +git clone git@github.com:/dify.git ``` ### Install backend diff --git a/README.md b/README.md index 8b8369704e..e80559bcf7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ 简体中文

-[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! diff --git a/README_CN.md b/README_CN.md index b8d659cb06..c72c03bd6a 100644 --- a/README_CN.md +++ b/README_CN.md @@ -5,7 +5,7 @@

-[官方网站](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 ## 贡献代码 diff --git a/api/controllers/console/app/conversation.py b/api/controllers/console/app/conversation.py index 62752deaac..419068468d 100644 --- a/api/controllers/console/app/conversation.py +++ b/api/controllers/console/app/conversation.py @@ -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, diff --git a/api/controllers/console/app/message.py b/api/controllers/console/app/message.py index 27698d965c..88594425de 100644 --- a/api/controllers/console/app/message.py +++ b/api/controllers/console/app/message.py @@ -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//completion-messages//more-like-this') api.add_resource(MessageSuggestedQuestionApi, '/apps//chat-messages//suggested-questions') -api.add_resource(ChatMessageApi, '/apps//chat-messages', endpoint='chat_messages') +api.add_resource(ChatMessageListApi, '/apps//chat-messages', endpoint='console_chat_messages') api.add_resource(MessageFeedbackApi, '/apps//feedbacks') api.add_resource(MessageAnnotationApi, '/apps//annotations') api.add_resource(MessageAnnotationCountApi, '/apps//annotations/count') +api.add_resource(MessageApi, '/apps//messages/', endpoint='console_message') diff --git a/api/core/conversation_message_task.py b/api/core/conversation_message_task.py index 0df26637a3..81477533e7 100644 --- a/api/core/conversation_message_task.py +++ b/api/core/conversation_message_task.py @@ -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) diff --git a/api/core/index/spiltter/fixed_text_splitter.py b/api/core/index/spiltter/fixed_text_splitter.py new file mode 100644 index 0000000000..aaaf8e5a10 --- /dev/null +++ b/api/core/index/spiltter/fixed_text_splitter.py @@ -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 diff --git a/api/core/indexing_runner.py b/api/core/indexing_runner.py index fd9e430116..f06f3a0034 100644 --- a/api/core/indexing_runner.py +++ b/api/core/indexing_runner.py @@ -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 diff --git a/api/models/model.py b/api/models/model.py index 9b4e2a38bc..c9fe5189ee 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -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): diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 40935d2926..a337377cfa 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -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: diff --git a/docker/nginx/conf.d/default.conf b/docker/nginx/conf.d/default.conf index 3b153f40b0..279de0c328 100644 --- a/docker/nginx/conf.d/default.conf +++ b/docker/nginx/conf.d/default.conf @@ -1,6 +1,6 @@ server { listen 80; - server_name localhost; + server_name _; location /console/api { proxy_pass http://api:5001; diff --git a/sdks/nodejs-client/babel.config.json b/sdks/nodejs-client/babel.config.json new file mode 100644 index 0000000000..0639bf7643 --- /dev/null +++ b/sdks/nodejs-client/babel.config.json @@ -0,0 +1,5 @@ +{ + "presets": [ + "@babel/preset-env" + ] +} \ No newline at end of file diff --git a/sdks/nodejs-client/index.js b/sdks/nodejs-client/index.js index 902ecbd880..48b6ea753f 100644 --- a/sdks/nodejs-client/index.js +++ b/sdks/nodejs-client/index.js @@ -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', diff --git a/sdks/nodejs-client/index.test.js b/sdks/nodejs-client/index.test.js new file mode 100644 index 0000000000..e08b8e82af --- /dev/null +++ b/sdks/nodejs-client/index.test.js @@ -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 + ) + }) +}) \ No newline at end of file diff --git a/sdks/nodejs-client/package.json b/sdks/nodejs-client/package.json index 350cfc9732..c658b73d98 100644 --- a/sdks/nodejs-client/package.json +++ b/sdks/nodejs-client/package.json @@ -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 @@ " <<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" } } \ No newline at end of file diff --git a/web/app/components/app/chat/copy-btn/index.tsx b/web/app/components/app/chat/copy-btn/index.tsx new file mode 100644 index 0000000000..f384e714d5 --- /dev/null +++ b/web/app/components/app/chat/copy-btn/index.tsx @@ -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 ( +
+ +
{ + copy(value) + setIsCopied(true) + }} + > +
+
+
+
+ ) +} + +export default CopyBtn diff --git a/web/app/components/app/chat/copy-btn/style.module.css b/web/app/components/app/chat/copy-btn/style.module.css new file mode 100644 index 0000000000..56c756025b --- /dev/null +++ b/web/app/components/app/chat/copy-btn/style.module.css @@ -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); +} \ No newline at end of file diff --git a/web/app/components/app/chat/index.tsx b/web/app/components/app/chat/index.tsx index 7b512c57d4..38d85edc4c 100644 --- a/web/app/components/app/chat/index.tsx +++ b/web/app/components/app/chat/index.tsx @@ -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 = ( @@ -105,7 +107,7 @@ const MoreInfo: FC<{ more: MessageMore; isQuestion: boolean }> = ({ more, isQues const { t } = useTranslation() return (
{`${t('appLog.detail.timeConsuming')} ${more.latency}${t('appLog.detail.second')}`} - {`${t('appLog.detail.tokenCost')} ${more.tokens}`} + {`${t('appLog.detail.tokenCost')} ${formatNumber(more.tokens)}`} · {more.time}
) @@ -345,6 +347,10 @@ const Answer: FC = ({ item, feedbackDisabled = false, isHideFeedba }
+ {!feedbackDisabled && !item.feedbackDisabled && renderItemOperation(displayScene !== 'console')} {/* Admin feedback is displayed only in the background. */} {!feedbackDisabled && renderFeedbackRating(localAdminFeedback?.rating, false, false)} diff --git a/web/app/components/app/chat/style.module.css b/web/app/components/app/chat/style.module.css index 0744311c6d..68e3f100d4 100644 --- a/web/app/components/app/chat/style.module.css +++ b/web/app/components/app/chat/style.module.css @@ -38,6 +38,14 @@ background: url(./icons/answer.svg) no-repeat; } +.copyBtn { + display: none; +} + +.answerWrap:hover .copyBtn { + display: block; +} + .answerWrap .itemOperation { display: none; } diff --git a/web/app/components/app/configuration/debug/index.tsx b/web/app/components/app/configuration/debug/index.tsx index c7ac273886..a4d19be265 100644 --- a/web/app/components/app/configuration/debug/index.tsx +++ b/web/app/components/app/configuration/debug/index.tsx @@ -247,8 +247,8 @@ const Debug: FC = ({ ...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), } } } diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index cfde271d53..e29eafa6f0 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -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, }) diff --git a/web/app/components/app/overview/customize/index.tsx b/web/app/components/app/overview/customize/index.tsx index a1d46ff0b2..0612c952c9 100644 --- a/web/app/components/app/overview/customize/index.tsx +++ b/web/app/components/app/overview/customize/index.tsx @@ -71,7 +71,7 @@ const CustomizeModal: FC = ({
{t(`${prefixCustomize}.way1.step2`)}
{t(`${prefixCustomize}.way1.step2Tip`)}
-
+          
             export const APP_ID = '{appId}'
export const API_KEY = {`''`}
diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx index 2be1ad5559..b348ca1eca 100644 --- a/web/app/components/header/account-dropdown/index.tsx +++ b/web/app/components/header/account-dropdown/index.tsx @@ -89,7 +89,7 @@ export default function AppSelector({ userProfile, onLogout, langeniusVersionInf
{t('common.userProfile.helpCenter')}