From 07b5bbae06acdac30493915a81facf72c8825264 Mon Sep 17 00:00:00 2001 From: kurokobo Date: Wed, 20 Nov 2024 10:32:59 +0900 Subject: [PATCH 001/103] feat: add a minimal separator between pinned apps and unpinned apps in the explore page (#10871) --- web/app/components/explore/sidebar/index.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/web/app/components/explore/sidebar/index.tsx b/web/app/components/explore/sidebar/index.tsx index a4a40a00a2..13d5a0ec8f 100644 --- a/web/app/components/explore/sidebar/index.tsx +++ b/web/app/components/explore/sidebar/index.tsx @@ -11,6 +11,7 @@ import cn from '@/utils/classnames' import { fetchInstalledAppList as doFetchInstalledAppList, uninstallApp, updatePinStatus } from '@/service/explore' import ExploreContext from '@/context/explore-context' import Confirm from '@/app/components/base/confirm' +import Divider from '@/app/components/base/divider' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' const SelectedDiscoveryIcon = () => ( @@ -89,6 +90,7 @@ const SideBar: FC = ({ fetchInstalledAppList() }, [controlUpdateInstalledApps]) + const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length return (
@@ -109,10 +111,9 @@ const SideBar: FC = ({ height: 'calc(100vh - 250px)', }} > - {installedApps.map(({ id, is_pinned, uninstallable, app: { name, icon_type, icon, icon_url, icon_background } }) => { - return ( + {installedApps.map(({ id, is_pinned, uninstallable, app: { name, icon_type, icon, icon_url, icon_background } }, index) => ( + = ({ setShowConfirm(true) }} /> - ) - })} + {index === pinnedAppsCount - 1 && index !== installedApps.length - 1 && } + + ))}
)} From 7e66e5a713b96fd9fcc17f89c74c32151872e581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Wed, 20 Nov 2024 10:07:30 +0800 Subject: [PATCH 002/103] feat: make toc panel can collapse (#10875) --- web/app/components/develop/doc.tsx | 58 ++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/web/app/components/develop/doc.tsx b/web/app/components/develop/doc.tsx index abf54fd39d..ce5471676d 100644 --- a/web/app/components/develop/doc.tsx +++ b/web/app/components/develop/doc.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react' import { useContext } from 'use-context-selector' import { useTranslation } from 'react-i18next' +import { RiListUnordered } from '@remixicon/react' import TemplateEn from './template/template.en.mdx' import TemplateZh from './template/template.zh.mdx' import TemplateAdvancedChatEn from './template/template_advanced_chat.en.mdx' @@ -21,6 +22,7 @@ const Doc = ({ appDetail }: IDocProps) => { const { locale } = useContext(I18n) const { t } = useTranslation() const [toc, setToc] = useState>([]) + const [isTocExpanded, setIsTocExpanded] = useState(false) const variables = appDetail?.model_config?.configs?.prompt_variables || [] const inputs = variables.reduce((res: any, variable: any) => { @@ -28,6 +30,11 @@ const Doc = ({ appDetail }: IDocProps) => { return res }, {}) + useEffect(() => { + const mediaQuery = window.matchMedia('(min-width: 1280px)') + setIsTocExpanded(mediaQuery.matches) + }, []) + useEffect(() => { const extractTOC = () => { const article = document.querySelector('article') @@ -53,21 +60,42 @@ const Doc = ({ appDetail }: IDocProps) => { return (
- +
+ {isTocExpanded + ? ( + + ) + : ( + + )} +
{(appDetail?.mode === 'chat' || appDetail?.mode === 'agent-chat') && ( locale !== LanguagesSupported[1] ? : From fbfc811a447b46bea05c58177551903c737dc1a6 Mon Sep 17 00:00:00 2001 From: GeorgeCaoJ Date: Wed, 20 Nov 2024 11:15:19 +0800 Subject: [PATCH 003/103] feat: support function call for ollama block chat api (#10784) --- .../model_providers/ollama/llm/llm.py | 68 +++++++++++++++++-- .../model_providers/ollama/ollama.yaml | 19 ++++++ 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/api/core/model_runtime/model_providers/ollama/llm/llm.py b/api/core/model_runtime/model_providers/ollama/llm/llm.py index a7ea53e0e9..094a674645 100644 --- a/api/core/model_runtime/model_providers/ollama/llm/llm.py +++ b/api/core/model_runtime/model_providers/ollama/llm/llm.py @@ -22,6 +22,7 @@ from core.model_runtime.entities.message_entities import ( PromptMessageTool, SystemPromptMessage, TextPromptMessageContent, + ToolPromptMessage, UserPromptMessage, ) from core.model_runtime.entities.model_entities import ( @@ -86,6 +87,7 @@ class OllamaLargeLanguageModel(LargeLanguageModel): credentials=credentials, prompt_messages=prompt_messages, model_parameters=model_parameters, + tools=tools, stop=stop, stream=stream, user=user, @@ -153,6 +155,7 @@ class OllamaLargeLanguageModel(LargeLanguageModel): credentials: dict, prompt_messages: list[PromptMessage], model_parameters: dict, + tools: Optional[list[PromptMessageTool]] = None, stop: Optional[list[str]] = None, stream: bool = True, user: Optional[str] = None, @@ -196,6 +199,8 @@ class OllamaLargeLanguageModel(LargeLanguageModel): if completion_type is LLMMode.CHAT: endpoint_url = urljoin(endpoint_url, "api/chat") data["messages"] = [self._convert_prompt_message_to_dict(m) for m in prompt_messages] + if tools: + data["tools"] = [self._convert_prompt_message_tool_to_dict(tool) for tool in tools] else: endpoint_url = urljoin(endpoint_url, "api/generate") first_prompt_message = prompt_messages[0] @@ -232,7 +237,7 @@ class OllamaLargeLanguageModel(LargeLanguageModel): if stream: return self._handle_generate_stream_response(model, credentials, completion_type, response, prompt_messages) - return self._handle_generate_response(model, credentials, completion_type, response, prompt_messages) + return self._handle_generate_response(model, credentials, completion_type, response, prompt_messages, tools) def _handle_generate_response( self, @@ -241,6 +246,7 @@ class OllamaLargeLanguageModel(LargeLanguageModel): completion_type: LLMMode, response: requests.Response, prompt_messages: list[PromptMessage], + tools: Optional[list[PromptMessageTool]], ) -> LLMResult: """ Handle llm completion response @@ -253,14 +259,16 @@ class OllamaLargeLanguageModel(LargeLanguageModel): :return: llm result """ response_json = response.json() - + tool_calls = [] if completion_type is LLMMode.CHAT: message = response_json.get("message", {}) response_content = message.get("content", "") + response_tool_calls = message.get("tool_calls", []) + tool_calls = [self._extract_response_tool_call(tool_call) for tool_call in response_tool_calls] else: response_content = response_json["response"] - assistant_message = AssistantPromptMessage(content=response_content) + assistant_message = AssistantPromptMessage(content=response_content, tool_calls=tool_calls) if "prompt_eval_count" in response_json and "eval_count" in response_json: # transform usage @@ -405,9 +413,28 @@ class OllamaLargeLanguageModel(LargeLanguageModel): chunk_index += 1 + def _convert_prompt_message_tool_to_dict(self, tool: PromptMessageTool) -> dict: + """ + Convert PromptMessageTool to dict for Ollama API + + :param tool: tool + :return: tool dict + """ + return { + "type": "function", + "function": { + "name": tool.name, + "description": tool.description, + "parameters": tool.parameters, + }, + } + def _convert_prompt_message_to_dict(self, message: PromptMessage) -> dict: """ Convert PromptMessage to dict for Ollama API + + :param message: prompt message + :return: message dict """ if isinstance(message, UserPromptMessage): message = cast(UserPromptMessage, message) @@ -432,6 +459,9 @@ class OllamaLargeLanguageModel(LargeLanguageModel): elif isinstance(message, SystemPromptMessage): message = cast(SystemPromptMessage, message) message_dict = {"role": "system", "content": message.content} + elif isinstance(message, ToolPromptMessage): + message = cast(ToolPromptMessage, message) + message_dict = {"role": "tool", "content": message.content} else: raise ValueError(f"Got unknown type {message}") @@ -452,6 +482,29 @@ class OllamaLargeLanguageModel(LargeLanguageModel): return num_tokens + def _extract_response_tool_call(self, response_tool_call: dict) -> AssistantPromptMessage.ToolCall: + """ + Extract response tool call + """ + tool_call = None + if response_tool_call and "function" in response_tool_call: + # Convert arguments to JSON string if it's a dict + arguments = response_tool_call.get("function").get("arguments") + if isinstance(arguments, dict): + arguments = json.dumps(arguments) + + function = AssistantPromptMessage.ToolCall.ToolCallFunction( + name=response_tool_call.get("function").get("name"), + arguments=arguments, + ) + tool_call = AssistantPromptMessage.ToolCall( + id=response_tool_call.get("function").get("name"), + type="function", + function=function, + ) + + return tool_call + def get_customizable_model_schema(self, model: str, credentials: dict) -> AIModelEntity: """ Get customizable model schema. @@ -461,10 +514,15 @@ class OllamaLargeLanguageModel(LargeLanguageModel): :return: model schema """ - extras = {} + extras = { + "features": [], + } if "vision_support" in credentials and credentials["vision_support"] == "true": - extras["features"] = [ModelFeature.VISION] + extras["features"].append(ModelFeature.VISION) + if "function_call_support" in credentials and credentials["function_call_support"] == "true": + extras["features"].append(ModelFeature.TOOL_CALL) + extras["features"].append(ModelFeature.MULTI_TOOL_CALL) entity = AIModelEntity( model=model, diff --git a/api/core/model_runtime/model_providers/ollama/ollama.yaml b/api/core/model_runtime/model_providers/ollama/ollama.yaml index 33747753bd..6560fcd180 100644 --- a/api/core/model_runtime/model_providers/ollama/ollama.yaml +++ b/api/core/model_runtime/model_providers/ollama/ollama.yaml @@ -96,3 +96,22 @@ model_credential_schema: label: en_US: 'No' zh_Hans: 否 + - variable: function_call_support + label: + zh_Hans: 是否支持函数调用 + en_US: Function call support + show_on: + - variable: __model_type + value: llm + default: 'false' + type: radio + required: false + options: + - value: 'true' + label: + en_US: 'Yes' + zh_Hans: 是 + - value: 'false' + label: + en_US: 'No' + zh_Hans: 否 From beb7953d38e7562da09e463d79368388187822c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Wed, 20 Nov 2024 11:24:45 +0800 Subject: [PATCH 004/103] feat: enhance the custom note (#8885) --- web/app/components/workflow/note-node/index.tsx | 1 - web/app/components/workflow/style.css | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/web/app/components/workflow/note-node/index.tsx b/web/app/components/workflow/note-node/index.tsx index ec2bb84f68..6d62b452e4 100644 --- a/web/app/components/workflow/note-node/index.tsx +++ b/web/app/components/workflow/note-node/index.tsx @@ -81,7 +81,6 @@ const NoteNode = ({ nodeData={data} icon={} minWidth={240} - maxWidth={640} minHeight={88} />
diff --git a/web/app/components/workflow/style.css b/web/app/components/workflow/style.css index 9ec8586ccc..ca1d24a52e 100644 --- a/web/app/components/workflow/style.css +++ b/web/app/components/workflow/style.css @@ -15,4 +15,8 @@ #workflow-container .react-flow__selection { border: 1px solid #528BFF; background: rgba(21, 94, 239, 0.05); +} + +#workflow-container .react-flow__node-custom-note { + z-index: -1000 !important; } \ No newline at end of file From d18754afdd3c2f792d14d29dbcccf36a5b834a87 Mon Sep 17 00:00:00 2001 From: Jason Tan Date: Wed, 20 Nov 2024 11:29:49 +0800 Subject: [PATCH 005/103] feat: admin can also change member role (#10651) --- .../account-setting/members-page/index.tsx | 7 +++---- .../members-page/operation/index.tsx | 21 ++++++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/web/app/components/header/account-setting/members-page/index.tsx b/web/app/components/header/account-setting/members-page/index.tsx index e09e4bbc0d..03d65af7a4 100644 --- a/web/app/components/header/account-setting/members-page/index.tsx +++ b/web/app/components/header/account-setting/members-page/index.tsx @@ -34,13 +34,12 @@ const MembersPage = () => { } const { locale } = useContext(I18n) - const { userProfile, currentWorkspace, isCurrentWorkspaceManager } = useAppContext() + const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext() const { data, mutate } = useSWR({ url: '/workspaces/current/members' }, fetchMembers) const [inviteModalVisible, setInviteModalVisible] = useState(false) const [invitationResults, setInvitationResults] = useState([]) const [invitedModalVisible, setInvitedModalVisible] = useState(false) const accounts = data?.accounts || [] - const owner = accounts.filter(account => account.role === 'owner')?.[0]?.email === userProfile.email const { plan, enableBilling } = useProviderContext() const isNotUnlimitedMemberPlan = enableBilling && plan.type !== Plan.team && plan.type !== Plan.enterprise const isMemberFull = enableBilling && isNotUnlimitedMemberPlan && accounts.length >= plan.total.teamMembers @@ -109,8 +108,8 @@ const MembersPage = () => {
{dayjs(Number((account.last_active_at || account.created_at)) * 1000).locale(locale === 'zh-Hans' ? 'zh-cn' : 'en').fromNow()}
{ - (owner && account.role !== 'owner') - ? + ((isCurrentWorkspaceOwner && account.role !== 'owner') || (isCurrentWorkspaceManager && !['owner', 'admin'].includes(account.role))) + ? :
{RoleMap[account.role] || RoleMap.normal}
}
diff --git a/web/app/components/header/account-setting/members-page/operation/index.tsx b/web/app/components/header/account-setting/members-page/operation/index.tsx index e1fe25cb96..82867ec522 100644 --- a/web/app/components/header/account-setting/members-page/operation/index.tsx +++ b/web/app/components/header/account-setting/members-page/operation/index.tsx @@ -26,11 +26,13 @@ const itemDescClassName = ` type IOperationProps = { member: Member + operatorRole: string onOperate: () => void } const Operation = ({ member, + operatorRole, onOperate, }: IOperationProps) => { const { t } = useTranslation() @@ -43,11 +45,20 @@ const Operation = ({ dataset_operator: t('common.members.datasetOperator'), } const roleList = useMemo(() => { - return [ - ...['admin', 'editor', 'normal'], - ...(datasetOperatorEnabled ? ['dataset_operator'] : []), - ] - }, [datasetOperatorEnabled]) + if (operatorRole === 'owner') { + return [ + ...['admin', 'editor', 'normal'], + ...(datasetOperatorEnabled ? ['dataset_operator'] : []), + ] + } + if (operatorRole === 'admin') { + return [ + ...['editor', 'normal'], + ...(datasetOperatorEnabled ? ['dataset_operator'] : []), + ] + } + return [] + }, [operatorRole, datasetOperatorEnabled]) const { notify } = useContext(ToastContext) const toHump = (name: string) => name.replace(/_(\w)/g, (all, letter) => letter.toUpperCase()) const handleDeleteMemberOrCancelInvitation = async () => { From 464cc26ccfad1cddd58f2ac58e33eeca5c937d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AC=BC=E9=A0=AD=E6=8B=93=E6=B5=B7?= <105633876+kitotakumi@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:30:25 +0900 Subject: [PATCH 006/103] Fix : Add a process to fetch the mime type from the file name for signed url in remote_url (#10872) --- api/factories/file_factory.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/factories/file_factory.py b/api/factories/file_factory.py index 94bbeebd6d..8cb45f194b 100644 --- a/api/factories/file_factory.py +++ b/api/factories/file_factory.py @@ -169,6 +169,7 @@ def _get_remote_file_info(url: str): mime_type = mimetypes.guess_type(url)[0] or "" file_size = -1 filename = url.split("/")[-1].split("?")[0] or "unknown_file" + mime_type = mime_type or mimetypes.guess_type(filename)[0] resp = ssrf_proxy.head(url, follow_redirects=True) if resp.status_code == httpx.codes.OK: From 33cfc56ad0e53e3e7e343d27adede936d40f378a Mon Sep 17 00:00:00 2001 From: Muntaser Abuzaid Date: Wed, 20 Nov 2024 06:33:02 +0200 Subject: [PATCH 007/103] fix: update email validation regex to allow periods in local part (#10868) --- api/core/tools/provider/builtin/email/tools/send_mail.py | 2 +- api/core/tools/provider/builtin/email/tools/send_mail_batch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/core/tools/provider/builtin/email/tools/send_mail.py b/api/core/tools/provider/builtin/email/tools/send_mail.py index d51d5439b7..33c040400c 100644 --- a/api/core/tools/provider/builtin/email/tools/send_mail.py +++ b/api/core/tools/provider/builtin/email/tools/send_mail.py @@ -17,7 +17,7 @@ class SendMailTool(BuiltinTool): invoke tools """ sender = self.runtime.credentials.get("email_account", "") - email_rgx = re.compile(r"^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$") + email_rgx = re.compile(r"^[a-zA-Z0-9._-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$") password = self.runtime.credentials.get("email_password", "") smtp_server = self.runtime.credentials.get("smtp_server", "") if not smtp_server: diff --git a/api/core/tools/provider/builtin/email/tools/send_mail_batch.py b/api/core/tools/provider/builtin/email/tools/send_mail_batch.py index ff7e176990..537dedb27d 100644 --- a/api/core/tools/provider/builtin/email/tools/send_mail_batch.py +++ b/api/core/tools/provider/builtin/email/tools/send_mail_batch.py @@ -18,7 +18,7 @@ class SendMailTool(BuiltinTool): invoke tools """ sender = self.runtime.credentials.get("email_account", "") - email_rgx = re.compile(r"^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$") + email_rgx = re.compile(r"^[a-zA-Z0-9._-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$") password = self.runtime.credentials.get("email_password", "") smtp_server = self.runtime.credentials.get("smtp_server", "") if not smtp_server: From f3af7b5f35b36fb1b7065eae31bc48be8056159f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Wed, 20 Nov 2024 12:54:24 +0800 Subject: [PATCH 008/103] fix: tool's file input display string (#10887) --- .../workflow/nodes/tool/components/input-var-list.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/app/components/workflow/nodes/tool/components/input-var-list.tsx b/web/app/components/workflow/nodes/tool/components/input-var-list.tsx index e47082f4b7..10c534509c 100644 --- a/web/app/components/workflow/nodes/tool/components/input-var-list.tsx +++ b/web/app/components/workflow/nodes/tool/components/input-var-list.tsx @@ -46,6 +46,8 @@ const InputVarList: FC = ({ const paramType = (type: string) => { if (type === FormTypeEnum.textNumber) return 'Number' + else if (type === FormTypeEnum.file) + return 'File' else if (type === FormTypeEnum.files) return 'Files' else if (type === FormTypeEnum.select) From 25fda7adc57e0d2852c02dfa407f14eb60a2ceac Mon Sep 17 00:00:00 2001 From: -LAN- Date: Wed, 20 Nov 2024 12:55:06 +0800 Subject: [PATCH 009/103] fix(http_request): allow content type `application/x-javascript` (#10862) --- .../workflow/nodes/http_request/entities.py | 60 ++++++-- .../nodes/http_request/test_entities.py | 140 ++++++++++++++++++ 2 files changed, 187 insertions(+), 13 deletions(-) create mode 100644 api/tests/unit_tests/core/workflow/nodes/http_request/test_entities.py diff --git a/api/core/workflow/nodes/http_request/entities.py b/api/core/workflow/nodes/http_request/entities.py index 36ded104c1..5e39ef79d1 100644 --- a/api/core/workflow/nodes/http_request/entities.py +++ b/api/core/workflow/nodes/http_request/entities.py @@ -1,4 +1,6 @@ +import mimetypes from collections.abc import Sequence +from email.message import Message from typing import Any, Literal, Optional import httpx @@ -7,14 +9,6 @@ from pydantic import BaseModel, Field, ValidationInfo, field_validator from configs import dify_config from core.workflow.nodes.base import BaseNodeData -NON_FILE_CONTENT_TYPES = ( - "application/json", - "application/xml", - "text/html", - "text/plain", - "application/x-www-form-urlencoded", -) - class HttpRequestNodeAuthorizationConfig(BaseModel): type: Literal["basic", "bearer", "custom"] @@ -93,13 +87,53 @@ class Response: @property def is_file(self): - content_type = self.content_type + """ + Determine if the response contains a file by checking: + 1. Content-Disposition header (RFC 6266) + 2. Content characteristics + 3. MIME type analysis + """ + content_type = self.content_type.split(";")[0].strip().lower() content_disposition = self.response.headers.get("content-disposition", "") - return "attachment" in content_disposition or ( - not any(non_file in content_type for non_file in NON_FILE_CONTENT_TYPES) - and any(file_type in content_type for file_type in ("application/", "image/", "audio/", "video/")) - ) + # Check if it's explicitly marked as an attachment + if content_disposition: + msg = Message() + msg["content-disposition"] = content_disposition + disp_type = msg.get_content_disposition() # Returns 'attachment', 'inline', or None + filename = msg.get_filename() # Returns filename if present, None otherwise + if disp_type == "attachment" or filename is not None: + return True + + # For application types, try to detect if it's a text-based format + if content_type.startswith("application/"): + # Common text-based application types + if any( + text_type in content_type + for text_type in ("json", "xml", "javascript", "x-www-form-urlencoded", "yaml", "graphql") + ): + return False + + # Try to detect if content is text-based by sampling first few bytes + try: + # Sample first 1024 bytes for text detection + content_sample = self.response.content[:1024] + content_sample.decode("utf-8") + # If we can decode as UTF-8 and find common text patterns, likely not a file + text_markers = (b"{", b"[", b"<", b"function", b"var ", b"const ", b"let ") + if any(marker in content_sample for marker in text_markers): + return False + except UnicodeDecodeError: + # If we can't decode as UTF-8, likely a binary file + return True + + # For other types, use MIME type analysis + main_type, _ = mimetypes.guess_type("dummy" + (mimetypes.guess_extension(content_type) or "")) + if main_type: + return main_type.split("/")[0] in ("application", "image", "audio", "video") + + # For unknown types, check if it's a media type + return any(media_type in content_type for media_type in ("image/", "audio/", "video/")) @property def content_type(self) -> str: diff --git a/api/tests/unit_tests/core/workflow/nodes/http_request/test_entities.py b/api/tests/unit_tests/core/workflow/nodes/http_request/test_entities.py new file mode 100644 index 0000000000..0f6b7e4ab6 --- /dev/null +++ b/api/tests/unit_tests/core/workflow/nodes/http_request/test_entities.py @@ -0,0 +1,140 @@ +from unittest.mock import Mock, PropertyMock, patch + +import httpx +import pytest + +from core.workflow.nodes.http_request.entities import Response + + +@pytest.fixture +def mock_response(): + response = Mock(spec=httpx.Response) + response.headers = {} + return response + + +def test_is_file_with_attachment_disposition(mock_response): + """Test is_file when content-disposition header contains 'attachment'""" + mock_response.headers = {"content-disposition": "attachment; filename=test.pdf", "content-type": "application/pdf"} + response = Response(mock_response) + assert response.is_file + + +def test_is_file_with_filename_disposition(mock_response): + """Test is_file when content-disposition header contains filename parameter""" + mock_response.headers = {"content-disposition": "inline; filename=test.pdf", "content-type": "application/pdf"} + response = Response(mock_response) + assert response.is_file + + +@pytest.mark.parametrize("content_type", ["application/pdf", "image/jpeg", "audio/mp3", "video/mp4"]) +def test_is_file_with_file_content_types(mock_response, content_type): + """Test is_file with various file content types""" + mock_response.headers = {"content-type": content_type} + # Mock binary content + type(mock_response).content = PropertyMock(return_value=bytes([0x00, 0xFF] * 512)) + response = Response(mock_response) + assert response.is_file, f"Content type {content_type} should be identified as a file" + + +@pytest.mark.parametrize( + "content_type", + [ + "application/json", + "application/xml", + "application/javascript", + "application/x-www-form-urlencoded", + "application/yaml", + "application/graphql", + ], +) +def test_text_based_application_types(mock_response, content_type): + """Test common text-based application types are not identified as files""" + mock_response.headers = {"content-type": content_type} + response = Response(mock_response) + assert not response.is_file, f"Content type {content_type} should not be identified as a file" + + +@pytest.mark.parametrize( + ("content", "content_type"), + [ + (b'{"key": "value"}', "application/octet-stream"), + (b"[1, 2, 3]", "application/unknown"), + (b"function test() {}", "application/x-unknown"), + (b"test", "application/binary"), + (b"var x = 1;", "application/data"), + ], +) +def test_content_based_detection(mock_response, content, content_type): + """Test content-based detection for text-like content""" + mock_response.headers = {"content-type": content_type} + type(mock_response).content = PropertyMock(return_value=content) + response = Response(mock_response) + assert not response.is_file, f"Content {content} with type {content_type} should not be identified as a file" + + +@pytest.mark.parametrize( + ("content", "content_type"), + [ + (bytes([0x00, 0xFF] * 512), "application/octet-stream"), + (bytes([0x89, 0x50, 0x4E, 0x47]), "application/unknown"), # PNG magic numbers + (bytes([0xFF, 0xD8, 0xFF]), "application/binary"), # JPEG magic numbers + ], +) +def test_binary_content_detection(mock_response, content, content_type): + """Test content-based detection for binary content""" + mock_response.headers = {"content-type": content_type} + type(mock_response).content = PropertyMock(return_value=content) + response = Response(mock_response) + assert response.is_file, f"Binary content with type {content_type} should be identified as a file" + + +@pytest.mark.parametrize( + ("content_type", "expected_main_type"), + [ + ("x-world/x-vrml", "model"), # VRML 3D model + ("font/ttf", "application"), # TrueType font + ("text/csv", "text"), # CSV text file + ("unknown/xyz", None), # Unknown type + ], +) +def test_mimetype_based_detection(mock_response, content_type, expected_main_type): + """Test detection using mimetypes.guess_type for non-application content types""" + mock_response.headers = {"content-type": content_type} + type(mock_response).content = PropertyMock(return_value=bytes([0x00])) # Dummy content + + with patch("core.workflow.nodes.http_request.entities.mimetypes.guess_type") as mock_guess_type: + # Mock the return value based on expected_main_type + if expected_main_type: + mock_guess_type.return_value = (f"{expected_main_type}/subtype", None) + else: + mock_guess_type.return_value = (None, None) + + response = Response(mock_response) + + # Check if the result matches our expectation + if expected_main_type in ("application", "image", "audio", "video"): + assert response.is_file, f"Content type {content_type} should be identified as a file" + else: + assert not response.is_file, f"Content type {content_type} should not be identified as a file" + + # Verify that guess_type was called + mock_guess_type.assert_called_once() + + +def test_is_file_with_inline_disposition(mock_response): + """Test is_file when content-disposition is 'inline'""" + mock_response.headers = {"content-disposition": "inline", "content-type": "application/pdf"} + # Mock binary content + type(mock_response).content = PropertyMock(return_value=bytes([0x00, 0xFF] * 512)) + response = Response(mock_response) + assert response.is_file + + +def test_is_file_with_no_content_disposition(mock_response): + """Test is_file when no content-disposition header is present""" + mock_response.headers = {"content-type": "application/pdf"} + # Mock binary content + type(mock_response).content = PropertyMock(return_value=bytes([0x00, 0xFF] * 512)) + response = Response(mock_response) + assert response.is_file From bf4b6e5f8018d2d1f1e64f492d5e15212901dffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Wed, 20 Nov 2024 13:26:42 +0800 Subject: [PATCH 010/103] feat: support custom tool upload file (#10796) --- api/core/tools/tool/api_tool.py | 13 ++++++++++--- api/core/tools/utils/parser.py | 3 +++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/api/core/tools/tool/api_tool.py b/api/core/tools/tool/api_tool.py index c779d704c3..0b4c5bd2c6 100644 --- a/api/core/tools/tool/api_tool.py +++ b/api/core/tools/tool/api_tool.py @@ -5,6 +5,7 @@ from urllib.parse import urlencode import httpx +from core.file.file_manager import download from core.helper import ssrf_proxy from core.tools.entities.tool_bundle import ApiToolBundle from core.tools.entities.tool_entities import ToolInvokeMessage, ToolProviderType @@ -138,6 +139,7 @@ class ApiTool(Tool): path_params = {} body = {} cookies = {} + files = [] # check parameters for parameter in self.api_bundle.openapi.get("parameters", []): @@ -166,8 +168,12 @@ class ApiTool(Tool): properties = body_schema.get("properties", {}) for name, property in properties.items(): if name in parameters: - # convert type - body[name] = self._convert_body_property_type(property, parameters[name]) + if property.get("format") == "binary": + f = parameters[name] + files.append((name, (f.filename, download(f), f.mime_type))) + else: + # convert type + body[name] = self._convert_body_property_type(property, parameters[name]) elif name in required: raise ToolParameterValidationError( f"Missing required parameter {name} in operation {self.api_bundle.operation_id}" @@ -182,7 +188,7 @@ class ApiTool(Tool): for name, value in path_params.items(): url = url.replace(f"{{{name}}}", f"{value}") - # parse http body data if needed, for GET/HEAD/OPTIONS/TRACE, the body is ignored + # parse http body data if needed if "Content-Type" in headers: if headers["Content-Type"] == "application/json": body = json.dumps(body) @@ -198,6 +204,7 @@ class ApiTool(Tool): headers=headers, cookies=cookies, data=body, + files=files, timeout=API_TOOL_DEFAULT_TIMEOUT, follow_redirects=True, ) diff --git a/api/core/tools/utils/parser.py b/api/core/tools/utils/parser.py index 5867a11bb3..ae44b1b99d 100644 --- a/api/core/tools/utils/parser.py +++ b/api/core/tools/utils/parser.py @@ -161,6 +161,9 @@ class ApiBasedToolSchemaParser: def _get_tool_parameter_type(parameter: dict) -> ToolParameter.ToolParameterType: parameter = parameter or {} typ = None + if parameter.get("format") == "binary": + return ToolParameter.ToolParameterType.FILE + if "type" in parameter: typ = parameter["type"] elif "schema" in parameter and "type" in parameter["schema"]: From 8ff65abbc6631530f2aaa54584b8f941c13cc2f2 Mon Sep 17 00:00:00 2001 From: liuhaoran <75237518+liuhaoran1212@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:44:35 +0800 Subject: [PATCH 011/103] ext_redis.py support redis clusters --- Fixes #9538 (#9789) Signed-off-by: root Co-authored-by: root Co-authored-by: Bowen Liang --- api/.env.example | 5 ++++ api/configs/middleware/cache/redis_config.py | 15 ++++++++++++ api/extensions/ext_redis.py | 9 ++++++- .../unit_tests/core/test_model_manager.py | 24 ++++++++++++------- docker/.env.example | 6 +++++ docker/docker-compose.yaml | 3 +++ 6 files changed, 52 insertions(+), 10 deletions(-) diff --git a/api/.env.example b/api/.env.example index 1a242a3daa..f8a2812563 100644 --- a/api/.env.example +++ b/api/.env.example @@ -42,6 +42,11 @@ REDIS_SENTINEL_USERNAME= REDIS_SENTINEL_PASSWORD= REDIS_SENTINEL_SOCKET_TIMEOUT=0.1 +# redis Cluster configuration. +REDIS_USE_CLUSTERS=false +REDIS_CLUSTERS= +REDIS_CLUSTERS_PASSWORD= + # PostgreSQL database configuration DB_USERNAME=postgres DB_PASSWORD=difyai123456 diff --git a/api/configs/middleware/cache/redis_config.py b/api/configs/middleware/cache/redis_config.py index 26b9b1347c..2e98c31ec3 100644 --- a/api/configs/middleware/cache/redis_config.py +++ b/api/configs/middleware/cache/redis_config.py @@ -68,3 +68,18 @@ class RedisConfig(BaseSettings): description="Socket timeout in seconds for Redis Sentinel connections", default=0.1, ) + + REDIS_USE_CLUSTERS: bool = Field( + description="Enable Redis Clusters mode for high availability", + default=False, + ) + + REDIS_CLUSTERS: Optional[str] = Field( + description="Comma-separated list of Redis Clusters nodes (host:port)", + default=None, + ) + + REDIS_CLUSTERS_PASSWORD: Optional[str] = Field( + description="Password for Redis Clusters authentication (if required)", + default=None, + ) diff --git a/api/extensions/ext_redis.py b/api/extensions/ext_redis.py index e1f8409f21..36f06c1104 100644 --- a/api/extensions/ext_redis.py +++ b/api/extensions/ext_redis.py @@ -1,11 +1,12 @@ import redis +from redis.cluster import ClusterNode, RedisCluster from redis.connection import Connection, SSLConnection from redis.sentinel import Sentinel from configs import dify_config -class RedisClientWrapper(redis.Redis): +class RedisClientWrapper: """ A wrapper class for the Redis client that addresses the issue where the global `redis_client` variable cannot be updated when a new Redis instance is returned @@ -71,6 +72,12 @@ def init_app(app): ) master = sentinel.master_for(dify_config.REDIS_SENTINEL_SERVICE_NAME, **redis_params) redis_client.initialize(master) + elif dify_config.REDIS_USE_CLUSTERS: + nodes = [ + ClusterNode(host=node.split(":")[0], port=int(node.split.split(":")[1])) + for node in dify_config.REDIS_CLUSTERS.split(",") + ] + redis_client.initialize(RedisCluster(startup_nodes=nodes, password=dify_config.REDIS_CLUSTERS_PASSWORD)) else: redis_params.update( { diff --git a/api/tests/unit_tests/core/test_model_manager.py b/api/tests/unit_tests/core/test_model_manager.py index 2808b5b0fa..d98e9f6bad 100644 --- a/api/tests/unit_tests/core/test_model_manager.py +++ b/api/tests/unit_tests/core/test_model_manager.py @@ -1,10 +1,12 @@ -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pytest +import redis from core.entities.provider_entities import ModelLoadBalancingConfiguration from core.model_manager import LBModelManager from core.model_runtime.entities.model_entities import ModelType +from extensions.ext_redis import redis_client @pytest.fixture @@ -38,6 +40,9 @@ def lb_model_manager(): def test_lb_model_manager_fetch_next(mocker, lb_model_manager): + # initialize redis client + redis_client.initialize(redis.Redis()) + assert len(lb_model_manager._load_balancing_configs) == 3 config1 = lb_model_manager._load_balancing_configs[0] @@ -55,12 +60,13 @@ def test_lb_model_manager_fetch_next(mocker, lb_model_manager): start_index += 1 return start_index - mocker.patch("redis.Redis.incr", side_effect=incr) - mocker.patch("redis.Redis.set", return_value=None) - mocker.patch("redis.Redis.expire", return_value=None) + with ( + patch.object(redis_client, "incr", side_effect=incr), + patch.object(redis_client, "set", return_value=None), + patch.object(redis_client, "expire", return_value=None), + ): + config = lb_model_manager.fetch_next() + assert config == config2 - config = lb_model_manager.fetch_next() - assert config == config2 - - config = lb_model_manager.fetch_next() - assert config == config3 + config = lb_model_manager.fetch_next() + assert config == config3 diff --git a/docker/.env.example b/docker/.env.example index be8d72339f..d29c66535d 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -240,6 +240,12 @@ REDIS_SENTINEL_USERNAME= REDIS_SENTINEL_PASSWORD= REDIS_SENTINEL_SOCKET_TIMEOUT=0.1 +# List of Redis Cluster nodes. If Cluster mode is enabled, provide at least one Cluster IP and port. +# Format: `:,:,:` +REDIS_USE_CLUSTERS=false +REDIS_CLUSTERS= +REDIS_CLUSTERS_PASSWORD= + # ------------------------------ # Celery Configuration # ------------------------------ diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index b6caff90d9..06b04d90b6 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -55,6 +55,9 @@ x-shared-env: &shared-api-worker-env REDIS_SENTINEL_USERNAME: ${REDIS_SENTINEL_USERNAME:-} REDIS_SENTINEL_PASSWORD: ${REDIS_SENTINEL_PASSWORD:-} REDIS_SENTINEL_SOCKET_TIMEOUT: ${REDIS_SENTINEL_SOCKET_TIMEOUT:-0.1} + REDIS_CLUSTERS: ${REDIS_CLUSTERS:-} + REDIS_USE_CLUSTERS: ${REDIS_USE_CLUSTERS:-false} + REDIS_CLUSTERS_PASSWORD: ${REDIS_CLUSTERS_PASSWORD:-} ACCESS_TOKEN_EXPIRE_MINUTES: ${ACCESS_TOKEN_EXPIRE_MINUTES:-60} CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://:difyai123456@redis:6379/1} BROKER_USE_SSL: ${BROKER_USE_SSL:-false} From c3d11c8ff61a5341c3a89691ac02dc4fb160cd51 Mon Sep 17 00:00:00 2001 From: ybalbert001 <120714773+ybalbert001@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:24:41 +0800 Subject: [PATCH 012/103] fix: aws presign url is not workable remote url (#10884) Co-authored-by: Yuanbo Li --- .../model_providers/bedrock/llm/llm.py | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/api/core/model_runtime/model_providers/bedrock/llm/llm.py b/api/core/model_runtime/model_providers/bedrock/llm/llm.py index ff0403ee47..ef4dfaf6f1 100644 --- a/api/core/model_runtime/model_providers/bedrock/llm/llm.py +++ b/api/core/model_runtime/model_providers/bedrock/llm/llm.py @@ -2,13 +2,11 @@ import base64 import json import logging -import mimetypes from collections.abc import Generator from typing import Optional, Union, cast # 3rd import import boto3 -import requests from botocore.config import Config from botocore.exceptions import ( ClientError, @@ -439,22 +437,10 @@ class BedrockLargeLanguageModel(LargeLanguageModel): sub_messages.append(sub_message_dict) elif message_content.type == PromptMessageContentType.IMAGE: message_content = cast(ImagePromptMessageContent, message_content) - if not message_content.data.startswith("data:"): - # fetch image data from url - try: - url = message_content.data - image_content = requests.get(url).content - if "?" in url: - url = url.split("?")[0] - mime_type, _ = mimetypes.guess_type(url) - base64_data = base64.b64encode(image_content).decode("utf-8") - except Exception as ex: - raise ValueError(f"Failed to fetch image data from url {message_content.data}, {ex}") - else: - data_split = message_content.data.split(";base64,") - mime_type = data_split[0].replace("data:", "") - base64_data = data_split[1] - image_content = base64.b64decode(base64_data) + data_split = message_content.data.split(";base64,") + mime_type = data_split[0].replace("data:", "") + base64_data = data_split[1] + image_content = base64.b64decode(base64_data) if mime_type not in {"image/jpeg", "image/png", "image/gif", "image/webp"}: raise ValueError( From 1be8365684cba16e9913016a809cba2bdb9a0def Mon Sep 17 00:00:00 2001 From: -LAN- Date: Wed, 20 Nov 2024 15:10:12 +0800 Subject: [PATCH 013/103] Fix/input-value-type-in-moderation (#10893) --- api/core/moderation/keywords/keywords.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/api/core/moderation/keywords/keywords.py b/api/core/moderation/keywords/keywords.py index 4846da8f93..00b3c56c03 100644 --- a/api/core/moderation/keywords/keywords.py +++ b/api/core/moderation/keywords/keywords.py @@ -1,3 +1,6 @@ +from collections.abc import Sequence +from typing import Any + from core.moderation.base import Moderation, ModerationAction, ModerationInputsResult, ModerationOutputsResult @@ -62,5 +65,5 @@ class KeywordsModeration(Moderation): def _is_violated(self, inputs: dict, keywords_list: list) -> bool: return any(self._check_keywords_in_value(keywords_list, value) for value in inputs.values()) - def _check_keywords_in_value(self, keywords_list, value) -> bool: - return any(keyword.lower() in value.lower() for keyword in keywords_list) + def _check_keywords_in_value(self, keywords_list: Sequence[str], value: Any) -> bool: + return any(keyword.lower() in str(value).lower() for keyword in keywords_list) From 4d6b45427cd9637c3158be2c0f46600759097d63 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Wed, 20 Nov 2024 15:10:41 +0800 Subject: [PATCH 014/103] Support streaming output for OpenAI o1-preview and o1-mini (#10890) --- .../model_providers/openai/llm/llm.py | 50 +------------------ .../model_providers/openrouter/llm/llm.py | 18 +------ 2 files changed, 3 insertions(+), 65 deletions(-) diff --git a/api/core/model_runtime/model_providers/openai/llm/llm.py b/api/core/model_runtime/model_providers/openai/llm/llm.py index 68317d7179..f16f81c125 100644 --- a/api/core/model_runtime/model_providers/openai/llm/llm.py +++ b/api/core/model_runtime/model_providers/openai/llm/llm.py @@ -615,19 +615,11 @@ class OpenAILargeLanguageModel(_CommonOpenAI, LargeLanguageModel): prompt_messages = self._clear_illegal_prompt_messages(model, prompt_messages) # o1 compatibility - block_as_stream = False if model.startswith("o1"): if "max_tokens" in model_parameters: model_parameters["max_completion_tokens"] = model_parameters["max_tokens"] del model_parameters["max_tokens"] - if stream: - block_as_stream = True - stream = False - - if "stream_options" in extra_model_kwargs: - del extra_model_kwargs["stream_options"] - if "stop" in extra_model_kwargs: del extra_model_kwargs["stop"] @@ -644,47 +636,7 @@ class OpenAILargeLanguageModel(_CommonOpenAI, LargeLanguageModel): if stream: return self._handle_chat_generate_stream_response(model, credentials, response, prompt_messages, tools) - block_result = self._handle_chat_generate_response(model, credentials, response, prompt_messages, tools) - - if block_as_stream: - return self._handle_chat_block_as_stream_response(block_result, prompt_messages, stop) - - return block_result - - def _handle_chat_block_as_stream_response( - self, - block_result: LLMResult, - prompt_messages: list[PromptMessage], - stop: Optional[list[str]] = None, - ) -> Generator[LLMResultChunk, None, None]: - """ - Handle llm chat response - - :param model: model name - :param credentials: credentials - :param response: response - :param prompt_messages: prompt messages - :param tools: tools for tool calling - :param stop: stop words - :return: llm response chunk generator - """ - text = block_result.message.content - text = cast(str, text) - - if stop: - text = self.enforce_stop_tokens(text, stop) - - yield LLMResultChunk( - model=block_result.model, - prompt_messages=prompt_messages, - system_fingerprint=block_result.system_fingerprint, - delta=LLMResultChunkDelta( - index=0, - message=AssistantPromptMessage(content=text), - finish_reason="stop", - usage=block_result.usage, - ), - ) + return self._handle_chat_generate_response(model, credentials, response, prompt_messages, tools) def _handle_chat_generate_response( self, diff --git a/api/core/model_runtime/model_providers/openrouter/llm/llm.py b/api/core/model_runtime/model_providers/openrouter/llm/llm.py index 736ab8e7a8..2d6ece8113 100644 --- a/api/core/model_runtime/model_providers/openrouter/llm/llm.py +++ b/api/core/model_runtime/model_providers/openrouter/llm/llm.py @@ -45,19 +45,7 @@ class OpenRouterLargeLanguageModel(OAIAPICompatLargeLanguageModel): user: Optional[str] = None, ) -> Union[LLMResult, Generator]: self._update_credential(model, credentials) - - block_as_stream = False - if model.startswith("openai/o1"): - block_as_stream = True - stop = None - - # invoke block as stream - if stream and block_as_stream: - return self._generate_block_as_stream( - model, credentials, prompt_messages, model_parameters, tools, stop, user - ) - else: - return super()._generate(model, credentials, prompt_messages, model_parameters, tools, stop, stream, user) + return super()._generate(model, credentials, prompt_messages, model_parameters, tools, stop, stream, user) def _generate_block_as_stream( self, @@ -69,9 +57,7 @@ class OpenRouterLargeLanguageModel(OAIAPICompatLargeLanguageModel): stop: Optional[list[str]] = None, user: Optional[str] = None, ) -> Generator: - resp: LLMResult = super()._generate( - model, credentials, prompt_messages, model_parameters, tools, stop, False, user - ) + resp = super()._generate(model, credentials, prompt_messages, model_parameters, tools, stop, False, user) yield LLMResultChunk( model=model, From d6ea1e2f12cf29370d3eeca84c0535c965f6e1c5 Mon Sep 17 00:00:00 2001 From: llinvokerl <38915183+llinvokerl@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:11:33 +0800 Subject: [PATCH 015/103] fix: explicitly use new token when retrying ssePost after refresh (#10864) Co-authored-by: liusurong.lsr --- web/service/base.ts | 57 +++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/web/service/base.ts b/web/service/base.ts index 9ee3033d8e..03421d92a4 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -124,6 +124,24 @@ function requiredWebSSOLogin() { globalThis.location.href = `/webapp-signin?redirect_url=${globalThis.location.pathname}` } +function getAccessToken(isPublicAPI?: boolean) { + if (isPublicAPI) { + const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] + const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' }) + let accessTokenJson = { [sharedToken]: '' } + try { + accessTokenJson = JSON.parse(accessToken) + } + catch (e) { + + } + return accessTokenJson[sharedToken] + } + else { + return localStorage.getItem('console_token') || '' + } +} + export function format(text: string) { let res = text.trim() if (res.startsWith('\n')) @@ -295,22 +313,8 @@ const baseFetch = ( getAbortController(abortController) options.signal = abortController.signal } - if (isPublicAPI) { - const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] - const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' }) - let accessTokenJson = { [sharedToken]: '' } - try { - accessTokenJson = JSON.parse(accessToken) - } - catch (e) { - - } - options.headers.set('Authorization', `Bearer ${accessTokenJson[sharedToken]}`) - } - else { - const accessToken = localStorage.getItem('console_token') || '' - options.headers.set('Authorization', `Bearer ${accessToken}`) - } + const accessToken = getAccessToken(isPublicAPI) + options.headers.set('Authorization', `Bearer ${accessToken}`) if (deleteContentType) { options.headers.delete('Content-Type') @@ -403,23 +407,7 @@ const baseFetch = ( export const upload = (options: any, isPublicAPI?: boolean, url?: string, searchParams?: string): Promise => { const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX - let token = '' - if (isPublicAPI) { - const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] - const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' }) - let accessTokenJson = { [sharedToken]: '' } - try { - accessTokenJson = JSON.parse(accessToken) - } - catch (e) { - - } - token = accessTokenJson[sharedToken] - } - else { - const accessToken = localStorage.getItem('console_token') || '' - token = accessToken - } + const token = getAccessToken(isPublicAPI) const defaultOptions = { method: 'POST', url: (url ? `${urlPrefix}${url}` : `${urlPrefix}/files/upload`) + (searchParams || ''), @@ -505,6 +493,9 @@ export const ssePost = ( if (body) options.body = JSON.stringify(body) + const accessToken = getAccessToken(isPublicAPI) + options.headers.set('Authorization', `Bearer ${accessToken}`) + globalThis.fetch(urlWithPrefix, options as RequestInit) .then((res) => { if (!/^(2|3)\d{2}$/.test(String(res.status))) { From 99b0369f1b1e12ccdbb68f9848db69946db8c084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E7=A8=8B?= Date: Wed, 20 Nov 2024 17:40:34 +0800 Subject: [PATCH 016/103] Gitee AI embedding tool (#10903) --- .../builtin/gitee_ai/tools/embedding.py | 25 +++++++++++++ .../builtin/gitee_ai/tools/embedding.yaml | 37 +++++++++++++++++++ .../builtin/gitee_ai/tools/text-to-image.py | 2 +- 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 api/core/tools/provider/builtin/gitee_ai/tools/embedding.py create mode 100644 api/core/tools/provider/builtin/gitee_ai/tools/embedding.yaml diff --git a/api/core/tools/provider/builtin/gitee_ai/tools/embedding.py b/api/core/tools/provider/builtin/gitee_ai/tools/embedding.py new file mode 100644 index 0000000000..ab03759c19 --- /dev/null +++ b/api/core/tools/provider/builtin/gitee_ai/tools/embedding.py @@ -0,0 +1,25 @@ +from typing import Any, Union + +import requests + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.tool.builtin_tool import BuiltinTool + + +class GiteeAIToolEmbedding(BuiltinTool): + def _invoke( + self, user_id: str, tool_parameters: dict[str, Any] + ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + headers = { + "content-type": "application/json", + "authorization": f"Bearer {self.runtime.credentials['api_key']}", + } + + payload = {"inputs": tool_parameters.get("inputs")} + model = tool_parameters.get("model", "bge-m3") + url = f"https://ai.gitee.com/api/serverless/{model}/embeddings" + response = requests.post(url, json=payload, headers=headers) + if response.status_code != 200: + return self.create_text_message(f"Got Error Response:{response.text}") + + return [self.create_text_message(response.content.decode("utf-8"))] diff --git a/api/core/tools/provider/builtin/gitee_ai/tools/embedding.yaml b/api/core/tools/provider/builtin/gitee_ai/tools/embedding.yaml new file mode 100644 index 0000000000..53e569d731 --- /dev/null +++ b/api/core/tools/provider/builtin/gitee_ai/tools/embedding.yaml @@ -0,0 +1,37 @@ +identity: + name: embedding + author: gitee_ai + label: + en_US: embedding + icon: icon.svg +description: + human: + en_US: Generate word embeddings using Serverless-supported models (compatible with OpenAI) + llm: This tool is used to generate word embeddings from text input. +parameters: + - name: model + type: string + required: true + in: path + description: + en_US: Supported Embedding (compatible with OpenAI) interface models + enum: + - bge-m3 + - bge-large-zh-v1.5 + - bge-small-zh-v1.5 + label: + en_US: Service Model + zh_Hans: 服务模型 + default: bge-m3 + form: form + - name: inputs + type: string + required: true + label: + en_US: Input Text + zh_Hans: 输入文本 + human_description: + en_US: The text input used to generate embeddings. + zh_Hans: 用于生成词向量的输入文本。 + llm_description: This text input will be used to generate embeddings. + form: llm diff --git a/api/core/tools/provider/builtin/gitee_ai/tools/text-to-image.py b/api/core/tools/provider/builtin/gitee_ai/tools/text-to-image.py index 14291d1729..bb0b2c915b 100644 --- a/api/core/tools/provider/builtin/gitee_ai/tools/text-to-image.py +++ b/api/core/tools/provider/builtin/gitee_ai/tools/text-to-image.py @@ -6,7 +6,7 @@ from core.tools.entities.tool_entities import ToolInvokeMessage from core.tools.tool.builtin_tool import BuiltinTool -class GiteeAITool(BuiltinTool): +class GiteeAIToolText2Image(BuiltinTool): def _invoke( self, user_id: str, tool_parameters: dict[str, Any] ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: From b42b333a72a4ad20ac6b9a9a01bfc42b727ffbb1 Mon Sep 17 00:00:00 2001 From: shisaru292 <87224749+shisaru292@users.noreply.github.com> Date: Wed, 20 Nov 2024 20:10:51 +0800 Subject: [PATCH 017/103] fix: handle redis authentication for healthcheck command (#10907) --- docker/README.md | 2 +- docker/docker-compose.middleware.yaml | 4 +++- docker/docker-compose.yaml | 2 ++ docker/middleware.env.example | 5 +++-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docker/README.md b/docker/README.md index 7ce3f9bd75..c3cd1f9e3c 100644 --- a/docker/README.md +++ b/docker/README.md @@ -36,7 +36,7 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T - Navigate to the `docker` directory. - Ensure the `middleware.env` file is created by running `cp middleware.env.example middleware.env` (refer to the `middleware.env.example` file). 2. **Running Middleware Services**: - - Execute `docker-compose -f docker-compose.middleware.yaml up -d` to start the middleware services. + - Execute `docker-compose -f docker-compose.middleware.yaml up --env-file middleware.env -d` to start the middleware services. ### Migration for Existing Users diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml index 2eea273e72..11f5302197 100644 --- a/docker/docker-compose.middleware.yaml +++ b/docker/docker-compose.middleware.yaml @@ -29,11 +29,13 @@ services: redis: image: redis:6-alpine restart: always + environment: + REDISCLI_AUTH: ${REDIS_PASSWORD:-difyai123456} volumes: # Mount the redis data directory to the container. - ${REDIS_HOST_VOLUME:-./volumes/redis/data}:/data # Set the redis password when startup redis server. - command: redis-server --requirepass difyai123456 + command: redis-server --requirepass ${REDIS_PASSWORD:-difyai123456} ports: - "${EXPOSE_REDIS_PORT:-6379}:6379" healthcheck: diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 06b04d90b6..b47573bfc8 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -366,6 +366,8 @@ services: redis: image: redis:6-alpine restart: always + environment: + REDISCLI_AUTH: ${REDIS_PASSWORD:-difyai123456} volumes: # Mount the redis data directory to the container. - ./volumes/redis/data:/data diff --git a/docker/middleware.env.example b/docker/middleware.env.example index 17ac819527..c4ce9f0114 100644 --- a/docker/middleware.env.example +++ b/docker/middleware.env.example @@ -42,11 +42,13 @@ POSTGRES_EFFECTIVE_CACHE_SIZE=4096MB # ----------------------------- # Environment Variables for redis Service -REDIS_HOST_VOLUME=./volumes/redis/data # ----------------------------- +REDIS_HOST_VOLUME=./volumes/redis/data +REDIS_PASSWORD=difyai123456 # ------------------------------ # Environment Variables for sandbox Service +# ------------------------------ SANDBOX_API_KEY=dify-sandbox SANDBOX_GIN_MODE=release SANDBOX_WORKER_TIMEOUT=15 @@ -54,7 +56,6 @@ SANDBOX_ENABLE_NETWORK=true SANDBOX_HTTP_PROXY=http://ssrf_proxy:3128 SANDBOX_HTTPS_PROXY=http://ssrf_proxy:3128 SANDBOX_PORT=8194 -# ------------------------------ # ------------------------------ # Environment Variables for ssrf_proxy Service From af53e2b6b08688441fb66edf35520fb454a0d161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AC=BC=E9=A0=AD=E6=8B=93=E6=B5=B7?= <105633876+kitotakumi@users.noreply.github.com> Date: Wed, 20 Nov 2024 23:57:49 +0900 Subject: [PATCH 018/103] Fix : Add a process to fetch the mime type from the file name for signed url in remote_url #10872 version2 (#10908) --- api/factories/file_factory.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/factories/file_factory.py b/api/factories/file_factory.py index 8cb45f194b..4da0140d19 100644 --- a/api/factories/file_factory.py +++ b/api/factories/file_factory.py @@ -166,10 +166,9 @@ def _build_from_remote_url( def _get_remote_file_info(url: str): - mime_type = mimetypes.guess_type(url)[0] or "" file_size = -1 filename = url.split("/")[-1].split("?")[0] or "unknown_file" - mime_type = mime_type or mimetypes.guess_type(filename)[0] + mime_type = mimetypes.guess_type(filename)[0] or "" resp = ssrf_proxy.head(url, follow_redirects=True) if resp.status_code == httpx.codes.OK: From ec9f6220c9b924ebdb208c523f4665b6b4ec77b1 Mon Sep 17 00:00:00 2001 From: yihong Date: Thu, 21 Nov 2024 10:34:23 +0800 Subject: [PATCH 019/103] doc: fix better doc for api develop, droping dead hint (#10906) Signed-off-by: yihong0618 --- api/README.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/api/README.md b/api/README.md index de2baee4c5..cfbc000bd8 100644 --- a/api/README.md +++ b/api/README.md @@ -18,12 +18,17 @@ ``` 2. Copy `.env.example` to `.env` + + ```cli + cp .env.example .env + ``` 3. Generate a `SECRET_KEY` in the `.env` file. + bash for Linux ```bash for Linux sed -i "/^SECRET_KEY=/c\SECRET_KEY=$(openssl rand -base64 42)" .env ``` - + bash for Mac ```bash for Mac secret_key=$(openssl rand -base64 42) sed -i '' "/^SECRET_KEY=/c\\ @@ -41,14 +46,6 @@ poetry install ``` - In case of contributors missing to update dependencies for `pyproject.toml`, you can perform the following shell instead. - - ```bash - poetry shell # activate current environment - poetry add $(cat requirements.txt) # install dependencies of production and update pyproject.toml - poetry add $(cat requirements-dev.txt) --group dev # install dependencies of development and update pyproject.toml - ``` - 6. Run migrate Before the first launch, migrate the database to the latest version. From 0067b16d1eedea27b9825f0094ac8370afa43609 Mon Sep 17 00:00:00 2001 From: yihong Date: Thu, 21 Nov 2024 10:34:43 +0800 Subject: [PATCH 020/103] fix: refactor all 'or []' and 'or {}' logic to make code more clear (#10883) Signed-off-by: yihong0618 --- api/core/agent/base_agent_runner.py | 15 ++++----------- .../app/task_pipeline/workflow_cycle_manage.py | 4 ++-- .../model_providers/cohere/llm/llm.py | 4 ++-- .../model_providers/openai/llm/llm.py | 4 ++-- .../openllm/llm/openllm_generate.py | 5 +++-- api/core/tools/tool/tool.py | 2 +- api/core/tools/tool_engine.py | 2 +- api/core/tools/utils/configuration.py | 2 +- api/services/app_service.py | 2 +- api/services/tools/tools_transform_service.py | 2 +- api/services/website_service.py | 4 ++-- 11 files changed, 20 insertions(+), 26 deletions(-) diff --git a/api/core/agent/base_agent_runner.py b/api/core/agent/base_agent_runner.py index 860ec5de0c..2f5e7c7793 100644 --- a/api/core/agent/base_agent_runner.py +++ b/api/core/agent/base_agent_runner.py @@ -114,16 +114,9 @@ class BaseAgentRunner(AppRunner): # check if model supports stream tool call llm_model = cast(LargeLanguageModel, model_instance.model_type_instance) model_schema = llm_model.get_model_schema(model_instance.model, model_instance.credentials) - if model_schema and ModelFeature.STREAM_TOOL_CALL in (model_schema.features or []): - self.stream_tool_call = True - else: - self.stream_tool_call = False - - # check if model supports vision - if model_schema and ModelFeature.VISION in (model_schema.features or []): - self.files = application_generate_entity.files - else: - self.files = [] + features = model_schema.features if model_schema and model_schema.features else [] + self.stream_tool_call = ModelFeature.STREAM_TOOL_CALL in features + self.files = application_generate_entity.files if ModelFeature.VISION in features else [] self.query = None self._current_thoughts: list[PromptMessage] = [] @@ -250,7 +243,7 @@ class BaseAgentRunner(AppRunner): update prompt message tool """ # try to get tool runtime parameters - tool_runtime_parameters = tool.get_runtime_parameters() or [] + tool_runtime_parameters = tool.get_runtime_parameters() for parameter in tool_runtime_parameters: if parameter.form != ToolParameter.ToolParameterForm.LLM: diff --git a/api/core/app/task_pipeline/workflow_cycle_manage.py b/api/core/app/task_pipeline/workflow_cycle_manage.py index 042339969f..46b8609277 100644 --- a/api/core/app/task_pipeline/workflow_cycle_manage.py +++ b/api/core/app/task_pipeline/workflow_cycle_manage.py @@ -381,7 +381,7 @@ class WorkflowCycleManage: id=workflow_run.id, workflow_id=workflow_run.workflow_id, sequence_number=workflow_run.sequence_number, - inputs=workflow_run.inputs_dict or {}, + inputs=workflow_run.inputs_dict, created_at=int(workflow_run.created_at.timestamp()), ), ) @@ -428,7 +428,7 @@ class WorkflowCycleManage: created_by=created_by, created_at=int(workflow_run.created_at.timestamp()), finished_at=int(workflow_run.finished_at.timestamp()), - files=self._fetch_files_from_node_outputs(workflow_run.outputs_dict or {}), + files=self._fetch_files_from_node_outputs(workflow_run.outputs_dict), ), ) diff --git a/api/core/model_runtime/model_providers/cohere/llm/llm.py b/api/core/model_runtime/model_providers/cohere/llm/llm.py index 3863ad3308..f230157a34 100644 --- a/api/core/model_runtime/model_providers/cohere/llm/llm.py +++ b/api/core/model_runtime/model_providers/cohere/llm/llm.py @@ -691,8 +691,8 @@ class CohereLargeLanguageModel(LargeLanguageModel): base_model_schema = cast(AIModelEntity, base_model_schema) base_model_schema_features = base_model_schema.features or [] - base_model_schema_model_properties = base_model_schema.model_properties or {} - base_model_schema_parameters_rules = base_model_schema.parameter_rules or [] + base_model_schema_model_properties = base_model_schema.model_properties + base_model_schema_parameters_rules = base_model_schema.parameter_rules entity = AIModelEntity( model=model, diff --git a/api/core/model_runtime/model_providers/openai/llm/llm.py b/api/core/model_runtime/model_providers/openai/llm/llm.py index f16f81c125..aea884e002 100644 --- a/api/core/model_runtime/model_providers/openai/llm/llm.py +++ b/api/core/model_runtime/model_providers/openai/llm/llm.py @@ -1130,8 +1130,8 @@ class OpenAILargeLanguageModel(_CommonOpenAI, LargeLanguageModel): base_model_schema = model_map[base_model] base_model_schema_features = base_model_schema.features or [] - base_model_schema_model_properties = base_model_schema.model_properties or {} - base_model_schema_parameters_rules = base_model_schema.parameter_rules or [] + base_model_schema_model_properties = base_model_schema.model_properties + base_model_schema_parameters_rules = base_model_schema.parameter_rules entity = AIModelEntity( model=model, diff --git a/api/core/model_runtime/model_providers/openllm/llm/openllm_generate.py b/api/core/model_runtime/model_providers/openllm/llm/openllm_generate.py index 351dcced15..2789a9250a 100644 --- a/api/core/model_runtime/model_providers/openllm/llm/openllm_generate.py +++ b/api/core/model_runtime/model_providers/openllm/llm/openllm_generate.py @@ -37,13 +37,14 @@ class OpenLLMGenerateMessage: class OpenLLMGenerate: def generate( self, + *, server_url: str, model_name: str, stream: bool, model_parameters: dict[str, Any], - stop: list[str], + stop: list[str] | None = None, prompt_messages: list[OpenLLMGenerateMessage], - user: str, + user: str | None = None, ) -> Union[Generator[OpenLLMGenerateMessage, None, None], OpenLLMGenerateMessage]: if not server_url: raise InvalidAuthenticationError("Invalid server URL") diff --git a/api/core/tools/tool/tool.py b/api/core/tools/tool/tool.py index 6cb6e18b6d..f17a26dfbd 100644 --- a/api/core/tools/tool/tool.py +++ b/api/core/tools/tool/tool.py @@ -261,7 +261,7 @@ class Tool(BaseModel, ABC): """ parameters = self.parameters or [] parameters = parameters.copy() - user_parameters = self.get_runtime_parameters() or [] + user_parameters = self.get_runtime_parameters() user_parameters = user_parameters.copy() # override parameters diff --git a/api/core/tools/tool_engine.py b/api/core/tools/tool_engine.py index 9e290c3651..01a1fe330f 100644 --- a/api/core/tools/tool_engine.py +++ b/api/core/tools/tool_engine.py @@ -55,7 +55,7 @@ class ToolEngine: # check if this tool has only one parameter parameters = [ parameter - for parameter in tool.get_runtime_parameters() or [] + for parameter in tool.get_runtime_parameters() if parameter.form == ToolParameter.ToolParameterForm.LLM ] if parameters and len(parameters) == 1: diff --git a/api/core/tools/utils/configuration.py b/api/core/tools/utils/configuration.py index 83600d21c1..8b5e27f538 100644 --- a/api/core/tools/utils/configuration.py +++ b/api/core/tools/utils/configuration.py @@ -127,7 +127,7 @@ class ToolParameterConfigurationManager(BaseModel): # get tool parameters tool_parameters = self.tool_runtime.parameters or [] # get tool runtime parameters - runtime_parameters = self.tool_runtime.get_runtime_parameters() or [] + runtime_parameters = self.tool_runtime.get_runtime_parameters() # override parameters current_parameters = tool_parameters.copy() for runtime_parameter in runtime_parameters: diff --git a/api/services/app_service.py b/api/services/app_service.py index 620d0ac270..af2b77d633 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -341,7 +341,7 @@ class AppService: if not app_model_config: return meta - agent_config = app_model_config.agent_mode_dict or {} + agent_config = app_model_config.agent_mode_dict # get all tools tools = agent_config.get("tools", []) diff --git a/api/services/tools/tools_transform_service.py b/api/services/tools/tools_transform_service.py index 1befa11531..a4aa870dc8 100644 --- a/api/services/tools/tools_transform_service.py +++ b/api/services/tools/tools_transform_service.py @@ -242,7 +242,7 @@ class ToolTransformService: # get tool parameters parameters = tool.parameters or [] # get tool runtime parameters - runtime_parameters = tool.get_runtime_parameters() or [] + runtime_parameters = tool.get_runtime_parameters() # override parameters current_parameters = parameters.copy() for runtime_parameter in runtime_parameters: diff --git a/api/services/website_service.py b/api/services/website_service.py index 13cc9c679a..230f5d7815 100644 --- a/api/services/website_service.py +++ b/api/services/website_service.py @@ -51,8 +51,8 @@ class WebsiteService: excludes = options.get("excludes").split(",") if options.get("excludes") else [] params = { "crawlerOptions": { - "includes": includes or [], - "excludes": excludes or [], + "includes": includes, + "excludes": excludes, "generateImgAltText": True, "limit": options.get("limit", 1), "returnOnlyUrls": False, From 2ae6460f46cdd91644a37318d45bfd0dbe7ca223 Mon Sep 17 00:00:00 2001 From: Steven sun <98230804+Tuyohai@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:39:49 +0800 Subject: [PATCH 021/103] Add googlenews tools from rapidapi (#10877) Co-authored-by: steven --- .../builtin/rapidapi/_assets/rapidapi.png | Bin 0 -> 63239 bytes .../provider/builtin/rapidapi/rapidapi.py | 22 ++++++++++ .../provider/builtin/rapidapi/rapidapi.yaml | 39 ++++++++++++++++++ .../builtin/rapidapi/tools/google_news.py | 33 +++++++++++++++ .../builtin/rapidapi/tools/google_news.yaml | 24 +++++++++++ 5 files changed, 118 insertions(+) create mode 100644 api/core/tools/provider/builtin/rapidapi/_assets/rapidapi.png create mode 100644 api/core/tools/provider/builtin/rapidapi/rapidapi.py create mode 100644 api/core/tools/provider/builtin/rapidapi/rapidapi.yaml create mode 100644 api/core/tools/provider/builtin/rapidapi/tools/google_news.py create mode 100644 api/core/tools/provider/builtin/rapidapi/tools/google_news.yaml diff --git a/api/core/tools/provider/builtin/rapidapi/_assets/rapidapi.png b/api/core/tools/provider/builtin/rapidapi/_assets/rapidapi.png new file mode 100644 index 0000000000000000000000000000000000000000..9c7468bb172326d7649a2f911c6f5dd7aa75584f GIT binary patch literal 63239 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 literal 0 HcmV?d00001 diff --git a/api/core/tools/provider/builtin/rapidapi/rapidapi.py b/api/core/tools/provider/builtin/rapidapi/rapidapi.py new file mode 100644 index 0000000000..31077b0894 --- /dev/null +++ b/api/core/tools/provider/builtin/rapidapi/rapidapi.py @@ -0,0 +1,22 @@ +from typing import Any + +from core.tools.errors import ToolProviderCredentialValidationError +from core.tools.provider.builtin.rapidapi.tools.google_news import GooglenewsTool +from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController + + +class RapidapiProvider(BuiltinToolProviderController): + def _validate_credentials(self, credentials: dict[str, Any]) -> None: + try: + GooglenewsTool().fork_tool_runtime( + meta={ + "credentials": credentials, + } + ).invoke( + user_id="", + tool_parameters={ + "language_region": "en-US", + }, + ) + except Exception as e: + raise ToolProviderCredentialValidationError(str(e)) diff --git a/api/core/tools/provider/builtin/rapidapi/rapidapi.yaml b/api/core/tools/provider/builtin/rapidapi/rapidapi.yaml new file mode 100644 index 0000000000..3f1d1c5824 --- /dev/null +++ b/api/core/tools/provider/builtin/rapidapi/rapidapi.yaml @@ -0,0 +1,39 @@ +identity: + name: rapidapi + author: Steven Sun + label: + en_US: RapidAPI + zh_Hans: RapidAPI + description: + en_US: RapidAPI is the world's largest API marketplace with over 1,000,000 developers and 10,000 APIs. + zh_Hans: RapidAPI是全球最大的API市场,拥有超过100万开发人员和10000个API。 + icon: rapidapi.png + tags: + - news +credentials_for_provider: + x-rapidapi-host: + type: text-input + required: true + label: + en_US: x-rapidapi-host + zh_Hans: x-rapidapi-host + placeholder: + en_US: Please input your x-rapidapi-host + zh_Hans: 请输入你的 x-rapidapi-host + help: + en_US: Get your x-rapidapi-host from RapidAPI. + zh_Hans: 从 RapidAPI 获取您的 x-rapidapi-host。 + url: https://rapidapi.com/ + x-rapidapi-key: + type: secret-input + required: true + label: + en_US: x-rapidapi-key + zh_Hans: x-rapidapi-key + placeholder: + en_US: Please input your x-rapidapi-key + zh_Hans: 请输入你的 x-rapidapi-key + help: + en_US: Get your x-rapidapi-key from RapidAPI. + zh_Hans: 从 RapidAPI 获取您的 x-rapidapi-key。 + url: https://rapidapi.com/ diff --git a/api/core/tools/provider/builtin/rapidapi/tools/google_news.py b/api/core/tools/provider/builtin/rapidapi/tools/google_news.py new file mode 100644 index 0000000000..d4b6dc4a46 --- /dev/null +++ b/api/core/tools/provider/builtin/rapidapi/tools/google_news.py @@ -0,0 +1,33 @@ +from typing import Any, Union + +import requests + +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.errors import ToolInvokeError, ToolProviderCredentialValidationError +from core.tools.tool.builtin_tool import BuiltinTool + + +class GooglenewsTool(BuiltinTool): + def _invoke( + self, + user_id: str, + tool_parameters: dict[str, Any], + ) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]: + """ + invoke tools + """ + key = self.runtime.credentials.get("x-rapidapi-key", "") + host = self.runtime.credentials.get("x-rapidapi-host", "") + if not all([key, host]): + raise ToolProviderCredentialValidationError("Please input correct x-rapidapi-key and x-rapidapi-host") + headers = {"x-rapidapi-key": key, "x-rapidapi-host": host} + lr = tool_parameters.get("language_region", "") + url = f"https://{host}/latest?lr={lr}" + response = requests.get(url, headers=headers) + if response.status_code != 200: + raise ToolInvokeError(f"Error {response.status_code}: {response.text}") + return self.create_text_message(response.text) + + def validate_credentials(self, parameters: dict[str, Any]) -> None: + parameters["validate"] = True + self._invoke(parameters) diff --git a/api/core/tools/provider/builtin/rapidapi/tools/google_news.yaml b/api/core/tools/provider/builtin/rapidapi/tools/google_news.yaml new file mode 100644 index 0000000000..547681b166 --- /dev/null +++ b/api/core/tools/provider/builtin/rapidapi/tools/google_news.yaml @@ -0,0 +1,24 @@ +identity: + name: google_news + author: Steven Sun + label: + en_US: GoogleNews + zh_Hans: 谷歌新闻 +description: + human: + en_US: google news is a news aggregator service developed by Google. It presents a continuous, customizable flow of articles organized from thousands of publishers and magazines. + zh_Hans: 谷歌新闻是由谷歌开发的新闻聚合服务。它提供了一个持续的、可定制的文章流,这些文章是从成千上万的出版商和杂志中整理出来的。 + llm: A tool to get the latest news from Google News. +parameters: + - name: language_region + type: string + required: true + label: + en_US: Language and Region + zh_Hans: 语言和地区 + human_description: + en_US: The language and region determine the language and region of the search results, and its value is assigned according to the "National Language Code Comparison Table", such as en-US, which stands for English (United States); zh-CN, stands for Chinese (Simplified). + zh_Hans: 语言和地区决定了搜索结果的语言和地区,其赋值按照《国家语言代码对照表》,形如en-US,代表英语(美国);zh-CN,代表中文(简体)。 + llm_description: The language and region determine the language and region of the search results, and its value is assigned according to the "National Language Code Comparison Table", such as en-US, which stands for English (United States); zh-CN, stands for Chinese (Simplified). + default: en-US + form: llm From 94c9cadbd86c738cdcb65ce96a23aa49dd80ac2a Mon Sep 17 00:00:00 2001 From: wy96f Date: Thu, 21 Nov 2024 13:03:16 +0800 Subject: [PATCH 022/103] fix image files not deleted on indexing_estimate #9541 (#10798) Co-authored-by: root --- api/core/indexing_runner.py | 14 ++++++++++++++ api/tasks/clean_dataset_task.py | 1 + api/tasks/clean_document_task.py | 1 + 3 files changed, 16 insertions(+) diff --git a/api/core/indexing_runner.py b/api/core/indexing_runner.py index 7db8f54f70..cae539b6a7 100644 --- a/api/core/indexing_runner.py +++ b/api/core/indexing_runner.py @@ -30,6 +30,7 @@ from core.rag.splitter.fixed_text_splitter import ( ) from core.rag.splitter.text_splitter import TextSplitter from core.tools.utils.text_processing_utils import remove_leading_symbols +from core.tools.utils.web_reader_tool import get_image_upload_file_ids from extensions.ext_database import db from extensions.ext_redis import redis_client from extensions.ext_storage import storage @@ -279,6 +280,19 @@ class IndexingRunner: if len(preview_texts) < 5: preview_texts.append(document.page_content) + # delete image files and related db records + image_upload_file_ids = get_image_upload_file_ids(document.page_content) + for upload_file_id in image_upload_file_ids: + image_file = db.session.query(UploadFile).filter(UploadFile.id == upload_file_id).first() + try: + storage.delete(image_file.key) + except Exception: + logging.exception( + "Delete image_files failed while indexing_estimate, \ + image_upload_file_is: {}".format(upload_file_id) + ) + db.session.delete(image_file) + if doc_form and doc_form == "qa_model": if len(preview_texts) > 0: # qa model document diff --git a/api/tasks/clean_dataset_task.py b/api/tasks/clean_dataset_task.py index 4d45df4d2a..a555fb2874 100644 --- a/api/tasks/clean_dataset_task.py +++ b/api/tasks/clean_dataset_task.py @@ -78,6 +78,7 @@ def clean_dataset_task( "Delete image_files failed when storage deleted, \ image_upload_file_is: {}".format(upload_file_id) ) + db.session.delete(image_file) db.session.delete(segment) db.session.query(DatasetProcessRule).filter(DatasetProcessRule.dataset_id == dataset_id).delete() diff --git a/api/tasks/clean_document_task.py b/api/tasks/clean_document_task.py index 54c89450c9..4d328643bf 100644 --- a/api/tasks/clean_document_task.py +++ b/api/tasks/clean_document_task.py @@ -51,6 +51,7 @@ def clean_document_task(document_id: str, dataset_id: str, doc_form: str, file_i "Delete image_files failed when storage deleted, \ image_upload_file_is: {}".format(upload_file_id) ) + db.session.delete(image_file) db.session.delete(segment) db.session.commit() From f358db9f02a6a5f7d3b417ff32c5086e09ce8e0e Mon Sep 17 00:00:00 2001 From: Kota-Yamaguchi <50980947+Kota-Yamaguchi@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:02:46 +0900 Subject: [PATCH 023/103] feat : Add Japanese translations for API documentation: chat, advanced-chat, completion, and workflow (#10927) --- web/app/components/develop/doc.tsx | 49 +- .../develop/template/template.ja.mdx | 551 ++++++++ .../template/template_advanced_chat.ja.mdx | 1105 ++++++++++++++++ .../develop/template/template_chat.ja.mdx | 1134 +++++++++++++++++ .../develop/template/template_workflow.ja.mdx | 607 +++++++++ 5 files changed, 3441 insertions(+), 5 deletions(-) create mode 100755 web/app/components/develop/template/template.ja.mdx create mode 100644 web/app/components/develop/template/template_advanced_chat.ja.mdx create mode 100644 web/app/components/develop/template/template_chat.ja.mdx create mode 100644 web/app/components/develop/template/template_workflow.ja.mdx diff --git a/web/app/components/develop/doc.tsx b/web/app/components/develop/doc.tsx index ce5471676d..d3076c4e74 100644 --- a/web/app/components/develop/doc.tsx +++ b/web/app/components/develop/doc.tsx @@ -5,12 +5,16 @@ import { useTranslation } from 'react-i18next' import { RiListUnordered } from '@remixicon/react' import TemplateEn from './template/template.en.mdx' import TemplateZh from './template/template.zh.mdx' +import TemplateJa from './template/template.ja.mdx' import TemplateAdvancedChatEn from './template/template_advanced_chat.en.mdx' import TemplateAdvancedChatZh from './template/template_advanced_chat.zh.mdx' +import TemplateAdvancedChatJa from './template/template_advanced_chat.ja.mdx' import TemplateWorkflowEn from './template/template_workflow.en.mdx' import TemplateWorkflowZh from './template/template_workflow.zh.mdx' +import TemplateWorkflowJa from './template/template_workflow.ja.mdx' import TemplateChatEn from './template/template_chat.en.mdx' import TemplateChatZh from './template/template_chat.zh.mdx' +import TemplateChatJa from './template/template_chat.ja.mdx' import I18n from '@/context/i18n' import { LanguagesSupported } from '@/i18n/language' @@ -57,7 +61,6 @@ const Doc = ({ appDetail }: IDocProps) => { // Run after component has rendered setTimeout(extractTOC, 0) }, [appDetail, locale]) - return (

@@ -98,16 +101,52 @@ const Doc = ({ appDetail }: IDocProps) => {
{(appDetail?.mode === 'chat' || appDetail?.mode === 'agent-chat') && ( - locale !== LanguagesSupported[1] ? : + (() => { + switch (locale) { + case LanguagesSupported[1]: + return + case LanguagesSupported[7]: + return + default: + return + } + })() )} {appDetail?.mode === 'advanced-chat' && ( - locale !== LanguagesSupported[1] ? : + (() => { + switch (locale) { + case LanguagesSupported[1]: + return + case LanguagesSupported[7]: + return + default: + return + } + })() )} {appDetail?.mode === 'workflow' && ( - locale !== LanguagesSupported[1] ? : + (() => { + switch (locale) { + case LanguagesSupported[1]: + return + case LanguagesSupported[7]: + return + default: + return + } + })() )} {appDetail?.mode === 'completion' && ( - locale !== LanguagesSupported[1] ? : + (() => { + switch (locale) { + case LanguagesSupported[1]: + return + case LanguagesSupported[7]: + return + default: + return + } + })() )}
diff --git a/web/app/components/develop/template/template.ja.mdx b/web/app/components/develop/template/template.ja.mdx new file mode 100755 index 0000000000..a6ab109229 --- /dev/null +++ b/web/app/components/develop/template/template.ja.mdx @@ -0,0 +1,551 @@ +import { CodeGroup } from '../code.tsx' +import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx' + +# Completion アプリ API + +テキスト生成アプリケーションはセッションレスをサポートし、翻訳、記事作成、要約AI等に最適です。 + +
+ ### ベースURL + + ```javascript + ``` + + + ### 認証 + + サービスAPIは`API-Key`認証を使用します。 + **APIキーの漏洩による重大な結果を避けるため、APIキーはサーバーサイドに保存し、クライアントサイドでは共有や保存しないことを強く推奨します。** + + すべてのAPIリクエストで、以下のように`Authorization` HTTPヘッダーにAPIキーを含めてください: + + + ```javascript + Authorization: Bearer {API_KEY} + + ``` + +
+ +--- + + + + + テキスト生成アプリケーションにリクエストを送信します。 + + ### リクエストボディ + + + + + アプリで定義された各種変数値を入力できます。 + `inputs`パラメータには複数のキー/値ペアが含まれ、各キーは特定の変数に対応し、各値はその変数の具体的な値となります。 + テキスト生成アプリケーションでは、少なくとも1つのキー/値ペアの入力が必要です。 + - `query` (string) 必須 + 入力テキスト、処理される内容。 + + + レスポンス返却モード、以下をサポート: + - `streaming` ストリーミングモード(推奨)、SSE([Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events))によるタイプライター風の出力を実装。 + - `blocking` ブロッキングモード、実行完了後に結果を返却。(処理が長い場合はリクエストが中断される可能性があります) + Cloudflareの制限により、100秒後に返却なしで中断されます。 + + + ユーザー識別子、エンドユーザーの身元を定義し、取得や統計に使用します。 + アプリケーション内で開発者が一意に定義する必要があります。 + + + ファイルリスト、モデルがVision機能をサポートしている場合のみ、テキスト理解と質問応答を組み合わせたファイル(画像)の入力に適しています。 + - `type` (string) サポートされるタイプ:`image`(現在は画像タイプのみサポート) + - `transfer_method` (string) 転送方法、画像URLの場合は`remote_url` / ファイルアップロードの場合は`local_file` + - `url` (string) 画像URL(転送方法が`remote_url`の場合) + - `upload_file_id` (string) アップロードされたファイルID、事前にファイルアップロードAPIを通じてアップロードする必要があります(転送方法が`local_file`の場合) + + + + ### レスポンス + `response_mode`が`blocking`の場合、CompletionResponseオブジェクトを返却します。 + `response_mode`が`streaming`の場合、ChunkCompletionResponseストリームを返却します。 + + ### ChatCompletionResponse + アプリの完全な結果を返却、`Content-Type`は`application/json`です。 + - `message_id` (string) 一意のメッセージID + - `mode` (string) アプリモード、固定で`chat` + - `answer` (string) 完全な応答内容 + - `metadata` (object) メタデータ + - `usage` (Usage) モデル使用情報 + - `retriever_resources` (array[RetrieverResource]) 引用と帰属のリスト + - `created_at` (int) メッセージ作成タイムスタンプ、例:1705395332 + + ### ChunkChatCompletionResponse + アプリが出力するストリームチャンクを返却、`Content-Type`は`text/event-stream`です。 + 各ストリーミングチャンクは`data:`で始まり、2つの改行文字`\n\n`で区切られます: + + ```streaming {{ title: 'Response' }} + data: {"event": "message", "task_id": "900bbd43-dc0b-4383-a372-aa6e6c414227", "id": "663c5084-a254-4040-8ad3-51f2a3c1a77c", "answer": "Hi", "created_at": 1705398420}\n\n + ``` + + ストリーミングチャンクの構造は`event`によって異なります: + - `event: message` LLMがテキストチャンクを返すイベント、つまり完全なテキストがチャンク形式で出力されます。 + - `task_id` (string) タスクID、リクエストの追跡と以下の生成停止APIに使用 + - `message_id` (string) 一意のメッセージID + - `answer` (string) LLMが返したテキストチャンクの内容 + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: message_end` メッセージ終了イベント、このイベントを受信するとストリーミングが終了したことを意味します。 + - `task_id` (string) タスクID、リクエストの追跡と以下の生成停止APIに使用 + - `message_id` (string) 一意のメッセージID + - `metadata` (object) メタデータ + - `usage` (Usage) モデル使用情報 + - `retriever_resources` (array[RetrieverResource]) 引用と帰属のリスト + - `event: tts_message` TTS音声ストリームイベント、つまり音声合成出力。内容はMp3形式の音声ブロックで、base64文字列としてエンコードされています。再生時は単にbase64をデコードしてプレーヤーに供給するだけです。(このメッセージは自動再生が有効な場合のみ利用可能) + - `task_id` (string) タスクID、リクエストの追跡と以下の応答停止インターフェースに使用 + - `message_id` (string) 一意のメッセージID + - `audio` (string) 音声合成後の音声、base64テキストコンテンツとしてエンコード、再生時は単にbase64をデコードしてプレーヤーに供給 + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: tts_message_end` TTS音声ストリーム終了イベント、このイベントを受信すると音声ストリームが終了したことを示します。 + - `task_id` (string) タスクID、リクエストの追跡と以下の応答停止インターフェースに使用 + - `message_id` (string) 一意のメッセージID + - `audio` (string) 終了イベントには音声がないため、空文字列 + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: message_replace` メッセージ内容置換イベント。 + 出力内容のモデレーションが有効な場合、コンテンツがフラグ付けされると、このイベントを通じてメッセージ内容が事前設定された返信に置き換えられます。 + - `task_id` (string) タスクID、リクエストの追跡と以下の生成停止APIに使用 + - `message_id` (string) 一意のメッセージID + - `answer` (string) 置換内容(LLMの返信テキストすべてを直接置換) + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: error` + ストリーミング処理中に発生した例外は、ストリームイベントの形式で出力され、エラーイベントを受信するとストリームが終了します。 + - `task_id` (string) タスクID、リクエストの追跡と以下の生成停止APIに使用 + - `message_id` (string) 一意のメッセージID + - `status` (int) HTTPステータスコード + - `code` (string) エラーコード + - `message` (string) エラーメッセージ + - `event: ping` 接続を維持するため10秒ごとのPingイベント。 + + ### エラー + - 404, 会話が存在しません + - 400, `invalid_param`, パラメータ入力異常 + - 400, `app_unavailable`, アプリ設定が利用できません + - 400, `provider_not_initialize`, 利用可能なモデル認証情報設定がありません + - 400, `provider_quota_exceeded`, モデル呼び出しクォータ不足 + - 400, `model_currently_not_support`, 現在のモデルは利用できません + - 400, `completion_request_error`, テキスト生成に失敗しました + - 500, 内部サーバーエラー + + + + + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/completion-messages' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "inputs": { + "query": "Hello, world!" + }, + "response_mode": "streaming", + "user": "abc-123" + }' + ``` + + + ### ブロッキングモード + + ```json {{ title: 'Response' }} + { + "event": "message", + "message_id": "9da23599-e713-473b-982c-4328d4f5c78a", + "mode": "completion", + "answer": "Hello World!...", + "metadata": { + "usage": { + "prompt_tokens": 1033, + "prompt_unit_price": "0.001", + "prompt_price_unit": "0.001", + "prompt_price": "0.0010330", + "completion_tokens": 128, + "completion_unit_price": "0.002", + "completion_price_unit": "0.001", + "completion_price": "0.0002560", + "total_tokens": 1161, + "total_price": "0.0012890", + "currency": "USD", + "latency": 0.7682376249867957 + } + }, + "created_at": 1705407629 + } + ``` + + ### ストリーミングモード + + ```streaming {{ title: 'Response' }} + data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "answer": " I", "created_at": 1679586595} + data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "answer": "'m", "created_at": 1679586595} + data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "answer": " glad", "created_at": 1679586595} + data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "answer": " to", "created_at": 1679586595} + data: {"event": "message", "message_id": : "5ad4cb98-f0c7-4085-b384-88c403be6290", "answer": " meet", "created_at": 1679586595} + data: {"event": "message", "message_id": : "5ad4cb98-f0c7-4085-b384-88c403be6290", "answer": " you", "created_at": 1679586595} + data: {"event": "message_end", "id": "5e52ce04-874b-4d27-9045-b3bc80def685", "metadata": {"usage": {"prompt_tokens": 1033, "prompt_unit_price": "0.001", "prompt_price_unit": "0.001", "prompt_price": "0.0010330", "completion_tokens": 135, "completion_unit_price": "0.002", "completion_price_unit": "0.001", "completion_price": "0.0002700", "total_tokens": 1168, "total_price": "0.0013030", "currency": "USD", "latency": 1.381760165997548}}} + data: {"event": "tts_message", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"} + data: {"event": "tts_message_end", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": ""} + ``` + + + + +--- + + + + メッセージ送信時に使用するファイル(現在は画像のみ対応)をアップロードし、画像とテキストのマルチモーダルな理解を可能にします。 + png、jpg、jpeg、webp、gif形式に対応しています。 + アップロードされたファイルは、現在のエンドユーザーのみが使用できます。 + + ### リクエストボディ + このインターフェースは`multipart/form-data`リクエストが必要です。 + - `file` (File) 必須 + アップロードするファイル。 + - `user` (string) 必須 + 開発者のルールで定義されたユーザー識別子。アプリケーション内で一意である必要があります。 + + ### レスポンス + アップロードが成功すると、サーバーはファイルのIDと関連情報を返します。 + - `id` (uuid) ID + - `name` (string) ファイル名 + - `size` (int) ファイルサイズ(バイト) + - `extension` (string) ファイル拡張子 + - `mime_type` (string) ファイルのMIMEタイプ + - `created_by` (uuid) エンドユーザーID + - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 + + ### エラー + - 400, `no_file_uploaded`, ファイルを提供する必要があります + - 400, `too_many_files`, 現在は1つのファイルのみ受け付けています + - 400, `unsupported_preview`, ファイルがプレビューに対応していません + - 400, `unsupported_estimate`, ファイルが推定に対応していません + - 413, `file_too_large`, ファイルが大きすぎます + - 415, `unsupported_file_type`, サポートされていない拡張子です。現在はドキュメントファイルのみ受け付けています + - 503, `s3_connection_failed`, S3サービスに接続できません + - 503, `s3_permission_denied`, S3へのファイルアップロード権限がありません + - 503, `s3_file_too_large`, ファイルがS3のサイズ制限を超えています + - 500, 内部サーバーエラー + + + + ### リクエスト例 + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/files/upload' \ + --header 'Authorization: Bearer {api_key}' \ + --form 'file=@"/path/to/file"' + ``` + + + + + ### レスポンス例 + + ```json {{ title: 'Response' }} + { + "id": "72fa9618-8f89-4a37-9b33-7e1178a24a67", + "name": "example.png", + "size": 1024, + "extension": "png", + "mime_type": "image/png", + "created_by": "6ad1ab0a-73ff-4ac1-b9e4-cdb312f71f13", + "created_at": 1577836800, + } + ``` + + + +--- + + + + + ストリーミングモードでのみサポートされています。 + ### パス + - `task_id` (string) タスクID、ストリーミングチャンクの返信から取得可能 + リクエストボディ + - `user` (string) 必須 + ユーザー識別子。エンドユーザーの身元を定義するために使用され、メッセージ送信インターフェースで渡されたユーザーと一致する必要があります。 + ### レスポンス + - `result` (string) 常に"success"を返します + + + ### リクエスト例 + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/completion-messages/:task_id/stop' \ + -H 'Authorization: Bearer {api_key}' \ + -H 'Content-Type: application/json' \ + --data-raw '{ + "user": "abc-123" + }' + ``` + + + ### レスポンス例 + + ```json {{ title: 'Response' }} + { + "result": "success" + } + ``` + + + + +--- + + + + + エンドユーザーはフィードバックメッセージを提供でき、アプリケーション開発者が期待される出力を最適化するのに役立ちます。 + + ### パス + + + メッセージID + + + + ### リクエストボディ + + + + 高評価は`like`、低評価は`dislike`、高評価の取り消しは`null` + + + 開発者のルールで定義されたユーザー識別子。アプリケーション内で一意である必要があります。 + + + + ### レスポンス + - `result` (string) 常に"success"を返します + + + + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "rating": "like", + "user": "abc-123" + }' + ``` + + + + + ```json {{ title: 'Response' }} + { + "result": "success" + } + ``` + + + + +--- + + + + + ページ開始時に、機能、入力パラメータ名、タイプ、デフォルト値などの情報を取得するために使用されます。 + + ### クエリ + + + + 開発者のルールで定義されたユーザー識別子。アプリケーション内で一意である必要があります。 + + + + ### レスポンス + - `opening_statement` (string) 開始文 + - `suggested_questions` (array[string]) 開始時の提案質問リスト + - `suggested_questions_after_answer` (object) 回答後の提案質問を有効にします。 + - `enabled` (bool) 有効かどうか + - `speech_to_text` (object) 音声からテキスト + - `enabled` (bool) 有効かどうか + - `retriever_resource` (object) 引用と帰属 + - `enabled` (bool) 有効かどうか + - `annotation_reply` (object) 注釈付き返信 + - `enabled` (bool) 有効かどうか + - `user_input_form` (array[object]) ユーザー入力フォーム設定 + - `text-input` (object) テキスト入力コントロール + - `label` (string) 変数表示ラベル名 + - `variable` (string) 変数ID + - `required` (bool) 必須かどうか + - `default` (string) デフォルト値 + - `paragraph` (object) 段落テキスト入力コントロール + - `label` (string) 変数表示ラベル名 + - `variable` (string) 変数ID + - `required` (bool) 必須かどうか + - `default` (string) デフォルト値 + - `select` (object) ドロップダウンコントロール + - `label` (string) 変数表示ラベル名 + - `variable` (string) 変数ID + - `required` (bool) 必須かどうか + - `default` (string) デフォルト値 + - `options` (array[string]) オプション値 + - `file_upload` (object) ファイルアップロード設定 + - `image` (object) 画像設定 + 現在は画像タイプのみ対応:`png`、`jpg`、`jpeg`、`webp`、`gif` + - `enabled` (bool) 有効かどうか + - `number_limits` (int) 画像数制限、デフォルトは3 + - `transfer_methods` (array[string]) 転送方法リスト、remote_url、local_file、いずれかを選択 + - `system_parameters` (object) システムパラメータ + - `file_size_limit` (int) ドキュメントアップロードサイズ制限(MB) + - `image_file_size_limit` (int) 画像ファイルアップロードサイズ制限(MB) + - `audio_file_size_limit` (int) 音声ファイルアップロードサイズ制限(MB) + - `video_file_size_limit` (int) 動画ファイルアップロードサイズ制限(MB) + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/parameters?user=abc-123' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + + ```json {{ title: 'Response' }} + { + "opening_statement": "Hello!", + "suggested_questions_after_answer": { + "enabled": true + }, + "speech_to_text": { + "enabled": true + }, + "retriever_resource": { + "enabled": true + }, + "annotation_reply": { + "enabled": true + }, + "user_input_form": [ + { + "paragraph": { + "label": "Query", + "variable": "query", + "required": true, + "default": "" + } + } + ], + "file_upload": { + "image": { + "enabled": false, + "number_limits": 3, + "detail": "high", + "transfer_methods": [ + "remote_url", + "local_file" + ] + } + }, + "system_parameters": { + "file_size_limit": 15, + "image_file_size_limit": 10, + "audio_file_size_limit": 50, + "video_file_size_limit": 100 + } + } + ``` + + + + +--- + + + + + テキストを音声に変換します。 + + ### リクエストボディ + + + + Difyが生成したテキストメッセージの場合、生成されたmessage-idを直接渡すだけです。バックエンドはmessage-idを使用して対応するコンテンツを検索し、音声情報を直接合成します。message_idとtextの両方が同時に提供された場合、message_idが優先されます。 + + + 音声生成コンテンツ。 + + + 開発者が定義したユーザー識別子。アプリ内で一意性を確保する必要があります。 + + + + + + + + ```bash {{ title: 'cURL' }} + curl -o text-to-audio.mp3 -X POST '${props.appDetail.api_base_url}/text-to-audio' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", + "text": "Hello Dify", + "user": "abc-123" + }' + ``` + + + + + ```json {{ title: 'headers' }} + { + "Content-Type": "audio/wav" + } + ``` + + + diff --git a/web/app/components/develop/template/template_advanced_chat.ja.mdx b/web/app/components/develop/template/template_advanced_chat.ja.mdx new file mode 100644 index 0000000000..b4c252e19a --- /dev/null +++ b/web/app/components/develop/template/template_advanced_chat.ja.mdx @@ -0,0 +1,1105 @@ +import { CodeGroup } from '../code.tsx' +import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx' + +# 高度なチャットアプリAPI + +チャットアプリケーションはセッションの持続性をサポートしており、以前のチャット履歴を応答のコンテキストとして使用できます。これは、チャットボットやカスタマーサービスAIなどに適用できます。 + +
+ ### ベースURL + + ```javascript + ``` + + + ### 認証 + + サービスAPIは`API-Key`認証を使用します。 + **APIキーはサーバー側に保存し、クライアント側で共有または保存しないことを強くお勧めします。APIキーの漏洩は深刻な結果を招く可能性があります。** + + すべてのAPIリクエストには、以下のように`Authorization`HTTPヘッダーにAPIキーを含めてください: + + + ```javascript + Authorization: Bearer {API_KEY} + + ``` + +
+ +--- + + + + + チャットアプリケーションにリクエストを送信します。 + + ### リクエストボディ + + + + ユーザー入力/質問内容 + + + アプリによって定義されたさまざまな変数値の入力を許可します。 + `inputs`パラメータには複数のキー/値ペアが含まれ、各キーは特定の変数に対応し、各値はその変数の特定の値です。 + 変数がファイルタイプの場合、以下の`files`で説明されているキーを持つオブジェクトを指定します。 + デフォルト`{}` + + + 応答の返却モードを指定します。サポートされているモード: + - `streaming` ストリーミングモード(推奨)、SSE([サーバー送信イベント](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events))を通じてタイプライターのような出力を実装します。 + - `blocking` ブロッキングモード、実行完了後に結果を返します。(プロセスが長い場合、リクエストが中断される可能性があります) + Cloudflareの制限により、リクエストは100秒後に返答なしで中断されます。 + + + ユーザー識別子、エンドユーザーの身元を定義するために使用され、統計のために使用されます。 + アプリケーション内で開発者によって一意に定義されるべきです。 + + + 会話ID、以前のチャット記録に基づいて会話を続けるには、以前のメッセージのconversation_idを渡す必要があります。 + + + ファイルリスト、テキストの理解と質問への回答を組み合わせたファイルの入力に適しており、モデルがビジョン機能をサポートしている場合にのみ利用可能です。 + - `type` (string) サポートされているタイプ: + - `document` ('TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB') + - `image` ('JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG') + - `audio` ('MP3', 'M4A', 'WAV', 'WEBM', 'AMR') + - `video` ('MP4', 'MOV', 'MPEG', 'MPGA') + - `transfer_method` (string) 転送方法、画像URLの場合は`remote_url` / ファイルアップロードの場合は`local_file` + - `url` (string) 画像URL(転送方法が`remote_url`の場合) + - `upload_file_id` (string) アップロードされたファイルID、事前にファイルアップロードAPIを通じて取得する必要があります(転送方法が`local_file`の場合) + + + タイトルを自動生成、デフォルトは`true`。 + `false`に設定すると、会話のリネームAPIを呼び出し、`auto_generate`を`true`に設定することで非同期タイトル生成を実現できます。 + + + + ### 応答 + response_modeがブロッキングの場合、CompletionResponseオブジェクトを返します。 + response_modeがストリーミングの場合、ChunkCompletionResponseストリームを返します。 + + ### ChatCompletionResponse + 完全なアプリ結果を返します。`Content-Type`は`application/json`です。 + - `message_id` (string) 一意のメッセージID + - `conversation_id` (string) 会話ID + - `mode` (string) アプリモード、`chat`として固定 + - `answer` (string) 完全な応答内容 + - `metadata` (object) メタデータ + - `usage` (Usage) モデル使用情報 + - `retriever_resources` (array[RetrieverResource]) 引用と帰属リスト + - `created_at` (int) メッセージ作成タイムスタンプ、例:1705395332 + + ### ChunkChatCompletionResponse + アプリによって出力されたストリームチャンクを返します。`Content-Type`は`text/event-stream`です。 + 各ストリーミングチャンクは`data:`で始まり、2つの改行文字`\n\n`で区切られます。以下のように表示されます: + + ```streaming {{ title: '応答' }} + data: {"event": "message", "task_id": "900bbd43-dc0b-4383-a372-aa6e6c414227", "id": "663c5084-a254-4040-8ad3-51f2a3c1a77c", "answer": "Hi", "created_at": 1705398420}\n\n + ``` + + ストリーミングチャンクの構造は`event`に応じて異なります: + - `event: message` LLMがテキストチャンクイベントを返します。つまり、完全なテキストがチャンク形式で出力されます。 + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `message_id` (string) 一意のメッセージID + - `conversation_id` (string) 会話ID + - `answer` (string) LLMが返したテキストチャンク内容 + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: message_file` メッセージファイルイベント、ツールによって新しいファイルが作成されました + - `id` (string) ファイル一意ID + - `type` (string) ファイルタイプ、現在は"image"のみ許可 + - `belongs_to` (string) 所属、ここでは'assistant'のみ + - `url` (string) ファイルのリモートURL + - `conversation_id` (string) 会話ID + - `event: message_end` メッセージ終了イベント、このイベントを受信するとストリーミングが終了したことを意味します。 + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `message_id` (string) 一意のメッセージID + - `conversation_id` (string) 会話ID + - `metadata` (object) メタデータ + - `usage` (Usage) モデル使用情報 + - `retriever_resources` (array[RetrieverResource]) 引用と帰属リスト + - `event: tts_message` TTSオーディオストリームイベント、つまり音声合成出力。内容はMp3形式のオーディオブロックで、base64文字列としてエンコードされています。再生時には、base64をデコードしてプレーヤーに入力するだけです。(このメッセージは自動再生が有効な場合にのみ利用可能) + - `task_id` (string) タスクID、リクエスト追跡と以下のストップ応答インターフェースに使用 + - `message_id` (string) 一意のメッセージID + - `audio` (string) 音声合成後のオーディオ、base64テキストコンテンツとしてエンコードされており、再生時にはbase64をデコードしてプレーヤーに入力するだけです + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: tts_message_end` TTSオーディオストリーム終了イベント、このイベントを受信するとオーディオストリームが終了したことを示します。 + - `task_id` (string) タスクID、リクエスト追跡と以下のストップ応答インターフェースに使用 + - `message_id` (string) 一意のメッセージID + - `audio` (string) 終了イベントにはオーディオがないため、これは空の文字列です + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: message_replace` メッセージ内容置換イベント。 + 出力内容のモデレーションが有効な場合、内容がフラグ付けされると、このイベントを通じてメッセージ内容がプリセットの返信に置き換えられます。 + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `message_id` (string) 一意のメッセージID + - `conversation_id` (string) 会話ID + - `answer` (string) 置換内容(すべてのLLM返信テキストを直接置き換えます) + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: workflow_started` ワークフローが実行を開始 + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `workflow_run_id` (string) ワークフロー実行の一意ID + - `event` (string) `workflow_started`に固定 + - `data` (object) 詳細 + - `id` (string) ワークフロー実行の一意ID + - `workflow_id` (string) 関連ワークフローのID + - `sequence_number` (int) 自己増加シリアル番号、アプリ内で自己増加し、1から始まります + - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 + - `event: node_started` ノード実行が開始 + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `workflow_run_id` (string) ワークフロー実行の一意ID + - `event` (string) `node_started`に固定 + - `data` (object) 詳細 + - `id` (string) ワークフロー実行の一意ID + - `node_id` (string) ノードのID + - `node_type` (string) ノードのタイプ + - `title` (string) ノードの名前 + - `index` (int) 実行シーケンス番号、トレースノードシーケンスを表示するために使用 + - `predecessor_node_id` (string) オプションのプレフィックスノードID、キャンバス表示実行パスに使用 + - `inputs` (array[object]) ノードで使用されるすべての前のノード変数の内容 + - `created_at` (timestamp) 開始のタイムスタンプ、例:1705395332 + - `event: node_finished` ノード実行が終了、成功または失敗は同じイベント内で異なる状態で示されます + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `workflow_run_id` (string) ワークフロー実行の一意ID + - `event` (string) `node_finished`に固定 + - `data` (object) 詳細 + - `id` (string) ワークフロー実行の一意ID + - `node_id` (string) ノードのID + - `node_type` (string) ノードのタイプ + - `title` (string) ノードの名前 + - `index` (int) 実行シーケンス番号、トレースノードシーケンスを表示するために使用 + - `predecessor_node_id` (string) オプションのプレフィックスノードID、キャンバス表示実行パスに使用 + - `inputs` (array[object]) ノードで使用されるすべての前のノード変数の内容 + - `process_data` (json) オプションのノードプロセスデータ + - `outputs` (json) オプションの出力内容 + - `status` (string) 実行の状態、`running` / `succeeded` / `failed` / `stopped` + - `error` (string) オプションのエラー理由 + - `elapsed_time` (float) オプションの使用される合計秒数 + - `execution_metadata` (json) メタデータ + - `total_tokens` (int) オプションの使用されるトークン数 + - `total_price` (decimal) オプションの合計コスト + - `currency` (string) オプション、例:`USD` / `RMB` + - `created_at` (timestamp) 開始のタイムスタンプ、例:1705395332 + - `event: workflow_finished` ワークフロー実行が終了、成功または失敗は同じイベント内で異なる状態で示されます + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `workflow_run_id` (string) ワークフロー実行の一意ID + - `event` (string) `workflow_finished`に固定 + - `data` (object) 詳細 + - `id` (string) ワークフロー実行のID + - `workflow_id` (string) 関連ワークフローのID + - `status` (string) 実行の状態、`running` / `succeeded` / `failed` / `stopped` + - `outputs` (json) オプションの出力内容 + - `error` (string) オプションのエラー理由 + - `elapsed_time` (float) オプションの使用される合計秒数 + - `total_tokens` (int) オプションの使用されるトークン数 + - `total_steps` (int) デフォルト0 + - `created_at` (timestamp) 開始時間 + - `finished_at` (timestamp) 終了時間 + - `event: error` + ストリーミングプロセス中に発生する例外はストリームイベントの形式で出力され、エラーイベントを受信するとストリームが終了します。 + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `message_id` (string) 一意のメッセージID + - `status` (int) HTTPステータスコード + - `code` (string) エラーコード + - `message` (string) エラーメッセージ + - `event: ping` 接続を維持するために10秒ごとにpingイベントが発生します。 + + ### エラー + - 404, 会話が存在しません + - 400, `invalid_param`, 異常なパラメータ入力 + - 400, `app_unavailable`, アプリ構成が利用できません + - 400, `provider_not_initialize`, 利用可能なモデル資格情報構成がありません + - 400, `provider_quota_exceeded`, モデル呼び出しクォータが不足しています + - 400, `model_currently_not_support`, 現在のモデルが利用できません + - 400, `completion_request_error`, テキスト生成に失敗しました + - 500, 内部サーバーエラー + + + + + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/chat-messages' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "inputs": {}, + "query": "eh", + "response_mode": "streaming", + "conversation_id": "1c7e55fb-1ba2-4e10-81b5-30addcea2276", + "user": "abc-123" + }' + ``` + + ### ブロッキングモード + + ```json {{ title: '応答' }} + { + "event": "message", + "message_id": "9da23599-e713-473b-982c-4328d4f5c78a", + "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", + "mode": "chat", + "answer": "iPhone 13 Pro Maxの仕様は次のとおりです:...", + "metadata": { + "usage": { + "prompt_tokens": 1033, + "prompt_unit_price": "0.001", + "prompt_price_unit": "0.001", + "prompt_price": "0.0010330", + "completion_tokens": 128, + "completion_unit_price": "0.002", + "completion_price_unit": "0.001", + "completion_price": "0.0002560", + "total_tokens": 1161, + "total_price": "0.0012890", + "currency": "USD", + "latency": 0.7682376249867957 + }, + "retriever_resources": [ + { + "position": 1, + "dataset_id": "101b4c97-fc2e-463c-90b1-5261a4cdcafb", + "dataset_name": "iPhone", + "document_id": "8dd1ad74-0b5f-4175-b735-7d98bbbb4e00", + "document_name": "iPhone List", + "segment_id": "ed599c7f-2766-4294-9d1d-e5235a61270a", + "score": 0.98457545, + "content": "\"Model\",\"Release Date\",\"Display Size\",\"Resolution\",\"Processor\",\"RAM\",\"Storage\",\"Camera\",\"Battery\",\"Operating System\"\n\"iPhone 13 Pro Max\",\"September 24, 2021\",\"6.7 inch\",\"1284 x 2778\",\"Hexa-core (2x3.23 GHz Avalanche + 4x1.82 GHz Blizzard)\",\"6 GB\",\"128, 256, 512 GB, 1TB\",\"12 MP\",\"4352 mAh\",\"iOS 15\"" + } + ] + }, + "created_at": 1705407629 + } + ``` + + ### ストリーミングモード + + ```streaming {{ title: '応答' }} + data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "sequence_number": 1, "created_at": 1679586595}} + data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} + data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} + data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} + data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " I", "created_at": 1679586595} + data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": "'m", "created_at": 1679586595} + data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " glad", "created_at": 1679586595} + data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " to", "created_at": 1679586595} + data: {"event": "message", "message_id": : "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " meet", "created_at": 1679586595} + data: {"event": "message", "message_id": : "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " you", "created_at": 1679586595} + data: {"event": "message_end", "id": "5e52ce04-874b-4d27-9045-b3bc80def685", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "metadata": {"usage": {"prompt_tokens": 1033, "prompt_unit_price": "0.001", "prompt_price_unit": "0.001", "prompt_price": "0.0010330", "completion_tokens": 135, "completion_unit_price": "0.002", "completion_price_unit": "0.001", "completion_price": "0.0002700", "total_tokens": 1168, "total_price": "0.0013030", "currency": "USD", "latency": 1.381760165997548}, "retriever_resources": [{"position": 1, "dataset_id": "101b4c97-fc2e-463c-90b1-5261a4cdcafb", "dataset_name": "iPhone", "document_id": "8dd1ad74-0b5f-4175-b735-7d98bbbb4e00", "document_name": "iPhone List", "segment_id": "ed599c7f-2766-4294-9d1d-e5235a61270a", "score": 0.98457545, "content": "\"Model\",\"Release Date\",\"Display Size\",\"Resolution\",\"Processor\",\"RAM\",\"Storage\",\"Camera\",\"Battery\",\"Operating System\"\n\"iPhone 13 Pro Max\",\"September 24, 2021\",\"6.7 inch\",\"1284 x 2778\",\"Hexa-core (2x3.23 GHz Avalanche + 4x1.82 GHz Blizzard)\",\"6 GB\",\"128, 256, 512 GB, 1TB\",\"12 MP\",\"4352 mAh\",\"iOS 15\""}]}} + data: {"event": "tts_message", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"} + data: {"event": "tts_message_end", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": ""} + ``` + + + + + +--- + + + + メッセージ送信時に使用するファイルをアップロードし、画像とテキストのマルチモーダル理解を可能にします。 + アプリケーションでサポートされている形式をサポートします。 + アップロードされたファイルは現在のエンドユーザーのみが使用できます。 + + ### リクエストボディ + このインターフェースは`multipart/form-data`リクエストを必要とします。 + - `file` (File) 必須 + アップロードするファイル。 + - `user` (string) 必須 + ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。 + + ### 応答 + アップロードが成功すると、サーバーはファイルのIDと関連情報を返します。 + - `id` (uuid) ID + - `name` (string) ファイル名 + - `size` (int) ファイルサイズ(バイト) + - `extension` (string) ファイル拡張子 + - `mime_type` (string) ファイルのMIMEタイプ + - `created_by` (uuid) エンドユーザーID + - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 + + ### エラー + - 400, `no_file_uploaded`, ファイルが提供されなければなりません + - 400, `too_many_files`, 現在は1つのファイルのみ受け付けます + - 400, `unsupported_preview`, ファイルはプレビューをサポートしていません + - 400, `unsupported_estimate`, ファイルは推定をサポートしていません + - 413, `file_too_large`, ファイルが大きすぎます + - 415, `unsupported_file_type`, サポートされていない拡張子、現在はドキュメントファイルのみ受け付けます + - 503, `s3_connection_failed`, S3サービスに接続できません + - 503, `s3_permission_denied`, S3にファイルをアップロードする権限がありません + - 503, `s3_file_too_large`, ファイルがS3のサイズ制限を超えています + - 500, 内部サーバーエラー + + + + + ### リクエスト例 + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/files/upload' \ + --header 'Authorization: Bearer {api_key}' \ + --form 'file=@"/path/to/file"' + ``` + + + + + ### 応答例 + + ```json {{ title: '応答' }} + { + "id": "72fa9618-8f89-4a37-9b33-7e1178a24a67", + "name": "example.png", + "size": 1024, + "extension": "png", + "mime_type": "image/png", + "created_by": "6ad1ab0a-73ff-4ac1-b9e4-cdb312f71f13", + "created_at": 1577836800, + } + ``` + + + +--- + + + + + ストリーミングモードでのみサポートされています。 + ### パス + - `task_id` (string) タスクID、ストリーミングチャンクの返り値から取得できます + ### リクエストボディ + - `user` (string) 必須 + ユーザー識別子、エンドユーザーの身元を定義するために使用され、送信メッセージインターフェースで渡されたユーザーと一致している必要があります。 + ### 応答 + - `result` (string) 常に"success"を返します + + + ### リクエスト例 + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/chat-messages/:task_id/stop' \ + -H 'Authorization: Bearer {api_key}' \ + -H 'Content-Type: application/json' \ + --data-raw '{ + "user": "abc-123" + }' + ``` + + + ### 応答例 + + ```json {{ title: '応答' }} + { + "result": "success" + } + ``` + + + + +--- + + + + + エンドユーザーはフィードバックメッセージを提供でき、アプリケーション開発者が期待される出力を最適化するのを支援します。 + + ### パス + + + メッセージID + + + + ### リクエストボディ + + + + アップボートは`like`、ダウンボートは`dislike`、アップボートの取り消しは`null` + + + ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。 + + + + ### 応答 + - `result` (string) 常に"success"を返します + + + + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "rating": "like", + "user": "abc-123" + }' + ``` + + + + + ```json {{ title: '応答' }} + { + "result": "success" + } + ``` + + + + +--- + + + + + 現在のメッセージに対する次の質問の提案を取得します + + ### パスパラメータ + + + + メッセージID + + + + ### クエリ + + + ユーザー識別子、エンドユーザーの身元を定義するために使用され、統計のために使用されます。 + アプリケーション内で開発者によって一意に定義されるべきです。 + + + + + + + + ```bash {{ title: 'cURL' }} + curl --location --request GET '${props.appDetail.api_base_url}/messages/{message_id}/suggested?user=abc-123' \ + --header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \ + --header 'Content-Type: application/json' \ + ``` + + + + + ```json {{ title: '応答' }} + { + "result": "success", + "data": [ + "a", + "b", + "c" + ] + } + ``` + + + + +--- + + + + + スクロールロード形式で履歴チャット記録を返し、最初のページは最新の`{limit}`メッセージを返します。つまり、逆順です。 + + ### クエリ + + + + 会話ID + + + ユーザー識別子、エンドユーザーの身元を定義するために使用され、統計のために使用されます。 + アプリケーション内で開発者によって一意に定義されるべきです。 + + + 現在のページの最初のチャット記録のID、デフォルトはnullです。 + + + 1回のリクエストで返すチャット履歴メッセージの数、デフォルトは20です。 + + + + ### 応答 + - `data` (array[object]) メッセージリスト + - `id` (string) メッセージID + - `conversation_id` (string) 会話ID + - `inputs` (array[object]) ユーザー入力パラメータ。 + - `query` (string) ユーザー入力/質問内容。 + - `message_files` (array[object]) メッセージファイル + - `id` (string) ID + - `type` (string) ファイルタイプ、画像の場合はimage + - `url` (string) プレビュー画像URL + - `belongs_to` (string) 所属、userまたはassistant + - `answer` (string) 応答メッセージ内容 + - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 + - `feedback` (object) フィードバック情報 + - `rating` (string) アップボートは`like` / ダウンボートは`dislike` + - `retriever_resources` (array[RetrieverResource]) 引用と帰属リスト + - `has_more` (bool) 次のページがあるかどうか + - `limit` (int) 返された項目数、入力がシステム制限を超える場合、システム制限数を返します + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/messages?user=abc-123&conversation_id=' + --header 'Authorization: Bearer {api_key}' + ``` + + + ### 応答例 + + ```json {{ title: '応答' }} + { + "limit": 20, + "has_more": false, + "data": [ + { + "id": "a076a87f-31e5-48dc-b452-0061adbbc922", + "conversation_id": "cd78daf6-f9e4-4463-9ff2-54257230a0ce", + "inputs": { + "name": "dify" + }, + "query": "iphone 13 pro", + "answer": "iPhone 13 Proは2021年9月24日に発売され、6.1インチのディスプレイと1170 x 2532の解像度を備えています。Hexa-core (2x3.23 GHz Avalanche + 4x1.82 GHz Blizzard)プロセッサ、6 GBのRAMを搭載し、128 GB、256 GB、512 GB、1 TBのストレージオプションを提供します。カメラは12 MP、バッテリー容量は3095 mAhで、iOS 15を搭載しています。", + "message_files": [], + "feedback": null, + "retriever_resources": [ + { + "position": 1, + "dataset_id": "101b4c97-fc2e-463c-90b1-5261a4cdcafb", + "dataset_name": "iPhone", + "document_id": "8dd1ad74-0b5f-4175-b735-7d98bbbb4e00", + "document_name": "iPhone List", + "segment_id": "ed599c7f-2766-4294-9d1d-e5235a61270a", + "score": 0.98457545, + "content": "\"Model\",\"Release Date\",\"Display Size\",\"Resolution\",\"Processor\",\"RAM\",\"Storage\",\"Camera\",\"Battery\",\"Operating System\"\n\"iPhone 13 Pro Max\",\"September 24, 2021\",\"6.7 inch\",\"1284 x 2778\",\"Hexa-core (2x3.23 GHz Avalanche + 4x1.82 GHz Blizzard)\",\"6 GB\",\"128, 256, 512 GB, 1TB\",\"12 MP\",\"4352 mAh\",\"iOS 15\"" + } + ], + "created_at": 1705569239, + } + ] + } + ``` + + + + +--- + + + + + 現在のユーザーの会話リストを取得し、デフォルトで最新の20件を返します。 + + ### クエリ + + + + ユーザー識別子、エンドユーザーの身元を定義するために使用され、統計のために使用されます。 + アプリケーション内で開発者によって一意に定義されるべきです。 + + + 現在のページの最後の記録のID、デフォルトはnullです。 + + + 1回のリクエストで返す記録の数、デフォルトは最新の20件です。 + + + ピン留めされた会話のみを`true`として返し、非ピン留めを`false`として返します + + + ソートフィールド(オプション)、デフォルト:-updated_at(更新時間で降順にソート) + - 利用可能な値:created_at, -created_at, updated_at, -updated_at + - フィールドの前の記号は順序または逆順を表し、"-"は逆順を表します。 + + + + ### 応答 + - `data` (array[object]) 会話のリスト + - `id` (string) 会話ID + - `name` (string) 会話名、デフォルトではLLMによって生成されます。 + - `inputs` (array[object]) ユーザー入力パラメータ。 + - `introduction` (string) 紹介 + - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 + - `has_more` (bool) + - `limit` (int) 返されたエントリ数、入力がシステム制限を超える場合、システム制限数が返されます + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/conversations?user=abc-123&last_id=&limit=20' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + + ```json {{ title: '応答' }} + { + "limit": 20, + "has_more": false, + "data": [ + { + "id": "10799fb8-64f7-4296-bbf7-b42bfbe0ae54", + "name": "新しいチャット", + "inputs": { + "book": "book", + "myName": "Lucy" + }, + "status": "normal", + "created_at": 1679667915 + }, + { + "id": "hSIhXBhNe8X1d8Et" + // ... + } + ] + } + ``` + + + + +--- + + + + + 会話を削除します。 + + ### パス + - `conversation_id` (string) 会話ID + + ### リクエストボディ + + + + ユーザー識別子、開発者によって定義され、アプリケーション内で一意であることを保証しなければなりません。 + + + + ### 応答 + - `result` (string) 常に"success"を返します + + + + + + ```bash {{ title: 'cURL' }} + curl -X DELETE '${props.appDetail.api_base_url}/conversations/{conversation_id}' \ + --header 'Content-Type: application/json' \ + --header 'Accept: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data '{ + "user": "abc-123" + }' + ``` + + + + + ```json {{ title: '応答' }} + { + "result": "success" + } + ``` + + + + +--- + + + + ### リクエストボディ + セッションの名前を変更します。セッション名は、複数のセッションをサポートするクライアントでの表示に使用されます。 + + ### パス + - `conversation_id` (string) 会話ID + + + + 会話の名前。`auto_generate`が`true`に設定されている場合、このパラメータは省略できます。 + + + タイトルを自動生成、デフォルトは`false` + + + ユーザー識別子、開発者によって定義され、アプリケーション内で一意であることを保証しなければなりません。 + + + + ### 応答 + - `id` (string) 会話ID + - `name` (string) 会話名 + - `inputs` array[object] ユーザー入力パラメータ。 + - `introduction` (string) 紹介 + - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 + + + + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/conversations/{conversation_id}/name' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "name": "", + "user": "abc-123" + }' + ``` + + + + + ```json {{ title: '応答' }} + { + "id": "cd78daf6-f9e4-4463-9ff2-54257230a0ce", + "name": "チャット vs AI", + "inputs": {}, + "introduction": "", + "created_at": 1705569238 + } + ``` + + + + +--- + + + + + このエンドポイントはmultipart/form-dataリクエストを必要とします。 + + ### リクエストボディ + + + + オーディオファイル。 + サポートされている形式:`['mp3', 'mp4', 'mpeg', 'mpga', 'm4a', 'wav', 'webm']` + ファイルサイズ制限:15MB + + + ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。 + + + + ### 応答 + - `text` (string) 出力テキスト + + + + + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/conversations/name' \ + --header 'Authorization: Bearer {api_key}' \ + --form 'file=@localfile;type=audio/mp3' + ``` + + + + + ```json {{ text: 'hello' }} + { + "text": "" + } + ``` + + + + +--- + + + + + テキストを音声に変換します。 + + ### リクエストボディ + + + + Difyによって生成されたテキストメッセージの場合、生成されたメッセージIDを直接渡します。バックエンドはメッセージIDを使用して対応する内容を検索し、音声情報を直接合成します。message_idとtextが同時に提供される場合、message_idが優先されます。 + + + 音声生成コンテンツ。 + + + ユーザー識別子、開発者によって定義され、アプリ内で一意であることを保証しなければなりません。 + + + + + + + + ```bash {{ title: 'cURL' }} + curl -o text-to-audio.mp3 -X POST '${props.appDetail.api_base_url}/text-to-audio' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", + "text": "Hello Dify", + "user": "abc-123", + "streaming": false + }' + ``` + + + + ```json {{ title: 'ヘッダー' }} + { + "Content-Type": "audio/wav" + } + ``` + + + + +--- + + + + + ページに入る際に、機能、入力パラメータ名、タイプ、デフォルト値などの情報を取得するために使用されます。 + + ### クエリ + + + + ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。 + + + + ### 応答 + - `opening_statement` (string) 開始の挨拶 + - `suggested_questions` (array[string]) 開始時の推奨質問のリスト + - `suggested_questions_after_answer` (object) 答えを有効にした後の質問を提案します。 + - `enabled` (bool) 有効かどうか + - `speech_to_text` (object) 音声からテキストへ + - `enabled` (bool) 有効かどうか + - `retriever_resource` (object) 引用と帰属 + - `enabled` (bool) 有効かどうか + - `annotation_reply` (object) 注釈返信 + - `enabled` (bool) 有効かどうか + - `user_input_form` (array[object]) ユーザー入力フォームの設定 + - `text-input` (object) テキスト入力コントロール + - `label` (string) 変数表示ラベル名 + - `variable` (string) 変数ID + - `required` (bool) 必須かどうか + - `default` (string) デフォルト値 + - `paragraph` (object) 段落テキスト入力コントロール + - `label` (string) 変数表示ラベル名 + - `variable` (string) 変数ID + - `required` (bool) 必須かどうか + - `default` (string) デフォルト値 + - `select` (object) ドロップダウンコントロール + - `label` (string) 変数表示ラベル名 + - `variable` (string) 変数ID + - `required` (bool) 必須かどうか + - `default` (string) デフォルト値 + - `options` (array[string]) オプション値 + - `file_upload` (object) ファイルアップロード設定 + - `image` (object) 画像設定 + 現在サポートされている画像タイプ:`png`, `jpg`, `jpeg`, `webp`, `gif` + - `enabled` (bool) 有効かどうか + - `number_limits` (int) 画像数の制限、デフォルトは3 + - `transfer_methods` (array[string]) 転送方法のリスト、remote_url, local_file、いずれかを選択する必要があります + - `system_parameters` (object) システムパラメータ + - `file_size_limit` (int) ドキュメントアップロードサイズ制限(MB) + - `image_file_size_limit` (int) 画像ファイルアップロードサイズ制限(MB) + - `audio_file_size_limit` (int) オーディオファイルアップロードサイズ制限(MB) + - `video_file_size_limit` (int) ビデオファイルアップロードサイズ制限(MB) + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/parameters?user=abc-123' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + + ```json {{ title: '応答' }} + { + "opening_statement": "こんにちは!", + "suggested_questions_after_answer": { + "enabled": true + }, + "speech_to_text": { + "enabled": true + }, + "retriever_resource": { + "enabled": true + }, + "annotation_reply": { + "enabled": true + }, + "user_input_form": [ + { + "paragraph": { + "label": "クエリ", + "variable": "query", + "required": true, + "default": "" + } + } + ], + "file_upload": { + "image": { + "enabled": false, + "number_limits": 3, + "detail": "high", + "transfer_methods": [ + "remote_url", + "local_file" + ] + } + }, + "system_parameters": { + "file_size_limit": 15, + "image_file_size_limit": 10, + "audio_file_size_limit": 50, + "video_file_size_limit": 100 + } + } + ``` + + + +--- + + + + + このアプリケーションのツールのアイコンを取得するために使用されます + ### クエリ + + + + ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。 + + + ### 応答 + - `tool_icons`(object[string]) ツールアイコン + - `tool_name` (string) + - `icon` (object|string) + - (object) アイコンオブジェクト + - `background` (string) 背景色(16進数形式) + - `content`(string) 絵文字 + - (string) アイコンのURL + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/meta?user=abc-123' \ + -H 'Authorization: Bearer {api_key}' + ``` + + + + + ```json {{ title: '応答' }} + { + "tool_icons": { + "dalle2": "https://cloud.dify.ai/console/api/workspaces/current/tool-provider/builtin/dalle/icon", + "api_tool": { + "background": "#252525", + "content": "\ud83d\ude01" + } + } + } + ``` + + + diff --git a/web/app/components/develop/template/template_chat.ja.mdx b/web/app/components/develop/template/template_chat.ja.mdx new file mode 100644 index 0000000000..a962177f0e --- /dev/null +++ b/web/app/components/develop/template/template_chat.ja.mdx @@ -0,0 +1,1134 @@ +import { CodeGroup } from '../code.tsx' +import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx' + +# チャットアプリAPI + +チャットアプリケーションはセッションの持続性をサポートしており、以前のチャット履歴を応答のコンテキストとして使用できます。これは、チャットボットやカスタマーサービスAIなどに適用できます。 + +
+ ### ベースURL + + ```javascript + ``` + + + ### 認証 + + サービスAPIは`API-Key`認証を使用します。 + **APIキーの漏洩を防ぐため、APIキーはクライアント側で共有または保存せず、サーバー側で保存することを強くお勧めします。** + + すべてのAPIリクエストにおいて、以下のように`Authorization`HTTPヘッダーにAPIキーを含めてください: + + + ```javascript + Authorization: Bearer {API_KEY} + + ``` + +
+ +--- + + + + + チャットアプリケーションにリクエストを送信します。 + + ### リクエストボディ + + + + ユーザー入力/質問内容 + + + アプリで定義されたさまざまな変数値の入力を許可します。 + `inputs`パラメータには複数のキー/値ペアが含まれ、各キーは特定の変数に対応し、各値はその変数の特定の値です。デフォルトは`{}` + + + 応答の返却モードを指定します。サポートされているモード: + - `streaming` ストリーミングモード(推奨)、SSE([Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events))を通じてタイプライターのような出力を実装します。 + - `blocking` ブロッキングモード、実行完了後に結果を返します。(プロセスが長い場合、リクエストが中断される可能性があります) + Cloudflareの制限により、100秒後に応答がない場合、リクエストは中断されます。 + 注:エージェントアシスタントモードではブロッキングモードはサポートされていません + + + ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用されます。 + アプリケーション内で開発者によって一意に定義される必要があります。 + + + 会話ID、以前のチャット記録に基づいて会話を続けるには、前のメッセージのconversation_idを渡す必要があります。 + + + ファイルリスト、テキストの理解と質問への回答を組み合わせたファイル(画像)の入力に適しており、モデルがビジョン機能をサポートしている場合にのみ利用可能です。 + - `type` (string) サポートされているタイプ:`image`(現在は画像タイプのみサポート) + - `transfer_method` (string) 転送方法、画像URLの場合は`remote_url` / ファイルアップロードの場合は`local_file` + - `url` (string) 画像URL(転送方法が`remote_url`の場合) + - `upload_file_id` (string) アップロードされたファイルID、事前にファイルアップロードAPIを通じて取得する必要があります(転送方法が`local_file`の場合) + + + タイトルを自動生成します。デフォルトは`true`です。 + `false`に設定すると、会話のリネームAPIを呼び出し、`auto_generate`を`true`に設定することで非同期タイトル生成を実現できます。 + + + + ### 応答 + response_modeがブロッキングの場合、CompletionResponseオブジェクトを返します。 + response_modeがストリーミングの場合、ChunkCompletionResponseストリームを返します。 + + ### ChatCompletionResponse + 完全なアプリ結果を返します。`Content-Type`は`application/json`です。 + - `message_id` (string) 一意のメッセージID + - `conversation_id` (string) 会話ID + - `mode` (string) アプリモード、`chat`として固定 + - `answer` (string) 完全な応答内容 + - `metadata` (object) メタデータ + - `usage` (Usage) モデル使用情報 + - `retriever_resources` (array[RetrieverResource]) 引用と帰属リスト + - `created_at` (int) メッセージ作成タイムスタンプ、例:1705395332 + + ### ChunkChatCompletionResponse + アプリによって出力されたストリームチャンクを返します。`Content-Type`は`text/event-stream`です。 + 各ストリーミングチャンクは`data:`で始まり、2つの改行文字`\n\n`で区切られます。以下のように表示されます: + + ```streaming {{ title: '応答' }} + data: {"event": "message", "task_id": "900bbd43-dc0b-4383-a372-aa6e6c414227", "id": "663c5084-a254-4040-8ad3-51f2a3c1a77c", "answer": "Hi", "created_at": 1705398420}\n\n + ``` + + ストリーミングチャンクの構造は`event`に応じて異なります: + - `event: message` LLMはテキストチャンクイベントを返します。つまり、完全なテキストがチャンク形式で出力されます。 + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `message_id` (string) 一意のメッセージID + - `conversation_id` (string) 会話ID + - `answer` (string) LLMが返したテキストチャンク内容 + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: agent_message` LLMはテキストチャンクイベントを返します。つまり、エージェントアシスタントが有効な場合、完全なテキストがチャンク形式で出力されます(エージェントモードでのみサポート) + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `message_id` (string) 一意のメッセージID + - `conversation_id` (string) 会話ID + - `answer` (string) LLMが返したテキストチャンク内容 + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: tts_message` TTSオーディオストリームイベント、つまり音声合成出力。内容はMp3形式のオーディオブロックで、base64文字列としてエンコードされています。再生時には、base64をデコードしてプレーヤーに入力するだけです。(このメッセージは自動再生が有効な場合にのみ利用可能) + - `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用 + - `message_id` (string) 一意のメッセージID + - `audio` (string) 音声合成後のオーディオ、base64テキストコンテンツとしてエンコードされており、再生時にはbase64をデコードしてプレーヤーに入力するだけです + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: tts_message_end` TTSオーディオストリーム終了イベント。このイベントを受信すると、オーディオストリームの終了を示します。 + - `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用 + - `message_id` (string) 一意のメッセージID + - `audio` (string) 終了イベントにはオーディオがないため、これは空の文字列です + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: agent_thought` エージェントの思考、LLMの思考、ツール呼び出しの入力と出力を含みます(エージェントモードでのみサポート) + - `id` (string) エージェント思考ID、各反復には一意のエージェント思考IDがあります + - `task_id` (string) (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `message_id` (string) 一意のメッセージID + - `position` (int) 現在のエージェント思考の位置、各メッセージには順番に複数の思考が含まれる場合があります。 + - `thought` (string) LLMが考えていること + - `observation` (string) ツール呼び出しからの応答 + - `tool` (string) 呼び出されたツールのリスト、;で区切られます + - `tool_input` (string) ツールの入力、JSON形式。例:`{"dalle3": {"prompt": "a cute cat"}}`。 + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `message_files` (array[string]) message_fileイベントを参照 + - `file_id` (string) ファイルID + - `conversation_id` (string) 会話ID + - `event: message_file` メッセージファイルイベント、ツールによって新しいファイルが作成されました + - `id` (string) ファイル一意ID + - `type` (string) ファイルタイプ、現在は"image"のみ許可 + - `belongs_to` (string) 所属、ここでは'assistant'のみ + - `url` (string) ファイルのリモートURL + - `conversation_id` (string) 会話ID + - `event: message_end` メッセージ終了イベント、このイベントを受信するとストリーミングが終了したことを意味します。 + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `message_id` (string) 一意のメッセージID + - `conversation_id` (string) 会話ID + - `metadata` (object) メタデータ + - `usage` (Usage) モデル使用情報 + - `retriever_resources` (array[RetrieverResource]) 引用と帰属リスト + - `event: message_replace` メッセージ内容置換イベント。 + 出力内容のモデレーションが有効な場合、内容がフラグされると、このイベントを通じてメッセージ内容が事前設定された返信に置き換えられます。 + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `message_id` (string) 一意のメッセージID + - `conversation_id` (string) 会話ID + - `answer` (string) 置換内容(すべてのLLM返信テキストを直接置換) + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: error` + ストリーミングプロセス中に発生した例外はストリームイベントの形式で出力され、エラーイベントを受信するとストリームが終了します。 + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `message_id` (string) 一意のメッセージID + - `status` (int) HTTPステータスコード + - `code` (string) エラーコード + - `message` (string) エラーメッセージ + - `event: ping` 接続を維持するために10秒ごとにpingイベントが発生します。 + + ### エラー + - 404, 会話が存在しません + - 400, `invalid_param`, 異常なパラメータ入力 + - 400, `app_unavailable`, アプリ構成が利用できません + - 400, `provider_not_initialize`, 利用可能なモデル資格情報構成がありません + - 400, `provider_quota_exceeded`, モデル呼び出しクォータが不足しています + - 400, `model_currently_not_support`, 現在のモデルは利用できません + - 400, `completion_request_error`, テキスト生成に失敗しました + - 500, 内部サーバーエラー + + + + + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/chat-messages' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "inputs": {}, + "query": "eh", + "response_mode": "streaming", + "conversation_id": "1c7e55fb-1ba2-4e10-81b5-30addcea2276", + "user": "abc-123" + }' + ``` + + ### ブロッキングモード + + ```json {{ title: '応答' }} + { + "event": "message", + "message_id": "9da23599-e713-473b-982c-4328d4f5c78a", + "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", + "mode": "chat", + "answer": "iPhone 13 Pro Maxの仕様は次のとおりです:...", + "metadata": { + "usage": { + "prompt_tokens": 1033, + "prompt_unit_price": "0.001", + "prompt_price_unit": "0.001", + "prompt_price": "0.0010330", + "completion_tokens": 128, + "completion_unit_price": "0.002", + "completion_price_unit": "0.001", + "completion_price": "0.0002560", + "total_tokens": 1161, + "total_price": "0.0012890", + "currency": "USD", + "latency": 0.7682376249867957 + }, + "retriever_resources": [ + { + "position": 1, + "dataset_id": "101b4c97-fc2e-463c-90b1-5261a4cdcafb", + "dataset_name": "iPhone", + "document_id": "8dd1ad74-0b5f-4175-b735-7d98bbbb4e00", + "document_name": "iPhone List", + "segment_id": "ed599c7f-2766-4294-9d1d-e5235a61270a", + "score": 0.98457545, + "content": "\"Model\",\"Release Date\",\"Display Size\",\"Resolution\",\"Processor\",\"RAM\",\"Storage\",\"Camera\",\"Battery\",\"Operating System\"\n\"iPhone 13 Pro Max\",\"September 24, 2021\",\"6.7 inch\",\"1284 x 2778\",\"Hexa-core (2x3.23 GHz Avalanche + 4x1.82 GHz Blizzard)\",\"6 GB\",\"128, 256, 512 GB, 1TB\",\"12 MP\",\"4352 mAh\",\"iOS 15\"" + } + ] + }, + "created_at": 1705407629 + } + ``` + + ### ストリーミングモード(基本アシスタント) + + ```streaming {{ title: '応答' }} + data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " I", "created_at": 1679586595} + data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": "'m", "created_at": 1679586595} + data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " glad", "created_at": 1679586595} + data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " to", "created_at": 1679586595} + data: {"event": "message", "message_id": : "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " meet", "created_at": 1679586595} + data: {"event": "message", "message_id": : "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " you", "created_at": 1679586595} + data: {"event": "message_end", "id": "5e52ce04-874b-4d27-9045-b3bc80def685", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "metadata": {"usage": {"prompt_tokens": 1033, "prompt_unit_price": "0.001", "prompt_price_unit": "0.001", "prompt_price": "0.0010330", "completion_tokens": 135, "completion_unit_price": "0.002", "completion_price_unit": "0.001", "completion_price": "0.0002700", "total_tokens": 1168, "total_price": "0.0013030", "currency": "USD", "latency": 1.381760165997548}, "retriever_resources": [{"position": 1, "dataset_id": "101b4c97-fc2e-463c-90b1-5261a4cdcafb", "dataset_name": "iPhone", "document_id": "8dd1ad74-0b5f-4175-b735-7d98bbbb4e00", "document_name": "iPhone List", "segment_id": "ed599c7f-2766-4294-9d1d-e5235a61270a", "score": 0.98457545, "content": "\"Model\",\"Release Date\",\"Display Size\",\"Resolution\",\"Processor\",\"RAM\",\"Storage\",\"Camera\",\"Battery\",\"Operating System\"\n\"iPhone 13 Pro Max\",\"September 24, 2021\",\"6.7 inch\",\"1284 x 2778\",\"Hexa-core (2x3.23 GHz Avalanche + 4x1.82 GHz Blizzard)\",\"6 GB\",\"128, 256, 512 GB, 1TB\",\"12 MP\",\"4352 mAh\",\"iOS 15\""}]}} + data: {"event": "tts_message", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"} + data: {"event": "tts_message_end", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": ""} + ``` + + ### 応答例(エージェントアシスタント) + + ```streaming {{ title: '応答' }} + data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " I", "created_at": 1679586595} + data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": "'m", "created_at": 1679586595} + data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " glad", "created_at": 1679586595} + data: {"event": "message", "message_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " to", "created_at": 1679586595} + data: {"event": "message", "message_id": : "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " meet", "created_at": 1679586595} + data: {"event": "message", "message_id": : "5ad4cb98-f0c7-4085-b384-88c403be6290", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "answer": " you", "created_at": 1679586595} + data: {"event": "message_end", "id": "5e52ce04-874b-4d27-9045-b3bc80def685", "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", "metadata": {"usage": {"prompt_tokens": 1033, "prompt_unit_price": "0.001", "prompt_price_unit": "0.001", "prompt_price": "0.0010330", "completion_tokens": 135, "completion_unit_price": "0.002", "completion_price_unit": "0.001", "completion_price": "0.0002700", "total_tokens": 1168, "total_price": "0.0013030", "currency": "USD", "latency": 1.381760165997548}, "retriever_resources": [{"position": 1, "dataset_id": "101b4c97-fc2e-463c-90b1-5261a4cdcafb", "dataset_name": "iPhone", "document_id": "8dd1ad74-0b5f-4175-b735-7d98bbbb4e00", "document_name": "iPhone List", "segment_id": "ed599c7f-2766-4294-9d1d-e5235a61270a", "score": 0.98457545, "content": "\"Model\",\"Release Date\",\"Display Size\",\"Resolution\",\"Processor\",\"RAM\",\"Storage\",\"Camera\",\"Battery\",\"Operating System\"\n\"iPhone 13 Pro Max\",\"September 24, 2021\",\"6.7 inch\",\"1284 x 2778\",\"Hexa-core (2x3.23 GHz Avalanche + 4x1.82 GHz Blizzard)\",\"6 GB\",\"128, 256, 512 GB, 1TB\",\"12 MP\",\"4352 mAh\",\"iOS 15\""}]}} + data: {"event": "tts_message", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"} + data: {"event": "tts_message_end", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": ""} + ``` + + + + +--- + + + + メッセージ送信時に使用するためのファイルをアップロードします(現在は画像のみサポート)。画像とテキストのマルチモーダル理解を可能にします。 + png、jpg、jpeg、webp、gif形式をサポートしています。 + アップロードされたファイルは現在のエンドユーザーのみが使用できます。 + + ### リクエストボディ + このインターフェースは`multipart/form-data`リクエストを必要とします。 + - `file` (File) 必須 + アップロードするファイル。 + - `user` (string) 必須 + ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。 + + ### 応答 + アップロードが成功すると、サーバーはファイルのIDと関連情報を返します。 + - `id` (uuid) ID + - `name` (string) ファイル名 + - `size` (int) ファイルサイズ(バイト) + - `extension` (string) ファイル拡張子 + - `mime_type` (string) ファイルのMIMEタイプ + - `created_by` (uuid) エンドユーザーID + - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 + + ### エラー + - 400, `no_file_uploaded`, ファイルが提供されなければなりません + - 400, `too_many_files`, 現在は1つのファイルのみ受け付けます + - 400, `unsupported_preview`, ファイルはプレビューをサポートしていません + - 400, `unsupported_estimate`, ファイルは推定をサポートしていません + - 413, `file_too_large`, ファイルが大きすぎます + - 415, `unsupported_file_type`, サポートされていない拡張子、現在はドキュメントファイルのみ受け付けます + - 503, `s3_connection_failed`, S3サービスに接続できません + - 503, `s3_permission_denied`, S3にファイルをアップロードする権限がありません + - 503, `s3_file_too_large`, ファイルがS3のサイズ制限を超えています + - 500, 内部サーバーエラー + + + + + ### リクエスト例 + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/files/upload' \ + --header 'Authorization: Bearer {api_key}' \ + --form 'file=@"/path/to/file"' + ``` + + + + + ### 応答例 + + ```json {{ title: '応答' }} + { + "id": "72fa9618-8f89-4a37-9b33-7e1178a24a67", + "name": "example.png", + "size": 1024, + "extension": "png", + "mime_type": "image/png", + "created_by": "6ad1ab0a-73ff-4ac1-b9e4-cdb312f71f13", + "created_at": 1577836800, + } + ``` + + + +--- + + + + + ストリーミングモードでのみサポートされています。 + ### パス + - `task_id` (string) タスクID、ストリーミングチャンクの返り値から取得できます + ### リクエストボディ + - `user` (string) 必須 + ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、メッセージ送信インターフェースで渡されたユーザーと一致している必要があります。 + ### 応答 + - `result` (string) 常に"success"を返します + + + ### リクエスト例 + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/chat-messages/:task_id/stop' \ + -H 'Authorization: Bearer {api_key}' \ + -H 'Content-Type: application/json' \ + --data-raw '{ + "user": "abc-123" + }' + ``` + + + ### 応答例 + + ```json {{ title: '応答' }} + { + "result": "success" + } + ``` + + + + +--- + + + + + エンドユーザーはフィードバックメッセージを提供でき、アプリケーション開発者が期待される出力を最適化するのに役立ちます。 + + ### パス + + + メッセージID + + + + ### リクエストボディ + + + + アップボートは`like`、ダウンボートは`dislike`、アップボートの取り消しは`null` + + + ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。 + + + + ### 応答 + - `result` (string) 常に"success"を返します + + + + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "rating": "like", + "user": "abc-123" + }' + ``` + + + + + ```json {{ title: '応答' }} + { + "result": "success" + } + ``` + + + + +--- + + + + + 現在のメッセージに対する次の質問の提案を取得します + + ### パスパラメータ + + + + メッセージID + + + + ### クエリ + + + ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、統計のために使用されます。 + アプリケーション内で開発者によって一意に定義される必要があります。 + + + + + + + + ```bash {{ title: 'cURL' }} + curl --location --request GET '${props.appDetail.api_base_url}/messages/{message_id}/suggested' \ + --header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \ + --header 'Content-Type: application/json' \ + ``` + + + + + ```json {{ title: '応答' }} + { + "result": "success", + "data": [ + "a", + "b", + "c" + ] + } + ``` + + + + +--- + + + + + スクロールロード形式で過去のチャット記録を返し、最初のページは最新の`{limit}`メッセージを返します。つまり、逆順です。 + + ### クエリ + + + + 会話ID + + + ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、統計のために使用されます。 + アプリケーション内で開発者によって一意に定義される必要があります。 + + + 現在のページの最初のチャット記録のID、デフォルトはnullです。 + + + 1回のリクエストで返すチャット履歴メッセージの数、デフォルトは20です。 + + + + ### 応答 + - `data` (array[object]) メッセージリスト + - `id` (string) メッセージID + - `conversation_id` (string) 会話ID + - `inputs` (array[object]) ユーザー入力パラメータ。 + - `query` (string) ユーザー入力/質問内容。 + - `message_files` (array[object]) メッセージファイル + - `id` (string) ID + - `type` (string) ファイルタイプ、画像の場合はimage + - `url` (string) プレビュー画像URL + - `belongs_to` (string) 所属、ユーザーまたはアシスタント + - `agent_thoughts` (array[object]) エージェントの思考(基本アシスタントの場合は空) + - `id` (string) エージェント思考ID、各反復には一意のエージェント思考IDがあります + - `message_id` (string) 一意のメッセージID + - `position` (int) 現在のエージェント思考の位置、各メッセージには順番に複数の思考が含まれる場合があります。 + - `thought` (string) LLMが考えていること + - `observation` (string) ツール呼び出しからの応答 + - `tool` (string) 呼び出されたツールのリスト、;で区切られます + - `tool_input` (string) ツールの入力、JSON形式。例:`{"dalle3": {"prompt": "a cute cat"}}`。 + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `message_files` (array[string]) message_fileイベントを参照 + - `file_id` (string) ファイルID + - `answer` (string) 応答メッセージ内容 + - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 + - `feedback` (object) フィードバック情報 + - `rating` (string) アップボートは`like` / ダウンボートは`dislike` + - `retriever_resources` (array[RetrieverResource]) 引用と帰属リスト + - `has_more` (bool) 次のページがあるかどうか + - `limit` (int) 返されたアイテムの数、入力がシステム制限を超える場合、システム制限の数を返します + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/messages?user=abc-123&conversation_id=' + --header 'Authorization: Bearer {api_key}' + ``` + + + ### 応答例(基本アシスタント) + + ```json {{ title: '応答' }} + { + "limit": 20, + "has_more": false, + "data": [ + { + "id": "a076a87f-31e5-48dc-b452-0061adbbc922", + "conversation_id": "cd78daf6-f9e4-4463-9ff2-54257230a0ce", + "inputs": { + "name": "dify" + }, + "query": "iphone 13 pro", + "answer": "iPhone 13 Proは2021年9月24日に発売され、6.1インチのディスプレイと1170 x 2532の解像度を備えています。Hexa-core (2x3.23 GHz Avalanche + 4x1.82 GHz Blizzard)プロセッサ、6 GBのRAMを搭載し、128 GB、256 GB、512 GB、1 TBのストレージオプションを提供します。カメラは12 MP、バッテリー容量は3095 mAhで、iOS 15を搭載しています。", + "message_files": [], + "feedback": null, + "retriever_resources": [ + { + "position": 1, + "dataset_id": "101b4c97-fc2e-463c-90b1-5261a4cdcafb", + "dataset_name": "iPhone", + "document_id": "8dd1ad74-0b5f-4175-b735-7d98bbbb4e00", + "document_name": "iPhone List", + "segment_id": "ed599c7f-2766-4294-9d1d-e5235a61270a", + "score": 0.98457545, + "content": "\"Model\",\"Release Date\",\"Display Size\",\"Resolution\",\"Processor\",\"RAM\",\"Storage\",\"Camera\",\"Battery\",\"Operating System\"\n\"iPhone 13 Pro Max\",\"September 24, 2021\",\"6.7 inch\",\"1284 x 2778\",\"Hexa-core (2x3.23 GHz Avalanche + 4x1.82 GHz Blizzard)\",\"6 GB\",\"128, 256, 512 GB, 1TB\",\"12 MP\",\"4352 mAh\",\"iOS 15\"" + } + ], + "agent_thoughts": [], + "created_at": 1705569239, + } + ] + } + ``` + + + ### 応答例(エージェントアシスタント) + + ```json {{ title: '応答' }} + { + "limit": 20, + "has_more": false, + "data": [ + { + "id": "d35e006c-7c4d-458f-9142-be4930abdf94", + "conversation_id": "957c068b-f258-4f89-ba10-6e8a0361c457", + "inputs": {}, + "query": "draw a cat", + "answer": "猫の画像を生成しました。メッセージを確認して画像を表示してください。", + "message_files": [ + { + "id": "976990d2-5294-47e6-8f14-7356ba9d2d76", + "type": "image", + "url": "http://127.0.0.1:5001/files/tools/976990d2-5294-47e6-8f14-7356ba9d2d76.png?timestamp=1705988524&nonce=55df3f9f7311a9acd91bf074cd524092&sign=z43nMSO1L2HBvoqADLkRxr7Biz0fkjeDstnJiCK1zh8=", + "belongs_to": "assistant" + } + ], + "feedback": null, + "retriever_resources": [], + "created_at": 1705988187, + "agent_thoughts": [ + { + "id": "592c84cf-07ee-441c-9dcc-ffc66c033469", + "chain_id": null, + "message_id": "d35e006c-7c4d-458f-9142-be4930abdf94", + "position": 1, + "thought": "", + "tool": "dalle2", + "tool_input": "{\"dalle2\": {\"prompt\": \"cat\"}}", + "created_at": 1705988186, + "observation": "画像はすでに作成され、ユーザーに送信されました。今すぐユーザーに確認するように伝えてください。", + "message_files": [ + "976990d2-5294-47e6-8f14-7356ba9d2d76" + ] + }, + { + "id": "73ead60d-2370-4780-b5ed-532d2762b0e5", + "chain_id": null, + "message_id": "d35e006c-7c4d-458f-9142-be4930abdf94", + "position": 2, + "thought": "猫の画像を生成しました。メッセージを確認して画像を表示してください。", + "tool": "", + "tool_input": "", + "created_at": 1705988199, + "observation": "", + "message_files": [] + } + ] + } + ] + } + ``` + + + + +--- + + + + + 現在のユーザーの会話リストを取得し、デフォルトで最新の20件を返します。 + + ### クエリ + + + + ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、統計のために使用されます。 + アプリケーション内で開発者によって一意に定義される必要があります。 + + + 現在のページの最後のレコードのID、デフォルトはnullです。 + + + 1回のリクエストで返すレコードの数、デフォルトは最新の20件です。 + + + ピン留めされた会話のみを`true`として返し、ピン留めされていないもののみを`false`として返します + + + ソートフィールド(オプション)、デフォルト:-updated_at(更新時間で降順にソート) + - 利用可能な値:created_at, -created_at, updated_at, -updated_at + - フィールドの前の記号は順序または逆順を表し、"-"は逆順を表します。 + + + + ### 応答 + - `data` (array[object]) 会話のリスト + - `id` (string) 会話ID + - `name` (string) 会話名、デフォルトでは、ユーザーが会話で最初に尋ねた質問のスニペットです。 + - `inputs` (array[object]) ユーザー入力パラメータ。 + - `introduction` (string) 紹介 + - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 + - `has_more` (bool) + - `limit` (int) 返されたエントリの数、入力がシステム制限を超える場合、システム制限の数を返します + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/conversations?user=abc-123&last_id=&limit=20' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + + ```json {{ title: '応答' }} + { + "limit": 20, + "has_more": false, + "data": [ + { + "id": "10799fb8-64f7-4296-bbf7-b42bfbe0ae54", + "name": "新しいチャット", + "inputs": { + "book": "book", + "myName": "Lucy" + }, + "status": "normal", + "created_at": 1679667915 + }, + { + "id": "hSIhXBhNe8X1d8Et" + // ... + } + ] + } + ``` + + + + +--- + + + + + 会話を削除します。 + + ### パス + - `conversation_id` (string) 会話ID + + ### リクエストボディ + + + + ユーザー識別子、開発者によって定義され、アプリケーション内で一意である必要があります。 + + + + ### 応答 + - `result` (string) 常に"success"を返します + + + + + + ```bash {{ title: 'cURL' }} + curl -X DELETE '${props.appDetail.api_base_url}/conversations/{conversation_id}' \ + --header 'Content-Type: application/json' \ + --header 'Accept: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data '{ + "user": "abc-123" + }' + ``` + + + + + ```json {{ title: '応答' }} + { + "result": "success" + } + ``` + + + + +--- + + + + ### リクエストボディ + セッションの名前を変更します。セッション名は、複数のセッションをサポートするクライアントでの表示に使用されます。 + + ### パス + - `conversation_id` (string) 会話ID + + + + 会話の名前。このパラメータは、`auto_generate`が`true`に設定されている場合、省略できます。 + + + タイトルを自動生成します。デフォルトは`false`です。 + + + ユーザー識別子、開発者によって定義され、アプリケーション内で一意である必要があります。 + + + + ### 応答 + - `id` (string) 会話ID + - `name` (string) 会話名 + - `inputs` array[object] ユーザー入力パラメータ。 + - `introduction` (string) 紹介 + - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 + + + + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/conversations/{conversation_id}/name' \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer {api_key}' \ + --data-raw '{ + "name": "", + "user": "abc-123" + }' + ``` + + + + + ```json {{ title: '応答' }} + { + "id": "cd78daf6-f9e4-4463-9ff2-54257230a0ce", + "name": "Chat vs AI", + "inputs": {}, + "introduction": "", + "created_at": 1705569238 + } + ``` + + + + +--- + + + + + このエンドポイントはmultipart/form-dataリクエストを必要とします。 + + ### リクエストボディ + + + + オーディオファイル。 + サポートされている形式:`['mp3', 'mp4', 'mpeg', 'mpga', 'm4a', 'wav', 'webm']` + ファイルサイズ制限:15MB + + + ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。 + + + + ### 応答 + - `text` (string) 出力テキスト + + + + + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/conversations/name' \ + --header 'Authorization: Bearer {api_key}' \ + --form 'file=@localfile;type=audio/mp3' + ``` + + + + + ```json {{ text: 'hello' }} + { + "text": "" + } + ``` + + + + +--- + + + + + テキストを音声に変換します。 + + ### リクエストボディ + + + + Difyによって生成されたテキストメッセージの場合、生成されたメッセージIDを直接渡します。バックエンドはメッセージIDを使用して対応するコンテンツを検索し、音声情報を直接合成します。message_idとtextが同時に提供される場合、message_idが優先されます。 + + + 音声生成コンテンツ。 + + + ユーザー識別子、開発者によって定義され、アプリ内で一意である必要があります。 + + + + + + + + ```bash {{ title: 'cURL' }} + curl --location --request POST '${props.appDetail.api_base_url}/text-to-audio' \ + --header 'Authorization: Bearer ENTER-YOUR-SECRET-KEY' \ + --form 'file=Hello Dify;user=abc-123;message_id=5ad4cb98-f0c7-4085-b384-88c403be6290' + ``` + + + + + ```json {{ title: 'ヘッダー' }} + { + "Content-Type": "audio/wav" + } + ``` + + + + +--- + + + + + ページに入る際に、機能、入力パラメータ名、タイプ、デフォルト値などの情報を取得するために使用されます。 + + ### クエリ + + + + ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。 + + + + ### 応答 + - `opening_statement` (string) 開始文 + - `suggested_questions` (array[string]) 開始時の推奨質問のリスト + - `suggested_questions_after_answer` (object) 答えを有効にした後の質問を提案します。 + - `enabled` (bool) 有効かどうか + - `speech_to_text` (object) 音声からテキストへ + - `enabled` (bool) 有効かどうか + - `retriever_resource` (object) 引用と帰属 + - `enabled` (bool) 有効かどうか + - `annotation_reply` (object) 注釈返信 + - `enabled` (bool) 有効かどうか + - `user_input_form` (array[object]) ユーザー入力フォームの構成 + - `text-input` (object) テキスト入力コントロール + - `label` (string) 変数表示ラベル名 + - `variable` (string) 変数ID + - `required` (bool) 必須かどうか + - `default` (string) デフォルト値 + - `paragraph` (object) 段落テキスト入力コントロール + - `label` (string) 変数表示ラベル名 + - `variable` (string) 変数ID + - `required` (bool) 必須かどうか + - `default` (string) デフォルト値 + - `select` (object) ドロップダウンコントロール + - `label` (string) 変数表示ラベル名 + - `variable` (string) 変数ID + - `required` (bool) 必須かどうか + - `default` (string) デフォルト値 + - `options` (array[string]) オプション値 + - `file_upload` (object) ファイルアップロード構成 + - `image` (object) 画像設定 + 現在サポートされている画像タイプ:`png`, `jpg`, `jpeg`, `webp`, `gif` + - `enabled` (bool) 有効かどうか + - `number_limits` (int) 画像数の制限、デフォルトは3 + - `transfer_methods` (array[string]) 転送方法のリスト、remote_url, local_file、いずれかを選択する必要があります + - `system_parameters` (object) システムパラメータ + - `file_size_limit` (int) ドキュメントアップロードサイズ制限(MB) + - `image_file_size_limit` (int) 画像ファイルアップロードサイズ制限(MB) + - `audio_file_size_limit` (int) オーディオファイルアップロードサイズ制限(MB) + - `video_file_size_limit` (int) ビデオファイルアップロードサイズ制限(MB) + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/parameters?user=abc-123' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + + ```json {{ title: '応答' }} + { + "opening_statement": "こんにちは!", + "suggested_questions_after_answer": { + "enabled": true + }, + "speech_to_text": { + "enabled": true + }, + "retriever_resource": { + "enabled": true + }, + "annotation_reply": { + "enabled": true + }, + "user_input_form": [ + { + "paragraph": { + "label": "クエリ", + "variable": "query", + "required": true, + "default": "" + } + } + ], + "file_upload": { + "image": { + "enabled": false, + "number_limits": 3, + "detail": "high", + "transfer_methods": [ + "remote_url", + "local_file" + ] + } + }, + "system_parameters": { + "file_size_limit": 15, + "image_file_size_limit": 10, + "audio_file_size_limit": 50, + "video_file_size_limit": 100 + } + } + ``` + + + +--- + + + + + このアプリケーションのツールのアイコンを取得するために使用されます + ### クエリ + + + + ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。 + + + ### 応答 + - `tool_icons`(object[string]) ツールアイコン + - `tool_name` (string) + - `icon` (object|string) + - (object) アイコンオブジェクト + - `background` (string) 背景色(16進数形式) + - `content`(string) 絵文字 + - (string) アイコンのURL + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/meta?user=abc-123' \ + -H 'Authorization: Bearer {api_key}' + ``` + + + + + ```json {{ title: '応答' }} + { + "tool_icons": { + "dalle2": "https://cloud.dify.ai/console/api/workspaces/current/tool-provider/builtin/dalle/icon", + "api_tool": { + "background": "#252525", + "content": "\ud83d\ude01" + } + } + } + ``` + + + diff --git a/web/app/components/develop/template/template_workflow.ja.mdx b/web/app/components/develop/template/template_workflow.ja.mdx new file mode 100644 index 0000000000..ad669430f2 --- /dev/null +++ b/web/app/components/develop/template/template_workflow.ja.mdx @@ -0,0 +1,607 @@ +import { CodeGroup } from '../code.tsx' +import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx' + +# ワークフローアプリAPI + +ワークフローアプリケーションは、セッションをサポートせず、翻訳、記事作成、要約AIなどに最適です。 + +
+ ### ベースURL + + ```javascript + ``` + + + ### 認証 + + サービスAPIは`API-Key`認証を使用します。 + **APIキーの漏洩を防ぐため、APIキーはクライアント側で共有または保存せず、サーバー側で保存することを強くお勧めします。** + + すべてのAPIリクエストにおいて、以下のように`Authorization`HTTPヘッダーにAPIキーを含めてください: + + + ```javascript + Authorization: Bearer {API_KEY} + + ``` + +
+ +--- + + + + + ワークフローを実行します。公開されたワークフローがないと実行できません。 + + ### リクエストボディ + - `inputs` (object) 必須 + アプリで定義されたさまざまな変数値の入力を許可します。 + `inputs`パラメータには複数のキー/値ペアが含まれ、各キーは特定の変数に対応し、各値はその変数の特定の値です。 + ワークフローアプリケーションは少なくとも1つのキー/値ペアの入力を必要とします。 + 変数がファイルタイプの場合、以下の`files`で説明されているキーを持つオブジェクトを指定してください。 + - `response_mode` (string) 必須 + 応答の返却モードを指定します。サポートされているモード: + - `streaming` ストリーミングモード(推奨)、SSE([Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events))を通じてタイプライターのような出力を実装します。 + - `blocking` ブロッキングモード、実行完了後に結果を返します。(プロセスが長い場合、リクエストが中断される可能性があります) + Cloudflareの制限により、100秒後に応答がない場合、リクエストは中断されます。 + - `user` (string) 必須 + ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用されます。 + アプリケーション内で開発者によって一意に定義される必要があります。 + - `files` (array[object]) オプション + ファイルリスト、テキストの理解と質問への回答を組み合わせたファイルの入力に適しており、モデルがビジョン機能をサポートしている場合にのみ利用可能です。 + - `type` (string) サポートされているタイプ: + - `document` ('TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB') + - `image` ('JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG') + - `audio` ('MP3', 'M4A', 'WAV', 'WEBM', 'AMR') + - `video` ('MP4', 'MOV', 'MPEG', 'MPGA') + - `transfer_method` (string) 転送方法、画像URLの場合は`remote_url` / ファイルアップロードの場合は`local_file` + - `url` (string) 画像URL(転送方法が`remote_url`の場合) + - `upload_file_id` (string) アップロードされたファイルID、事前にファイルアップロードAPIを通じて取得する必要があります(転送方法が`local_file`の場合) + + ### 応答 + `response_mode`が`blocking`の場合、CompletionResponseオブジェクトを返します。 + `response_mode`が`streaming`の場合、ChunkCompletionResponseストリームを返します。 + + ### CompletionResponse + アプリの結果を返します。`Content-Type`は`application/json`です。 + - `workflow_run_id` (string) ワークフロー実行の一意のID + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `data` (object) 結果の詳細 + - `id` (string) ワークフロー実行のID + - `workflow_id` (string) 関連するワークフローのID + - `status` (string) 実行のステータス、`running` / `succeeded` / `failed` / `stopped` + - `outputs` (json) オプションの出力内容 + - `error` (string) オプションのエラー理由 + - `elapsed_time` (float) オプションの使用時間(秒) + - `total_tokens` (int) オプションの使用トークン数 + - `total_steps` (int) デフォルト0 + - `created_at` (timestamp) 開始時間 + - `finished_at` (timestamp) 終了時間 + + ### ChunkCompletionResponse + アプリによって出力されたストリームチャンクを返します。`Content-Type`は`text/event-stream`です。 + 各ストリーミングチャンクは`data:`で始まり、2つの改行文字`\n\n`で区切られます。以下のように表示されます: + + ```streaming {{ title: '応答' }} + data: {"event": "message", "task_id": "900bbd43-dc0b-4383-a372-aa6e6c414227", "id": "663c5084-a254-4040-8ad3-51f2a3c1a77c", "answer": "Hi", "created_at": 1705398420}\n\n + ``` + + ストリーミングチャンクの構造は`event`に応じて異なります: + - `event: workflow_started` ワークフローが実行を開始 + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `workflow_run_id` (string) ワークフロー実行の一意のID + - `event` (string) `workflow_started`に固定 + - `data` (object) 詳細 + - `id` (string) ワークフロー実行の一意のID + - `workflow_id` (string) 関連するワークフローのID + - `sequence_number` (int) 自己増加シリアル番号、アプリ内で自己増加し、1から始まります + - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 + - `event: node_started` ノード実行開始 + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `workflow_run_id` (string) ワークフロー実行の一意のID + - `event` (string) `node_started`に固定 + - `data` (object) 詳細 + - `id` (string) ワークフロー実行の一意のID + - `node_id` (string) ノードのID + - `node_type` (string) ノードのタイプ + - `title` (string) ノードの名前 + - `index` (int) 実行シーケンス番号、トレースノードシーケンスを表示するために使用 + - `predecessor_node_id` (string) オプションのプレフィックスノードID、キャンバス表示実行パスに使用 + - `inputs` (array[object]) ノードで使用されるすべての前のノード変数の内容 + - `created_at` (timestamp) 開始のタイムスタンプ、例:1705395332 + - `event: node_finished` ノード実行終了、同じイベントで異なる状態で成功または失敗 + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `workflow_run_id` (string) ワークフロー実行の一意のID + - `event` (string) `node_finished`に固定 + - `data` (object) 詳細 + - `id` (string) ワークフロー実行の一意のID + - `node_id` (string) ノードのID + - `node_type` (string) ノードのタイプ + - `title` (string) ノードの名前 + - `index` (int) 実行シーケンス番号、トレースノードシーケンスを表示するために使用 + - `predecessor_node_id` (string) オプションのプレフィックスノードID、キャンバス表示実行パスに使用 + - `inputs` (array[object]) ノードで使用されるすべての前のノード変数の内容 + - `process_data` (json) オプションのノードプロセスデータ + - `outputs` (json) オプションの出力内容 + - `status` (string) 実行のステータス、`running` / `succeeded` / `failed` / `stopped` + - `error` (string) オプションのエラー理由 + - `elapsed_time` (float) オプションの使用時間(秒) + - `execution_metadata` (json) メタデータ + - `total_tokens` (int) オプションの使用トークン数 + - `total_price` (decimal) オプションの総コスト + - `currency` (string) オプション 例:`USD` / `RMB` + - `created_at` (timestamp) 開始のタイムスタンプ、例:1705395332 + - `event: workflow_finished` ワークフロー実行終了、同じイベントで異なる状態で成功または失敗 + - `task_id` (string) タスクID、リクエスト追跡と以下のStop Generate APIに使用 + - `workflow_run_id` (string) ワークフロー実行の一意のID + - `event` (string) `workflow_finished`に固定 + - `data` (object) 詳細 + - `id` (string) ワークフロー実行のID + - `workflow_id` (string) 関連するワークフローのID + - `status` (string) 実行のステータス、`running` / `succeeded` / `failed` / `stopped` + - `outputs` (json) オプションの出力内容 + - `error` (string) オプションのエラー理由 + - `elapsed_time` (float) オプションの使用時間(秒) + - `total_tokens` (int) オプションの使用トークン数 + - `total_steps` (int) デフォルト0 + - `created_at` (timestamp) 開始時間 + - `finished_at` (timestamp) 終了時間 + - `event: tts_message` TTSオーディオストリームイベント、つまり音声合成出力。内容はMp3形式のオーディオブロックで、base64文字列としてエンコードされています。再生時には、base64をデコードしてプレーヤーに入力するだけです。(このメッセージは自動再生が有効な場合にのみ利用可能) + - `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用 + - `message_id` (string) 一意のメッセージID + - `audio` (string) 音声合成後のオーディオ、base64テキストコンテンツとしてエンコードされており、再生時にはbase64をデコードしてプレーヤーに入力するだけです + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: tts_message_end` TTSオーディオストリーム終了イベント。このイベントを受信すると、オーディオストリームの終了を示します。 + - `task_id` (string) タスクID、リクエスト追跡と以下の停止応答インターフェースに使用 + - `message_id` (string) 一意のメッセージID + - `audio` (string) 終了イベントにはオーディオがないため、これは空の文字列です + - `created_at` (int) 作成タイムスタンプ、例:1705395332 + - `event: ping` 接続を維持するために10秒ごとに送信されるPingイベント。 + + ### エラー + - 400, `invalid_param`, 異常なパラメータ入力 + - 400, `app_unavailable`, アプリの設定が利用できません + - 400, `provider_not_initialize`, 利用可能なモデル資格情報の設定がありません + - 400, `provider_quota_exceeded`, モデル呼び出しのクォータが不足しています + - 400, `model_currently_not_support`, 現在のモデルは利用できません + - 400, `workflow_request_error`, ワークフロー実行に失敗しました + - 500, 内部サーバーエラー + + + + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/workflows/run' \ + --header 'Authorization: Bearer {api_key}' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "inputs": {}, + "response_mode": "streaming", + "user": "abc-123" + }' + ``` + + + ### ブロッキングモード + + ```json {{ title: '応答' }} + { + "workflow_run_id": "djflajgkldjgd", + "task_id": "9da23599-e713-473b-982c-4328d4f5c78a", + "data": { + "id": "fdlsjfjejkghjda", + "workflow_id": "fldjaslkfjlsda", + "status": "succeeded", + "outputs": { + "text": "Nice to meet you." + }, + "error": null, + "elapsed_time": 0.875, + "total_tokens": 3562, + "total_steps": 8, + "created_at": 1705407629, + "finished_at": 1727807631 + } + } + ``` + + ### ストリーミングモード + + ```streaming {{ title: '応答' }} + data: {"event": "workflow_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "sequence_number": 1, "created_at": 1679586595}} + data: {"event": "node_started", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "created_at": 1679586595}} + data: {"event": "node_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "node_id": "dfjasklfjdslag", "node_type": "start", "title": "Start", "index": 0, "predecessor_node_id": "fdljewklfklgejlglsd", "inputs": {}, "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "execution_metadata": {"total_tokens": 63127864, "total_price": 2.378, "currency": "USD"}, "created_at": 1679586595}} + data: {"event": "workflow_finished", "task_id": "5ad4cb98-f0c7-4085-b384-88c403be6290", "workflow_run_id": "5ad498-f0c7-4085-b384-88cbe6290", "data": {"id": "5ad498-f0c7-4085-b384-88cbe6290", "workflow_id": "dfjasklfjdslag", "outputs": {}, "status": "succeeded", "elapsed_time": 0.324, "total_tokens": 63127864, "total_steps": "1", "created_at": 1679586595, "finished_at": 1679976595}} + data: {"event": "tts_message", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"} + data: {"event": "tts_message_end", "conversation_id": "23dd85f3-1a41-4ea0-b7a9-062734ccfaf9", "message_id": "a8bdc41c-13b2-4c18-bfd9-054b9803038c", "created_at": 1721205487, "task_id": "3bf8a0bb-e73b-4690-9e66-4e429bad8ee7", "audio": ""} + ``` + + + + + +--- + + + + + ワークフロー実行IDに基づいて、ワークフロータスクの現在の実行結果を取得します。 + ### パス + - `workflow_id` (string) ワークフローID、ストリーミングチャンクの返り値から取得可能 + ### 応答 + - `id` (string) ワークフロー実行のID + - `workflow_id` (string) 関連するワークフローのID + - `status` (string) 実行のステータス、`running` / `succeeded` / `failed` / `stopped` + - `inputs` (json) 入力内容 + - `outputs` (json) 出力内容 + - `error` (string) エラー理由 + - `total_steps` (int) タスクの総ステップ数 + - `total_tokens` (int) 使用されるトークンの総数 + - `created_at` (timestamp) 開始時間 + - `finished_at` (timestamp) 終了時間 + - `elapsed_time` (float) 使用される総秒数 + + + ### リクエスト例 + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/workflows/run/:workflow_id' \ + -H 'Authorization: Bearer {api_key}' \ + -H 'Content-Type: application/json' + ``` + + + ### 応答例 + + ```json {{ title: '応答' }} + { + "id": "b1ad3277-089e-42c6-9dff-6820d94fbc76", + "workflow_id": "19eff89f-ec03-4f75-b0fc-897e7effea02", + "status": "succeeded", + "inputs": "{\"sys.files\": [], \"sys.user_id\": \"abc-123\"}", + "outputs": null, + "error": null, + "total_steps": 3, + "total_tokens": 0, + "created_at": "Thu, 18 Jul 2024 03:17:40 -0000", + "finished_at": "Thu, 18 Jul 2024 03:18:10 -0000", + "elapsed_time": 30.098514399956912 + } + ``` + + + + +--- + + + + + ストリーミングモードでのみサポートされています。 + ### パス + - `task_id` (string) タスクID、ストリーミングチャンクの返り値から取得可能 + ### リクエストボディ + - `user` (string) 必須 + ユーザー識別子、エンドユーザーのアイデンティティを定義するために使用され、送信メッセージインターフェースで渡されたユーザーと一致している必要があります。 + ### 応答 + - `result` (string) 常に"success"を返します + + + ### リクエスト例 + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/workflows/tasks/:task_id/stop' \ + -H 'Authorization: Bearer {api_key}' \ + -H 'Content-Type: application/json' \ + --data-raw '{ + "user": "abc-123" + }' + ``` + + + ### 応答例 + + ```json {{ title: '応答' }} + { + "result": "success" + } + ``` + + + + +--- + + + + + メッセージ送信時に使用するためのファイルをアップロードし、画像とテキストのマルチモーダル理解を可能にします。 + ワークフローでサポートされている任意の形式をサポートします。 + アップロードされたファイルは、現在のエンドユーザーのみが使用できます。 + + ### リクエストボディ + このインターフェースは`multipart/form-data`リクエストを必要とします。 + - `file` (File) 必須 + アップロードするファイル。 + - `user` (string) 必須 + ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。 + + ### 応答 + アップロードが成功すると、サーバーはファイルのIDと関連情報を返します。 + - `id` (uuid) ID + - `name` (string) ファイル名 + - `size` (int) ファイルサイズ(バイト) + - `extension` (string) ファイル拡張子 + - `mime_type` (string) ファイルのMIMEタイプ + - `created_by` (uuid) エンドユーザーID + - `created_at` (timestamp) 作成タイムスタンプ、例:1705395332 + + ### エラー + - 400, `no_file_uploaded`, ファイルが提供されていません + - 400, `too_many_files`, 現在は1つのファイルのみ受け付けています + - 400, `unsupported_preview`, ファイルはプレビューをサポートしていません + - 400, `unsupported_estimate`, ファイルは推定をサポートしていません + - 413, `file_too_large`, ファイルが大きすぎます + - 415, `unsupported_file_type`, サポートされていない拡張子、現在はドキュメントファイルのみ受け付けています + - 503, `s3_connection_failed`, S3サービスに接続できません + - 503, `s3_permission_denied`, S3にファイルをアップロードする権限がありません + - 503, `s3_file_too_large`, ファイルがS3のサイズ制限を超えています + - 500, 内部サーバーエラー + + + + + ### リクエスト例 + + + ```bash {{ title: 'cURL' }} + curl -X POST '${props.appDetail.api_base_url}/files/upload' \ + --header 'Authorization: Bearer {api_key}' \ + --form 'file=@"/path/to/file"' + ``` + + + + + ### 応答例 + + ```json {{ title: '応答' }} + { + "id": "72fa9618-8f89-4a37-9b33-7e1178a24a67", + "name": "example.png", + "size": 1024, + "extension": "png", + "mime_type": "image/png", + "created_by": "6ad1ab0a-73ff-4ac1-b9e4-cdb312f71f13", + "created_at": 1577836800, + } + ``` + + + + +--- + + + + + ページに入る際に、機能、入力パラメータ名、タイプ、デフォルト値などの情報を取得するために使用されます。 + + ### クエリ + + + + ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。 + + + + ### 応答 + - `user_input_form` (array[object]) ユーザー入力フォームの設定 + - `text-input` (object) テキスト入力コントロール + - `label` (string) 変数表示ラベル名 + - `variable` (string) 変数ID + - `required` (bool) 必須かどうか + - `default` (string) デフォルト値 + - `paragraph` (object) 段落テキスト入力コントロール + - `label` (string) 変数表示ラベル名 + - `variable` (string) 変数ID + - `required` (bool) 必須かどうか + - `default` (string) デフォルト値 + - `select` (object) ドロップダウンコントロール + - `label` (string) 変数表示ラベル名 + - `variable` (string) 変数ID + - `required` (bool) 必須かどうか + - `default` (string) デフォルト値 + - `options` (array[string]) オプション値 + - `file_upload` (object) ファイルアップロード設定 + - `image` (object) 画像設定 + 現在サポートされている画像タイプのみ:`png`, `jpg`, `jpeg`, `webp`, `gif` + - `enabled` (bool) 有効かどうか + - `number_limits` (int) 画像数の制限、デフォルトは3 + - `transfer_methods` (array[string]) 転送方法のリスト、remote_url, local_file、いずれかを選択する必要があります + - `system_parameters` (object) システムパラメータ + - `file_size_limit` (int) ドキュメントアップロードサイズ制限(MB) + - `image_file_size_limit` (int) 画像ファイルアップロードサイズ制限(MB) + - `audio_file_size_limit` (int) オーディオファイルアップロードサイズ制限(MB) + - `video_file_size_limit` (int) ビデオファイルアップロードサイズ制限(MB) + + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/parameters?user=abc-123' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + + ```json {{ title: '応答' }} + { + "user_input_form": [ + { + "paragraph": { + "label": "Query", + "variable": "query", + "required": true, + "default": "" + } + } + ], + "file_upload": { + "image": { + "enabled": false, + "number_limits": 3, + "detail": "high", + "transfer_methods": [ + "remote_url", + "local_file" + ] + } + }, + "system_parameters": { + "file_size_limit": 15, + "image_file_size_limit": 10, + "audio_file_size_limit": 50, + "video_file_size_limit": 100 + } + } + ``` + + + + +--- + + + + + ワークフローログを返します。最初のページは最新の`{limit}`メッセージを返します。つまり、逆順です。 + + ### クエリ + + + + 検索するキーワード + + + succeeded/failed/stopped + + + 現在のページ、デフォルトは1。 + + + 1回のリクエストで返すチャット履歴メッセージの数、デフォルトは20。 + + + + ### 応答 + - `page` (int) 現在のページ + - `limit` (int) 返されたアイテムの数、入力がシステム制限を超える場合、システム制限量を返します + - `total` (int) 合計アイテム数 + - `has_more` (bool) 次のページがあるかどうか + - `data` (array[object]) ログリスト + - `id` (string) ID + - `workflow_run` (object) ワークフロー実行 + - `id` (string) ID + - `version` (string) バージョン + - `status` (string) 実行のステータス、`running` / `succeeded` / `failed` / `stopped` + - `error` (string) オプションのエラー理由 + - `elapsed_time` (float) 使用される総秒数 + - `total_tokens` (int) 使用されるトークン数 + - `total_steps` (int) デフォルト0 + - `created_at` (timestamp) 開始時間 + - `finished_at` (timestamp) 終了時間 + - `created_from` (string) 作成元 + - `created_by_role` (string) 作成者の役割 + - `created_by_account` (string) オプションの作成者アカウント + - `created_by_end_user` (object) エンドユーザーによって作成 + - `id` (string) ID + - `type` (string) タイプ + - `is_anonymous` (bool) 匿名かどうか + - `session_id` (string) セッションID + - `created_at` (timestamp) 作成時間 + + + + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/workflows/logs?limit=1' + --header 'Authorization: Bearer {api_key}' + ``` + + + ### 応答例 + + ```json {{ title: '応答' }} + { + "page": 1, + "limit": 1, + "total": 7, + "has_more": true, + "data": [ + { + "id": "e41b93f1-7ca2-40fd-b3a8-999aeb499cc0", + "workflow_run": { + "id": "c0640fc8-03ef-4481-a96c-8a13b732a36e", + "version": "2024-08-01 12:17:09.771832", + "status": "succeeded", + "error": null, + "elapsed_time": 1.3588523610014818, + "total_tokens": 0, + "total_steps": 3, + "created_at": 1726139643, + "finished_at": 1726139644 + }, + "created_from": "service-api", + "created_by_role": "end_user", + "created_by_account": null, + "created_by_end_user": { + "id": "7f7d9117-dd9d-441d-8970-87e5e7e687a3", + "type": "service_api", + "is_anonymous": false, + "session_id": "abc-123" + }, + "created_at": 1726139644 + } + ] + } + ``` + + + From ea0ebc020c48e66b3bc70d393b13be942d01c9e8 Mon Sep 17 00:00:00 2001 From: Hash Brown Date: Thu, 21 Nov 2024 14:12:01 +0800 Subject: [PATCH 024/103] fix: chat history might be empty in log detail view (#10905) --- .../__snapshots__/utils.spec.ts.snap | 251 +++++++++++++++++- .../base/chat/__tests__/partialMessages.json | 122 +++++++++ .../base/chat/__tests__/utils.spec.ts | 13 +- web/app/components/base/chat/utils.ts | 11 +- 4 files changed, 386 insertions(+), 11 deletions(-) create mode 100644 web/app/components/base/chat/__tests__/partialMessages.json diff --git a/web/app/components/base/chat/__tests__/__snapshots__/utils.spec.ts.snap b/web/app/components/base/chat/__tests__/__snapshots__/utils.spec.ts.snap index 7da09c4529..4ffcfa31e9 100644 --- a/web/app/components/base/chat/__tests__/__snapshots__/utils.spec.ts.snap +++ b/web/app/components/base/chat/__tests__/__snapshots__/utils.spec.ts.snap @@ -1804,8 +1804,85 @@ exports[`build chat item tree and get thread messages should get thread messages ] `; -exports[`build chat item tree and get thread messages should work with partial messages 1`] = ` +exports[`build chat item tree and get thread messages should work with partial messages 1 1`] = ` [ + { + "children": [ + { + "agent_thoughts": [ + { + "chain_id": null, + "created_at": 1726105799, + "files": [], + "id": "9730d587-9268-4683-9dd9-91a1cab9510b", + "message_id": "4c5d0841-1206-463e-95d8-71f812877658", + "observation": "", + "position": 1, + "thought": "I'll go with 112. Your turn!", + "tool": "", + "tool_input": "", + "tool_labels": {}, + }, + ], + "children": [], + "content": "I'll go with 112. Your turn!", + "conversationId": "dd6c9cfd-2656-48ec-bd51-2139c1790d80", + "feedbackDisabled": false, + "id": "4c5d0841-1206-463e-95d8-71f812877658", + "input": { + "inputs": {}, + "query": "99", + }, + "isAnswer": true, + "log": [ + { + "files": [], + "role": "user", + "text": "Let's play a game, I say a number , and you response me with another bigger, yet randomly number. I'll start first, 38", + }, + { + "files": [], + "role": "assistant", + "text": "Sure, I'll play! My number is 57. Your turn!", + }, + { + "files": [], + "role": "user", + "text": "58", + }, + { + "files": [], + "role": "assistant", + "text": "I choose 83. What's your next number?", + }, + { + "files": [], + "role": "user", + "text": "99", + }, + { + "files": [], + "role": "assistant", + "text": "I'll go with 112. Your turn!", + }, + ], + "message_files": [], + "more": { + "latency": "1.49", + "time": "09/11/2024 09:50 PM", + "tokens": 86, + }, + "parentMessageId": "question-4c5d0841-1206-463e-95d8-71f812877658", + "siblingIndex": 0, + "workflow_run_id": null, + }, + ], + "content": "99", + "id": "question-4c5d0841-1206-463e-95d8-71f812877658", + "isAnswer": false, + "message_files": [], + "parentMessageId": "73bbad14-d915-499d-87bf-0df14d40779d", + }, { "children": [ { @@ -2078,6 +2155,178 @@ exports[`build chat item tree and get thread messages should work with partial m ] `; +exports[`build chat item tree and get thread messages should work with partial messages 2 1`] = ` +[ + { + "children": [ + { + "children": [], + "content": "237.", + "id": "ebb73fe2-15de-46dd-aab5-75416d8448eb", + "isAnswer": true, + "parentMessageId": "question-ebb73fe2-15de-46dd-aab5-75416d8448eb", + "siblingIndex": 0, + }, + ], + "content": "123", + "id": "question-ebb73fe2-15de-46dd-aab5-75416d8448eb", + "isAnswer": false, + "parentMessageId": "57c989f9-3fa4-4dec-9ee5-c3568dd27418", + }, + { + "children": [ + { + "children": [], + "content": "My number is 256.", + "id": "3553d508-3850-462e-8594-078539f940f9", + "isAnswer": true, + "parentMessageId": "question-3553d508-3850-462e-8594-078539f940f9", + "siblingIndex": 1, + }, + ], + "content": "123", + "id": "question-3553d508-3850-462e-8594-078539f940f9", + "isAnswer": false, + "parentMessageId": "57c989f9-3fa4-4dec-9ee5-c3568dd27418", + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [], + "content": "My number is 3e (approximately 8.15).", + "id": "9eac3bcc-8d3b-4e56-a12b-44c34cebc719", + "isAnswer": true, + "parentMessageId": "question-9eac3bcc-8d3b-4e56-a12b-44c34cebc719", + "siblingIndex": 0, + }, + ], + "content": "e", + "id": "question-9eac3bcc-8d3b-4e56-a12b-44c34cebc719", + "isAnswer": false, + "parentMessageId": "5c56a2b3-f057-42a0-9b2c-52a35713cd8c", + }, + ], + "content": "My number is 2π (approximately 6.28).", + "id": "5c56a2b3-f057-42a0-9b2c-52a35713cd8c", + "isAnswer": true, + "parentMessageId": "question-5c56a2b3-f057-42a0-9b2c-52a35713cd8c", + "siblingIndex": 0, + }, + ], + "content": "π", + "id": "question-5c56a2b3-f057-42a0-9b2c-52a35713cd8c", + "isAnswer": false, + "parentMessageId": "46a49bb9-0881-459e-8c6a-24d20ae48d2f", + }, + ], + "content": "My number is 145.", + "id": "46a49bb9-0881-459e-8c6a-24d20ae48d2f", + "isAnswer": true, + "parentMessageId": "question-46a49bb9-0881-459e-8c6a-24d20ae48d2f", + "siblingIndex": 0, + }, + ], + "content": "78", + "id": "question-46a49bb9-0881-459e-8c6a-24d20ae48d2f", + "isAnswer": false, + "parentMessageId": "3cded945-855a-4a24-aab7-43c7dd54664c", + }, + ], + "content": "My number is 7.89.", + "id": "3cded945-855a-4a24-aab7-43c7dd54664c", + "isAnswer": true, + "parentMessageId": "question-3cded945-855a-4a24-aab7-43c7dd54664c", + "siblingIndex": 0, + }, + ], + "content": "3.11", + "id": "question-3cded945-855a-4a24-aab7-43c7dd54664c", + "isAnswer": false, + "parentMessageId": "a956de3d-ef95-4d90-84fe-f7a26ef28cd7", + }, + ], + "content": "My number is 22.", + "id": "a956de3d-ef95-4d90-84fe-f7a26ef28cd7", + "isAnswer": true, + "parentMessageId": "question-a956de3d-ef95-4d90-84fe-f7a26ef28cd7", + "siblingIndex": 0, + }, + ], + "content": "-5", + "id": "question-a956de3d-ef95-4d90-84fe-f7a26ef28cd7", + "isAnswer": false, + "parentMessageId": "93bac05d-1470-4ac9-b090-fe21cd7c3d55", + }, + ], + "content": "My number is 4782.", + "id": "93bac05d-1470-4ac9-b090-fe21cd7c3d55", + "isAnswer": true, + "parentMessageId": "question-93bac05d-1470-4ac9-b090-fe21cd7c3d55", + "siblingIndex": 0, + }, + ], + "content": "3306", + "id": "question-93bac05d-1470-4ac9-b090-fe21cd7c3d55", + "isAnswer": false, + "parentMessageId": "9e51a13b-7780-4565-98dc-f2d8c3b1758f", + }, + ], + "content": "My number is 2048.", + "id": "9e51a13b-7780-4565-98dc-f2d8c3b1758f", + "isAnswer": true, + "parentMessageId": "question-9e51a13b-7780-4565-98dc-f2d8c3b1758f", + "siblingIndex": 0, + }, + ], + "content": "1024", + "id": "question-9e51a13b-7780-4565-98dc-f2d8c3b1758f", + "isAnswer": false, + "parentMessageId": "507f9df9-1f06-4a57-bb38-f00228c42c22", + }, + ], + "content": "My number is 259.", + "id": "507f9df9-1f06-4a57-bb38-f00228c42c22", + "isAnswer": true, + "parentMessageId": "question-507f9df9-1f06-4a57-bb38-f00228c42c22", + "siblingIndex": 2, + }, + ], + "content": "123", + "id": "question-507f9df9-1f06-4a57-bb38-f00228c42c22", + "isAnswer": false, + "parentMessageId": "57c989f9-3fa4-4dec-9ee5-c3568dd27418", + }, +] +`; + exports[`build chat item tree and get thread messages should work with real world messages 1`] = ` [ { diff --git a/web/app/components/base/chat/__tests__/partialMessages.json b/web/app/components/base/chat/__tests__/partialMessages.json new file mode 100644 index 0000000000..916c6ad254 --- /dev/null +++ b/web/app/components/base/chat/__tests__/partialMessages.json @@ -0,0 +1,122 @@ +[ + { + "id": "question-ebb73fe2-15de-46dd-aab5-75416d8448eb", + "content": "123", + "isAnswer": false, + "parentMessageId": "57c989f9-3fa4-4dec-9ee5-c3568dd27418" + }, + { + "id": "ebb73fe2-15de-46dd-aab5-75416d8448eb", + "content": "237.", + "isAnswer": true, + "parentMessageId": "question-ebb73fe2-15de-46dd-aab5-75416d8448eb" + }, + { + "id": "question-3553d508-3850-462e-8594-078539f940f9", + "content": "123", + "isAnswer": false, + "parentMessageId": "57c989f9-3fa4-4dec-9ee5-c3568dd27418" + }, + { + "id": "3553d508-3850-462e-8594-078539f940f9", + "content": "My number is 256.", + "isAnswer": true, + "parentMessageId": "question-3553d508-3850-462e-8594-078539f940f9" + }, + { + "id": "question-507f9df9-1f06-4a57-bb38-f00228c42c22", + "content": "123", + "isAnswer": false, + "parentMessageId": "57c989f9-3fa4-4dec-9ee5-c3568dd27418" + }, + { + "id": "507f9df9-1f06-4a57-bb38-f00228c42c22", + "content": "My number is 259.", + "isAnswer": true, + "parentMessageId": "question-507f9df9-1f06-4a57-bb38-f00228c42c22" + }, + { + "id": "question-9e51a13b-7780-4565-98dc-f2d8c3b1758f", + "content": "1024", + "isAnswer": false, + "parentMessageId": "507f9df9-1f06-4a57-bb38-f00228c42c22" + }, + { + "id": "9e51a13b-7780-4565-98dc-f2d8c3b1758f", + "content": "My number is 2048.", + "isAnswer": true, + "parentMessageId": "question-9e51a13b-7780-4565-98dc-f2d8c3b1758f" + }, + { + "id": "question-93bac05d-1470-4ac9-b090-fe21cd7c3d55", + "content": "3306", + "isAnswer": false, + "parentMessageId": "9e51a13b-7780-4565-98dc-f2d8c3b1758f" + }, + { + "id": "93bac05d-1470-4ac9-b090-fe21cd7c3d55", + "content": "My number is 4782.", + "isAnswer": true, + "parentMessageId": "question-93bac05d-1470-4ac9-b090-fe21cd7c3d55" + }, + { + "id": "question-a956de3d-ef95-4d90-84fe-f7a26ef28cd7", + "content": "-5", + "isAnswer": false, + "parentMessageId": "93bac05d-1470-4ac9-b090-fe21cd7c3d55" + }, + { + "id": "a956de3d-ef95-4d90-84fe-f7a26ef28cd7", + "content": "My number is 22.", + "isAnswer": true, + "parentMessageId": "question-a956de3d-ef95-4d90-84fe-f7a26ef28cd7" + }, + { + "id": "question-3cded945-855a-4a24-aab7-43c7dd54664c", + "content": "3.11", + "isAnswer": false, + "parentMessageId": "a956de3d-ef95-4d90-84fe-f7a26ef28cd7" + }, + { + "id": "3cded945-855a-4a24-aab7-43c7dd54664c", + "content": "My number is 7.89.", + "isAnswer": true, + "parentMessageId": "question-3cded945-855a-4a24-aab7-43c7dd54664c" + }, + { + "id": "question-46a49bb9-0881-459e-8c6a-24d20ae48d2f", + "content": "78", + "isAnswer": false, + "parentMessageId": "3cded945-855a-4a24-aab7-43c7dd54664c" + }, + { + "id": "46a49bb9-0881-459e-8c6a-24d20ae48d2f", + "content": "My number is 145.", + "isAnswer": true, + "parentMessageId": "question-46a49bb9-0881-459e-8c6a-24d20ae48d2f" + }, + { + "id": "question-5c56a2b3-f057-42a0-9b2c-52a35713cd8c", + "content": "π", + "isAnswer": false, + "parentMessageId": "46a49bb9-0881-459e-8c6a-24d20ae48d2f" + }, + { + "id": "5c56a2b3-f057-42a0-9b2c-52a35713cd8c", + "content": "My number is 2π (approximately 6.28).", + "isAnswer": true, + "parentMessageId": "question-5c56a2b3-f057-42a0-9b2c-52a35713cd8c" + }, + { + "id": "question-9eac3bcc-8d3b-4e56-a12b-44c34cebc719", + "content": "e", + "isAnswer": false, + "parentMessageId": "5c56a2b3-f057-42a0-9b2c-52a35713cd8c" + }, + { + "id": "9eac3bcc-8d3b-4e56-a12b-44c34cebc719", + "content": "My number is 3e (approximately 8.15).", + "isAnswer": true, + "parentMessageId": "question-9eac3bcc-8d3b-4e56-a12b-44c34cebc719" + } +] diff --git a/web/app/components/base/chat/__tests__/utils.spec.ts b/web/app/components/base/chat/__tests__/utils.spec.ts index 1dead1c949..0bff8a77a1 100644 --- a/web/app/components/base/chat/__tests__/utils.spec.ts +++ b/web/app/components/base/chat/__tests__/utils.spec.ts @@ -7,6 +7,7 @@ import mixedTestMessages from './mixedTestMessages.json' import multiRootNodesMessages from './multiRootNodesMessages.json' import multiRootNodesWithLegacyTestMessages from './multiRootNodesWithLegacyTestMessages.json' import realWorldMessages from './realWorldMessages.json' +import partialMessages from './partialMessages.json' function visitNode(tree: ChatItemInTree | ChatItemInTree[], path: string): ChatItemInTree { return get(tree, path) @@ -256,9 +257,15 @@ describe('build chat item tree and get thread messages', () => { expect(threadMessages6_2).toMatchSnapshot() }) - const partialMessages = (realWorldMessages as ChatItemInTree[]).slice(-10) - const tree7 = buildChatItemTree(partialMessages) - it('should work with partial messages', () => { + const partialMessages1 = (realWorldMessages as ChatItemInTree[]).slice(-10) + const tree7 = buildChatItemTree(partialMessages1) + it('should work with partial messages 1', () => { expect(tree7).toMatchSnapshot() }) + + const partialMessages2 = (partialMessages as ChatItemInTree[]) + const tree8 = buildChatItemTree(partialMessages2) + it('should work with partial messages 2', () => { + expect(tree8).toMatchSnapshot() + }) }) diff --git a/web/app/components/base/chat/utils.ts b/web/app/components/base/chat/utils.ts index 61dfaecffc..326805c930 100644 --- a/web/app/components/base/chat/utils.ts +++ b/web/app/components/base/chat/utils.ts @@ -127,19 +127,16 @@ function buildChatItemTree(allMessages: IChatItem[]): ChatItemInTree[] { lastAppendedLegacyAnswer = answerNode } else { - if (!parentMessageId) + if ( + !parentMessageId + || !allMessages.some(item => item.id === parentMessageId) // parent message might not be fetched yet, in this case we will append the question to the root nodes + ) rootNodes.push(questionNode) else map[parentMessageId]?.children!.push(questionNode) } } - // If no messages have parentMessageId=null (indicating a root node), - // then we likely have a partial chat history. In this case, - // use the first available message as the root node. - if (rootNodes.length === 0 && allMessages.length > 0) - rootNodes.push(map[allMessages[0]!.id]!) - return rootNodes } From 83b6abf4ad0197c50310ef58415fec6115788ab4 Mon Sep 17 00:00:00 2001 From: Pedro Gomes <113145167+PedroGomes02@users.noreply.github.com> Date: Thu, 21 Nov 2024 06:14:07 +0000 Subject: [PATCH 025/103] Update parse.py to handle empty list result (#10915) Co-authored-by: crazywoola <427733928@qq.com> --- api/core/tools/provider/builtin/json_process/tools/parse.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/core/tools/provider/builtin/json_process/tools/parse.py b/api/core/tools/provider/builtin/json_process/tools/parse.py index 37cae40153..f91432ee77 100644 --- a/api/core/tools/provider/builtin/json_process/tools/parse.py +++ b/api/core/tools/provider/builtin/json_process/tools/parse.py @@ -40,6 +40,9 @@ class JSONParseTool(BuiltinTool): expr = parse(json_filter) result = [match.value for match in expr.find(input_data)] + if not result: + return "" + if len(result) == 1: result = result[0] From 80da0c58307d373ae72d4b9825f8222982fbb648 Mon Sep 17 00:00:00 2001 From: yihong Date: Thu, 21 Nov 2024 16:36:05 +0800 Subject: [PATCH 026/103] fix: default max_chunks set to 1 as other providers (#10937) Signed-off-by: yihong0618 --- .../model_providers/volcengine_maas/text_embedding/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/model_runtime/model_providers/volcengine_maas/text_embedding/models.py b/api/core/model_runtime/model_providers/volcengine_maas/text_embedding/models.py index ce4f0c3ab1..4a6f5b6f7b 100644 --- a/api/core/model_runtime/model_providers/volcengine_maas/text_embedding/models.py +++ b/api/core/model_runtime/model_providers/volcengine_maas/text_embedding/models.py @@ -22,7 +22,7 @@ def get_model_config(credentials: dict) -> ModelConfig: return ModelConfig( properties=ModelProperties( context_size=int(credentials.get("context_size", 0)), - max_chunks=int(credentials.get("max_chunks", 0)), + max_chunks=int(credentials.get("max_chunks", 1)), ) ) return model_configs From 82575a7aea904c0c56f8103fe0ceea39e97cf0f4 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Thu, 21 Nov 2024 16:42:48 +0800 Subject: [PATCH 027/103] fix(gpt-4o-audio-preview): Remove the vision feature (#10932) --- .../model_providers/openai/llm/gpt-4o-audio-preview.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/api/core/model_runtime/model_providers/openai/llm/gpt-4o-audio-preview.yaml b/api/core/model_runtime/model_providers/openai/llm/gpt-4o-audio-preview.yaml index 256e87edbe..e07dea2ee1 100644 --- a/api/core/model_runtime/model_providers/openai/llm/gpt-4o-audio-preview.yaml +++ b/api/core/model_runtime/model_providers/openai/llm/gpt-4o-audio-preview.yaml @@ -7,7 +7,6 @@ features: - multi-tool-call - agent-thought - stream-tool-call - - vision model_properties: mode: chat context_size: 128000 From cb0c55daa7bf81081d95d9adb7ada07ca6cf437e Mon Sep 17 00:00:00 2001 From: AkisAya Date: Thu, 21 Nov 2024 17:53:20 +0800 Subject: [PATCH 028/103] fix weight rerank of knowledge retrieval (#10931) --- api/core/rag/rerank/rerank_model.py | 6 +++--- api/core/rag/rerank/weight_rerank.py | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/api/core/rag/rerank/rerank_model.py b/api/core/rag/rerank/rerank_model.py index fc82b2080b..6ae432a526 100644 --- a/api/core/rag/rerank/rerank_model.py +++ b/api/core/rag/rerank/rerank_model.py @@ -27,11 +27,11 @@ class RerankModelRunner(BaseRerankRunner): :return: """ docs = [] - doc_id = set() + doc_ids = set() unique_documents = [] for document in documents: - if document.provider == "dify" and document.metadata["doc_id"] not in doc_id: - doc_id.add(document.metadata["doc_id"]) + if document.provider == "dify" and document.metadata["doc_id"] not in doc_ids: + doc_ids.add(document.metadata["doc_id"]) docs.append(document.page_content) unique_documents.append(document) elif document.provider == "external": diff --git a/api/core/rag/rerank/weight_rerank.py b/api/core/rag/rerank/weight_rerank.py index b706f29bb1..4719be012f 100644 --- a/api/core/rag/rerank/weight_rerank.py +++ b/api/core/rag/rerank/weight_rerank.py @@ -37,11 +37,10 @@ class WeightRerankRunner(BaseRerankRunner): :return: """ unique_documents = [] - doc_id = set() + doc_ids = set() for document in documents: - doc_id = document.metadata.get("doc_id") - if doc_id not in doc_id: - doc_id.add(doc_id) + if document.metadata["doc_id"] not in doc_ids: + doc_ids.add(document.metadata["doc_id"]) unique_documents.append(document) documents = unique_documents From 01014a6a8481add41deb1312c4e5f50bdab5b0e6 Mon Sep 17 00:00:00 2001 From: "cooper.wu" Date: Thu, 21 Nov 2024 18:01:47 +0800 Subject: [PATCH 029/103] fix: external dataset missing score_threshold_enabled (#10943) --- api/fields/dataset_fields.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/fields/dataset_fields.py b/api/fields/dataset_fields.py index b32423f10c..533e3a0837 100644 --- a/api/fields/dataset_fields.py +++ b/api/fields/dataset_fields.py @@ -41,6 +41,7 @@ dataset_retrieval_model_fields = { external_retrieval_model_fields = { "top_k": fields.Integer, "score_threshold": fields.Float, + "score_threshold_enabled": fields.Boolean, } tag_fields = {"id": fields.String, "name": fields.String, "type": fields.String} From 1a6b961b5f63896c230e8c096f4713e595765a90 Mon Sep 17 00:00:00 2001 From: LastHopeOfGPNU <28721054@qq.com> Date: Thu, 21 Nov 2024 18:03:49 +0800 Subject: [PATCH 030/103] Resolve 8475 support rerank model from infinity (#10939) Co-authored-by: linyanxu --- .../openai_api_compatible/rerank/rerank.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/api/core/model_runtime/model_providers/openai_api_compatible/rerank/rerank.py b/api/core/model_runtime/model_providers/openai_api_compatible/rerank/rerank.py index 508da4bf20..407dc7190e 100644 --- a/api/core/model_runtime/model_providers/openai_api_compatible/rerank/rerank.py +++ b/api/core/model_runtime/model_providers/openai_api_compatible/rerank/rerank.py @@ -64,7 +64,7 @@ class OAICompatRerankModel(RerankModel): # TODO: Do we need truncate docs to avoid llama.cpp return error? - data = {"model": model_name, "query": query, "documents": docs, "top_n": top_n} + data = {"model": model_name, "query": query, "documents": docs, "top_n": top_n, "return_documents": True} try: response = post(str(URL(url) / "rerank"), headers=headers, data=dumps(data), timeout=60) @@ -83,7 +83,13 @@ class OAICompatRerankModel(RerankModel): index = result["index"] # Retrieve document text (fallback if llama.cpp rerank doesn't return it) - text = result.get("document", {}).get("text", docs[index]) + text = docs[index] + document = result.get("document", {}) + if document: + if isinstance(document, dict): + text = document.get("text", docs[index]) + elif isinstance(document, str): + text = document # Normalize the score normalized_score = (result["relevance_score"] - min_score) / score_range From 8c2f62fb92b591ac16e9203196e3717d576b0163 Mon Sep 17 00:00:00 2001 From: Xu Song Date: Thu, 21 Nov 2024 18:32:54 +0800 Subject: [PATCH 031/103] Feat: support json output for bing-search (#10904) --- .../builtin/bing/tools/bing_web_search.py | 35 +++++++++++++++++++ .../builtin/bing/tools/bing_web_search.yaml | 11 ++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/api/core/tools/provider/builtin/bing/tools/bing_web_search.py b/api/core/tools/provider/builtin/bing/tools/bing_web_search.py index 8bed2c556c..1afe2f8385 100644 --- a/api/core/tools/provider/builtin/bing/tools/bing_web_search.py +++ b/api/core/tools/provider/builtin/bing/tools/bing_web_search.py @@ -66,6 +66,41 @@ class BingSearchTool(BuiltinTool): results.append(self.create_text_message(text=f'{related.get("displayText", "")}{url}')) return results + elif result_type == "json": + result = {} + if search_results: + result["organic"] = [ + { + "title": item.get("name", ""), + "snippet": item.get("snippet", ""), + "url": item.get("url", ""), + "siteName": item.get("siteName", ""), + } + for item in search_results + ] + + if computation and "expression" in computation and "value" in computation: + result["computation"] = {"expression": computation["expression"], "value": computation["value"]} + + if entities: + result["entities"] = [ + { + "name": item.get("name", ""), + "url": item.get("url", ""), + "description": item.get("description", ""), + } + for item in entities + ] + + if news: + result["news"] = [{"name": item.get("name", ""), "url": item.get("url", "")} for item in news] + + if related_searches: + result["related searches"] = [ + {"displayText": item.get("displayText", ""), "url": item.get("webSearchUrl", "")} for item in news + ] + + return self.create_json_message(result) else: # construct text text = "" diff --git a/api/core/tools/provider/builtin/bing/tools/bing_web_search.yaml b/api/core/tools/provider/builtin/bing/tools/bing_web_search.yaml index a3f60bb09b..f5c932c37b 100644 --- a/api/core/tools/provider/builtin/bing/tools/bing_web_search.yaml +++ b/api/core/tools/provider/builtin/bing/tools/bing_web_search.yaml @@ -113,9 +113,9 @@ parameters: zh_Hans: 结果类型 pt_BR: result type human_description: - en_US: return a list of links or texts - zh_Hans: 返回一个连接列表还是纯文本内容 - pt_BR: return a list of links or texts + en_US: return a list of links, json or texts + zh_Hans: 返回一个列表,内容是链接、json还是纯文本 + pt_BR: return a list of links, json or texts default: text options: - value: link @@ -123,6 +123,11 @@ parameters: en_US: Link zh_Hans: 链接 pt_BR: Link + - value: json + label: + en_US: JSON + zh_Hans: JSON + pt_BR: JSON - value: text label: en_US: Text From fefda40acfd5b82fdf0f3449e9e0a6e89c2aaace Mon Sep 17 00:00:00 2001 From: marvin-season <64943287+marvin-season@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:07:02 +0800 Subject: [PATCH 032/103] fix: fix bugs of frontend-workflow panel operator (#10945) Co-authored-by: marvin --- .../_base/components/panel-operator/panel-operator-popup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup.tsx b/web/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup.tsx index bd642fcd66..cd44d15606 100644 --- a/web/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup.tsx +++ b/web/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup.tsx @@ -128,7 +128,7 @@ const PanelOperatorPopup = ({ className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50' onClick={() => { onClosePopup() - handleNodesCopy() + handleNodesCopy(id) }} > {t('workflow.common.copy')} From 8b16f07eb0df69d42a5eed47ebcf1e9ef982a881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Thu, 21 Nov 2024 22:25:18 +0800 Subject: [PATCH 033/103] feat: add cURL import for http request node (#8656) --- .../nodes/http/components/curl-panel.tsx | 154 ++++++++++++++++++ .../components/workflow/nodes/http/panel.tsx | 43 ++++- .../workflow/nodes/http/use-config.ts | 22 +++ web/i18n/en-US/workflow.ts | 4 + web/i18n/zh-Hans/workflow.ts | 4 + 5 files changed, 218 insertions(+), 9 deletions(-) create mode 100644 web/app/components/workflow/nodes/http/components/curl-panel.tsx diff --git a/web/app/components/workflow/nodes/http/components/curl-panel.tsx b/web/app/components/workflow/nodes/http/components/curl-panel.tsx new file mode 100644 index 0000000000..9c5dddedb7 --- /dev/null +++ b/web/app/components/workflow/nodes/http/components/curl-panel.tsx @@ -0,0 +1,154 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { BodyType, type HttpNodeType, Method } from '../types' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import Toast from '@/app/components/base/toast' +import { useNodesInteractions } from '@/app/components/workflow/hooks' + +type Props = { + nodeId: string + isShow: boolean + onHide: () => void + handleCurlImport: (node: HttpNodeType) => void +} + +const parseCurl = (curlCommand: string): { node: HttpNodeType | null; error: string | null } => { + if (!curlCommand.trim().toLowerCase().startsWith('curl')) + return { node: null, error: 'Invalid cURL command. Command must start with "curl".' } + + const node: Partial = { + title: 'HTTP Request', + desc: 'Imported from cURL', + method: Method.get, + url: '', + headers: '', + params: '', + body: { type: BodyType.none, data: '' }, + } + const args = curlCommand.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [] + + for (let i = 1; i < args.length; i++) { + const arg = args[i].replace(/^['"]|['"]$/g, '') + switch (arg) { + case '-X': + case '--request': + if (i + 1 >= args.length) + return { node: null, error: 'Missing HTTP method after -X or --request.' } + node.method = (args[++i].replace(/^['"]|['"]$/g, '') as Method) || Method.get + break + case '-H': + case '--header': + if (i + 1 >= args.length) + return { node: null, error: 'Missing header value after -H or --header.' } + node.headers += (node.headers ? '\n' : '') + args[++i].replace(/^['"]|['"]$/g, '') + break + case '-d': + case '--data': + case '--data-raw': + case '--data-binary': + if (i + 1 >= args.length) + return { node: null, error: 'Missing data value after -d, --data, --data-raw, or --data-binary.' } + node.body = { type: BodyType.rawText, data: args[++i].replace(/^['"]|['"]$/g, '') } + break + case '-F': + case '--form': { + if (i + 1 >= args.length) + return { node: null, error: 'Missing form data after -F or --form.' } + if (node.body?.type !== BodyType.formData) + node.body = { type: BodyType.formData, data: '' } + const formData = args[++i].replace(/^['"]|['"]$/g, '') + const [key, ...valueParts] = formData.split('=') + if (!key) + return { node: null, error: 'Invalid form data format.' } + let value = valueParts.join('=') + + // To support command like `curl -F "file=@/path/to/file;type=application/zip"` + // the `;type=application/zip` should translate to `Content-Type: application/zip` + const typeMatch = value.match(/^(.+?);type=(.+)$/) + if (typeMatch) { + const [, actualValue, mimeType] = typeMatch + value = actualValue + node.headers += `${node.headers ? '\n' : ''}Content-Type: ${mimeType}` + } + + node.body.data += `${node.body.data ? '\n' : ''}${key}:${value}` + break + } + case '--json': + if (i + 1 >= args.length) + return { node: null, error: 'Missing JSON data after --json.' } + node.body = { type: BodyType.json, data: args[++i].replace(/^['"]|['"]$/g, '') } + break + default: + if (arg.startsWith('http') && !node.url) + node.url = arg + break + } + } + + if (!node.url) + return { node: null, error: 'Missing URL or url not start with http.' } + + // Extract query params from URL + const urlParts = node.url?.split('?') || [] + if (urlParts.length > 1) { + node.url = urlParts[0] + node.params = urlParts[1].replace(/&/g, '\n').replace(/=/g, ': ') + } + + return { node: node as HttpNodeType, error: null } +} + +const CurlPanel: FC = ({ nodeId, isShow, onHide, handleCurlImport }) => { + const [inputString, setInputString] = useState('') + const { handleNodeSelect } = useNodesInteractions() + const { t } = useTranslation() + + const handleSave = useCallback(() => { + const { node, error } = parseCurl(inputString) + if (error) { + Toast.notify({ + type: 'error', + message: error, + }) + return + } + if (!node) + return + + onHide() + handleCurlImport(node) + // Close the panel then open it again to make the panel re-render + handleNodeSelect(nodeId, true) + setTimeout(() => { + handleNodeSelect(nodeId) + }, 0) + }, [onHide, nodeId, inputString, handleNodeSelect, handleCurlImport]) + + return ( + +
+